3b047fc6702e32feea0638700764eaad4cda2e12
15 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
13d882957b |
Use React.JSX instead of JSX (#3511)
The global JSX type is deprecated in React 18.3 and removed in React 19 RC. This PR changes the code to use the supported React.JSX syntax instead. PS. Would you accept a similar PR for 1.x? I personally haven't upgraded all my projects yet. |
||
|
|
70f88f4e26 |
Fix hanging tests when using anchor prop (#3357)
* add test that verifies unit test hang * bail when parsing the `maxHeight` results in `NaN` * playground cleanup Testing using this playground example, so cleaned it up to be more modern using newer components, transition prop and so on. * use CSS instead of JS Let's make it a CSS problem instead of a JS problem. The `round(up, <valueToRound>, <roundingInterval>)` will behave similar to a `Math.ceil()` that we had in the JS implementation. See: https://developer.mozilla.org/en-US/docs/Web/CSS/round * Remove CSS solution for now I want to re-enable this in the future, but unfortunately for now we can't use it because Chrome only introduced support for this in the last 2 months. This reverts commit daac60d45ec3f02b324d0d8b18078a995e885733. * update changelog |
||
|
|
07ba551e63 |
Add transition prop to DialogPanel and DialogBackdrop components (#3309)
* add internal `ResetOpenClosedProvider`
This will allow us to reset the `OpenClosedProvider` and reset the
"boundary". This is important when we want to wrap a `Dialog` inside of
a `Transition` that exists in another component that is wrapped in a
transition itself.
This will be used in let's say a `DisclosurePanel`:
```tsx
<Disclosure> // OpenClosedProvider
<Transition>
<DisclosurePanel> // ResetOpenClosedProvider
<Dialog /> // Can safely wrap `<Dialog />` in `<Transition />`
</DisclosurePanel>
</Transition>
</Disclosure>
```
* use `ResetOpenClosedProvider` in `PopoverPanel` and `DisclosurePanel`
* add `transition` prop to `<Transition>` component
This prop allows us to enabled / disable the `Transition` functionality.
E.g.: expose the underlying data attributes.
But it will still setup a `Transition` boundary for coordinating the
`TransitionChild` components.
* always wrap `Dialog` in a `Transition` component
+ add `transition` props to the `Dialog`, `DialogPanel` and `DialogBackdrop`
This will allow us individually control the transition on each element,
but also setup the transition boundary on the `Dialog` for coordination
purposes.
* improve dialog playground example
* update built in transition playground example to use individual transition props
* speedup example transitions
* Add validations to DialogFn
This technically means most or all of them can be removed from InternalDialog but we can do that later
* Pass `unmount={false}` from the Dialog to the wrapping transition
* Only wrap Dialog in a Transition if it’s not `static`
I’m not 100% sure this is right but it seems like it might be given that `static` implies it’s always rendered.
* remove validations from `InternalDialog`
Already validated by `Dialog` itself
* use existing `usesOpenClosedState`
* reword comment
* remove flawed test
The reason this test is flawed and why it's safe to delete it:
This test opened the dialog, then clicked on an element outside of the
dialog to close it and prove that we correctly focused that new element
instead of going to the button that opened the dialog in the first
place.
This test used to work before marked the rest of the page as `inert`.
Right now we mark the rest of the page as `inert`, so running this in a
real browser means that we can't click or focus an element outside of
the `dialog` simply because the rest of the page is inert.
The reason it fails all of a sudden is that the introduction of
`<Transition>` around the `<Dialog>` by default purely delays the
mounting just enough to record different elements to try and restore
focus to.
That said, this test clicked outside of a dialog and focused that
element which can't work in a real browser because the element can't be
interacted with at all.
* update changelog
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
|
||
|
|
cf0c535f7e |
Add transition prop to <Dialog /> component (#3307)
* add `transition` prop to `Dialog`
Internally this will make sure that the `Dialog` itself gets wrapped in a `<Transition />` component.
Next, the `<DialogPanel>` will also be wrapped in a `<TransitionChild />` component.
We also re-introduce the `DialogBackdrop` that will also be wrapped in a
`<TransitionChild />` component based on the `transition` prop of the
`Dialog`.
This simplifies the `<Dialog />` component, especially now that we can
use transitions with data attributes.
E.g.:
```tsx
<Transition show={open}>
<Dialog onClose={setOpen}>
<TransitionChild
enter="ease-in-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in-out duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div />
</TransitionChild>
<TransitionChild
enter="ease-in-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in-out duration-300"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<DialogPanel>
{/* … */}
</DialogPanel>
</TransitionChild>
</Dialog>
</Transition>
```
↓↓↓↓↓
```tsx
<Transition show={open}>
<Dialog onClose={setOpen}>
<TransitionChild>
<div className="ease-in-out duration-300 data-[closed]:opacity-0 data-[closed]:scale-95" />
</TransitionChild>
<TransitionChild>
<DialogPanel className="ease-in-out duration-300 data-[closed]:opacity-0 data-[closed]:scale-95 bg-white">
{/* … */}
</DialogPanel>
</TransitionChild>
</Dialog>
</Transition>
```
↓↓↓↓↓
```tsx
<Dialog transition open={open} onClose={setOpen}>
<DialogBackdrop className="ease-in-out duration-300 data-[closed]:opacity-0 data-[closed]:scale-95" />
<DialogPanel className="ease-in-out duration-300 data-[closed]:opacity-0 data-[closed]:scale-95 bg-white">
{/* … */}
</DialogPanel>
</Dialog>
```
* update test now that we expose `DialogBackdrop`
* add built-in `<Dialog transition />` playground example
* update changelog
|
||
|
|
1c3f9a6230 |
Improve UX by freezing <ComboboxOptions /> component while closing (#3304)
* add internal `Frozen` component and `useFrozenData` hook * implement frozen state for the `Combobox` component When the `Combobox` is in a closed state, but still visible (aka transitioning out), then we want to freeze the `children` of the `ComboboxOptions`. This way we still look at the old list while transitioning out and you can safely reset any `state` that filters the options in the `onClose` callback. Note: we want to only freeze the children of the `ComboboxOptions`, not the `ComboboxOptions` itself because we are still applying the necessary data attributes to make the transition happen. Similarly, if you are using the `virtual` prop, then we only freeze the `virtual.options` and render the _old_ list while transitioning out. * use `useFrozenData` in `Listbox` component * use `data-*` attributes and `transition` prop to simplify playgrounds * update changelog * improve comment * simplify frozen conditions * use existing variable for frozen state |
||
|
|
29e7d94503 |
Implement <Transition /> and <TransitionChild /> on top of data attributes (#3303)
* add optional `start` and `end` events to `useTransitionData` This will be used when we implement the `<Transition />` component purely using the `useTransitionData` information. But because there is a hierarchy between `<Transition />` and `<TransitionChild />` we need to know when transitions start and end. * implement `<Transition />` and `<TransitionChild />` on top of `useTransitionData()` * update tests Due to a timing issue bug, we updated the snapshot tests in https://github.com/tailwindlabs/headlessui/pull/3273 incorrectly so this commit fixes that which is why there are a lot of changes. Most tests have `show={true}` but not `appear` which means that they should _not_ transition which means that no data attributes should be present. * wait a microTask to ensure that `prepare()` has the time to render Now that we set state instead of mutating the DOM directly we need to wait a tiny bit and then we can trigger the transition to ensure a smooth transition. * cleanup `prepareTransition` now that it returns a cleanup function * move `waitForTransition` and `prepareTransition` into `useTransitionData` * remove existing `useTransition` hook and related utilities * rename `useTransitionData` to `useTransition` * update changelog * Update packages/@headlessui-react/src/components/transition/transition.tsx Co-authored-by: Jordan Pittman <jordan@cryptica.me> * add missing `TransitionState.Enter` This makes sure that the `Enter` state is applied initially when it has to. This also means that we can simplify the `prepareTransition` code again because we don't need to wait for the next microTask which made sure `TransitionState.Enter` was available. * add transition playground page with both APIs * update tests to reflect latest bug fix --------- Co-authored-by: Jordan Pittman <jordan@cryptica.me> |
||
|
|
6ac6930116 |
Implement sibling <Dialog /> components (#3242)
* add `DefaultMap` implementation
* add `useHierarchy` hook
* start `FocusTrapFeatures.None` with `0` instead of `1`
* simplify `Dialog`'s implementation
By making use of the new `useHierarchy` hook.
* delete `StackContext` and `StackProvider` components
They are now replaced by the new `useHierarchy` hook.
* use `useHierarchy` in `useOutsideClick` hook
This way we can scope the hierarchy inside of the `useOutsideClick`
hook. This now ensures that we only enable the `useOutsideClick` on the
top-most element (the one that last enabled it).
* use `useHierarchy` in `useInertOthers` hook
* add new `useEscape` hook
* use new `useEscape` hook
* use `useHierarchy` in `useEscape` hook
* use `useHierarchy` in `useScrollLock` hook
* pass features instead of `enabled` boolean
* simplify demo mode feature flags
No need to setup focus feature flags and then disable it all again if
demo mode is enabled.
* use similar signature for hooks with `enabled` parameter
Whenever a hook requires an `enabled` state, the `enabled` parameter is
moved to the front. Initially this was the last argument and enabled by
default but everywhere we use these hooks we have to pass a dedicated
boolean anyway.
This makes sure these hooks follow a similar pattern. Bonus points
because Prettier can now improve formatting the usage of these hooks.
The reason why is because there is no additional argument after the
potential last callback.
Before:
```ts
let enabled = data.__demoMode ? false : modal && data.comboboxState === ComboboxState.Open
useInertOthers(
{
allowed: useEvent(() => [
data.inputRef.current,
data.buttonRef.current,
data.optionsRef.current,
]),
},
enabled
)
```
After:
```ts
let enabled = data.__demoMode ? false : modal && data.comboboxState === ComboboxState.Open
useInertOthers(enabled, {
allowed: useEvent(() => [
data.inputRef.current,
data.buttonRef.current,
data.optionsRef.current,
]),
})
```
* move `focusTrapFeatures` parameter to the front
* drop `FocusTrapFeatures.All`, list them explicitly
The `All` feature didn't include all feature flags (didn't include
`FocusTrapFeatures.AutoFocus` for example).
* always enable `FocusTrapFeatures.RestoreFocus` when enabled
This way we can get rid of the `Position.HasChild` check, which will
allow us to do more cleanup soon in the `useHierarchy` hook.
* use `useHierarchy` in `<FocusTrap>` component
* drop `useHierarchy` from `<Dialog>` component
* simplify focusTrapFeatures setup
* simplify `useHierarchy`
The `useHierarchy` hook allowed us to determine whether we are in the
root, in the middle, a leaf, have a parent, have a child, ... but we
only ever checked whether we are a leaf node or not.
In other words, you can think of it like "are we the top layer"
This simplifies the implementation and usage of this new hook.
* move `enabled`-like argument to front
Just to be consistent with the other hooks.
* polyfill `toSpliced` for older Node versions
* add sibling dialogs playground
* rename `useHierarchy` to `useIsTopLayer`
* inline variable
* remove `unstable_batchedUpdates`
This is not necessary.
* add tiny bit of information to dialog
Because it might not be super obvious that we are going to close the
`<Dialog />`.
* update changelog
* re-add internal `PortalGroup`
This is necessary to make sure that a component like a `<MenuItems
anchor />` is rendered inside of the `<Dialog />` and not as a sibling.
While this all works from a functional perspective, if you rely on a
CSS variable that was defined on the `<Dialog />` and you use it in the
`<MenuItems />` then without this change it wouldn't work.
|
||
|
|
a45cb6ff6a |
Remove deprecated DialogBackdrop and DialogOverlay components (#3171)
* remove `DialogBackdrop` and `DialogOverlay` We deprecated those components in v1.6, since they are no longer documented and this is a major release, we can safely get rid of it. * update changelog * migrate playground example to use `Dialog.Panel` |
||
|
|
8c7cbb3b09 |
Add string shorthand for the anchor prop (#3133)
* allow to define `anchor` as a string. E.g.: `anchor="bottom"` * use `--anchor-gap`, `--anchor-offset` and `--anchor-padding` variables by default This way simply adding `anchor="bottom"` to one of the anchorable components will also use these variables defined on the component. * update playgrounds to use new string-based `anchor` prop + CSS variables * update changelog |
||
|
|
b6aa1d6d24 |
Add portal prop to Combobox, Listbox, Menu and Popover components (#3124)
* move duplicated `useScrollLock` to dedicated hook
* accept `enabled` prop on `Portal` component
This way we can always use `<Portal>`, but enable / disable it
conditionally.
* use `useSyncRefs` in portal
This allows us to _not_ provide the ref is no ref was passed in.
* refactor inner workings of `useInert`
moved logic from the `useEffect`, to module scope. We will re-use this
logic in a future commit.
* add `useInertOthers` hook
Mark all elements on the page as inert, except for the ones that are allowed.
We move up the tree from the allowed elements, and mark all their
siblings as `inert`. If any of the children happens to be a parent of
one of the elements, then that child will not be marked as `inert`.
```
<body> <!-- Stop at body -->
<header></header> <!-- Inert, sibling of parent of allowed element -->
<main> <!-- Not inert, parent of allowed element -->
<div>Sidebar</div> <!-- Inert, sibling of parent of allowed element -->
<div> <!-- Not inert, parent of allowed element -->
<Listbox> <!-- Not inert, parent of allowed element -->
<ListboxButton></ListboxButton> <!-- Not inert, allowed element -->
<ListboxOptions></ListboxOptions> <!-- Not inert, allowed element -->
</Listbox>
</div>
</main>
<footer></footer> <!-- Inert, sibling of parent of allowed element -->
</body>
```
* add `portal` prop, and change meaning of `modal` prop on `MenuItems`
- This adds a `portal` prop that renders the `MenuItems` in a portal.
Defaults to `false`.
- If you pass an `anchor` prop, the `portal` prop will always be set
to `true`.
- The `modal` prop enables the following behavior:
- Scroll locking is enabled when the `modal` prop is passed and the
`Menu` is open.
- Other elements but the `Menu` are marked as `inert`.
* add `portal` prop, and change meaning of `modal` prop on `ListboxOptions`
- This adds a `portal` prop that renders the `ListboxOptions` in a
portal. Defaults to `false`.
- If you pass an `anchor` prop, the `portal` prop will always be set
to `true`.
- The `modal` prop enables the following behavior:
- Scroll locking is enabled when the `modal` prop is passed and the
`Listbox` is open.
- Other elements but the `Listbox` are marked as `inert`.
* add `portal` and `modal` prop on `ComboboxOptions`
- This adds a `portal` prop that renders the `ComboboxOptions` in a
portal. Defaults to `false`.
- If you pass an `anchor` prop, the `portal` prop will always be set
to `true`.
- The `modal` prop enables the following behavior:
- Scroll locking is enabled when the `modal` prop is passed and the
`Combobox` is open.
- Other elements but the `Combobox` are marked as `inert`.
* add `portal` prop, and change meaning of `modal` prop on `PopoverPanel`
- This adds a `portal` prop that renders the `PopoverPanel` in a portal.
Defaults to `false`.
- If you pass an `anchor` prop, the `portal` prop will always be set
to `true`.
- The `modal` prop enables the following behavior:
- Scroll locking is enabled when the `modal` prop is passed and the
`Panel` is open.
* simplify popover playground, use provided `anchor` prop
* remove internal `Modal` component
This is now implemented on a per component basis with some hooks.
* remove `Modal` handling from `Dialog`
The `Modal` component is removed, so there is no need to handle this in
the `Dialog`. It's also safe to remove because the components with
"portals" that are rendered inside the `Dialog` are portalled into the
`Dialog` and not as a sibling of the `Dialog`.
* ensure we use `groupTarget` if it is already available
Before this, we were waiting for a "next render" to mount the portal if
it was used inside a specific group. This happens when using `<Portal/>`
inside of a `<Dialog/>`.
* update changelog
* add tests for `useInertOthers`
* ensure we stop before the `body`
We used to have a `useInertOthers` hook, but it also made everything
inside `document.body` inert. This means that third party packages or
browser extensions that inject something in the `document.body` were
also marked as `inert`. This is something we don't want.
We fixed that previously by introducing a simpler `useInert` where we
explicitly marked certain elements as inert: https://github.com/tailwindlabs/headlessui/pull/2290
But I believe this new implementation is better, especially with this
commit where we stop once we hit `document.body`. This means that we
will never mark `body > *` elements as `inert`.
* add `allowed` and `disallowed` to `useInertOthers`
This way we have a list of allowed and disallowed containers. The
`disallowed` elements will be marked as inert as-is.
The allowed elements will not be marked as `inert`, but it will mark its
children as inert. Then goes op the parent tree and repeats the process.
* simplify `useInertOthers` in `Dialog` code
* update `use-inert` tests to always use `useInertOthers`
* remove `useInert` hook in favor of `useInertOthers`
* rename `use-inert` to `use-inert-others`
* cleanup default values for `useInertOthers`
|
||
|
|
8fa5caf0dc |
Change default tag from div to Fragment on Transition components (#3110)
* use `Fragment` as the default element for `Transition` components * update tests to reflect default tag change * only error on missing `ref` if it's actually required If the `<Transition />` component "root" is used as a root placeholder (for state management) and not making actual transitions itself, then we don't require a `ref` element. * add test to ensure we don't error on missing `ref` when not required + add `className="…"` to some places to indicate that we _do_ want to perform a transition and thus have to fail if the `ref` is missing. * improve `requiresRef` check Also ensure that a ref is required if the `as` prop is provided and it's not a `Fragment` * add `shouldForwardRef` helper * fix broken tests These tests were rendering a `Debug` element that didn't render any DOM nodes. Adding `as="div"` ensures that we are forwarding the ref correctly. * update changelog * update playgrounds to reflect tag change * Tweak changelog --------- Co-authored-by: Jonathan Reinink <jonathan@reinink.ca> |
||
|
|
c8037fc96e |
Fix enter transitions for the Transition component (#3074)
* improve `transition` logic I spend a lot of time trying to debug this, and I'm not 100% sure that I'm correct yet. However, what we used to do is apply the "before" set of classes, wait a frame and then apply the "after" set of classes which should trigger a transition. However, for some reason, applying the "before" classes already start a transition. My current thinking is that our component is doing a lot of work and therfore prematurely applying the classes and actually triggering the transition. For example, if we want to go from `opacity-0` to `opacity-100`, it looks like setting `opacity-0` is already transitioning (from 100% because that's the default value). Then, we set `opacity-100`, but our component was just going from 100 -> 0 and we were only at let's say 98%. It looks like we cancelled the transition and only went from 98% -> 100%. I can't fully explain it purely because I don't 100% get what's going on. However, this commit fixes it in a way where we first prepare the transition by explicitly cancelling all in-flight transitions. Once that is done, we can apply the "before" set of classes, then we can apply the "after" set of classes. This seems to work consistently (even though we have failing tests, might be a JSDOM issue). This tells me that at least parts of my initial thoughts are correct where some transition is already happening (for some reason). I'm not sure what the reason is exactly. Are we doing too much work and blocking the main thread? That would be my guess... * simplify check * updating playgrounds to improve transitions * update changelog * track in-flight transitions * document `disposables()` and `useDisposables()` functions * ensure we alway return a cleanup function * move comment * apply `enterTo` or `leaveTo` classes once we are done transitioning * cleanup & simplify logic * update comment * fix another bug + update tests * add failing test as playground * simplify event callbacks Instead of always ensuring that there is an event, let's use the `?.` operator and conditionally call the callbacks instead. * use existing `useOnDisappear` hook * remove repro * only unmount if we are not transitioning ourselves * `show` is already guaranteed to be a boolean In a previous commit we checked for `undefined` and threw an error. Which means that this part is unreachable if `show` is undefined. * cleanup temporary test changes * Update packages/@headlessui-react/src/components/transition/utils/transition.ts Co-authored-by: Jonathan Reinink <jonathan@reinink.ca> * Update packages/@headlessui-react/src/components/transition/utils/transition.ts Co-authored-by: Jonathan Reinink <jonathan@reinink.ca> * Update packages/@headlessui-react/src/components/transition/utils/transition.ts Co-authored-by: Jonathan Reinink <jonathan@reinink.ca> * Update packages/@headlessui-react/src/components/transition/utils/transition.ts Co-authored-by: Jonathan Reinink <jonathan@reinink.ca> * Update packages/@headlessui-react/src/utils/disposables.ts Co-authored-by: Jonathan Reinink <jonathan@reinink.ca> * Update packages/@headlessui-react/src/components/transition/transition.tsx Co-authored-by: Jonathan Reinink <jonathan@reinink.ca> * Update packages/@headlessui-react/src/components/transition/transition.tsx Co-authored-by: Jonathan Reinink <jonathan@reinink.ca> * run prettier --------- Co-authored-by: Jonathan Reinink <jonathan@reinink.ca> |
||
|
|
d03fbb19f5 |
Make the Combobox component nullable by default (#3064)
* remove `nullable` prop
* prevent selecting active option on blur
+ cleanup and adjust comments
* remove nullable from comments
* bump TypeScript to 5.4
This gives us `NoInfer<T>`!
* simplify types of `Combobox`
Now that `nullable` is gone, we can take another look at the type
definition. This in combination with the new `NoInfer` type makes types
drastically simpler and more correct.
* re-add `nullable` to prevent type issues
But let's mark it as deprecated to hint that something changed.
* update changelog
* improve `ByComparator` type
If we are just checking for `T extends null`, then
`{id:1,name:string}|null` will also be true and therefore we would
eventually return `string` instead of `"id" | "name"`.
To solve this, we first check if `NonNullable<T> extends never`, this
would be the case if `T` is `null`.
Otherwise, we know it's not just `null` but it can be something else
with or without `null`. To be sure, we use `keyof NonNullable<null>` to
get rid of the `null` part and to only keep the rest of the object (if
it's an object).
* ensure the `by` prop type handles `multiple` values correctly
This way the `by` prop will still compare single values that are present
inside the array.
This now also solves a pending TypeScript issue that we used to `//
@ts-expect-error` before.
* type uncontrolled `Combobox` components correctly
We have some tests that use uncontrolled components which means that we
can't infer the type from the `value` type.
* simplify `onChange` calls
Now that we don't infer the type when using the generic inside of
`onChange`, it means that we can use `onChange={setValue}` directly
because we don't have to worry about the updater function of `setValue`
anymore.
* correctly type `onChange`, by adding `null`
If you are in single value mode, then the `onChange` can (and will)
receive `null` as a value (when you clear the input field). We never
properly typed it so this fixes that.
In multiple value mode this won't happen, if anything the value will be
`[]` but not `null`.
* remove `nullable` prop from playground
* drop `nullable` mentions in tests
|
||
|
|
056b311522 |
Expose --input-width and --button-width CSS variables on the ComboboxOptions component (#3057)
* add both `--input-width` and `--button-width` to `ComboboxOptions` * use `--input-width` and `--button-width` in combobox countries example * update changelog |
||
|
|
a73007388f |
Ensure playgrounds work + switch to npm workspaces (#2907)
* bump Next in playground * convert legacy Link after Next.js bump * update yarn.lock * switch to npm workspaces * move `packages/playground-*` to `playgrounds/*` * use `npm` instead of `yarn` * sync package-lock.json * use node 20 for insiders releases |