The global JSX type is deprecated in React 18.3 and removed in React 19
RC. This PR changes the code to use the supported React.JSX syntax
instead.
PS. Would you accept a similar PR for 1.x? I personally haven't upgraded
all my projects yet.
Using @headlessui close methods/types in a project with
eslint-typescript currently causes "UnboundMethod" errors because we're
using class member syntax to define the functions.
I tweaked the declarations here to use arrow syntax in few places. The
behavior should be unchanged, but we are no longer implying the
existence of a "this"
This PR fixes an issue where React hooks were called unconditionally>
The `PopoverButton` and `DisclosureButton` act as a `CloseButton` when
used inside of a panel. We conditionally handled the `ref` when it's
inside a panel. To ensure that the callback is stable, the conditionally
used function was wrapped in a `useEvent(…)` hook.
This seemed to be ok (even though we break the rules of hooks) because a
button can only be in a panel or not be in a panel, but it can't switch
during the lifetime of the button. Aka, the rules of hooks are not
broken because all code paths lead to the same hooks being called.
```ts
<Disclosure defaultOpen>
<DisclosureButton>Open</DisclosureButton>
<DisclosurePanel>
<DisclosureButton>Close</DisclosureButton>
</DisclosurePanel>
<Disclosure>
```
But... it can be called conditionally, because the way we know whether
we are in a panel relies on a state value which comes from context and
is populated by a `useEffect(…)` hook.
The reason we didn't catch this in the `Disclosure` component, is
because all the state is stable and known by the time the
`DisclosurePanel` opens. But if you use the `defaultOpen` prop, the
`DisclosurePanel` is already open and then the state is not ready yet
(because we have to wait for the `useEffect(…)` hook).
Long story short, moved the `isWithinPanel` check inside the
`useEvent(…)` hook that holds the stable function which means that we
don't call this hook unconditionally anymore.
This PR fixes a bug where the components don't always properly close
when using the `transition` prop on those components.
The issue here is that the internal `useTransition(…)` hook relies on a
DOM node. Whenever the DOM node changes, we need to re-run the
`useTransition(…)`. This is why we store the DOM element in state
instead of relying on a `useRef(…)`.
Let's say you have a `Popover` component, then the structure looks like
this:
```ts
<Popover>
<PopoverButton>Show</PopoverButton>
<PopoverPanel>Contents</PopoverPanel>
</Popover>
```
We store a DOM reference to the button and the panel in state, and the
state lives in the `Popover` component. The reason we do that is so that
the button can reference the panel and the panel can reference the
button. This is needed for some `aria-*` attributes for example:
```ts
<PopoverButton aria-controls={panelElement.id}>
```
For the transitions, we set some state to make sure that the panel is
visible or hidden, then we wait for transitions to finish by listening
to transition related events on the DOM node directly.
If you now say, "hey panel, please re-render because you have to become
visible/hidden" then the component re-renders, the panel DOM node
(stored in the `Popover` component) eventually updates and then the
`useTransition(…)` hooks receives the new value (either the DOM node or
null when the leave transition is complete).
The problem here is the round trip that it first has to go to the root
`<Popover/>` component, re-render everything and provide the new DOM
node to the `useTransition(…)` hook.
The solution? Local state so that the panel can re-render on its own and
doesn't require the round trip via the parent.
Fixes: https://github.com/tailwindlabs/headlessui/issues/3438
Fixes: https://github.com/tailwindlabs/headlessui/issues/3437
Fixes: https://github.com/tailwindlabs/tailwindui-issues/issues/1625
---------
Co-authored-by: Jonathan Reinink <jonathan@reinink.ca>
* `useDidElementMove`: handle `HTMLElement`
This change should be temporary, and it will allow us to use the
`useDidElementMove` with ref objects and direct `HTMLElement`s.
* `useResolveButtonType`: handle `HTMLElement`
This change should be temporary, and it will allow us to use the
`useResolveButtonType` hook with ref objects and direct `HTMLElement`s.
* `useRefocusableInput`: handle `HTMLElement`
This change should be temporary, and it will allow us to use the
`useRefocusableInput` hook with ref objects and direct `HTMLElement`s.
* `useTransition`: handle `HTMLElement`
Accept `HTMLElement| null` instead of `MutableRefObject<HTMLElement |
null>` in the `useTransition` hook.
* ensure `containers` are a dependency of `useEffect`
* `Menu`: track `button` and `items` elements in state
So far we've been tracking the `button` and the the `items` DOM nodes in
a ref. Typically, this is the way you do it, you keep track of it in a
ref, later you can access it in a `useEffect` or similar by accessing
the `ref.current`.
There are some problems with this. There are places where we require the
DOM element during render (for example when picking out the `.id` from
the DOM node directly).
Another issue is that we want to re-run some `useEffect`'s whenever the
underlying DOM node changes. We currently work around that, but storing
it directly in state would solve these issues because the component will
re-render and we will have access to the new DOM node.
* `Combobox`: track `input`, `button` and `options` elements in state
* `Disclosure`: track `button` and `panel` elements in state
* `Listbox`: track `button` and `options` elements in state
* `Popover`: track `button` and `panel` elements in state
* `Transition`: track the `container` element in state
* remove incorrect leftover `style=""` attribute
* simplify `useDidElementMove`, only accept `HTMLElement | null`
This doesn't support the `MutableRefObject<HTMLElement | null>` anymore.
* pass `HTMLElement | null` directly to `useResolveButtonType`
* simplify `useResolveButtonType`, only handle `HTMLElement | null`
We don't handle `MutableRefObject<HTMLElement | null>` anymore
* simplify `useRefocusableInput`
* simplify `useElementSize`
* simplify `useOutsideClick`
Only accept `HTMLElement | null` instead of `MutableRefObject<HTMLElement | null>`
* do not rely on `HTMLButtonElement` being available
* update changelog
* use `span` as default element for `Hidden` component
This improves the HTML DOM tree if this happens to be used in let's say
a `p` tag where `div` elements are not allowed. The `Hidden` element is
hidden so it doesn't really matter what the underlying element is.
Fixes: #3319
* refactor `useRootContainers` and introduce `MainTreeProvider`
As a general recap, when an outside click happens, we need to react to
it and typically use the `useOutsideClick` hook.
We also require the context of "allowed root containers", this means
that clicking on a 3rd party toast when a dialog is open, that we allow
this even though we are technically clicking outside of the dialog. This
is simply because we don't have control over these elements.
We also need a reference to know what the "main tree" container is,
because this is the container where your application lives and we _know_
that we are not allowed to click on anything in this container. The
complex part is getting a reference to this element.
```html
<html>
<head>
<title></title>
</head>
<body>
<div id="app"> <-- main root container -->
<div></div>
<div>
<Popover></Popover> <!-- Current component -->
</div>
<div></div>
</div>
<!-- Allowed container #1 -->
<3rd-party-toast-container></3rd-party-toast-container>
</body>
<!-- Allowed container #2 -->
<grammarly-extension></grammarly-extension>
</html>
```
Some examples:
- In case of a `Dialog`, the `Dialog` is rendered in a `Portal` which
means that a DOM ref to the `Dialog` or anything inside will not point
to the "main tree" node.
- In case of a `Popover` we can use the `PopoverButton` as an element
that lives in the main tree. However, if you work with nested
`Popover` components, and the outer `PopoverPanel` uses the `anchor`
or `portal` props, then the inner `PortalButton` will not be in the
main tree either because it will live in the portalled `PopoverPanel`
of the parent.
This is where the `MainTreeProvider` comes in handy. This component will
use the passed in `node` as the main tree node reference and pass this
via the context through the React tree. This means that a nested
`Popover` will still use a reference from the parent `Popover`.
In case of the `Dialog`, we wrap the `Dialog` itself with this provider
which means that the provider will be in the main tree and can be used
inside the portalled `Dialog`.
Another part of the `MainTreeProvider` is that if no node exists in the
parent (reading from context), and no node is provided via props, then
we will briefly render a hidden element, find the root node of the main
tree (aka, the parent element that is a direct child of the body, `body
> *`). Once we found it, we remove the hidden element again. This way we
don't keep unnecessary DOM nodes around.
* update changelog
* Update packages/@headlessui-react/src/hooks/use-root-containers.tsx
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
* Update packages/@headlessui-react/src/hooks/use-root-containers.tsx
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
* Update packages/@headlessui-react/src/hooks/use-root-containers.tsx
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
* Update packages/@headlessui-react/src/hooks/use-root-containers.tsx
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
* Update packages/@headlessui-react/src/hooks/use-root-containers.tsx
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
* use early return
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
* add test that verifies unit test hang
* bail when parsing the `maxHeight` results in `NaN`
* playground cleanup
Testing using this playground example, so cleaned it up to be more
modern using newer components, transition prop and so on.
* use CSS instead of JS
Let's make it a CSS problem instead of a JS problem. The
`round(up, <valueToRound>, <roundingInterval>)` will behave similar to a
`Math.ceil()` that we had in the JS implementation.
See: https://developer.mozilla.org/en-US/docs/Web/CSS/round
* Remove CSS solution for now
I want to re-enable this in the future, but unfortunately for now we
can't use it because Chrome only introduced support for this in the last
2 months.
This reverts commit daac60d45ec3f02b324d0d8b18078a995e885733.
* update changelog
* add function to map transition data to data attributes
* use transition data attributes in props
Instead of in the `slot` because this would also expose this information
as render props but we just want to set it as props without exposing it
as render props.
* rename `slot` to `transitionData` for consistency
* update changelog
* 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 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>
* 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.
* 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
* 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
* 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
* 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`
* 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>
* 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`
* expose `data-focus` on the `TabsPanel` component
* expose `data-disabled` on the `MenuButton` component
* expose `data-disabled` on the `PopoverButton` component
* expose `data-disabled` on the `DisclosureButton` component
* cleanup repetition
* update changelog
* add `satisfies` statements to ensure all data is present
* update changelog entry
* 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>
* only render `<MainTreeNode />` in `Popover.Group` instead of after every `Popover`
* make Vue Popover consistent
* apply same `MainTreeNode` logic to Vue version
* update changelog
* define `aria-expanded` based on open/closed state
You shouldn't be able to open a Listbox/Menu/Combobox/... when the
component is in a disabled state, however if you open it, and then
disable it then it is still in an open state. Therefore the
`aria-expanded` should still be present.
This is also how other libraries behave.
It is also how the native `<select>` behaves. You can open it, disable
it programmatically and then you are still able to make a selection.
This seems enough evidence that this change is an improvement without
being a breaking change.
Fixes: #2602
* update changelog
* 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`
* add a bunch of tests to ensure we won't regress on this again
* fix incorrect warning when using multiple `Popover.Button` inside `Popover.Panel`
* 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
* 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