diff --git a/src/OptionList.tsx b/src/OptionList.tsx index 21e69308..27113026 100644 --- a/src/OptionList.tsx +++ b/src/OptionList.tsx @@ -257,6 +257,12 @@ const OptionList: React.ForwardRefRenderFunction = (_, r }, })); + // Skip group headers to match native , + ); + + // The hidden accessibility container is the listbox itself in virtual mode + const getHiddenOptions = () => + Array.from(document.querySelectorAll('#virtual-select_list div[role="option"]')); + + // Active index is 0, so the hidden container renders options 0 and 1 + let hiddenOptions = getHiddenOptions(); + expect(hiddenOptions.map((option) => option.getAttribute('aria-posinset'))).toEqual([ + '1', + '2', + ]); + hiddenOptions.forEach((option) => { + expect(option).toHaveAttribute('aria-setsize', '5'); + }); + + // Move active option to the middle of the list + keyDown(container.querySelector('input')!, KeyCode.DOWN); + keyDown(container.querySelector('input')!, KeyCode.DOWN); + + // Active index is 2, so the hidden container renders options 1, 2 and 3 + hiddenOptions = getHiddenOptions(); + expect(hiddenOptions.map((option) => option.getAttribute('aria-posinset'))).toEqual([ + '2', + '3', + '4', + ]); + hiddenOptions.forEach((option) => { + expect(option).toHaveAttribute('aria-setsize', '5'); + }); + }); + + it('aria-posinset and aria-setsize should skip group headers like native optgroup', () => { + const { container } = render( + , + ); + + const hiddenContainer = document.querySelector('#virtual-select_list'); + + // Activate the last grouped option so the window spans the group + // boundary: options '2', '3' (grouped) and '4' (top-level) + keyDown(container.querySelector('input')!, KeyCode.DOWN); + keyDown(container.querySelector('input')!, KeyCode.DOWN); + + // Grouped options stay wrapped; the top-level option renders bare + const groupWrappers = Array.from(hiddenContainer.querySelectorAll('div[role="group"]')); + expect(groupWrappers).toHaveLength(1); + expect(groupWrappers[0]).toHaveAttribute('aria-label', 'Group'); + expect( + Array.from(groupWrappers[0].querySelectorAll('div[role="option"]')).map((option) => + option.getAttribute('aria-posinset'), + ), + ).toEqual(['2', '3']); + + const topLevelOption = Array.from( + hiddenContainer.querySelectorAll('div[role="option"]'), + ).find((option) => option.getAttribute('aria-posinset') === '4'); + expect(topLevelOption).toBeTruthy(); + expect(topLevelOption.parentElement).toBe(hiddenContainer); + expect(topLevelOption).toHaveAttribute('aria-setsize', '4'); + }); + it('should have correct aria and role attributes in virtual false', () => { render(