Commit Graph

336 Commits

Author SHA1 Message Date
Robin Malfait df88f4c48b 2.2.2 - @headlessui/react 2025-04-17 15:16:30 +02:00
Robin Malfait 97cb20806a Improve Combobox component performance (#3697)
This PR improves the performance of the `Combobox` component. This is a
similar implementation as:

- https://github.com/tailwindlabs/headlessui/pull/3685
- https://github.com/tailwindlabs/headlessui/pull/3688

Before this PR, the `Combobox` component is built in a way where all the
state lives in the `Combobox` itself. If state changes, everything
re-renders and re-computes the necessary derived state.

However, if you have a 1000 items, then every time the active item
changes, all 1000 items have to re-render.

To solve this, we can move the state outside of the `Combobox`
component, and "subscribe" to state changes using the `useSlice` hook
introduced in https://github.com/tailwindlabs/headlessui/pull/3684.

This will allow us to subscribe to a slice of the state, and only
re-render if the computed slice actually changes.

If the active item changes, only 3 things will happen:

1. The `ComboboxOptions` will re-render and have an updated
`aria-activedescendant`
2. The `ComboboxOption` that _was_ active, will re-render and the
`data-focus` attribute wil be removed.
3. The `ComboboxOption` that is now active, will re-render and the
`data-focus` attribute wil be added.

The `Combobox` component already has a `virtual` option if you want to
render many many more items. This is a bit of a different model where
all the options are passed in via an array instead of rendering all
`ComboboxOption` components immediately.

Because of this, I didn't want to batch the registration of the options
as part of this PR (similar to what we do in the `Menu` and `Listbox`)
because it behaves differently compared to what mode you are using
(virtual or not). Since not all components will be rendered, batching
the registration until everything is registered doesn't really make
sense in the general case. However, it does make sense in non-virtual
mode. But because of this difference, I didn't want to implement this as
part of this PR and increase the complexity of the PR even more.

Instead I will follow up with more PRs with more improvements. But the
key improvement of looking at the slice of the data is what makes the
biggest impact. This also means that we can do another release once this
is merged.

Last but not least, recently we fixed a bug where the `Combobox` in
`virtual` mode could crash if you search for an item that doesn't exist.
To solve it, we implemented a workaround in:

- https://github.com/tailwindlabs/headlessui/pull/3678

Which used a double `requestAnimationFrame` to delay the scrolling to
the item. While this solved this issue, this also caused visual flicker
when holding down your arrow keys.

I also fixed it in this PR by introducing `patch-package` and work
around the issue in the `@tanstack/virtual-core` package itself.

More info: 96f4da70b16d5cf259643

Before:


https://github.com/user-attachments/assets/132520d3-b4d6-42f9-9152-57427de20639

After:


https://github.com/user-attachments/assets/41f198fe-9326-42d1-a09f-077b60a9f65d

## Test plan

1. All tests still pass
2. Tested this in the browser with a 1000 items. In the videos below the
only thing I'm doing is holding down the `ArrowDown` key.

Before:


https://github.com/user-attachments/assets/945692a3-96e6-4ac7-bee0-36a1fd89172b

After:


https://github.com/user-attachments/assets/98a151d0-16cc-4823-811c-fcee0019937a
2025-04-17 15:16:30 +02:00
Robin Malfait c5f95b02af Fix Transition component from incorrectly exposing the Closing state (#3696)
This PR fixes an issue where the scroll locking logic was incorrectly
re-enabled in Dialogs if you were using a `Transition` component or a
`transition` prop _and_ you had nested components with the `transition`
prop (or a nested `TransitionChild` component) _and_ the parent
transition finishes before any of its children.

To visualize this, it would happen in this situation:

```tsx
<Dialog transition> /* No transition classes */
  <DialogBackdrop transition className="duration-500" />
  <DialogPanel transition className="duration-200" />
  </DialogPanel>
</Dialog>
```

With the `transition` prop, internally these components would render a
wrapper `Transition` component.
The `Dialog` will look at the open/closed state provided by the
`Transition` component to know whether to unmount its children or not.

The `Dialog` component also has some internal hooks to make it behave as
a dialog. One of those hooks is the `useScrollLock` hook. This hook will
be enabled if the `Dialog` is open and disabled when it's closed.

If you are using the `Transition` component or the `transition` prop,
then we have to make sure that the `useScrollLock` gets disabled
immediate, and not wait until the transition completes. This is done by
looking at the `Closing` state. The reason for this is that disabling
the `useScrollLock` also means that we restore the scroll position. But
if you in the meantime navigate to a different page which also changes
the scroll position, then we would restore the scroll position on a
totally different page.

We already had this logic setup, but the problem is that the `Closing`
state was incorrectly derived from the transition state. That state was
only looking at the current component (in the example above, the
`Dialog` component) but not at any of the child components.

Since the `Dialog` didn't have any transitions itself, the `Closing`
state was only briefly there.

If there is no `Closing` state, then the `useScrollLock` is looking at
the `open` state of the `Dialog`. Because other child components were
still transitioning, the `Dialog` was still in an open state. This
actually **re-enabled** the `useScrollLock` hook. Because from the
`Dialog`s perspective no transitions were happening anymore.

Eventually the transitions of all the children completed causing the
`Transition` and thus the `Dialog` to unmount. This in turn caused the
`useScrollLock` hook to also clean up and restore the scroll position.

But as you might have guessed, now this second time, it's restoring
_after_ the transition is done.

Luckily, the fix is simple. Make sure that the `Closing` state also
keeps the full hierarchy into account and not only the state of the
current element.
2025-04-16 18:24:52 +00:00
Robin Malfait 51acc1bff5 Open Menu and Listbox on mousedown (#3689)
This is a small behavioral change, but this PR will change when the
`Menu` and `Listbox` components open.

This PR will now open the `Menu` and `Listbox` components on `mousedown`
instead of `click`. This will make it feel more responsive and faster to
the user.

This is also how macOS for example opens menu-like components on the OS
level. This is also how the native `<select>` (at least on macOS) works.
2025-04-11 00:00:39 +02:00
Robin Malfait 9685af7148 Improve Listbox component performance (#3688)
This PR improves the performance of the `Listbox` component.

Before this PR, the `Listbox` component is built in a way where all the
state lives in the `Listbox` itself. If state changes, everything
re-renders and re-computes the necessary derived state.

However, if you have a 1000 options, then every time the active option
changes, all 1000 options have to re-render.

To solve this, we can move the state outside of the `Listbox` component,
and "subscribe" to state changes using the `useSlice` hook introduced in
https://github.com/tailwindlabs/headlessui/pull/3684.

This will allow us to subscribe to a slice of the state, and only
re-render if the computed slice actually changes.

If the active option changes, only 3 things will happen:

1. The `ListboxOptions` will re-render and have an updated
`aria-activedescendant`
2. The `ListboxOption` that _was_ active, will re-render and the
`data-focus` attribute wil be removed.
3. The `ListboxOption` that is now active, will re-render and the
`data-focus` attribute wil be added.

Another improvement is that in order to make sure that your arrow keys
go to the correct option, we need to sort the DOM nodes and make sure
that we go to the correct option when using arrow up and down. This
sorting was happening every time a new `ListboxOption` was registered.

Luckily, once an array is sorted, you don't have to do a lot, but you
still have to loop over `n` options which is not ideal.

This PR will now delay the sorting until all `ListboxOption`s are
registered.

On that note, we also batch the `RegisterOption` so we can perform a
single update instead of `n` updates. We use a microTask for the
batching (so if you only are registering a single option, you don't have
to wait compared to a `setTimeout` or a `requestAnimationFrame`).

## Test plan

1. All tests still pass
2. Tested this in the browser with a 2000 options. In the videos below
the only thing I'm doing is holding down the `ArrowDown` key.

Before:


https://github.com/user-attachments/assets/a2850c84-57f6-428a-aa51-e6f83d2aee97

After:


https://github.com/user-attachments/assets/157c6e99-5da8-4d72-87c6-a5e34f122531
2025-04-10 22:43:35 +02:00
Robin Malfait a293af9788 Improve Menu component performance (#3685)
This PR improves the performance of the `Menu` component.

Before this PR, the `Menu` component is built in a way where all the
state lives in the `Menu` itself. If state changes, everything
re-renders and re-computes the necessary derived state.

However, if you have a 1000 items, then every time the active item
changes, all 1000 items have to re-render.

To solve this, we can move the state outside of the `Menu` component,
and "subscribe" to state changes using the `useSlice` hook introduced in
https://github.com/tailwindlabs/headlessui/pull/3684.

This will allow us to subscribe to a slice of the state, and only
re-render if the computed slice actually changes.

If the active item changes, only 3 things will happen:

1. The `MenuItems` will re-render and have an updated
`aria-activedescendant`
2. The `MenuItem` that _was_ active, will re-render and the `data-focus`
attribute wil be removed.
3. The `MenuItem` that is now active, will re-render and the
`data-focus` attribute wil be added.

Another improvement is that in order to make sure that your arrow keys
go to the correct item, we need to sort the DOM nodes and make sure that
we go to the correct item when using arrow up and down. This sorting was
happening every time a new `MenuItem` was registered.

Luckily, once an array is sorted, you don't have to do a lot, but you
still have to loop over `n` items which is not ideal.

This PR will now delay the sorting until all `MenuItem`s are registered.

On that note, we also batch the `RegisterItem` so we can perform a
single update instead of `n` updates. We use a microTask for the
batching (so if you only are registering a single item, you don't have
to wait compared to a `setTimeout` or a `requestAnimationFrame`).

## Test plan

1. All tests still pass
2. Tested this in the browser with a 1000 items. In the videos below the
only thing I'm doing is holding down the `ArrowDown` key.

Before:


https://github.com/user-attachments/assets/513b02c1-fc69-47f3-a97e-c56d44dd585a

After:


https://github.com/user-attachments/assets/266236a0-b64a-4322-9a54-ead7fb62191f
2025-04-10 22:27:11 +02:00
Robin Malfait ef9c17217e 2.2.1 - @headlessui/react 2025-04-04 16:45:09 +02:00
Robin Malfait 9d3b0ff611 Fix Unexpected undefined crash in Combobox component with virtual mode (#3678)
This PR fixes an issue where the `Combobox` component crashes if you are
using the `virtual` option and you quickly type something such that the
`Combobox` opens but no valid options are available.

We already check if the current active index is available in the
internal `options` list. However, if you then call
`virtualizer.scrollToIndex(data.activeOptionIndex)` it will crash if you
are too fast.


https://github.com/user-attachments/assets/f48172e6-4098-4a31-aa16-67ce21f074d1

If you are typing slowly, then it will work as expected.


https://github.com/user-attachments/assets/9d522bd5-5b54-4c12-9250-a2d92a511b35

I did find an open issue on TanStack's repo about this:
https://github.com/TanStack/virtual/issues/879

This PR is basically a workaround by delaying the call. But it does have
the expected behavior now.


https://github.com/user-attachments/assets/2e5e47a5-b021-4897-b098-568711723b77


Fixes: #3583
2025-04-04 14:16:42 +02:00
Robin Malfait 9a4c030003 Add invalid prop to Combobox component (#3677)
This PR adds the `invalid` prop to the `Combobox` component. This will
also expose the `invalid` value as a render prop to the `Combobox.Input`
and `Combobox.Button` components.

It will also expose the `data-invalid` attribute on these components
when the `invalid` prop is set to `true`.

```tsx
<Combobox invalid>
 <Combobox.Input />
 <Combobox.Button />
</Combobox>
```

Without `invalid` prop:
<img width="916" alt="image"
src="https://github.com/user-attachments/assets/2c199691-7b29-450f-89a5-4b84e6704c6a"
/>


With invalid prop:
<img width="913" alt="image"
src="https://github.com/user-attachments/assets/4bdde518-39b3-4998-b353-604a818a3c99"
/>

Notice the `data-invalid` prop on the `<input>` and the `<button>`.
2025-04-04 09:46:50 +00:00
Robin Malfait 20f46dd9e8 Accept tabIndex on <RadioGroup> component (#3646)
This PR allows you to pass a `tabIndex` to the `<RadioGroup>` component
and it will internally pass it down to the correct `<Radio />` or
`<RadioGroupOption>` component.

The reason we do it this way is because only a single radio should be
focusable (moving between radios can be done via the arrow keys instead
of the tab key).
2025-02-21 15:27:32 +00:00
Robin Malfait 466bb07ba6 Accept tabIndex on Checkbox component (#3645)
This PR allows you to pass a `tabIndex` to the `Checkbox` component.
2025-02-21 14:54:31 +00:00
Robin Malfait 87252e1c33 Fix aria-invalid attributes to have a valid 'true' value (#3639)
This PR fixes an issue with the `aria-invalid` attributes on some form
elements.

In theory this shouldn't matter and behaves the same as other
attributes. MDN also mentions that any other value than the known set of
values will be treated as `true`.

However, some tools, including the Accessibility tab in Google Chrome
will complain because we set it to `aria-invalid=""`.

We already used `'true'` for `aria-checked` as well, so this change
makes it more consistent.

It will also make sure that `aria-invalid:flex` in Tailwind CSS works as
expected because this compiles to:

```css
.aria-invalid\:flex {
  &[aria-invalid="true"] {
    display: flex;
  }
}
```

Which means that the current implementation didn't work in this case
either.

Fixes: #3623
2025-02-13 14:50:42 +00:00
Robin Malfait b6f355d154 update changelog 2024-12-12 17:40:57 +01:00
Robin Malfait d71fb9cd2e 2.2.0 - @headlessui/react 2024-10-25 15:51:43 +02:00
Robin Malfait 8814b0eecd Add React 19 support (#3543)
This PR fixes an issue where `@headlessui/react` was not compatible with
React 19.

We made sure that accessing `ref`s is safe and works in React 18 and
React 19. We also made sure to include React 19 as a valid version in
the peer dependencies. For now, we also allowed the RC versions of React
and React DOM.
2024-10-25 13:50:58 +00:00
Robin Malfait 5eb3b12a95 2.1.10 - @headlessui/react 2024-10-10 20:56:58 +02:00
Robin Malfait a4953a2b11 Fix crash in ListboxOptions when using as={Fragment} (#3513)
This PR fixes an issue where a `Maximum update depth exceeded` error
occurs if you use `as={Fragment}` in the `ListboxOptions` component.

This PR also includes a refactor to make sure this exact issue cannot
happen anymore in other components.

Fixes: #3507
2024-10-09 23:25:17 +02:00
Robin Malfait 3b047fc670 update changelog 2024-10-08 23:09:49 +02:00
Robin Malfait 242225000f 2.1.9 - @headlessui/react 2024-10-03 11:57:46 +02:00
Robin Malfait 02b43d042d Cleanup process in Combobox component when using virtualization (#3495)
This PR is a different approach compared to #3487. 

Instead of checking whether we are in a test environment (specifically
in a Jest environment), I think we can just get rid of the check
entirely and use the virtualizer in all environments.

This will remove an unnecessary check for `process` being available and
gets rid of `process` entirely. It also fixes an issue that #3487 tries
to solve where `process` is available, but `process.env` is not.

Closes: #3487
2024-09-27 11:45:45 +02:00
Robin Malfait 63daa86b61 Fix crash when using instanceof HTMLElement in some environments (#3494)
This PR fixes an issue where in some environments where `HTMLElement` is
not
available (on the server) and AG Grid is used, we crashed.

This happens because the `HTMLElement` is polyfilled to an empty object.
This means that the `typeof HTMLElement !== 'undefined'` check passed,
but the `v instanceof HTMLElement` translated to `v instanceof {}` which
is invalid and resulted in a crash...

This PR solves it by checking for exactly what we need, in this case
whether the `outerHTML` property is available.

Alternatively, we could use `return v?.outerHTML ?? v`, but not sure if
that's always safe to do.

Fixes: #3471
2024-09-27 11:39:42 +02:00
Robin Malfait f2c80c4207 Ensure Element is available before polyfilling (#3493)
In some environments `Element` won't be available, which is needed for
the `Element.prototype.getAnimations` polyfill. If `Element` is not
available at all, it means that we are not in a browser so we don't need
the polyfill.

Fixes: #3490
2024-09-27 11:32:12 +02:00
Robin Malfait 994303f936 2.1.8 - @headlessui/react 2024-09-12 12:35:23 +02:00
Robin Malfait b6708960b8 Fix crash when using as={Fragment} on MenuButton, ListboxButton, DisclosureButton or Button components (#3478)
This PR fixes an issue where a maximum update depth exceeded error was
thrown when using `as={Fragment}` on button related components.

The issue here is that the `ref` on a element would re-fire every render
_if_ the a function was used _and_ the function is a new function (aka
not a stable function).

This resulted in the `ref` being called with the DOM element, then
`null`, then the DOM element, then `null`, and so on.

To solve this, we have to make sure that the `ref` is always a stable
reference.

Fixes: #3476
Fixes: #3439
2024-09-12 12:35:03 +02:00
Robin Malfait dde00da9e7 2.1.7 - @headlessui/react 2024-09-11 17:29:20 +02:00
Robin Malfait 4737c6df97 Prevent crash in environments where Element.prototype.getAnimations is not available (#3473)
Recently we made improvements to the `Transition` component and internal
`useTransition` hook. We now use the `Element.prototype.getAnimations`
API to know whether or not all transitions are done.

This API has been available in browsers since 2020, however jsdom
doesn't have support for this. This results in a lot of failing tests
where users rely on jsdom (e.g. inside of Jest or Vitest).

In a perfect world, jsdom is not used because it's not a real browser
and there is a lot you need to workaround to even mimic a real browser.

I understand that just switching to real browser tests (using Playwright
for example) is not an easy task that can be done easily.

Even our tests still rely on jsdom…

So to make the development experience better, we polyfill the
`Element.prototype.getAnimations` API only in tests
(`process.env.NODE_ENV === 'test'`) and show a warning in the console on
how to proceed.

The polyfill we ship simply returns an empty array for
`node.getAnimations()`. This means that it will be _enough_ for most
tests to pass. The exception is if you are actually relying on
`transition-duration` and `transition-delay` CSS properties.


The warning you will get looks like this:
``````
Headless UI has polyfilled `Element.prototype.getAnimations` for your tests.
Please install a proper polyfill e.g. `jsdom-testing-mocks`, to silence these warnings.

Example usage:
```js
import { mockAnimationsApi } from 'jsdom-testing-mocks'
mockAnimationsApi()
```
``````

Fixes: #3470
Fixes: #3469
Fixes: #3468
2024-09-11 17:19:55 +02:00
Robin Malfait 5b365f5cae 2.1.6 - @headlessui/react 2024-09-09 21:14:18 +02:00
Robin Malfait ef78d58a2e Fix crash when using DisclosureButton inside of a DisclosurePanel when the Disclosure is open by default (#3465)
This PR fixes an issue where React hooks were called unconditionally>

The `PopoverButton` and `DisclosureButton` act as a `CloseButton` when
used inside of a panel. We conditionally handled the `ref` when it's
inside a panel. To ensure that the callback is stable, the conditionally
used function was wrapped in a `useEvent(…)` hook.

This seemed to be ok (even though we break the rules of hooks) because a
button can only be in a panel or not be in a panel, but it can't switch
during the lifetime of the button. Aka, the rules of hooks are not
broken because all code paths lead to the same hooks being called.

```ts
<Disclosure defaultOpen>
  <DisclosureButton>Open</DisclosureButton>
  <DisclosurePanel>
    <DisclosureButton>Close</DisclosureButton>
  </DisclosurePanel>
<Disclosure>
```

But... it can be called conditionally, because the way we know whether
we are in a panel relies on a state value which comes from context and
is populated by a `useEffect(…)` hook.

The reason we didn't catch this in the `Disclosure` component, is
because all the state is stable and known by the time the
`DisclosurePanel` opens. But if you use the `defaultOpen` prop, the
`DisclosurePanel` is already open and then the state is not ready yet
(because we have to wait for the `useEffect(…)` hook).

Long story short, moved the `isWithinPanel` check inside the
`useEvent(…)` hook that holds the stable function which means that we
don't call this hook unconditionally anymore.
2024-09-09 21:13:45 +02:00
Robin Malfait 07c9f1f711 Fix ListboxOptions being incorrectly marked as inert (#3466)
This PR fixes an issue where the `ListboxOptions` component was
incorrectly marked as `inert`.

We only mark the other elements on the page as `inert` once the
`Listbox` is in a visible state. The issue is that the
`data.optionsElement` (a reference to the DOM node) was not populated
with the actual DOM node yet at the time the `useInertOthers(…)` hook
was applied.

Due to the usage of `useEvent(…)`, instead of `useCallback(…)` the
internal `useEffect(…)` hook didn't re-run because the `allowed`
function was already stable.

With this fix, the `allowed` function will change whenever its
dependencies change.

Fixes: #3464
2024-09-09 21:09:48 +02:00
Robin Malfait cb86665f5b 2.1.5 - @headlessui/react 2024-09-04 16:36:30 +02:00
Robin Malfait 971ff6b67e Fix transition bug on Firefox, triggered by clicking the PopoverButton in rapid succession (#3452)
We recently landed a fix for `Popover`s not closing correctly when using
the `transition` prop (#3448). Once this fix was published, some users
still ran into issues using Firefox on Windows (see:
https://github.com/tailwindlabs/tailwindui-issues/issues/1625).

One fun thing I discovered is that transitions somehow behave
differently based on where they are triggered from (?). What I mean by
this is that holding down the <kbd>space</kbd> key on the button does
properly open/close the `Popover`. But if you rapidly click the button,
the `Popover` will eventually get stuck.

> Note: when testing this, I made sure that the handling of the `space`
key (in a `keydown` handler) and the clicking of the mouse (handled in a
`click` handler) called the exact same code. It still happened.

The debugging continues…

One thing I noticed is that when the `Popover` gets stuck, it meant that
a transition didn't properly complete.

The current implementation of the internal `useTransition(…)` hook has
to wait for all the transitions to finish. This is done using a
`waitForTransition(…)` helper. This helper sets up some event listeners
(`transitionstart`, `transitionend`, …) and waits for them to fire.

This seems to be unreliable on Firefox for some unknown reason.

I knew the code for waiting for transitions wasn't ideal, so I wanted to
see if using the native `node.getAnimations()` simplifies this and makes
it work in general.

Lo and behold, it did! 🎉

This now has multiple benefits:

1. It works as expected on Firefox
2. The code is much much simpler
3. Uses native features

The `getAnimations(…)` function is supported in all modern browsers
(since 2020). At the time it was too early to rely on it, but right now
it should be safe to use.

Fixes: https://github.com/tailwindlabs/tailwindui-issues/issues/1625
2024-09-04 16:09:33 +02:00
Robin Malfait 75619eef3b 2.1.4 - @headlessui/react 2024-09-03 17:23:03 +02:00
Robin Malfait 071aa0e260 Fix components not properly closing when using the transition prop (#3448)
This PR fixes a bug where the components don't always properly close
when using the `transition` prop on those components.

The issue here is that the internal `useTransition(…)` hook relies on a
DOM node. Whenever the DOM node changes, we need to re-run the
`useTransition(…)`. This is why we store the DOM element in state
instead of relying on a `useRef(…)`.

Let's say you have a `Popover` component, then the structure looks like
this:
```ts
<Popover>
  <PopoverButton>Show</PopoverButton>
  <PopoverPanel>Contents</PopoverPanel>
</Popover>
```

We store a DOM reference to the button and the panel in state, and the
state lives in the `Popover` component. The reason we do that is so that
the button can reference the panel and the panel can reference the
button. This is needed for some `aria-*` attributes for example:
```ts
<PopoverButton aria-controls={panelElement.id}>
```

For the transitions, we set some state to make sure that the panel is
visible or hidden, then we wait for transitions to finish by listening
to transition related events on the DOM node directly.

If you now say, "hey panel, please re-render because you have to become
visible/hidden" then the component re-renders, the panel DOM node
(stored in the `Popover` component) eventually updates and then the
`useTransition(…)` hooks receives the new value (either the DOM node or
null when the leave transition is complete).

The problem here is the round trip that it first has to go to the root
`<Popover/>` component, re-render everything and provide the new DOM
node to the `useTransition(…)` hook.

The solution? Local state so that the panel can re-render on its own and
doesn't require the round trip via the parent.

Fixes: https://github.com/tailwindlabs/headlessui/issues/3438
Fixes: https://github.com/tailwindlabs/headlessui/issues/3437
Fixes: https://github.com/tailwindlabs/tailwindui-issues/issues/1625

---------

Co-authored-by: Jonathan Reinink <jonathan@reinink.ca>
2024-09-03 17:19:13 +02:00
Philipp Spiess 49c081d199 2.1.3 - @headlessui/react 2024-08-23 15:49:35 +02:00
Philipp Spiess a08be96f8a Add client hint to close provider (#3429)
This PR adds a missing client boundary in the close provider file that
was causing crashes when used with Turbopack as reported in the Next.js
repo: https://github.com/vercel/next.js/issues/68205

## Test plan

Thanks to @richardasymmetric [fantastic
repro](https://github.com/vercel/next.js/issues/68205 ) I could check
out the example repo and link a tarball build of the updated
`@headlessui/react` package to validate that this is enough to resolve
the issue. After this change, `next dev` and `next dev --turbo` work in
the same way.
2024-08-19 15:07:23 +02:00
Robin Malfait 226042231d Fix closing components using the transition prop, and after scrolling the page (#3407)
* `useDidElementMove`: handle `HTMLElement`

This change should be temporary, and it will allow us to use the
`useDidElementMove` with ref objects and direct `HTMLElement`s.

* `useResolveButtonType`: handle `HTMLElement`

This change should be temporary, and it will allow us to use the
`useResolveButtonType` hook with ref objects and direct `HTMLElement`s.

* `useRefocusableInput`: handle `HTMLElement`

This change should be temporary, and it will allow us to use the
`useRefocusableInput` hook with ref objects and direct `HTMLElement`s.

* `useTransition`: handle `HTMLElement`

Accept `HTMLElement| null` instead of `MutableRefObject<HTMLElement |
null>` in the `useTransition` hook.

* ensure `containers` are a dependency of `useEffect`

* `Menu`: track `button` and `items` elements in state

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.

* `Combobox`: track `input`, `button` and `options` elements in state

* `Disclosure`: track `button` and `panel` elements in state

* `Listbox`: track `button` and `options` elements in state

* `Popover`: track `button` and `panel` elements in state

* `Transition`: track the `container` element in state

* remove incorrect leftover `style=""` attribute

* simplify `useDidElementMove`, only accept `HTMLElement | null`

This doesn't support the `MutableRefObject<HTMLElement | null>` anymore.

* pass `HTMLElement | null` directly to `useResolveButtonType`

* simplify `useResolveButtonType`, only handle `HTMLElement | null`

We don't handle `MutableRefObject<HTMLElement | null>` anymore

* simplify `useRefocusableInput`

* simplify `useElementSize`

* simplify `useOutsideClick`

Only accept `HTMLElement | null` instead of `MutableRefObject<HTMLElement | null>`

* do not rely on `HTMLButtonElement` being available

* update changelog
2024-08-02 22:45:11 +02:00
Robin Malfait 13b3100906 Ensure Transition component state doesn't change when it becomes hidden (#3372)
* do not change visibility of `Transition` component

This was originally introduced in
https://github.com/tailwindlabs/headlessui/pull/1519 to fix an issue
where some enter transitions where broken: https://github.com/tailwindlabs/headlessui/issues/1503

However, since we refactored the `Transition` component to make use of
the `useTransition` hook, I can't seem to reproduce this issue anymore.

In fact, removing this code fixes an issue.

The bigger issue here is that when the component becomes hidden, that we
always set the state to hidden as well. But if it becomes visible again,
we don't show it again.

Right now, since I couldn't reproduce any of the other issue, I opted to
just removing the code entirely. But if we ever need it again, we
probably have to make sure that it becomes visible in the other scenario
as well.

Fixes: #3328

* update changelog
2024-07-10 11:28:28 +02:00
Robin Malfait a36380fdb6 2.1.2 - @headlessui/react 2024-07-05 18:13:14 +02:00
Robin Malfait 5fb738f693 Fix flushSync warning for Combobox component with immediate prop enabled (#3366)
* wrap flushSync call in microTask

This will make sure that React is able to flush this correctly by delaying the call using a microTask.

* update changelog

* reformat comments

Now that it's nested, let's adjust the width of the comments
2024-07-05 17:46:16 +02:00
Robin Malfait 91e959714b Fix restoring focus to correct element when closing Dialog component (#3365)
* resolve focusable element when recording elements

Right now, we have to record when a click / mousedown / focus event happens on an element. But when you click on a non-focusable element inside of a focusable element then we record the inner element instead of the outer one.

This happens in this scenario:
```html
<button>
  <span>click me</span>
</button>
```

This solves it by resolving the closest focusable element (and we fallback to the e.target as a last resort)

* update changelog
2024-07-05 16:14:50 +02:00
Robin Malfait da466ecf9c Fix outside click in nested portalled Popover components (#3362)
* use `span` as default element for `Hidden` component

This improves the HTML DOM tree if this happens to be used in let's say
a `p` tag where `div` elements are not allowed. The `Hidden` element is
hidden so it doesn't really matter what the underlying element is.

Fixes: #3319

* refactor `useRootContainers` and introduce `MainTreeProvider`

As a general recap, when an outside click happens, we need to react to
it and typically use the `useOutsideClick` hook.

We also require the context of "allowed root containers", this means
that clicking on a 3rd party toast when a dialog is open, that we allow
this even though we are technically clicking outside of the dialog. This
is simply because we don't have control over these elements.

We also need a reference to know what the "main tree" container is,
because this is the container where your application lives and we _know_
that we are not allowed to click on anything in this container. The
complex part is getting a reference to this element.

```html
<html>
<head>
   <title></title>
</head>
<body>
   <div id="app"> <-- main root container -->
      <div></div>
      <div>
         <Popover></Popover> <!-- Current component -->
      </div>
      <div></div>
   </div>

   <!-- Allowed container #1 -->
   <3rd-party-toast-container></3rd-party-toast-container>
</body>

<!-- Allowed container #2 -->
<grammarly-extension></grammarly-extension>
</html>
```

Some examples:

- In case of a `Dialog`, the `Dialog` is rendered in a `Portal` which
  means that a DOM ref to the `Dialog` or anything inside will not point
  to the "main tree" node.

- In case of a `Popover` we can use the `PopoverButton` as an element
  that lives in the main tree. However, if you work with nested
  `Popover` components, and the outer `PopoverPanel` uses the `anchor`
  or `portal` props, then the inner `PortalButton` will not be in the
  main tree either because it will live in the portalled `PopoverPanel`
  of the parent.

This is where the `MainTreeProvider` comes in handy. This component will
use the passed in `node` as the main tree node reference and pass this
via the context through the React tree. This means that a nested
`Popover` will still use a reference from the parent `Popover`.

In case of the `Dialog`, we wrap the `Dialog` itself with this provider
which means that the provider will be in the main tree and can be used
inside the portalled `Dialog`.

Another part of the `MainTreeProvider` is that if no node exists in the
parent (reading from context), and no node is provided via props, then
we will briefly render a hidden element, find the root node of the main
tree (aka, the parent element that is a direct child of the body, `body
> *`). Once we found it, we remove the hidden element again. This way we
don't keep unnecessary DOM nodes around.

* update changelog

* Update packages/@headlessui-react/src/hooks/use-root-containers.tsx

Co-authored-by: Jordan Pittman <jordan@cryptica.me>

* Update packages/@headlessui-react/src/hooks/use-root-containers.tsx

Co-authored-by: Jordan Pittman <jordan@cryptica.me>

* Update packages/@headlessui-react/src/hooks/use-root-containers.tsx

Co-authored-by: Jordan Pittman <jordan@cryptica.me>

* Update packages/@headlessui-react/src/hooks/use-root-containers.tsx

Co-authored-by: Jordan Pittman <jordan@cryptica.me>

* Update packages/@headlessui-react/src/hooks/use-root-containers.tsx

Co-authored-by: Jordan Pittman <jordan@cryptica.me>

* use early return

---------

Co-authored-by: Jordan Pittman <jordan@cryptica.me>
2024-07-05 10:11:51 +02:00
Robin Malfait d8f44e0cd7 Fix transition and focus prop combination for PopoverPanel component (#3361)
* trigger effect when ref value changes

* update changelog
2024-07-04 12:36:12 +02:00
Robin Malfait 70f88f4e26 Fix hanging tests when using anchor prop (#3357)
* add test that verifies unit test hang

* bail when parsing the `maxHeight` results in `NaN`

* playground cleanup

Testing using this playground example, so cleaned it up to be more
modern using newer components, transition prop and so on.

* use CSS instead of JS

Let's make it a CSS problem instead of a JS problem. The
`round(up, <valueToRound>, <roundingInterval>)` will behave similar to a
`Math.ceil()` that we had in the JS implementation.

See: https://developer.mozilla.org/en-US/docs/Web/CSS/round

* Remove CSS solution for now

I want to re-enable this in the future, but unfortunately for now we
can't use it because Chrome only introduced support for this in the last
2 months.

This reverts commit daac60d45ec3f02b324d0d8b18078a995e885733.

* update changelog
2024-07-03 22:37:44 +02:00
Robin Malfait d65829b08a Fix crash in Combobox component when in virtual mode when options are empty (#3356)
* bump `@tanstack/react-virtual`

* only enable the virtualizer when there are options

* update changelog
2024-07-03 00:37:02 +02:00
Robin Malfait 990b1796a5 Ensure unmount on Dialog works in combination with the transition prop on DialogBackdrop and DialogPanel components (#3352)
* inherit `unmount` from `Dialog` in `DialogBackdrop` and `DialogPanel` components

Only the `Dialog` accepts an `unmount` prop because it's the `Dialog`
that is conditionally rendered and the `DialogBackdrop` and
`DialogPanel` will conditionally show together with the `Dialog`.

However, now that the `Dialog` is wrapped in a `Transition` (which can
be unmounted) and the `DialogBackdrop` and `DialogPanel` will also be
wrapped in a `TransitionChild` (when the `transition` prop is passed)
then we do have to deal with the `unmount` state on the `TransitionChild`.

This is important because if you make the `Dialog` `unmount={false}`, then the
`DialogPanel` will still unmount because the `TransitionChild` is unmounting its
children. This now means that you will lose data (such as form state of inputs).

This commit solves that by inheriting the `unmount` state of the
`Dialog` in the `TransitionChild` wrappers such that they behave the way
you expect them to behave.

* update changelog
2024-07-01 15:24:01 +02:00
Robin Malfait fbad6a9b06 Fix prematurely added anchoring styles on ListboxOptions (#3337)
* fix prematurely adding anchoring styles on `ListboxOptions`

* update changelog
2024-06-27 13:09:28 +02:00
Robin Malfait abd86fca3e 2.1.1 - @headlessui/react 2024-06-26 15:10:43 +02:00
Robin Malfait 3224a9e34d Fix incorrect Transition boundary for Dialog component (#3331)
* always wrap `Dialog` in a `Transition`

Initially we didn't do this, because it's a bit silly to do that if you
already had a `Transition` component on the outside. E.g.:

```tsx
<Transition show={open}>
  <Dialog onClose={() => setOpen(false)}>
    {/* ... */}
  </Dialog>
</Transition>
```

Because this means that we technically have this:
```tsx
<Transition show={open}>
  <Dialog onClose={() => setOpen(false)}>
    <Transition>
      <InternalDialog>
        {/* ... */}
      </InternalDialog>
    </Dialog>
  </Transition>
</Transition>
```

The good part is that the inner `Transition` is rendering a `Fragment`
and forwards all the props to the underlying element (the internal
dialog).

This way we have a guaranteed transition boundary.

* use public `transition` API instead of private internal API

This also mimics better what we are actually trying to do.

* update changelog
2024-06-26 15:07:48 +02:00
Jordan Pittman 18fcde7330 Update changelog 2024-06-25 12:53:09 -04:00
Robin Malfait ff41b27d27 Fix initial anchor="selection" state (#3324)
* compute `selectedOptionIndex` when using `anchor="selection"`

Instead of relying on the DOM directly, we can compute the
`selectedOptionIndex` and rely on the data directly.

We will also freeze the value while closing to prevent UI changes.

* update changelog
2024-06-25 15:16:33 +02:00