Commit Graph

779 Commits

Author SHA1 Message Date
Robin Malfait b478189fad Mark SwitchGroup as deprecated, prefer Field instead (#3232)
* mark `SwitchGroup` as deprecated

Also updated the `Switch.Group` message to also prefer the `<Field>`
component.

* update changelog
2024-05-23 16:24:26 +02:00
Robin Malfait 7fcb410be4 do not apply aria-modal in demo mode (#3227)
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.
2024-05-22 14:57:57 +02:00
Robin Malfait 045f2bc761 Ensure page doesn't scroll down when pressing Escape to close the Dialog component (#3218)
* ensure we blur the `document.activeElement` when pressing `Escape`

* update changelog
2024-05-21 13:07:04 +02:00
Jonathan Reinink 300e9eb957 Update changelog 2024-05-13 08:22:33 -04:00
Jonathan Reinink 3407625c51 Use same tense 2024-05-13 08:18:05 -04:00
Jonathan Reinink ea65164e66 Add active prop deprecation to changelog 2024-05-13 08:16:25 -04:00
Jordan Pittman 4eff138ada Don’t set a focus fallback for Dialog’s in demo mode (#3194)
* Don’t set a focus fallback for Dialog’s in demo mode

* Update changelog
2024-05-10 14:55:17 -04:00
Robin Malfait 031b39d522 update @headlessui/vue version in package-lock.json 2024-05-08 13:35:04 +02:00
Robin Malfait 35e7cbb375 sync with 1.x branch 2024-05-08 12:56:49 +02:00
Jordan Pittman f513614ffe 2.0.3 - @headlessui/react 2024-05-07 14:14:50 -04:00
Jordan Pittman a303819018 Make sure disabling demo mode on <Combobox> works (#3182)
* Make sure disabling demo mode on `<Combobox>` works

* Update changelog
2024-05-07 14:10:38 -04:00
Robin Malfait 48cf712d80 2.0.2 - @headlessui/react 2024-05-07 19:19:19 +02:00
Robin Malfait e0688c4865 Improve performance of internal useInertOthers hook (#3181)
* 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
2024-05-07 19:18:57 +02:00
Robin Malfait 886fdf7e6c Ensure clicking a ComboboxOption after filtering the options, correctly triggers a change (#3180)
* 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`
2024-05-07 16:49:07 +02:00
Jordan Pittman 2d5d35a533 2.0.1 - @headlessui/react 2024-05-06 15:07:53 -04:00
Jordan Pittman cfbcf5b840 Remove accidental deprecation comments on <DialogPanel> and <DialogTitle> (#3176)
* Remove accidental deprecations

These got left in when they shouldn’t’ve been

* Update changelog
2024-05-06 15:05:55 -04:00
Robin Malfait fb131905b4 2.0.0 - @headlessui/react 2024-05-06 17:41:56 +02:00
Jordan Pittman d416c1ca59 Don’t cancel touchmove on input elements inside a dialog (#3166)
* Don’t cancel touchmove on `input` elements inside a dialog

* Update changelog

* Update
2024-05-03 10:35:50 -04:00
Robin Malfait 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`
2024-05-03 16:22:39 +02:00
Jordan Pittman 1ae1af72ab Prep changelog for v2 (#3156)
* Prep changelog for v2

* Update packages/@headlessui-react/CHANGELOG.md

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>

* Tweak changelog

* Fix grammar

* wip

* Update

---------

Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
Co-authored-by: Jonathan Reinink <jonathan@reinink.ca>
2024-05-03 10:07:53 -04:00
Jordan Pittman 8d20cfb0c6 Deprecate dot notation (#3170)
* 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
2024-05-03 09:55:02 -04:00
Robin Malfait 0bd8c47c28 use HTMLElement for generic DOM node types (#3169)
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.
2024-05-03 15:32:55 +02:00
Robin Malfait f0e3e5b4a6 Bump dependencies (#3158)
* 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.
2024-05-02 14:41:58 +02:00
Jordan Pittman 1a440e1ee7 Fix issues with scrolling in a virtual combobox (#3163)
* 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
2024-05-01 15:57:28 -04:00
Jordan Pittman db702a7cec Only render virtual options wrapper when there are items to show (#3161)
* Only render virtual options wrapper when there are items to show

* Update changelog
2024-05-01 09:56:48 -04:00
Jordan Pittman 5952268bf7 Update changelog 2024-05-01 09:15:45 -04:00
Jordan Pittman 2e6cb126ef Render virtual items during an exiting transition (#3160)
Otherwise the render function will get called with different options by `render()` resulting in undefined
2024-05-01 09:14:49 -04:00
Jordan Pittman cb1abe42e6 Fix anchored elements not flipping when there is padding (#3157)
* Fix anchored elements not flipping when there is padding

* Update changelog
2024-04-30 16:08:02 -04:00
Robin Malfait f35214db4c calculate the size of an element as soon as possible (#3153)
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.
2024-04-30 19:31:40 +02:00
Jordan Pittman 319bcbad3b Only check for elements in useInertOthers (#3154)
* Only check for elements in useInertOthers

* Update changelog

* Update packages/@headlessui-react/CHANGELOG.md

---------

Co-authored-by: Jonathan Reinink <jonathan@reinink.ca>
2024-04-30 10:40:58 -04:00
Robin Malfait 4acf9e27f1 Ensure the static and portal props work nicely together (#3152)
* ensure we keep the `static` prop into account when using the `<Portal/>`
around anchored elements.

* update changelog
2024-04-29 20:47:00 +02:00
Robin Malfait 872808c8fc ensure we always set portal to true when the anchor props is truthy
We already had this for most components, but I missed it for this
component. This fixes that.
2024-04-29 20:29:28 +02:00
Robin Malfait beaae6a3f3 Use var(--anchor-max-height) when using the anchor prop (#3148)
* 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
2024-04-29 17:16:04 +02:00
Robin Malfait afc9cb648b Ensure TransitionRoot component without props transitions correctly (#3147)
* 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
2024-04-26 23:28:43 +02:00
Robin Malfait 539c124c69 Improve internal demo mode (#3143)
* 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
2024-04-26 16:35:12 +02:00
Robin Malfait 26e164447f Add overflow: auto when using the anchor prop (#3138) 2024-04-25 18:14:41 +02:00
Robin Malfait 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
2024-04-25 02:13:25 +02:00
Robin Malfait 36616b217e Update minimal peer dependency version requirements for react and react-dom (#3131)
* require at least React 18

We already relied on React 18 for Headless UI v2, but now it's also
reflected in the package.json

* update changelog
2024-04-24 19:39:12 +02:00
Robin Malfait d56a77bf52 Add frozen value to ComboboxOptions component (#3126)
* 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
2024-04-24 19:17:55 +02:00
Robin Malfait 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`
2024-04-24 17:10:41 +02:00
Jordan Pittman 166e862f01 Make sure data-disabled is available on virtualized options (#3128)
* 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>
2024-04-24 09:32:41 -04:00
Robin Malfait d2b734536f Add optional onClose callback to Combobox component (#3122)
* add optional `onClose` callback to `Combobox` component

* update changelog

* add tests to ensure `onClose` is called when `Combobox` closes
2024-04-23 15:45:22 +02:00
Robin Malfait 6c9e4b2b6f Allow passing a boolean to the anchor prop (#3121) 2024-04-22 23:42:58 +02:00
Robin Malfait 8a272c1333 cleanup changelog
Added and removed same feature, so dropping it entirely from the changelog.
2024-04-22 22:29:32 +02:00
Robin Malfait b4cda76f91 Remove the anchor.strategy option (#3120)
* 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
2024-04-22 17:05:30 +02:00
Robin Malfait 38551c8512 Ensure anchored components are always rendered in a stacking context (#3115)
* 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
2024-04-20 01:01:23 +02:00
Robin Malfait dcbcd79047 Move focus to ListboxOptions and MenuItems when they are rendered later (#3112)
* ensure `useEffect` is executed again if ref contents changed since last render

* update changelog
2024-04-19 18:35:31 +02:00
Robin Malfait b517a39445 Ensure anchored components are properly stacked on top of Dialog components (#3111)
* 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
2024-04-19 18:31:14 +02:00
Robin Malfait 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>
2024-04-19 16:15:11 +02:00
Robin Malfait 83cda0aa75 Change default tags for ListboxOptions, ListboxOption, ComboboxOptions, ComboboxOption and TabGroup components (#3109)
* 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
2024-04-19 01:54:25 +02:00