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(