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

Fix closing components using the transition prop, and after scrolling the page #3407

Merged
merged 20 commits into from
Aug 2, 2024

Conversation

RobinMalfait
Copy link
Member

This PR fixes an issue where components such as the MenuItems and the ListboxOptions won't close correctly if these are used in combination with the transition prop and after you scrolled the page a little bit.

For some background information: the Menu and Listbox components can be used with the transition prop so that the MenuItems and ListboxOptions can be transitioned in and out based on the open/closed state.

To make the user experience better, we have some logic to check whether the MenuButton or ListboxButton moved from its initial position. If it did move when it's closing, then we immediately stop all transitions by unmounting the MenuItems or ListboxOptions immediately.

A situation this happens in:

  1. Open the Menu (with the transition prop)
  2. Press tab to focus the next element on the page
  3. The browser scrolls the page to make the next element visible
  4. The MenuItems will fade out while moving around (while the page scrolls)
    1. Bonus points, if you also use the anchor prop, then the MenuItems will reposition itself which makes the UX even worse.

While debugging this issue, I noticed a timing related issue because we are using the DOM element in a useTransition hook. Typically you store those DOM elements in a useRef. This is good practice because the DOM element will be filled in once you hit the useEffect hook, and you don't introduce unnecessary re-renders.

However, if the DOM element changes, then the useRef value will update, but the useEffect hook doesn't run again (because no state change happened).

There are other places in Headless UI where we also need to re-run effects when the DOM node changes. There are also places where we need access to the DOM element during render.

One of React's rules is to not read / write a ref during render.

So, this PR simplifies a few components and hooks by actually storing the DOM elements in state directly instead of in a ref. This way we can use the DOM element in dependency arrays of useEffect hooks, and we can use the DOM element during render.

Maybe a bit of an unorthodox solution, but it works quite well and it simplifies the code quite a bit.


It might be easier to review commit by commit. The reason why is that in some hooks I added temporary support for HTMLElement | null in places where we wanted MutableRefObject<HTMLElement | null>. These now temporary look like MutableRefObject<HTMLElement | null> | HTMLElement | null.

This way we can use both versions while refactoring. By the end of the commits those are simplified again such that we only handle HTMLElement | null instead of MutableRefObject<HTMLElement | null>.

Fixes: #3398
Fixes: #3403

This change should be temporary, and it will allow us to use the
`useDidElementMove` with ref objects and direct `HTMLElement`s.
This change should be temporary, and it will allow us to use the
`useResolveButtonType` hook with ref objects and direct `HTMLElement`s.
This change should be temporary, and it will allow us to use the
`useRefocusableInput` hook with ref objects and direct `HTMLElement`s.
Accept `HTMLElement| null` instead of `MutableRefObject<HTMLElement |
null>` in the `useTransition` hook.
So far we've been tracking the `button` and the the `items` DOM nodes in
a ref. Typically, this is the way you do it, you keep track of it in a
ref, later you can access it in a `useEffect` or similar by accessing
the `ref.current`.

There are some problems with this. There are places where we require the
DOM element during render (for example when picking out the `.id` from
the DOM node directly).

Another issue is that we want to re-run some `useEffect`'s whenever the
underlying DOM node changes. We currently work around that, but storing
it directly in state would solve these issues because the component will
re-render and we will have access to the new DOM node.
This doesn't support the `MutableRefObject<HTMLElement | null>` anymore.
We don't handle `MutableRefObject<HTMLElement | null>` anymore
Only accept `HTMLElement | null` instead of `MutableRefObject<HTMLElement | null>`
Copy link

vercel bot commented Aug 1, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
headlessui-react ✅ Ready (Inspect) Visit Preview 💬 Add feedback Aug 2, 2024 9:00am
headlessui-vue ✅ Ready (Inspect) Visit Preview 💬 Add feedback Aug 2, 2024 9:00am

@RobinMalfait RobinMalfait marked this pull request as ready for review August 2, 2024 08:59
@RobinMalfait RobinMalfait changed the title Fix closing components in combination with the transition prop, and after scrolling the page Fix closing components using the transition prop, and after scrolling the page Aug 2, 2024
@thecrypticace
Copy link
Contributor

I like how much cleaner this makes some of the code 💯

@RobinMalfait RobinMalfait merged commit 2260422 into main Aug 2, 2024
8 checks passed
@RobinMalfait RobinMalfait deleted the fix/issue-3398-3403 branch August 2, 2024 20:45
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.

Listbox not closing when scrolling the page Unable to manually close Menu Component
2 participants