Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ auto-generated per-PR notes; this file is the curated, human-readable history.
never executes queries.

### Fixed
- **Open combobox popovers stay anchored while their containing strip scrolls**
(#296). The shared fixed-position listbox now listens for capture-phase
document scroll events only while open, so horizontal scrolling in either the
Workbench variable strip or Dashboard filter strip updates its anchor instead
of leaving the dropdown behind over unrelated fields.
- **The Workbench favorite star now drives Dashboard tile membership** (part of
#299, post-#287 aggregate). Since `dashboard.tiles[]` became the canonical
Dashboard membership (#280/#287), starring a query only flipped `spec.favorite`
Expand Down
11 changes: 9 additions & 2 deletions src/ui/combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@
// under the input via `fixedAnchor` (dom.js) at open time — the same trick
// the File/user menus already use to escape an ancestor's `overflow`
// clipping (the var-strip / dashboard filter bar both scroll horizontally).
// Repositioning only happens on open/refresh, not continuously on scroll —
// a known, minor v1 limitation (documented, not silently ignored).
// While open, the listbox repositions on capture-phase document scroll events.
// `scroll` does not bubble, so capture is what sees horizontal strip scrolling
// as well as ordinary document scrolling.

import { fixedAnchor } from './dom.js';

Expand Down Expand Up @@ -154,6 +155,10 @@ export function createCombobox(
listEl.style.minWidth = rect.width + 'px';
}

function repositionOnScroll(): void {
if (open) position();
}

function render(): void {
const ids: string[] = [];
const children: HTMLElement[] = [];
Expand Down Expand Up @@ -193,6 +198,7 @@ export function createCombobox(
options = getOptions(input.value) || [];
position();
render();
d.addEventListener('scroll', repositionOnScroll, true);
}

function closeList(): void {
Expand All @@ -202,6 +208,7 @@ export function createCombobox(
input.setAttribute('aria-expanded', 'false');
activeIndex = -1;
input.removeAttribute('aria-activedescendant');
d.removeEventListener('scroll', repositionOnScroll, true);
// Review (phase-7 user feedback): this is the ONE place every close path
// funnels through — blur, Escape, Enter (with or without an active
// option), AND an option's mousedown-commit (which closes the list
Expand Down
19 changes: 19 additions & 0 deletions tests/unit/combobox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,25 @@ describe('createCombobox — open/close + ARIA', () => {
combo.onBlur();
expect(combo.isOpen()).toBe(false);
});
it('repositions its fixed listbox on an ancestor scroll while open, then detaches on close', () => {
const { input, listEl, combo } = build();
const scroller = document.createElement('div');
document.body.append(scroller);
scroller.append(input);
let left = 20;
input.getBoundingClientRect = () => ({ bottom: 30, left, width: 100 } as DOMRect);

combo.onFocus();
expect(listEl.style.left).toBe('20px');
left = 60;
scroller.dispatchEvent(new Event('scroll'));
expect(listEl.style.left).toBe('60px');

combo.close();
left = 90;
scroller.dispatchEvent(new Event('scroll'));
expect(listEl.style.left).toBe('60px');
});
it('the live region announces the option count, or "No matches"', () => {
const { input, liveEl, combo } = build();
combo.onFocus();
Expand Down
Loading