diff --git a/packages/@react-aria/autocomplete/src/useAutocomplete.ts b/packages/@react-aria/autocomplete/src/useAutocomplete.ts index 9a52f592130..10143d25e5d 100644 --- a/packages/@react-aria/autocomplete/src/useAutocomplete.ts +++ b/packages/@react-aria/autocomplete/src/useAutocomplete.ts @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {AriaLabelingProps, BaseEvent, DOMProps, Node, RefObject} from '@react-types/shared'; +import {AriaLabelingProps, BaseEvent, DOMProps, FocusableElement, Node, RefObject} from '@react-types/shared'; import {AriaTextFieldProps} from '@react-aria/textfield'; import {AutocompleteProps, AutocompleteState} from '@react-stately/autocomplete'; import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, getActiveElement, getOwnerDocument, isCtrlKeyPressed, mergeProps, mergeRefs, useEffectEvent, useEvent, useLabels, useObjectRef, useSlotId} from '@react-aria/utils'; @@ -40,7 +40,12 @@ export interface AriaAutocompleteProps extends AutocompleteProps { * Whether or not to focus the first item in the collection after a filter is performed. * @default false */ - disableAutoFocusFirst?: boolean + disableAutoFocusFirst?: boolean, + + /** + * Whether the autocomplete should disable virtual focus, instead making the wrapped collection directly tabbable. + */ + disallowVirtualFocus?: boolean } export interface AriaAutocompleteOptions extends Omit, 'children'> { @@ -52,7 +57,7 @@ export interface AriaAutocompleteOptions extends Omit { /** Props for the autocomplete textfield/searchfield element. These should be passed to the textfield/searchfield aria hooks respectively. */ - textFieldProps: AriaTextFieldProps, + textFieldProps: AriaTextFieldProps, /** Props for the collection, to be passed to collection's respective aria hook (e.g. useMenu). */ collectionProps: CollectionOptions, /** Ref to attach to the wrapped collection. */ @@ -72,7 +77,8 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Aut inputRef, collectionRef, filter, - disableAutoFocusFirst = false + disableAutoFocusFirst = false, + disallowVirtualFocus = false } = props; let collectionId = useSlotId(); @@ -83,7 +89,7 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Aut // For mobile screen readers, we don't want virtual focus, instead opting to disable FocusScope's restoreFocus and manually // moving focus back to the subtriggers - let shouldUseVirtualFocus = getInteractionModality() !== 'virtual'; + let shouldUseVirtualFocus = getInteractionModality() !== 'virtual' && !disallowVirtualFocus; useEffect(() => { return () => clearTimeout(timeout.current); @@ -254,15 +260,17 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Aut } let shouldPerformDefaultAction = true; - if (focusedNodeId == null) { - shouldPerformDefaultAction = collectionRef.current?.dispatchEvent( - new KeyboardEvent(e.nativeEvent.type, e.nativeEvent) - ) || false; - } else { - let item = document.getElementById(focusedNodeId); - shouldPerformDefaultAction = item?.dispatchEvent( - new KeyboardEvent(e.nativeEvent.type, e.nativeEvent) - ) || false; + if (collectionRef.current !== null) { + if (focusedNodeId == null) { + shouldPerformDefaultAction = collectionRef.current?.dispatchEvent( + new KeyboardEvent(e.nativeEvent.type, e.nativeEvent) + ) || false; + } else { + let item = document.getElementById(focusedNodeId); + shouldPerformDefaultAction = item?.dispatchEvent( + new KeyboardEvent(e.nativeEvent.type, e.nativeEvent) + ) || false; + } } if (shouldPerformDefaultAction) { @@ -282,6 +290,9 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Aut } break; } + } else { + // TODO: check if we can do this, want to stop textArea from using its default Enter behavior so items are properly triggered + e.preventDefault(); } }; @@ -359,25 +370,28 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Aut let textFieldProps = { value: state.inputValue, onChange - } as AriaTextFieldProps; + } as AriaTextFieldProps; + + let virtualFocusProps = { + onKeyDown, + 'aria-activedescendant': state.focusedNodeId ?? undefined, + onBlur, + onFocus + }; if (collectionId) { textFieldProps = { ...textFieldProps, - onKeyDown, - autoComplete: 'off', - 'aria-haspopup': collectionId ? 'listbox' : undefined, + ...(shouldUseVirtualFocus && virtualFocusProps), + enterKeyHint: 'go', 'aria-controls': collectionId, // TODO: readd proper logic for completionMode = complete (aria-autocomplete: both) 'aria-autocomplete': 'list', - 'aria-activedescendant': state.focusedNodeId ?? undefined, // This disable's iOS's autocorrect suggestions, since the autocomplete provides its own suggestions. autoCorrect: 'off', // This disable's the macOS Safari spell check auto corrections. spellCheck: 'false', - enterKeyHint: 'go', - onBlur, - onFocus + autoComplete: 'off' }; } diff --git a/packages/react-aria-components/package.json b/packages/react-aria-components/package.json index c986fbd2af3..51c6ad6e953 100644 --- a/packages/react-aria-components/package.json +++ b/packages/react-aria-components/package.json @@ -51,6 +51,7 @@ "@react-aria/live-announcer": "^3.4.4", "@react-aria/overlays": "^3.28.0", "@react-aria/ssr": "^3.9.10", + "@react-aria/textfield": "^3.18.0", "@react-aria/toolbar": "3.0.0-beta.19", "@react-aria/utils": "^3.30.0", "@react-aria/virtualizer": "^4.1.8", diff --git a/packages/react-aria-components/src/Autocomplete.tsx b/packages/react-aria-components/src/Autocomplete.tsx index 05e40294c0c..6c800a21f06 100644 --- a/packages/react-aria-components/src/Autocomplete.tsx +++ b/packages/react-aria-components/src/Autocomplete.tsx @@ -10,29 +10,44 @@ * governing permissions and limitations under the License. */ -import {AriaAutocompleteProps, CollectionOptions, useAutocomplete} from '@react-aria/autocomplete'; +import {AriaAutocompleteProps, useAutocomplete} from '@react-aria/autocomplete'; +import {AriaLabelingProps, DOMProps, FocusableElement, FocusEvents, KeyboardEvents, Node, ValueBase} from '@react-types/shared'; +import {AriaTextFieldProps} from '@react-aria/textfield'; import {AutocompleteState, useAutocompleteState} from '@react-stately/autocomplete'; -import {InputContext} from './Input'; +import {ContextValue, Provider, removeDataAttributes, SlotProps, SlottedContextValue, useSlottedContext} from './utils'; import {mergeProps} from '@react-aria/utils'; -import {Node} from '@react-types/shared'; -import {Provider, removeDataAttributes, SlotProps, SlottedContextValue, useSlottedContext} from './utils'; -import React, {createContext, JSX, RefObject, useRef} from 'react'; -import {SearchFieldContext} from './SearchField'; -import {TextFieldContext} from './TextField'; +import React, {createContext, JSX, useRef} from 'react'; export interface AutocompleteProps extends AriaAutocompleteProps, SlotProps {} -interface InternalAutocompleteContextValue { +// TODO: naming +// IMO I think this could also contain the props that useSelectableCollection takes (minus the selection options?) +interface CollectionContextValue extends DOMProps, AriaLabelingProps { filter?: (nodeTextValue: string, node: Node) => boolean, - collectionProps: CollectionOptions, - collectionRef: RefObject + /** Whether the collection items should use virtual focus instead of being focused directly. */ + shouldUseVirtualFocus?: boolean, + /** Whether typeahead is disabled. */ + disallowTypeAhead?: boolean } +// TODO: naming +interface FieldInputContextValue extends + DOMProps, + FocusEvents, + KeyboardEvents, + Pick, 'onChange' | 'value'>, + Pick {} + export const AutocompleteContext = createContext>>>(null); export const AutocompleteStateContext = createContext(null); -// This context is to pass the register and filter down to whatever collection component is wrapped by the Autocomplete -// TODO: export from RAC, but rename to something more appropriate -export const UNSTABLE_InternalAutocompleteContext = createContext | null>(null); + +// TODO export from RAC, maybe move up and out of Autocomplete +// also can't make this use ContextValue (so that we can call useContextProps) like FieldInput for a similar reason. The HTMLElement type for the ref +// makes useContextProps complain since it doesn't mesh up with HTMLDivElement +export const CollectionContext = createContext, HTMLElement>>(null); +// TODO: too restrictive to type this as a HTMLInputElement? Needed for the ref merging that happens in TextField/SearchField +// Attempted to use FocusableElement but as mentioned above, SearchField and TextField complain since they expect HTMLInputElement for their hooks and stuff +export const FieldInputContext = createContext>(null); /** * An autocomplete combines a TextField or SearchField with a Menu or ListBox, allowing users to search or filter a list of suggestions. @@ -61,13 +76,14 @@ export function Autocomplete(props: AutocompleteProps): JSX ) => boolean, - collectionProps, - collectionRef: mergedCollectionRef + [FieldInputContext, { + ...textFieldProps, + ref: inputRef + }], + [CollectionContext, { + ...collectionProps, + filter: filterFn, + ref: mergedCollectionRef }] ]}> {props.children} diff --git a/packages/react-aria-components/src/GridList.tsx b/packages/react-aria-components/src/GridList.tsx index c47ca47edc8..aa488f4ad08 100644 --- a/packages/react-aria-components/src/GridList.tsx +++ b/packages/react-aria-components/src/GridList.tsx @@ -13,6 +13,7 @@ import {AriaGridListProps, DraggableItemResult, DragPreviewRenderer, DropIndicat import {ButtonContext} from './Button'; import {CheckboxContext} from './RSPContexts'; import {Collection, CollectionBuilder, createLeafComponent, FilterLessNode, ItemNode} from '@react-aria/collections'; +import {CollectionContext, FieldInputContext} from './Autocomplete'; import {CollectionProps, CollectionRendererContext, DefaultCollectionRenderer, ItemRenderProps} from './Collection'; import {ContextValue, DEFAULT_SLOT, Provider, RenderProps, SlotProps, StyleProps, StyleRenderProps, useContextProps, useRenderProps} from './utils'; import {DragAndDropContext, DropIndicatorContext, DropIndicatorProps, useDndPersistedKeys, useRenderDropIndicator} from './DragAndDrop'; @@ -23,7 +24,6 @@ import {forwardRefType, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, Pre import {ListStateContext} from './ListBox'; import React, {createContext, ForwardedRef, forwardRef, HTMLAttributes, JSX, ReactNode, useContext, useEffect, useMemo, useRef} from 'react'; import {TextContext} from './Text'; -import {UNSTABLE_InternalAutocompleteContext} from './Autocomplete'; export interface GridListRenderProps { /** @@ -106,7 +106,9 @@ interface GridListInnerProps { function GridListInner({props, collection, gridListRef: ref}: GridListInnerProps) { // TODO: for now, don't grab collection ref and collectionProps from the autocomplete, rely on the user tabbing to the gridlist // figure out if we want to support virtual focus for grids when wrapped in an autocomplete - let {filter, collectionProps} = useContext(UNSTABLE_InternalAutocompleteContext) || {}; + let contextProps; + [contextProps] = useContextProps({}, null, CollectionContext); + let {filter, ...collectionProps} = contextProps; // eslint-disable-next-line @typescript-eslint/no-unused-vars let {shouldUseVirtualFocus, disallowTypeAhead, ...DOMCollectionProps} = collectionProps || {}; let {dragAndDropHooks, keyboardNavigationBehavior = 'arrow', layout = 'stack'} = props; @@ -422,7 +424,9 @@ export const GridListItem = /*#__PURE__*/ createLeafComponent(GridListNode, func } }], [CollectionRendererContext, DefaultCollectionRenderer], - [ListStateContext, null] + [ListStateContext, null], + [CollectionContext, null], + [FieldInputContext, null] ]}> {renderProps.children} diff --git a/packages/react-aria-components/src/ListBox.tsx b/packages/react-aria-components/src/ListBox.tsx index e923ece946e..5de95b0ec16 100644 --- a/packages/react-aria-components/src/ListBox.tsx +++ b/packages/react-aria-components/src/ListBox.tsx @@ -12,18 +12,18 @@ import {AriaListBoxOptions, AriaListBoxProps, DraggableItemResult, DragPreviewRenderer, DroppableCollectionResult, DroppableItemResult, FocusScope, ListKeyboardDelegate, mergeProps, useCollator, useFocusRing, useHover, useListBox, useListBoxSection, useLocale, useOption} from 'react-aria'; import {Collection, CollectionBuilder, createBranchComponent, createLeafComponent, FilterLessNode, ItemNode, SectionNode} from '@react-aria/collections'; +import {CollectionContext} from './Autocomplete'; import {CollectionProps, CollectionRendererContext, ItemRenderProps, SectionContext, SectionProps} from './Collection'; import {ContextValue, DEFAULT_SLOT, Provider, RenderProps, SlotProps, StyleProps, StyleRenderProps, useContextProps, useRenderProps, useSlot} from './utils'; import {DragAndDropContext, DropIndicatorContext, DropIndicatorProps, useDndPersistedKeys, useRenderDropIndicator} from './DragAndDrop'; import {DragAndDropHooks} from './useDragAndDrop'; import {DraggableCollectionState, DroppableCollectionState, ListState, Node, Orientation, SelectionBehavior, UNSTABLE_useFilteredListState, useListState} from 'react-stately'; -import {filterDOMProps, inertValue, LoadMoreSentinelProps, mergeRefs, useLoadMoreSentinel, useObjectRef} from '@react-aria/utils'; +import {filterDOMProps, inertValue, LoadMoreSentinelProps, useLoadMoreSentinel, useObjectRef} from '@react-aria/utils'; import {forwardRefType, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, PressEvents, RefObject} from '@react-types/shared'; import {HeaderContext} from './Header'; import React, {createContext, ForwardedRef, forwardRef, JSX, ReactNode, useContext, useEffect, useMemo, useRef} from 'react'; import {SeparatorContext} from './Separator'; import {TextContext} from './Text'; -import {UNSTABLE_InternalAutocompleteContext} from './Autocomplete'; export interface ListBoxRenderProps { /** @@ -120,11 +120,14 @@ interface ListBoxInnerProps { } function ListBoxInner({state: inputState, props, listBoxRef}: ListBoxInnerProps) { - let {filter, collectionProps, collectionRef} = useContext(UNSTABLE_InternalAutocompleteContext) || {}; + // TODO: bit silly that I have to do this, alternative is to use "props" in useContextProps and update the ListBoxInnerProps type to + // include filter + let contextProps; + // TODO: still had to use unknown here to stop typescript from complaining about the difference in the ref type difference between the menu ref and the more generic ref from the context + [contextProps, listBoxRef as unknown] = useContextProps({}, listBoxRef, CollectionContext); + let {filter, ...collectionProps} = contextProps; props = useMemo(() => collectionProps ? ({...props, ...collectionProps}) : props, [props, collectionProps]); let {dragAndDropHooks, layout = 'stack', orientation = 'vertical'} = props; - // Memoed so that useAutocomplete callback ref is properly only called once on mount and not everytime a rerender happens - listBoxRef = useObjectRef(useMemo(() => mergeRefs(listBoxRef, collectionRef !== undefined ? collectionRef as RefObject : null), [collectionRef, listBoxRef])); let state = UNSTABLE_useFilteredListState(inputState, filter); let {collection, selectionManager} = state; let isListDraggable = !!dragAndDropHooks?.useDraggableCollectionState; diff --git a/packages/react-aria-components/src/Menu.tsx b/packages/react-aria-components/src/Menu.tsx index 7068f5032ab..3d751e60c2b 100644 --- a/packages/react-aria-components/src/Menu.tsx +++ b/packages/react-aria-components/src/Menu.tsx @@ -13,9 +13,10 @@ import {AriaMenuProps, FocusScope, mergeProps, useHover, useMenu, useMenuItem, useMenuSection, useMenuTrigger, useSubmenuTrigger} from 'react-aria'; import {BaseCollection, Collection, CollectionBuilder, CollectionNode, createBranchComponent, createLeafComponent, ItemNode, SectionNode} from '@react-aria/collections'; import {MenuTriggerProps as BaseMenuTriggerProps, Collection as ICollection, Node, RootMenuTriggerState, TreeState, useMenuTriggerState, useSubmenuTriggerState, useTreeState} from 'react-stately'; +import {CollectionContext, FieldInputContext} from './Autocomplete'; import {CollectionProps, CollectionRendererContext, ItemRenderProps, SectionContext, SectionProps, usePersistedKeys} from './Collection'; import {ContextValue, DEFAULT_SLOT, Provider, RenderProps, SlotProps, StyleRenderProps, useContextProps, useRenderProps, useSlot, useSlottedContext} from './utils'; -import {filterDOMProps, mergeRefs, useObjectRef, useResizeObserver} from '@react-aria/utils'; +import {filterDOMProps, useObjectRef, useResizeObserver} from '@react-aria/utils'; import {FocusStrategy, forwardRefType, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, MultipleSelection, PressEvents} from '@react-types/shared'; import {HeaderContext} from './Header'; import {KeyboardContext} from './Keyboard'; @@ -39,7 +40,6 @@ import React, { } from 'react'; import {SeparatorContext} from './Separator'; import {TextContext} from './Text'; -import {UNSTABLE_InternalAutocompleteContext} from './Autocomplete'; export const MenuContext = createContext, HTMLDivElement>>(null); export const MenuStateContext = createContext | null>(null); @@ -202,9 +202,12 @@ interface MenuInnerProps { } function MenuInner({props, collection, menuRef: ref}: MenuInnerProps) { - let {filter, collectionProps: autocompleteMenuProps, collectionRef} = useContext(UNSTABLE_InternalAutocompleteContext) || {}; - // Memoed so that useAutocomplete callback ref is properly only called once on mount and not everytime a rerender happens - ref = useObjectRef(useMemo(() => mergeRefs(ref, collectionRef !== undefined ? collectionRef as RefObject : null), [collectionRef, ref])); + // TODO: bit silly that I have to do this, alternative is to use "props" in useContextProps and update the MenuInnerProps type to + // include filter + let contextProps; + // TODO: still had to use unknown here to stop typescript from complaining about the ref type difference between the menu ref and the more generic ref from the context + [contextProps, ref as unknown] = useContextProps({}, ref, CollectionContext); + let {filter, ...autocompleteMenuProps} = contextProps; let filteredCollection = useMemo(() => filter ? collection.filter(filter) : collection, [collection, filter]); let state = useTreeState({ ...props, @@ -251,7 +254,8 @@ function MenuInner({props, collection, menuRef: ref}: MenuInne [SectionContext, {name: 'MenuSection', render: MenuSectionInner}], [SubmenuTriggerContext, {parentMenuRef: ref, shouldUseVirtualFocus: autocompleteMenuProps?.shouldUseVirtualFocus}], [MenuItemContext, null], - [UNSTABLE_InternalAutocompleteContext, null], + [CollectionContext, null], + [FieldInputContext, null], [SelectionManagerContext, state.selectionManager], /* Ensure root MenuTriggerState is defined, in case Menu is rendered outside a MenuTrigger. */ /* We assume the context can never change between defined and undefined. */ diff --git a/packages/react-aria-components/src/SearchField.tsx b/packages/react-aria-components/src/SearchField.tsx index 3695385688d..8e209e7e560 100644 --- a/packages/react-aria-components/src/SearchField.tsx +++ b/packages/react-aria-components/src/SearchField.tsx @@ -15,7 +15,8 @@ import {ButtonContext} from './Button'; import {ContextValue, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot, useSlottedContext} from './utils'; import {createHideableComponent} from '@react-aria/collections'; import {FieldErrorContext} from './FieldError'; -import {filterDOMProps, mergeProps} from '@react-aria/utils'; +import {FieldInputContext} from './Autocomplete'; +import {filterDOMProps} from '@react-aria/utils'; import {FormContext} from './Form'; import {GlobalDOMAttributes} from '@react-types/shared'; import {GroupContext} from './Group'; @@ -59,7 +60,7 @@ export const SearchField = /*#__PURE__*/ createHideableComponent(function Search let {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {}; let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native'; let inputRef = useRef(null); - let [inputContextProps, mergedInputRef] = useContextProps({}, inputRef, InputContext); + [props, inputRef as unknown] = useContextProps(props, inputRef, FieldInputContext); let [labelRef, label] = useSlot( !props['aria-label'] && !props['aria-labelledby'] ); @@ -72,7 +73,7 @@ export const SearchField = /*#__PURE__*/ createHideableComponent(function Search ...removeDataAttributes(props), label, validationBehavior - }, state, mergedInputRef); + }, state, inputRef); let renderProps = useRenderProps({ ...props, @@ -100,7 +101,7 @@ export const SearchField = /*#__PURE__*/ createHideableComponent(function Search extends BaseCollection implements ITableCollection { headerRows: GridNode[] = []; @@ -371,7 +371,9 @@ interface TableInnerProps { function TableInner({props, forwardedRef: ref, selectionState, collection}: TableInnerProps) { - let {filter, collectionProps} = useContext(UNSTABLE_InternalAutocompleteContext) || {}; + let contextProps; + [contextProps] = useContextProps({}, null, CollectionContext); + let {filter, ...collectionProps} = contextProps; // eslint-disable-next-line @typescript-eslint/no-unused-vars let {shouldUseVirtualFocus, disallowTypeAhead, ...DOMCollectionProps} = collectionProps || {}; let tableContainerContext = useContext(ResizableTableContainerContext); @@ -491,7 +493,9 @@ function TableInner({props, forwardedRef: ref, selectionState, collection}: Tabl [TableStateContext, filteredState], [TableColumnResizeStateContext, layoutState], [DragAndDropContext, {dragAndDropHooks, dragState, dropState}], - [DropIndicatorContext, {render: TableDropIndicatorWrapper}] + [DropIndicatorContext, {render: TableDropIndicatorWrapper}], + [CollectionContext, null], + [FieldInputContext, null] ]}> , 'children' | 'items' | 'label' | 'description' | 'errorMessage' | 'keyboardDelegate'>, DOMProps, SlotProps, GlobalDOMAttributes {} @@ -75,7 +75,9 @@ interface TagGroupInnerProps { } function TagGroupInner({props, forwardedRef: ref, collection}: TagGroupInnerProps) { - let {filter, collectionProps} = useContext(UNSTABLE_InternalAutocompleteContext) || {}; + let contextProps; + [contextProps] = useContextProps({}, null, CollectionContext); + let {filter, ...collectionProps} = contextProps; // eslint-disable-next-line @typescript-eslint/no-unused-vars let {shouldUseVirtualFocus, disallowTypeAhead, ...DOMCollectionProps} = collectionProps || {}; let tagListRef = useRef(null); diff --git a/packages/react-aria-components/src/TextField.tsx b/packages/react-aria-components/src/TextField.tsx index 1f80249ca5a..f5af026fd00 100644 --- a/packages/react-aria-components/src/TextField.tsx +++ b/packages/react-aria-components/src/TextField.tsx @@ -14,7 +14,8 @@ import {AriaTextFieldProps, useTextField} from 'react-aria'; import {ContextValue, DOMProps, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot, useSlottedContext} from './utils'; import {createHideableComponent} from '@react-aria/collections'; import {FieldErrorContext} from './FieldError'; -import {filterDOMProps, mergeProps} from '@react-aria/utils'; +import {FieldInputContext} from './Autocomplete'; +import {filterDOMProps} from '@react-aria/utils'; import {FormContext} from './Form'; import {GlobalDOMAttributes} from '@react-types/shared'; import {GroupContext} from './Group'; @@ -61,8 +62,8 @@ export const TextField = /*#__PURE__*/ createHideableComponent(function TextFiel [props, ref] = useContextProps(props, ref, TextFieldContext); let {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {}; let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native'; - let inputRef = useRef(null); - let [inputContextProps, mergedInputRef] = useContextProps({}, inputRef, InputContext); + let inputRef = useRef(null); + [props, inputRef as unknown] = useContextProps(props, inputRef, FieldInputContext); let [labelRef, label] = useSlot( !props['aria-label'] && !props['aria-labelledby'] ); @@ -72,16 +73,16 @@ export const TextField = /*#__PURE__*/ createHideableComponent(function TextFiel inputElementType, label, validationBehavior - }, mergedInputRef); + }, inputRef); // Intercept setting the input ref so we can determine what kind of element we have. // useTextField uses this to determine what props to include. let inputOrTextAreaRef = useCallback((el) => { - mergedInputRef.current = el; + inputRef.current = el; if (el) { setInputElementType(el instanceof HTMLTextAreaElement ? 'textarea' : 'input'); } - }, [mergedInputRef]); + }, [inputRef]); let renderProps = useRenderProps({ ...props, @@ -110,7 +111,7 @@ export const TextField = /*#__PURE__*/ createHideableComponent(function TextFiel { return ( - +
@@ -144,7 +145,7 @@ export const AutocompleteExample: AutocompleteStory = { export const AutocompleteSearchfield: AutocompleteStory = { render: (args) => { return ( - +
@@ -304,7 +305,7 @@ export const AutocompleteMenuDynamic: AutocompleteStory = { return ( <> - +
@@ -326,7 +327,7 @@ export const AutocompleteMenuDynamic: AutocompleteStory = { export const AutocompleteOnActionOnMenuItems: AutocompleteStory = { render: (args) => { return ( - +
@@ -355,7 +356,7 @@ let items: AutocompleteItem[] = [{id: '1', name: 'Foo'}, {id: '2', name: 'Bar'}, export const AutocompleteDisabledKeys: AutocompleteStory = { render: (args) => { return ( - +
@@ -395,14 +396,14 @@ const AsyncExample = (args: any): React.ReactElement => { }; } }); - let {onSelectionChange, selectionMode, includeLoadState, escapeKeyBehavior} = args; + let {onSelectionChange, selectionMode, includeLoadState, escapeKeyBehavior, disallowVirtualFocus} = args; let renderEmptyState; if (includeLoadState) { renderEmptyState = list.isLoading ? () => 'Loading' : () => 'No results found.'; } return ( - +
@@ -440,7 +441,7 @@ const CaseSensitiveFilter = (args) => { let defaultFilter = (itemText, input) => contains(itemText, input); return ( - +
@@ -479,7 +480,7 @@ export const AutocompleteWithListbox: AutocompleteStory = { height: 250 }}> {() => ( - +
@@ -556,7 +557,7 @@ export const AutocompleteWithVirtualizedListbox: AutocompleteStory = { height: 250 }}> {() => ( - +
@@ -903,7 +904,7 @@ export const AutocompleteWithAsyncListBox = (args) => { }); return ( - +
@@ -1101,7 +1102,7 @@ function AutocompleteNodeFiltering(args) { }; return ( - +
@@ -1125,3 +1126,66 @@ export const AutocompletePreserveFirstSectionStory: AutocompleteStory = { } } }; + + +let names = [ + {id: 1, name: 'David'}, + {id: 2, name: 'Sam'}, + {id: 3, name: 'Julia'} +]; + +const UserCustomFiltering = (args): React.ReactElement => { + let [value, setValue] = useState(''); + + let {contains} = useFilter({sensitivity: 'base'}); + + + let filter = (textValue, inputValue) => { + let index = inputValue.lastIndexOf('@'); + let filterText = ''; + if (index > -1) { + filterText = value.slice(index + 1); + } + + return contains(textValue, filterText); + }; + + let onAction = (key) => { + let index = value.lastIndexOf('@'); + if (index === -1) { + index = value.length; + } + let name = names.find(person => person.id === key)!.name; + setValue(value.slice(0, index).concat(name)); + }; + + return ( + +
+ + +