Skip to content

Conversation

LFDanLu
Copy link
Member

@LFDanLu LFDanLu commented Sep 12, 2025

Also attempts to replace Dialog in S2 Popover so users trying to recreate a Combobox/Autocomplete like experience can simply use Popover

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change (for new code or code which already has tests).
  • Filled out test instructions.
  • Updated documentation (if it already exists for this component).
  • Looked at the Accessibility Practices for this feature - Aria Practices

📝 Test Instructions:

Autocomplete behavior should largely be the same as it already is on main, simply sanity check that aria-activedescendant only appears when the wrapped collection support virtual focus. For S2 components that used to use Popover (ComboBox, ContextualHelp, DatePicker, Menu, Picker, TabsPicker) check that their styling and behavior haven't changed with the refactor.

🧢 Your Project:

RSP

// moving focus back to the subtriggers
let isMobileScreenReader = getInteractionModality() === 'virtual' && (isIOS() || isAndroid());
let shouldUseVirtualFocus = !isMobileScreenReader && !disableVirtualFocus;
let [shouldUseVirtualFocus, setShouldUseVirtualFocus] = useState(!isMobileScreenReader && !disableVirtualFocus);
Copy link
Member Author

Choose a reason for hiding this comment

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

We still want to keep disableVirtualFocus as a prop so a user can opt out of the virtual focus behavior based on their use case

Copy link
Member Author

Choose a reason for hiding this comment

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

For context, the Popover used to render a Dialog for the user, but this meant Dialog specific behavior like auto focusing the Dialog would make it impossible for a user to use the S2 Popover for a custom Combobox. We've opted to instead rely on RAC Popover applying a "dialog" role instead since users don't have access to PopoverBase

Copy link
Member

Choose a reason for hiding this comment

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

I think this makes sense, I had it on my list of things to explore since the change in RAC came after we wrote a good chunk of S2 popover usage

let contextProps;
[contextProps] = useContextProps({}, null, SelectableCollectionContext);
let {filter, ...collectionProps} = contextProps;
[props, ref] = useContextProps(props, ref, SelectableCollectionContext);
Copy link
Member Author

Choose a reason for hiding this comment

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

we now use the ref from useAutocomplete so the Autocomplete input knows it has been connected to a collection element it can filter

@LFDanLu LFDanLu marked this pull request as ready for review September 16, 2025 18:29
@rspbot
Copy link

rspbot commented Sep 16, 2025

@rspbot
Copy link

rspbot commented Sep 16, 2025

});

let turnOffVirtualFocus = useCallback(() => {
setShouldUseVirtualFocus(false);
Copy link
Member

Choose a reason for hiding this comment

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

This probably needs to stopPropagation so that nested collections don't accidentally tell one farther up as well

@rspbot
Copy link

rspbot commented Sep 23, 2025

@rspbot
Copy link

rspbot commented Sep 23, 2025

@rspbot
Copy link

rspbot commented Sep 23, 2025


let {
children,
disableInnerDiv,
Copy link
Member Author

Choose a reason for hiding this comment

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

super gross, maybe we just export PopoverBase as something like CustomPopover to mirror CustomDialog

@LFDanLu LFDanLu force-pushed the quarry_autocomplete_updates branch from b667d5b to 6ad1bbc Compare September 24, 2025 18:21
@rspbot
Copy link

rspbot commented Sep 24, 2025

@rspbot
Copy link

rspbot commented Sep 24, 2025

Comment on lines 653 to 656
// Subtract by 2 since these widths are set on the inner div rather than on the outermost div element that has
// a border
width: menuWidth ? `calc(${menuWidth}px - 2 * var(--s2-container-border-width))` : undefined,
'--trigger-width': `calc(${triggerWidth} - 2 * var(--s2-container-border-width))`
Copy link
Member Author

Choose a reason for hiding this comment

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

unfortunate consequence of switching to Popover from PopoverBase

Copy link
Member

Choose a reason for hiding this comment

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

not sure i understand this comment

wish we didn't need to use UNSAFE style to declare variables that are arbitrary and the result of calculations

wonder if we should consider a new style prop just for variables, that should be safe

Copy link
Member Author

Choose a reason for hiding this comment

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

its because previously these width values would go to the outer div that had the border applied to it, but now that we use Popover these values go to the inner div since we need to modify that div's padding/overflow. I'd really like if there was just a single container div per say, but applying overflow: auto to the outer div will hide the popover arrow...

Comment on lines 406 to 411
// TODO: not sure how best to type styles so it also can accept arbitrary css vars
// @ts-ignore
styles={style({
marginStart: {
isQuiet: -12
'--cross-offset': {
type: 'width',
value: -12
Copy link
Member Author

Choose a reason for hiding this comment

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

Open to any suggestions on how best to type styles in Popover to accept arbitrary values for setting css variables, might be forgetting where we do this. From what I can tell, this pattern usually happens when setting className which accepts a style string

Copy link
Member

Choose a reason for hiding this comment

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

is this?

// TODO: not sure how best to type styles so it also can accept arbitrary css vars

you'd need to set it in UNSAFE_style, otherwise you need to specify every possible value and take it as an argument

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah, but setting it in UNSAFE_style doesn't allow for the above syntax and thus we'd need to manually transform that value to rems (or for cases like borderOffsetWidth, the value is actually in px) essentially duplicating a bunch of the work done in the theme

Comment on lines 235 to 247
type popoverOverrides = [
'overflow',
'padding',
'paddingStart',
'paddingEnd',
'paddingTop',
'paddingBottom',
'paddingX',
'paddingY',
'display',
'flexDirection',
'gap'
];
Copy link
Member Author

Choose a reason for hiding this comment

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

new overrides that users can set on the Popover. These are set on the inner div

@LFDanLu
Copy link
Member Author

LFDanLu commented Sep 24, 2025

@rspbot
Copy link

rspbot commented Sep 24, 2025

Comment on lines 653 to 656
// Subtract by 2 since these widths are set on the inner div rather than on the outermost div element that has
// a border
width: menuWidth ? `calc(${menuWidth}px - 2 * var(--s2-container-border-width))` : undefined,
'--trigger-width': `calc(${triggerWidth} - 2 * var(--s2-container-border-width))`
Copy link
Member

Choose a reason for hiding this comment

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

not sure i understand this comment

wish we didn't need to use UNSAFE style to declare variables that are arbitrary and the result of calculations

wonder if we should consider a new style prop just for variables, that should be safe

Comment on lines 406 to 411
// TODO: not sure how best to type styles so it also can accept arbitrary css vars
// @ts-ignore
styles={style({
marginStart: {
isQuiet: -12
'--cross-offset': {
type: 'width',
value: -12
Copy link
Member

Choose a reason for hiding this comment

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

is this?

// TODO: not sure how best to type styles so it also can accept arbitrary css vars

you'd need to set it in UNSAFE_style, otherwise you need to specify every possible value and take it as an argument

Copy link
Member

Choose a reason for hiding this comment

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

I think this makes sense, I had it on my list of things to explore since the change in RAC came after we wrote a good chunk of S2 popover usage

@rspbot
Copy link

rspbot commented Sep 25, 2025

collection should only have a tab index if it isnt using virtual focus
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
@rspbot
Copy link

rspbot commented Sep 25, 2025

@rspbot
Copy link

rspbot commented Sep 25, 2025

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.
@rspbot
Copy link

rspbot commented Sep 25, 2025

@rspbot
Copy link

rspbot commented Sep 26, 2025

devongovett
devongovett previously approved these changes Sep 26, 2025
# Conflicts:
#	packages/@react-spectrum/s2/src/Popover.tsx
@rspbot
Copy link

rspbot commented Sep 26, 2025

@rspbot
Copy link

rspbot commented Sep 26, 2025

## API Changes

react-aria-components

/react-aria-components:SelectableCollectionContext

+SelectableCollectionContext {
+  UNTYPED
+}

/react-aria-components:FieldInputContext

+FieldInputContext {
+  UNTYPED
+}

/react-aria-components:SelectableCollectionContextValue

+SelectableCollectionContextValue <T> {
+  aria-describedby?: string
+  aria-details?: string
+  aria-label?: string
+  aria-labelledby?: string
+  disallowTypeAhead?: boolean
+  filter?: (string, Node<T>) => boolean
+  id?: string
+  shouldUseVirtualFocus?: boolean
+}

@react-spectrum/s2

/@react-spectrum/s2:Popover

 Popover {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
-  children?: ReactNode | (DialogRenderProps) => ReactNode
+  children?: ChildrenOrFunction<PopoverRenderProps>
   containerPadding?: number = 12
   crossOffset?: number = 0
   hideArrow?: boolean = false
   id?: string
   isOpen?: boolean
   offset?: number = 8
   onOpenChange?: (boolean) => void
+  padding?: 'default' | 'none' = 'default'
   placement?: Placement = 'bottom'
   role?: 'dialog' | 'alertdialog' = 'dialog'
   shouldFlip?: boolean = true
   size?: 'S' | 'M' | 'L'
   slot?: string | null
-  styles?: StylesProp
+  styles?: PopoverStylesProp
   triggerRef?: RefObject<Element | null>
 }

/@react-spectrum/s2:ColorSchemeContext

+ColorSchemeContext {
+  UNTYPED
+}

@devongovett devongovett added this pull request to the merge queue Sep 26, 2025
Merged via the queue into main with commit 4314a1e Sep 26, 2025
32 checks passed
@devongovett devongovett deleted the quarry_autocomplete_updates branch September 26, 2025 01:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants