* fix VoiceOver bug for Listbox in Chrome
Chrome currently has a bug if you use a `Listbox` with a `Label` and use
the `aria-multiselectable` attribute. This combination will cause
VoiceOver to _not_ announce the `role="option"` elements when
interacting with them.
If we drop the `aria-multiselectable` OR the `aria-labelledby` it starts
working. Alternatively replacing `aria-labelledby` with `aria-label`
won't work either.
I filed a Chrome bug report about this here: https://bugs.chromium.org/p/chromium/issues/detail?id=1498261
---
Luckily there is a workaround in our `Listbox` implementation. Right now
we always require the `Listbox.Button` to be there. The
`Listbox.Options` component doesn't work on its own in our
implementation.
This means that whenever we open the `Listbox` that we have to go via
the `Listbox.Button`. This `Listbox.Button` is already labelled by the
`Listbox.Label` if there is one.
This also means that we can safely drop the `id` of the label inside the
`aria-labelledby` from the `Listbox.Options`.
This wouldn't have worked if our `Listbox.Options` could be used in a
standalone way without the `Listbox.Button`.
At the end of the day the hierarchy looks like this:
- Options is labelled by the Button
- Button is labelled by the Label
- Label
Fixes: #2817
* update changelog
* 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
* 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>
* 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>
* Merge vnode refs when rendering
In some cases if we used our own ref (we do this in `<TransitionRoot>` for instance) and rendered slot children we would wipe out user-specified refs. So we set a flag when calling `cloneVNode` to merge our refs and any user-specified refs.
* Update changelog
* Tweak `dom()` helper
The helper can currently return a component instance when it should only ever return a DOM element. So, we fix the implementation to return null if it’s not an `Element` _and_ adjust the types such that if a `ComponentPublicInstance` is passed we change the return type to `Element`.
* Specialize DOM helper to HTML elements
Technically it could be an SVG element but much of Headless UI assumes HTML elements all over. So we’ll adjust the types to assume HTMLElement instead.
* Allow `dom()` helper to return any `Node` type
It doesn’t actually always return an HTMLElement but we have behavior that relies on it returning and checking for `Comment` nodes
* Detect `<button>` nested in components inside `<PopoverButton>`
* Update changelog
* add scripts to help with automating releases
* add prepare-release and release workflows
* bump actions from v2 to v3
* use `github.ref_name` for getting the tag name
* ensure we use `**` for matching tags with slashes in them
* WIP
* Add warning for unsupported roles to `<Dialog>`
* Update assertions
* Add test for React
* Add support for `role=alertdialog` to Vue
---------
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
* explicitly add the `aria-hidden="true"` attribute
The `Hidden` component only adds the `aria-hidden` by default if the
`Focusable` feature is passed. In our case we don't want it to be
focusable so therefore we didn't pass this feature flag.
Because we didn't pass the `Focusable` feature, the `display: hidden`
was used which makes it completely unfocusable to the keyboard of the
user which is what we want.
However, the VoiceOver cursor _can_ get into those elements. Adding the
`aria-hidden` manually to these tabs solves the issue.
* update changelog
* Register portal based on element presence in the DOM
This always coincides with `onMounted` currently but that’s about to change
* Mount element lazily for portals
This prevent’s SSR hydration issues and matches the behavior of React’s `<Portal>` element
* Fix portal tests
* Update comment
* Update changelog
* 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
* ensure `appear` works in combination with SSR
* add appear transition example
* update changelog
* add scale to appear example
* trigger immediate transition once the DOM is ready
* ensure React doesn't change the `className` underneath us
* handle all base classes
We are bypassing React when handling classes in the Transition
component. Let's ensure the base classes from the prop are also added
correctly.
* add missing `base` to tests
* simplify `useTransition` hook
* add react-hot-toast example
* make TS happy
* ensure the `classNames` are unique
* remove classNames if it results in an empty string
This will ensure that we don't end up with `class=""` in the DOM
* ensure `unmount` is defaulting to `true`
* do not read from `prevShow` in render
After fixing the other bugs, this part only caused bugs right now. Even
when re-rendering the Transition component while transitioning. Dropping
this fixes that behaviour.
* extend `appear` demo with appear, show, unmount booleans
+ a `lazily` one to mimic a conditional render on the client instead of
a fresh page refresh.
* disable smooth scrolling when opening/closing Dialogs
For iOS workaround related purposes we have to capture the scroll
position and offset the margin top with that amount and then
`scrollTo(0,0,)` to prevent all kinds of funny UI jumps.
However, if you have `scroll-behavior: smooth` enabled on your `html`,
then offseting the margin-top and later `scrollTo(0,0)` would be
handled in a smooth way, which means that the actual position would be
off.
To solve this, we disable smooth scrolling entirely in order to make the
position of the Dialog correct. This shouldn't be a problem in practice
since the page itself isn't suppose to scroll anyway.
Once the Dialog closes we reset it such that everything else keeps
working as expected in a (smooth) way.
* add `microTask` to disposables
* ensure the fix works in React's double rendering dev mode
* update changelog
* Only use useServerHandoffComplete in React < 18
It’s only useful for the useId hook. It is not compatible with `<Suspense>` because hydration is delayed then.
* Make sure portals first render matches the server by rendering nothing
Since Portals cannot SSR the first render MUST also return `null`. React really needs an `isHydrating` API.
* Lazily resolve root containers
This fixes a problem where clicks were assumed to be outside because of the delayed `<Portal>` render. The second portal render doesn’t cause the dialog to re-render thus the initial ref values were stale.
* Update changelog
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
* only render `<MainTreeNode />` in `Popover.Group` instead of after every `Popover`
* make Vue Popover consistent
* apply same `MainTreeNode` logic to Vue version
* update changelog
* 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
When you pass in an element to the `attemptSubmit` that has a
`type="submit"`, then the `attemptSubmit` will just click this element.
We want to skip the current one and fallback to `form.requestSubmit()`
instead.