diff --git a/.changeset/olive-sheep-run.md b/.changeset/olive-sheep-run.md new file mode 100644 index 000000000..b36d18b88 --- /dev/null +++ b/.changeset/olive-sheep-run.md @@ -0,0 +1,5 @@ +--- +'@qwik-ui/headless': patch +--- + +refactor: improved select focus navigation diff --git a/apps/website/src/routes/docs/headless/select/snippets/select.css b/apps/website/src/routes/docs/headless/select/snippets/select.css index 582740d8a..cadcd66d9 100644 --- a/apps/website/src/routes/docs/headless/select/snippets/select.css +++ b/apps/website/src/routes/docs/headless/select/snippets/select.css @@ -17,12 +17,18 @@ display: flex; justify-content: center; align-items: center; + margin-top: 0.25rem; } .select-trigger:hover { background-color: hsla(var(--primary) / 0.08); } +.select-trigger:focus-visible { + outline: 2px solid hsla(var(--primary) / 1); + outline-offset: 2px; +} + .select-popover { width: 100%; max-width: var(--select-width); diff --git a/cla-signs/v1/cla.json b/cla-signs/v1/cla.json index d3e66f98c..e4feac877 100644 --- a/cla-signs/v1/cla.json +++ b/cla-signs/v1/cla.json @@ -479,14 +479,6 @@ "created_at": "2024-05-31T08:39:43Z", "repoId": 545159943, "pullRequestNo": 810 - }, - { - "name": "harshmangalam", - "id": 57381638, - "comment_id": 2145776407, - "created_at": "2024-06-03T17:40:03Z", - "repoId": 545159943, - "pullRequestNo": 820 } ] } \ No newline at end of file diff --git a/packages/kit-headless/src/components/select/select-context.ts b/packages/kit-headless/src/components/select/select-context.ts index 2e79fdd43..3893d7f73 100644 --- a/packages/kit-headless/src/components/select/select-context.ts +++ b/packages/kit-headless/src/components/select/select-context.ts @@ -14,6 +14,7 @@ export type SelectContext = { listboxRef: Signal; groupRef: Signal; labelRef: Signal; + highlightedItemRef: Signal; // core state itemsMapSig: Readonly>; diff --git a/packages/kit-headless/src/components/select/select-item.tsx b/packages/kit-headless/src/components/select/select-item.tsx index 5889da954..f61449337 100644 --- a/packages/kit-headless/src/components/select/select-item.tsx +++ b/packages/kit-headless/src/components/select/select-item.tsx @@ -8,6 +8,9 @@ import { useTask$, type PropsOf, useContextProvider, + sync$, + useOnWindow, + QRL, } from '@builder.io/qwik'; import { isServer, isBrowser } from '@builder.io/qwik/build'; import SelectContextId, { @@ -34,8 +37,18 @@ export const HSelectItem = component$((props) => { const itemRef = useSignal(); const localIndexSig = useSignal(null); const itemId = `${context.localId}-${_index}`; + const typeaheadFnSig = useSignal Promise>>(); - const { selectionManager$ } = useSelect(); + const { selectionManager$, getNextEnabledItemIndex$, getPrevEnabledItemIndex$ } = + useSelect(); + + // we're getting the same function instance from the trigger, without needing to restructure context + useOnWindow( + 'typeaheadFn', + $((e: CustomEvent) => { + typeaheadFnSig.value = e.detail; + }), + ); const isSelectedSig = useComputed$(() => { const index = _index ?? null; @@ -43,19 +56,31 @@ export const HSelectItem = component$((props) => { }); const isHighlightedSig = useComputed$(() => { - return !disabled && context.highlightedIndexSig.value === _index; + if (disabled) return; + + if (context.highlightedIndexSig.value === _index) { + itemRef.value?.focus(); + return true; + } else { + return false; + } }); - useTask$(function getIndexTask() { + useTask$(async function getIndexTask() { if (_index === undefined) throw Error('Qwik UI: Select component item cannot find its proper index.'); localIndexSig.value = _index; }); - useTask$(function scrollableTask({ track, cleanup }) { + useTask$(async function navigationTask({ track, cleanup }) { track(() => context.highlightedIndexSig.value); + // update the context with the highlighted item ref + if (localIndexSig.value === context.highlightedIndexSig.value) { + context.highlightedItemRef = itemRef; + } + if (isServer) return; let observer: IntersectionObserver; @@ -109,6 +134,101 @@ export const HSelectItem = component$((props) => { isSelectedSig, }; + const handleKeyDownSync$ = sync$((e: KeyboardEvent) => { + const keys = [ + 'ArrowUp', + 'ArrowDown', + 'ArrowRight', + 'ArrowLeft', + 'Home', + 'End', + 'PageDown', + 'PageUp', + 'Enter', + ' ', + ]; + if (keys.includes(e.key)) { + e.preventDefault(); + } + }); + + const handleKeyDown$ = $(async (e: KeyboardEvent) => { + typeaheadFnSig.value?.(e.key); + + switch (e.key) { + case 'ArrowDown': + if (context.isListboxOpenSig.value) { + context.highlightedIndexSig.value = await getNextEnabledItemIndex$( + context.highlightedIndexSig.value!, + ); + if (context.multiple && e.shiftKey) { + await selectionManager$(context.highlightedIndexSig.value, 'toggle'); + } + } + break; + + case 'ArrowUp': + if (context.isListboxOpenSig.value) { + context.highlightedIndexSig.value = await getPrevEnabledItemIndex$( + context.highlightedIndexSig.value!, + ); + if (context.multiple && e.shiftKey) { + await selectionManager$(context.highlightedIndexSig.value, 'toggle'); + } + } + break; + + case 'Home': + if (context.isListboxOpenSig.value) { + context.highlightedIndexSig.value = await getNextEnabledItemIndex$(-1); + } + break; + + case 'End': + if (context.isListboxOpenSig.value) { + const lastEnabledOptionIndex = await getPrevEnabledItemIndex$( + context.itemsMapSig.value.size, + ); + context.highlightedIndexSig.value = lastEnabledOptionIndex; + } + break; + + case 'Escape': + context.triggerRef.value?.focus(); + context.isListboxOpenSig.value = false; + break; + + case 'Tab': + context.isListboxOpenSig.value = false; + break; + + case 'Enter': + case ' ': + if (context.isListboxOpenSig.value) { + const action = context.multiple ? 'toggle' : 'add'; + await selectionManager$(context.highlightedIndexSig.value, action); + + if (!context.multiple) { + context.triggerRef.value?.focus(); + } + } + context.isListboxOpenSig.value = context.multiple + ? true + : !context.isListboxOpenSig.value; + break; + + case 'a': + if (e.ctrlKey && context.multiple) { + for (const [index, item] of context.itemsMapSig.value) { + if (!item.disabled) { + await selectionManager$(index, 'add'); + } + } + } + break; + } + }); + useContextProvider(selectItemContextId, selectContext); return ( @@ -116,6 +236,7 @@ export const HSelectItem = component$((props) => { {...rest} id={itemId} onClick$={[handleClick$, props.onClick$]} + onKeyDown$={[handleKeyDownSync$, handleKeyDown$, props.onKeyDown$]} onPointerOver$={[handlePointerOver$, props.onPointerOver$]} ref={itemRef} tabIndex={-1} diff --git a/packages/kit-headless/src/components/select/select-root.tsx b/packages/kit-headless/src/components/select/select-root.tsx index cd8d43efb..ffadae429 100644 --- a/packages/kit-headless/src/components/select/select-root.tsx +++ b/packages/kit-headless/src/components/select/select-root.tsx @@ -161,6 +161,7 @@ export const HSelectImpl = component$ & InternalSelectProps const currDisplayValueSig = useSignal(); const initialLoadSig = useSignal(true); + const highlightedItemRef = useSignal(); const context: SelectContext = { itemsMapSig, @@ -170,6 +171,7 @@ export const HSelectImpl = component$ & InternalSelectProps listboxRef, labelRef, groupRef, + highlightedItemRef, localId, highlightedIndexSig, selectedIndexSetSig, diff --git a/packages/kit-headless/src/components/select/select-trigger.tsx b/packages/kit-headless/src/components/select/select-trigger.tsx index b4a870a6d..294dce161 100644 --- a/packages/kit-headless/src/components/select/select-trigger.tsx +++ b/packages/kit-headless/src/components/select/select-trigger.tsx @@ -1,4 +1,12 @@ -import { $, Slot, component$, sync$, useContext, type PropsOf } from '@builder.io/qwik'; +import { + $, + Slot, + component$, + sync$, + useContext, + useSignal, + type PropsOf, +} from '@builder.io/qwik'; import SelectContextId from './select-context'; import { useSelect, useTypeahead } from './use-select'; @@ -9,7 +17,7 @@ export const HSelectTrigger = component$((props) => { useSelect(); const labelId = `${context.localId}-label`; const descriptionId = `${context.localId}-description`; - + const initialKeyDownSig = useSignal(true); const { typeahead$ } = useTypeahead(); const handleClickSync$ = sync$((e: MouseEvent) => { @@ -42,66 +50,23 @@ export const HSelectTrigger = component$((props) => { const handleKeyDown$ = $(async (e: KeyboardEvent) => { if (!context.itemsMapSig.value) return; - typeahead$(e.key); + if (!context.isListboxOpenSig.value) { + typeahead$(e.key); + } switch (e.key) { - case 'Enter': - case ' ': - if (context.isListboxOpenSig.value) { - const action = context.multiple ? 'toggle' : 'add'; - await selectionManager$(context.highlightedIndexSig.value, action); - } - context.isListboxOpenSig.value = context.multiple - ? true - : !context.isListboxOpenSig.value; + case 'Tab': + case 'Escape': + context.isListboxOpenSig.value = false; break; case 'ArrowDown': - if (context.isListboxOpenSig.value) { - context.highlightedIndexSig.value = await getNextEnabledItemIndex$( - context.highlightedIndexSig.value!, - ); - if (context.multiple && e.shiftKey) { - await selectionManager$(context.highlightedIndexSig.value, 'toggle'); - } - } else { - context.isListboxOpenSig.value = true; - } - break; - case 'ArrowUp': - if (context.isListboxOpenSig.value) { - context.highlightedIndexSig.value = await getPrevEnabledItemIndex$( - context.highlightedIndexSig.value!, - ); - if (context.multiple && e.shiftKey) { - await selectionManager$(context.highlightedIndexSig.value, 'toggle'); - } - } else { + if (!context.isListboxOpenSig.value) { context.isListboxOpenSig.value = true; } break; - case 'Home': - if (context.isListboxOpenSig.value) { - context.highlightedIndexSig.value = await getNextEnabledItemIndex$(-1); - } - break; - - case 'End': - if (context.isListboxOpenSig.value) { - const lastEnabledOptionIndex = await getPrevEnabledItemIndex$( - context.itemsMapSig.value.size, - ); - context.highlightedIndexSig.value = lastEnabledOptionIndex; - } - break; - - case 'Tab': - case 'Escape': - context.isListboxOpenSig.value = false; - break; - case 'ArrowRight': if (!context.multiple) { const currentIndex = context.highlightedIndexSig.value ?? -1; @@ -121,22 +86,27 @@ export const HSelectTrigger = component$((props) => { } break; - case 'a': - if (e.ctrlKey && context.multiple) { - for (const [index, item] of context.itemsMapSig.value) { - if (!item.disabled) { - await selectionManager$(index, 'add'); - } - } - } + case 'Enter': + case ' ': + context.isListboxOpenSig.value = context.multiple + ? true + : !context.isListboxOpenSig.value; break; } /** When initially opening the listbox, we want to grab the first enabled option index */ if (context.highlightedIndexSig.value === null) { context.highlightedIndexSig.value = await getNextEnabledItemIndex$(-1); - return; } + + // Wait for the popover code to be executed + while (context.highlightedItemRef.value !== document.activeElement) { + await new Promise((resolve) => setTimeout(resolve, 5)); + context.highlightedItemRef.value?.focus(); + } + + if (!initialKeyDownSig.value) return; + document.dispatchEvent(new CustomEvent('typeaheadFn', { detail: typeahead$ })); }); return ( diff --git a/packages/kit-headless/src/components/select/select.driver.ts b/packages/kit-headless/src/components/select/select.driver.ts index ac5a3f912..99cb1e152 100644 --- a/packages/kit-headless/src/components/select/select.driver.ts +++ b/packages/kit-headless/src/components/select/select.driver.ts @@ -35,6 +35,10 @@ export function createTestDriver(rootLocator: T) { return getTrigger().locator('[data-value]'); }; + const getHighlightedItem = () => { + return getRoot().locator('[data-highlighted]'); + }; + const openListbox = async (key: OpenKeys | 'click') => { await getTrigger().focus(); @@ -59,5 +63,6 @@ export function createTestDriver(rootLocator: T) { getItemAt, getValueElement, openListbox, + getHighlightedItem, }; } diff --git a/packages/kit-headless/src/components/select/select.test.ts b/packages/kit-headless/src/components/select/select.test.ts index aea49f463..5c7850682 100644 --- a/packages/kit-headless/src/components/select/select.test.ts +++ b/packages/kit-headless/src/components/select/select.test.ts @@ -7,17 +7,8 @@ async function setup(page: Page, exampleName: string) { const driver = createTestDriver(page.getByRole('combobox')); - const { getRoot, getListbox, getTrigger, getItemAt, getValueElement, openListbox } = - driver; - return { driver, - getRoot, - getListbox, - getTrigger, - getItemAt, - getValueElement, - openListbox, }; } @@ -101,17 +92,6 @@ test.describe('Mouse Behavior', () => { await expect(d.getValueElement()).toHaveText(expectedValue); }); - // if we want to add focusing the trigger on blur - // test(`GIVEN a basic select - // WHEN the listbox is open and the blur event is triggered - // THEN focus should go back to the trigger`, async ({ page }) => { - // const { getTrigger, openListbox } = await setup(page, 'hero'); - - // await openListbox('click'); - // await getTrigger().blur(); - // await expect(getTrigger()).toBeFocused(); - // }); - test(`GIVEN an open hero select WHEN clicking on the group label THEN the listbox should remain open`, async ({ page }) => { @@ -221,146 +201,113 @@ test.describe('Keyboard Behavior', () => { WHEN pressing the down arrow key THEN the listbox should be opened AND the first option should have data-highlighted`, async ({ page }) => { - const { - getTrigger, - getListbox, - getItemAt: getOptionAt, - } = await setup(page, 'hero'); + const { driver: d } = await setup(page, 'hero'); - await getTrigger().focus(); - await getTrigger().press('ArrowDown'); - await expect(getListbox()).toBeVisible(); + await d.getTrigger().focus(); + await d.getTrigger().press('ArrowDown'); + await expect(d.getListbox()).toBeVisible(); - await expect(getOptionAt(0)).toHaveAttribute('data-highlighted'); + await expect(d.getItemAt(0)).toHaveAttribute('data-highlighted'); }); test(`GIVEN a hero select WHEN pressing the enter key THEN open up the listbox AND the first option should have data-highlighted`, async ({ page }) => { - const { - getTrigger, - getListbox, - getItemAt: getOptionAt, - } = await setup(page, 'hero'); + const { driver: d } = await setup(page, 'hero'); - await getTrigger().focus(); - await getTrigger().press('Enter'); - await expect(getListbox()).toBeVisible(); + await d.getTrigger().focus(); + await d.getTrigger().press('Enter'); + await expect(d.getListbox()).toBeVisible(); - await expect(getOptionAt(0)).toHaveAttribute('data-highlighted'); + await expect(d.getItemAt(0)).toHaveAttribute('data-highlighted'); }); test(`GIVEN a hero select WHEN pressing the space key THEN open up the listbox AND the first option should have data-highlighted`, async ({ page }) => { - const { - getTrigger, - getListbox, - getItemAt: getOptionAt, - } = await setup(page, 'hero'); + const { driver: d } = await setup(page, 'hero'); - await getTrigger().focus(); - await getTrigger().press('Space'); - await expect(getListbox()).toBeVisible(); + await d.getTrigger().focus(); + await d.getTrigger().press('Space'); + await expect(d.getListbox()).toBeVisible(); - await expect(getOptionAt(0)).toHaveAttribute('data-highlighted'); + await expect(d.getItemAt(0)).toHaveAttribute('data-highlighted'); }); test(`GIVEN a hero select WHEN pressing the up arrow THEN open up the listbox AND the first option should have data-highlighted`, async ({ page }) => { - const { - getTrigger, - getListbox, - getItemAt: getOptionAt, - } = await setup(page, 'hero'); + const { driver: d } = await setup(page, 'hero'); - await getTrigger().focus(); - await getTrigger().press('ArrowUp'); - await expect(getListbox()).toBeVisible(); + await d.getTrigger().focus(); + await d.getTrigger().press('ArrowUp'); + await expect(d.getListbox()).toBeVisible(); - await expect(getOptionAt(0)).toHaveAttribute('data-highlighted'); + await expect(d.getItemAt(0)).toHaveAttribute('data-highlighted'); }); test(`GIVEN an open hero select WHEN pressing the end key THEN the last option should have data-highlighted`, async ({ page }) => { - const { - getTrigger, - getItemAt: getOptionAt, - openListbox, - } = await setup(page, 'hero'); + const { driver: d } = await setup(page, 'hero'); - await openListbox('click'); + await d.openListbox('Enter'); - await getTrigger().focus(); - await getTrigger().press('End'); + await expect(d.getItemAt(0)).toHaveAttribute('data-highlighted'); + await d.getHighlightedItem().press('End'); - await expect(getOptionAt('last')).toHaveAttribute('data-highlighted'); + await expect(d.getItemAt('last')).toHaveAttribute('data-highlighted'); }); test(`GIVEN an open hero select WHEN pressing the home key after the end key THEN the first option should have data-highlighted`, async ({ page }) => { - const { - getTrigger, - getItemAt: getOptionAt, - openListbox, - } = await setup(page, 'hero'); + const { driver: d } = await setup(page, 'hero'); - await openListbox('click'); + await d.openListbox('Enter'); // to last index - await getTrigger().focus(); - await getTrigger().press('End'); - - await expect(getOptionAt('last')).toHaveAttribute('data-highlighted'); + await d.getHighlightedItem().press('End'); + await expect(d.getItemAt('last')).toHaveAttribute('data-highlighted'); // to first index - await getTrigger().press('Home'); - await expect(getOptionAt(0)).toHaveAttribute('data-highlighted'); + await d.getHighlightedItem().press('Home'); + await expect(d.getItemAt(0)).toHaveAttribute('data-highlighted'); }); test(`GIVEN an open hero select WHEN the first option is highlighted and the down arrow key is pressed THEN the second option should have data-highlighted`, async ({ page }) => { - const { - getTrigger, - getItemAt: getOptionAt, - openListbox, - } = await setup(page, 'hero'); + const { driver: d } = await setup(page, 'hero'); - await openListbox('Enter'); + await d.openListbox('Enter'); // first index highlighted - await expect(getOptionAt(0)).toHaveAttribute('data-highlighted'); + await expect(d.getHighlightedItem()).toHaveAttribute('data-highlighted'); - await getTrigger().focus(); - await getTrigger().press('ArrowDown'); - await expect(getOptionAt(1)).toHaveAttribute('data-highlighted'); + await d.getHighlightedItem().focus(); + await d.getHighlightedItem().press('ArrowDown'); + await expect(d.getItemAt(1)).toHaveAttribute('data-highlighted'); }); test(`GIVEN an open hero select WHEN the third option is highlighted and the up arrow key is pressed THEN the second option should have data-highlighted`, async ({ page }) => { - const { - getTrigger, - getItemAt: getOptionAt, - openListbox, - } = await setup(page, 'hero'); + const { driver: d } = await setup(page, 'hero'); - await openListbox('Enter'); + await d.openListbox('Enter'); - await expect(getOptionAt(0)).toHaveAttribute('data-highlighted'); - await getTrigger().press('ArrowDown'); - await getTrigger().press('ArrowDown'); + await expect(d.getItemAt(0)).toHaveAttribute('data-highlighted'); + await d.getHighlightedItem().press('ArrowDown'); + await d.getHighlightedItem().press('ArrowDown'); + 1; - await getTrigger().press('ArrowUp'); - await expect(getOptionAt(1)).toHaveAttribute('data-highlighted'); + await d.getHighlightedItem().press('ArrowUp'); + await expect(d.getItemAt(1)).toHaveAttribute('data-highlighted'); }); test(`GIVEN a hero select with a chosen option @@ -368,88 +315,67 @@ test.describe('Keyboard Behavior', () => { THEN the data-highlighted option should not change on re-open`, async ({ page, }) => { - const { - getTrigger, - getListbox, - getItemAt: getOptionAt, - openListbox, - } = await setup(page, 'hero'); + const { driver: d } = await setup(page, 'hero'); - await openListbox('Enter'); + await d.openListbox('Enter'); // second option highlighted + await d.getHighlightedItem().press('ArrowDown'); + await expect(d.getItemAt(1)).toHaveAttribute('data-highlighted'); + await d.getHighlightedItem().press('Enter'); + await expect(d.getListbox()).toBeHidden(); - await getTrigger().press('ArrowDown'); - await expect(getOptionAt(1)).toHaveAttribute('data-highlighted'); - await getTrigger().press('Enter'); - await expect(getListbox()).toBeHidden(); - - await getTrigger().press('ArrowDown'); - await expect(getOptionAt(1)).toHaveAttribute('data-highlighted'); + await d.getHighlightedItem().press('ArrowDown'); + await expect(d.getItemAt(1)).toHaveAttribute('data-highlighted'); }); }); test.describe('selecting options', () => { - test(`GIVEN an opened hero select with the first option highlighted + test(`GIVEN an opened select with the first option highlighted WHEN the Enter key is pressed THEN the listbox should be closed and aria-expanded should be false`, async ({ page, }) => { - const { - getTrigger, - getListbox, - getItemAt: getOptionAt, - openListbox, - } = await setup(page, 'hero'); - - await openListbox('Enter'); - - await expect(getOptionAt(0)).toHaveAttribute('data-highlighted'); - await getTrigger().press('Enter'); - await expect(getListbox()).toBeHidden(); - await expect(getTrigger()).toHaveAttribute('aria-expanded', 'false'); + const { driver: d } = await setup(page, 'hero'); + + await d.openListbox('Enter'); + + await expect(d.getItemAt(0)).toHaveAttribute('data-highlighted'); + await d.getHighlightedItem().press('Enter'); + await expect(d.getListbox()).toBeHidden(); + await expect(d.getTrigger()).toHaveAttribute('aria-expanded', 'false'); }); - test(`GIVEN an open hero select + test(`GIVEN an open select WHEN an option has data-highlighted AND the Enter key is pressed THEN option value should be the selected value AND should have an aria-selected of true`, async ({ page }) => { - const { - getTrigger, - getItemAt: getOptionAt, - getValueElement, - openListbox, - } = await setup(page, 'hero'); - - await openListbox('Enter'); - - await expect(getOptionAt(0)).toHaveAttribute('data-highlighted'); - const expectedValue = await getOptionAt(0).textContent(); - await getTrigger().press('Enter'); - await expect(getValueElement()).toHaveText(expectedValue!); + const { driver: d } = await setup(page, 'hero'); + + await d.openListbox('Enter'); + + await expect(d.getItemAt(0)).toHaveAttribute('data-highlighted'); + const expectedValue = await d.getItemAt(0).textContent(); + await d.getHighlightedItem().press('Enter'); + await expect(d.getValueElement()).toHaveText(expectedValue!); }); - test(`GIVEN an open hero select + test(`GIVEN an open select WHEN an option has data-highlighted AND the Space key is pressed THEN the listbox should be closed and aria-expanded false`, async ({ page, }) => { - const { - getTrigger, - getListbox, - getItemAt: getOptionAt, - openListbox, - } = await setup(page, 'hero'); + const { driver: d } = await setup(page, 'hero'); - await openListbox('Space'); + await d.openListbox('Space'); // second option highlighted - await expect(getOptionAt(0)).toHaveAttribute('data-highlighted'); - await getTrigger().press('Space'); - await expect(getListbox()).toBeHidden(); - await expect(getTrigger()).toHaveAttribute('aria-expanded', 'false'); + await expect(d.getItemAt(0)).toHaveAttribute('data-highlighted'); + await d.getHighlightedItem().press('Space'); + await expect(d.getListbox()).toBeHidden(); + await expect(d.getTrigger()).toHaveAttribute('aria-expanded', 'false'); }); test(`GIVEN an open hero select @@ -457,19 +383,25 @@ test.describe('Keyboard Behavior', () => { AND the Space key is pressed THEN option value should be the selected value AND should have an aria-selected of true`, async ({ page }) => { - const { - getTrigger, - getItemAt: getOptionAt, - getValueElement, - openListbox, - } = await setup(page, 'hero'); - - await openListbox('Space'); - - await expect(getOptionAt(0)).toHaveAttribute('data-highlighted'); - const expectedValue = await getOptionAt(0).textContent(); - await getTrigger().press('Space'); - await expect(getValueElement()).toHaveText(expectedValue!); + const { driver: d } = await setup(page, 'hero'); + + await d.openListbox('Space'); + + await expect(d.getItemAt(0)).toHaveAttribute('data-highlighted'); + const expectedValue = await d.getItemAt(0).textContent(); + await d.getItemAt(0).press('Space'); + await expect(d.getValueElement()).toHaveText(expectedValue!); + }); + + test(`GIVEN an open select + WHEN an option is selected + THEN focus should go back to the trigger`, async ({ page }) => { + const { driver: d } = await setup(page, 'hero'); + + await d.openListbox('Space'); + + await d.getHighlightedItem().press('Enter'); + await expect(d.getTrigger()).toBeFocused(); }); test(`GIVEN no selected item and a placeholder @@ -477,19 +409,15 @@ test.describe('Keyboard Behavior', () => { THEN the first enabled option should be selected and have aria-selected`, async ({ page, }) => { - const { - getTrigger, - getItemAt: getOptionAt, - getValueElement, - } = await setup(page, 'hero'); - - const firstItemValue = await getOptionAt(0).textContent(); - await getTrigger().focus(); - await getTrigger().press('ArrowRight'); - - expect(getValueElement()).toHaveText(firstItemValue!); - await expect(getOptionAt(0)).toHaveAttribute('aria-selected', 'true'); - await expect(getOptionAt(0)).toHaveAttribute('data-highlighted'); + const { driver: d } = await setup(page, 'hero'); + + const firstItemValue = await d.getItemAt(0).textContent(); + await d.getTrigger().focus(); + await d.getTrigger().press('ArrowRight'); + + expect(d.getValueElement()).toHaveText(firstItemValue!); + await expect(d.getItemAt(0)).toHaveAttribute('aria-selected', 'true'); + await expect(d.getItemAt(0)).toHaveAttribute('data-highlighted'); }); test(`GIVEN no selected item and a placeholder @@ -516,23 +444,19 @@ test.describe('Keyboard Behavior', () => { THEN the first item should be selected and have aria-selected & data-highlighted`, async ({ page, }) => { - const { - getTrigger, - getItemAt: getOptionAt, - getValueElement, - } = await setup(page, 'hero'); + const { driver: d } = await setup(page, 'hero'); // get initial selected value // const firstItemValue = await getOptionAt(0).textContent(); - await getTrigger().focus(); - await getTrigger().press('ArrowRight'); - await expect(getValueElement()).toHaveText('Tim'); - await getTrigger().press('ArrowRight'); - - await getTrigger().press('ArrowLeft'); - await expect(getValueElement()).toHaveText('Tim'); - await expect(getOptionAt(0)).toHaveAttribute('aria-selected', 'true'); - await expect(getOptionAt(0)).toHaveAttribute('data-highlighted'); + await d.getTrigger().focus(); + await d.getTrigger().press('ArrowRight'); + await expect(d.getValueElement()).toHaveText('Tim'); + await d.getTrigger().press('ArrowRight'); + + await d.getTrigger().press('ArrowLeft'); + await expect(d.getValueElement()).toHaveText('Tim'); + await expect(d.getItemAt(0)).toHaveAttribute('aria-selected', 'true'); + await expect(d.getItemAt(0)).toHaveAttribute('data-highlighted'); }); }); @@ -542,10 +466,10 @@ test.describe('Keyboard Behavior', () => { THEN the first option starting with the letter "j" should have data-highlighted`, async ({ page, }) => { - const { getRoot, getTrigger, openListbox } = await setup(page, 'typeahead'); - await openListbox('ArrowDown'); - await getTrigger().pressSequentially('j'); - const highlightedOpt = getRoot().locator('[data-highlighted]'); + const { driver: d } = await setup(page, 'typeahead'); + await d.openListbox('ArrowDown'); + await d.getItemAt(0).press('j'); + const highlightedOpt = d.getRoot().locator('[data-highlighted]'); await expect(highlightedOpt).toContainText('j', { ignoreCase: true }); }); @@ -554,11 +478,11 @@ test.describe('Keyboard Behavior', () => { THEN the second option starting with the letter "j" should have data-highlighted`, async ({ page, }) => { - const { getRoot, getTrigger, openListbox } = await setup(page, 'typeahead'); - await openListbox('ArrowDown'); - await getTrigger().pressSequentially('jj', { delay: 1250 }); - const highlightedOpt = getRoot().locator('[data-highlighted]'); - await expect(highlightedOpt).toContainText('jessie', { ignoreCase: true }); + const { driver: d } = await setup(page, 'typeahead'); + await d.openListbox('ArrowDown'); + + await d.getHighlightedItem().pressSequentially('jj', { delay: 1250 }); + await expect(d.getHighlightedItem()).toContainText('jessie', { ignoreCase: true }); }); test(`GIVEN an open select with a typeahead support @@ -566,11 +490,10 @@ test.describe('Keyboard Behavior', () => { THEN the first option starting with the letter "t" should have data-highlighted`, async ({ page, }) => { - const { getRoot, getTrigger, openListbox } = await setup(page, 'typeahead'); - await openListbox('ArrowDown'); - await getTrigger().pressSequentially('jjt', { delay: 1250 }); - const highlightedOpt = getRoot().locator('[data-highlighted]'); - await expect(highlightedOpt).toContainText('tim', { ignoreCase: true }); + const { driver: d } = await setup(page, 'typeahead'); + await d.openListbox('ArrowDown'); + await d.getHighlightedItem().pressSequentially('jjt', { delay: 1250 }); + await expect(d.getHighlightedItem()).toContainText('tim', { ignoreCase: true }); }); test(`GIVEN an open select with typeahead support and multiple characters @@ -579,22 +502,20 @@ test.describe('Keyboard Behavior', () => { THEN the first option starting with "je" should have data-highlighted`, async ({ page, }) => { - const { getRoot, getTrigger, openListbox } = await setup(page, 'typeahead'); - await openListbox('ArrowDown'); - await getTrigger().pressSequentially('a', { delay: 1500 }); - await getTrigger().pressSequentially('je', { delay: 100 }); - const highlightedOpt = getRoot().locator('[data-highlighted]'); - await expect(highlightedOpt).toContainText('jessie', { ignoreCase: true }); + const { driver: d } = await setup(page, 'typeahead'); + await d.openListbox('ArrowDown'); + await d.getHighlightedItem().pressSequentially('a', { delay: 1500 }); + await d.getHighlightedItem().pressSequentially('je', { delay: 100 }); + await expect(d.getHighlightedItem()).toContainText('jessie', { ignoreCase: true }); }); test(`GIVEN an open select with typeahead support and multiple characters WHEN the user types in a letter that does not match any option THEN the data-highlighted value should not change.`, async ({ page }) => { - const { getRoot, getTrigger, openListbox } = await setup(page, 'typeahead'); - await openListbox('ArrowDown'); - await getTrigger().pressSequentially('am', { delay: 1250 }); - const highlightedOpt = getRoot().locator('[data-highlighted]'); - await expect(highlightedOpt).toContainText('abby', { ignoreCase: true }); + const { driver: d } = await setup(page, 'typeahead'); + await d.openListbox('ArrowDown'); + await d.getHighlightedItem().pressSequentially('am', { delay: 1250 }); + await expect(d.getHighlightedItem()).toContainText('abby', { ignoreCase: true }); }); test(`GIVEN an open select with typeahead support and repeated characters @@ -602,44 +523,41 @@ test.describe('Keyboard Behavior', () => { THEN the data-highlighted value should cycle through the options`, async ({ page, }) => { - const { getRoot, getTrigger, openListbox } = await setup(page, 'typeahead'); - await openListbox('ArrowDown'); - await getTrigger().pressSequentially('jjj', { delay: 1250 }); - const highlightedOpt = getRoot().locator('[data-highlighted]'); - await expect(highlightedOpt).toContainText('jim', { ignoreCase: true }); + const { driver: d } = await setup(page, 'typeahead'); + await d.openListbox('ArrowDown'); + await d.getHighlightedItem().pressSequentially('jjj', { delay: 1250 }); + await expect(d.getHighlightedItem()).toContainText('jim', { ignoreCase: true }); }); test(`GIVEN an open select with typeahead support and grouped options WHEN the user types a letter matching an option in one group AND the user types a letter matching an option in another group THEN the data-highlighted value should switch groups`, async ({ page }) => { - const { getRoot, getTrigger, openListbox } = await setup(page, 'group'); - await openListbox('ArrowDown'); - await getTrigger().press('j'); - const highlightedOpt = getRoot().locator('[data-highlighted]'); - await expect(highlightedOpt).toContainText('Jim', { ignoreCase: true }); - await getTrigger().press('d'); - await expect(highlightedOpt).toContainText('dog', { ignoreCase: true }); + const { driver: d } = await setup(page, 'group'); + await d.openListbox('ArrowDown'); + await d.getHighlightedItem().press('j'); + await expect(d.getHighlightedItem()).toContainText('Jim', { ignoreCase: true }); + await d.getHighlightedItem().press('d'); + await expect(d.getHighlightedItem()).toContainText('dog', { ignoreCase: true }); }); test(`GIVEN a closed select with typeahead support WHEN the user types a letter matching an option THEN the display value should first matching option`, async ({ page }) => { - const { getTrigger } = await setup(page, 'hero'); - await getTrigger().focus(); - await getTrigger().press('j'); - await expect(getTrigger()).toHaveText('Jim'); + const { driver: d } = await setup(page, 'hero'); + await d.getTrigger().press('j'); + await expect(d.getTrigger()).toHaveText('Jim'); }); test(`GIVEN a closed select with typeahead support WHEN the user types a letter matching an option THEN the first matching option should be selected`, async ({ page }) => { // ideally want to refactor this so that even if the test example is changed, the test will still pass, getting it more programmatically. - const { getRoot, getTrigger } = await setup(page, 'hero'); - await getTrigger().focus(); + const { driver: d } = await setup(page, 'hero'); + await d.getTrigger().focus(); const char = 'j'; - await getTrigger().press(char); - const firstJOption = getRoot().locator('li', { hasText: char }).nth(0); + await d.getTrigger().press(char); + const firstJOption = d.getRoot().locator('li', { hasText: char }).nth(0); await expect(firstJOption).toHaveAttribute('aria-selected', 'true'); await expect(firstJOption).toHaveAttribute('data-highlighted'); }); @@ -651,95 +569,69 @@ test.describe('Keyboard Behavior', () => { AND the last option is data-highlighted WHEN the down key is pressed THEN data-highlighted should stay on the last option`, async ({ page }) => { - const { - getTrigger, - getItemAt: getOptionAt, - openListbox, - } = await setup(page, 'hero'); + const { driver: d } = await setup(page, 'hero'); // initially last option is highlighted - await openListbox('Enter'); - const trigger = getTrigger(); - await trigger.focus(); - await trigger.press('End'); - - await expect(getOptionAt('last')).toHaveAttribute('data-highlighted'); + await d.openListbox('Enter'); + await d.getItemAt(0).press('End'); - await trigger.focus(); - await trigger.press('ArrowDown'); - await expect(getOptionAt('last')).toHaveAttribute('data-highlighted'); + await d.getItemAt('last').press('ArrowDown'); + await expect(d.getItemAt('last')).toHaveAttribute('data-highlighted'); }); test(`GIVEN an open basic select AND the first option is data-highlighted WHEN the up arrow key is pressed THEN data-highlighted should stay on the first option`, async ({ page }) => { - const { - getTrigger, - getItemAt: getOptionAt, - openListbox, - } = await setup(page, 'hero'); - - await openListbox('Enter'); - const option = getOptionAt(0); - await expect(option).toHaveAttribute('data-highlighted'); - await getTrigger().focus(); - await getTrigger().press('ArrowUp'); - await expect(option).toHaveAttribute('data-highlighted'); + const { driver: d } = await setup(page, 'hero'); + + await d.openListbox('Enter'); + const firstItem = d.getItemAt(0); + await expect(firstItem).toHaveAttribute('data-highlighted'); + await firstItem.press('ArrowUp'); + await expect(firstItem).toHaveAttribute('data-highlighted'); }); test(`GIVEN a closed basic select AND the last option is selected WHEN the right arrow key is pressed THEN it should stay on the last option`, async ({ page }) => { - const { - getTrigger, - getItemAt: getOptionAt, - getListbox, - openListbox, - } = await setup(page, 'hero'); + const { driver: d } = await setup(page, 'hero'); // initially last option is highlighted & listbox closed - await openListbox('Enter'); - await getTrigger().focus(); - await getTrigger().press('End'); - - await expect(getOptionAt('last')).toHaveAttribute('data-highlighted'); - await getTrigger().press('Enter'); - await expect(getOptionAt('last')).toHaveAttribute('aria-selected', 'true'); - await expect(getListbox()).toBeHidden(); - - await getTrigger().focus(); - await getTrigger().press('ArrowRight'); - await expect(getOptionAt('last')).toHaveAttribute('data-highlighted'); - await expect(getOptionAt('last')).toHaveAttribute('aria-selected', 'true'); + await d.openListbox('Enter'); + await d.getItemAt(0).press('End'); + + const lastItem = d.getItemAt('last'); + await expect(lastItem).toHaveAttribute('data-highlighted'); + await lastItem.press('Enter'); + await expect(lastItem).toHaveAttribute('aria-selected', 'true'); + await expect(d.getListbox()).toBeHidden(); + + await d.getTrigger().press('ArrowRight'); + await expect(lastItem).toHaveAttribute('data-highlighted'); + await expect(lastItem).toHaveAttribute('aria-selected', 'true'); }); test(`GIVEN a closed basic select AND the first option is selected WHEN the left arrow key is pressed THEN it should stay on the first option`, async ({ page }) => { - const { - getTrigger, - getItemAt: getOptionAt, - getListbox, - openListbox, - } = await setup(page, 'hero'); + const { driver: d } = await setup(page, 'hero'); // initially first option is highlighted & listbox closed - await openListbox('Enter'); - await expect(getListbox()).toBeVisible(); - await getTrigger().focus(); - await getTrigger().press('Enter'); - - await expect(getOptionAt(0)).toHaveAttribute('data-highlighted'); - await expect(getOptionAt(0)).toHaveAttribute('aria-selected', 'true'); - await expect(getListbox()).toBeHidden(); - - await getTrigger().focus(); - await getTrigger().press('ArrowLeft'); - await expect(getOptionAt(0)).toHaveAttribute('data-highlighted'); - await expect(getOptionAt(0)).toHaveAttribute('aria-selected', 'true'); + await d.openListbox('Enter'); + await expect(d.getListbox()).toBeVisible(); + await d.getItemAt(0).press('Enter'); + + await expect(d.getItemAt(0)).toHaveAttribute('data-highlighted'); + await expect(d.getItemAt(0)).toHaveAttribute('aria-selected', 'true'); + await expect(d.getListbox()).toBeHidden(); + + await d.getTrigger().focus(); + await d.getTrigger().press('ArrowLeft'); + await expect(d.getItemAt(0)).toHaveAttribute('data-highlighted'); + await expect(d.getItemAt(0)).toHaveAttribute('aria-selected', 'true'); }); }); @@ -748,94 +640,71 @@ test.describe('Keyboard Behavior', () => { AND the last option is data-highlighted WHEN the down arrow key is pressed THEN the first option should have data-highlighted`, async ({ page }) => { - const { - getTrigger, - getItemAt: getOptionAt, - openListbox, - } = await setup(page, 'loop'); + const { driver: d } = await setup(page, 'loop'); // initially last option is highlighted - await openListbox('Enter'); - await getTrigger().focus(); - await getTrigger().press('End'); + await d.openListbox('Enter'); + await d.getHighlightedItem().press('End'); - await expect(getOptionAt('last')).toHaveAttribute('data-highlighted'); + await expect(d.getItemAt('last')).toHaveAttribute('data-highlighted'); - await getTrigger().focus(); - await getTrigger().press('ArrowDown'); - await expect(getOptionAt(0)).toHaveAttribute('data-highlighted'); + await d.getHighlightedItem().press('ArrowDown'); + await expect(d.getItemAt(0)).toHaveAttribute('data-highlighted'); }); test(`GIVEN an open select with loop enabled AND the first option is data-highlighted WHEN the up arrow key is pressed THEN the last option should have data-highlighted`, async ({ page }) => { - const { - getTrigger, - getItemAt: getOptionAt, - openListbox, - } = await setup(page, 'loop'); + const { driver: d } = await setup(page, 'loop'); // initially last option is highlighted - await openListbox('Enter'); - await expect(getOptionAt(0)).toHaveAttribute('data-highlighted'); + await d.openListbox('Enter'); + await expect(d.getItemAt(0)).toHaveAttribute('data-highlighted'); - await getTrigger().focus(); - await getTrigger().press('ArrowUp'); - await expect(getOptionAt('last')).toHaveAttribute('data-highlighted'); + await d.getHighlightedItem().press('ArrowUp'); + await expect(d.getItemAt('last')).toHaveAttribute('data-highlighted'); }); test(`GIVEN a closed select with loop enabled AND the last option is selected WHEN the right arrow key is pressed THEN it should loop to the first option`, async ({ page }) => { - const { - getTrigger, - getItemAt: getOptionAt, - openListbox, - getListbox, - } = await setup(page, 'loop'); + const { driver: d } = await setup(page, 'loop'); // initially last option is highlighted - await openListbox('Enter'); - await getTrigger().focus(); - await getTrigger().press('End'); - await getTrigger().press('Enter'); + await d.openListbox('Enter'); + await d.getHighlightedItem().press('End'); + await d.getHighlightedItem().press('Enter'); - await expect(getListbox()).toBeHidden(); + await expect(d.getListbox()).toBeHidden(); - await expect(getOptionAt('last')).toHaveAttribute('aria-selected', 'true'); + await expect(d.getItemAt('last')).toHaveAttribute('aria-selected', 'true'); - await getTrigger().focus(); - await getTrigger().press('ArrowRight'); - await expect(getOptionAt(0)).toHaveAttribute('data-highlighted'); - await expect(getOptionAt(0)).toHaveAttribute('aria-selected', 'true'); + await d.getTrigger().focus(); + await d.getTrigger().press('ArrowRight'); + await expect(d.getItemAt(0)).toHaveAttribute('data-highlighted'); + await expect(d.getItemAt(0)).toHaveAttribute('aria-selected', 'true'); }); test(`GIVEN a closed select with loop enabled AND the first option is selected WHEN the right arrow key is pressed THEN it should loop to the first option`, async ({ page }) => { - const { - getTrigger, - getItemAt: getOptionAt, - openListbox, - getListbox, - } = await setup(page, 'loop'); + const { driver: d } = await setup(page, 'loop'); // initially select first option - await openListbox('Enter'); - await getTrigger().focus(); - await getTrigger().press('Enter'); + await d.openListbox('Enter'); + await d.getHighlightedItem().press('Enter'); - await expect(getListbox()).toBeHidden(); + await expect(d.getListbox()).toBeHidden(); - await expect(getOptionAt(0)).toHaveAttribute('aria-selected', 'true'); + await expect(d.getItemAt(0)).toHaveAttribute('aria-selected', 'true'); - await getTrigger().focus(); - await getTrigger().press('ArrowLeft'); - await expect(getOptionAt('last')).toHaveAttribute('data-highlighted'); - await expect(getOptionAt('last')).toHaveAttribute('aria-selected', 'true'); + await d.getTrigger().focus(); + await d.getTrigger().press('ArrowLeft'); + await expect(d.getItemAt('last')).toHaveAttribute('data-highlighted'); + await expect(d.getItemAt('last')).toHaveAttribute('aria-selected', 'true'); }); }); }); @@ -844,18 +713,13 @@ test.describe('Keyboard Behavior', () => { AND the last option is not visible WHEN the end key is pressed THEN the last option should be visible`, async ({ page }) => { - const { - getTrigger, - getItemAt: getOptionAt, - openListbox, - } = await setup(page, 'scrollable'); + const { driver: d } = await setup(page, 'scrollable'); - await openListbox('Enter'); + await d.openListbox('Enter'); - await getTrigger().focus(); - await getTrigger().press('End'); + await d.getHighlightedItem().press('End'); - await expect(getOptionAt('last')).toBeInViewport(); + await expect(d.getItemAt('last')).toBeInViewport(); }); }); @@ -863,60 +727,32 @@ test.describe('Disabled', () => { test(`GIVEN an open disabled select with the first option disabled WHEN clicking the disabled option It should have aria-disabled`, async ({ page }) => { - const { - getTrigger, - getItemAt: getOptionAt, - openListbox, - } = await setup(page, 'disabled'); + const { driver: d } = await setup(page, 'disabled'); - await openListbox('Enter'); - - await getTrigger().focus(); - await getTrigger().press('ArrowDown'); - await expect(getOptionAt(0)).toBeDisabled(); + await d.openListbox('Enter'); + await d.getHighlightedItem().press('ArrowDown'); + await expect(d.getItemAt(0)).toBeDisabled(); }); - // causing false positives? - // test(`GIVEN an open disabled select with the first option disabled - // WHEN clicking the disabled option - // THEN the listbox should stay open`, async ({ page }) => { - // const { getListbox, getOptionAt, openListbox } = await setup( - // page, - // 'disabled', - // ); - - // await openListbox('Enter'); - - // const options = await getOptionAt(); - // // eslint-disable-next-line playwright/no-force-option - // await options[0].click({ force: true }); - // await expect(getListbox()).toBeVisible(); - // }); - test(`GIVEN an open disabled select WHEN first option is disabled THEN the second option should have data-highlighted`, async ({ page }) => { - const { getItemAt: getOptionAt, openListbox } = await setup(page, 'disabled'); + const { driver: d } = await setup(page, 'disabled'); - await openListbox('ArrowDown'); + await d.openListbox('ArrowDown'); - await expect(getOptionAt(1)).toHaveAttribute('data-highlighted'); + await expect(d.getItemAt(1)).toHaveAttribute('data-highlighted'); }); test(`GIVEN an open disabled select WHEN the last option is disabled and the end key is pressed THEN the second last index should have data-highlighted`, async ({ page }) => { - const { - driver, - getTrigger, - getItemAt: getOptionAt, - openListbox, - } = await setup(page, 'disabled'); - - await openListbox('ArrowDown'); - await getTrigger().press('End'); - const length = await driver.getItemsLength(); - const lastEnabledOption = await getOptionAt(length - 2); + const { driver: d } = await setup(page, 'disabled'); + + await d.openListbox('ArrowDown'); + await d.getHighlightedItem().press('End'); + const length = await d.getItemsLength(); + const lastEnabledOption = d.getItemAt(length - 2); await expect(lastEnabledOption).toHaveAttribute('data-highlighted'); }); @@ -924,36 +760,28 @@ test.describe('Disabled', () => { WHEN the second option is highlighted and the down arrow key is pressed AND the first and third options are disabled THEN the fourth option should be highlighted`, async ({ page }) => { - const { - getTrigger, - getItemAt: getOptionAt, - openListbox, - } = await setup(page, 'disabled'); - - await openListbox('ArrowDown'); - await expect(getOptionAt(1)).toHaveAttribute('data-highlighted'); - await getTrigger().press('ArrowDown'); - await expect(getOptionAt(3)).toHaveAttribute('data-highlighted'); + const { driver: d } = await setup(page, 'disabled'); + + await d.openListbox('ArrowDown'); + await expect(d.getItemAt(1)).toHaveAttribute('data-highlighted'); + await d.getHighlightedItem().press('ArrowDown'); + await expect(d.getItemAt(3)).toHaveAttribute('data-highlighted'); }); test(`GIVEN an open disabled select WHEN the fourth is highlighted and the up key is pressed AND the first and third options are disabled THEN the second option should be highlighted`, async ({ page }) => { - const { - getTrigger, - getItemAt: getOptionAt, - openListbox, - } = await setup(page, 'disabled'); + const { driver: d } = await setup(page, 'disabled'); // initially the fourh option is highlighted - await openListbox('ArrowDown'); - const secondOption = await getOptionAt(1); + await d.openListbox('ArrowDown'); + const secondOption = await d.getItemAt(1); await expect(secondOption).toHaveAttribute('data-highlighted'); - await getTrigger().press('ArrowDown'); - await expect(getOptionAt(3)).toHaveAttribute('data-highlighted'); + await d.getHighlightedItem().press('ArrowDown'); + await expect(d.getItemAt(3)).toHaveAttribute('data-highlighted'); - await getTrigger().press('ArrowUp'); + await d.getHighlightedItem().press('ArrowUp'); await expect(secondOption).toHaveAttribute('data-highlighted'); }); }); @@ -964,9 +792,9 @@ test.describe('Props', () => { THEN the placeholder should be presented instead of a selected value`, async ({ page, }) => { - const { getValueElement } = await setup(page, 'hero'); + const { driver: d } = await setup(page, 'hero'); - await expect(getValueElement()).toHaveText('Select an option'); + await expect(d.getValueElement()).toHaveText('Select an option'); }); test(`GIVEN a select with an onChange$ prop @@ -974,17 +802,13 @@ test.describe('Props', () => { THEN the placeholder should be presented instead of a selected value`, async ({ page, }) => { - const { - openListbox, - getItemAt: getOptionAt, - getRoot, - } = await setup(page, 'change-value'); + const { driver: d } = await setup(page, 'change-value'); - await openListbox('click'); + await d.openListbox('click'); - await getOptionAt(3).click(); + await d.getItemAt(3).click(); - const sibling = getRoot().locator('+ p'); + const sibling = d.getRoot().locator('+ p'); await expect(sibling).toHaveText('You have changed 1 times'); }); @@ -993,11 +817,11 @@ test.describe('Props', () => { THEN the placeholder should be presented instead of a selected value`, async ({ page, }) => { - const { getRoot, openListbox } = await setup(page, 'open-change'); + const { driver: d } = await setup(page, 'open-change'); - await openListbox('click'); + await d.openListbox('click'); - const sibling = getRoot().locator('+ p'); + const sibling = d.getRoot().locator('+ p'); await expect(sibling).toHaveText('The listbox opened and closed 1 time(s)'); }); @@ -1023,13 +847,13 @@ test.describe('Props', () => { THEN the selected value should be the data passed to the bind:value prop AND should should have data-highlighted AND aria-selected set to true`, async ({ page }) => { - const { getValueElement, getItemAt: getOptionAt } = await setup(page, 'controlled'); + const { driver: d } = await setup(page, 'controlled'); - const expectedValue = await getOptionAt(1).textContent(); + const expectedValue = await d.getItemAt(1).textContent(); - await expect(getValueElement()).toContainText(expectedValue!); - await expect(getOptionAt(1)).toHaveAttribute('data-highlighted'); - await expect(getOptionAt(1)).toHaveAttribute('aria-selected', 'true'); + await expect(d.getValueElement()).toContainText(expectedValue!); + await expect(d.getItemAt(1)).toHaveAttribute('data-highlighted'); + await expect(d.getItemAt(1)).toHaveAttribute('aria-selected', 'true'); }); test(`GIVEN a controlled closed select with a bind:open prop on the root component @@ -1049,14 +873,13 @@ test.describe('Props', () => { test(`GIVEN a select with distinct display and option values WHEN the 2nd option is selected THEN the selected value matches the 2nd option's value`, async ({ page }) => { - const { openListbox, getTrigger } = await setup(page, 'item-value'); + const { driver: d } = await setup(page, 'item-value'); - await openListbox('Enter'); + await d.openListbox('Enter'); await expect(page.locator('p')).toContainText('The selected value is: null'); - await getTrigger().focus(); - await getTrigger().press('ArrowDown'); - await getTrigger().press('Enter'); + await d.getHighlightedItem().press('ArrowDown'); + await d.getHighlightedItem().press('Enter'); await expect(page.locator('p')).toContainText('The selected value is: 1'); }); @@ -1064,12 +887,12 @@ test.describe('Props', () => { test(`GIVEN a select with distinct display and option values WHEN a controlled value is set to the 5th option THEN the selected value matches the 5th option's value`, async ({ page }) => { - const { getTrigger } = await setup(page, 'controlled-value'); + const { driver: d } = await setup(page, 'controlled-value'); - await expect(getTrigger()).toHaveText('Ryan'); + await expect(d.getTrigger()).toHaveText('Ryan'); await page.getByRole('button', { name: 'Change to Abby' }).click(); - await expect(getTrigger()).toHaveText(`Abby`); + await expect(d.getTrigger()).toHaveText(`Abby`); }); test(`GIVEN a select with distinct display and option values @@ -1098,7 +921,7 @@ test.describe('Props', () => { /** TODO: add docs telling people how to add an aria-label to the root component. (accessible name) */ test.describe('A11y', () => { test('Axe Validation Test', async ({ page }) => { - const { openListbox } = await setup(page, 'hero'); + const { driver: d } = await setup(page, 'hero'); const initialResults = await new AxeBuilder({ page }) .include('[role="combobox"]') @@ -1106,7 +929,7 @@ test.describe('A11y', () => { expect(initialResults.violations).toEqual([]); - await openListbox('click'); + await d.openListbox('click'); const afterClickResults = await new AxeBuilder({ page }) .include('[role="combobox"]') @@ -1119,11 +942,11 @@ test.describe('A11y', () => { WHEN the user adds a new group THEN the group should have an aria-labelledby attribute AND its associated label`, async ({ page }) => { - const { getRoot, openListbox } = await setup(page, 'group'); - await openListbox('ArrowDown'); - const labelId = await getRoot().getByRole('listitem').first().getAttribute('id'); + const { driver: d } = await setup(page, 'group'); + await d.openListbox('ArrowDown'); + const labelId = await d.getRoot().getByRole('listitem').first().getAttribute('id'); - await expect(getRoot().getByRole('group').first()).toHaveAttribute( + await expect(d.getRoot().getByRole('group').first()).toHaveAttribute( 'aria-labelledby', labelId!, ); @@ -1134,31 +957,27 @@ test.describe('A11y', () => { THEN aria-activedescendent should be the id of the second option`, async ({ page, }) => { - const { - getTrigger, - getRoot, - openListbox, - getItemAt: getOptionAt, - } = await setup(page, 'hero'); - await openListbox('ArrowDown'); - await getTrigger().focus(); - await getTrigger().press('ArrowDown'); - - const secondOptionId = await getOptionAt(1).getAttribute('id'); - - await expect(getRoot()).toHaveAttribute('aria-activedescendant', `${secondOptionId}`); + const { driver: d } = await setup(page, 'hero'); + await d.openListbox('ArrowDown'); + await d.getHighlightedItem().press('ArrowDown'); + + const secondOptionId = await d.getItemAt(1).getAttribute('id'); + + await expect(d.getRoot()).toHaveAttribute( + 'aria-activedescendant', + `${secondOptionId}`, + ); }); test(`GIVEN an open hero select with aria-activedescendent WHEN the listbox is closed THEN aria-activedescendent should be an empty string`, async ({ page }) => { - const { getTrigger, getRoot, openListbox, getListbox } = await setup(page, 'hero'); - await openListbox('ArrowDown'); - await getTrigger().focus(); - await getTrigger().press('Enter'); - await expect(getListbox()).toBeHidden(); + const { driver: d } = await setup(page, 'hero'); + await d.openListbox('ArrowDown'); + await d.getHighlightedItem().press('Enter'); + await expect(d.getListbox()).toBeHidden(); - await expect(getRoot()).toHaveAttribute('aria-activedescendant', ''); + await expect(d.getRoot()).toHaveAttribute('aria-activedescendant', ''); }); test(`GIVEN a hero select with aria-controls @@ -1166,11 +985,11 @@ test.describe('A11y', () => { THEN the root's aria-controls should be equal to the ID of the listbox`, async ({ page, }) => { - const { getRoot, getListbox, openListbox } = await setup(page, 'hero'); - await openListbox('Enter'); - const listboxId = await getListbox().getAttribute('id'); + const { driver: d } = await setup(page, 'hero'); + await d.openListbox('Enter'); + const listboxId = await d.getListbox().getAttribute('id'); - await expect(getRoot()).toHaveAttribute('aria-controls', `${listboxId}`); + await expect(d.getRoot()).toHaveAttribute('aria-controls', `${listboxId}`); }); }); @@ -1236,9 +1055,9 @@ test.describe('Multiple Selection', () => { }) => { const { driver: d } = await setup(page, 'multiple'); await d.openListbox('Enter'); - await d.getTrigger().press(key); + await d.getHighlightedItem().press(key); await expect(d.getItemAt(0)).toHaveAttribute('aria-selected', 'true'); - await d.getTrigger().press(key); + await d.getHighlightedItem().press(key); await expect(d.getItemAt(0)).toHaveAttribute('aria-selected', 'false'); }); } @@ -1251,7 +1070,7 @@ test.describe('Multiple Selection', () => { await d.openListbox('click'); await d.getItemAt(0).click(); await expect(d.getItemAt(0)).toHaveAttribute('aria-selected', 'true'); - await d.getTrigger().press('Escape'); + await d.getHighlightedItem().press('Escape'); await expect(d.getListbox()).toBeHidden(); }); @@ -1261,8 +1080,8 @@ test.describe('Multiple Selection', () => { page, }) => { const { driver: d } = await setup(page, 'multiple'); - await d.openListbox('click'); - await d.getTrigger().press('Shift+ArrowDown'); + await d.openListbox('Enter'); + await d.getHighlightedItem().press('Shift+ArrowDown'); await expect(d.getItemAt(1)).toHaveAttribute('data-highlighted'); await expect(d.getItemAt(1)).toHaveAttribute('aria-selected', 'true'); }); @@ -1275,25 +1094,12 @@ test.describe('Multiple Selection', () => { const { driver: d } = await setup(page, 'multiple'); // initial setup await d.openListbox('Enter'); - await d.getTrigger().press('ArrowDown'); + await d.getHighlightedItem().press('ArrowDown'); await expect(d.getItemAt(1)).toHaveAttribute('data-highlighted'); - await d.getTrigger().press('Shift+ArrowUp'); + await d.getHighlightedItem().press('Shift+ArrowUp'); await expect(d.getItemAt(0)).toHaveAttribute('data-highlighted'); await expect(d.getItemAt(0)).toHaveAttribute('aria-selected', 'true'); }); - - // test(`GIVEN a multi select - // WHEN hitting the ctrl + a key - // THEN select all options`, async ({ page }) => { - // const { driver: d } = await setup(page, 'multiple'); - // // initial setup - // await d.openListbox('Enter'); - // await d.getTrigger().press('Control+A'); - - // for (let i = 0; i < (await d.getOptionsLength()); i++) { - // await expect(d.getOptionAt(i)).toHaveAttribute('aria-selected', 'true'); - // } - // }); }); });