diff --git a/change/@fluentui-react-combobox-0207f64c-2f2c-4b8f-b386-397ce38c1b95.json b/change/@fluentui-react-combobox-0207f64c-2f2c-4b8f-b386-397ce38c1b95.json
new file mode 100644
index 00000000000000..6fb344ea7d380e
--- /dev/null
+++ b/change/@fluentui-react-combobox-0207f64c-2f2c-4b8f-b386-397ce38c1b95.json
@@ -0,0 +1,7 @@
+{
+ "type": "patch",
+ "comment": "Exposed `activeDescendantImperativeRef` on Combobox, Dropdown, and Listbox",
+ "packageName": "@fluentui/react-combobox",
+ "email": "personal_dev@mkurr.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 fa49c97753a481..c6d4cdc298b5bb 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
@@ -49,6 +49,7 @@ export type ComboboxBaseProps = SelectionProps & HighlightedOptionProps & Pick
;
};
// @public
@@ -174,6 +175,7 @@ export type ListboxContextValues = {
// @public
export type ListboxProps = ComponentProps & SelectionProps & {
disableAutoFocus?: boolean;
+ activeDescendantImperativeRef?: React_2.RefObject;
};
// @public (undocumented)
diff --git a/packages/react-components/react-combobox/library/src/components/Combobox/Combobox.test.tsx b/packages/react-components/react-combobox/library/src/components/Combobox/Combobox.test.tsx
index bb9054a841c8a3..defae58103af28 100644
--- a/packages/react-components/react-combobox/library/src/components/Combobox/Combobox.test.tsx
+++ b/packages/react-components/react-combobox/library/src/components/Combobox/Combobox.test.tsx
@@ -8,6 +8,7 @@ import { isConformant } from '../../testing/isConformant';
import { resetIdsForTests } from '@fluentui/react-utilities';
import { comboboxClassNames } from './useComboboxStyles.styles';
import type { ComboboxProps } from '@fluentui/react-combobox';
+import type { ActiveDescendantImperativeRef } from '@fluentui/react-aria';
describe('Combobox', () => {
beforeEach(() => {
@@ -1197,4 +1198,26 @@ describe('Combobox', () => {
expect(container.querySelector(`.${comboboxClassNames.expandIcon}`)).not.toBeInTheDocument();
});
});
+
+ describe('activeDescendantImperativeRef', () => {
+ it('passes activeDescendantImperativeRef through to useActiveDescendant', () => {
+ const imperativeRef = React.createRef();
+
+ const { getByRole } = render(
+
+
+
+
+ ,
+ );
+
+ const firstId = imperativeRef.current?.first();
+ const lastId = imperativeRef.current?.last();
+
+ expect(firstId).toBeTruthy();
+ expect(lastId).toBeTruthy();
+ expect(lastId).not.toBe(firstId);
+ expect(getByRole('combobox').getAttribute('aria-activedescendant')).toBe(lastId);
+ });
+ });
});
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 3b133a321a05e8..2fa5ac1803ca63 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
@@ -46,6 +46,7 @@ export const useComboboxBase_unstable = (
controller: activeDescendantController,
} = useActiveDescendant({
matchOption: isComboboxOptionElement,
+ imperativeRef: props.activeDescendantImperativeRef,
});
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/Dropdown.test.tsx b/packages/react-components/react-combobox/library/src/components/Dropdown/Dropdown.test.tsx
index 3897440821022c..b73debba41e909 100644
--- a/packages/react-components/react-combobox/library/src/components/Dropdown/Dropdown.test.tsx
+++ b/packages/react-components/react-combobox/library/src/components/Dropdown/Dropdown.test.tsx
@@ -8,6 +8,7 @@ import { isConformant } from '../../testing/isConformant';
import { resetIdsForTests } from '@fluentui/react-utilities';
import { dropdownClassNames } from './useDropdownStyles.styles';
import type { DropdownProps } from '@fluentui/react-combobox';
+import type { ActiveDescendantImperativeRef } from '@fluentui/react-aria';
describe('Dropdown', () => {
beforeEach(() => {
@@ -807,4 +808,26 @@ describe('Dropdown', () => {
expect(activeOptionText).toBe('Red');
});
});
+
+ describe('activeDescendantImperativeRef', () => {
+ it('passes activeDescendantImperativeRef through to useActiveDescendant', () => {
+ const imperativeRef = React.createRef();
+
+ const { getByRole } = render(
+
+
+
+
+ ,
+ );
+
+ const firstId = imperativeRef.current?.first();
+ const lastId = imperativeRef.current?.last();
+
+ expect(firstId).toBeTruthy();
+ expect(lastId).toBeTruthy();
+ expect(lastId).not.toBe(firstId);
+ expect(getByRole('combobox').getAttribute('aria-activedescendant')).toBe(lastId);
+ });
+ });
});
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 eba6fd354d89b9..c581bd3c343743 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
@@ -40,6 +40,7 @@ export const useDropdownBase_unstable = (
controller: activeDescendantController,
} = useActiveDescendant({
matchOption: isComboboxOptionElement,
+ imperativeRef: props.activeDescendantImperativeRef,
});
const dropdownInternalState = useComboboxBaseState({ ...props, activeDescendantController, freeform: false });
diff --git a/packages/react-components/react-combobox/library/src/components/Listbox/Listbox.test.tsx b/packages/react-components/react-combobox/library/src/components/Listbox/Listbox.test.tsx
index 2c5b2edd8c56e4..855e0d066d2502 100644
--- a/packages/react-components/react-combobox/library/src/components/Listbox/Listbox.test.tsx
+++ b/packages/react-components/react-combobox/library/src/components/Listbox/Listbox.test.tsx
@@ -3,6 +3,7 @@ import { fireEvent, render } from '@testing-library/react';
import { Listbox } from './Listbox';
import { Option } from '../Option/index';
import { isConformant } from '../../testing/isConformant';
+import type { ActiveDescendantImperativeRef } from '@fluentui/react-aria';
describe('Listbox', () => {
isConformant({
@@ -376,4 +377,26 @@ describe('Listbox', () => {
expect(getByTestId('red').getAttribute('aria-selected')).toEqual('true');
expect(onOptionSelect).toHaveBeenCalled();
});
+
+ describe('activeDescendantImperativeRef', () => {
+ it('passes activeDescendantImperativeRef through to useActiveDescendant', () => {
+ const imperativeRef = React.createRef();
+
+ const { getByRole } = render(
+
+
+
+
+ ,
+ );
+
+ const firstId = imperativeRef.current?.first();
+ const lastId = imperativeRef.current?.last();
+
+ expect(firstId).toBeTruthy();
+ expect(lastId).toBeTruthy();
+ expect(lastId).not.toBe(firstId);
+ expect(getByRole('listbox').getAttribute('aria-activedescendant')).toBe(lastId);
+ });
+ });
});
diff --git a/packages/react-components/react-combobox/library/src/components/Listbox/Listbox.types.ts b/packages/react-components/react-combobox/library/src/components/Listbox/Listbox.types.ts
index db505cbd6a0ece..c06759f313ed6a 100644
--- a/packages/react-components/react-combobox/library/src/components/Listbox/Listbox.types.ts
+++ b/packages/react-components/react-combobox/library/src/components/Listbox/Listbox.types.ts
@@ -1,3 +1,4 @@
+import type * as React from 'react';
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
import type {
ActiveDescendantChangeEvent,
@@ -24,6 +25,13 @@ export type ListboxProps = ComponentProps &
* @default false
*/
disableAutoFocus?: boolean;
+
+ /**
+ * Imperative ref that lets you manually control the active descendant associated
+ * with the Listbox. Typical use case for this is if you need to programmatically
+ * focus an active descendant.
+ */
+ activeDescendantImperativeRef?: React.RefObject;
};
/**
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 1cf0067c484999..083023b2afa9db 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
@@ -40,7 +40,7 @@ const UNSAFE_noLongerUsed = {
* @param ref - reference to root HTMLElement of Listbox
*/
export const useListbox_unstable = (props: ListboxProps, ref: React.Ref): ListboxState => {
- const { multiselect, disableAutoFocus = false } = props;
+ const { multiselect, disableAutoFocus = false, activeDescendantImperativeRef } = props;
const optionCollection = useOptionCollection();
const {
@@ -49,6 +49,7 @@ export const useListbox_unstable = (props: ListboxProps, ref: React.Ref({
matchOption: isComboboxOptionElement,
+ imperativeRef: activeDescendantImperativeRef,
});
const hasListboxContext = useHasParentContext(ListboxContext);
diff --git a/packages/react-components/react-combobox/library/src/utils/ComboboxBase.types.ts b/packages/react-components/react-combobox/library/src/utils/ComboboxBase.types.ts
index d700c6100a9d47..0be184f3bf5155 100644
--- a/packages/react-components/react-combobox/library/src/utils/ComboboxBase.types.ts
+++ b/packages/react-components/react-combobox/library/src/utils/ComboboxBase.types.ts
@@ -1,5 +1,9 @@
import type * as React from 'react';
-import type { ActiveDescendantChangeEvent, ActiveDescendantContextValue } from '@fluentui/react-aria';
+import type {
+ ActiveDescendantChangeEvent,
+ ActiveDescendantContextValue,
+ ActiveDescendantImperativeRef,
+} from '@fluentui/react-aria';
import type { PositioningShorthand } from '@fluentui/react-positioning';
import type { EventData, EventHandler } from '@fluentui/react-utilities';
import type { ComboboxContextValue } from '../contexts/ComboboxContext';
@@ -86,6 +90,13 @@ export type ComboboxBaseProps = SelectionProps &
* Use this with `onOptionSelect` to directly control the displayed value string
*/
value?: string;
+
+ /**
+ * Imperative ref that lets you manually control the active descendant associated
+ * with the Combobox. Typical use case for this is if you need to programmatically
+ * focus an active descendant.
+ */
+ activeDescendantImperativeRef?: React.RefObject;
};
/**