* 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
* add frozen state to `Combobox` component
Once you choose an option, the `selected` state remains on the "old"
value until the combobox is fully closed. This way the potential visual
indicators such as a check mark doesn't move around while the Combobox
is closing (when using transitions)
Same as the `Listbox`, this is purely about visual state and exposed
data from the `ComboboxOptions` component and down that tree. The
top-level `Combobox` and `ComboboxInput` components still know the
correct (new) value and will update the `aria-activedescendant`
correctly.
This is achieved by storing the old data (only in single value mode),
and overriding the `isSelected` check function via context provided by
the `ComboboxOptions` component.
* remove check that verified that no `aria-selected` was present
But now with this change, it will be present.
* update changelog
* Update CHANGELOG.md
* 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`
* Make sure data-disabled is available on virtualized options
* Add test
* Update changelog
* calculate `disabled` state once
This way we only have to calculate it once and we can re-use that
information throughout the component. If the `value` changes of the
option, then the component has to re-render anyway which will re-compute
the `disabled` state.
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
* remove the `strategy` options, use "absolute" by default
Right now there is no good reason to expose the strategy, because the
default is good performance wise, and using `absolute` is fine because
we are portalled so there is no parent relative container to worry
about.
* update changelog
* provide `floatingStyles` based on incoming `anchor` information
Before this change, we were only providing the `floatingStyles` based on
the `isEnabled` state. However, this relies on information that is only
available in the next render.
Now the styles are provided one render too late. This means, that there
will be a moment where the `ListboxOptions` (in case of a `Listbox`) is
rendered at the end of the page (and expanding the height of the parent)
without positioning it on top of it in a separate layer (due to the
`position: absolute;`)
The reason this was added was to prevent applying styles to the
`ListboxOptions` if it did not require anchoring (aka no `anchor={{…}}`
prop is provided).
Instead of relying on the `isEnabled` value (which is computed based on
information that is only available in the next render), we provide the
styles based on the incoming `anchor` information which is available
immediately.
The cool thing is that Floating UI is already providing a default
`position: absolute; top: 0; left: 0;` style. If we apply this, it's
already stacked instead of rendering at the end of the page.
* update changelog
* 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
* 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>
* use `div` as default tag for `ListboxOptions` and `ListboxOption` components
* use `div` as default tag for `ComboboxOptions` and `ComboboxOption` components
* use `div` as default tag for `TabGroup`
* update changelog
* ensure we pluck out the `nullable` prop from the props
This should help improve migrating to v2 because otherwise the
`nullable` prop (that doesn't do anything) could end up on the
`Fragment` and cause errors.
* update changelog
* add test to ensure `<Combobox nullable />` doesn't crash
* set default anchor strategy to `absolute`
This is done for 2 reasons:
1. The default strategy in Floating UI was already `absolute`
2. It can improve performance drastically when using transitions
The main reason we used `fixed` was to ensure that it wasn't
accidentally positioned to another `relative` element. However, all
components that use this `FloatingProvider` will also use a portal which
puts elements in the `<body>` already so this should be safe.
If it is not safe for your application, then you can still use the
`fixed` strategy.
* update changelog
* ensure `TMultiple` is correct when passing explicit types to the `Combobox`
When you use the `Combobox` component with explicit types:
```ts
<Combobox<MyType> />
```
Then we make sure that we properly set the `TMultiple` prop as well. It
defaults to `false` which seems to be the better default.
* update changelog
* 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 `overrides` prop to internal `FormFields`
By default, the hidden form fields render an `<input type="hidden" />`.
However, we have some components where we explicitly want to change the
type for example `checkbox` or `radio` types.
I first tried to encode this information in the data itself. That
requires us to use symbols so that we don't accidentally choose a key
that actually exists in the data.
An overrides key solves this for our use cases. The component is also
internal, so nothing is exposed to the user.
* always render hidden form elements
In case of checkboxes and radio elements, we will only render the hidden
inputs if:
1. You pass a `name` prop to make it form-aware
2. When the checkbox / radio is "checked"
This is now changed to always render the hidden input as long as the
`name` prop is passed to make it form-aware.
* update changelog
* drop unnecessary rootDir / paths in tsconfig
This was causing issues with automatic imports that pointed to
`utils/foo` instead of `../utils/foo` and caused the build to break.
* drop unnecessary tsconfig configuration from `@headlessui/vue`
* mark `entered` as deprecated
- We keep the `enterTo` classes once the `enter` transition finishes
- We keep the `leaveTo` classes once the `leave` transition finishes
* update changelog
* 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>
* 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`
* bail the refocus if focus is already on the correct element
* use `mousedown` instead of `click` event
The `mousedown` event happens before the `focus` event. When we
`e.preventDefault()` in this listener, the `focus` event will not fire.
This also means that the focus is not lost on the actual `input`
component which in turn means that we can maintain the selection /
cursor position inside the `input`.
We still use the `refocusInput()` as a fallback in case something else
goes wrong.
* add comments to describe _why_ we use `mousedown`
* ensure we handle mouse buttons correctly
* ensure we handle `Enter` and `Space` explicitly
Now that we use the `mousedown` event instead of the `click` event, we
have to make sure that we handle the `enter` and `space` keys
explicitly.
This used to be covered by the `click` event, but not for the `mousedown` event.
* ensure we focus the first element when using `ArrowDown` on the `ComboboxButton`
We go to the last one on `ArrownUp`, but we forgot to do this on
`ArrowDown`.
* fix tiny typo
Not related to this PR, but noticed it and fixed it anyway.
* update changelog
* ensure we reset the `isTyping` flag
While we are typing, the flag can remain true. But once we stop typing,
the `nextFrame` handler will kick in and set it to `false` again.
It currently behaves as a debounce-like function such that the
`nextFrame` callbacks are cancelled once a new event is fired.
* ensure unique callbacks in the `_disposables` array
This allows us to keep re-adding dispose functions and only register the
callbacks once.
Ideally we can use a `Set`, but we also want to remove a single callback
if the callback is disposed on its own instead of disposing the whole
group. For this we do require an `idx` which is not available in a
`Set` unless you are looping over all disposable functions.
* Update packages/@headlessui-react/src/components/combobox/combobox.tsx
Co-authored-by: Jonathan Reinink <jonathan@reinink.ca>
* Update packages/@headlessui-react/src/components/combobox/combobox.tsx
Co-authored-by: Jonathan Reinink <jonathan@reinink.ca>
* update comments
* abstract confusing logic to a `useFrameDebounce()` hook
* use correct path import
* add some breathing room
---------
Co-authored-by: Jonathan Reinink <jonathan@reinink.ca>
* 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
* 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
* add both `--input-width` and `--button-width` to `ComboboxOptions`
* use `--input-width` and `--button-width` in combobox countries example
* update changelog
* memoize the `currentDisplayValue`
This used to be re-executed every single render. This should typically
not be an issue, but if you use non-deterministic code (E.g.:
`Math.random`, `Date.now`, …) then it could result in incorrect values.
Using `useMemo` allows us to only re-run it if the `data.value` or thte
`displayValue` actually changes.
* add test to verify `currentDisplayValue` is stable
* update changelog
* prefer `data-*` props that already exist
If the component already had `data-foo`, and we set `data-foo` then the
`data-foo` that was already there should stay.
* update changelog
This commit adds provenance for all published packages. See the NPM documentation [0].
Provenance will allow people to verify that the headlessui packages were actually built on GH Actions and with the content of the corresponding commit. This will help with supply chain security.
For this to work, the `id-token` permission was added only where necessary.
[0]: https://docs.npmjs.com/generating-provenance-statements