Commit Graph

895 Commits

Author SHA1 Message Date
Robin Malfait 8a24040316 2.2.3 - @headlessui/react 2025-05-12 23:48:06 +02:00
Robin Malfait ad7300b076 Fix closing Menu when other Menu is opened (#3726)
Fixes: #3701

This PR fixes an issue where an open `Menu` is not closed when opening a
new `Menu`. This is also fixed for `Listbox` and `Combobox` that used
the same techniques.

This happened because we recently shipped an improvement where the
`Menu` opens on `pointerdown` instead of on `click`. This means that the
`useOutsideClick` hook was not correct anymore because it relies on
`click`.

We could try and figure out that we should already close on
`pointerdown` but this might not be expected for other components.
Instead we want to simplify things a bit and ideally not even worry
about what event caused a specific state change.

Instead of trying to fight timing issues when certain events happen,
this PR takes a slightly different approach.

We already had the concept of a "top-layer" similar to the browser's
`#top-layer` (when using native `dialog`). This essentially lets us know
which component sits on top of the hierarchy.

This top-layer is important because when you have the following
structure:

```
<Dialog>
  <Menu />
</Dialog>
```

Assuming that both the `Dialog` and `Menu` are open, clicking outside or
pressing escape should _only_ close the `Menu`. Once the `Menu` is
closed, we should close the `Dialog`.

In this case, we can enable/disable the `useOutsideClick` hook based on
whether the current component is the top-layer or not.

Some components like the `Menu`, `Listbox` and `Combobox` should
immediately close when they are not the top-layer anymore. A `Dialog`
can stay open, because you can have interactable elements like the
example above in the `Dialog`.

Luckily, these components that should immediately close already use
their own state machine. This allows us to listen to the `OpenMenu` (or
`OpenListbox`, `OpenCombobox`) event, and if that happens, we can push
the current component on the shared stack machine.

This now means that it doesn't matter _how_ the `Menu` is opened, but
the moment a user event (click, enter, ...) opens the `Menu`, we now
that we are on top of the stack.

All other components could listen to push events on the stack. Once
those happen, we can close the current component immediately. This has
the nice side effect that we don't have to use a `useEffect` to check
for state changes. We can just act immediately when an event happens.

The `useOutsideClick` hooks is still used and useful in situations where
you literally just clicked somewhere else. But in case you are opening
another `Menu` or another `Listbox`, we can immediately close the one
that was open before.

## Test plan

Before:


https://github.com/user-attachments/assets/f2efd94b-9aa2-404c-ad54-c8747b4d46ac

After:


https://github.com/user-attachments/assets/25c78fc4-c1da-4e51-89b6-4270f2804ab0
2025-05-12 23:45:59 +02:00
Robin Malfait afc04bc288 Implement Popover using a separate state machine (#3725)
This PR is an internal refactor, similar to what we did recently for the
`Menu`, `Listbox` and `Combobox` components by using a dedicated state
machine.

This is basically a one-to-one translation without any further
optimizations


## Test plan

1. All tests are passing
1. Verified that the Popover still works as expected:
https://headlessui-react-git-chore-popover-using-st-7fa062-tailwindlabs.vercel.app/popover/popover
2025-05-12 20:09:44 +02:00
Robin Malfait 7ff4b5bb9b fix typo
+ drop unnecessary hook dependency
2025-05-10 02:34:36 +02:00
Robin Malfait 18cf98454d Performance improvement: only re-render top-level component (#3722)
This PR fixes a performance issue where all components using the
`useIsTopLayer` hook will re-render when the hook changes.

For context, the internal hook is used to know which component is the
top most component. This is important in a situation like this:

```
<Dialog>
  <Menu />
</Dialog>
```

If the Menu inside the Dialog is open, it is considered the top most
component. Clicking outside of the Menu or pressing escape should only
close the Menu and not the Dialog.

This behavior is similar to the native `#top-layer` you see when using
native dialogs for example.

The issue however is that the `useIsTopLayer` subscribes to an external
store which is shared across all components. This means that when the
store changes, all components using the hook will re-render.

To make things worse, since we can't use these hooks unconditionally,
they will all be subscribed to the store even if the Menu component(s)
are not open.

To solve this, we will use a new state machine and use the `useMachine`
hook. This internally uses a `useSyncExternalStoreWithSelector` to
subscribe to the store.

This means that the component will only re-render if the state computed
by the selector changes.

This now means that at most 2 components will re-render when the store
changes:

1. The component that _was_ in the top most position
2. The component that is going to be in the top most position

Fixes: #3630
Closes: #3662


# Test plan

Behavior before: notice how all Menu components re-render:


https://github.com/user-attachments/assets/3172b632-0fa4-42db-970c-39efc827dd84

After this change, only the Menu that was opened / closed will
re-render:


https://github.com/user-attachments/assets/5d254bfc-5233-47a7-94d3-eb7a8593e14f
2025-05-10 02:31:19 +02:00
Robin Malfait 662663d06a revert accidentally removed part of test 2025-05-09 21:45:16 +02:00
Robin Malfait 130b3d76d5 bump Tailwind CSS 2025-05-09 16:16:23 +02:00
Robin Malfait a7e0f0a937 Simplify internal tests (#3720)
This PR simplifies the internal tests a bit.

1. Don't explicitly test if a component has a specific ID
1. Don't mock the `useId` hook if it's not necessary

What we care about more is that 2 components (E.g.: `MenuButton` and
`MenuItems`) are connected to each other. This is done via `id` and
`aria-controls` attributes. The exact ID is not important.

The main motivation for this is that every time we introduce some
`useId()` hook call somewhere, the IDs will shift and it will look like
some tests are broken.

If we are not explicitly testing the IDs, we also don't really care
about deterministic incrementing IDs in tests, so therefore we can
remove some `useId` mocking.

Note: some tests still have mocks like this (e.g.: `description.test.ts`
& `label.test.ts`) but that's because they have some snapshot tests.
2025-05-09 12:04:16 +02:00
Robin Malfait c9f8f30b90 Fix Listbox not focusing first or last option on ArrowUp/ArrowDown (#3721)
This PR fixes an issue where if a Listbox does not have a value yet, and
it's opened via an ArrowUp or ArrowDown (on the ListboxButton) then it
didn't correctly go to the firs or last option.

Before, we were opening the listbox in a `flushSync()` call, after that
call we were focusing the first or last option depending on if you used
the ArrowDown or ArrowUp keys.

However, the options can and will be registered at a later point in
time, which means that the focus of first or last option is technically
going to fail because no options are available yet.

With this fix we don't need the `flushSync` call, and instead we
passthrough a pending focus. Once the options are registered, if a
pending focus is present, only then will we focus the correct option.

This gets rid of timing issues.
2025-05-09 09:58:00 +00:00
Kamil Dzieniszewski 0b8deaf7a9 Update @react-aria/focus and @react-aria/interactions dependencies to latest versions (#3712)
Support React 19

Fixes: #3711
2025-05-04 15:18:25 +02:00
Robin Malfait 30a6d51665 Fix focus not returned to SVG Element (#3704)
This PR fixes an issue where the focus is not returned to an `SVG`
element with a `tabIndex` correctly.

There are a few issues going on here:

1. We assume that the element to focus (`e.target`) is an instanceof
`HTMLElement`, but the `SVGElement` is not an instanceof `HTMLElement`.
2. By using `instanceof` we are checking against concrete classes, so if
this happen to cross certain contexts (Shadow DOM, Iframes, ...) then
the instances would be different.

To solve this, we will now:

1. Relax the types and only care about the actual attributes and methods
we are interested in. In most cases this means changing internal types
from `HTMLElement` to `Element` for example.
2. We will check whether certain properties are available in the object
to deduce the correct type from the object.

Fixes: #3660

## Test plan

Added an SVG to open a Dialog component and made sure that pressing
`escape` or clicking outside of the Dialog does restore the focus to the
SVG itself.
```tsx
<svg
  tabIndex={0}
  onKeyDown={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      setIsOpen((v) => !v)
    }
  }}
  onClick={() => setIsOpen((v) => !v)}
  className="h-6 w-6 text-gray-500"
>
  <BookOpenIcon />
</svg>
```


Here is a video of that behavior:


https://github.com/user-attachments/assets/1805ca67-8bc7-4315-98a7-2490cba9230c
2025-04-25 14:52:32 +02:00
Robin Malfait 0558bdb68e update changelog 2025-04-25 13:50:04 +02:00
Robin Malfait 3e3f45df81 Ensure clicking on interactive elements inside <Label> works (#3709)
This PR fixes an issue where clicking on an interactive element _inside_
of a `<Label>` component should work as expected.

For example, if you have this situation:

```html
<label for="tac">
  <input id="tac" type="checkbox" name="terms-and-conditions" />
  I agree to the <a href="terms-and-conditions.html">Terms and Conditions</a>
</label>
```

Clicking on the `<a href="#">` inside the label should _not_ check the
checkbox, but should open the link instead.

Fixes: #3658
2025-04-25 11:48:13 +00:00
Robin Malfait ca05e7c0ee Fix clicking <Label /> opens <input type="file"> (#3707)
This PR fixes an issue where the `<Label>` component didn't open the
`<input type="file">` when clicking it.

For native elements, the `Label` component already renders a native
`<label>` behind the scenes. Some native elements like `<input
type="checkbox">` immediately change the state of the element whereas
some other elements don't such as `<select></select>` you just get the
focus.

However, `<input type="file">` should also immediately open the file
picker when clicking the label and this was not the case. This PR fixes
that.

Since we are already using a native `<label>` _and_ linking the
`<label>` with its `<input type="file">` performing a `.click()` is
allowed.

Fixes: #3680


## Test plan

You can play with it here:
https://headlessui-react-git-fix-issue-3680-tailwindlabs.vercel.app/combinations/form

This video shows how it behaves now:


https://github.com/user-attachments/assets/26467f83-d91d-4a79-98f9-dd91214ea037
2025-04-25 12:46:52 +02:00
Robin Malfait 1461b65810 Add a quick trigger action to the Menu, Listbox and Combobox components (#3700)
This PR adds a new quick trigger feature to the `Menu`. Not sure what
the best
name for this is, but essentially this is the behavior:

Recently we made sure that the `Menu` opens on `mousedown` (not just
`click`).

This means that we can perform the following quick action:
1. `mousedown` on the `MenuButton` — this will open the `Menu`
2. Without releasing the mouse button yet, move your mouse over one of
the `MenuItem`s — this will highlight the currently active `MenuItem`.
3. Release the mouse button — this will invoke the currently active
`MenuItem` and close the `Menu`.

This now means that you can perform actions very quickly.

What this PR doesn't do yet is if you have a scrollable list, then it
won't scroll up or down when you reach the ends of the list. For this we
would need to introduce some new elements. The native Menu items on
macOS show a little placeholder arrow. If you put your cursor in that
area, it starts scrolling:

<img width="489" alt="image"
src="https://github.com/user-attachments/assets/e3a90d5a-daa7-4711-9e19-050578be3e02"
/>


## Test plan

1. Everything still works as expected
2. Quick release has been added:

- Listbox:
https://headlessui-react-git-feat-quick-trigger-tailwindlabs.vercel.app/listbox/listbox-with-pure-tailwind
- Menu:
https://headlessui-react-git-feat-quick-trigger-tailwindlabs.vercel.app/menu/menu
- Combobox:
https://headlessui-react-git-feat-quick-trigger-tailwindlabs.vercel.app/combobox/combobox-countries
2025-04-24 16:01:38 +02:00
Robin Malfait 730ab68345 run prettier 2025-04-17 15:21:29 +02:00
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 e10f54bc12 Migrate React playground to Tailwind CSS v4 (#3695)
This PR bumps the internal React playground to use Tailwind CSS v4
2025-04-11 19:28:04 +02:00
Jordan Pittman dc30c09ab0 Only register an option once in strict mode (#3692)
I'm not 100% sure this is the right fix but it _does_ fix keyboard
navigation in listbox and menu when using strict mode.

Basically the same items/options are being registered more than once and
that causes the arrow up/down logic to only advance on every other
keypress.
2025-04-11 11:27:24 +02:00
Robin Malfait 51775d2f1b Improve performance of Listbox and Menu when closing (#3690)
In a previous PR, we already batched registering options. This PR also
batches unregistering options to make the closing behavior smoother when
there are a lot of items rendered.
2025-04-10 22:01:43 +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 e2a63760aa Prepare for performance improvements (#3684)
This PR is just a chore to prepare for future performance optimizations.
Essentially I want to improve the performance of the `Menu`, `Listbox`
and `Combobox` components but I want to do it in separate PRs such that
reverting the improvements can be done if needed.

This PR just sets up a `Machine` for state machines, and adds some
helpers such as a `useSlice` to calculate parts of the state machine.
Component using the `useSlice` will only re-render _if_ the slice
changes.

So apart from adding a library (`useSyncExternalStoreWithSelector`) and
adding some setup code. Nothing in this PR changes the behavior of the
components.
2025-04-10 22:26:12 +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
Philipp Spiess 0a8de016e8 Bring back issue template 2025-02-27 14:56:04 +01: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 1be0e67c76 0.2.2 - @headlessui/tailwindcss 2025-02-06 14:21:42 +01:00
Philipp Spiess 4f5506ef99 Support installing with Tailwind CSS v4 (#3634)
Resolves #3633

Bumps the version range for `@headlessui/tailwindcss` to ensure support
with Tailwind CSS v4.
2025-02-06 14:19:45 +01:00
Robin Malfait b6f355d154 update changelog 2024-12-12 17:40:57 +01:00
Kevin Chung 058a14b058 Bump @tanstack/react-virtual (#3588)
`@tanstack/react-virtual` added peer deps support for React 19 in
[v3.11.0](https://github.com/TanStack/virtual/releases/tag/v3.11.0)
(https://github.com/TanStack/virtual/pull/893)

This PR upgrades the packages. This can resolve some warnings on some
React 19 projects, e.g.:

```
npm warn ERESOLVE overriding peer dependency
npm warn While resolving: @tanstack/react-virtual@3.10.9
npm warn Found: react@19.0.0
npm warn node_modules/react
npm warn   peer react@"^18 || ^19 || ^19.0.0-rc" from @headlessui/react@2.2.0
npm warn   node_modules/@headlessui/react
npm warn     @headlessui/react@"^2.2.0" from the root project
npm warn   15 more (@floating-ui/react, @floating-ui/react-dom, ...)
npm warn
npm warn Could not resolve dependency:
npm warn peer react@"^16.8.0 || ^17.0.0 || ^18.0.0" from @tanstack/react-virtual@3.10.9
npm warn node_modules/@headlessui/react/node_modules/@tanstack/react-virtual
npm warn   @tanstack/react-virtual@"^3.8.1" from @headlessui/react@2.2.0
npm warn   node_modules/@headlessui/react
npm warn
npm warn Conflicting peer dependency: react@18.3.1
npm warn node_modules/react
npm warn   peer react@"^16.8.0 || ^17.0.0 || ^18.0.0" from @tanstack/react-virtual@3.10.9
npm warn   node_modules/@headlessui/react/node_modules/@tanstack/react-virtual
npm warn     @tanstack/react-virtual@"^3.8.1" from @headlessui/react@2.2.0
npm warn     node_modules/@headlessui/react
npm warn ERESOLVE overriding peer dependency
npm warn While resolving: @tanstack/react-virtual@3.10.9
npm warn Found: react-dom@19.0.0
npm warn node_modules/react-dom
npm warn   peer react-dom@"^18 || ^19 || ^19.0.0-rc" from @headlessui/react@2.2.0
npm warn   node_modules/@headlessui/react
npm warn     @headlessui/react@"^2.2.0" from the root project
npm warn   5 more (@floating-ui/react, @floating-ui/react-dom, ...)
npm warn
npm warn Could not resolve dependency:
npm warn peer react-dom@"^16.8.0 || ^17.0.0 || ^18.0.0" from @tanstack/react-virtual@3.10.9
npm warn node_modules/@headlessui/react/node_modules/@tanstack/react-virtual
npm warn   @tanstack/react-virtual@"^3.8.1" from @headlessui/react@2.2.0
npm warn   node_modules/@headlessui/react
npm warn
npm warn Conflicting peer dependency: react-dom@18.3.1
npm warn node_modules/react-dom
npm warn   peer react-dom@"^16.8.0 || ^17.0.0 || ^18.0.0" from @tanstack/react-virtual@3.10.9
npm warn   node_modules/@headlessui/react/node_modules/@tanstack/react-virtual
npm warn     @tanstack/react-virtual@"^3.8.1" from @headlessui/react@2.2.0
npm warn     node_modules/@headlessui/react
```
2024-12-12 17:38:38 +01:00
Robin Malfait 03fe3c573d Use correct ownerDocument when using internal <Portal/> (#3594)
This PR improves the internal `<Portal>` component by allowing to pass
in a custom `ownerDocument`.

This fixes an issue if you do something like this:

```ts
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
import { useState } from 'react'
import { createPortal } from 'react-dom'

export default function App() {
  let [target, setTarget] = useState(null)

  return (
    <div className="grid min-h-full place-content-center">
      <iframe
        ref={(iframe) => {
          if (!iframe) return
          if (target) return

          let el = iframe.contentDocument.createElement('div')
          iframe.contentDocument.body.appendChild(el)
          setTarget(el)
        }}
        className="h-[50px] w-[75px] border-black bg-white"
      >
        {target && createPortal(<MenuExample />, target)}
      </iframe>
    </div>
  )
}

function MenuExample() {
  return (
    <Menu>
      <MenuButton>Open</MenuButton>
      <MenuItems
        anchor="bottom"
        className="flex min-w-[var(--button-width)] flex-col bg-white shadow"
      >
        <MenuItem>
          <a className="block data-[focus]:bg-blue-100" href="/settings">
            Settings
          </a>
        </MenuItem>
        <MenuItem>
          <a className="block data-[focus]:bg-blue-100" href="/support">
            Support
          </a>
        </MenuItem>
        <MenuItem>
          <a className="block data-[focus]:bg-blue-100" href="/license">
            License
          </a>
        </MenuItem>
      </MenuItems>
    </Menu>
  )
}
```

---

Here is a little reproduction video. The `<Menu/>` you see is rendered
in an `<iframe>`, the goal is that `<MenuItems/>` _also_ render inside
of the `<iframe>`.

In the video below we start with the fix where you can see that the
items are inside the iframe (and unstyled because I didn't load any
styles). The second part of the video is the before, where you can see
that the `<MenuItems/>` escape the `<iframe>` and are styled. That's not
what we want.


https://github.com/user-attachments/assets/2da7627e-7846-4c4d-bb14-278f80a03cd8
2024-12-12 15:45:02 +00: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
Magnus Markling 13d882957b Use React.JSX instead of JSX (#3511)
The global JSX type is deprecated in React 18.3 and removed in React 19
RC. This PR changes the code to use the supported React.JSX syntax
instead.

PS. Would you accept a similar PR for 1.x? I personally haven't upgraded
all my projects yet.
2024-10-08 23:08:51 +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
BrandonGoren 5ca68a9c95 Update react types to avoid unbound method lint errors (#3480)
Using @headlessui close methods/types in a project with
eslint-typescript currently causes "UnboundMethod" errors because we're
using class member syntax to define the functions.

I tweaked the declarations here to use arrow syntax in few places. The
behavior should be unchanged, but we are no longer implying the
existence of a "this"
2024-09-23 13:40:18 +02:00
Robin Malfait 994303f936 2.1.8 - @headlessui/react 2024-09-12 12:35:23 +02:00