diff --git a/packages/@react-aria/autocomplete/src/useAutocomplete.ts b/packages/@react-aria/autocomplete/src/useAutocomplete.ts index 7f895a676b1..248912760e7 100644 --- a/packages/@react-aria/autocomplete/src/useAutocomplete.ts +++ b/packages/@react-aria/autocomplete/src/useAutocomplete.ts @@ -13,12 +13,12 @@ import {AriaLabelingProps, BaseEvent, DOMProps, FocusableElement, FocusEvents, KeyboardEvents, Node, RefObject, ValueBase} 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, isAndroid, isCtrlKeyPressed, isIOS, mergeProps, mergeRefs, useEffectEvent, useEvent, useLabels, useObjectRef, useSlotId} from '@react-aria/utils'; +import {CLEAR_FOCUS_EVENT, DISALLOW_VIRTUAL_FOCUS, FOCUS_EVENT, getActiveElement, getOwnerDocument, isAndroid, isCtrlKeyPressed, isIOS, mergeProps, mergeRefs, useEffectEvent, useEvent, useId, useLabels, useObjectRef} from '@react-aria/utils'; import {dispatchVirtualBlur, dispatchVirtualFocus, getVirtuallyFocusedElement, moveVirtualFocus} from '@react-aria/focus'; import {getInteractionModality} from '@react-aria/interactions'; // @ts-ignore import intlMessages from '../intl/*.json'; -import {FocusEvent as ReactFocusEvent, KeyboardEvent as ReactKeyboardEvent, useCallback, useEffect, useMemo, useRef} from 'react'; +import {FocusEvent as ReactFocusEvent, KeyboardEvent as ReactKeyboardEvent, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {useLocalizedStringFormatter} from '@react-aria/i18n'; export interface CollectionOptions extends DOMProps, AriaLabelingProps { @@ -88,7 +88,7 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Aut disableVirtualFocus = false } = props; - let collectionId = useSlotId(); + let collectionId = useId(); let timeout = useRef | undefined>(undefined); let delayNextActiveDescendant = useRef(false); let queuedActiveDescendant = useRef(null); @@ -97,7 +97,11 @@ 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 isMobileScreenReader = getInteractionModality() === 'virtual' && (isIOS() || isAndroid()); - let shouldUseVirtualFocus = !isMobileScreenReader && !disableVirtualFocus; + let [shouldUseVirtualFocus, setShouldUseVirtualFocus] = useState(!isMobileScreenReader && !disableVirtualFocus); + // Tracks if a collection has been connected to the autocomplete. If false, we don't want to add various attributes to the autocomplete input + // since it isn't attached to a filterable collection (e.g. Tabs) + let [hasCollection, setHasCollection] = useState(false); + useEffect(() => { return () => clearTimeout(timeout.current); }, []); @@ -136,6 +140,10 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Aut delayNextActiveDescendant.current = false; }); + let turnOffVirtualFocus = useCallback(() => { + setShouldUseVirtualFocus(false); + }, []); + let callbackRef = useCallback((collectionNode) => { if (collectionNode != null) { // When typing forward, we want to delay the setting of active descendant to not interrupt the native screen reader announcement @@ -145,10 +153,17 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Aut lastCollectionNode.current?.removeEventListener('focusin', updateActiveDescendant); lastCollectionNode.current = collectionNode; collectionNode.addEventListener('focusin', updateActiveDescendant); + // If useSelectableCollection isn't passed shouldUseVirtualFocus even when useAutocomplete provides it + // that means the collection doesn't support it (e.g. Table). If that is the case, we need to disable it here regardless + // of what the user's provided so that the input doesn't recieve the onKeyDown and autocomplete props. + collectionNode.addEventListener(DISALLOW_VIRTUAL_FOCUS, turnOffVirtualFocus); + setHasCollection(true); } else { lastCollectionNode.current?.removeEventListener('focusin', updateActiveDescendant); + lastCollectionNode.current?.removeEventListener(DISALLOW_VIRTUAL_FOCUS, turnOffVirtualFocus); + setHasCollection(false); } - }, [updateActiveDescendant]); + }, [updateActiveDescendant, turnOffVirtualFocus]); // Make sure to memo so that React doesn't keep registering a new event listeners on every rerender of the wrapped collection let mergedCollectionRef = useObjectRef(useMemo(() => mergeRefs(collectionRef, callbackRef), [collectionRef, callbackRef])); @@ -393,7 +408,7 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Aut onFocus }; - if (collectionId) { + if (hasCollection) { inputProps = { ...inputProps, ...(shouldUseVirtualFocus && virtualFocusProps), @@ -413,7 +428,7 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Aut inputProps, collectionProps: mergeProps(collectionProps, { shouldUseVirtualFocus, - disallowTypeAhead: true + disallowTypeAhead: shouldUseVirtualFocus }), collectionRef: mergedCollectionRef, filter: filter != null ? filterFn : undefined diff --git a/packages/@react-aria/selection/src/useSelectableCollection.ts b/packages/@react-aria/selection/src/useSelectableCollection.ts index 825888ffea6..319d046fcb7 100644 --- a/packages/@react-aria/selection/src/useSelectableCollection.ts +++ b/packages/@react-aria/selection/src/useSelectableCollection.ts @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, focusWithoutScrolling, getActiveElement, isCtrlKeyPressed, mergeProps, scrollIntoView, scrollIntoViewport, useEffectEvent, useEvent, useRouter, useUpdateLayoutEffect} from '@react-aria/utils'; +import {CLEAR_FOCUS_EVENT, DISALLOW_VIRTUAL_FOCUS, FOCUS_EVENT, focusWithoutScrolling, getActiveElement, isCtrlKeyPressed, mergeProps, scrollIntoView, scrollIntoViewport, useEffectEvent, useEvent, useRouter, useUpdateLayoutEffect} from '@react-aria/utils'; import {dispatchVirtualFocus, getFocusableTreeWalker, moveVirtualFocus} from '@react-aria/focus'; import {DOMAttributes, FocusableElement, FocusStrategy, Key, KeyboardDelegate, RefObject} from '@react-types/shared'; import {flushSync} from 'react-dom'; @@ -396,6 +396,17 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions } }; + useEffect(() => { + if (!shouldUseVirtualFocus) { + let disableVirtualFocus = new CustomEvent(DISALLOW_VIRTUAL_FOCUS, { + cancelable: true, + bubbles: true + }); + + ref.current?.dispatchEvent(disableVirtualFocus); + } + }, [shouldUseVirtualFocus, ref]); + // Ref to track whether the first item in the collection should be automatically focused. Specifically used for autocomplete when user types // to focus the first key AFTER the collection updates. // TODO: potentially expand the usage of this diff --git a/packages/@react-aria/utils/src/constants.ts b/packages/@react-aria/utils/src/constants.ts index 665779cf30c..b023c63c9c7 100644 --- a/packages/@react-aria/utils/src/constants.ts +++ b/packages/@react-aria/utils/src/constants.ts @@ -13,3 +13,5 @@ // Custom event names for updating the autocomplete's aria-activedecendant. export const CLEAR_FOCUS_EVENT = 'react-aria-clear-focus'; export const FOCUS_EVENT = 'react-aria-focus'; +// Custom event to tell autocomplete that virtual focus isn't supported for this component (e.g. Table) +export const DISALLOW_VIRTUAL_FOCUS = 'react-aria-disallow-virtual-focus'; diff --git a/packages/@react-aria/utils/src/index.ts b/packages/@react-aria/utils/src/index.ts index 530c5fa6d6c..9d7d0d89919 100644 --- a/packages/@react-aria/utils/src/index.ts +++ b/packages/@react-aria/utils/src/index.ts @@ -47,7 +47,7 @@ export {useFormReset} from './useFormReset'; export {useLoadMore} from './useLoadMore'; export {useLoadMoreSentinel, useLoadMoreSentinel as UNSTABLE_useLoadMoreSentinel} from './useLoadMoreSentinel'; export {inertValue} from './inertValue'; -export {CLEAR_FOCUS_EVENT, FOCUS_EVENT} from './constants'; +export {CLEAR_FOCUS_EVENT, DISALLOW_VIRTUAL_FOCUS, FOCUS_EVENT} from './constants'; export {isCtrlKeyPressed} from './keyboard'; export {useEnterAnimation, useExitAnimation} from './animation'; export {isFocusable, isTabbable} from './isFocusable'; diff --git a/packages/@react-spectrum/s2/src/ComboBox.tsx b/packages/@react-spectrum/s2/src/ComboBox.tsx index de788b6f530..9523d139a58 100644 --- a/packages/@react-spectrum/s2/src/ComboBox.tsx +++ b/packages/@react-spectrum/s2/src/ComboBox.tsx @@ -59,7 +59,7 @@ import intlMessages from '../intl/*.json'; import {mergeRefs, useResizeObserver, useSlotId} from '@react-aria/utils'; import {Node} from 'react-stately'; import {Placement} from 'react-aria'; -import {PopoverBase} from './Popover'; +import {Popover} from './Popover'; import {pressScale} from './pressScale'; import {ProgressCircle} from './ProgressCircle'; import {TextFieldRef} from '@react-types/textfield'; @@ -643,7 +643,7 @@ const ComboboxInner = forwardRef(function ComboboxInner(props: ComboBoxProps {errorMessage} - - + ); diff --git a/packages/@react-spectrum/s2/src/ContextualHelp.tsx b/packages/@react-spectrum/s2/src/ContextualHelp.tsx index 9fdad216498..9319fb5355b 100644 --- a/packages/@react-spectrum/s2/src/ContextualHelp.tsx +++ b/packages/@react-spectrum/s2/src/ContextualHelp.tsx @@ -103,6 +103,8 @@ export const ContextualHelp = forwardRef(function ContextualHelp(props: Contextu offset={offset} crossOffset={crossOffset} hideArrow + // TODO: Unfortunately, we can't pass these styles via the styles prop + // since we don't want to actually allow modifying width and padding for the popover... styles={popover}> {content} - + ); } diff --git a/packages/@react-spectrum/s2/src/Picker.tsx b/packages/@react-spectrum/s2/src/Picker.tsx index 21b9ecf27c3..55a90fef351 100644 --- a/packages/@react-spectrum/s2/src/Picker.tsx +++ b/packages/@react-spectrum/s2/src/Picker.tsx @@ -69,7 +69,7 @@ import {IconContext} from './Icon'; import intlMessages from '../intl/*.json'; import {mergeStyles} from '../style/runtime'; import {Placement} from 'react-aria'; -import {PopoverBase} from './Popover'; +import {Popover} from './Popover'; import {PressResponder} from '@react-aria/interactions'; import {pressScale} from './pressScale'; import {ProgressCircle} from './ProgressCircle'; @@ -394,13 +394,16 @@ export const Picker = /*#__PURE__*/ (forwardRef as forwardRefType)(function Pick estimatedHeadingHeight: 50, padding: 8, loaderHeight: LOADER_ROW_HEIGHTS[size][scale]}}> - - + diff --git a/packages/@react-spectrum/s2/src/Popover.tsx b/packages/@react-spectrum/s2/src/Popover.tsx index b9c4ab4d119..5c5219666c3 100644 --- a/packages/@react-spectrum/s2/src/Popover.tsx +++ b/packages/@react-spectrum/s2/src/Popover.tsx @@ -15,7 +15,6 @@ import { PopoverProps as AriaPopoverProps, composeRenderProps, ContextValue, - Dialog, DialogProps, OverlayArrow, OverlayTriggerStateContext, @@ -23,8 +22,9 @@ import { } from 'react-aria-components'; import {colorScheme, getAllowedOverrides, StyleProps, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {ColorSchemeContext} from './Provider'; -import {createContext, forwardRef, MutableRefObject, useCallback, useContext} from 'react'; +import {createContext, ForwardedRef, forwardRef, useCallback, useContext, useMemo} from 'react'; import {DOMRef, DOMRefValue, GlobalDOMAttributes} from '@react-types/shared'; +import {mergeRefs} from '@react-aria/utils'; import {mergeStyles} from '../style/runtime'; import {style} from '../style' with {type: 'macro'}; import {StyleString} from '../style/types' with {type: 'macro'}; @@ -155,8 +155,7 @@ let arrow = style({ export const PopoverContext = createContext>>(null); export const InPopoverContext = createContext(false); -export const PopoverBase = forwardRef(function PopoverBase(props: PopoverProps, ref: DOMRef) { - [props, ref] = useSpectrumContextProps(props, ref, PopoverContext); +export const PopoverBase = forwardRef(function PopoverBase(props: PopoverProps, ref: ForwardedRef) { let { hideArrow = false, UNSAFE_className = '', @@ -164,18 +163,18 @@ export const PopoverBase = forwardRef(function PopoverBase(props: PopoverProps, styles, size } = props; - let domRef = useDOMRef(ref); let colorScheme = useContext(ColorSchemeContext); let {locale, direction} = useLocale(); // TODO: should we pass through lang and dir props in RAC? let popoverRef = useCallback((el: HTMLDivElement) => { - (domRef as MutableRefObject).current = el; if (el) { el.lang = locale; el.dir = direction; } - }, [locale, direction, domRef]); + }, [locale, direction]); + // Memoed so it doesn't break ComboBox/Picker scrolling + let mergedRef = useMemo(() => mergeRefs(popoverRef, ref), [ref, popoverRef]); // On small devices, show a modal (or eventually a tray) instead of a popover. // TODO: reverted this until we have trays. @@ -204,7 +203,7 @@ export const PopoverBase = forwardRef(function PopoverBase(props: PopoverProps, , Omit, StyleProps { +export interface PopoverDialogProps extends Pick, Omit, StyleProps { } - +// TODO this now goes on the Popover itself, do we want to allow users to override the height? +// That made sense for the inner dialog when that existed, but probably is undesirable for the popover itself +// Will move padding into the popover styles instead when this is decided const dialogStyle = style({ padding: 8, - boxSizing: 'border-box', - outlineStyle: 'none', - borderRadius: 'inherit', - overflow: 'auto', - position: 'relative', - width: 'full', - maxSize: 'inherit' + display: 'block', + overflow: 'auto' }, getAllowedOverrides({height: true})); /** * A popover is an overlay element positioned relative to a trigger. */ -export const Popover = forwardRef(function Popover(props: PopoverDialogProps, ref: DOMRef) { +export const Popover = forwardRef(function Popover(props: PopoverDialogProps, ref: DOMRef) { + [props, ref] = useSpectrumContextProps(props, ref, PopoverContext); let domRef = useDOMRef(ref); - const {triggerRef, isOpen, onOpenChange, ...otherProps} = props; - return ( - - - {composeRenderProps(props.children, (children) => ( - // Reset OverlayTriggerStateContext so the buttons inside the dialog don't retain their hover state. - - {children} - - ))} - + + {composeRenderProps(props.children, (children) => ( + // Reset OverlayTriggerStateContext so the buttons inside the dialog don't retain their hover state. + + {children} + + ))} ); }); diff --git a/packages/@react-spectrum/s2/src/TabsPicker.tsx b/packages/@react-spectrum/s2/src/TabsPicker.tsx index e986191cff2..fe4b56d54b0 100644 --- a/packages/@react-spectrum/s2/src/TabsPicker.tsx +++ b/packages/@react-spectrum/s2/src/TabsPicker.tsx @@ -45,7 +45,7 @@ import {forwardRefType} from './types'; import {HeaderContext, HeadingContext, Text, TextContext} from './Content'; import {IconContext} from './Icon'; import {Placement} from 'react-aria'; -import {PopoverBase} from './Popover'; +import {Popover} from './Popover'; import {pressScale} from './pressScale'; import {raw} from '../style/style-macro' with {type: 'macro'}; import React, {createContext, forwardRef, ReactNode, useContext, useRef} from 'react'; @@ -241,11 +241,13 @@ function Picker(props: PickerProps, ref: FocusableRef - (props: PickerProps, ref: FocusableRef - + )} diff --git a/packages/dev/s2-docs/src/SearchMenu.tsx b/packages/dev/s2-docs/src/SearchMenu.tsx index 34946567a5b..1a79e60f627 100644 --- a/packages/dev/s2-docs/src/SearchMenu.tsx +++ b/packages/dev/s2-docs/src/SearchMenu.tsx @@ -16,7 +16,7 @@ import reactAriaDocs from 'docs:react-aria-components'; import {ReactAriaLogo} from './icons/ReactAriaLogo'; // @ts-ignore import reactSpectrumDocs from 'docs:@react-spectrum/s2'; -import {SelectableCollectionContext} from '../../../react-aria-components/src/context'; +import {SelectableCollectionContext} from '../../../react-aria-components/src/RSPContexts'; import {style} from '@react-spectrum/s2/style' with { type: 'macro' }; import {Tab, TabList, TabPanel, Tabs} from './Tabs'; import {TextFieldRef} from '@react-types/textfield'; diff --git a/packages/react-aria-components/src/Autocomplete.tsx b/packages/react-aria-components/src/Autocomplete.tsx index 11370b2363a..34dc9c46f44 100644 --- a/packages/react-aria-components/src/Autocomplete.tsx +++ b/packages/react-aria-components/src/Autocomplete.tsx @@ -12,7 +12,7 @@ import {AriaAutocompleteProps, useAutocomplete} from '@react-aria/autocomplete'; import {AutocompleteState, useAutocompleteState} from '@react-stately/autocomplete'; -import {FieldInputContext, SelectableCollectionContext} from './context'; +import {FieldInputContext, SelectableCollectionContext} from './RSPContexts'; import {mergeProps} from '@react-aria/utils'; import {Provider, removeDataAttributes, SlotProps, SlottedContextValue, useSlottedContext} from './utils'; import React, {createContext, JSX, useRef} from 'react'; diff --git a/packages/react-aria-components/src/GridList.tsx b/packages/react-aria-components/src/GridList.tsx index d3a87620b09..d1ea997aeae 100644 --- a/packages/react-aria-components/src/GridList.tsx +++ b/packages/react-aria-components/src/GridList.tsx @@ -11,14 +11,13 @@ */ import {AriaGridListProps, DraggableItemResult, DragPreviewRenderer, DropIndicatorAria, DroppableCollectionResult, FocusScope, ListKeyboardDelegate, mergeProps, useCollator, useFocusRing, useGridList, useGridListItem, useGridListSection, useGridListSelectionCheckbox, useHover, useLocale, useVisuallyHidden} from 'react-aria'; import {ButtonContext} from './Button'; -import {CheckboxContext} from './RSPContexts'; +import {CheckboxContext, FieldInputContext, SelectableCollectionContext, SelectableCollectionContextValue} from './RSPContexts'; import {Collection, CollectionBuilder, createBranchComponent, createLeafComponent, HeaderNode, ItemNode, LoaderNode, SectionNode} from '@react-aria/collections'; import {CollectionProps, CollectionRendererContext, DefaultCollectionRenderer, ItemRenderProps, SectionProps} from './Collection'; import {ContextValue, DEFAULT_SLOT, Provider, RenderProps, SlotProps, StyleProps, StyleRenderProps, useContextProps, useRenderProps} from './utils'; import {DragAndDropContext, DropIndicatorContext, DropIndicatorProps, useDndPersistedKeys, useRenderDropIndicator} from './DragAndDrop'; import {DragAndDropHooks} from './useDragAndDrop'; import {DraggableCollectionState, DroppableCollectionState, Collection as ICollection, ListState, Node, SelectionBehavior, UNSTABLE_useFilteredListState, useListState} from 'react-stately'; -import {FieldInputContext, SelectableCollectionContext} from './context'; 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'; @@ -99,29 +98,25 @@ export const GridList = /*#__PURE__*/ (forwardRef as forwardRefType)(function Gr }); interface GridListInnerProps { - props: GridListProps, + props: GridListProps & SelectableCollectionContextValue, collection: ICollection>, - gridListRef: RefObject + gridListRef: RefObject } 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 contextProps; - [contextProps] = useContextProps({}, null, SelectableCollectionContext); - let {filter, ...collectionProps} = contextProps; + [props, ref] = useContextProps(props, ref, SelectableCollectionContext); // eslint-disable-next-line @typescript-eslint/no-unused-vars - let {shouldUseVirtualFocus, disallowTypeAhead, ...DOMCollectionProps} = collectionProps || {}; + let {shouldUseVirtualFocus, filter, disallowTypeAhead, ...DOMCollectionProps} = props; let {dragAndDropHooks, keyboardNavigationBehavior = 'arrow', layout = 'stack'} = props; let {CollectionRoot, isVirtualized, layoutDelegate, dropTargetDelegate: ctxDropTargetDelegate} = useContext(CollectionRendererContext); let gridlistState = useListState({ - ...props, + ...DOMCollectionProps, collection, children: undefined, layoutDelegate }); - let filteredState = UNSTABLE_useFilteredListState(gridlistState, filter); + let filteredState = UNSTABLE_useFilteredListState(gridlistState as ListState, filter); let collator = useCollator({usage: 'search', sensitivity: 'base'}); let {disabledBehavior, disabledKeys} = filteredState.selectionManager; let {direction} = useLocale(); @@ -139,7 +134,6 @@ function GridListInner({props, collection, gridListRef: ref}: ), [filteredState.collection, ref, layout, disabledKeys, disabledBehavior, layoutDelegate, collator, direction]); let {gridProps} = useGridList({ - ...props, ...DOMCollectionProps, keyboardDelegate, // Only tab navigation is supported in grid layout. @@ -244,7 +238,7 @@ function GridListInner({props, collection, gridListRef: ref}:
} slot={props.slot || undefined} onScroll={props.onScroll} data-drop-target={isRootDropTarget || undefined} diff --git a/packages/react-aria-components/src/ListBox.tsx b/packages/react-aria-components/src/ListBox.tsx index 0421d790022..bfc3fec024f 100644 --- a/packages/react-aria-components/src/ListBox.tsx +++ b/packages/react-aria-components/src/ListBox.tsx @@ -21,7 +21,7 @@ import {filterDOMProps, inertValue, LoadMoreSentinelProps, useLoadMoreSentinel, 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 {SelectableCollectionContext, SelectableCollectionContextValue} from './context'; +import {SelectableCollectionContext, SelectableCollectionContextValue} from './RSPContexts'; import {SeparatorContext} from './Separator'; import {TextContext} from './Text'; diff --git a/packages/react-aria-components/src/Menu.tsx b/packages/react-aria-components/src/Menu.tsx index 5798c5d43bc..85ebf69ca0d 100644 --- a/packages/react-aria-components/src/Menu.tsx +++ b/packages/react-aria-components/src/Menu.tsx @@ -15,7 +15,7 @@ import {BaseCollection, Collection, CollectionBuilder, CollectionNode, createBra import {MenuTriggerProps as BaseMenuTriggerProps, Collection as ICollection, Node, RootMenuTriggerState, TreeState, useMenuTriggerState, useSubmenuTriggerState, useTreeState} from 'react-stately'; import {CollectionProps, CollectionRendererContext, ItemRenderProps, SectionContext, SectionProps, usePersistedKeys} from './Collection'; import {ContextValue, DEFAULT_SLOT, Provider, RenderProps, SlotProps, StyleRenderProps, useContextProps, useRenderProps, useSlot, useSlottedContext} from './utils'; -import {FieldInputContext, SelectableCollectionContext, SelectableCollectionContextValue} from './context'; +import {FieldInputContext, SelectableCollectionContext, SelectableCollectionContextValue} from './RSPContexts'; 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'; diff --git a/packages/react-aria-components/src/RSPContexts.ts b/packages/react-aria-components/src/RSPContexts.ts index 2b5db6a7462..1e45acc3e11 100644 --- a/packages/react-aria-components/src/RSPContexts.ts +++ b/packages/react-aria-components/src/RSPContexts.ts @@ -10,6 +10,8 @@ * governing permissions and limitations under the License. */ +import {AriaLabelingProps, DOMProps, FocusableElement, FocusEvents, KeyboardEvents, Node, ValueBase} from '@react-types/shared'; +import {AriaTextFieldProps} from '@react-aria/textfield'; import {CheckboxProps} from './Checkbox'; import {ColorAreaProps} from './ColorArea'; import {ColorFieldProps} from './ColorField'; @@ -31,3 +33,20 @@ export const ColorFieldContext = createContext, HTMLDivElement>>(null); export const ColorWheelContext = createContext, HTMLDivElement>>(null); export const HeadingContext = createContext>({}); + +export interface SelectableCollectionContextValue extends DOMProps, AriaLabelingProps { + filter?: (nodeTextValue: string, node: Node) => boolean, + /** Whether the collection items should use virtual focus instead of being focused directly. */ + shouldUseVirtualFocus?: boolean, + /** Whether typeahead is disabled. */ + disallowTypeAhead?: boolean +} +interface FieldInputContextValue extends + DOMProps, + FocusEvents, + KeyboardEvents, + Pick, 'onChange' | 'value'>, + Pick {} + +export const SelectableCollectionContext = createContext, HTMLElement>>(null); +export const FieldInputContext = createContext>(null); diff --git a/packages/react-aria-components/src/SearchField.tsx b/packages/react-aria-components/src/SearchField.tsx index db871dc545a..43c708c2575 100644 --- a/packages/react-aria-components/src/SearchField.tsx +++ b/packages/react-aria-components/src/SearchField.tsx @@ -15,7 +15,7 @@ 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 {FieldInputContext} from './context'; +import {FieldInputContext} from './RSPContexts'; import {filterDOMProps} from '@react-aria/utils'; import {FormContext} from './Form'; import {GlobalDOMAttributes} from '@react-types/shared'; diff --git a/packages/react-aria-components/src/Table.tsx b/packages/react-aria-components/src/Table.tsx index 497346964ef..c370dd39baa 100644 --- a/packages/react-aria-components/src/Table.tsx +++ b/packages/react-aria-components/src/Table.tsx @@ -2,7 +2,7 @@ import {AriaLabelingProps, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, import {BaseCollection, Collection, CollectionBuilder, CollectionNode, createBranchComponent, createLeafComponent, FilterableNode, LoaderNode, useCachedChildren} from '@react-aria/collections'; import {buildHeaderRows, TableColumnResizeState} from '@react-stately/table'; import {ButtonContext} from './Button'; -import {CheckboxContext} from './RSPContexts'; +import {CheckboxContext, FieldInputContext, SelectableCollectionContext, SelectableCollectionContextValue} from './RSPContexts'; 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'; @@ -10,7 +10,6 @@ import {DisabledBehavior, DraggableCollectionState, DroppableCollectionState, Mu import {DragAndDropContext, DropIndicatorContext, DropIndicatorProps, useDndPersistedKeys, useRenderDropIndicator} from './DragAndDrop'; import {DragAndDropHooks} from './useDragAndDrop'; import {DraggableItemResult, DragPreviewRenderer, DropIndicatorAria, DroppableCollectionResult, FocusScope, ListKeyboardDelegate, mergeProps, useFocusRing, useHover, useLocale, useLocalizedStringFormatter, useTable, useTableCell, useTableColumnHeader, useTableColumnResize, useTableHeaderRow, useTableRow, useTableRowGroup, useTableSelectAllCheckbox, useTableSelectionCheckbox, useVisuallyHidden} from 'react-aria'; -import {FieldInputContext, SelectableCollectionContext} from './context'; import {filterDOMProps, inertValue, isScrollable, LoadMoreSentinelProps, mergeRefs, useLayoutEffect, useLoadMoreSentinel, useObjectRef, useResizeObserver} from '@react-aria/utils'; import {GridNode} from '@react-types/grid'; // @ts-ignore @@ -355,23 +354,21 @@ export const Table = forwardRef(function Table(props: TableProps, ref: Forwarded }); interface TableInnerProps { - props: TableProps, - forwardedRef: ForwardedRef, + props: TableProps & SelectableCollectionContextValue, + forwardedRef: ForwardedRef, selectionState: MultipleSelectionState, collection: ITableCollection> } function TableInner({props, forwardedRef: ref, selectionState, collection}: TableInnerProps) { - let contextProps; - [contextProps] = useContextProps({}, null, SelectableCollectionContext); - let {filter, ...collectionProps} = contextProps; + [props, ref] = useContextProps(props, ref, SelectableCollectionContext); // eslint-disable-next-line @typescript-eslint/no-unused-vars - let {shouldUseVirtualFocus, disallowTypeAhead, ...DOMCollectionProps} = collectionProps || {}; + let {shouldUseVirtualFocus, disallowTypeAhead, filter, ...DOMCollectionProps} = props; let tableContainerContext = useContext(ResizableTableContainerContext); ref = useObjectRef(useMemo(() => mergeRefs(ref, tableContainerContext?.tableRef), [ref, tableContainerContext?.tableRef])); let tableState = useTableState({ - ...props, + ...DOMCollectionProps, collection, children: undefined, UNSAFE_selectionState: selectionState @@ -381,7 +378,6 @@ function TableInner({props, forwardedRef: ref, selectionState, collection}: Tabl let {isVirtualized, layoutDelegate, dropTargetDelegate: ctxDropTargetDelegate, CollectionRoot} = useContext(CollectionRendererContext); let {dragAndDropHooks} = props; let {gridProps} = useTable({ - ...props, ...DOMCollectionProps, layoutDelegate, isVirtualized @@ -493,7 +489,7 @@ function TableInner({props, forwardedRef: ref, selectionState, collection}: Tabl } slot={props.slot || undefined} onScroll={props.onScroll} data-allows-dragging={isListDraggable || undefined} diff --git a/packages/react-aria-components/src/TagGroup.tsx b/packages/react-aria-components/src/TagGroup.tsx index 22b781a693a..05f972a9b71 100644 --- a/packages/react-aria-components/src/TagGroup.tsx +++ b/packages/react-aria-components/src/TagGroup.tsx @@ -16,12 +16,12 @@ import {Collection, CollectionBuilder, createLeafComponent, ItemNode} from '@rea 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'; -import {forwardRefType, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, PressEvents} from '@react-types/shared'; +import {forwardRefType, GlobalDOMAttributes, HoverEvents, Key, LinkDOMProps, PressEvents, RefObject} from '@react-types/shared'; import {LabelContext} from './Label'; import {ListState, Node, UNSTABLE_useFilteredListState, useListState} from 'react-stately'; import {ListStateContext} from './ListBox'; import React, {createContext, ForwardedRef, forwardRef, JSX, ReactNode, useContext, useEffect, useRef} from 'react'; -import {SelectableCollectionContext} from './context'; +import {SelectableCollectionContext, SelectableCollectionContextValue} from './RSPContexts'; import {TextContext} from './Text'; export interface TagGroupProps extends Omit, 'children' | 'items' | 'label' | 'description' | 'errorMessage' | 'keyboardDelegate'>, DOMProps, SlotProps, GlobalDOMAttributes {} @@ -70,48 +70,48 @@ export const TagGroup = /*#__PURE__*/ (forwardRef as forwardRefType)(function Ta ); }); -interface TagGroupInnerProps { - props: TagGroupProps, +interface TagGroupInnerProps { + props: TagGroupProps & SelectableCollectionContextValue, forwardedRef: ForwardedRef, collection } -function TagGroupInner({props, forwardedRef: ref, collection}: TagGroupInnerProps) { - let contextProps; - [contextProps] = useContextProps({}, null, SelectableCollectionContext); - let {filter, ...collectionProps} = contextProps; +function TagGroupInner({props, forwardedRef: ref, collection}: TagGroupInnerProps) { + let tagListRef = useRef(null); + // Extract the user provided id so it doesn't clash with the collection id provided by Autocomplete + let {id, ...otherProps} = props; + [otherProps, tagListRef] = useContextProps(otherProps, tagListRef, SelectableCollectionContext); // eslint-disable-next-line @typescript-eslint/no-unused-vars - let {shouldUseVirtualFocus, disallowTypeAhead, ...DOMCollectionProps} = collectionProps || {}; - let tagListRef = useRef(null); + let {filter, shouldUseVirtualFocus, ...DOMCollectionProps} = otherProps; let [labelRef, label] = useSlot( !props['aria-label'] && !props['aria-labelledby'] ); let tagGroupState = useListState({ - ...props, + ...DOMCollectionProps, children: undefined, collection }); - let filteredState = UNSTABLE_useFilteredListState(tagGroupState, filter); + let filteredState = UNSTABLE_useFilteredListState(tagGroupState as ListState, filter); // Prevent DOM props from going to two places. - let domProps = filterDOMProps(props, {global: true}); - let domPropOverrides = Object.fromEntries(Object.entries(domProps).map(([k]) => [k, undefined])); + let domProps = filterDOMProps(otherProps, {global: true}); + let domPropOverrides = Object.fromEntries(Object.entries(domProps).map(([k, val]) => [k, k === 'id' ? val : undefined])); let { gridProps, labelProps, descriptionProps, errorMessageProps } = useTagGroup({ - ...props, - ...domPropOverrides, ...DOMCollectionProps, + ...domPropOverrides, label }, filteredState, tagListRef); return (
}], [ListStateContext, filteredState], [TextContext, { slots: { diff --git a/packages/react-aria-components/src/TextField.tsx b/packages/react-aria-components/src/TextField.tsx index 3899bf22b07..5be86cef6d3 100644 --- a/packages/react-aria-components/src/TextField.tsx +++ b/packages/react-aria-components/src/TextField.tsx @@ -14,7 +14,7 @@ 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 {FieldInputContext} from './context'; +import {FieldInputContext} from './RSPContexts'; import {filterDOMProps} from '@react-aria/utils'; import {FormContext} from './Form'; import {GlobalDOMAttributes} from '@react-types/shared'; diff --git a/packages/react-aria-components/src/context.tsx b/packages/react-aria-components/src/context.tsx deleted file mode 100644 index 4ce7a97be30..00000000000 --- a/packages/react-aria-components/src/context.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2025 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -import {AriaLabelingProps, DOMProps, FocusableElement, FocusEvents, KeyboardEvents, Node, ValueBase} from '@react-types/shared'; -import {AriaTextFieldProps} from '@react-aria/textfield'; -import {ContextValue} from './utils'; -import {createContext} from 'react'; - -export interface SelectableCollectionContextValue extends DOMProps, AriaLabelingProps { - filter?: (nodeTextValue: string, node: Node) => boolean, - /** Whether the collection items should use virtual focus instead of being focused directly. */ - shouldUseVirtualFocus?: boolean, - /** Whether typeahead is disabled. */ - disallowTypeAhead?: boolean -} - -interface FieldInputContextValue extends - DOMProps, - FocusEvents, - KeyboardEvents, - Pick, 'onChange' | 'value'>, - Pick {} - -export const SelectableCollectionContext = createContext, HTMLElement>>(null); -export const FieldInputContext = createContext>(null); diff --git a/packages/react-aria-components/src/index.ts b/packages/react-aria-components/src/index.ts index 36cf3e61b43..4243bb4f399 100644 --- a/packages/react-aria-components/src/index.ts +++ b/packages/react-aria-components/src/index.ts @@ -14,7 +14,7 @@ // to import it from a React Server Component in a framework like Next.js. import 'client-only'; -export {CheckboxContext, ColorAreaContext, ColorFieldContext, ColorSliderContext, ColorWheelContext, HeadingContext} from './RSPContexts'; +export {CheckboxContext, ColorAreaContext, ColorFieldContext, ColorSliderContext, ColorWheelContext, HeadingContext, SelectableCollectionContext, FieldInputContext} from './RSPContexts'; export {Autocomplete, AutocompleteContext, AutocompleteStateContext} from './Autocomplete'; export {Breadcrumbs, BreadcrumbsContext, Breadcrumb} from './Breadcrumbs'; @@ -152,3 +152,4 @@ export type {CalendarState, CheckboxGroupState, Color, ColorAreaState, ColorFiel export type {AutocompleteState} from '@react-stately/autocomplete'; export type {ListLayoutOptions, GridLayoutOptions, WaterfallLayoutOptions} from '@react-stately/layout'; export type {ValidationResult, RouterConfig} from '@react-types/shared'; +export type {SelectableCollectionContextValue} from './RSPContexts';