* add `DefaultMap` implementation
* add `useHierarchy` hook
* start `FocusTrapFeatures.None` with `0` instead of `1`
* simplify `Dialog`'s implementation
By making use of the new `useHierarchy` hook.
* delete `StackContext` and `StackProvider` components
They are now replaced by the new `useHierarchy` hook.
* use `useHierarchy` in `useOutsideClick` hook
This way we can scope the hierarchy inside of the `useOutsideClick`
hook. This now ensures that we only enable the `useOutsideClick` on the
top-most element (the one that last enabled it).
* use `useHierarchy` in `useInertOthers` hook
* add new `useEscape` hook
* use new `useEscape` hook
* use `useHierarchy` in `useEscape` hook
* use `useHierarchy` in `useScrollLock` hook
* pass features instead of `enabled` boolean
* simplify demo mode feature flags
No need to setup focus feature flags and then disable it all again if
demo mode is enabled.
* use similar signature for hooks with `enabled` parameter
Whenever a hook requires an `enabled` state, the `enabled` parameter is
moved to the front. Initially this was the last argument and enabled by
default but everywhere we use these hooks we have to pass a dedicated
boolean anyway.
This makes sure these hooks follow a similar pattern. Bonus points
because Prettier can now improve formatting the usage of these hooks.
The reason why is because there is no additional argument after the
potential last callback.
Before:
```ts
let enabled = data.__demoMode ? false : modal && data.comboboxState === ComboboxState.Open
useInertOthers(
{
allowed: useEvent(() => [
data.inputRef.current,
data.buttonRef.current,
data.optionsRef.current,
]),
},
enabled
)
```
After:
```ts
let enabled = data.__demoMode ? false : modal && data.comboboxState === ComboboxState.Open
useInertOthers(enabled, {
allowed: useEvent(() => [
data.inputRef.current,
data.buttonRef.current,
data.optionsRef.current,
]),
})
```
* move `focusTrapFeatures` parameter to the front
* drop `FocusTrapFeatures.All`, list them explicitly
The `All` feature didn't include all feature flags (didn't include
`FocusTrapFeatures.AutoFocus` for example).
* always enable `FocusTrapFeatures.RestoreFocus` when enabled
This way we can get rid of the `Position.HasChild` check, which will
allow us to do more cleanup soon in the `useHierarchy` hook.
* use `useHierarchy` in `<FocusTrap>` component
* drop `useHierarchy` from `<Dialog>` component
* simplify focusTrapFeatures setup
* simplify `useHierarchy`
The `useHierarchy` hook allowed us to determine whether we are in the
root, in the middle, a leaf, have a parent, have a child, ... but we
only ever checked whether we are a leaf node or not.
In other words, you can think of it like "are we the top layer"
This simplifies the implementation and usage of this new hook.
* move `enabled`-like argument to front
Just to be consistent with the other hooks.
* polyfill `toSpliced` for older Node versions
* add sibling dialogs playground
* rename `useHierarchy` to `useIsTopLayer`
* inline variable
* remove `unstable_batchedUpdates`
This is not necessary.
* add tiny bit of information to dialog
Because it might not be super obvious that we are going to close the
`<Dialog />`.
* update changelog
* re-add internal `PortalGroup`
This is necessary to make sure that a component like a `<MenuItems
anchor />` is rendered inside of the `<Dialog />` and not as a sibling.
While this all works from a functional perspective, if you rely on a
CSS variable that was defined on the `<Dialog />` and you use it in the
`<MenuItems />` then without this change it wouldn't work.
* Expose new components under dot notation
Most of them already where so only a couple were missing.
* Deprecate dot notation
* Add deprecation to `RadioGroupOption`
* Update deprecations
* Update changelog
* Update changelog
We keep specific types for elements with special meaning, such as
`HTMLButtonElement`, `HTMLLabelElement` or `HTMLInputElement` because
they receive certain attributes that generic DOM nodes (such as
HTMLDivElement) don't
For the components where we use simple `div` elements (and where people
use `as={...}`) that renders a different element, it doesn't make sense
to use `HTMLDivElement`. Using a more generic `HTMLElement` is simpler
and more correct (we still had `HTMLUListElement` and `HTMLLIElement`
for "div" DOM nodes which is incorrect).
This shouldn't be a breaking change because an `HTMLDivElement` is still
a valid `HTMLElement`. The other way around wouldn't be the case.
* 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
* 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>
* 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>
* fix(tabs): wrong tab focus when Tab contains a Dialog
* refactor(focus-trap): rename variable and move logic
* test(tabs): improve test by asserting the active element
* ensure `FocusTrap` is not active when `enabled = false`
* fix: move the enabled check to unmounting
* refactor to `useOnUnmount` hook
This will allow us to make the code relatively similar between React and
Vue.
* update changelog
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
* feat: addEventListener on document loaded
* Refactor
* Fix import
* Update changelog
* use function instead of arrow function
* make callback in `onDocumentReady` mandatory
---------
Co-authored-by: lkr <lkr@bytedance.com>
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
* 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 (failing) test to verify moving focus to 3rd party containers work
* pass `resolveRootContainers` to `FocusTrap`
* handle lazy containers in `FocusTrap`
* 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
* 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
* 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
* add `?raw` option to playground
This will render the component as-is without the wrapper.
* delay initial focus and make consistent between React and Vue
This will delay the initial focus and makes it consistent between React
and Vue.
Some explanation from within the code why this is happening:
Delaying the focus to the next microtask ensures that a few
conditions are true:
- The container is rendered
- Transitions could be started
If we don't do this, then focusing an element will immediately cancel
any transitions. This is not ideal because transitions will look
broken. There is an additional issue with doing this immediately. The
FocusTrap is used inside a Dialog, the Dialog is rendered inside of a
Portal and the Portal is rendered at the end of the `document.body`.
This means that the moment we call focus, the browser immediately
tries to focus the element, which will still be at the bodem
resulting in the page to scroll down. Delaying this will prevent the
page to scroll down entirely.
* update test to reflect initial focus delay
Now that we are triggering the initial focus inside a `queueMicroTask`
we have to make sure that our tests wait a frame so that the micro task
could run, otherwise we will have incorrect results.
Also make the implementation similar in React and Vue
* update changelog
* 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
* 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
* 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
* 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
* forward ref to all components
* fix playground pages
This isn't a perfect fix of course. But the TypeScript changes required
to do it properly are a bit bigger and require more work.
Having this ready is a good step forward.
* update changelog
* add tests to verify the nested Dialog behaviour
* set mounted to true once rendered once
* cache useWindowEvent listener
We only care about the very last version of the listener function. This
allows us to only change the event listener if the event name (string)
and options (boolean | object) change.
* add/delete messages when mounting/unmounting
We don't require a dedicated hook anymore, so this is a bit of cleanup!
* add comments to the FocusResult enum
* splitup functionality and make it a bit more clear using feature flags
* add getDialogOverlays helper
* simplify the Portal component
We don't need to add the current element to the Stack. We only want to
take care of that in the Dialog component itself.
* drop dom-containers
Currently it is only used in a single spot, so I inlined it into that
file.
* simplify the FocusTrap component, use new API
* improve Dialog component
* update CHANGELOG
* delay initialization of Dialog
We were using a useLayoutEffect, now let's use a useEffect instead. It
still moves focus to the correct element, but that process is now a bit
delayed. This means that users will less-likely be urged to "hack"
around the issue by using fake focusable elements which will result in
worse accessibility.
* add hook to deal with server handoff
This will allow us to delay certain features. For example we can delay
the focus trapping until it is fully hydrated. We can also delay
rendering the Portal to ensure hydration works correctly.
* use server handoff complete hook
* update changelog
* Fixed typos (#350)
* chore: Fix typo in render.ts (#347)
* Better vue link (#353)
* Better vue link
* add better React link
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
* Enable NoScroll feature for the initial useFocusTrap hook (#356)
* enable NoScroll feature for the initial useFocusTrap hook
Once you are using Tab and Shift+Tab it does the scrolling.
Fixes: #345
* update changelog
* Revert "Enable NoScroll feature for the initial useFocusTrap hook (#356)"
This reverts commit 19590b07624d7e3d751cbf11de869dfb0ea432ba.
Solution is not 100% correct, so will revert for now!
* Improve search (#385)
* make search case insensitive for the listbox
* make search case insensitive for the menu
* update changelog
* add `disabled` prop to RadioGroup and RadioGroup Option (#401)
* add `disabled` prop to RadioGroup and RadioGroup Option
Also did some general cleanup which in turn fixed an issue where the
RadioGroup is unreachable when a value is used that doesn't exist in the
list of options.
Fixes: #378
* update changelog
* Fix type of `RadioGroupOption` (#400)
Match RadioGroupOption value types to match modelValue allowed types for RadioGroup
* update changelog
* fix typo's
* chore(CI): update main workflow (#395)
* chore(CI): update main workflow
* Update main.yml
* fix dialog event propagation (#422)
* re-export the `screen` utility for quick debugging purposes
* stop event propagation when clicking inside a Dialog
Fixes: #414
* improve dialog escape (#430)
* Make sure that `Escape` only closes the top most Dialog
* update changelog
* add defaultOpen prop to Disclosure component (#447)
* add defaultOpen prop to Disclosure component
* update changelog
Co-authored-by: Shuvro Roy <shuvro.roy@northsouth.edu>
Co-authored-by: Alex Nault <nault.alex@gmail.com>
Co-authored-by: Eugene Kopich <github@web2033.com>
Co-authored-by: Nathan Shoemark <n.shoemark@gmail.com>
Co-authored-by: Michaël De Boey <info@michaeldeboey.be>
* increase maximum error offset for CI tests
We try to detect how long durations took. However there is no nice way
to time this in JSDOM. Instead we take snapshots every
requestAnimationFrame and when things change we also write down the
time.
This solution is not ideal and results in false positives (especially on
CI environments).
However, it is good enough to ensure that the duration is not 0 and not
500.
* cleanup README's and link to docs site
* remove readme's in favor of doc site
This will be easier, so that we don't have to maintain multiple repo's.
* add little editor hack
By adding a html`..` to the template strings editors can get syntax
highlighting for these template strings. Even better, prettier can even
format the contents inside those because now it is "aware" of what kind
of content is inside of these template strings.
You might notice that for the Menu component I have a jsx`..`, this is
another little hack, this only provides us with syntax highlighting and
not with prettier support.
The reason why we have this is that for some reason, when you have:
html`
<MenuItem>
`
It will be formatted as:
html`
<menuitem>
`
There might exist a better name we can use instead of jsx, but for now,
this will do. Having syntax highlighting is already 10x better than what
we had before!
* add Alert component
* update changelog
* update REACT readme for Alert component
* expose Alert component
* add Disclosure component
* expose Disclosure component
* add FocusTrap component
* add FocusTrap example
* expose FocusTrap
* update test utils
We've been making some changes in the React utils, so we have to update
them here as well!
* add Popover component
* expose Popover
* drop unused state
* type Disclosure's API object
* add Portal component
* add Portal example
* expose Portal component
* use correct containElement assertion
* add useInertOthers hook
* add Dialog component
* fix various typo's
* expose Dialog
* add Popover example
* force focus on the Popover button on click
* drop own id when using labels
We are nesting the Label and Description components, if we also add the
id of ourselves we get strange results when using Voice Over.
First you would hear the contents (which includes the labels and
descriptions) then you would hear the labels and descriptions again.
We don't want to hear things twice!
* add Dialog example
* ensure to stop propagation
Otherwise if you nest a Menu inside a Dialog and you press `Escape` the
Dialog will close as well, which is not the expected behaviour.
* improve focus management
When you trigger a Popover using a `click` event, then start using `Tab`
the next `a`-tags do not contain the default focus styles. These only
happen when you trigger it using the keyboard first.
Using a tabindex="0" does make it "focusable" and the default browser
styles will be visible. If we remove the tabindex in a
requestAnimationFrame or a setTimeout then the focus styles will be
removed as well.
This should not cause to many issues (fingers crossed) because the
document.activeElement was already referring to the correct element!
* remove Alert component
There are a lot of unknowns and context dependendant questions to
implement Alerts in a good way. The current Alert component just had a
role set, and it had no JS attached.
We will revisit this, once we start working on Alert Dialogs,
Notification center notifications (dismissable, hide after x time, ...)
* ensure Popover.Overlay auto shows/hides based on Popover state
* enable focus trapping based on `open` prop
Only enable focus trapping in the Dialog when `open` is true, regardless
of the `static` prop.
* handle attrs on Dialog manually
* add low level Description component
* add low level Label component
* add RadioGroup component
* expose RadioGroup
* update README with links to new components
* update changelog with all the changes
* add RadioGroup example
* improve type in test
* cleanup internal Dialog Description
We have a low level Description component abstraction that can be used
instead of the Dialog specifiction Description.
* refactor raw window events to a shared useWindowEvent
* passthrough prop bag via context for abstract Description
The Description component is a generic low level component that is
re-used. This causes an issue that the render prop "bag"/"slot" doesn't
contain the data from let's say a Dialog component.
This commit will ensure that you can specify a bag (React) and slot
(Vue) on the DescriptionProvider, so that the Description component can
read it from the context.
* improve render function in React
These contain a few changes that are purely internal changes. Nothing
changes / breaks in the public API of the components.
- Instead of using multiple arguments in your `render()` functions, we now
use an object.
- `propsBag` / `bag` is renamed to `slot`.
- We also provide a `name` to the render function, so that we can use
that to improve error messages.
* use the new internal render api (React)
* improve render function in Vue
* use the new internal render api (Vue)
* add Alert component
* expose Alert
* rename forgotten FLYOUT to POPOVER
* use PopoverRenderPropArg
* organize imports in a consistent way
* ensure Portals behave as expected
Portals can be nested from a React perspective, however in the DOM they
are rendered as siblings, this is mostly fine.
However, when they are rendered inside a Dialog, the Dialog itself is
marked with `role="modal"` which makes all the other content inert. This
means that rendering Menu.Items in a Portal or an Alert in a portal
makes it non-interactable. Alerts are not even announced.
To fix this, we ensure that we make the `root` of the Portal the actual
dialog. This allows you to still interact with it, because an open modal
is the "root" for the assistive technology.
But there is a catch, a Dialog in a Dialog *can* render as a sibling,
because you force the focus into the new Dialog. So we also ensured that
Dialogs are always rendered in the portal root, and not inside another
Dialog.
* add dialog with alert example
* add internal Description component
* add internal Label component
* add RadioGroup component
* expose RadioGroup
* add RadioGroup example
* ensure to include tha RadioGroup.Option own id
* update changelog
* split documentation
* apply re-focus bug fix to Popover
* force focus in Menu.Items from within Menu.Items component itself
* force focus in Listbox.Options from within Listbox.Options component itself
* fix undefined values in id's
We were setting the element in state, but updates to the id were not taken into account
* update the caniuse db
* ensure useInertOthers works in multiple places
Previously each hook call would take care of the whole tree. However
when multiple calls to this hook are happening we need to make sure that
you are not removing the aria-hidden when another hook is still used.
This will fix that by keeping track of a list of "interactable" items,
and updating the parents (root of the body) accordingly.
* add the concept of a Stack
When you are rendering a Dialog, we will make sure that this Dialog is
rendered inside a Portal. However, when you are also rendering a Menu,
there is a chance that your Menu doesn't fit within the Dialog,
therefore you will likely render the Menu.Items inside a Portal so that
you can style it as if it is rendered inside but overflows the Dialog
correctly.
This introduces an interesting/annoying problem. Your Menu.Items are now
rendered in a Portal, as a *sibling* to the Dialog. This means that
autoFocus, focusTrap, ... all these features don't work as expected.
Introducing this Stack will allow us to register DOM nodes into a list
of contains that we consider being part of the main container. In other
words, the sibling Menu.Items will now be considered part of the Dialog.
Even though it is rendered *outside* of the Dialog.
This concept also allows for some fun stuff, for example, nesting
Dialog's is no problem with this approach. Dialogs are technically
rendered as siblings in the Portal, but the FocusTrap, and all that just
works as expected.
* capture keyboard events in the capturing phase
This will allow us to use event.stopPropagation() in the code (which
will be required, probably) but still see the keystrokes in the
playground.
* stop propagating keyboard events
This looks a bit silly, and ideally we can solve this in a more elegant
way. However when you nest a Menu inside a Dialog, both of those
components have a `close on escape` functionality built in. However when
your Menu is open, and you press escape, you only want to close the
Menu, not the Dialog. Therefore if we `event.stopPropagation()` it
allows us to stop the `escape` keystroke in the Menu from reaching all
the way to the Dialog itself.
* update Dialog example that showcases nested Dialogs, and nested Menu
* update changelog
* add Disclosure component
* expose the Disclosure component
* add Disclosure example component page
* temporary fix selector because of JSDOM bug
* add useFocusTrap hook
* add FocusTrap component
* expose FocusTrap
* add Dialog component
* add Dialog example component page
* expose Dialog
* random cleanup
* make TypeScript a bit more happy
* add Switch.Description component for React
* add Switch.Description component for Vue
* ensure focus event is triggered on click when element is focusable
* remove Dialog.Button and Dialog.Panel from accessibility assertions
* add Portal component
* expose Portal
* always render Dialog in a Portal
* add useInertOthers hook
This will allow us to mark everything but the current ref as "inert".
This is important for screenreaders, to ensure that screenreaders and
assistive technology can't interact with other content but the current
ref.
This implementation is not ideal yet. It doesn't take into account that
you can use the hook in 2 different components. For now this is fine,
since we only use it in a Dialog and you should also probably only have
a single Dialog open at a time.
Will improve this in the future!
* use the useInertOthers hook
* add scroll lock to the dialog
* ensure we respect autoFocus on form elements within the Dialog
If we have an autoFocus on an input, that input will receive focus. Once
we try to focus the first focusable element in the Dialog this could be
lead to unwanted behaviour. Therefore we check if the focus already is
within the Dialog, if it is, keep it like that.
* only mark aria-modal when Dialog is open
* add initialFocus option to Dialog, FocusTrap & useFocusTrap
* add tests and a few fixes for the initialFocusRef functionality
* forward ref to underlying Dialog component
* close Dialog when it becomes hidden
Could happen when this is in md:hidden for example
* prevent infinite loop
When we `Tab` in a FocusTrap it will try and focus the Next element. If
we are in a state where none of the elements inside the FocusTrap can be
focused, then we keep trying to focus the next one in line. This results
in an infinite loop...
To mitigate this issue, we check if we looped around, if we did, it
means that we tried all the other focusable elements, therefore we can
stop.
* isIntersecting doesn't work in every scenario
When page is scrollable, when dialog is translated of the page. Now just checking for sizes, which should be enough for md:hiden cases
* render Portal contents in a div
Otherwise you can't use multiple Portal components if you render multiple children inside each Portal
* ensure the props bag is typed
* add getByText and assertContainsActiveElement helpers
* add Popover component
* expose Popover
* add Popover example component page
* add quick checks to prevent useless renders
* drop incorrect close function
* update Changelog
* make test error more readable when comparing DOM nodes
* actually call .focus() on the element
This ensures that the document.activeElement becomes the focused element.
* improve useSyncRefs, because ...refs is *always* different
* add dedicated focus management utilities
* refactor useFocusTrap, use focus management utilities
* fix regression while using outside click
There might be a chance that you didn't even notice this *bug*. The idea
is that when you click outside, that the Menu or Listbox closes. However
there is another step that happens:
1. When you click on a focusable item, keep the focus on that item.
2. When you click on a non-focusable item, move focus back to the
Menu.Button or Listbox.Button
We broke part 2, we never returned to the Menu.Button or Listbox.Button.
This is (might) be important for screenreaders so that they don't "get lost",
because if you click on a non-focusable item, the document.body becomes
the active element. Confusing.
* add outside-click to Dialog itself
* update docs