Commit Graph

117 Commits

Author SHA1 Message Date
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 8652f806eb replace as unknown as XYZ with as XYZ (#3091) 2024-04-11 11:26:19 +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 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 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 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
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
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
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
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
Timur Zurbaev fa952626c7 Add immediate prop to <Combobox /> for immediately opening the Combobox when the input receives focus (#2686)
* 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>
2023-08-31 15:23:56 +02:00
Robin Malfait 134c0fb221 Ensure blurring the Combobox.Input component closes the Combobox (#2712)
* ensure blurring the `Combobox.Input` component closes the `Combobox`

* update changelog

* select the value on blur if we are in single value mode
2023-08-29 16:24:37 +02:00
Robin Malfait 88b068cff1 Improve resetting values when using the nullable prop on the Combobox component (#2660)
* 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
2023-08-08 19:09:01 +02:00
Robin Malfait 88a0138d92 Fix incorrectly focused Combobox.Input component on page load (#2654)
* only call `setSelectionRange` when the input is the focused input

* update changelog
2023-08-07 15:03:12 +02:00
Jordan Pittman b380d03ccc Use correct value when resetting <Listbox multiple> and <Combobox multiple> (#2626)
* 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
2023-07-28 11:46:33 -04:00
Robin Malfait 1739edb479 Calculate aria-expanded purely based on the open/closed state (#2610)
* define `aria-expanded` based on open/closed state

You shouldn't be able to open a Listbox/Menu/Combobox/... when the
component is in a disabled state, however if you open it, and then
disable it then it is still in an open state. Therefore the
`aria-expanded` should still be present.

This is also how other libraries behave.

It is also how the native `<select>` behaves. You can open it, disable
it programmatically and then you are still able to make a selection.

This seems enough evidence that this change is an improvement without
being a breaking change.

Fixes: #2602

* update changelog
2023-07-24 12:51:21 +02:00
Robin Malfait 00fdb7e3cd Ensure IME works on Android devices (#2580)
* simplify `isComposing`

We had an issue #2409 where typing using an IME (Input Method Editor,
E.g.: Japanese — Romaji) already submitted characters while the user was
still composing the characters together.

1. Type `wa`
2. Expected result: `わ`
3. Actual result: `wあ` (where `あ` is the character `a`)

This was solved by not triggering change events at all until the
`compositionend` event was triggered. This worked fine for this use
case. However this also meant that only at the end of your typing
session (when you press `enter`/`space`) the actual value was submitted.

Fast forward to today, we received a new issue #2575 where this
behaviour completely broke on Android devices. Android _always_ use the
IME APIs for handling input... if we think about our solution form
above, it also means that while you are typing on an Android device no
options are being filtered at all. The moment you hit enter/space the
combobox will open and results will be filtered.

This is where this fix comes in. The goals are simple:

1. Make it work
2. Try to make the current code simpler

I started digging to see _why_ this `wあ` was even submitted. A normal
input field doesn't do that?! We have some code that does the following
things:

1. Sync the selected value with the `input` such that if you update the
   value from the outside, then the value in the `input` is up-to-date
   with the `displayValue` of that incoming value.
2. A fix for macOS VoiceOver to improve the VoiceOver experience when
   opening the `Combobox` component. This is done by manually resetting
   the value of the `input` field.

   1. Keep track of the current value
   2. Keep track of the current selection range (start/end) state
   3. Reset the input to an empty string `''`
   4. Restore the value to the captured value
   5. Restore the selection range

When you are typing, the input field doesn't have to update yet because
typing doesn't cause an option to become the `selected` option,
therefore it doesn't have to sync the value yet. So (1.) isn't the issue
here.

However, when you start typing, the Combobox should open and then we
trigger the macOS VoiceOver fix. This is touching the `input` field
because we change the value & selection.

Because we touched the `input` while the user was still in a composing
mode, it bailed and submitted whatever characters it had. This is the
part that we don't want. Not applying the macOS VoiceOver fix while
typing solves this issue. In addition, because _we_ are touching the
input field, VoiceOver is acting normally.

In hindsight, the solution is very simple: do not touch the input field
when the user is typing.

We still keep track whether the user `isComposing` so that we can bail
on the default `Enter` behaviour (marking the current option as the
selected option) because pressing `Enter` while composing should get out
of the IME.

Fixes: #2575

* update changelog
2023-07-06 14:35:35 +02:00
Robin Malfait 04fc6cfa06 Ensure the caret is in a consistent position when syncing the Combobox.Input value (#2568)
* 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
2023-06-30 12:58:44 +02:00
Jordan Pittman b98e642a67 Correctly handle IME composition in <Combobox.Input> (#2426)
* Don’t try to open combobox when composing characters

* wip

* Delay IME composition end until after keydown events

* Use `d.nextFrame` to handle `compositionend` event

* Update changelog
2023-04-17 10:17:28 -04:00
Robin Malfait c92a84782f Improve Combobox types to improve false positives (#2411)
* swap `Combobox` type order

If you use the default `Combobox` component then it defaults to
`Fragment`. This also means that if you provide an additional prop that
it would be forwarded to the `Fragment` but that won't work.

You do get a runtime error, but the types aren't 100% clear on what's
going on. In fact, they make it very confusing because it will use the
last "fallback" in all the `Combobox` definitions which marks the value
as "multiple".

Concretely, this means:
```ts
let [value, setValue] = useState('Tom Cook')

<Combobox
  value={value}
  onChange={setValue}
  placeholder="Hello!"
/>
```

Starts complaining about the `value` and `onChange` not handling the
`multiple` case correctly. But it should complain about the `placeholder`.

Switching the order _does_ solve this, but it is not the cleanest
solution.

Maybe we should be explicit about the `Fragment` case somehow.

However, there is a use case where I don't think TypeScript will be able
to help and it's a bit unfortunate.

```ts
let [value, setValue] = useState('Tom Cook')

<Combobox value={value} onChange={setValue} placeholder="Hello!">
   <div>
      {/* ... */}
   </div>
</Combobox>
```

This is valid because at runtime we will forward all the props to the
`div`. So not 100% sure what we should do here instead.

* update changelog
2023-04-03 17:46:20 +02:00
Arber Sylejmani fb612f7580 Add form prop to form-like components such as RadioGroup, Switch, Listbox, and Combobox (#2356)
* Adds form prop to Switch component

* add `form` prop to `Switch` component in Vue

+ tests for both React and Vue

* add `form` prop to `Combobox` component

* add `form` prop to `Listbox` comopnent

* add `form` prop to `RadioGroup` component

* update changelog

* add Oxford comma

* cleanup `screen` import

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2023-03-14 13:36:49 +01:00
Robin Malfait 989cd6b040 Fix XYZPropsWeControl and cleanup internal TypeScript types (#2329)
* 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
2023-03-02 22:50:41 +01:00
Jordan Pittman b8c214eebb Make React types more compatible with other libraries (#2282)
* Export explicit props types

* wip

* wip

* wip

* wip dialog types

* wip

* Fix build

* Upgrade esbuild

* Add aliased types for ComponentLabel and ComponentDescription

* Update lockfile

* Update changelog

* Update exported prop type names

* Make onChange optional

* Update tests

* Use `never` in CleanProps

Using a branded type doesn’t work properly with unions

* Fix types

* wip

* work on types

* wip

* wip

* Tweak types in render helpers

* Fix CS

* Fix changelog

* Tweak render prop types for combobox

* Update hidden props type name

* remove unused type

* Tweak types

* Update TypeScript version
2023-02-20 12:26:17 -05:00
Robin Malfait adfe121678 Start cleanup phase of the Dialog component when going into the Closing state (#2264)
* introduce `opening` and `closing` states

Also represent them as bits so that we can easily combine them while we
are transitioning from one state to the other.

* update `open/closed` state checks

Instead of checking whether it is in one state or an other, we can check
if the current state contains some potential sub-state.

This allows us to still check if we are in the `Open` state, while also
`Closing` because the state will be `S.Open | S.Closing`.

* expose `flags` from the `useFlags` hook

* add the `Closing` and `Opening` states to the Open/Closed state

* create dedicated `abcEnabled` variables

* keep the `State.Closing` into account for `scroll locking` and `inert others`

* add a test for the `Closing` state impacting the `Dialog` component

* cleanup unused imports

* add `unmount` util to the Vue Test renderer

* update changelog
2023-02-15 12:14:03 +01:00
Robin Malfait fcfd554514 Ensure we reset the activeOptionIndex if the active option is unmounted (#2274)
* 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
2023-02-10 19:54:58 +01:00
Robin Malfait b9af614919 Re-focus Combobox.Input when a Combobox.Option is selected (#2272)
* re-focus `Combobox.Input` when a `Combobox.Option` is selected

Except on mobile devices (ideally devices using a virtual keyboard), so
that the virtual keyboard won't be triggered every single time we
re-focus that input field.

* update changelog
2023-02-10 16:30:53 +01:00
Robin Malfait 7ecf8323dc Move aria-multiselectable to [role=listbox] in the Combobox component (#2271)
* move `aria-multiselectable` to `[role=listbox]` in the `Combobox` component

* update changelog
2023-02-10 15:41:30 +01:00
Robin Malfait 5051fff04f Ensure we handle null dataRef values correctly (#2258)
* ensure we handle `null` dataRef values correctly

Initially when the `dataRef` is created, then the `current` value is
going to be `null`. We didn't properly encode this in the types. Now
that we do, it exposed some places where this was used incorrectly
(because we assumed it was always defined).

* update changelog
2023-02-06 18:11:47 +01:00
Robin Malfait d146b78a97 Revert "Use the import * as React from 'react' pattern (#2242)"
This reverts commit 0276231c31.
2023-02-06 12:30:30 +01:00
Robin Malfait 0276231c31 Use the import * as React from 'react' pattern (#2242)
* use the `import * as React from 'react'` pattern

We use named imports, but we have to import `React` itself as well for
JSX because it compiles to `React.createElement`. We could get rid of
our own JSX and use it directly, or we can use this `import * as React
from 'react'` syntax.

This fixes an issue for people using `allowSyntheticDefaultImports: false` in TypeScript.

Fixes: #2117

* update changelog
2023-02-02 15:34:16 +01:00
Adventune f2d9ea08b6 fix(@headless-react / @headless-vue): update WAI-ARIA reference links (#2230)
* fix(@headlessui/react): WAI-ARIA reference links

* fix(@headlessui-vue): Fix WAI-ARIA reference links

---------

Co-authored-by: lumilno <nooa.lumilaakso@gmail.com>
2023-01-31 11:42:02 -05:00
Robin Malfait 1d94d15c79 Improve Combobox accessibility (#2153)
* add the `aria-autocomplete` attribute

* drop the `aria-activedescendant` attribute on the `Combobox.Options` component

It is only required on the `Combobox.Input` component.

* improve triggering VoiceOver when opening the `Combobox`

We do this by mutating the `input` value for a split second to trigger a
change that VoiceOver will pick up. We will also ensure to restore the
value and the selection / cursor position so that the end user won't
notice a difference at all.

* update changelog

Fixes: #2129
Co-authored-by: Andrea Fercia <a.fercia@gmail.com>
2023-01-24 18:10:14 +01:00
Robin Malfait 3cb80795ae update aria-haspopup to use the correct role (#2101)
`aria-haspopup` should now contain the corresponding role instead of
just true or false. The `aria-haspopup="true"` is considered a `menu`
now.

Context: https://w3c.github.io/aria/#aria-haspopup

Fixes: #2099
2022-12-15 16:32:14 +01:00
Robin Malfait 1f2de63b40 Fix displayValue syncing when Combobox.Input is unmounted and re-mounted in different trees (#2090)
* 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
2022-12-12 19:16:46 +01:00
Robin Malfait 46754e637c Fix regression where displayValue crashes (#2087)
* 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
2022-12-12 16:43:57 +01:00
Jordan Pittman 2e941f85dd Ignore mouse move/leave events when the cursor hasn’t moved (#2069)
* Ignore mouse move/leave events when the cursor hasn’t moved

A mouse enter / leave event where the cursor hasn’t moved happen only because of:
- Scrolling
- The container moved

* Fix linting errors

* Update changelog

* wip

* Fix tests

* fix linting error

* Tweak tests to bypass tracked pointer checks

* Fixup

* Add stuff

* Fix build script

* fix stuff

* wip
2022-12-07 13:50:57 -05:00
Robin Malfait 219901c84f Allow passing in your own id prop (#2060)
* accept `id` as a prop where it is currently hardcoded (React)

Continuation of #2020

Co-authored-by: Olivier Louvignes <olivier@mgcrea.io>

* accept `id` as a prop where it is currently hardcoded (Vue)

* update changelog

* apply React's hook rules

Co-authored-by: Olivier Louvignes <olivier@mgcrea.io>
2022-12-02 23:56:33 +01:00
Robin Malfait a0bcbae93a Fix crash when using multiple mode without value prop (uncontrolled) for Listbox and Combobox components (#2058)
* ensure `value` in `multiple` mode is always an array

In case nothing or `undefined` was passed.

* update changelog
2022-12-02 15:18:31 +01:00
Robin Malfait 4da0b3aba9 Improve syncing of the Combobox.Input value (#2042)
* 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
2022-11-23 16:16:10 +01:00
Robin Malfait 8b4363d7a4 Ensure shift+home and shift+end works as expected in the Combobox.Input component (#2024)
* ensure shift+home and shift+end works as expected

While testing with a normal input, the Home and End keys don't do
anything on their own, so therefore using them to go to the first or
last item respectively is still a good solution.

However, shift+home and shift+end will do _something_, it will select
the text in the input.

* update changelog
2022-11-17 15:00:26 +01:00