diff --git a/change/@fluentui-react-combobox-aa8ff159-8fa3-4a0b-acf2-8b9bad6c66b7.json b/change/@fluentui-react-combobox-aa8ff159-8fa3-4a0b-acf2-8b9bad6c66b7.json
new file mode 100644
index 0000000000000..02fbdfb85912e
--- /dev/null
+++ b/change/@fluentui-react-combobox-aa8ff159-8fa3-4a0b-acf2-8b9bad6c66b7.json
@@ -0,0 +1,7 @@
+{
+ "type": "minor",
+ "comment": "feat: allow space character insertion while typing in freeform combobox",
+ "packageName": "@fluentui/react-combobox",
+ "email": "sarah.higley@microsoft.com",
+ "dependentChangeType": "patch"
+}
diff --git a/packages/react-components/react-combobox/src/components/Combobox/Combobox.test.tsx b/packages/react-components/react-combobox/src/components/Combobox/Combobox.test.tsx
index 0eece7c10057d..0375144dfb9c4 100644
--- a/packages/react-components/react-combobox/src/components/Combobox/Combobox.test.tsx
+++ b/packages/react-components/react-combobox/src/components/Combobox/Combobox.test.tsx
@@ -662,6 +662,58 @@ describe('Combobox', () => {
});
});
+ /* Freeform space key behavior */
+ it('inserts space character when typing in a freeform combobox', () => {
+ const onOptionSelect = jest.fn();
+
+ const { getByRole } = render(
+
+
+
+
+ ,
+ );
+
+ userEvent.type(getByRole('combobox'), 're ');
+
+ expect(onOptionSelect).not.toHaveBeenCalled();
+ expect((getByRole('combobox') as HTMLInputElement).value).toEqual('re ');
+ });
+
+ it('uses space to select after arrowing through options in a freeform combobox', () => {
+ const onOptionSelect = jest.fn();
+
+ const { getByRole } = render(
+
+
+
+
+ ,
+ );
+
+ userEvent.type(getByRole('combobox'), 're{ArrowDown} ');
+
+ expect(onOptionSelect).toHaveBeenCalledTimes(1);
+ expect((getByRole('combobox') as HTMLInputElement).value).toEqual('Green');
+ });
+
+ it('inserts space character in closed freeform combobox', () => {
+ const onOptionSelect = jest.fn();
+
+ const { getByRole } = render(
+
+
+
+
+ ,
+ );
+
+ userEvent.type(getByRole('combobox'), 'r{ArrowDown}{Escape} ');
+
+ expect(onOptionSelect).not.toHaveBeenCalled();
+ expect((getByRole('combobox') as HTMLInputElement).value).toEqual('r ');
+ });
+
/* Active option */
it('should set active option on click', () => {
const { getByTestId } = render(
diff --git a/packages/react-components/react-combobox/src/components/Combobox/useCombobox.tsx b/packages/react-components/react-combobox/src/components/Combobox/useCombobox.tsx
index a9c6761cce3d2..43c181bfb3130 100644
--- a/packages/react-components/react-combobox/src/components/Combobox/useCombobox.tsx
+++ b/packages/react-components/react-combobox/src/components/Combobox/useCombobox.tsx
@@ -62,6 +62,10 @@ export const useCombobox_unstable = (props: ComboboxProps, ref: React.Ref();
React.useEffect(() => {
@@ -146,20 +150,6 @@ export const useCombobox_unstable = (props: ComboboxProps, ref: React.Ref) => {
- if (!open && getDropdownActionFromKey(ev) === 'Type') {
- baseState.setOpen(ev, true);
- }
-
- // clear activedescendant when moving the text insertion cursor
- if (ev.key === ArrowLeft || ev.key === ArrowRight) {
- setHideActiveDescendant(true);
- } else {
- setHideActiveDescendant(false);
- }
- };
-
// resolve input and listbox slot props
let triggerSlot: Slot<'input'>;
let listboxSlot: Slot | undefined;
@@ -174,9 +164,9 @@ export const useCombobox_unstable = (props: ComboboxProps, ref: React.Ref) => {
+ if (!open && getDropdownActionFromKey(ev) === 'Type') {
+ baseState.setOpen(ev, true);
+ }
+
+ // clear activedescendant when moving the text insertion cursor
+ if (ev.key === ArrowLeft || ev.key === ArrowRight) {
+ setHideActiveDescendant(true);
+ } else {
+ setHideActiveDescendant(false);
+ }
+
+ // update typing state to true if the user is typing
+ const action = getDropdownActionFromKey(ev, { open, multiselect });
+ if (action === 'Type') {
+ isTyping.current = true;
+ }
+ // otherwise, update the typing state to false if opening or navigating dropdown options
+ // other actions, like closing the dropdown, should not impact typing state.
+ else if (
+ (action === 'Open' && ev.key !== ' ') ||
+ action === 'Next' ||
+ action === 'Previous' ||
+ action === 'First' ||
+ action === 'Last' ||
+ action === 'PageUp' ||
+ action === 'PageDown'
+ ) {
+ isTyping.current = false;
+ }
+
+ // allow space to insert a character if freeform & the last action was typing, or if the popup is closed
+ if (freeform && (isTyping.current || !open) && ev.key === ' ') {
+ resolvedPropsOnKeyDown?.(ev);
+ return;
+ }
+
+ // if we're not allowing space to type, continue with default behavior
+ defaultOnTriggerKeyDown?.(ev);
+ });
+
/* handle open/close + focus change when clicking expandIcon */
const { onMouseDown: onIconMouseDown, onClick: onIconClick } = state.expandIcon || {};
const onExpandIconMouseDown = useEventCallback(