* merge incoming `style` prop
We were overriding the `style` prop entirely on the `<ComboboxOptions>`,
`<ListboxOptions>`, `<MenuItems>`, and `<PopoverPanel>` for anchoring
purposes, as well as provided some CSS variables.
This now ensures that the incoming `style` prop gets merged in.
* update changelog
* keep `<Combobox />` open when clicking in scrollbar on `<ComboboxOptions>`
* update changelog
* Update packages/@headlessui-react/CHANGELOG.md
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
* Update packages/@headlessui-react/src/components/combobox/combobox.tsx
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
* format comment
---------
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
* add `DefaultMap` implementation
* add `useHierarchy` hook
* start `FocusTrapFeatures.None` with `0` instead of `1`
* simplify `Dialog`'s implementation
By making use of the new `useHierarchy` hook.
* delete `StackContext` and `StackProvider` components
They are now replaced by the new `useHierarchy` hook.
* use `useHierarchy` in `useOutsideClick` hook
This way we can scope the hierarchy inside of the `useOutsideClick`
hook. This now ensures that we only enable the `useOutsideClick` on the
top-most element (the one that last enabled it).
* use `useHierarchy` in `useInertOthers` hook
* add new `useEscape` hook
* use new `useEscape` hook
* use `useHierarchy` in `useEscape` hook
* use `useHierarchy` in `useScrollLock` hook
* pass features instead of `enabled` boolean
* simplify demo mode feature flags
No need to setup focus feature flags and then disable it all again if
demo mode is enabled.
* use similar signature for hooks with `enabled` parameter
Whenever a hook requires an `enabled` state, the `enabled` parameter is
moved to the front. Initially this was the last argument and enabled by
default but everywhere we use these hooks we have to pass a dedicated
boolean anyway.
This makes sure these hooks follow a similar pattern. Bonus points
because Prettier can now improve formatting the usage of these hooks.
The reason why is because there is no additional argument after the
potential last callback.
Before:
```ts
let enabled = data.__demoMode ? false : modal && data.comboboxState === ComboboxState.Open
useInertOthers(
{
allowed: useEvent(() => [
data.inputRef.current,
data.buttonRef.current,
data.optionsRef.current,
]),
},
enabled
)
```
After:
```ts
let enabled = data.__demoMode ? false : modal && data.comboboxState === ComboboxState.Open
useInertOthers(enabled, {
allowed: useEvent(() => [
data.inputRef.current,
data.buttonRef.current,
data.optionsRef.current,
]),
})
```
* move `focusTrapFeatures` parameter to the front
* drop `FocusTrapFeatures.All`, list them explicitly
The `All` feature didn't include all feature flags (didn't include
`FocusTrapFeatures.AutoFocus` for example).
* always enable `FocusTrapFeatures.RestoreFocus` when enabled
This way we can get rid of the `Position.HasChild` check, which will
allow us to do more cleanup soon in the `useHierarchy` hook.
* use `useHierarchy` in `<FocusTrap>` component
* drop `useHierarchy` from `<Dialog>` component
* simplify focusTrapFeatures setup
* simplify `useHierarchy`
The `useHierarchy` hook allowed us to determine whether we are in the
root, in the middle, a leaf, have a parent, have a child, ... but we
only ever checked whether we are a leaf node or not.
In other words, you can think of it like "are we the top layer"
This simplifies the implementation and usage of this new hook.
* move `enabled`-like argument to front
Just to be consistent with the other hooks.
* polyfill `toSpliced` for older Node versions
* add sibling dialogs playground
* rename `useHierarchy` to `useIsTopLayer`
* inline variable
* remove `unstable_batchedUpdates`
This is not necessary.
* add tiny bit of information to dialog
Because it might not be super obvious that we are going to close the
`<Dialog />`.
* update changelog
* re-add internal `PortalGroup`
This is necessary to make sure that a component like a `<MenuItems
anchor />` is rendered inside of the `<Dialog />` and not as a sibling.
While this all works from a functional perspective, if you rely on a
CSS variable that was defined on the `<Dialog />` and you use it in the
`<MenuItems />` then without this change it wouldn't work.
* move `enabled` parameter in hooks to front
Whenever a hook requires an `enabled` state, the `enabled` parameter is
moved to the front. Initially this was the last argument and enabled by
default but everywhere that we use these hooks we have to pass a
dedicated boolean anyway.
This makes sure these hooks follow a similar pattern. Bonus points
because Prettier can now improve formatting the usage of these hooks.
The reason why is because there is no additional argument after the
potential last callback.
Before:
```ts
let enabled = data.__demoMode ? false : modal && data.comboboxState === ComboboxState.Open
useInertOthers(
{
allowed: useEvent(() => [
data.inputRef.current,
data.buttonRef.current,
data.optionsRef.current,
]),
},
enabled
)
```
After:
```ts
let enabled = data.__demoMode ? false : modal && data.comboboxState === ComboboxState.Open
useInertOthers(enabled, {
allowed: useEvent(() => [
data.inputRef.current,
data.buttonRef.current,
data.optionsRef.current,
]),
})
```
Much better!
* inline variables
* improve TypeScript types for `Fieldset` component
* use `fieldset` instead of `div` by default
* only apply `role="group"` when not using a native `fieldset`
* apply `disabled` attribute
This is necessary if we want to make use of the default fieldset tag
(which also disables native form elements)
* adjust tests reflecting new changes
* conditionally apply props based on rendered element
* add `useResolvedTag` hook
This allows us to compute the `tag` name of a component. We can use a
shortcut based on the `props.as` and/or the `DEFAULT_XXX_TAG` of a
component. If this is not known/passed, then we compute it based on the
`ref` instead which requires an actual re-render.
* use `useResolvedTag` hook
* reflect change in `Field` related test
* update changelog
* inline variable
* add `useDefaultValue` hook
This allows us to have a guaranteed `default value` that never changes
unless the component re-mounts.
Since the hook returns a stable value, we can safely include it in
dependency arrays of certain hooks.
Before this change, including this is in the dependency arrays it would
cause a trigger or change of the hook when the `defaultValue` changes
but we never want that.
* do not handle `reset` when no `defaultValue` or `defaultChecked` was provided
If a `defaultValue` is provided, then the reset will be handled and the
`onChange` will be called with this value.
If no `defaultValue` was provided, we won't handle the `reset`,
otherwise we would call the `onChange` with `undefined` which is
incorrect.
* update changelog
* ensure we allow focus in the focus sentinel button
We already checked this button when inside of a `PopoverGroup`, but we
didn't when you weren't using a `PopoverGroup` component.
* add test
* update changelog
* ensure we correctly merge the `virtual` configuration
* use more generic `UpdateVirtualOptions`
This way we can passthrough the `disabled` function as well.
* properly handle re-use of `disabled` function
* use same order in objects
* cleanup `!` and `?` if we already know we are in `virtual` mode
* directly enable virtual mode in state if previously we weren't using virtual mode
* update changelog
When you have a `role="dialog"` and an `aria-modal="true"` at the same
time, then Safari will focus the first focusable element inside the
dialog.
This is not ideal, because in demo mode this means that the focus is
moved around to various DOM elements.
This commit ensures that the `aria-modal` is not applied when demo mode
is enabled to prevent that behavior in Safari.
* do not use default function for `allowed` and `disallowed`
Otherwise the fallback function will be used which will be a new
reference on each render. On pages with lots of elements this causes
performance issues.
* update changelog
* add mouse buttons
* add `useDisposables` hook
* add `useFrameDebounce` hook
Schedule a task in the next frame
* ensure we reset the `isTyping` flag correctly
* use same `mousedown` API as we did in React
This allows us to never leave the `input`, even when clicking on an
option.
* update changelog
* format comments
* inline `cb`
* remove `DialogBackdrop` and `DialogOverlay`
We deprecated those components in v1.6, since they are no longer
documented and this is a major release, we can safely get rid of it.
* update changelog
* migrate playground example to use `Dialog.Panel`
* Expose new components under dot notation
Most of them already where so only a couple were missing.
* Deprecate dot notation
* Add deprecation to `RadioGroupOption`
* Update deprecations
* Update changelog
* Update changelog
We keep specific types for elements with special meaning, such as
`HTMLButtonElement`, `HTMLLabelElement` or `HTMLInputElement` because
they receive certain attributes that generic DOM nodes (such as
HTMLDivElement) don't
For the components where we use simple `div` elements (and where people
use `as={...}`) that renders a different element, it doesn't make sense
to use `HTMLDivElement`. Using a more generic `HTMLElement` is simpler
and more correct (we still had `HTMLUListElement` and `HTMLLIElement`
for "div" DOM nodes which is incorrect).
This shouldn't be a breaking change because an `HTMLDivElement` is still
a valid `HTMLElement`. The other way around wouldn't be the case.
* use `act` from `react` instead of `@testing-library/react`
* bump dependencies
* bump `@testing-library/react`
* bump `@react-aria/interactions`
* bump "@tanstack/react-virtual"
* add `ResizeObserver` polyfill, and enable it by default for tests
* mock `getBoundingClientRect`
Otherwise the virtualization tests don't work as expected because they
rely on the client rect which is not supported (or not correctly
measured) in JSDOM.
* Scrolling to active option in combobox after opening when last option was selected with the mouse
* Allow virtual combobox to scroll freely with mouse wheel after changing options with keyboard
* Update changelog
Instead of waiting for a `useEffect` to trigger, we can use `useMemo` to
always compute the size the moment a DOM element is available (or
changes).
We also make sure to force a re-render when resizes are detected on the
stable DOM node, which in turn causes the `useMemo` to recompute.
* Only check for elements in useInertOthers
* Update changelog
* Update packages/@headlessui-react/CHANGELOG.md
---------
Co-authored-by: Jonathan Reinink <jonathan@reinink.ca>
* use `var(--anchor-max-height)` if available
When using the `anchor` prop, we try to position the anchored element
within the viewport. We use the size middleware of Floating UI to ensure
we are working in a constrained `max-width` and `max-height`.
However, if you want to limit the height of let's say a
`ComboboxOptions` then you instinctively add `max-h-60` for example. The
problem is that the `max-height` set by Floating UI will win because
it's inline styles.
You could use `!max-h-60` which makes it `!important`, but then you can
run into an issue where the max height set by the user is larger than
the available space which results in combobox options that are
unavailable.
To solve this, we want best of both worlds by ensuring we prefer the
size from the user, but constrain it with the value we know.
We now read from a `var(--anchor-max-height)` variable where you can set
your own preferred max height.
E.g.:
```ts
<Combobox>
<ComboboxInput />
<ComboboxOptions anchor="bottom start" className="[--anchor-gap:var(--spacing-4)] [--anchor-max-height:var(--spacing-60)]">
…
</ComboboxOptions>
</Combobox>
```
* update changelog
* ensure `TransitionRoot` component without props transitions correctly
A bit of a weird one, but you can use the `TransitionRoot` component
without any props for transitions themselves (so no real transition can
happen). Even crazier, it can happen that it doesn't even render a DOM
node, but just its children.
At this point, the `TransitionRoot` component is purely there for
orchestration purposes of child components.
Since there is no DOM node in certain situations, the transitions (and
its `onStart` and `onStop` callbacks) won't even happen at all. This
causes a bug (obvious in react strict mode) where children don't
properly mount or the transition component doesn't properly unmount.
* update changelog
* 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
* 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