From 4314a1e707d88ecba73419e424cd79a2e668d87f Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 25 Sep 2025 17:59:44 -0700 Subject: [PATCH 1/4] feat: Automatically detect if wrapped Autocomplete collection supports virtual focus (#8862) * exporting contexts for quarry * refactor autocomplete hook so it detects if it is attached to a filterable collection and if said collection supports virtual focus * initial attempt to get rid of Dialog from S2 Popover * fix tests * add inner div with overflow so that popover arrow shows up * fix items overflowing when partially scrolled into view see https://github.com/adobe/react-spectrum/pull/7672/files * update Popover so it accepts more values in styles instead * fix lint * export colorSchemeContext * clean up extra comment * fix chromatic * review comments * fix calc * substitute custom event in favor of collection tabindex check collection should only have a tab index if it isnt using virtual focus * update width calculations turns out the border of the popover shouldnt be included in the total width calculation, it is considered outside of the popover so no need to adjust. The border will be removed in favor of a box shadow to conform with update designs later * forgot to remove commented out code * move popover styling to user provided inner div as per discussion with team, if someone wants to modify their popover internals, they are expected to add a inner wrapping div themselves and turn off padding like custom dialog. * forgot to bring back styles --------- Co-authored-by: Robert Snow --- .../autocomplete/src/useAutocomplete.ts | 24 +++-- packages/@react-spectrum/s2/src/ComboBox.tsx | 89 ++++++++++--------- .../@react-spectrum/s2/src/ContextualHelp.tsx | 68 +++++++------- .../@react-spectrum/s2/src/DatePicker.tsx | 43 +++++---- packages/@react-spectrum/s2/src/Menu.tsx | 23 +++-- packages/@react-spectrum/s2/src/Picker.tsx | 71 ++++++++------- packages/@react-spectrum/s2/src/Popover.tsx | 67 ++++++++------ .../@react-spectrum/s2/src/TabsPicker.tsx | 61 +++++++------ packages/@react-spectrum/s2/src/index.ts | 2 +- .../@react-spectrum/s2/src/style-utils.ts | 4 +- packages/dev/s2-docs/src/SearchMenu.tsx | 2 +- .../src/Autocomplete.tsx | 2 +- .../react-aria-components/src/GridList.tsx | 22 ++--- .../react-aria-components/src/ListBox.tsx | 2 +- packages/react-aria-components/src/Menu.tsx | 2 +- .../react-aria-components/src/RSPContexts.ts | 19 ++++ .../react-aria-components/src/SearchField.tsx | 2 +- packages/react-aria-components/src/Table.tsx | 18 ++-- .../react-aria-components/src/TagGroup.tsx | 34 +++---- .../react-aria-components/src/TextField.tsx | 2 +- .../react-aria-components/src/context.tsx | 34 ------- packages/react-aria-components/src/index.ts | 3 +- 22 files changed, 317 insertions(+), 277 deletions(-) delete mode 100644 packages/react-aria-components/src/context.tsx diff --git a/packages/@react-aria/autocomplete/src/useAutocomplete.ts b/packages/@react-aria/autocomplete/src/useAutocomplete.ts index 7f895a676b1..313615d5977 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, 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); }, []); @@ -145,8 +149,16 @@ 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. + if (collectionNode.getAttribute('tabindex') != null) { + setShouldUseVirtualFocus(false); + } + setHasCollection(true); } else { lastCollectionNode.current?.removeEventListener('focusin', updateActiveDescendant); + setHasCollection(false); } }, [updateActiveDescendant]); @@ -393,7 +405,7 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Aut onFocus }; - if (collectionId) { + if (hasCollection) { inputProps = { ...inputProps, ...(shouldUseVirtualFocus && virtualFocusProps), @@ -413,7 +425,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-spectrum/s2/src/ComboBox.tsx b/packages/@react-spectrum/s2/src/ComboBox.tsx index a8b30d75827..6a0a383ec2f 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'; @@ -478,7 +478,7 @@ const ComboboxInner = forwardRef(function ComboboxInner(props: ComboBoxProps(null); - // Make menu width match input + button + // Make menu width match input + button let [triggerWidth, setTriggerWidth] = useState(null); let onResize = useCallback(() => { if (triggerRef.current) { @@ -643,57 +643,62 @@ const ComboboxInner = forwardRef(function ComboboxInner(props: ComboBoxProps {errorMessage} - - - - ( - - {loadingState === 'loading' ? stringFormatter.format('table.loading') : stringFormatter.format('combobox.noResults')} - - )} - items={items} - className={listbox({size})}> - {renderer} - - - - +
+ + + ( + + {loadingState === 'loading' ? stringFormatter.format('table.loading') : stringFormatter.format('combobox.noResults')} + + )} + items={items} + className={listbox({size})}> + {renderer} + + + +
+ ); diff --git a/packages/@react-spectrum/s2/src/ContextualHelp.tsx b/packages/@react-spectrum/s2/src/ContextualHelp.tsx index 9fdad216498..0590b534438 100644 --- a/packages/@react-spectrum/s2/src/ContextualHelp.tsx +++ b/packages/@react-spectrum/s2/src/ContextualHelp.tsx @@ -11,7 +11,7 @@ import InfoIcon from '../s2wf-icons/S2_Icon_InfoCircle_20_N.svg'; // @ts-ignore import intlMessages from '../intl/*.json'; import {mergeStyles} from '../style/runtime'; -import {PopoverBase, PopoverDialogProps} from './Popover'; +import {Popover, PopoverDialogProps} from './Popover'; import {space, style} from '../style' with {type: 'macro'}; import {StyleProps} from './style-utils' with { type: 'macro' }; import {useLocalizedStringFormatter} from '@react-aria/i18n'; @@ -39,11 +39,12 @@ export interface ContextualHelpProps extends size?: 'XS' | 'S' } -const popover = style({ - fontFamily: 'sans', +const wrappingDiv = style({ minWidth: 268, width: 268, - padding: 24 + padding: 24, + boxSizing: 'border-box', + height: 'full' }); export const ContextualHelpContext = createContext, FocusableRefValue>>(null); @@ -96,39 +97,42 @@ export const ContextualHelp = forwardRef(function ContextualHelp(props: Contextu isQuiet> {variant === 'info' ? : } - containerPadding={containerPadding} offset={offset} crossOffset={crossOffset} - hideArrow - styles={popover}> - - - {children} - - - + hideArrow> +
+ + + {children} + + +
+ ); }); diff --git a/packages/@react-spectrum/s2/src/DatePicker.tsx b/packages/@react-spectrum/s2/src/DatePicker.tsx index e58f37b671a..6fac65b2a44 100644 --- a/packages/@react-spectrum/s2/src/DatePicker.tsx +++ b/packages/@react-spectrum/s2/src/DatePicker.tsx @@ -33,7 +33,7 @@ import {FieldGroup, FieldLabel, HelpText} from './Field'; import {forwardRefType, GlobalDOMAttributes, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; // @ts-ignore import intlMessages from '../intl/*.json'; -import {PopoverBase} from './Popover'; +import {Popover} from './Popover'; import {pressScale} from './pressScale'; import {useLocalizedStringFormatter} from '@react-aria/i18n'; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -237,25 +237,30 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function export function CalendarPopover(props: PropsWithChildren): ReactElement { return ( - - - - {props.children} - - - + padding="none"> +
+ + + {props.children} + + +
+ ); } diff --git a/packages/@react-spectrum/s2/src/Menu.tsx b/packages/@react-spectrum/s2/src/Menu.tsx index a13e23903ec..b6b11040164 100644 --- a/packages/@react-spectrum/s2/src/Menu.tsx +++ b/packages/@react-spectrum/s2/src/Menu.tsx @@ -41,7 +41,7 @@ import {forwardRefType} from './types'; import {HeaderContext, HeadingContext, KeyboardContext, Text, TextContext} from './Content'; import {IconContext} from './Icon'; // chevron right removed?? import {ImageContext} from './Image'; -import {InPopoverContext, PopoverBase, PopoverContext} from './Popover'; +import {InPopoverContext, Popover, PopoverContext} from './Popover'; import LinkOutIcon from '../ui-icons/LinkOut'; import {mergeStyles} from '../style/runtime'; import {Placement, useLocale} from 'react-aria'; @@ -320,6 +320,11 @@ let InternalMenuContext = createContext<{size: 'S' | 'M' | 'L' | 'XL', isSubmenu let InternalMenuTriggerContext = createContext | null>(null); +let wrappingDiv = style({ + display: 'flex', + size: 'full' +}); + /** * Menus display a list of actions or options that a user can choose. */ @@ -366,14 +371,16 @@ export const Menu = /*#__PURE__*/ (forwardRef as forwardRefType)(function Menu - {content} - + padding="none" + hideArrow> +
+ {content} +
+ ); } diff --git a/packages/@react-spectrum/s2/src/Picker.tsx b/packages/@react-spectrum/s2/src/Picker.tsx index 312670de82d..40cd4f22878 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'; @@ -77,7 +77,7 @@ import {raw} from '../style/style-macro' with {type: 'macro'}; import React, {createContext, forwardRef, ReactNode, useContext, useMemo, useRef, useState} from 'react'; import {useFocusableRef} from '@react-spectrum/utils'; import {useGlobalListeners, useSlotId} from '@react-aria/utils'; -import {useLocalizedStringFormatter} from '@react-aria/i18n'; +import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n'; import {useScale} from './utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -339,6 +339,8 @@ export const Picker = /*#__PURE__*/ (forwardRef as forwardRefType)(function Pick ); } let scale = useScale(); + let {direction: dir} = useLocale(); + let RTLFlipOffset = dir === 'rtl' ? -1 : 1; return ( - - - - {renderer} - - - +
+ + + {renderer} + + +
+ @@ -542,7 +549,7 @@ const PickerButton = createHideableComponent(function PickerButton - {selectedItems.length <= 1 + {selectedItems.length <= 1 ? defaultChildren : {stringFormatter.format('picker.selectedCount', {count: selectedItems.length})} } diff --git a/packages/@react-spectrum/s2/src/Popover.tsx b/packages/@react-spectrum/s2/src/Popover.tsx index 43b104d63e3..b738786041d 100644 --- a/packages/@react-spectrum/s2/src/Popover.tsx +++ b/packages/@react-spectrum/s2/src/Popover.tsx @@ -15,17 +15,17 @@ import { PopoverProps as AriaPopoverProps, composeRenderProps, ContextValue, - Dialog, DialogProps, OverlayArrow, OverlayTriggerStateContext, useLocale } from 'react-aria-components'; -import {colorScheme, getAllowedOverrides, StyleProps, UnsafeStyles} from './style-utils' with {type: 'macro'}; +import {colorScheme, getAllowedOverrides, heightProperties, UnsafeStyles, widthProperties} 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 {lightDark, style} from '../style' with {type: 'macro'}; +import {mergeRefs} from '@react-aria/utils'; import {mergeStyles} from '../style/runtime'; import {StyleString} from '../style/types' with {type: 'macro'}; import {useDOMRef} from '@react-spectrum/utils'; @@ -154,8 +154,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 = '', @@ -163,18 +162,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. @@ -203,7 +202,7 @@ export const PopoverBase = forwardRef(function PopoverBase(props: PopoverProps, , Omit, StyleProps { +type PopoverStylesProp = StyleString<((typeof widthProperties)[number] | (typeof heightProperties)[number])>; +export interface PopoverDialogProps extends Pick, Omit, UnsafeStyles { + /** + * The amount of padding around the contents of the dialog. + * @default 'default' + */ + padding?: 'default' | 'none', + /** Spectrum-defined styles, returned by the `style()` macro. */ + styles?: PopoverStylesProp } - -const dialogStyle = style({ - padding: 8, +const innerDivStyle = style({ + padding: { + padding: { + default: 8, + none: 0 + } + }, boxSizing: 'border-box', outlineStyle: 'none', borderRadius: 'inherit', @@ -246,25 +257,29 @@ const dialogStyle = style({ /** * 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; - + let { + UNSAFE_className, + UNSAFE_style, + styles, + padding = 'default', + ...otherProps + } = props; return ( - - - {composeRenderProps(props.children, (children) => ( - // Reset OverlayTriggerStateContext so the buttons inside the dialog don't retain their hover state. + + {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..d381a63e62b 100644 --- a/packages/@react-spectrum/s2/src/TabsPicker.tsx +++ b/packages/@react-spectrum/s2/src/TabsPicker.tsx @@ -44,8 +44,8 @@ import {FocusableRef, FocusableRefValue, SpectrumLabelableProps} from '@react-ty 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 {Placement, useLocale} from 'react-aria'; +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'; @@ -178,6 +178,9 @@ function Picker(props: PickerProps, ref: FocusableRef (props: PickerProps, ref: FocusableRef - - - - {children} - - - +
+ + + {children} + + +
+ )}
diff --git a/packages/@react-spectrum/s2/src/index.ts b/packages/@react-spectrum/s2/src/index.ts index bccfb56a2f3..6fbe68b7e52 100644 --- a/packages/@react-spectrum/s2/src/index.ts +++ b/packages/@react-spectrum/s2/src/index.ts @@ -65,7 +65,7 @@ export {Picker, PickerItem, PickerSection, PickerContext} from './Picker'; export {Popover} from './Popover'; export {ProgressBar, ProgressBarContext} from './ProgressBar'; export {ProgressCircle, ProgressCircleContext} from './ProgressCircle'; -export {Provider} from './Provider'; +export {Provider, ColorSchemeContext} from './Provider'; export {Radio} from './Radio'; export {RadioGroup, RadioGroupContext} from './RadioGroup'; export {RangeCalendar, RangeCalendarContext} from './RangeCalendar'; diff --git a/packages/@react-spectrum/s2/src/style-utils.ts b/packages/@react-spectrum/s2/src/style-utils.ts index 07f2334db24..3f5a7f0bd41 100644 --- a/packages/@react-spectrum/s2/src/style-utils.ts +++ b/packages/@react-spectrum/s2/src/style-utils.ts @@ -307,13 +307,13 @@ const allowedOverrides = [ 'visibility' ] as const; -const widthProperties = [ +export const widthProperties = [ 'width', 'minWidth', 'maxWidth' ] as const; -const heightProperties = [ +export const heightProperties = [ 'size', 'height', 'minHeight', diff --git a/packages/dev/s2-docs/src/SearchMenu.tsx b/packages/dev/s2-docs/src/SearchMenu.tsx index c2ee702f53d..2ea5d6b2247 100644 --- a/packages/dev/s2-docs/src/SearchMenu.tsx +++ b/packages/dev/s2-docs/src/SearchMenu.tsx @@ -9,7 +9,7 @@ import {type Library, TAB_DEFS} from './constants'; import NoSearchResults from '@react-spectrum/s2/illustrations/linear/NoSearchResults'; import {Page} from '@parcel/rsc'; import React, {CSSProperties, useEffect, useMemo, useRef, useState} from 'react'; -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 5229595ea62..2eadef8a14d 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'; @@ -101,29 +100,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(); @@ -141,7 +136,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. @@ -246,7 +240,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 5e0ba0d5e22..36464a35bd5 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 {SelectionIndicatorContext} from './SelectionIndicator'; import {SeparatorContext} from './Separator'; import {SharedElementTransition} from './SharedElementTransition'; diff --git a/packages/react-aria-components/src/Menu.tsx b/packages/react-aria-components/src/Menu.tsx index c9eac9b1c93..635e9ed3ac5 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 19d2ae1a46b..9d97a2bf3c7 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 @@ -357,23 +356,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 @@ -383,7 +380,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 @@ -495,7 +491,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 3fd30b4455f..03d5857224f 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 {SelectionIndicatorContext} from './SelectionIndicator'; import {SharedElementTransition} from './SharedElementTransition'; import {TextContext} from './Text'; @@ -72,48 +72,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 a76ea9c6e8c..63a0727f248 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'; @@ -156,3 +156,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'; From 02cc8744e046995e8dcc81b1398bae671fd8822e Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Thu, 25 Sep 2025 18:00:14 -0700 Subject: [PATCH 2/4] revert: use timeout to determine scroll end (#8929) --- .../virtualizer/src/ScrollView.tsx | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/@react-aria/virtualizer/src/ScrollView.tsx b/packages/@react-aria/virtualizer/src/ScrollView.tsx index e89d2f2df1c..7c4988a5b3e 100644 --- a/packages/@react-aria/virtualizer/src/ScrollView.tsx +++ b/packages/@react-aria/virtualizer/src/ScrollView.tsx @@ -86,15 +86,6 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject { - state.isScrolling = false; - setScrolling(false); - state.scrollTimeout = null; - - window.dispatchEvent(new Event('tk.connect-observer')); - onScrollEnd?.(); - }, [state, onScrollEnd]); - let onScroll = useCallback((e) => { if (e.target !== e.currentTarget) { return; @@ -128,21 +119,29 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject { + state.isScrolling = false; + setScrolling(false); + state.scrollTimeout = null; + + window.dispatchEvent(new Event('tk.connect-observer')); + if (onScrollEnd) { + onScrollEnd(); + } + }, 300); } }); - }, [props, direction, state, contentSize, onVisibleRectChange, onScrollStart, onScrollTimeout]); + }, [props, direction, state, contentSize, onVisibleRectChange, onScrollStart, onScrollEnd]); // Attach event directly to ref so RAC Virtualizer doesn't need to send props upward. useEvent(ref, 'scroll', onScroll); - useEvent(ref, 'scrollend', onScrollTimeout); useEffect(() => { return () => { From 769cbb064da409cd599f851a8eae2a08de170ed7 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Thu, 25 Sep 2025 18:00:38 -0700 Subject: [PATCH 3/4] fix: gridlist section accessibility updates (#8932) * fix: gridlist section accessibility updates pt2 * remove unused import --- .../react-aria-components/src/GridList.tsx | 26 +++++++++---------- packages/react-aria-components/src/index.ts | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/react-aria-components/src/GridList.tsx b/packages/react-aria-components/src/GridList.tsx index 2eadef8a14d..6c087d25295 100644 --- a/packages/react-aria-components/src/GridList.tsx +++ b/packages/react-aria-components/src/GridList.tsx @@ -20,7 +20,6 @@ import {DragAndDropHooks} from './useDragAndDrop'; import {DraggableCollectionState, DroppableCollectionState, Collection as ICollection, ListState, Node, SelectionBehavior, UNSTABLE_useFilteredListState, useListState} from 'react-stately'; 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 {ListStateContext} from './ListBox'; import React, {createContext, ForwardedRef, forwardRef, HTMLAttributes, JSX, ReactNode, useContext, useEffect, useMemo, useRef} from 'react'; import {SelectionIndicatorContext} from './SelectionIndicator'; @@ -579,11 +578,11 @@ export interface GridListSectionProps extends SectionProps {} /** * A GridListSection represents a section within a GridList. */ -export const GridListSection = /*#__PURE__*/ createBranchComponent(SectionNode, (props: GridListSectionProps, ref: ForwardedRef, item: Node) => { +export const GridListSection = /*#__PURE__*/ createBranchComponent(SectionNode, (props: GridListSectionProps, ref: ForwardedRef, item: Node) => { let state = useContext(ListStateContext)!; let {CollectionBranch} = useContext(CollectionRendererContext); let headingRef = useRef(null); - ref = useObjectRef(ref); + ref = useObjectRef(ref); let {rowHeaderProps, rowProps, rowGroupProps} = useGridListSection({ 'aria-label': props['aria-label'] ?? undefined }, state, ref); @@ -598,33 +597,34 @@ export const GridListSection = /*#__PURE__*/ createBranchComponent(SectionNode, delete DOMProps.id; return ( -
-
+
); }); -const GridListHeaderContext = createContext | null>(null); +export const GridListHeaderContext = createContext, HTMLDivElement>>({}); +const GridListHeaderInnerContext = createContext | null>(null); -export const GridListHeader = /*#__PURE__*/ createLeafComponent(HeaderNode, function Header(props: HTMLAttributes, ref: ForwardedRef) { - [props, ref] = useContextProps(props, ref, HeaderContext); - let rowHeaderProps = useContext(GridListHeaderContext); +export const GridListHeader = /*#__PURE__*/ createLeafComponent(HeaderNode, function Header(props: HTMLAttributes, ref: ForwardedRef) { + [props, ref] = useContextProps(props, ref, GridListHeaderContext); + let rowHeaderProps = useContext(GridListHeaderInnerContext); return ( -
+
{props.children}
-
+
); }); diff --git a/packages/react-aria-components/src/index.ts b/packages/react-aria-components/src/index.ts index 63a0727f248..dc9bdfb3909 100644 --- a/packages/react-aria-components/src/index.ts +++ b/packages/react-aria-components/src/index.ts @@ -39,7 +39,7 @@ export {DropZone, DropZoneContext} from './DropZone'; export {FieldError, FieldErrorContext} from './FieldError'; export {FileTrigger} from './FileTrigger'; export {Form, FormContext} from './Form'; -export {GridListLoadMoreItem, GridList, GridListItem, GridListContext, GridListHeader, GridListSection} from './GridList'; +export {GridListLoadMoreItem, GridList, GridListItem, GridListContext, GridListHeader, GridListHeaderContext, GridListSection} from './GridList'; export {Group, GroupContext} from './Group'; export {Header, HeaderContext} from './Header'; export {Heading} from './Heading'; From eba6cc03fc30e291db60cf88d3bc0429a75c2f4d Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 25 Sep 2025 18:01:09 -0700 Subject: [PATCH 4/4] feat: Support placeholders in S2 components (#8692) * readd placeholder support to S2 components * progress * adding tests * fix combobox placeholder warning * apply specific color to placeholders * getting rid of unneeded warnings and adding missing placeholders * fix tests * review comments * fix tests * remove placeholder warning * add docs for placeholder and fix docs control for it and other string type props * remove unused tests and fix lint * sigh lint * review comments * update new combobox example * Add more placeholders in docs --------- Co-authored-by: Devon Govett --- .storybook-s2/docs/Migrating.jsx | 5 --- .../s2/chromatic/Accordion.stories.tsx | 6 ++-- .../s2/chromatic/ColorField.stories.tsx | 2 +- .../s2/chromatic/Forms.stories.tsx | 22 ++++++------ .../s2/chromatic/TextField.stories.tsx | 16 ++++++--- .../@react-spectrum/s2/src/ColorField.tsx | 5 +-- packages/@react-spectrum/s2/src/ComboBox.tsx | 4 ++- packages/@react-spectrum/s2/src/Field.tsx | 5 ++- .../@react-spectrum/s2/src/NumberField.tsx | 5 +-- .../@react-spectrum/s2/src/SearchField.tsx | 3 +- packages/@react-spectrum/s2/src/TextField.tsx | 17 ++++++---- .../s2/stories/Accordion.stories.tsx | 11 +++--- .../s2/stories/ColorField.stories.tsx | 6 +++- .../s2/stories/ComboBox.stories.tsx | 8 +++-- .../s2/stories/CustomDialog.stories.tsx | 2 +- .../s2/stories/Form.stories.tsx | 24 ++++++------- .../s2/stories/NumberField.stories.tsx | 7 ++-- .../s2/stories/Popover.stories.tsx | 4 +-- .../s2/stories/SearchField.stories.tsx | 8 +++-- .../s2/stories/TextField.stories.tsx | 9 +++-- .../@react-spectrum/s2/test/Combobox.test.tsx | 1 + packages/dev/codemods/src/s1-to-s2/UPGRADE.md | 5 --- .../__snapshots__/colorfield.test.ts.snap | 6 ---- .../__snapshots__/combobox.test.ts.snap | 25 -------------- .../__snapshots__/searchfield.test.ts.snap | 13 ------- .../__snapshots__/textarea.test.ts.snap | 13 ------- .../__snapshots__/textfield.test.ts.snap | 13 ------- .../src/s1-to-s2/__tests__/colorfield.test.ts | 6 ---- .../src/s1-to-s2/__tests__/combobox.test.ts | 24 ------------- .../s1-to-s2/__tests__/searchfield.test.ts | 12 ------- .../src/s1-to-s2/__tests__/textarea.test.ts | 12 ------- .../src/s1-to-s2/__tests__/textfield.test.ts | 12 ------- .../components/ColorField/transform.ts | 4 --- .../codemods/components/ComboBox/transform.ts | 4 --- .../components/SearchField/transform.ts | 4 --- .../codemods/components/TextArea/transform.ts | 4 --- .../components/TextField/transform.ts | 4 --- packages/dev/s2-docs/pages/s2/ColorField.mdx | 9 +++-- packages/dev/s2-docs/pages/s2/ComboBox.mdx | 18 ++++++---- packages/dev/s2-docs/pages/s2/Form.mdx | 16 +++++---- packages/dev/s2-docs/pages/s2/NumberField.mdx | 15 +++++--- packages/dev/s2-docs/pages/s2/SearchField.mdx | 9 +++-- packages/dev/s2-docs/pages/s2/TextArea.mdx | 9 +++-- packages/dev/s2-docs/pages/s2/TextField.mdx | 9 +++-- packages/dev/s2-docs/src/IconCards.tsx | 2 +- .../dev/s2-docs/src/IllustrationCards.tsx | 2 +- .../dev/s2-docs/src/VisualExampleClient.tsx | 34 +++++++++++-------- packages/react-aria-components/src/Input.tsx | 8 ++++- 48 files changed, 189 insertions(+), 273 deletions(-) diff --git a/.storybook-s2/docs/Migrating.jsx b/.storybook-s2/docs/Migrating.jsx index f819f96d16e..c6845f6d924 100644 --- a/.storybook-s2/docs/Migrating.jsx +++ b/.storybook-s2/docs/Migrating.jsx @@ -133,7 +133,6 @@ export function Migrating() {

ColorField

  • Remove isQuiet (it is no longer supported in Spectrum 2)
  • -
  • Remove placeholder (it has been removed due to accessibility issues)
  • Change validationState="invalid" to isInvalid
  • Remove validationState="valid" (it is no longer supported in Spectrum 2)
@@ -155,7 +154,6 @@ export function Migrating() {
  • Change menuWidth value from a DimensionValue to a pixel value
  • Remove isQuiet (it is no longer supported in Spectrum 2)
  • -
  • Remove placeholder (it is no longer supported in Spectrum 2)
  • Change validationState="invalid" to isInvalid
  • Remove validationState="valid" (it is no longer supported in Spectrum 2)
  • Update Item to be a ComboBoxItem
  • @@ -338,7 +336,6 @@ export function Migrating() {

    SearchField

      -
    • Remove placeholder (it has been removed due to accessibility issues)
    • [PENDING] Comment out icon (it has not been implemented yet)
    • Remove isQuiet (it is no longer supported in Spectrum 2)
    • Change validationState="invalid" to isInvalid
    • @@ -405,7 +402,6 @@ export function Migrating() {
      • [PENDING] Comment out icon (it has not been implemented yet)
      • Remove isQuiet (it is no longer supported in Spectrum 2)
      • -
      • Remove placeholder (it has been removed due to accessibility issues)
      • Change validationState="invalid" to isInvalid
      • Remove validationState="valid" (it is no longer supported in Spectrum 2)
      @@ -414,7 +410,6 @@ export function Migrating() {
      • [PENDING] Comment out icon (it has not been implemented yet)
      • Remove isQuiet (it is no longer supported in Spectrum 2)
      • -
      • Remove placeholder (it has been removed due to accessibility issues)
      • Change validationState="invalid" to isInvalid
      • Remove validationState="valid" (it is no longer supported in Spectrum 2)
      diff --git a/packages/@react-spectrum/s2/chromatic/Accordion.stories.tsx b/packages/@react-spectrum/s2/chromatic/Accordion.stories.tsx index 2a16fb20c7f..3ac77693a82 100644 --- a/packages/@react-spectrum/s2/chromatic/Accordion.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/Accordion.stories.tsx @@ -45,7 +45,7 @@ export const Example: Story = { People - + @@ -107,7 +107,7 @@ export const WithDisabledDisclosure: Story = { People - + @@ -152,7 +152,7 @@ export const WithActionButton: Story = { - + diff --git a/packages/@react-spectrum/s2/chromatic/ColorField.stories.tsx b/packages/@react-spectrum/s2/chromatic/ColorField.stories.tsx index 553903d7904..03f9dec2837 100644 --- a/packages/@react-spectrum/s2/chromatic/ColorField.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/ColorField.stories.tsx @@ -45,7 +45,7 @@ const Template = ({combos, ...args}: ColorFieldProps & {combos: any[]}): ReactEl key = 'default'; } return ( - + ); })} diff --git a/packages/@react-spectrum/s2/chromatic/Forms.stories.tsx b/packages/@react-spectrum/s2/chromatic/Forms.stories.tsx index b3811ada6e6..5da7087d322 100644 --- a/packages/@react-spectrum/s2/chromatic/Forms.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/Forms.stories.tsx @@ -62,9 +62,9 @@ type Story = StoryObj; export const Example: Story = { render: (args) => (
      - - - + + + Soccer Baseball @@ -75,10 +75,10 @@ export const Example: Story = { Dog Plant - - + + -