* update dialog playground example
Includes a generic `Button` component that has explicit focus styles.
* keep track of "focus" history
Safari doesn't "focus" buttons when you mousedown on them. This means
that we don't capture the correct element to restore focus to when
closing a `Dialog` for example.
Now, we will make sure to keep track of a list of last "focused" items.
We do this by also capturing elements when you "click", "mousedown" or
"focus".
* let's use a button instead of a div in tests
* make `target` for Vue consistent with React
* update changelog
* add a bunch of tests to ensure we won't regress on this again
* fix incorrect warning when using multiple `Popover.Button` inside `Popover.Panel`
* update changelog
* 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
* add (failing) test to verify moving focus to 3rd party containers work
* pass `resolveRootContainers` to `FocusTrap`
* handle lazy containers in `FocusTrap`
* update changelog
* add native label behavior for switch
* Add reference tests for React and Vue
These don’t work in JSDOM so they’re skipped but we can use these to reference expected behavior once we have playwright-based tests
* Fix Vue playground switch example
* Only prevent default when the element is a label
* Port change to Vue
* Update changelog
---------
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
* 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
Let's wrap the test in `act` to get rid of the warning. In practice
(while testing in the browser) the actual warning doesn't seem to affect
the user experience at all.
The `act` function is typed in a strange way (`Promise<undefined> &
void`). Yet the actual contents of the `act` callback is returned as
expected. Therefore we overrode the type of `act` to make sure this
reflects reality better. (Thanks @thecrypticace!)
Also added an additional check to make sure the actual `container` is
available to extra ensure we are not lying by overriding the type.
* 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.
Test in line 105 already covers using the as tag to change the underlying dom tag to an anchor tag. Therefore we can test whether a span tag is correctly rendered for example.
* 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
* 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
* 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
* 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
* Work on SSR tests for react
* Use React internals to count tabs and panels
React’s double rendering in strict mode in development makes SSR + hydration matching impossible without reaching into internals. This is unfortunate but the way react works. Production builds of React are unaffected by this but still require a consistent mechanism that works so in that case we use Symbols just like we do in SSR.
* Update changelog
* 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>
* fix `failed to removeChild on Node` bug
Let's introduce a bit more defensive code to make sure that the code
doesn't crash when we don't pass a `Node` to `removeChild`
* update changelog
* 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
* 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
* remove `transitioncancel` logic
On Desktop Sarai, Chrome, and on mobile iOS Safari the
`transitioncancel` is never called on outside click of the Dialog.
However, on mobile Android Chrome it _is_ called, and the
`transitionend` is never triggered for _some_ reason.
According to the MDN docs:
> If the transitioncancel event is fired, the transitionend event will not fire.
>
> — https://developer.mozilla.org/en-US/docs/Web/API/Element/transitioncancel_event
When testing this, I never got into the `transitionend` when I got into
the `transitioncancel` first. But, once I removed the `transitioncancel`
logic, the `transitionend` code _was_ being called.
The code is now both simpler, and works again. The nice part is that we
never did anything with the `cancel` event. We marked it as done using
the `Reason.Cancelled` and that's about it.
* cleanup transition completion `Reason`
* update changelog