Commit Graph

393 Commits

Author SHA1 Message Date
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
Jordan Pittman a303819018 Make sure disabling demo mode on <Combobox> works (#3182)
* Make sure disabling demo mode on `<Combobox>` works

* Update changelog
2024-05-07 14:10:38 -04:00
Jordan Pittman cfbcf5b840 Remove accidental deprecation comments on <DialogPanel> and <DialogTitle> (#3176)
* Remove accidental deprecations

These got left in when they shouldn’t’ve been

* Update changelog
2024-05-06 15:05:55 -04:00
Robin Malfait a45cb6ff6a Remove deprecated DialogBackdrop and DialogOverlay components (#3171)
* remove `DialogBackdrop` and `DialogOverlay`

We deprecated those components in v1.6, since they are no longer
documented and this is a major release, we can safely get rid of it.

* update changelog

* migrate playground example to use `Dialog.Panel`
2024-05-03 16:22:39 +02:00
Jordan Pittman 8d20cfb0c6 Deprecate dot notation (#3170)
* Expose new components under dot notation

Most of them already where so only a couple were missing.

* Deprecate dot notation

* Add deprecation to `RadioGroupOption`

* Update deprecations

* Update changelog

* Update changelog
2024-05-03 09:55:02 -04:00
Robin Malfait 0bd8c47c28 use HTMLElement for generic DOM node types (#3169)
We keep specific types for elements with special meaning, such as
`HTMLButtonElement`, `HTMLLabelElement` or `HTMLInputElement` because
they receive certain attributes that generic DOM nodes (such as
HTMLDivElement) don't

For the components where we use simple `div` elements (and where people
use `as={...}`) that renders a different element, it doesn't make sense
to use `HTMLDivElement`. Using a more generic `HTMLElement` is simpler
and more correct (we still had `HTMLUListElement` and `HTMLLIElement`
for "div" DOM nodes which is incorrect).

This shouldn't be a breaking change because an `HTMLDivElement` is still
a valid `HTMLElement`. The other way around wouldn't be the case.
2024-05-03 15:32:55 +02:00
Robin Malfait f0e3e5b4a6 Bump dependencies (#3158)
* use `act` from `react` instead of `@testing-library/react`

* bump dependencies

* bump `@testing-library/react`

* bump `@react-aria/interactions`

* bump "@tanstack/react-virtual"

* add `ResizeObserver` polyfill, and enable it by default for tests

* mock `getBoundingClientRect`

Otherwise the virtualization tests don't work as expected because they
rely on the client rect which is not supported (or not correctly
measured) in JSDOM.
2024-05-02 14:41:58 +02:00
Jordan Pittman 1a440e1ee7 Fix issues with scrolling in a virtual combobox (#3163)
* Scrolling to active option in combobox after opening when last option was selected with the mouse

* Allow virtual combobox to scroll freely with mouse wheel after changing options with keyboard

* Update changelog
2024-05-01 15:57:28 -04:00
Jordan Pittman db702a7cec Only render virtual options wrapper when there are items to show (#3161)
* Only render virtual options wrapper when there are items to show

* Update changelog
2024-05-01 09:56:48 -04:00
Jordan Pittman 2e6cb126ef Render virtual items during an exiting transition (#3160)
Otherwise the render function will get called with different options by `render()` resulting in undefined
2024-05-01 09:14:49 -04:00
Robin Malfait f35214db4c calculate the size of an element as soon as possible (#3153)
Instead of waiting for a `useEffect` to trigger, we can use `useMemo` to
always compute the size the moment a DOM element is available (or
changes).

We also make sure to force a re-render when resizes are detected on the
stable DOM node, which in turn causes the `useMemo` to recompute.
2024-04-30 19:31:40 +02:00
Robin Malfait 4acf9e27f1 Ensure the static and portal props work nicely together (#3152)
* ensure we keep the `static` prop into account when using the `<Portal/>`
around anchored elements.

* update changelog
2024-04-29 20:47:00 +02:00
Robin Malfait 872808c8fc ensure we always set portal to true when the anchor props is truthy
We already had this for most components, but I missed it for this
component. This fixes that.
2024-04-29 20:29:28 +02:00
Robin Malfait afc9cb648b Ensure TransitionRoot component without props transitions correctly (#3147)
* ensure `TransitionRoot` component without props transitions correctly

A bit of a weird one, but you can use the `TransitionRoot` component
without any props for transitions themselves (so no real transition can
happen). Even crazier, it can happen that it doesn't even render a DOM
node, but just its children.

At this point, the `TransitionRoot` component is purely there for
orchestration purposes of child components.

Since there is no DOM node in certain situations, the transitions (and
its `onStart` and `onStop` callbacks) won't even happen at all. This
causes a bug (obvious in react strict mode) where children don't
properly mount or the transition component doesn't properly unmount.

* update changelog
2024-04-26 23:28:43 +02:00
Robin Malfait 539c124c69 Improve internal demo mode (#3143)
* improve demo mode for the `Dialog` component

This still disabled `inert` and focus stealing code. But it does allow
outside click.

* improve demo mode for the `Combobox` component

Before this, once you start interacting with the `Combobox`, the options
weren't properly scrolled into view.

* improve demo mode for the `Menu` component

* improve demo mode for the `Popover` component

* add demo mode to the `Listbox` component
2024-04-26 16:35:12 +02:00
Robin Malfait d56a77bf52 Add frozen value to ComboboxOptions component (#3126)
* add frozen state to `Combobox` component

Once you choose an option, the `selected` state remains on the "old"
value until the combobox is fully closed. This way the potential visual
indicators such as a check mark doesn't move around while the Combobox
is closing (when using transitions)

Same as the `Listbox`, this is purely about visual state and exposed
data from the `ComboboxOptions` component and down that tree. The
top-level `Combobox` and `ComboboxInput` components still know the
correct (new) value and will update the `aria-activedescendant`
correctly.

This is achieved by storing the old data (only in single value mode),
and overriding the `isSelected` check function via context provided by
the `ComboboxOptions` component.

* remove check that verified that no `aria-selected` was present

But now with this change, it will be present.

* update changelog

* Update CHANGELOG.md
2024-04-24 19:17:55 +02:00
Robin Malfait b6aa1d6d24 Add portal prop to Combobox, Listbox, Menu and Popover components (#3124)
* move duplicated `useScrollLock` to dedicated hook

* accept `enabled` prop on `Portal` component

This way we can always use `<Portal>`, but enable / disable it
conditionally.

* use `useSyncRefs` in portal

This allows us to _not_ provide the ref is no ref was passed in.

* refactor inner workings of `useInert`

moved logic from the `useEffect`, to module scope. We will re-use this
logic in a future commit.

* add `useInertOthers` hook

Mark all elements on the page as inert, except for the ones that are allowed.

We move up the tree from the allowed elements, and mark all their
siblings as `inert`. If any of the children happens to be a parent of
one of the elements, then that child will not be marked as `inert`.

```
<body>                                    <!-- Stop at body -->
  <header></header>                       <!-- Inert, sibling of parent of allowed element -->
  <main>                                  <!-- Not inert, parent of allowed element -->
    <div>Sidebar</div>                    <!-- Inert, sibling of parent of allowed element -->
    <div>                                 <!-- Not inert, parent of allowed element -->
      <Listbox>                           <!-- Not inert, parent of allowed element -->
        <ListboxButton></ListboxButton>   <!-- Not inert, allowed element -->
        <ListboxOptions></ListboxOptions> <!-- Not inert, allowed element -->
      </Listbox>
    </div>
  </main>
  <footer></footer>                       <!-- Inert, sibling of parent of allowed element -->
</body>
```

* add `portal` prop, and change meaning of `modal` prop on `MenuItems`

- This adds a `portal` prop that renders the `MenuItems` in a portal.
  Defaults to `false`.
  - If you pass an `anchor` prop, the `portal` prop will always be set
    to `true`.
- The `modal` prop enables the following behavior:
  - Scroll locking is enabled when the `modal` prop is passed and the
    `Menu` is open.
  - Other elements but the `Menu` are marked as `inert`.

* add `portal` prop, and change meaning of `modal` prop on `ListboxOptions`

- This adds a `portal` prop that renders the `ListboxOptions` in a
  portal. Defaults to `false`.
  - If you pass an `anchor` prop, the `portal` prop will always be set
    to `true`.
- The `modal` prop enables the following behavior:
  - Scroll locking is enabled when the `modal` prop is passed and the
    `Listbox` is open.
  - Other elements but the `Listbox` are marked as `inert`.

* add `portal` and `modal` prop on `ComboboxOptions`

- This adds a `portal` prop that renders the `ComboboxOptions` in a
  portal. Defaults to `false`.
  - If you pass an `anchor` prop, the `portal` prop will always be set
    to `true`.
- The `modal` prop enables the following behavior:
  - Scroll locking is enabled when the `modal` prop is passed and the
    `Combobox` is open.
  - Other elements but the `Combobox` are marked as `inert`.

* add `portal` prop, and change meaning of `modal` prop on `PopoverPanel`

- This adds a `portal` prop that renders the `PopoverPanel` in a portal.
  Defaults to `false`.
  - If you pass an `anchor` prop, the `portal` prop will always be set
    to `true`.
- The `modal` prop enables the following behavior:
  - Scroll locking is enabled when the `modal` prop is passed and the
    `Panel` is open.

* simplify popover playground, use provided `anchor` prop

* remove internal `Modal` component

This is now implemented on a per component basis with some hooks.

* remove `Modal` handling from `Dialog`

The `Modal` component is removed, so there is no need to handle this in
the `Dialog`. It's also safe to remove because the components with
"portals" that are rendered inside the `Dialog` are portalled into the
`Dialog` and not as a sibling of the `Dialog`.

* ensure we use `groupTarget` if it is already available

Before this, we were waiting for a "next render" to mount the portal if
it was used inside a specific group. This happens when using `<Portal/>`
inside of a `<Dialog/>`.

* update changelog

* add tests for `useInertOthers`

* ensure we stop before the `body`

We used to have a `useInertOthers` hook, but it also made everything
inside `document.body` inert. This means that third party packages or
browser extensions that inject something in the `document.body` were
also marked as `inert`. This is something we don't want.

We fixed that previously by introducing a simpler `useInert` where we
explicitly marked certain elements as inert: https://github.com/tailwindlabs/headlessui/pull/2290

But I believe this new implementation is better, especially with this
commit where we stop once we hit `document.body`. This means that we
will never mark `body > *` elements as `inert`.

* add `allowed` and `disallowed` to `useInertOthers`

This way we have a list of allowed and disallowed containers. The
`disallowed` elements will be marked as inert as-is.

The allowed elements will not be marked as `inert`, but it will mark its
children as inert. Then goes op the parent tree and repeats the process.

* simplify `useInertOthers` in `Dialog` code

* update `use-inert` tests to always use `useInertOthers`

* remove `useInert` hook in favor of `useInertOthers`

* rename `use-inert` to `use-inert-others`

* cleanup default values for `useInertOthers`
2024-04-24 17:10:41 +02:00
Jordan Pittman 166e862f01 Make sure data-disabled is available on virtualized options (#3128)
* Make sure data-disabled is available on virtualized options

* Add test

* Update changelog

* calculate `disabled` state once

This way we only have to calculate it once and we can re-use that
information throughout the component. If the `value` changes of the
option, then the component has to re-render anyway which will re-compute
the `disabled` state.

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2024-04-24 09:32:41 -04:00
Robin Malfait d2b734536f Add optional onClose callback to Combobox component (#3122)
* add optional `onClose` callback to `Combobox` component

* update changelog

* add tests to ensure `onClose` is called when `Combobox` closes
2024-04-23 15:45:22 +02:00
Robin Malfait 6c9e4b2b6f Allow passing a boolean to the anchor prop (#3121) 2024-04-22 23:42:58 +02:00
Robin Malfait dcbcd79047 Move focus to ListboxOptions and MenuItems when they are rendered later (#3112)
* ensure `useEffect` is executed again if ref contents changed since last render

* update changelog
2024-04-19 18:35:31 +02:00
Robin Malfait b517a39445 Ensure anchored components are properly stacked on top of Dialog components (#3111)
* ensure `Dialog` knows about `Modal`s via the `StackProvider`

When you render a `Listbox` in a `Dialog`, then clicking outside of the
`Listbox` will only close the `Listbox` and not the `Dialog`.

This is because the `Listbox` is rendered _inside_ the `Dialog`, and the
`useOutsideClick` hook will prevent the event from propagating to the
`Dialog` therefore it stays open.

Then, if you add the `anchor` prop to the `ListboxOptions` then a few
things will happen:

1. We will render the `ListboxOptions` in a `Modal`, which portals the
   component to the end of the `body` (aka, it won't be in the `Dialog`
   anymore).
2. The `anchor` prop, will use Floating UI to position the element
   correctly.

If you now click outside of the open `Listbox`, then the `Dialog` will
receive the click event (because it is rendered somewhere else in the
DOM) and therefore the `Listbox` **and** the `Dialog` will close.

The `Dialog` also uses a `StackProvider` to know if it is the top-level
`Dialog` or not. The problem is that the `Modal` doesn't use that
`StackProvider` to tell the `Dialog` that something is stacked on top of
the current `Dialog`.

That's what this commit fixes, the `Modal` will now use a
`StackProvider` to tell the `Dialog` that it's not the top-most element
anymore so it shouldn't enable the `useOutsideClick` behavior.

That said, this is one of the things that will be changed in the future
to make "parallel" dialogs possible. Essentially, we will track a global
stack and the top-most element (last one that was "opened") will win.

Then hooks such as `useOutsideClick` and `useScrollLock` will use that
information to know if they should undo scroll locking for example if
another element is still open.

* update CHANGELOG
2024-04-19 18:31:14 +02:00
Robin Malfait 8fa5caf0dc Change default tag from div to Fragment on Transition components (#3110)
* use `Fragment` as the default element for `Transition` components

* update tests to reflect default tag change

* only error on missing `ref` if it's actually required

If the `<Transition />` component "root" is used as a root placeholder
(for state management) and not making actual transitions itself, then we
don't require a `ref` element.

* add test to ensure we don't error on missing `ref` when not required

+ add `className="…"` to some places to indicate that we _do_ want to
  perform a transition and thus have to fail if the `ref` is missing.

* improve `requiresRef` check

Also ensure that a ref is required if the `as` prop is provided and
it's not a `Fragment`

* add `shouldForwardRef` helper

* fix broken tests

These tests were rendering a `Debug` element that didn't render any DOM
nodes. Adding `as="div"` ensures that we are forwarding the ref
correctly.

* update changelog

* update playgrounds to reflect tag change

* Tweak changelog

---------

Co-authored-by: Jonathan Reinink <jonathan@reinink.ca>
2024-04-19 16:15:11 +02:00
Robin Malfait 83cda0aa75 Change default tags for ListboxOptions, ListboxOption, ComboboxOptions, ComboboxOption and TabGroup components (#3109)
* use `div` as default tag for `ListboxOptions` and `ListboxOption` components

* use `div` as default tag for `ComboboxOptions` and `ComboboxOption` components

* use `div` as default tag for `TabGroup`

* update changelog
2024-04-19 01:54:25 +02:00
Robin Malfait 004b8dcf34 Omit nullable prop from Combobox component (#3100)
* ensure we pluck out the `nullable` prop from the props

This should help improve migrating to v2 because otherwise the
`nullable` prop (that doesn't do anything) could end up on the
`Fragment` and cause errors.

* update changelog

* add test to ensure `<Combobox nullable />` doesn't crash
2024-04-15 18:17:15 +02:00
Robin Malfait 512bf44150 Ensure the multiple prop is typed correctly when passing explicit types to the Combobox component (#3099)
* ensure `TMultiple` is correct when passing explicit types to the `Combobox`

When you use the `Combobox` component with explicit types:

```ts
<Combobox<MyType> />
```

Then we make sure that we properly set the `TMultiple` prop as well. It
defaults to `false` which seems to be the better default.

* update changelog
2024-04-15 17:42:59 +02:00
Robin Malfait b86737b698 Add new CloseButton component and useClose hook (#3096)
* add `useClose` hook and `CloseButton` component

* expose `useClose` hook and `CloseButton` components

* use `CloseProvider` in the `Popover` component

* use `CloseProvider` in the `Dialog` component

* use `CloseProvider` in the `Disclosure` component

* update changelog
2024-04-12 15:54:01 +02:00