* 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>
* 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>
* 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
* 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
* 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>
* 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
* 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.
* make `handleOutsideClick` stable
* cancel "outside click" when "scrolling" on touch device
When on a touch device, then the `touchend` event will fire, even if you
scrolled a bit and scrolling was your intention.
This now tracks that touches were at least 30px apart in either the X or
Y direction. If that's the case, then we do not consider it an outside
click.
* add `enabled` parameter to `useDocumentEvent` and `useWindowEvent`
* update `useDocumentEvent` and `useWindowEvent` usages
This now takes the new `enabled` value into account.
* update changelog
* bump vue and vite in playground
* 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
* 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
* Use `useId` instead of React internals
These may break in any React release which may result in blocking the ecosystem from seemlessly upgrading to new React releases.
We can use `useId` instead which is available since React 18.
* inline `useId` hook
No need to use `useStableCollectionKey` anymore
* reformat comment
* update changelog
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
* 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>
* 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
* 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>
* 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.
* 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
* 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
* 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
* 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
* 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
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.
* do not use default function for `allowed` and `disallowed`
Otherwise the fallback function will be used which will be a new
reference on each render. On pages with lots of elements this causes
performance issues.
* update changelog
* add mouse buttons
* add `useDisposables` hook
* add `useFrameDebounce` hook
Schedule a task in the next frame
* ensure we reset the `isTyping` flag correctly
* use same `mousedown` API as we did in React
This allows us to never leave the `input`, even when clicking on an
option.
* update changelog
* format comments
* inline `cb`
* 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`
* 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
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.
* 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.
* 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