* make `disposables` consistent
Also added a `group` function, this allows us to spawn a _sub_
disposables group that can be disposed on its own, but will also be
disposed the moment the "parent" is disposed.
* ensure Transition component works when nothing is transitioning
* update changelog
* use the Dialog's parent as the root for the Intersection observer
We have some code that allows us to auto-close the dialog the moment it
gets hidden. This is useful if you use a dialog for a mobile menu and
you resizet he browser. If you wrap the dialog in a `md:hidden` then it
auto closes. If we don't do this, then the dialog is still locking the
scrolling, keeping the focus in the dialog, ... but it is not visible.
To solve this we use an `IntersectionObserver` to verify that the
`boundingClientRect` is "gone" (x = 0, y = 0, width = 0 and height = 0).
However, the intersection observer is not always triggered. This happens
if the main content is scrollable.
Setting the `root` of the `IntersectionObserver` to the parent of the
`Dialog` does seem to solve it.
Not 100% sure what causes this behaviour exactly.
* use a `ResizeObserver` instead of `IntersectionObserver`
* implement a `ResizeObserver` for the tests
* update changelog
* drop `d.enqueue` & `d.workQueue`
This was only used in tests and doesn't seem to be necessary.
* drop `handleChange` from the `ComboboxInput` component
This only emitted a `change` event, which Vue already emits as well.
* drop `onChange` from incoming props
This is an odd one. In Chrome this means that the `@change` is still
being called, but if we keep it, then the `@change` is _also_ called on
blur resulting in odd bugs.
Droping it fixes that issue.
That said, the `@change` is _still_ emitted and therefore the callback
is properly called and the `ComboboxInput` still can interact with the
`@change` event.
* update changelog
* drop `@ts-expect-error`, because `inert` is available now
* fix logical error
We want to apply `inert` when we _don't_ have nested dialogs, because if
we _do_ have nested dialogs, then the inert should be applied from the
nested dialog (or visually the top most dialog).
* update changelog
* replace `useInertOthers` with `useInert`
* add `assertInert` and `assertNotInert` accessibility assertion helpers
* ensure the `main tree` root is marked as inert
As well as the parent dialogs in case of nested dialogs.
* 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
* 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
* 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
* Fix overflow when swapping dialogs that use transition
* Refactor
* refactor
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* Inline shim for ESM support
Until the official package adds an ESM version with a wildcard import we can’t use it. This version was copied from Remix Router
* Add dialog shadow root examples
* Fix SSR error
* Add repro for iOS scrolling issue
* Try to fix vercel build
idk what’s wrong here
* Update repro
A transition is required to delay closing enough to demonstrate the bug
* Port global dialog state to Vue
* Add dialog test to Vue
* wip
* wip
* Workaround bug
This shouldn’t happen at all and we need to find the source of the bug but this should “fix” things for the time being
* wip
* Rebuild overflow locking with simpler API
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* Update deps
* wip
* simplify
* Port to Vue
* wip
* wip
* Tweak tests
* Update changelog
* Ensure meta callbacks are cleaned up
* cleanup
* wip
* ensure chaning the `selectedIndex` tabs properly wraps around
We never want to use and index that doesn't map to a proper tab.
This commit also makes the implementation similar for both React and
Vue.
* add tests to prove the underflow and overflow wrapping
* drop updating the index manually
This is already adjusted when tabs change internally. You can still
manually change it of course, but for these tests that doesn't matter
and cause different results.
* update changelog
* add tests to guarantee `FocusTrap` with a single element works as expected
* it should keep the focus in the Dialog
Even if there is only 1 element. We were skipping the current active
element so the container didn't have any elements anymore and just
continued to the next focusable element in line. This will prevent that
and ensure that we can only skip elements if there are multiple ones.
* update changelog
* ensure `relatedTarget` is an `HTMLElement`
Or in other words, Robin trust the type system...
I was assuming that this was always an `HTMLElement` or `null` but
that's not the case. Just using `e.relatedTarget` shows that `dataset`
is not always available.
* update changelog
* 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>
* detect change in `Tab` order
This will guarantee that when you are using your arrow keys that the
previous / next values are the correct ones instead of the "old" values
before the order change happened.
Fixes: #2131
* update changelog
* do not add `disabled` prop to `MenuItem`
We use the `aria-disabled` instead so that you can still style it and
that assistive techonology can read the disabled state. If it has the
`disabled` prop itself, then often you can't interact with it at all.
We also default to `disabled = false`, which means that the default
behaviour was a `<element disabled="false">` in the DOM. If you then
have CSS like `[disabled] { opacity: 0.8; }` then this also applies to
the elements with `disabled="false"`.
Fixes: #2134
* ensure Vue playground still works
* ensure Vue overrides the `onXXX` correctly
* update changelog
* Allow clicks inside dialog panel when target is inside shadow root
* Introduce resettable “server” state
This will aid in testing
* Add SSR and hydration tests for react
* Fix server rendering of Tabs on React 17
* Fix CS
* Skip hydration tests
* Tweak SSR implementation in Vue
* Update changelog
`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-haspopupFixes: #2099
* improve types for addEventListener inside disposables
* improve scroll locking
Instead of using the "simple" hack with the `position: fixed;` we now
went back to the `touchmove` implementation.
The `position: fixed;` causes some annoying issues. For starters, on iOS
you will now get a strange gap (due to safe areas). Some applications
also saw "blank" screens based on how the page was implemented.
We also saw some issues internally, where clicking changing the scroll
position on the main page from within the Dialog.
Think about something along the lines of:
```html
<a href="#interesting-link-on-the-current-page">Interesting link on the page</a>
```
This doesn't work becauase the page is now fixed, and there is nothing
to scroll...
Instead, we now use the `touchmove` again. The problem with this last
time was that this disabled _all_ touch move events. This is obviously
not good.
Luckily, we already have a concept of "safe containers". This is what we
use for the `outside click` behaviour as well. Basically in a Dialog,
your `Dialog.Panel` is the safe container. But also third party DOM
elements that are rendered inside that Panel (or as a sibling of the
Dialog, but not your main app).
We can re-use this knowledge of "safe containers", and only cancel the
`touchmove` behaviour if this didn't happen in any of the safe
containers.
* update changelog
* sort DOM nodes using tabIndex first
It will still keep the same DOM order if tabIndex matches, thanks to
stable sorts!
* refactor `focusIn` API
All the arguments resulted in usage like `focusIn(container,
Focus.First, true, null)`, and to make things worse, we need to add
something else to this list in the future.
Instead, let's keep the `container` and the type of `Focus` as known
params, all the other things can sit in an options object.
* fix FocusTrap escape due to strange tabindex values
This code will now ensure that we can't escape the FocusTrap if you use
`<tab>` and you happen to tab to an element outside of the FocusTrap
because the next item in line happens to be outside of the FocusTrap and
we never hit any of the focus guard elements.
How it works is as follows:
1. The `onBlur` is implemented on the `FocusTrap` itself, this will give
us some information in the event itself.
- `e.target` is the element that is being blurred (think of it as `from`)
- `e.currentTarget` is the element with the event listener (the dialog)
- `e.relatedTarget` is the element we are going to (think of it as `to`)
2. If the blur happened due to a `<tab>` or `<shift>+<tab>`, then we
will move focus back inside the FocusTrap, and go from the `e.target`
to the next or previous value.
3. If the blur happened programmatically (so no tab keys are involved,
aka no direction is known), then the focus is restored to the
`e.target` value.
Fixes: #1656
* 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
* 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>
* 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
* 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
* 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
* Pass default slot instead of raw children to components
This is essentially how the internal implementation of `<component>` works. This works even for element VNodes.
* Update changelog
* expose `close` function for `Menu` and `Menu.Item` components
The `Menu` will already automatically close if you invoke the
`Menu.Item` (which is typically an `a` or a `button`). However you have
control over this, so if you add an explicit `onClick={e =>
e.preventDefault()}` then we respect that and don't execute the default
behavior, ergo closing the menu.
The problem occurs when you are using another component like the Inertia
`Link` component, that does have this `e.preventDefault()` built-in to
guarantee SPA-like page transitions without refreshing the browser.
Because of this, the menu will never close (unless you go to a totally
different page where the menu is not present of course).
This is where the explicit `close` function comes in, now you can use
that function to "force" close a menu, if your 3rd party tool already
bypassed the default behaviour.
This API is also how we do it in the `Popover` component for scenario's
where you can't rely on the default behaviour.
* update changelog
* rework Tabs so that they don't change on focus
The "change on focus" was an incorrect implementation detail that made
it a bit easier but this causes a problem as seen in #1858.
If you want to conditionally check if you want to change the tab or note
(e.g. by using `window.confirm`) then the focus is lost while the popup
is shown. Regardless of your choice, the browser will re-focus the Tab
therefore asking you *again* what you want to do.
This fixes that by only activating the tab if needed while using arrow
keys or when you click the tab (not when it is focused).
* update changelog