Skip to content
Merged
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
Copy link
Member

Choose a reason for hiding this comment

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

todo?

Copy link
Member Author

Choose a reason for hiding this comment

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

it seems to work, but I'm realizing with the grid virtual focus work that there are a ton of configurations to test haha

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?

Copy link
Member Author

Choose a reason for hiding this comment

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

From my understanding, the eventual intent for these contexts is that they aren't specifically tied to Autocomplete and simply contain values/attributes/properties that a input might have. It may not even be an input though, could be a text area or some other control hence the generic default.

The future use case is still a bit hazy though tbh

Copy link
Member

Choose a reason for hiding this comment

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

Sure, the one for collections is, but this input one does seem very tied to autocomplete

That or I'd argue both context should be defined somewhere else, that isn't inside the Autocomplete package

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
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?

Copy link
Member Author

Choose a reason for hiding this comment

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

the error is as follows:
ideally we define the CollectionContext like so

export const CollectionContext = createContext<ContextValue<CollectionContextValue<any>, HTMLElement>>(null);

so that a consuming component like Menu can then call useContextProps like

[collectionProps, ref] = useContextProps(collectionProps, ref, CollectionContext);

in order to merge its own set of collectionProps and its own ref with the ones being provided by the Autocomplete via context. However, the ref type in Menu might be something like RefObject<HTMLDivElement | null> vs the CollectionContext's RefObject<HTMLElement | null> which then conflicts and typescript complains. The context's ref just needs to be something that we can dispatch events on and thus should be as generic as possible, but ContextValue doesn't take a generic it seems...

Copy link
Member Author

Choose a reason for hiding this comment

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

it isn't necessary to use useContextProps, hence the current approach working in Menu/Listbox working due to casting the final ref merge as the desired ref type, but it would be nice to have that work IMO

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated with another attempt at the types

Copy link
Member

Choose a reason for hiding this comment

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

Could use EventTarget? if it's just for dispatching events? or something else that they both inherit from?

Copy link
Member Author

Choose a reason for hiding this comment

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

figured something out with @devongovett's help

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);

/**
* 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) || {};
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],
[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