Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: auto-adjust menu width for custom triggers #7710

Conversation

nwidynski
Copy link
Contributor

Closes #7663 (comment)
Context #7663 (comment)

✅ 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:

🧢 Your Project:

@nwidynski
Copy link
Contributor Author

@snowystinger This is the fix for #7663. You can merge this into your feature branch for follow up review if you like 🚀

let style = useMemo(() => {
let triggerWidth = props.style?.['--trigger-width']?.replace('px', '');

if (props.triggerRef?.current && !isNaN(Number(triggerWidth))) {
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 breaks the rules for reading from a ref during render, would need to read the ref during an effect, then store the width in state.

I'm not entirely sure I follow what's going on here. To clamp to whatever value is passed in that is smaller than --trigger-width, couldn't we use maxWidth?

Copy link
Contributor Author

@nwidynski nwidynski Feb 4, 2025

Choose a reason for hiding this comment

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

Thanks, too reliant on eslint already 🤣

Originally, I proposed setting --trigger-width to the width of triggerRef when it's larger than inputRef + buttonRef as a courtesy. This is how people expect it to behave when providing a custom triggerRef.

You are right, maxWidth could be used to clamp smaller values - equivalently we should probably use minWidth here instead of overriding --trigger-width. A bit unfortunate to have 2 resize observers for often times the same ref, but can be improved with a similar concept to idsUpdaterMap at some point 👍

@@ -43,6 +43,8 @@ const hiddenFragment = typeof DocumentFragment !== 'undefined' ? new DocumentFra
export function Hidden(props: {children: ReactNode}) {
let isHidden = useContext(HiddenContext);
let isSSR = useIsSSR();
// eslint-disable-next-line react-hooks/exhaustive-deps
let wasSSR = useMemo(() => isSSR, []);
Copy link
Member

Choose a reason for hiding this comment

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

what was actually happening here? was strictmode running on the server or something?

Copy link
Contributor Author

@nwidynski nwidynski Feb 4, 2025

Choose a reason for hiding this comment

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

See #7663 (comment)

TLDR; During hydration, useIsSSR() remains true, meaning we render into a template element on the first client render. The ref is attached but React can't maintain it as everything is moved to the portal in subsequent renders, in this case caused by the state update of the parent. This change forces a consistent strategy.

I tried using portal for the initial render as well as assigning both portal and template the same key, but to no avail also. I suppose this is due to bad support for template elements in general facebook/react#19932

Copy link
Member

Choose a reason for hiding this comment

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

i see, so now it will always render into template if it's an SSR app? we'll have to see if there's a performance hit for that
the reason for doing it in the portal was so we could use a super slimmed down document

Copy link
Member

@snowystinger snowystinger left a comment

Choose a reason for hiding this comment

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

I'll merge this into my PR and we can continue discussing there
Thanks so much for looking into this

@@ -43,6 +43,8 @@ const hiddenFragment = typeof DocumentFragment !== 'undefined' ? new DocumentFra
export function Hidden(props: {children: ReactNode}) {
let isHidden = useContext(HiddenContext);
let isSSR = useIsSSR();
// eslint-disable-next-line react-hooks/exhaustive-deps
let wasSSR = useMemo(() => isSSR, []);
Copy link
Member

Choose a reason for hiding this comment

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

i see, so now it will always render into template if it's an SSR app? we'll have to see if there's a performance hit for that
the reason for doing it in the portal was so we could use a super slimmed down document

@snowystinger snowystinger merged commit 2cb2cf6 into adobe:fix-ssr-combobox-inner-ref Feb 7, 2025
29 checks passed
github-merge-queue bot pushed a commit that referenced this pull request Feb 20, 2025
* fix: SSR Combobox inner ref lost

* fix lint

* feat: auto-adjust menu width for custom triggers (#7710)

* feat: auto-adjust menu width for custom triggers

* fix: test assert label compatibility

* fix: rules of react

* feat: popover minWidth for custom trigger elements

* fix: use resize observer

* chore: reset combobox story

* remove minimum size on popovers

* use more stable ref instead of memo

* update based on new information

* Update to reduce extra renders

---------

Co-authored-by: Nikolas Schröter <[email protected]>
@nwidynski nwidynski deleted the fix-combobox-triggerref branch February 21, 2025 09:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants