Skip to content

fix: (WIP) Autocomplete context refactor #8695

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 10 commits into
base: baseCollection_filter
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions packages/@react-aria/autocomplete/src/useAutocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,15 +254,17 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, 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) {
Expand All @@ -282,6 +284,9 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, 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();
}
};

Expand Down Expand Up @@ -366,7 +371,6 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
...textFieldProps,
onKeyDown,
autoComplete: 'off',
'aria-haspopup': collectionId ? 'listbox' : undefined,
'aria-controls': collectionId,
// TODO: readd proper logic for completionMode = complete (aria-autocomplete: both)
'aria-autocomplete': 'list',
Expand Down
1 change: 1 addition & 0 deletions packages/react-aria-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
53 changes: 35 additions & 18 deletions packages/react-aria-components/src/Autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,45 @@
* 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, 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';

export interface AutocompleteProps<T> extends AriaAutocompleteProps<T>, SlotProps {}

interface InternalAutocompleteContextValue<T> {
// TODO: naming
// IMO I think this could also contain the props that useSelectableCollection takes (minus the selection options?)
interface CollectionContextValue<T> extends DOMProps, AriaLabelingProps {
filter?: (nodeTextValue: string, node: Node<T>) => boolean,
collectionProps: CollectionOptions,
collectionRef: RefObject<HTMLElement | null>
/** Whether the collection items should use virtual focus instead of being focused directly. */
shouldUseVirtualFocus?: boolean,
/** Whether typeahead is disabled. */
disallowTypeAhead?: boolean,
collectionRef?: RefObject<HTMLElement | null>
}

// TODO: naming
interface FieldInputContextValue<T = HTMLInputElement> extends
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
interface FieldInputContextValue<T = HTMLInputElement> extends
interface AutocompleteInputContextValue<T = HTMLInputElement> extends

this seems more reasonable, this is specific to the Autocomplete, whereas the CollectionContext is specific to the Collections. Unless we had something in mind for this context that wasn't Autocomplete?

DOMProps,
FocusEvents<T>,
KeyboardEvents,
Pick<ValueBase<string>, 'onChange' | 'value'>,
Pick<AriaTextFieldProps, 'enterKeyHint' | 'aria-controls' | 'aria-autocomplete' | 'aria-activedescendant' | 'spellCheck' | 'autoCorrect' | 'autoComplete'> {}

export const AutocompleteContext = createContext<SlottedContextValue<Partial<AutocompleteProps<any>>>>(null);
export const AutocompleteStateContext = createContext<AutocompleteState | null>(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<InternalAutocompleteContextValue<any> | 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
Comment on lines +45 to +47
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, move up
what is the ts error? Maybe we can fix it. though is useContextProps the correct thing to use anyways? Would we ever expect these to be passed as actual props? or only through the context?

export const CollectionContext = createContext<CollectionContextValue<any> | null>(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<ContextValue<FieldInputContextValue, HTMLInputElement>>(null);
Comment on lines +45 to +51
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Types are the final problem here. The problem is two fold, ideally we'd be able to have CollectionContext use the ContextValue type so that we can merge the refs via useContextProps, but the collection element that will use that merged ref complain because HTMLElement doesn't overlap sufficiently with HTMLDivElement/etc.

For FieldInputContext, we'd ideally use a more general type than HTMLInputElement, but ContextValue doesn't take a generic and FocusableElement doesn't mesh with SearchField's HTMLInputElement ref type


/**
* An autocomplete combines a TextField or SearchField with a Menu or ListBox, allowing users to search or filter a list of suggestions.
Expand Down Expand Up @@ -61,12 +77,13 @@ export function Autocomplete<T extends object>(props: AutocompleteProps<T>): JSX
<Provider
values={[
[AutocompleteStateContext, state],
[SearchFieldContext, textFieldProps],
[TextFieldContext, textFieldProps],
[InputContext, {ref: inputRef}],
[UNSTABLE_InternalAutocompleteContext, {
filter: filterFn as (nodeTextValue: string, node: Node<T>) => boolean,
collectionProps,
[FieldInputContext, {
...textFieldProps,
ref: inputRef
}],
[CollectionContext, {
...collectionProps,
filter: filterFn,
collectionRef: mergedCollectionRef
}]
]}>
Expand Down
4 changes: 2 additions & 2 deletions packages/react-aria-components/src/GridList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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} 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';
Expand All @@ -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 {
/**
Expand Down Expand Up @@ -106,7 +106,7 @@ interface GridListInnerProps<T extends object> {
function GridListInner<T extends object>({props, collection, gridListRef: ref}: GridListInnerProps<T>) {
// 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 {filter, ...collectionProps} = useContext(CollectionContext) || {};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let {shouldUseVirtualFocus, disallowTypeAhead, ...DOMCollectionProps} = collectionProps || {};
let {dragAndDropHooks, keyboardNavigationBehavior = 'arrow', layout = 'stack'} = props;
Expand Down
4 changes: 2 additions & 2 deletions packages/react-aria-components/src/ListBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

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';
Expand All @@ -23,7 +24,6 @@ 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 {
/**
Expand Down Expand Up @@ -120,7 +120,7 @@ interface ListBoxInnerProps<T> {
}

function ListBoxInner<T extends object>({state: inputState, props, listBoxRef}: ListBoxInnerProps<T>) {
let {filter, collectionProps, collectionRef} = useContext(UNSTABLE_InternalAutocompleteContext) || {};
let {filter, collectionRef, ...collectionProps} = useContext(CollectionContext) || {};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we still using UNSTABLE_useFilteredListState as the name for this? or should these also be updated out of UNSTABLE?

not actually attached to this line, but it's nearby

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
Expand Down
6 changes: 3 additions & 3 deletions packages/react-aria-components/src/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
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} 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';
Expand All @@ -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<ContextValue<MenuProps<any>, HTMLDivElement>>(null);
export const MenuStateContext = createContext<TreeState<any> | null>(null);
Expand Down Expand Up @@ -202,7 +202,7 @@ interface MenuInnerProps<T> {
}

function MenuInner<T extends object>({props, collection, menuRef: ref}: MenuInnerProps<T>) {
let {filter, collectionProps: autocompleteMenuProps, collectionRef} = useContext(UNSTABLE_InternalAutocompleteContext) || {};
let {filter, collectionRef, ...autocompleteMenuProps} = useContext(CollectionContext) || {};
// 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<HTMLDivElement> : null), [collectionRef, ref]));
let filteredCollection = useMemo(() => filter ? collection.filter(filter) : collection, [collection, filter]);
Expand Down Expand Up @@ -251,7 +251,7 @@ function MenuInner<T extends object>({props, collection, menuRef: ref}: MenuInne
[SectionContext, {name: 'MenuSection', render: MenuSectionInner}],
[SubmenuTriggerContext, {parentMenuRef: ref, shouldUseVirtualFocus: autocompleteMenuProps?.shouldUseVirtualFocus}],
[MenuItemContext, null],
[UNSTABLE_InternalAutocompleteContext, null],
[CollectionContext, null],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should ListBox and GridList also clear this now that we support nested grids? ex TagGroup inside Cell?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should also clear the new input context

[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. */
Expand Down
9 changes: 5 additions & 4 deletions packages/react-aria-components/src/SearchField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<HTMLInputElement>(null);
let [inputContextProps, mergedInputRef] = useContextProps({}, inputRef, InputContext);
[props, inputRef] = useContextProps(props, inputRef, FieldInputContext);
let [labelRef, label] = useSlot(
!props['aria-label'] && !props['aria-labelledby']
);
Expand All @@ -72,7 +73,7 @@ export const SearchField = /*#__PURE__*/ createHideableComponent(function Search
...removeDataAttributes(props),
label,
validationBehavior
}, state, mergedInputRef);
}, state, inputRef);

let renderProps = useRenderProps({
...props,
Expand Down Expand Up @@ -100,7 +101,7 @@ export const SearchField = /*#__PURE__*/ createHideableComponent(function Search
<Provider
values={[
[LabelContext, {...labelProps, ref: labelRef}],
[InputContext, {...mergeProps(inputProps, inputContextProps), ref: mergedInputRef}],
[InputContext, {...inputProps, ref: inputRef}],
[ButtonContext, clearButtonProps],
[TextContext, {
slots: {
Expand Down
4 changes: 2 additions & 2 deletions packages/react-aria-components/src/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {BaseCollection, Collection, CollectionBuilder, CollectionNode, createBra
import {buildHeaderRows, TableColumnResizeState} from '@react-stately/table';
import {ButtonContext} from './Button';
import {CheckboxContext} from './RSPContexts';
import {CollectionContext} from './Autocomplete';
import {CollectionProps, CollectionRendererContext, DefaultCollectionRenderer, ItemRenderProps} from './Collection';
import {ColumnSize, ColumnStaticSize, TableCollection as ITableCollection, TableProps as SharedTableProps} from '@react-types/table';
import {ContextValue, DEFAULT_SLOT, DOMProps, Provider, RenderProps, SlotProps, StyleProps, StyleRenderProps, useContextProps, useRenderProps} from './utils';
Expand All @@ -16,7 +17,6 @@ import {GridNode} from '@react-types/grid';
import intlMessages from '../intl/*.json';
import React, {createContext, ForwardedRef, forwardRef, JSX, ReactElement, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
import ReactDOM from 'react-dom';
import {UNSTABLE_InternalAutocompleteContext} from './Autocomplete';

class TableCollection<T> extends BaseCollection<T> implements ITableCollection<T> {
headerRows: GridNode<T>[] = [];
Expand Down Expand Up @@ -371,7 +371,7 @@ interface TableInnerProps {


function TableInner({props, forwardedRef: ref, selectionState, collection}: TableInnerProps) {
let {filter, collectionProps} = useContext(UNSTABLE_InternalAutocompleteContext) || {};
let {filter, ...collectionProps} = useContext(CollectionContext) || {};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let {shouldUseVirtualFocus, disallowTypeAhead, ...DOMCollectionProps} = collectionProps || {};
let tableContainerContext = useContext(ResizableTableContainerContext);
Expand Down
4 changes: 2 additions & 2 deletions packages/react-aria-components/src/TagGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import {AriaTagGroupProps, useFocusRing, useHover, useTag, useTagGroup} from 'react-aria';
import {ButtonContext} from './Button';
import {Collection, CollectionBuilder, createLeafComponent, ItemNode} from '@react-aria/collections';
import {CollectionContext} from './Autocomplete';
import {CollectionProps, CollectionRendererContext, DefaultCollectionRenderer, ItemRenderProps, usePersistedKeys} from './Collection';
import {ContextValue, DOMProps, Provider, RenderProps, SlotProps, StyleRenderProps, useContextProps, useRenderProps, useSlot} from './utils';
import {filterDOMProps, mergeProps, useObjectRef} from '@react-aria/utils';
Expand All @@ -22,7 +23,6 @@ import {ListState, Node, UNSTABLE_useFilteredListState, useListState} from 'reac
import {ListStateContext} from './ListBox';
import React, {createContext, ForwardedRef, forwardRef, JSX, ReactNode, useContext, useEffect, useRef} from 'react';
import {TextContext} from './Text';
import {UNSTABLE_InternalAutocompleteContext} from './Autocomplete';

export interface TagGroupProps extends Omit<AriaTagGroupProps<unknown>, 'children' | 'items' | 'label' | 'description' | 'errorMessage' | 'keyboardDelegate'>, DOMProps, SlotProps, GlobalDOMAttributes<HTMLDivElement> {}

Expand Down Expand Up @@ -75,7 +75,7 @@ interface TagGroupInnerProps {
}

function TagGroupInner({props, forwardedRef: ref, collection}: TagGroupInnerProps) {
let {filter, collectionProps} = useContext(UNSTABLE_InternalAutocompleteContext) || {};
let {filter, ...collectionProps} = useContext(CollectionContext) || {};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let {shouldUseVirtualFocus, disallowTypeAhead, ...DOMCollectionProps} = collectionProps || {};
let tagListRef = useRef<HTMLDivElement>(null);
Expand Down
Loading