Commit Graph

419 Commits

Author SHA1 Message Date
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 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
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 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 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 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 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
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
Konrad Schultz ca6a455a3a Fix Combobox virtual mode types for multiple (#3392)
When multiple is specified we expect TValue to already be an array, so lets just
ensure its an array and use it as the type for virtual.options. Similarly the
disabled callback gets passed a TValue, but when multiple is true it is passed
something of type element of TValue.

Behaviour in javascript lines up with this fine, just a type level fix afaik.
2024-07-18 09:58:49 -04:00
Todd Austin f3353728bd updating various typos (#3387) 2024-07-16 12:48:21 -04: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 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 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 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 275c2051fe Remove unused @ts-expect-error directive 2024-06-25 12:53:09 -04:00
Jordan Pittman cf1fc7caec Use any for ComboboxOptions render prop option (#3327) 2024-06-25 12:49:54 -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
Jordan Pittman a593d19955 Fix issues spreading omitted props onto components (#3313)
* Simplify props types

wip

wip

* Remove unused types

* Remove test

it’s no longer relevant

* Update changelog
2024-06-24 14:00:57 -04:00
Robin Malfait f1446664bc Remove transition state from render props (#3312)
* add function to map transition data to data attributes

* use transition data attributes in props

Instead of in the `slot` because this would also expose this information
as render props but we just want to set it as props without exposing it
as render props.

* rename `slot` to `transitionData` for consistency

* update changelog
2024-06-21 15:55:06 +02:00
Robin Malfait f26cca57bd Add aria-hidden to DialogBackdrop component (#3310)
* add `aria-hidden` to `DialogBackdrop`

* update changelog
2024-06-21 12:50:46 +02:00
Robin Malfait 07ba551e63 Add transition prop to DialogPanel and DialogBackdrop components (#3309)
* add internal `ResetOpenClosedProvider`

This will allow us to reset the `OpenClosedProvider` and reset the
"boundary". This is important when we want to wrap a `Dialog` inside of
a `Transition` that exists in another component that is wrapped in a
transition itself.

This will be used in let's say a `DisclosurePanel`:

```tsx
<Disclosure>              // OpenClosedProvider
  <Transition>
    <DisclosurePanel>     // ResetOpenClosedProvider
      <Dialog />          // Can safely wrap `<Dialog />` in `<Transition />`
    </DisclosurePanel>
  </Transition>
</Disclosure>
```

* use `ResetOpenClosedProvider` in `PopoverPanel` and `DisclosurePanel`

* add `transition` prop to `<Transition>` component

This prop allows us to enabled / disable the `Transition` functionality.
E.g.: expose the underlying data attributes.

But it will still setup a `Transition` boundary for coordinating the
`TransitionChild` components.

* always wrap `Dialog` in a `Transition` component

+ add `transition` props to the `Dialog`, `DialogPanel` and `DialogBackdrop`

This will allow us individually control the transition on each element,
but also setup the transition boundary on the `Dialog` for coordination
purposes.

* improve dialog playground example

* update built in transition playground example to use individual transition props

* speedup example transitions

* Add validations to DialogFn

This technically means most or all of them can be removed from InternalDialog but we can do that later

* Pass `unmount={false}` from the Dialog to the wrapping transition

* Only wrap Dialog in a Transition if it’s not `static`

I’m not 100% sure this is right but it seems like it might be given that `static` implies it’s always rendered.

* remove validations from `InternalDialog`

Already validated by `Dialog` itself

* use existing `usesOpenClosedState`

* reword comment

* remove flawed test

The reason this test is flawed and why it's safe to delete it:

This test opened the dialog, then clicked on an element outside of the
dialog to close it and prove that we correctly focused that new element
instead of going to the button that opened the dialog in the first
place.

This test used to work before marked the rest of the page as `inert`.
Right now we mark the rest of the page as `inert`, so running this in a
real browser means that we can't click or focus an element outside of
the `dialog` simply because the rest of the page is inert.

The reason it fails all of a sudden is that the introduction of
`<Transition>` around the `<Dialog>` by default purely delays the
mounting just enough to record different elements to try and restore
focus to.

That said, this test clicked outside of a dialog and focused that
element which can't work in a real browser because the element can't be
interacted with at all.

* update changelog

---------

Co-authored-by: Jordan Pittman <jordan@cryptica.me>
2024-06-21 00:44:12 +02:00
Jordan Pittman 9ae054ec12 Rename PopoverOverlay to PopoverBackdrop (#3308)
* Rename `PopoverOverlay` to `PopoverBackdrop`

We're aliasing `PopoverOverlay` to `PopoverBackdrop` and `PopoverOverlayProps` to `PopoverBackdropProps` for backwards compatability.

* Update changelog

* Update packages/@headlessui-react/CHANGELOG.md

Co-authored-by: Jonathan Reinink <jonathan@reinink.ca>

---------

Co-authored-by: Jonathan Reinink <jonathan@reinink.ca>
2024-06-20 15:18:31 -04:00
Robin Malfait cf0c535f7e Add transition prop to <Dialog /> component (#3307)
* add `transition` prop to `Dialog`

Internally this will make sure that the `Dialog` itself gets wrapped in a `<Transition />` component.
Next, the `<DialogPanel>` will also be wrapped in a `<TransitionChild />` component.

We also re-introduce the `DialogBackdrop` that will also be wrapped in a
`<TransitionChild />` component based on the `transition` prop of the
`Dialog`.

This simplifies the `<Dialog />` component, especially now that we can
use transitions with data attributes.

E.g.:

```tsx
<Transition show={open}>
  <Dialog onClose={setOpen}>
    <TransitionChild
      enter="ease-in-out duration-300"
      enterFrom="opacity-0"
      enterTo="opacity-100"
      leave="ease-in-out duration-300"
      leaveFrom="opacity-100"
      leaveTo="opacity-0"
    >
      <div />
    </TransitionChild>

     <TransitionChild
       enter="ease-in-out duration-300"
       enterFrom="opacity-0 scale-95"
       enterTo="opacity-100 scale-100"
       leave="ease-in-out duration-300"
       leaveFrom="opacity-100 scale-100"
       leaveTo="opacity-0 scale-95"
     >
       <DialogPanel>
         {/* … */}
       </DialogPanel>
     </TransitionChild>
  </Dialog>
</Transition>
```

↓↓↓↓↓

```tsx
<Transition show={open}>
  <Dialog onClose={setOpen}>
    <TransitionChild>
      <div className="ease-in-out duration-300 data-[closed]:opacity-0 data-[closed]:scale-95" />
    </TransitionChild>

     <TransitionChild>
       <DialogPanel className="ease-in-out duration-300 data-[closed]:opacity-0 data-[closed]:scale-95 bg-white">
         {/* … */}
       </DialogPanel>
     </TransitionChild>
  </Dialog>
</Transition>
```

↓↓↓↓↓

```tsx
<Dialog transition open={open} onClose={setOpen}>
  <DialogBackdrop className="ease-in-out duration-300 data-[closed]:opacity-0 data-[closed]:scale-95" />

  <DialogPanel className="ease-in-out duration-300 data-[closed]:opacity-0 data-[closed]:scale-95 bg-white">
    {/* … */}
  </DialogPanel>
</Dialog>
```

* update test now that we expose `DialogBackdrop`

* add built-in `<Dialog transition />` playground example

* update changelog
2024-06-20 17:41:45 +02:00
Robin Malfait 1c3f9a6230 Improve UX by freezing <ComboboxOptions /> component while closing (#3304)
* add internal `Frozen` component and `useFrozenData` hook

* implement frozen state for the `Combobox` component

When the `Combobox` is in a closed state, but still visible (aka
transitioning out), then we want to freeze the `children` of the
`ComboboxOptions`. This way we still look at the old list while
transitioning out and you can safely reset any `state` that filters the
options in the `onClose` callback.

Note: we want to only freeze the children of the `ComboboxOptions`, not
the `ComboboxOptions` itself because we are still applying the necessary
data attributes to make the transition happen.

Similarly, if you are using the `virtual` prop, then we only freeze the
`virtual.options` and render the _old_ list while transitioning out.

* use `useFrozenData` in `Listbox` component

* use `data-*` attributes and `transition` prop to simplify playgrounds

* update changelog

* improve comment

* simplify frozen conditions

* use existing variable for frozen state
2024-06-20 16:15:07 +02:00
Robin Malfait 29e7d94503 Implement <Transition /> and <TransitionChild /> on top of data attributes (#3303)
* add optional `start` and `end` events to `useTransitionData`

This will be used when we implement the `<Transition />` component
purely using the `useTransitionData` information. But because there is a
hierarchy between `<Transition />` and `<TransitionChild />` we need to
know when transitions start and end.

* implement `<Transition />` and `<TransitionChild />` on top of `useTransitionData()`

* update tests

Due to a timing issue bug, we updated the snapshot tests in
https://github.com/tailwindlabs/headlessui/pull/3273 incorrectly so this
commit fixes that which is why there are a lot of changes.

Most tests have `show={true}` but not `appear` which means that they
should _not_ transition which means that no data attributes should be
present.

* wait a microTask to ensure that `prepare()` has the time to render

Now that we set state instead of mutating the DOM directly we need to
wait a tiny bit and then we can trigger the transition to ensure
a smooth transition.

* cleanup `prepareTransition` now that it returns a cleanup function

* move `waitForTransition` and `prepareTransition` into `useTransitionData`

* remove existing `useTransition` hook and related utilities

* rename `useTransitionData` to `useTransition`

* update changelog

* Update packages/@headlessui-react/src/components/transition/transition.tsx

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

* add missing `TransitionState.Enter`

This makes sure that the `Enter` state is applied initially when it has
to.

This also means that we can simplify the `prepareTransition` code again
because we don't need to wait for the next microTask which made sure
`TransitionState.Enter` was available.

* add transition playground page with both APIs

* update tests to reflect latest bug fix

---------

Co-authored-by: Jordan Pittman <jordan@cryptica.me>
2024-06-19 23:42:14 +02:00
Robin Malfait 20920492b5 Rename data-from -> data-closed and data-exit -> data-leave (#3285)
* rename `data-from` to `data-closed`, `data-exit` to `data-leave`

* update changelog
2024-06-12 14:27:47 +02:00
Robin Malfait 1e9a3f1640 Allow Tab and Shift+Tab when Listbox is open (#3284)
* allow `Tab` and `Shift+Tab` in `Listbox` component

This will make it consistent with the `Menu` and the ARIA Authoring
Practices Guide.

* update tests

* update changelog
2024-06-12 01:02:22 +02:00
Robin Malfait 6b6c259010 Introduce CSS based transitions (#3273)
* simplify `useFlags`

* add new `useTransitionData` hook

* use new `useTransitionData` hook

* add ability to cancel transitions mid-transition

* handle cancellations in both directions properly

* re-use existing `prepareTransition`

* expose `data-*` attributes for transitions in `<Transition />` component

* update tests to reflect added data attributes

* update changelog

* only call `getAnimations` if available

This has been around since 2020, but JSDOM doesn't know about this yet,
so tests using JSDOM will fail otherwise.
2024-06-11 17:53:21 +02:00
Robin Malfait 2d3ec80314 Internal refactor: use flushSync() instead of d.nextFrame() (#3263)
* use `flushSync` instead of `d.nextFrame`

This guarantees that after the `flushSync` call the DOM is updated. This
means that we don't have to guess and delay by a double
`requestAnimationFrame` (`nextFrame`) and _hope_ that the DOM was
updated already.

* inline disposables call

Each function in the `disposables()` object returns a cleanup function
which means we can return this directly.

* inline if-statements

Small one, but consistent with `<Menu />` and `<Listbox />` components.

* inline `flushSync()` callbacks
2024-06-03 16:17:16 +02:00
Robin Malfait 479853d5ed Ensure ComboboxInput does not sync while you are still typing (#3259)
* track `isTyping` in state

While you are typing, we should not sync the value with the `<input>`
because otherwise it would override your changes.

The moment you close the Combobox (by selecting an option, clicking
outside, pressing escape or tabbing away) we can mark the component as
not typing anymore. Once you are not typing anymore, then we can re-sync
the input with the given value.

* remove unused `useFrameDebounce` hook

* require `isTyping` boolean

* update changelog
2024-05-31 22:44:29 +02:00
Dan Thompson 025e115277 Fix visual jitter in Combobox component when using native scrollbar (#3190)
* Avoid `scrollToIndex` when using native scrollbar in `ComboBox`

* format comment

* set `ActivationTrigger` in mousedown

If you use the scrollbar, a `mousedown` and `pointerdown` event is fired
before you start interacting with the scrollbar. If you use the `scroll`
event, then the callback is fired while scrolling.

To improve performance a bit more, we will set the activation trigger in
the `mousedown` event because we only need to set it once.

If you are done scrolling, we don't reset it (we didn't do that before
either), but instead we rely on other events to override the trigger
(e.g.: you start using the keyboard).

The big benefit is that this now only calls the `setActivationTrigger`
once, instead of `n` times with the same value.

* optimize `onWheel` to only trigger once

This is a similar performance trick as before. We only need to set the
activationTrigger once, so there is no need to keep calling this
function for no reason.

* update changelog

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2024-05-29 14:21:44 +02:00
Robin Malfait b3a508b6ca Prevent focus on <Checkbox /> when it is disabled (#3251)
* prevent focus on `<Checkbox />` when disabled

* update changelog
2024-05-28 15:57:32 +02:00
Robin Malfait 94bc4e15fd Merge incoming style prop on ComboboxOptions, ListboxOptions, MenuItems, and PopoverPanel components (#3250)
* merge incoming `style` prop

We were overriding the `style` prop entirely on the `<ComboboxOptions>`,
`<ListboxOptions>`, `<MenuItems>`, and `<PopoverPanel>` for anchoring
purposes, as well as provided some CSS variables.

This now ensures that the incoming `style` prop gets merged in.

* update changelog
2024-05-28 15:37:33 +02:00
Robin Malfait 7267cc4c1c Keep <Combobox /> open when clicking in scrollbar on <ComboboxOptions> (#3249)
* keep `<Combobox />` open when clicking in scrollbar on `<ComboboxOptions>`

* update changelog

* Update packages/@headlessui-react/CHANGELOG.md

Co-authored-by: Adam Wathan <adam.wathan@gmail.com>

* Update packages/@headlessui-react/src/components/combobox/combobox.tsx

Co-authored-by: Adam Wathan <adam.wathan@gmail.com>

* format comment

---------

Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
2024-05-28 15:33:14 +02:00
Robin Malfait 6ac6930116 Implement sibling <Dialog /> components (#3242)
* add `DefaultMap` implementation

* add `useHierarchy` hook

* start `FocusTrapFeatures.None` with `0` instead of `1`

* simplify `Dialog`'s implementation

By making use of the new `useHierarchy` hook.

* delete `StackContext` and `StackProvider` components

They are now replaced by the new `useHierarchy` hook.

* use `useHierarchy` in `useOutsideClick` hook

This way we can scope the hierarchy inside of the `useOutsideClick`
hook. This now ensures that we only enable the `useOutsideClick` on the
top-most element (the one that last enabled it).

* use `useHierarchy` in `useInertOthers` hook

* add new `useEscape` hook

* use new `useEscape` hook

* use `useHierarchy` in `useEscape` hook

* use `useHierarchy` in `useScrollLock` hook

* pass features instead of `enabled` boolean

* simplify demo mode feature flags

No need to setup focus feature flags and then disable it all again if
demo mode is enabled.

* use similar signature for hooks with `enabled` parameter

Whenever a hook requires an `enabled` state, the `enabled` parameter is
moved to the front. Initially this was the last argument and enabled by
default but everywhere we use these hooks we have to pass a dedicated
boolean anyway.

This makes sure these hooks follow a similar pattern. Bonus points
because Prettier can now improve formatting the usage of these hooks.
The reason why is because there is no additional argument after the
potential last callback.

Before:
```ts
let enabled = data.__demoMode ? false : modal && data.comboboxState === ComboboxState.Open
useInertOthers(
  {
    allowed: useEvent(() => [
      data.inputRef.current,
      data.buttonRef.current,
      data.optionsRef.current,
    ]),
  },
  enabled
)
```

After:
```ts
let enabled = data.__demoMode ? false : modal && data.comboboxState === ComboboxState.Open
useInertOthers(enabled, {
  allowed: useEvent(() => [
    data.inputRef.current,
    data.buttonRef.current,
    data.optionsRef.current,
  ]),
})
```

* move `focusTrapFeatures` parameter to the front

* drop `FocusTrapFeatures.All`, list them explicitly

The `All` feature didn't include all feature flags (didn't include
`FocusTrapFeatures.AutoFocus` for example).

* always enable `FocusTrapFeatures.RestoreFocus` when enabled

This way we can get rid of the `Position.HasChild` check, which will
allow us to do more cleanup soon in the `useHierarchy` hook.

* use `useHierarchy` in `<FocusTrap>` component

* drop `useHierarchy` from `<Dialog>` component

* simplify focusTrapFeatures setup

* simplify `useHierarchy`

The `useHierarchy` hook allowed us to determine whether we are in the
root, in the middle, a leaf, have a parent, have a child, ... but we
only ever checked whether we are a leaf node or not.

In other words, you can think of it like "are we the top layer"

This simplifies the implementation and usage of this new hook.

* move `enabled`-like argument to front

Just to be consistent with the other hooks.

* polyfill `toSpliced` for older Node versions

* add sibling dialogs playground

* rename `useHierarchy` to `useIsTopLayer`

* inline variable

* remove `unstable_batchedUpdates`

This is not necessary.

* add tiny bit of information to dialog

Because it might not be super obvious that we are going to close the
`<Dialog />`.

* update changelog

* re-add internal `PortalGroup`

This is necessary to make sure that a component like a `<MenuItems
anchor />` is rendered inside of the `<Dialog />` and not as a sibling.

While this all works from a functional perspective, if you rely on a
CSS variable that was defined on the `<Dialog />` and you use it in the
`<MenuItems />` then without this change it wouldn't work.
2024-05-27 23:35:38 +02:00
Robin Malfait 1ee4cfd1b7 [internal] Move enabled parameter in hooks to first argument (#3245)
* move `enabled` parameter in hooks to front

Whenever a hook requires an `enabled` state, the `enabled` parameter is
moved to the front. Initially this was the last argument and enabled by
default but everywhere that we use these hooks we have to pass a
dedicated boolean anyway.

This makes sure these hooks follow a similar pattern. Bonus points
because Prettier can now improve formatting the usage of these hooks.
The reason why is because there is no additional argument after the
potential last callback.

Before:
```ts
let enabled = data.__demoMode ? false : modal && data.comboboxState === ComboboxState.Open
useInertOthers(
  {
    allowed: useEvent(() => [
      data.inputRef.current,
      data.buttonRef.current,
      data.optionsRef.current,
    ]),
  },
  enabled
)
```

After:
```ts
let enabled = data.__demoMode ? false : modal && data.comboboxState === ComboboxState.Open
useInertOthers(enabled, {
  allowed: useEvent(() => [
    data.inputRef.current,
    data.buttonRef.current,
    data.optionsRef.current,
  ]),
})
```

Much better!

* inline variables
2024-05-27 17:45:21 +02:00
Robin Malfait f740050c2a Use native fieldset instead of div by default for <Fieldset /> component (#3237)
* improve TypeScript types for `Fieldset` component

* use `fieldset` instead of `div` by default

* only apply `role="group"` when not using a native `fieldset`

* apply `disabled` attribute

This is necessary if we want to make use of the default fieldset tag
(which also disables native form elements)

* adjust tests reflecting new changes

* conditionally apply props based on rendered element

* add `useResolvedTag` hook

This allows us to compute the `tag` name of a component. We can use a
shortcut based on the `props.as` and/or the `DEFAULT_XXX_TAG` of a
component. If this is not known/passed, then we compute it based on the
`ref` instead which requires an actual re-render.

* use `useResolvedTag` hook

* reflect change in `Field` related test

* update changelog

* inline variable
2024-05-25 00:27:24 +02:00
Robin Malfait 8c3499cc8d Only handle form reset when defaultValue is used (#3240)
* add `useDefaultValue` hook

This allows us to have a guaranteed `default value` that never changes
unless the component re-mounts.

Since the hook returns a stable value, we can safely include it in
dependency arrays of certain hooks.

Before this change, including this is in the dependency arrays it would
cause a trigger or change of the hook when the `defaultValue` changes
but we never want that.

* do not handle `reset` when no `defaultValue` or `defaultChecked` was provided

If a `defaultValue` is provided, then the reset will be handled and the
`onChange` will be called with this value.

If no `defaultValue` was provided, we won't handle the `reset`,
otherwise we would call the `onChange` with `undefined` which is
incorrect.

* update changelog
2024-05-25 00:26:51 +02:00
Robin Malfait c2754bcb16 Ensure tabbing to a portalled <PopoverPanel> component moves focus inside (without using <PortalGroup>) (#3239)
* ensure we allow focus in the focus sentinel button

We already checked this button when inside of a `PopoverGroup`, but we
didn't when you weren't using a `PopoverGroup` component.

* add test

* update changelog
2024-05-24 23:48:09 +02:00
Robin Malfait b822c8a400 Fix crash when toggling between virtual and non-virtual mode in Combobox component (#3236)
* ensure we correctly merge the `virtual` configuration

* use more generic `UpdateVirtualOptions`

This way we can passthrough the `disabled` function as well.

* properly handle re-use of `disabled` function

* use same order in objects

* cleanup `!` and `?` if we already know we are in `virtual` mode

* directly enable virtual mode in state if previously we weren't using virtual mode

* update changelog
2024-05-24 18:15:14 +02:00
Robin Malfait b478189fad Mark SwitchGroup as deprecated, prefer Field instead (#3232)
* mark `SwitchGroup` as deprecated

Also updated the `Switch.Group` message to also prefer the `<Field>`
component.

* update changelog
2024-05-23 16:24:26 +02:00
Robin Malfait 7fcb410be4 do not apply aria-modal in demo mode (#3227)
When you have a `role="dialog"` and an `aria-modal="true"` at the same
time, then Safari will focus the first focusable element inside the
dialog.

This is not ideal, because in demo mode this means that the focus is
moved around to various DOM elements.

This commit ensures that the `aria-modal` is not applied when demo mode
is enabled to prevent that behavior in Safari.
2024-05-22 14:57:57 +02:00
Robin Malfait 045f2bc761 Ensure page doesn't scroll down when pressing Escape to close the Dialog component (#3218)
* ensure we blur the `document.activeElement` when pressing `Escape`

* update changelog
2024-05-21 13:07:04 +02:00
Jordan Pittman 4eff138ada Don’t set a focus fallback for Dialog’s in demo mode (#3194)
* Don’t set a focus fallback for Dialog’s in demo mode

* Update changelog
2024-05-10 14:55:17 -04:00