From 3f57751d2014b304ca4cd5ebb565a9fa893ee044 Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Thu, 7 May 2026 11:03:17 +0200 Subject: [PATCH] fix(react-combobox): use role attribute instead of classname for active descendant (#36109) --- ...-ad77e7e8-12a4-426a-83c3-428085e15705.json | 7 ++++ .../library/etc/react-combobox.api.md | 3 ++ .../src/components/Combobox/useCombobox.tsx | 4 +-- .../src/components/Dropdown/useDropdown.tsx | 4 +-- .../src/components/Listbox/useListbox.ts | 4 +-- .../react-combobox/library/src/index.ts | 1 + .../src/utils/isComboboxOptionElement.test.ts | 34 +++++++++++++++++++ .../src/utils/isComboboxOptionElement.ts | 10 ++++++ 8 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 change/@fluentui-react-combobox-ad77e7e8-12a4-426a-83c3-428085e15705.json create mode 100644 packages/react-components/react-combobox/library/src/utils/isComboboxOptionElement.test.ts create mode 100644 packages/react-components/react-combobox/library/src/utils/isComboboxOptionElement.ts diff --git a/change/@fluentui-react-combobox-ad77e7e8-12a4-426a-83c3-428085e15705.json b/change/@fluentui-react-combobox-ad77e7e8-12a4-426a-83c3-428085e15705.json new file mode 100644 index 0000000000000..b0459569597c3 --- /dev/null +++ b/change/@fluentui-react-combobox-ad77e7e8-12a4-426a-83c3-428085e15705.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: use role attribute instead of classname for active descendant", + "packageName": "@fluentui/react-combobox", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-combobox/library/etc/react-combobox.api.md b/packages/react-components/react-combobox/library/etc/react-combobox.api.md index b91c710ade0b8..fa49c97753a48 100644 --- a/packages/react-components/react-combobox/library/etc/react-combobox.api.md +++ b/packages/react-components/react-combobox/library/etc/react-combobox.api.md @@ -150,6 +150,9 @@ export type DropdownState = ComponentState & Omit; diff --git a/packages/react-components/react-combobox/library/src/components/Combobox/useCombobox.tsx b/packages/react-components/react-combobox/library/src/components/Combobox/useCombobox.tsx index f6011298c9051..57852c31334b0 100644 --- a/packages/react-components/react-combobox/library/src/components/Combobox/useCombobox.tsx +++ b/packages/react-components/react-combobox/library/src/components/Combobox/useCombobox.tsx @@ -26,7 +26,7 @@ import type { } from './Combobox.types'; import { useListboxSlot } from '../../utils/useListboxSlot'; import { useInputTriggerSlot } from './useInputTriggerSlot'; -import { optionClassNames } from '../Option/useOptionStyles.styles'; +import { isComboboxOptionElement } from '../../utils/isComboboxOptionElement'; /** * Create the base state required to render Combobox, without design-only props. @@ -47,7 +47,7 @@ export const useComboboxBase_unstable = ( activeParentRef, controller: activeDescendantController, } = useActiveDescendant({ - matchOption: el => el.classList.contains(optionClassNames.root), + matchOption: isComboboxOptionElement, }); const comboboxInternalState = useComboboxBaseState({ ...props, editable: true, activeDescendantController }); const { appearance: _appearance, size: _size, ...baseState } = comboboxInternalState; diff --git a/packages/react-components/react-combobox/library/src/components/Dropdown/useDropdown.tsx b/packages/react-components/react-combobox/library/src/components/Dropdown/useDropdown.tsx index 7abed1d449e23..85ac60794029d 100644 --- a/packages/react-components/react-combobox/library/src/components/Dropdown/useDropdown.tsx +++ b/packages/react-components/react-combobox/library/src/components/Dropdown/useDropdown.tsx @@ -19,8 +19,8 @@ import { Listbox } from '../Listbox/Listbox'; import type { DropdownBaseProps, DropdownBaseState, DropdownProps, DropdownState } from './Dropdown.types'; import { useListboxSlot } from '../../utils/useListboxSlot'; import { useButtonTriggerSlot } from './useButtonTriggerSlot'; -import { optionClassNames } from '../Option/useOptionStyles.styles'; import type { ComboboxOpenEvents } from '../Combobox/Combobox.types'; +import { isComboboxOptionElement } from '../../utils/isComboboxOptionElement'; /** * Create the base state required to render Dropdown, without design-only props. @@ -41,7 +41,7 @@ export const useDropdownBase_unstable = ( activeParentRef, controller: activeDescendantController, } = useActiveDescendant({ - matchOption: el => el.classList.contains(optionClassNames.root), + matchOption: isComboboxOptionElement, }); const dropdownInternalState = useComboboxBaseState({ ...props, activeDescendantController, freeform: false }); diff --git a/packages/react-components/react-combobox/library/src/components/Listbox/useListbox.ts b/packages/react-components/react-combobox/library/src/components/Listbox/useListbox.ts index 10404a22f7082..ad7506e6fed27 100644 --- a/packages/react-components/react-combobox/library/src/components/Listbox/useListbox.ts +++ b/packages/react-components/react-combobox/library/src/components/Listbox/useListbox.ts @@ -19,9 +19,9 @@ import type { ListboxProps, ListboxState } from './Listbox.types'; import { getDropdownActionFromKey } from '../../utils/dropdownKeyActions'; import { useOptionCollection } from '../../utils/useOptionCollection'; import { useSelection } from '../../utils/useSelection'; -import { optionClassNames } from '../Option/useOptionStyles.styles'; import { ListboxContext, useListboxContext_unstable } from '../../contexts/ListboxContext'; import { useOnKeyboardNavigationChange } from '@fluentui/react-tabster'; +import { isComboboxOptionElement } from '../../utils/isComboboxOptionElement'; // eslint-disable-next-line @typescript-eslint/naming-convention const UNSAFE_noLongerUsed = { @@ -50,7 +50,7 @@ export const useListbox_unstable = (props: ListboxProps, ref: React.Ref({ - matchOption: el => el.classList.contains(optionClassNames.root), + matchOption: isComboboxOptionElement, }); const hasListboxContext = useHasParentContext(ListboxContext); diff --git a/packages/react-components/react-combobox/library/src/index.ts b/packages/react-components/react-combobox/library/src/index.ts index 7245ce08a22f9..89fca52157cfe 100644 --- a/packages/react-components/react-combobox/library/src/index.ts +++ b/packages/react-components/react-combobox/library/src/index.ts @@ -76,3 +76,4 @@ export { useButtonTriggerSlot } from './components/Dropdown/useButtonTriggerSlot export { useInputTriggerSlot } from './components/Combobox/useInputTriggerSlot'; export { useListboxSlot } from './utils/useListboxSlot'; export type { ComboboxBaseState, ComboboxBaseProps } from './utils/ComboboxBase.types'; +export { isComboboxOptionElement } from './utils/isComboboxOptionElement'; diff --git a/packages/react-components/react-combobox/library/src/utils/isComboboxOptionElement.test.ts b/packages/react-components/react-combobox/library/src/utils/isComboboxOptionElement.test.ts new file mode 100644 index 0000000000000..6b1456223cbff --- /dev/null +++ b/packages/react-components/react-combobox/library/src/utils/isComboboxOptionElement.test.ts @@ -0,0 +1,34 @@ +import { isComboboxOptionElement } from './isComboboxOptionElement'; + +/** + * Helper function to create an element with a specified role + */ +function createElementWithRole(tagName: string, role?: string) { + const element = document.createElement(tagName); + if (role) { + element.setAttribute('role', role); + } + return element; +} + +describe('isComboboxOptionElement', () => { + it('returns true for elements with role="option"', () => { + const element = createElementWithRole('div', 'option'); + expect(isComboboxOptionElement(element)).toBe(true); + }); + + it('returns true for elements with role="menuitemcheckbox"', () => { + const element = createElementWithRole('div', 'menuitemcheckbox'); + expect(isComboboxOptionElement(element)).toBe(true); + }); + + it('returns false for elements without role attribute', () => { + const element = createElementWithRole('div'); + expect(isComboboxOptionElement(element)).toBe(false); + }); + + it('returns false for elements with other role values', () => { + const element = createElementWithRole('div', 'listbox'); + expect(isComboboxOptionElement(element)).toBe(false); + }); +}); diff --git a/packages/react-components/react-combobox/library/src/utils/isComboboxOptionElement.ts b/packages/react-components/react-combobox/library/src/utils/isComboboxOptionElement.ts new file mode 100644 index 0000000000000..0861edfdbbd41 --- /dev/null +++ b/packages/react-components/react-combobox/library/src/utils/isComboboxOptionElement.ts @@ -0,0 +1,10 @@ +/** + * Checks whether the given element is a combobox option element. + * Supports elements with role="option" or role="menuitemcheckbox". + * + * @param element - the element to check + * @returns true if the element has a valid combobox option role, false otherwise + */ +export function isComboboxOptionElement(element: HTMLElement): boolean { + return element.role === 'option' || element.role === 'menuitemcheckbox'; +}