* 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>
* 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 `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
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.
* 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.
* 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
* 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`
* 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
* 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
* add `useOnDisappear` hook
This hook allows us to trigger a callback if the element becomes
"hidden". We use the bounding client rect and check the dimensions to
know wether we are "hidden" or not.
* use new `useOnDisappear` hook in components with the `anchor` prop
* update changelog
* document `useOnDisappear`
* use `event.preventDefault()` in the `useOutsideClick` on Dialog's
When using a `Dialog`, we should prevent the default behaviour of the
event that triggered the "close" in the `useOutsideClick` call.
We recently made improvements to improve outside click behaviour on
touch devices (https://github.com/tailwindlabs/headlessui/pull/2572) but
due to the `touchend` event, the touch is still forwarded and therefore
a potential button _behind_ the "backdrop" will also be clicked. This is
not what we want.
Added the `event.preventDefault()` for the Dialog specifically because
there are other places where we use `useOutsideClick` and where we _do_
want the behaviour where the click just continues. A concrete example of
this is 2 `Menu`'s next to eachother where you open the first one, and
then click on the second one. This should close first one (outside
click) and open the second one (by not preventing the event)
* update changelog
* bump React & React DOM dependencies
* fix typo `TOmitableProps` → `TOmittableProps`
* bump prettier
* run prettier after prettier version bump
* bump TypeScript
* run prettier after TypeScript version bump
* enable `verbatimModuleSyntax`
This ensures all imported types are using the `type` keyword.
* add `type` to type related imports
* add common testing scenarios
Will be used in the new and existing components.
* add script to make Next.js happy
Right now Next.js does barrel file optimization and re-writing imports
to a real path in the `dist` folder. Most of those rewrites don't
actually exist because they have an assumption:
```js
import { FooBar } from '@headlessui/react'
```
is rewritten as:
```js
import { FooBar } from '@headlessui/react/dist/components/foo-bar/foo-bar'
```
This script will make sure these paths exist...
* improve `by` prop, introduce `useByComparator`
This hook has a default implementation when comparing objects. If the
object contains an `id`, then we will compare the objects by their
`id`'s without the user of the library needing to specify `by="id"`.
If the objects don't have an `id` prop, then the default is still to
compare by reference (unless specicified otherwise).
* sync yarn.lock
* rename `Features` to `HiddenFeatures` for `Hidden` component
* rename `Features` to `FocusTrapFeatures` in `FocusTrap` component
* rename `Features` to `RenderFeatures` in `render` util
* add `floating-ui` as a dependency + introduce internal floating related components
* bump Vue dependencies
* ensure scroll bar calculations can't go negative
* improve types in `@headlessui/vue`
* use snapshot tests for `Transition` tests in `@headlessui/vue`
* use snapshot tests for `portal` tests in `@headlessui/vue`
* rename `src/components/transitions/` to `src/components/transition/` (singular)
This is so that we can be consistent with the other components.
* drop custom `toMatchFormattedCss`, prefer snapshot tests instead
* use snapshot tests for `Label` tests in `@headlessui/vue`
* use snapshot tests for `Description` tests in `@headlessui/vue`
* sort exported components in tests for `@headlessui/vue`
* use snapshot tests in `@headlessui/tailwindcss`
* rename `mergeProps` to `mergePropsAdvanced`
This is a more complex version of a soon to be exported `mergeProps`
that we will be using in our components.
* do not expose `aria-labelledby` if it is only referencing itself
* expose boolean state as `kebab-case` instead of `camelCase`
These are the ones being exposed inside `data-headlessui-state="..."`
* expose boolean data attributes
A slot with `{active,focus,hover}` will be exposed as:
```html
<span data-headlessui-state="active focus hover"></span>
```
But also as boolean attributes:
```html
<span data-active data-focus data-hover></span>
```
* improve internal types for `className` in `render` util
* ensure we keep exposed data attributes into account when trying to forward them to the component inside the `Fragment`
* add small typescript type fix
This is internal code, and the public API is not influenced by this
`:any`. It does make TypeScript happy.
* introduce `mergeProps` util to be used in our components
This will help us to merge props, when event handlers are available they
will be merged by wrapping them in a function such that both (or more)
event handlers are called for the same `event`.
* add new internal `Modal` component
* fix: when using `Focus.Previous` with `activeIndex = -1` should start at the end
* prefer `window.scrollY` instead of `window.pageYOffset`
Because `window.pageYOffset` is deprecated.
* add `'use client'` directives on client only components
These components use hooks that won't work in server components and you
will receive an error otherwise.
* drop `import 'client-only'` in favor of the `'use client'` directive
* add React Aria dependencies
* pin beta dependencies
* prettier bump formatting
* improve TypeScript types in tests
* use new Jest matchers instead of deprecated ones
* improve typescript types in Vue
* prefer `useLabelledBy` and `useDescribedBy`
* add internal `DisabledProvider`
* add internal `IdProvider`
* add internal `useDidElementMove` hook
* add internal `useElementSize` hook
* add internal `useIsTouchDevice` hook
* add internal `useActivePress` hook
* use snapshot tests for `Description` tests
* use snapshot tests for `Label` tests
* use snapshot tests for `Portal` tests
* use snapshot tests for `render` tests
* add (private) `Tooltip` component
Currently this one is not ready yet, so its not publicly exposed yet.
* add internal `FormFields` component
This one adds a component to render (hidden) inputs for native form
support. It also ensures that form fields can be hoisted to the end of
the nearest `Field`. If the components are not inside a `Field` they
will be rendered in place.
* add new `Button` component
* add new `Checkbox` component
* add new `DataInteractive` component
* add new `Field` component
* add new `Fieldset` component
* add new `Legend` component
* add new `Input` component
* add new `Select` component
* add new `Textarea` component
* export new components
* WIP
* remove `within: true`
This only makes sense if anything inside the current element receives
focus, which is not the case for `input`, `select`, `textarea` or
`Radio/RadioOption`.
* group focus/hover/active hooks together
* conditionally link anchor panel
* immediately focus the container
* prevent premature disabling of `Listbox`'s floating integration
+ Track whether the button moved or not when disabling such that we can
disable the transitions earlier.
* improve scroll locking on iOS
* skip hydration tests for now
* skip certain focus trap tests for now
* update CHANGELOG.md
* add missing requires
* drop unused `@ts-expect-error`
* ignore type issues in playgrounds
These playgrounds are mainly test playgrounds. Lower priority for now,
we will get back to them.
* add yarn resolutions to solve swc bug
* add `prettier-plugin-organize-imports` and `prettier-plugin-tailwindcss`
* format
* bump Tailwind CSS
* format playgrounds using updated Tailwind CSS and Prettier plugins
* use import syntax
* export component interfaces, and mark them as internal
This is not ideal because we don't want these to be public. However, if
you are creating components on top of Headless UI, the TypeScript
compiler needs access to them.
So now they are public in a sense, but you shouldn't be interacting with
them directly.
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
* Update changelog
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
* WIP
* Add warning for unsupported roles to `<Dialog>`
* Update assertions
* Add test for React
* Add support for `role=alertdialog` to Vue
---------
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
* Only use useServerHandoffComplete in React < 18
It’s only useful for the useId hook. It is not compatible with `<Suspense>` because hydration is delayed then.
* Make sure portals first render matches the server by rendering nothing
Since Portals cannot SSR the first render MUST also return `null`. React really needs an `isHydrating` API.
* Lazily resolve root containers
This fixes a problem where clicks were assumed to be outside because of the delayed `<Portal>` render. The second portal render doesn’t cause the dialog to re-render thus the initial ref values were stale.
* Update changelog
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
* abstract resolving root containers to hook
This way we can reuse it in other components when needed.
* allow registering a `Portal` component to a parent
This allows us to find all the `Portal` components that are nested in a
given component without manually adding refs to every `Portal` component
itself.
This will come in handy in the `Popover` component where we will allow
focus in the child `Portal` components otherwise a focus outside of the
`Popover` will close the it. In other components we often crawl the DOM
directly using `[data-headlessui-portal]` data attributes, however this
will fetch _all_ the `Portal` components, not the ones that started in
the current component.
* allow focus in portalled containers
The `Popover` component will close by default if focus is moved outside
of it. However, if you use a `Portal` comopnent inside the
`Popover.Panel` then from a DOM perspective you are moving the focus
outside of the `Popover.Panel`. This prevents the closing, and allows
the focus into the `Portal`.
It currently only allows for `Portal` components that originated from
the `Popover` component. This means that if you open a `Dialog`
component from within the `Popover` component, the `Dialog` already
renders a `Portal` but since this is part of the `Dialog` and not the
`Popover` it will close the `Popover` when focus is moved to the
`Dialog` component.
* ensure `useNestedPortals` register/unregister with the parent
This ensures that if you have a structure like this:
```jsx
<Dialog> {/* Renders a portal internally */}
<Popover>
<Portal> {/* First level */}
<Popover.Panel>
<Menu>
<Portal> {/* Second level */}
<Menu.Items>
{/* ... */}
</Menu.Items>
</Portal>
</Menu>
</Popover.Panel>
</Portal>
</Popover>
</Dialog>
```
That working with the `Menu` doesn't close the `Popover` or the `Dialog`.
* cleanup `useRootContainers` hook
This will allow you to pass in portal elements as well. + cleanup of
the resolving of all DOM nodes.
* handle nested portals in `Dialog` component
* expose `contains` function from `useRootContainers`
Shorthand to check if any of the root containers contains the given
element.
* add tests to verify that actions in `Portal` components won't close the `Popover`
* update changelog
* re-order use-outside-click logic
To make it similar between React & Vue
* inject the `PortalWrapper` context in the correct spot
* ensure to forward the incoming `attrs`
* update dialog playground example
Includes a generic `Button` component that has explicit focus styles.
* keep track of "focus" history
Safari doesn't "focus" buttons when you mousedown on them. This means
that we don't capture the correct element to restore focus to when
closing a `Dialog` for example.
Now, we will make sure to keep track of a list of last "focused" items.
We do this by also capturing elements when you "click", "mousedown" or
"focus".
* let's use a button instead of a div in tests
* make `target` for Vue consistent with React
* update changelog
* cleanup `XYZPropsWeControl`
The idea behind the `PropsWeControl` is that we can omit all the fields
that we are controlling entirely. In this case, passing a prop like
`role`, but if we already set the role ourselves then the prop won't do
anything at all. This is why we want to alert the end user that it is an
"error".
It can also happen that we "control" the value by default, but keep
incoming props into account. For example we generate a unique ID for
most components, but you can provide your own to override it. In this
case we _don't_ want to include the ID in the `XYZPropsWeControl`.
Additionally, we introduced some functionality months ago where we call
event callbacks (`onClick`, ...) from the incoming props before our own
callbacks. This means that by definition all `onXYZ` callbacks can be
provided.
* improve defining types
Whenever we explicitly provide custom types for certain props, then we
make sure to omit those keys first from the original props (of let's say
an `input`). This is important so that TypeScript doesn't try to "merge"
those types together.
* cleanup: move `useEffect`
* add `defaultValue` explicitly
* ensure tests are not using `any` because of `onChange={console.log}`
The `console.log` is typed as `(...args: any[]) => void` which means
that it will incorrectly mark its incoming data as `any` as well.
Converting it to `x => console.log(x)` makes TypeScript happy. Or in
this case, angry since it found a bug.
This is required because it _can_ be that your value (e.g.: the value of
a Combobox) is an object (e.g.: a `User`), but it is also nullable.
Therefore we can provide the value `null`. This would mean that
eventually this resolves to `keyof null` which is `never`, but we just
want a string in this case.
```diff
-export type ByComparator<T> = (keyof T & string) | ((a: T, b: T) => boolean)
+export type ByComparator<T> =
+ | (T extends null ? string : keyof T & string)
+ | ((a: T, b: T) => boolean)
```
* improve the internal types of the `Combobox` component
* improve the internal types of the `Disclosure` component
* improve the internal types of the `Listbox` component
* improve the internal types of the `Menu` component
* improve the internal types of the `Popover` component
* improve the internal types of the `Tabs` component
* improve the internal types of the `Transition` component
* use `Override` in `Hidden` as well
* cleanup unused code
* don't check the `useSyncExternalStoreShimClient`
* don't check the `useSyncExternalStoreShimServer`
* improve types in the render tests
* fix `Ref<TTag>` to be `Ref<HTMLElement>`
* improve internal types of the `Transition` component (Vue)
+ add `attrs.class` as well
* use different type for `AnyComponent`
* update changelog
* add (failing) test to verify moving focus to 3rd party containers work
* pass `resolveRootContainers` to `FocusTrap`
* handle lazy containers in `FocusTrap`
* update changelog
* use the Dialog's parent as the root for the Intersection observer
We have some code that allows us to auto-close the dialog the moment it
gets hidden. This is useful if you use a dialog for a mobile menu and
you resizet he browser. If you wrap the dialog in a `md:hidden` then it
auto closes. If we don't do this, then the dialog is still locking the
scrolling, keeping the focus in the dialog, ... but it is not visible.
To solve this we use an `IntersectionObserver` to verify that the
`boundingClientRect` is "gone" (x = 0, y = 0, width = 0 and height = 0).
However, the intersection observer is not always triggered. This happens
if the main content is scrollable.
Setting the `root` of the `IntersectionObserver` to the parent of the
`Dialog` does seem to solve it.
Not 100% sure what causes this behaviour exactly.
* use a `ResizeObserver` instead of `IntersectionObserver`
* implement a `ResizeObserver` for the tests
* update changelog
* drop `@ts-expect-error`, because `inert` is available now
* fix logical error
We want to apply `inert` when we _don't_ have nested dialogs, because if
we _do_ have nested dialogs, then the inert should be applied from the
nested dialog (or visually the top most dialog).
* update changelog
* replace `useInertOthers` with `useInert`
* add `assertInert` and `assertNotInert` accessibility assertion helpers
* ensure the `main tree` root is marked as inert
As well as the parent dialogs in case of nested dialogs.
* introduce `opening` and `closing` states
Also represent them as bits so that we can easily combine them while we
are transitioning from one state to the other.
* update `open/closed` state checks
Instead of checking whether it is in one state or an other, we can check
if the current state contains some potential sub-state.
This allows us to still check if we are in the `Open` state, while also
`Closing` because the state will be `S.Open | S.Closing`.
* expose `flags` from the `useFlags` hook
* add the `Closing` and `Opening` states to the Open/Closed state
* create dedicated `abcEnabled` variables
* keep the `State.Closing` into account for `scroll locking` and `inert others`
* add a test for the `Closing` state impacting the `Dialog` component
* cleanup unused imports
* add `unmount` util to the Vue Test renderer
* update changelog
* use the `import * as React from 'react'` pattern
We use named imports, but we have to import `React` itself as well for
JSX because it compiles to `React.createElement`. We could get rid of
our own JSX and use it directly, or we can use this `import * as React
from 'react'` syntax.
This fixes an issue for people using `allowSyntheticDefaultImports: false` in TypeScript.
Fixes: #2117
* update changelog
* Fix overflow when swapping dialogs that use transition
* Refactor
* refactor
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* Inline shim for ESM support
Until the official package adds an ESM version with a wildcard import we can’t use it. This version was copied from Remix Router
* Add dialog shadow root examples
* Fix SSR error
* Add repro for iOS scrolling issue
* Try to fix vercel build
idk what’s wrong here
* Update repro
A transition is required to delay closing enough to demonstrate the bug
* Port global dialog state to Vue
* Add dialog test to Vue
* wip
* wip
* Workaround bug
This shouldn’t happen at all and we need to find the source of the bug but this should “fix” things for the time being
* wip
* Rebuild overflow locking with simpler API
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* Update deps
* wip
* simplify
* Port to Vue
* wip
* wip
* Tweak tests
* Update changelog
* Ensure meta callbacks are cleaned up
* cleanup
* wip
* add tests to guarantee `FocusTrap` with a single element works as expected
* it should keep the focus in the Dialog
Even if there is only 1 element. We were skipping the current active
element so the container didn't have any elements anymore and just
continued to the next focusable element in line. This will prevent that
and ensure that we can only skip elements if there are multiple ones.
* update changelog
* improve types for addEventListener inside disposables
* improve scroll locking
Instead of using the "simple" hack with the `position: fixed;` we now
went back to the `touchmove` implementation.
The `position: fixed;` causes some annoying issues. For starters, on iOS
you will now get a strange gap (due to safe areas). Some applications
also saw "blank" screens based on how the page was implemented.
We also saw some issues internally, where clicking changing the scroll
position on the main page from within the Dialog.
Think about something along the lines of:
```html
<a href="#interesting-link-on-the-current-page">Interesting link on the page</a>
```
This doesn't work becauase the page is now fixed, and there is nothing
to scroll...
Instead, we now use the `touchmove` again. The problem with this last
time was that this disabled _all_ touch move events. This is obviously
not good.
Luckily, we already have a concept of "safe containers". This is what we
use for the `outside click` behaviour as well. Basically in a Dialog,
your `Dialog.Panel` is the safe container. But also third party DOM
elements that are rendered inside that Panel (or as a sibling of the
Dialog, but not your main app).
We can re-use this knowledge of "safe containers", and only cancel the
`touchmove` behaviour if this didn't happen in any of the safe
containers.
* update changelog
* sort DOM nodes using tabIndex first
It will still keep the same DOM order if tabIndex matches, thanks to
stable sorts!
* refactor `focusIn` API
All the arguments resulted in usage like `focusIn(container,
Focus.First, true, null)`, and to make things worse, we need to add
something else to this list in the future.
Instead, let's keep the `container` and the type of `Focus` as known
params, all the other things can sit in an options object.
* fix FocusTrap escape due to strange tabindex values
This code will now ensure that we can't escape the FocusTrap if you use
`<tab>` and you happen to tab to an element outside of the FocusTrap
because the next item in line happens to be outside of the FocusTrap and
we never hit any of the focus guard elements.
How it works is as follows:
1. The `onBlur` is implemented on the `FocusTrap` itself, this will give
us some information in the event itself.
- `e.target` is the element that is being blurred (think of it as `from`)
- `e.currentTarget` is the element with the event listener (the dialog)
- `e.relatedTarget` is the element we are going to (think of it as `to`)
2. If the blur happened due to a `<tab>` or `<shift>+<tab>`, then we
will move focus back inside the FocusTrap, and go from the `e.target`
to the next or previous value.
3. If the blur happened programmatically (so no tab keys are involved,
aka no direction is known), then the focus is restored to the
`e.target` value.
Fixes: #1656
* update changelog
* accept `id` as a prop where it is currently hardcoded (React)
Continuation of #2020
Co-authored-by: Olivier Louvignes <olivier@mgcrea.io>
* accept `id` as a prop where it is currently hardcoded (Vue)
* update changelog
* apply React's hook rules
Co-authored-by: Olivier Louvignes <olivier@mgcrea.io>