Commit Graph

359 Commits

Author SHA1 Message Date
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 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 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 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
Robin Malfait 004b8dcf34 Omit nullable prop from Combobox component (#3100)
* 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
2024-04-15 18:17:15 +02:00
Robin Malfait 512bf44150 Ensure the multiple prop is typed correctly when passing explicit types to the Combobox component (#3099)
* 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
2024-04-15 17:42:59 +02:00
Robin Malfait b86737b698 Add new CloseButton component and useClose hook (#3096)
* 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
2024-04-12 15:54:01 +02:00
Robin Malfait 00f84acb21 Render hidden form input fields for Checkbox, Switch and RadioGroup components (#3095)
* 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
2024-04-11 18:03:16 +02:00
Robin Malfait 8652f806eb replace as unknown as XYZ with as XYZ (#3091) 2024-04-11 11:26:19 +02:00
Robin Malfait b3377eb550 Deprecate the entered prop on the Transition component (#3089)
* 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
2024-04-09 20:08:31 +02:00
Robin Malfait ae8c253c21 Fix typos (#3086)
* fix a bunch of typos

* fix typos in `@headlessui/vue`
2024-04-08 23:31:50 +02:00
Robin Malfait 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>
2024-04-05 16:05:06 +02:00
Robin Malfait c1d3b7ecda Close the Combobox, Dialog, Listbox, Menu and Popover components when the trigger disappears (#3075)
* 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`
2024-04-03 15:10:58 +02:00
Robin Malfait 4ed69f640c Keep focus inside of the <ComboboxInput /> component (#3073)
* 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>
2024-04-03 15:06:41 +02:00
Robin Malfait 4f89588239 Fix cursor position when re-focusing the ComboboxInput component (#3065)
* add `useRefocusableInput` hook

* use the new `useRefocusableInput` hook

* update changelog

* infer types of the `ref`
2024-03-29 16:15:29 +01:00
Robin Malfait 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
2024-03-29 15:41:12 +01:00
Robin Malfait 6d44a8d049 Expose missing data-disabled and data-focus attributes on the TabsPanel, MenuButton, PopoverButton and DisclosureButton components (#3061)
* 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
2024-03-27 17:04:28 +01:00
Robin Malfait bf4dc77f7a Expose the --button-width CSS variable on the PopoverPanel component (#3058) 2024-03-26 20:00:01 +01:00
Robin Malfait 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
2024-03-26 17:58:19 +01:00
Robin Malfait 000e0c0192 Prevent unnecessary execution of the displayValue callback in the ComboboxInput component (#3048)
* 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
2024-03-21 13:46:17 +01:00
Robin Malfait 834dbf423e Respect selectedIndex for controlled <Tab/> components (#3037)
* ensure controlled `<Tab>` components respect the `selectedIndex`

* update changelog

* use older syntax in tests

* run prettier to fix lint step
2024-03-15 14:37:16 +01:00
Robin Malfait a50be9255a Forward disabled state to hidden inputs in form-like components (#3004)
* make hidden inputs disabled if the wrapping component is disabled

* add tests to verify disabled hidden form elements

* update changelog
2024-02-21 14:16:31 +01:00
Jordan Pittman c71c6763f1 Make Listbox submit form on Enter when not open (#2972) 2024-02-06 16:06:35 -05:00
Robin Malfait 0e0277a684 Allow setting custom tabIndex on the <Switch /> component (#2966)
* allow setting a custom `tabIndex` on the `<Switch />` component

* update changelog
2024-02-03 17:31:02 +01:00
Robin Malfait da94b80860 Attempt form submission when pressing Enter on Checkbox component (#2962)
* attempt to submit form when pressing `Enter` on the `Checkbox` component

* add test to verify form submissions when pressing enter works

* update changelog
2024-02-02 15:51:58 +01:00
Robin Malfait ce17c6d15f Ensure children prop of Field component can be a render prop (#2941)
* ensure `children` prop can be a render prop

* update changelog
2024-01-23 14:01:05 +01:00
Robin Malfait f2bc6fdd40 Use isFocused instead of isFocusVisible for Input and Textarea components (#2940)
* use `isFocused` in `Input` and `Textarea`

We do this because we always want to show the focus ring regardless of
whether you used the mouse or the keyboard.

* update changelog
2024-01-23 12:13:55 +01:00
Jordan Pittman 3b2a102e8d Don’t override explicit disabled prop for components inside <MenuItem> (#2929)
* Don’t override explicit disabled prop inside `<MenuItem>`

* Update changelog
2024-01-16 15:34:41 -05:00
Robin Malfait aff438eb06 Prevent default behaviour when clicking outside of a Dialog.Panel (#2919)
* use `event.preventDefault()` in the `useOutsideClick` on Dialog's

When using a `Dialog`, we should prevent the default behaviour of the
event that triggered the "close" in the `useOutsideClick` call.

We recently made improvements to improve outside click behaviour on
touch devices (https://github.com/tailwindlabs/headlessui/pull/2572) but
due to the `touchend` event, the touch is still forwarded and therefore
a potential button _behind_ the "backdrop" will also be clicked. This is
not what we want.

Added the `event.preventDefault()` for the Dialog specifically because
there are other places where we use `useOutsideClick` and where we _do_
want the behaviour where the click just continues. A concrete example of
this is 2 `Menu`'s next to eachother where you open the first one, and
then click on the second one. This should close first one (outside
click) and open the second one (by not preventing the event)

* update changelog
2024-01-09 20:06:55 +01:00
Robin Malfait 2b7a57e337 Expose disabled state on <Tab /> component (#2918)
* expose `disabled` on `<Tab/>` component

This will expose it such that you can use it with `ui-disabled`. In the
Alpha version of React, you can also use `data-[disabled]` because it
will be exposed as `data-disabled` over there as well.

Fixes: #2864

* update changelog
2024-01-09 15:45:30 +01:00
Robin Malfait 3b961a690f Ensure Input, Select and Textarea expose Ref related types (#2902)
* ensure `Input`, `Select` and `Textarea` expose `Ref` related types

* update changelog
2023-12-27 20:34:32 +01:00
Robin Malfait e662f12398 2.0.0 Alpha prep (#2887)
* bump React & React DOM dependencies

* fix typo `TOmitableProps` → `TOmittableProps`

* bump prettier

* run prettier after prettier version bump

* bump TypeScript

* run prettier after TypeScript version bump

* enable `verbatimModuleSyntax`

This ensures all imported types are using the `type` keyword.

* add `type` to type related imports

* add common testing scenarios

Will be used in the new and existing components.

* add script to make Next.js happy

Right now Next.js does barrel file optimization and re-writing imports
to a real path in the `dist` folder. Most of those rewrites don't
actually exist because they have an assumption:

```js
import { FooBar } from '@headlessui/react'
```

is rewritten as:
```js
import { FooBar } from '@headlessui/react/dist/components/foo-bar/foo-bar'
```

This script will make sure these paths exist...

* improve `by` prop, introduce `useByComparator`

This hook has a default implementation when comparing objects. If the
object contains an `id`, then we will compare the objects by their
`id`'s without the user of the library needing to specify `by="id"`.

If the objects don't have an `id` prop, then the default is still to
compare by reference (unless specicified otherwise).

* sync yarn.lock

* rename `Features` to `HiddenFeatures` for `Hidden` component

* rename `Features` to `FocusTrapFeatures` in `FocusTrap` component

* rename `Features` to `RenderFeatures` in `render` util

* add `floating-ui` as a dependency + introduce internal floating related components

* bump Vue dependencies

* ensure scroll bar calculations can't go negative

* improve types in `@headlessui/vue`

* use snapshot tests for `Transition` tests in `@headlessui/vue`

* use snapshot tests for `portal` tests in `@headlessui/vue`

* rename `src/components/transitions/` to `src/components/transition/` (singular)

This is so that we can be consistent with the other components.

* drop custom `toMatchFormattedCss`, prefer snapshot tests instead

* use snapshot tests for `Label` tests in `@headlessui/vue`

* use snapshot tests for `Description` tests in `@headlessui/vue`

* sort exported components in tests for `@headlessui/vue`

* use snapshot tests in `@headlessui/tailwindcss`

* rename `mergeProps` to `mergePropsAdvanced`

This is a more complex version of a soon to be exported `mergeProps`
that we will be using in our components.

* do not expose `aria-labelledby` if it is only referencing itself

* expose boolean state as `kebab-case` instead of `camelCase`

These are the ones being exposed inside `data-headlessui-state="..."`

* expose boolean data attributes

A slot with `{active,focus,hover}` will be exposed as:
```html
<span data-headlessui-state="active focus hover"></span>
```

But also as boolean attributes:
```html
<span data-active data-focus data-hover></span>
```

* improve internal types for `className` in `render` util

* ensure we keep exposed data attributes into account when trying to forward them to the component inside the `Fragment`

* add small typescript type fix

This is internal code, and the public API is not influenced by this
`:any`. It does make TypeScript happy.

* introduce `mergeProps` util to be used in our components

This will help us to merge props, when event handlers are available they
will be merged by wrapping them in a function such that both (or more)
event handlers are called for the same `event`.

* add new internal `Modal` component

* fix: when using `Focus.Previous` with `activeIndex = -1` should start at the end

* prefer `window.scrollY` instead of `window.pageYOffset`

Because `window.pageYOffset` is deprecated.

* add `'use client'` directives on client only components

These components use hooks that won't work in server components and you
will receive an error otherwise.

* drop `import 'client-only'` in favor of the `'use client'` directive

* add React Aria dependencies

* pin beta dependencies

* prettier bump formatting

* improve TypeScript types in tests

* use new Jest matchers instead of deprecated ones

* improve typescript types in Vue

* prefer `useLabelledBy` and `useDescribedBy`

* add internal `DisabledProvider`

* add internal `IdProvider`

* add internal `useDidElementMove` hook

* add internal `useElementSize` hook

* add internal `useIsTouchDevice` hook

* add internal `useActivePress` hook

* use snapshot tests for `Description` tests

* use snapshot tests for `Label` tests

* use snapshot tests for `Portal` tests

* use snapshot tests for `render` tests

* add (private) `Tooltip` component

Currently this one is not ready yet, so its not publicly exposed yet.

* add internal `FormFields` component

This one adds a component to render (hidden) inputs for native form
support. It also ensures that form fields can be hoisted to the end of
the nearest `Field`. If the components are not inside a `Field` they
will be rendered in place.

* add new `Button` component

* add new `Checkbox` component

* add new `DataInteractive` component

* add new `Field` component

* add new `Fieldset` component

* add new `Legend` component

* add new `Input` component

* add new `Select` component

* add new `Textarea` component

* export new components

* WIP

* remove `within: true`

This only makes sense if anything inside the current element receives
focus, which is not the case for `input`, `select`, `textarea` or
`Radio/RadioOption`.

* group focus/hover/active hooks together

* conditionally link anchor panel

* immediately focus the container

* prevent premature disabling of `Listbox`'s floating integration

+ Track whether the button moved or not when disabling such that we can
  disable the transitions earlier.

* improve scroll locking on iOS

* skip hydration tests for now

* skip certain focus trap tests for now

* update CHANGELOG.md

* add missing requires

* drop unused `@ts-expect-error`

* ignore type issues in playgrounds

These playgrounds are mainly test playgrounds. Lower priority for now,
we will get back to them.

* add yarn resolutions to solve swc bug
2023-12-20 19:57:57 +01:00
Евгений c24ba868bd Fix error when transition classes contain new lines (#2871)
* fix DOMException when remove class with '\n' character in react 'transition' component

* Split classes on all whitespace

* Revert "fix DOMException when remove class with '\n' character in react 'transition' component"

This reverts commit 76e835441b9cb84a1d867e1a37496c55cae86179.

* fix typo

* Add test

* Fix CS

* Update changelog

---------

Co-authored-by: Jordan Pittman <jordan@cryptica.me>
2023-12-12 15:09:55 -05:00
Robin Malfait bfacb648dc Fix VoiceOver bug for Listbox component in Chrome (#2824)
* fix VoiceOver bug for Listbox in Chrome

Chrome currently has a bug if you use a `Listbox` with a `Label` and use
the `aria-multiselectable` attribute. This combination will cause
VoiceOver to _not_ announce the `role="option"` elements when
interacting with them.

If we drop the `aria-multiselectable` OR the `aria-labelledby` it starts
working. Alternatively replacing `aria-labelledby` with `aria-label`
won't work either.

I filed a Chrome bug report about this here: https://bugs.chromium.org/p/chromium/issues/detail?id=1498261

---

Luckily there is a workaround in our `Listbox` implementation. Right now
we always require the `Listbox.Button` to be there. The
`Listbox.Options` component doesn't work on its own in our
implementation.

This means that whenever we open the `Listbox` that we have to go via
the `Listbox.Button`. This `Listbox.Button` is already labelled by the
`Listbox.Label` if there is one.

This also means that we can safely drop the `id` of the label inside the
`aria-labelledby` from the `Listbox.Options`.

This wouldn't have worked if our `Listbox.Options` could be used in a
standalone way without the `Listbox.Button`.

At the end of the day the hierarchy looks like this:

- Options is labelled by the Button
   - Button is labelled by the Label
      - Label

Fixes: #2817

* update changelog
2023-11-02 17:52:43 +01:00
Robin Malfait 99cdf91631 Implement new virtual API for the Combobox component (#2779)
* add `(Vue)` or `(React)` to playground header

* show amount of items in virtualized example

* improve calculating the active index

* disable strict mode

* update virtualized playground examples with preferred API

* optimize `calculateActiveIndex`

* implement new `virtual` API

* update changelog
2023-10-02 11:55:20 +02:00
Jordan Pittman 0f34486064 Make sure as={Fragment} doesn’t result in a render loop (#2760)
* Make sure `as={Fragment}` doesn’t result in a render loop

* Add comment about usage

* Fix render loop on popover panel too

* Update changelog
2023-09-20 08:57:27 -04:00
zakstucke 2a64c131bd Update Radio's OptionFn effect deps to fix infinite loop. (#2758) 2023-09-19 11:24:59 +02:00
Robin Malfait f016dc51db Add virtual prop to Combobox component (#2740)
* type timezones in playground data

* add `@tanstack/react-virtual` and `@tanstack/vue-virtual`

* use latest stable Tailwind CSS version

* add Combobox with virtual prop example

* add `virtual` prop to `Combobox`

Co-authored-by: Jordan Pittman <jordan@cryptica.me>

* add tests for `virtual` prop

- Also wrap `click` helpers in `act` for React (use `rawClick` without
  `act` in tests related to `Transition`)

* update changelog

---------

Co-authored-by: Jordan Pittman <jordan@cryptica.me>
2023-09-15 14:29:18 +02:00
Robin Malfait f2179f36c0 further improve import sorting 2023-09-11 19:09:53 +02:00
Robin Malfait 76dd10ea55 Sort imports automatically (#2741)
* add `prettier-plugin-organize-imports` and `prettier-plugin-tailwindcss`

* format

* bump Tailwind CSS

* format playgrounds using updated Tailwind CSS and Prettier plugins

* use import syntax
2023-09-11 18:36:30 +02:00
Jordan Pittman 8e93cd0630 Export component interfaces and mark them as internal (#2313)
* export component interfaces, and mark them as internal

This is not ideal because we don't want these to be public. However, if
you are creating components on top of Headless UI, the TypeScript
compiler needs access to them.

So now they are public in a sense, but you shouldn't be interacting with
them directly.

Co-authored-by: Jordan Pittman <jordan@cryptica.me>

* Update changelog

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2023-08-31 11:38:18 -04:00