* 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
* attempt to submit form when pressing `Enter` on the `Checkbox` component
* add test to verify form submissions when pressing enter works
* update changelog
* 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
* 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
* 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
* bump Next in playground
* convert legacy Link after Next.js bump
* update yarn.lock
* switch to npm workspaces
* move `packages/playground-*` to `playgrounds/*`
* use `npm` instead of `yarn`
* sync package-lock.json
* use node 20 for insiders releases
* only cancel certain events when disabled
The initial idea was that whenever an element had the `disabled` or
`aria-disabled` prop/attribute that we were going to remove all the
event listeners.
However, this is not ideal because in some scenario's we were actually
explicitly adding `onClick()` listeners (for `<a>` elements) to
`e.preventDefault()` when the link was marked as "disabled" to prevent
it executing the actual link click.
This commit will allow all defined listeners as-is, however, if you are
using one of the following event listeners:
- `onClick`
- `onPointerUp`
- `onPointerDown`
- `onMouseUp`
- `onMouseDown`
- `onKeyUp`
- `onKeyPress`
- `onKeyDown`
Then we will replace it with `(e) => e.preventDefault()` instead.
This way we are still invoking your own listeners, but are explicitly
calling `e.preventDefault()` on listeners that should not be executed in
the first place when an element is `disabled`.
* update CHANGELOG.md
* update CHANGELOG.md
* 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
* 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>