Commit Graph

75 Commits

Author SHA1 Message Date
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
Robin Malfait c0f0d43383 Reset form-like components when the parent <form> resets (#2004)
* 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
2022-11-09 23:39:23 +01:00
Ernest dce2a1af07 fix: the order of compositionend events varies by browser (#1890)
* fix: the order of compositionend events varies by browser

* apply our style conventions

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2022-11-03 16:10:28 +01:00
Jordan Pittman ab78fbd91e Fire user’s onChange handler when we update the combobox input value internally (#1916)
* Fire user’s onChange handler when we update the input value internally

* Update changelog

* Fix CS
2022-10-10 15:17:52 -04:00
Jordan Pittman 1127a55a76 Warn when changing Combobox between controlled and uncontrolled (#1878) 2022-10-10 10:41:15 -04:00
Jordan Pittman 060f37b667 Fix use of undefined and displayValue in Combobox (#1865)
* Work around Vue multi-source + undefined bug

* Update changelog
2022-09-19 09:56:57 -04:00
Ernest e1f3ef862a Prevent option selection in ComboboxInput while composing (#1850)
* Fix should do nothing when event is fired within a composing session

* update changelog

* link to PR instead of issue

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2022-09-14 15:01:34 +02:00
Robin Malfait 46a7ab67ab Ensure Combobox.Label is properly linked when rendered after Combobox.Button and Combobox.Input components (#1838)
* ensure `Combbox.Label` is properly linked when rendered after other components

Even when rendered after the Combobox.Input / Combobox.Button

* update changelog
2022-09-08 23:18:33 +02:00
Robin Malfait b296b73783 Ensure Tab order stays consistent, and the currently active Tab stays active (#1837)
* 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
2022-09-08 19:14:42 +02:00
Robin Malfait 1fd8cfcad0 Expose the value from the Combobox and Listbox components render prop (#1822)
* expose the `value` for `Combobox` and `Listbox`

* update changelog
2022-09-05 16:11:12 +02:00
Jordan Pittman 13463f0295 Fix nullable by prop for combobox and radio group (#1815)
* Fix nullable by prop for combobox and radio group

* Update changelog
2022-09-02 16:10:10 -04:00
Robin Malfait 4878092712 Improve accessibility when announcing Listbox.Option and Combobox.Option components (#1812)
* 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
2022-09-02 16:31:46 +02:00
Robin Malfait 486ac8075d Improve the types of the Combobox component (#1761)
* 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
2022-08-11 23:03:56 +02:00
Jordan Pittman b28d177a95 Fix displayValue syncing problem (#1755)
* 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>
2022-08-10 12:38:30 -04:00
Robin Malfait 122eed7dbe Only select the active option when using "singular" mode (#1750)
* only select the active option when using "singular" mode

* update changelog
2022-08-09 16:38:29 +02:00
Robin Malfait 4d88dd9d7e Improve Combobox re-opening keyboard issue on mobile (#1732)
* prevent re-focusing Combobox Input after choosing selection

On mobile this gives a bit of annoying results where after choosing an
option, the keyboard is shown again because the input is focused again.

* update changelog
2022-08-01 15:27:39 +02:00
Robin Malfait 1831832458 Make form components uncontrollable (#1683)
* 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
2022-08-01 12:37:50 +02:00
Robin Malfait 6598db59b7 Ensure by comparison is used in multiple mode (#1717)
* 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
2022-07-26 17:34:14 +02:00
Jordan Pittman cf78d97716 Resync input when display value changes (#1679)
* Resync input when display value changes

* Update changelog
2022-07-15 10:24:02 -04:00
Robin Malfait a294fdbc6c Revert "prepare 1.6.7"
This reverts commit 9807e2ba7e.
2022-07-12 16:04:24 +02:00
Robin Malfait 9807e2ba7e prepare 1.6.7 2022-07-12 15:59:01 +02:00
Robin Malfait fc7def3295 Revert "prepare 1.6.6"
This reverts commit 06df02a158.
2022-07-07 23:06:18 +02:00
Robin Malfait 06df02a158 prepare 1.6.6 2022-07-07 22:57:45 +02:00
Robin Malfait 65bbacd894 Ensure CMD+Backspace works in nullable mode for Combobox component (#1617)
* ensure cmd+backspace works

The issue is that cmd+backspace technically already does work, but we
only allowed it when the Combobox is in an open state. We can remove
this check and apply the proper logic always.

* update changelog
2022-06-24 15:50:38 +02:00
Robin Malfait bc0b64a8c9 Revert "prepare 1.6.5"
This reverts commit c31136032b.
2022-06-20 18:00:31 +02:00
Robin Malfait c31136032b prepare 1.6.5 2022-06-20 16:22:05 +02:00
Robin Malfait 63417d5fd9 Fix missing aria-expanded for ComboboxInput component (#1605)
* add test to verify `Combobox.Input` state

* incorrect missing `aria-expanded` on `ComboboxInput`

* update changelog
2022-06-20 11:53:45 +02:00
Robin Malfait f2fc6d823b fix TypeScript issues (#1587)
We had a bunch of unused imports sitting around
2022-06-14 15:04:22 +02:00
Robin Malfait ea5f21af8e Improve Combobox input cursor position (#1574)
* fix(combobox): fix focus on option select

* update changelog

Co-authored-by: Dan Roujinsky <d.roujinsky@island.io>
2022-06-10 17:53:36 +02:00
Robin Malfait bdd1b3b785 Improve outside click of Dialog component (#1546)
* convert dialog in playground to use Dialog.Panel

* convert `tabs-in-dialog` example to use `Dialog.Panel`

* add scrollable dialog example to the playground

* simplify `outside click` behaviour

Here is a little story. We used to use the `click` event listener on the
window to try and detect whether we clicked outside of the main area we
are working in.

This all worked fine, until we got a bug report that it didn't work
properly on Mobile, especially iOS. After a bit of debugging we switched
this behaviour to use `pointerdown` instead of the `click` event
listener. Worked great! Maybe...

The reason the `click` didn't work was because of another bug fix. In
React if you render a `<form><Dialog></form>` and your `Dialog` contains
a button without a type, (or an input where you press enter) then the
form would submit... even though we portalled the `Dialog` to a
different location, but it bubbled the event up via the SyntethicEvent
System. To fix this, we've added a "simple" `onClick(e) { e.stopPropagation() }`
to make sure that click events didn't leak out.

Alright no worries, but, now that we switched to `pointerdown` we got
another bug report that it didn't work on older iOS devices. Fine, let's
add a `mousedown` next to the `pointerdown` event. Now this works all
great! Maybe...

This doesn't work quite as we expected because it could happen that both
events fire and then the `onClose` of the Dialog component would fire
twice. In fact, there is an open issue about this: #1490 at the time of
writing this commit message.
We tried to only call the close function once by checking if those
events happen within the same "tick", which is not always the case...

Alright, let's ignore that issue for a second, there is another issue
that popped up... If you have a Dialog that is scrollable (because it is
greater than the current viewport) then a wild scrollbar appears (what a
weird Pokémon). The moment you try to click the scrollbar or drag it the
Dialog closes. What in the world...?

Well... turns out that `pointerdown` gets fired if you happen to "click"
(or touch) on the scrollbar. A click event does not get fired. No
worries we can fix this! Maybe...

(Narrator: ... nope ...)

One thing we can try is to measure the scrollbar width, and if you
happen to click near the edge then we ignore this click. You can think
of it like `let safeArea = viewportWidth - scrollBarWidth`. Everything
works great now! Maybe...

Well, let me tell you about macOS and "floating" scrollbars... you can't
measure those... AAAAAAAARGHHHH

Alright, scratch that, let's add an invisible 20px gap all around the
viewport without measuring as a safe area. Nobody will click in the 20px
gap, right, right?! Everything works great now! Maybe...

Mobile devices, yep, Dialogs are used there as well and usually there is
not a lot of room around those Dialogs so you almost always hit the
"safe area". Should we now try and detect the device people are
using...?

/me takes a deep breath...

Inhales... Exhales...

Alright, time to start thinking again... The outside click with a
"simple" click worked on Menu and Listbox not on the Dialog so this
should be enough right?

WAIT A MINUTE

Remember this piece of code from earlier:

```js
onClick(event) {
  event.stopPropagation()
}
```

The click event never ever reaches the `window` so we can't detect the
click outside...

Let's move that code to the `Dialog.Panel` instead of on the `Dialog`
itself, this will make sure that we stop the click event from leaking
if you happen to nest a Dialog in a form and have a submitable
button/input in the `Dialog.Panel`. But if you click outside of the
`Dialog.Panel` the "click" event will bubble to the `window` so that we
can detect a click and check whether it was outside or not.

Time to start cleaning:
  - ☑️ Remove all the scrollbar measuring code...
    - Closing works on mobile now, no more safe area hack
  - ☑️ Remove the pointerdown & mousedown event
    - Outside click doesn't fire twice anymore
  - ☑️ Use a "simple" click event listener
    - We can click the scrollbar and the browser ignores it for us

All issues have been fixed! (Until the next one of course...)

* ensure a `Dialog.Panel` exists

* cleanup unnecessary code

* use capture phase for outside click behaviour

* further improve outside click

We added event.preventDefault() & event.defaultPrevented checks to make
sure that we only handle 1 layer at a time.

E.g.:

```js
<Dialog>
  <Menu>
    <Menu.Button>Button</Menu.Button>
    <Menu.Items>...</Menu.Items>
  </Menu>
</Dialog>
```

If you open the Dialog, then open the Menu, pressing `Escape` will close
the Menu but not the Dialog, pressing `Escape` again will close the
Dialog.

Now this is also applied to the outside click behaviour.
If you open the Dialog, then open the Menu, clicking outside will close
the Menu but not the Dialog, outside again will close the Dialog.

* add explicit `enabled` value to the `useOutsideClick` hook

* ensure outside click properly works with Poratl components

Usually this works out of the box, however our Portal components will
render inside the Dialog component "root" to ensure that it is inside
the non-inert tree and is inside the Dialog visually.

This means that the Portal is not in a separate container and
technically outside of the `Dialog.Panel` which means that it will close
when you click on a non-interactive item inside that Portal...

This fixes that and allows all Portal components.

* update changelog
2022-06-03 16:20:56 +02:00
Robin Malfait 1d53ac312f Revert "prepare 1.6.4"
This reverts commit 842d07146e.
2022-05-29 14:57:35 +02:00
Robin Malfait 842d07146e prepare 1.6.4 2022-05-29 14:51:38 +02:00
Robin Malfait a7154dca1f Remove leftover code in Combobox component (#1514)
* remove leftover code

This code existed before we had the option to make the first option the
"active" one.

This also contains a bug in the React code where pressing "ArrowDown" in
a closed Combobox opens the combobox and goes to the second item instead
of the first option.

* update changelog
2022-05-28 17:17:19 +02:00
Robin Malfait eefc03ce16 Ensure Escape propagates correctly in Combobox component (#1511)
* ensure `Escape` propagates correctly in Combobox component

* update changelog
2022-05-27 17:57:28 +02:00
Robin Malfait 08b419e9b7 Revert "prepare 1.6.3"
This reverts commit 3aaf20b9ac.
2022-05-25 15:25:48 +02:00
Robin Malfait 3aaf20b9ac prepare 1.6.3 2022-05-25 15:13:09 +02:00
Robin Malfait 2396c497f6 small cleanup
In the cleanup PR, we added the `Data` and `Actions` type, but we
already had a `Actions` type so had to rename it to something. Chose
`Command` but this is now inconsistent with the rest of the codebase.

Instead, let's revert that change and use these shorthands:

- `Data` -> `_Data`
- `Actions` -> `_Actions`
- `Commands` -> `Actions`
- `CommandTypes` -> `ActionTypes`

The `_` prefix is a little bit strange, but it is a private type and not
exposed so fine for now.
2022-05-23 12:16:19 +02:00
Robin Malfait e819c0a7b2 General/random internal cleanup (part 1) (#1484)
* sort React imports

* improve type signature of the `useEvent` hook

* use more correct `useIsoMorphicEffect` check in `useEvent`

* refactor `useCallback` to cleaner `useEvent`

* convert `const` to `let`

Just for consistency..

* cleanup `Tabs` code

Created explicit functions that can be called from child components
instead of calling `dispatch` directly. Introduced a `useData` and
`useActions` hook to make child components easier.

The seperation of `useData` allows us to pass down props directly
instead of going via the `useReducer` hook and dispatching actions to
make values up to date.

* cleanup `Combobox` code

* cleanup `RadioGroup` code
2022-05-23 11:26:22 +02:00
Robin Malfait d200be5f6f Add by prop for Listbox, Combobox and RadioGroup (#1482)
* Add `by` prop for `Listbox`, `Combobox` and `RadioGroup`

* update changelog
2022-05-20 23:01:10 +02:00
Robin Malfait 9280d92d24 Allow to override the type on the Combobox.Input (#1476)
* allow to override the `type` on the `Combobox.Input`

This still defaults to `text`.

* update changelog
2022-05-19 16:57:35 +02:00
Robin Malfait bf0d1120d3 Improve FocusTrap behaviour (#1432)
* refactor `VisuallyHidden` to `Hidden` component

This new component will also make sure that it is visually hidden to
sighted users. However, it contains a few more features that are going
to be useful in other places as well. These features include:

1. Make visually hidden to sighted users (default)
2. Hide from assistive technology via `features={Features.Hidden}`
   (will add `display: none;`)
3. Hide from assistive technology but make the element focusable via
   `features={Features.Focusable}` (will add `aria-hidden="true"`)

* add `useEvent` hook

This will behave the same (roughly) as the new to be released `useEvent`
hook in React 18.X

This hook allows you to have a stable function that can "see" the latest
data it is using. We already had this concept using:

```js
let handleX = useLatestValue(() => {
  // ...
})
```

But this returned a stable ref so you had to call `handleX.current()`.
This new hook is a bit nicer to work with but doesn't change much in the
end.

* add `useTabDirection` hook

This keeps track of the direction people are tabbing in. This returns a
ref so no re-renders happen because of this hook.

* add `useWatch` hook

This is similar to the `useEffect` hook, but only executes if values are
_actually_ changing... 😒

* add `microTask` util

* refactor `useFocusTrap` hook to `FocusTrap` component

Using a component directly allows us to simplify the focus trap logic
itself. Instead of intercepting the <kbd>Tab</kbd> keydown event and
figuring out the correct element to focus, we will now add 2 "guard"
buttons (hence why we require a component now). These buttons will
receive focus and if they do, redirect the focus to the first/last
element inside the focus trap.

The sweet part is that all the tabs in between those buttons will now be
handled natively by the browser. No need to find the first non disabled,
non hidden with correct tabIndex element!

* refactor the `Dialog` component to use the `FocusTrap` component

Also added a hidden button so that we know the correct "main" tree of
the application. Before this we were assuming the previous active
element which will still be correct in most cases but we don't have
access to that anymore since the logic is encapsulated inside the
FocusTrap component.

* ensure `<Portal />` properly cleans up

We make sure that the Portal is cleaning up its `element` properly.
We also make sure to call the `target.appendChild(element)`
conditionally because I ran into a super annoying bug where a focused
element got blurred because I believe that this re-mounts the element
instead of 'moving' it or just ignoring it, if it already is in the
correct spot.

* refactor: use `useEvent` instead of `useLatestValue`

Not really necessary, just cleaner.

* update changelog
2022-05-11 15:03:54 +02:00
Robin Malfait 0c34fe802c Add explicit multiple prop (#1355)
* add explicit `multiple` prop to the `Combobox`

This allows you to set the value to a **tuple** in `single-value` mode,
which was not possible before the `multiple` prop was introduced,
because then it resulted in `multi-value` mode instead of `single-value`
mode.

* add explicit `multiple` prop to the `Listbox`

This allows you to set the value to a **tuple** in `single-value` mode,
which was not possible before the `multiple` prop was introduced,
because then it resulted in `multi-value` mode instead of `single-value`
mode.

* update changelog

* update playground to use `multiple` prop
2022-04-22 18:55:55 +02:00
Robin Malfait b4a4e0b307 Add Dialog.Backdrop and Dialog.Panel components (#1333)
* implement `Dialog.Backdrop` and `Dialog.Panel`

* cleanup TypeScript warnings

* update changelog
2022-04-14 17:15:43 +02:00
Robin Malfait 0162c57d88 add React 18 compatibility (#1326)
* bump dev dependencies to React 18

* setup Jest to include `IS_REACT_ACT_ENVIRONMENT`

* prefer `useId` from React 18 if it exists

In React 16 & 17, where `useId` doesn't exist, we will fallback to our
implementation we have been using up until now.

The `useId` exposed by React 18, ensures stable references even in SSR
environments.

* update expected events

React 18 now uses the proper events:
- `blur` -> `focusout`
- `focus` -> `focusin`

* ensure to wait a bit longer

This is a bit unfortunate, but since React 18 now does an extra
unmount/remount in `StrictMode` to ensure that your code is
ConcurrentMode ready, it takes a bit longer to settle what the DOM sees.

That said, this is a temporary "hack". We are going to experiment with
using tools like Puppeteer/Playwright to run our tests in an actual
browser instead to eliminate all the weird details that we have to keep
in mind.

* prefer `.focus()` over `fireEvent.focus(el)`

* abstract `microTask` polyfill code

* prefer our `focus(el)` function over `el.focus()`

Internally we would still use `el.focus()`, but this allows us to have
more control over that `focus` function.

* add React 18 to the React Playground

* improve hooks for React 18

- Improving the cleanup of useEffect hooks
- useIsoMorphicEffect instead of normal useEffect, so that we can use
  useLayoutEffect to be a bit quicker.

* improve disposables

- This allows us to add event listeners on a node, and get automatic
  cleanup once `dispose` gets called.
- We also return all the `d.add` calls, so that we can cleanup specific
  parts only instead of everything or nothing.

* reimplement the Transition component to be React 18 ready

* wait an additional frame for everything to settle

* update playground examples

* suppressConsoleLogs for RadioGroup components

* update changelog

* keep the `to` classes for a smoother transition

In the next transition we will remove _all_ classes provided and re-add
the once we need.

---

Some extra special thanks:

- Thanks @silvenon for your initial work on the `transition` events in #926
- Thanks @thecrypticace for doing late-night debugging sessions

Co-authored-by: =?UTF-8?q?Matija=20Marohni=C4=87?= <matija.marohnic@gmail.com>
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
2022-04-13 22:07:01 +02:00
Robin Malfait ab6310c278 Implement nullable mode on Combobox in single value mode (#1295)
* implement `backspace` behaviour in tests

* add `Delete` Key

* implement `nullable` mode on Combobox in single value mode

If you pass a `nullable` prop to the Combobox, then it's possible to
unset the Combobox value by setting it to `null`.
This is triggered by removing all text from the input which will reset
the value itself as well.

* update changelog
2022-03-31 23:35:04 +02:00
Robin Malfait c475cab451 Allow Enter for form submit in RadioGroup, Switch and Combobox improvements (#1285)
* improve rendering of hidden form fields

* add `attemptSubmit` helper

This will allow us to _try_ and submit a form based on any element you
pass it. It will try and lookup the current form and if it is
submittable it will attempt to submit it.

Instead of submitting the form directly, we try to follow the native
browser support where it looks for the first `input[type=submit]`,
`input[type=image]`, `button` or `button[type=submit]`, then it clicks
it.

This allows you to disable your submit button, or have an `onClick` that
does an `event.preventDefault()` just like the native form in a browser
would do.

* ensure we can submit a form from a closed Combobox

When the Combobox is closed, then the `Enter` keydown event will be
ignored and thus not use `event.preventDefault()`.

With recent changes where we always have an active option, it means that
you will always be able to select an option.

If we have no option at all (some edge case) or when the combobox is
closed, then the `Enter` keydown event will just bubble, allowing you to
submit a form.

Fixes: #1282

This is a continuation of a PR ([#1176](https://github.com/tailwindlabs/headlessui/pull/1176)) provided by Alexander, so wanted to include
them as a co-author because of their initial work.

Co-authored-by: Alexander Lyon <arlyon@me.com>

* ensure we can submit a form from a RadioGroup

* ensure we can submit a form from a Switch

* simplify / refactor form playground example

* update changelog

Co-authored-by: Alexander Lyon <arlyon@me.com>
2022-03-31 21:42:34 +02:00
Robin Malfait 6897d2ccf1 Fix required double ArrowDown requirement (#1281)
* fix double arrow down requirement

If the `activeOptionIndex` is set to `null`, then we default to the very
first non-disabled option. This data is _not_ stored in state because if
you as the user go to a specific option, then start searching then we
will maintain the active option. This means that we have to **update**
the `activeOptionIndex` when options are moving around.

While making the first option the active one, we can't store that in
state directly otherwise the very first option becomes the active one.
If we then inject combobox options _before_ the current one then all of
a sudden your active option would jump around a bit.

We don't want this jumping to happen, we want the very first option to
be the one that's active no matter which option it is.

Since this is not stored in state, our keydown handler was a bit borked.
Internally it thinks we are still at `activeOptionIndex === null`
therefore pressing arrow down would move us to `activeOptionIndex ===
0`. To go to the second option, we can press down again which would move
us to `activeOptionIndex === 1`. The only issue is that visually we were
already at `0`.

This fixes that by making sure that if we have `activeOptionIndex ===
null` that we fallback to the very first non disabled option _before_ we
execute the `goToOption()` code.

 ### Before:

**Open combobox**, `activeOptionIndex === null`

| Combobox                |
| ----------------------- |
| **Option A** _(active)_ |
| Option B                |
| Option C                |

**Arrow Down**, `activeOptionIndex === 0`

| Combobox                |
| ----------------------- |
| **Option A** _(active)_ |
| Option B                |
| Option C                |

**Arrow Down**, `activeOptionIndex === 1`

| Combobox                |
| ----------------------- |
| Option A                |
| **Option B** _(active)_ |
| Option C                |

 ### After:

**Open combobox**, `activeOptionIndex === null`

| Combobox                |
| ----------------------- |
| **Option A** _(active)_ |
| Option B                |
| Option C                |

**Arrow Down**, `activeOptionIndex === 1`

| Combobox                |
| ----------------------- |
| Option A                |
| **Option B** _(active)_ |
| Option C                |

* update changelog
2022-03-29 18:52:44 +02:00
Robin Malfait 419ffdac2d Ensure that there is always an active option in the Combobox (#1279)
* ensure that the first option is always active

This will ensure that the first non-disabled option is the active one if
no other active options exist. This means that any time you search for
something that the first result is the active one and you can just press
<kbd>Enter</kbd> to activate the option.

However, there are a few rules that we have to take into account:
- If you just open the Combobox, and there is a `selected`
  Combobox.Option, then we can't make the first option the active one.
  The first selected Combobox.Option has precedence over this one. This
  is important and rather tricky because Combobox.Option's register
  themselves at some point (later) in time.
- If you already have an active option, then that option should stay
  active. If it changes position, then the activeOptionIndex is adjusted
  to account for that.
- If you "mouse leave" an option, then no option should be active. It
  will be re-enabled the moment you start typing OR if you re-open the
  Combobox. Otherwise, it can happen that you are at the bottom of the
  list, mouse leave, and we scroll all the way back up to make the first
  item the active one which is not good for UX reasons.

* filter list based on query in the playground

* update changelog
2022-03-29 15:30:10 +02:00
Robin Malfait 3e19aa5c97 Properly merge incoming props (#1265)
* rename inconsistent `passThroughProps` and `passthroughProps` to more
concise `incomingProps`

This is going to make a bit more sense in the next commits of this
branch, hold on!

* split props into `propsWeControl` and `propsTheyControl`

This will allow us to merge the props with a bit more control. Instead
of overriding every prop from the user' props with our props, we can now
merge event listeners.

* update `render` API to accept `propsWeControl` and `propsTheyControl`

* improve the merge logic

This will essentially do the exact same thing we were doing before:
```js
let props = { ...propsTheyControl, ...propsWeControl }
```

But instead of overriding everything, we will merge the event listener
related props like `onClick`, `onKeyDown`, ...

* fix typo in tests

* simplify naming

- Rename `propsWeControl` to `ourProps`
- Rename `propsTheyControl` to `theirProps`

* update changelog
2022-03-22 17:32:11 +01:00