diff --git a/.changeset/few-weeks-serve.md b/.changeset/few-weeks-serve.md new file mode 100644 index 00000000000..c910b97efa6 --- /dev/null +++ b/.changeset/few-weeks-serve.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +Add `openInFocus` prop (default: true) to `AutocompleteInput` diff --git a/packages/react/src/Autocomplete/Autocomplete.docs.json b/packages/react/src/Autocomplete/Autocomplete.docs.json index 39b19c9ce45..80da9258ba4 100644 --- a/packages/react/src/Autocomplete/Autocomplete.docs.json +++ b/packages/react/src/Autocomplete/Autocomplete.docs.json @@ -19,6 +19,12 @@ "name": "as", "type": "React.ElementType", "defaultValue": "TextInput" + }, + { + "name": "openOnFocus", + "type": "boolean", + "defaultValue": "true", + "description": "Whether the associated autocomplete menu should open on an input focus event" } ], "passthrough": { diff --git a/packages/react/src/Autocomplete/Autocomplete.stories.tsx b/packages/react/src/Autocomplete/Autocomplete.stories.tsx index 0ff949cb574..ed0fb56ddc9 100644 --- a/packages/react/src/Autocomplete/Autocomplete.stories.tsx +++ b/packages/react/src/Autocomplete/Autocomplete.stories.tsx @@ -29,6 +29,9 @@ const getArgsByChildComponent = ({ menuLoading, selectionVariant, + // Autocomplete.Input + openOnFocus, + // Autocomplete.Overlay anchorSide, height, @@ -65,6 +68,7 @@ any) => { } return { menuArgs: {emptyStateText, loading: menuLoading, selectionVariant}, + inputArgs: {openOnFocus}, overlayArgs: {anchorSide, height, maxHeight: overlayMaxHeight, width}, textInputArgs, textInputWithTokensArgs: { @@ -131,6 +135,7 @@ const autocompleteStoryMeta: Meta = { emptyStateText: 'No selectable options', menuLoading: false, selectionVariant: 'single', + openOnFocus: true, anchorSide: undefined, height: 'auto', overlayMaxHeight: undefined, @@ -161,6 +166,16 @@ const autocompleteStoryMeta: Meta = { }, }, + // Autocomplete.Input + openOnFocus: { + control: { + type: 'boolean', + }, + table: { + category: 'Autocomplete.Input', + }, + }, + // Autocomplete.Overlay anchorSide: { control: { @@ -217,7 +232,7 @@ const autocompleteStoryMeta: Meta = { export const Default = (args: FormControlArgs) => { const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args) - const {menuArgs, overlayArgs, textInputArgs} = getArgsByChildComponent(args) + const {menuArgs, inputArgs, overlayArgs, textInputArgs} = getArgsByChildComponent(args) const isMultiselect = menuArgs.selectionVariant === 'multiple' const [selectedItemIds, setSelectedItemIds] = useState>([]) const onSelectedChange = (newlySelectedItems: Datum | Datum[]) => { @@ -228,12 +243,19 @@ export const Default = (args: FormControlArgs) => { setSelectedItemIds(newlySelectedItems.map(item => item.id)) } + const autocompleteInput = {...inputArgs, ...textInputArgs} + const formValidationId = 'validation-field' return ( event.preventDefault()}> - + ) => { {captionArgs.children && } {validationArgs.children && validationArgs.variant && ( - + )} diff --git a/packages/react/src/Autocomplete/AutocompleteInput.tsx b/packages/react/src/Autocomplete/AutocompleteInput.tsx index 7cdb0469491..f114d881aee 100644 --- a/packages/react/src/Autocomplete/AutocompleteInput.tsx +++ b/packages/react/src/Autocomplete/AutocompleteInput.tsx @@ -10,11 +10,27 @@ import useSafeTimeout from '../hooks/useSafeTimeout' type InternalAutocompleteInputProps = { // eslint-disable-next-line @typescript-eslint/no-explicit-any as?: React.ComponentType> + // When false, the autocomplete menu will not render either on mouse click or + // keyboard focus. + openOnFocus?: boolean } +const ARROW_KEYS_NAV = new Set(['ArrowUp', 'ArrowDown']) + const AutocompleteInput = React.forwardRef( ( - {as: Component = TextInput, onFocus, onBlur, onChange, onKeyDown, onKeyUp, onKeyPress, value, ...props}, + { + as: Component = TextInput, + onFocus, + onBlur, + onChange, + onKeyDown, + onKeyUp, + onKeyPress, + value, + openOnFocus = true, + ...props + }, forwardedRef, ) => { const autocompleteContext = useContext(AutocompleteContext) @@ -38,10 +54,12 @@ const AutocompleteInput = React.forwardRef( const handleInputFocus: FocusEventHandler = useCallback( event => { - onFocus && onFocus(event) - setShowMenu(true) + if (openOnFocus) { + onFocus?.(event) + setShowMenu(true) + } }, - [onFocus, setShowMenu], + [onFocus, setShowMenu, openOnFocus], ) const handleInputBlur: FocusEventHandler = useCallback( @@ -83,8 +101,11 @@ const AutocompleteInput = React.forwardRef( setInputValue('') inputRef.current.value = '' } + if (!showMenu && ARROW_KEYS_NAV.has(event.key) && !event.altKey) { + setShowMenu(true) + } }, - [inputRef, setInputValue, setHighlightRemainingText, onKeyDown], + [inputRef, setInputValue, setHighlightRemainingText, onKeyDown, showMenu, setShowMenu], ) const handleInputKeyUp: KeyboardEventHandler = useCallback(