* 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>
* 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
* 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
* 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
* 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
* 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
* 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>
* 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
* Allow to open combobox on input focus
* Close focused combobox with openOnFocus prop when clicking the button
* ensure tabbing through a few fields, doesn't result in an incorrectly selected item
When you have a fwe inputs such as:
```html
<form>
<input />
<input />
<input />
<Combobox>
<Combobox.Input />
</Combobox>
<input />
<input />
<input />
</form>
```
Tabbing through this list will open the combobox once you are on the
input field. When you continue tabbing, the first item would be
selected. However, if the combobox is not marked as nullable, it means
that just going through the form means that we set a value we can't
unset anymore.
We still want to open the combobox, we just don't want to select
anything in this case.
* only `openOnFocus` if the `<Combobox.Input />` is focused from the
outside
If the focus is coming from the `<Combobox.Button />` or as a side
effect of selecting an `<Combobox.Option />` then we don't want to
re-open the `<Combobox />`
* update tests to ensure that the `Combobox.Input` is the active element
* order `handleBlur` and `handleFocus` the same way in Vue & React
* only select the active option when the Combobox wasn't opened by focusing the input field
* convert to `immediate` prop on the `Combobox` itself
* update changelog
* ensure we see the "relatedTarget" in Safari
Safari doesn't fire a `focus` event when clicking a button, therefore it
does not become the `document.activeElement`, and events like `blur` or
`focus` doesn't set the button as the `event.relatedTarget`.
Keeping track of a history like this solves that problem. We already had
the code for the `FocusTrap` component.
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
* move `nullable` handling to `onChange` of `Combobox.Input` itself
We were specifically handling backspace/delete keys to verify if the
`Combobox.Input` becomes empty then we can clear the value if we are in
single value and in nullable mode.
However, this doesn't capture other ways of clearing the
`Combobox.Input`, for example when use `cmd+x` or `ctrl+y` in the input.
Moving the logic, gives us some of these cases for free.
* ensure pressing `escape` also clears the input in nullable, single value mode without an active value
* adjust test to ensure we don't have a selected option instead of an active option
We still will have an active option (because we default to the first
option if nothing is active while the combobox is open). But since we
cleared the value when using the `nullable` prop, then it means the
`selected` option should be cleared.
* ensure `input` event is fired when firing keydown events
* ensure `defaultToFirstOption` is always set when going to an option
We recently made a Vue improvement that delayed the going to an option,
but this also included a bug where the `defaultToFirstOption` was not
set at the right time anymore.
* update changelog
* fix `than` / `then` typo
* Fix bug with non-controlled, multiple combobox in Vue
It thought it was always controlled which broke things
* Use correct value when resetting `<Listbox multiple>` and `<Combobox multiple>`
* Update changelog
* ensure the caret is at the end of the input, after syncing the value
This will ensure the caret is always in a consistent location once the
input value has synced. We will _not_ do this while the user is typing
because changing the position while typing will result in odd results.
Safari already kept the caret at the end, Chrome put the caret at the
end but once you synced the value once the caret was in front of the
text.
* update changelog
* add selection guards
Ensure that we only move the caret to the end, if the selection didn't
change in the meantime yet. For example when you have code like this:
```js
<Combobox.Input onFocus={e => e.target.select()} />
```
This will select all the text in the input field, if we just move the
caret position without keeping this into account then we would undo this
behaviour.
* add tests
* [vue] Fix Combobox input disabled state
* Add tests for disabled input in React and Vue
* Update changelog
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
* cleanup `XYZPropsWeControl`
The idea behind the `PropsWeControl` is that we can omit all the fields
that we are controlling entirely. In this case, passing a prop like
`role`, but if we already set the role ourselves then the prop won't do
anything at all. This is why we want to alert the end user that it is an
"error".
It can also happen that we "control" the value by default, but keep
incoming props into account. For example we generate a unique ID for
most components, but you can provide your own to override it. In this
case we _don't_ want to include the ID in the `XYZPropsWeControl`.
Additionally, we introduced some functionality months ago where we call
event callbacks (`onClick`, ...) from the incoming props before our own
callbacks. This means that by definition all `onXYZ` callbacks can be
provided.
* improve defining types
Whenever we explicitly provide custom types for certain props, then we
make sure to omit those keys first from the original props (of let's say
an `input`). This is important so that TypeScript doesn't try to "merge"
those types together.
* cleanup: move `useEffect`
* add `defaultValue` explicitly
* ensure tests are not using `any` because of `onChange={console.log}`
The `console.log` is typed as `(...args: any[]) => void` which means
that it will incorrectly mark its incoming data as `any` as well.
Converting it to `x => console.log(x)` makes TypeScript happy. Or in
this case, angry since it found a bug.
This is required because it _can_ be that your value (e.g.: the value of
a Combobox) is an object (e.g.: a `User`), but it is also nullable.
Therefore we can provide the value `null`. This would mean that
eventually this resolves to `keyof null` which is `never`, but we just
want a string in this case.
```diff
-export type ByComparator<T> = (keyof T & string) | ((a: T, b: T) => boolean)
+export type ByComparator<T> =
+ | (T extends null ? string : keyof T & string)
+ | ((a: T, b: T) => boolean)
```
* improve the internal types of the `Combobox` component
* improve the internal types of the `Disclosure` component
* improve the internal types of the `Listbox` component
* improve the internal types of the `Menu` component
* improve the internal types of the `Popover` component
* improve the internal types of the `Tabs` component
* improve the internal types of the `Transition` component
* use `Override` in `Hidden` as well
* cleanup unused code
* don't check the `useSyncExternalStoreShimClient`
* don't check the `useSyncExternalStoreShimServer`
* improve types in the render tests
* fix `Ref<TTag>` to be `Ref<HTMLElement>`
* improve internal types of the `Transition` component (Vue)
+ add `attrs.class` as well
* use different type for `AnyComponent`
* update changelog
* ensure we reset the `activeOptionIndex` if the active option is unmounted
Unmounting of the active option can happen when you are in a
multi-select Combobox, and you filter out all the selected values. This
means that the moment you press "Enter" on an active item, it becomes
the selected item and therefore will be filtered out.
* update changelog
* simplify `currentDisplayValue` calculation
Always calculate the currentDisplayValue, and only apply it if the user
is not typing. In all other cases it can be applied (e.g.: when the
value changes from the outside, inside or on reset)
* update changelog
* fix regression where `displayValue` crashes
It regressed in the sense that it now uses `displayValue` for the
`defaultValue` as well, but if nothing is passed it would crash.
Right now, it makes sure to only run the displayValue value on the
actual value and the actual default value if they are not undefined.
Note: if your displayValue is implemented like `(value) => value.name`,
and your `value` is passed as `null`, it will still crash (as expected)
because then you are in charge of rendering something else than null. If
we would "fix" this, then no value can be rendered instead of `null`.
Fixes: #2084
* update changelog
* make combobox playgrounds in React and Vue similar
* syncing of the input should happen when the value changes internally or externally
I also got rid of the manually dispatching of the change event if the
value changes from internally.
I think the correct mental model is:
- That the `Combobox.Input` value should change if the selected value
changes from the outside or from the inside.
- Note: It should _not_ do that if you are currently typing (once you
choose a new value it will re-sync, once you reset (escape / outside
click) it will also sync again).
- The `onChange`/`onInput` of the `Combobox.Input` itself should only be
called if you as the user type something. Not when the value is
"synced" based on the selected value. We were currently manually
dispatching events which works (to a certain extend) but smelled a bit
fishy to me.
The manual dispatching of events tried to solve an issue
(https://github.com/tailwindlabs/headlessui/issues/1875), but I think
this can be solved in 2 other ways that make a bit more sense:
1. (Today) Use the `onBlur` on the input to reset the `query` value to filter
options.
2. (In the future) Use an exposed `onClose` (or similar) event to reset
your `query` value.
* update changelog
* ignore flakey test
* add reset button to form example
* refactor React Listbox
This splitsup the raw `[state, dispatch]` to separate `useActions` and `useData` hooks.
This allows us to make the actions themselves simpler and include logic
that doesn't really belong in the reducer itself.
This also allows us to expose data via the `useData` hook that doesn't
belong in the state exposed from the `useReducer` hook.
E.g.: we used to store a `propsRef` from the root `Listbox`, and update
the ref with the new props in a `useEffect`. Now, we will just expose
that information directly via the `useData` hook. This simplifies the
code, removes useEffect's and so on.
* refactor Tabs, ensure function reference stays the same
If the `isControlled` value changes, then the references to all the
functions changed. Now they won't because of the `useEvent` hooks.
* type the actions abg similar to how we type the data bag
* refactor RadioGroup to use useData/useActions hooks
* reset Listbox to defaultValue on form reset
* reset Combobox to defaultValue on form reset
* reset RadioGroup to defaultValue on form reset
* reset Switch to defaultChecked on form reset
* port combinations/form playground example to Vue
* update changelog
* ensure `Combbox.Label` is properly linked when rendered after other components
Even when rendered after the Combobox.Input / Combobox.Button
* update changelog
* ensure tabs order stays consistent
This ensures that whenever you insert or delete tabs before the current
tab, that the current tab stays active with the proper panel.
To do this we had to start rendering the non-visible panels as well, but
we used the `Hidden` component already which is position fixed and
completely hidden so this should not break layouts where using flexbox
or grid.
* update changelog
* fix TypeScript issue
* ensure that `aria-selected` is explicitly set to `false`
The WAI-ARIA Best Practices don't recommend this and prefer
`aria-selected: true` or undefined (aka not existing when it is
"false"). However in practice, both MacOS VoiceOver and NVDA experience
strange issues if you don't do this (e.g.: everything before the
selected item is also selected)
* update tests to ensure we are checking for `aria-selected=false`
* update changelog
* improve types of `Combobox`
Now given the `multiple` and/or `nullable` props we ensure that the
types for the `value`, `defaultValue`, `onChange`, `by`, render prop,
... are all correct.
You will also be able to easily tell which type to use instead of
inferring it by doing something like this:
```tsx
<Combobox<ExplicitTypeHere>
value={...}
onChange={...}
...
>
...
</Combobox>
```
* update changelog
* ensure `syncInputValue` is updated correctly
* WIP
* WIP
* Don’t resync on open
* Fix react value syncing
update
* Add comment
* Port new setup over to Vue
* Remove `inputPropsRef`
We hardly knew ye
* Remove repro
* Cleanup
* Update changelog
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
* implement uncontrolled form components
A few versions ago we introduced compatibility with the native `form`
element. This means that behind the scenes we render hidden inputs that
are kept in sync which allows you to submit your normal form and get
data via `new FormData(event.currentTarget)`.
Before this change every form related component (Switch, RadioGroup,
Listbox and Combobox) always had to be passed a `value` and an
`onChange` regardless of this change.
This change will allow you to not even use the `value` and the
`onChange` at all and keep it completely uncontrolled.
This has some changes:
- `value` is made optional
- `onChange` is made optional (but will still be called if passed
regardless of being controlled or uncontrolled)
- `defaultValue` got added so that you can still pre-fill your values
with known values.
- `value` render prop got exposed so that you can still use this while
rendering.
This should also make it completely compatible with tools like Remix
without wiring up your own state.
* update example combinations/form playground to use uncontrolled
components
* improve types, add missing render prop arguments
* add tests for uncontrolled components (React)
* implement uncontrolled form elements in Vue
* use the `compare` function in multiple mode
* add tests to verify fix of incorrect `by` behaviour
* improve TypeScript types for the `by` prop
* update changelog