* 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
* prevent infinite loop
When you use `as={Fragment}` an unmount and remount can happen. This
means that the `ref` gets called with `null` for the unmount and
`HTMLButtonElement` for the mount.
This keeps toggling which results in an infinite loop and eventually a
Maximum callback size exceeded issue.
This ensures that we only set the button if we have a button.
* 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
* 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>
* remove `forceRerender` code
This was necessary to ensure the `Panel` and the `Tab` were properly
connected with eachother because it could happen that the `Tab` renders
but the corresponding `Panel` is not the active one which means that it
didn't have a DOM node and no `id` attached.
Whenever new `Tab` became active, it rerendered but the `Panel` wasn't
available yet, after that the `Panel` rendered and an `id` was available
but the actual `Tab` was already rendered so there was no link between
them.
We then forced a re-render because now the `Panel` does have a DOM node
ref attached and the `aria-labelledby` could be filled in.
However, in #1837 we fixed an issue where the order of `Tab` elements
with their corresponding `Panel` elements weren't always correct. To fix
this we ensured that the `Panel` always rendered a `<Hidden />`
component which means that a DOM node is always available.
This now means that we can get rid of the `forceRerender`.
* update changelog
* ensure `Combbox.Label` is properly linked when rendered after other components
Even when rendered after the Combobox.Input / Combobox.Button
* update changelog
* 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
* use a simpler `position: fixed` approach to prevent scroll locking
This isn't super ideal, but just preventing the default behavior on the
entire document while `touchmove`-ing isn't ideal either because then
you can't scroll inside the dialog or on the backdrop if your dialog
panel is larger than the viewport.
Again, this is not 100% correct, but it is better because you will be
able to scroll the dialog, and not the body.
* update changelog
* fix ref stealing
When a higher-level component (like `Transition`) provides a `ref` to
its child component, then it will override the `ref` that was
potentially already on the child.
This will make sure that these are merged together correctly.
Fixes: #985
* update changelog
* 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
* improve tracking of transitionableChildren
* remove weird outlier snapshots
If anything is still wrong the tests will still fail but the diffs will
be easier to read.
* remove event handling from `useTransition`
* handle before/after events in `Transition` directly
* fix incorrect logic bug in tests
* add very explicit test for transition event order
* ignore flakey tests for now
We will get back to these!
* ensure cancellation of transitions works properly
* update changelog
* only restore focus to the Menu Button if necessary
This will check whether the focus got moved to somewhere else or not
once we activate an item via click or pressing `enter`.
Pressing escape will still move focus to the Menu Button.
* update changelog
* 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
* 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>
* 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
* 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
* Don’t overwrite `element.focus` on popover panels
* Update changelog
* Add test
This test isn’t exactly right for JSDOM but it does mirror what we would do in the browser to reproduce the problem
* 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
* 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
* improve event handler merging
This will ensure that an actual event is passed before checking the
`event.defaultPrevented`.
For React, we also have to make sure that we are not dealing with a
SyntehticEvent.
Thanks @Mookiepiece!
Co-authored-by: =?UTF-8?q?=E5=BD=BC=E8=A1=93=E5=90=91?= <48076971+Mookiepiece@users.noreply.github.com>
* update changelog
Co-authored-by: =?UTF-8?q?=E5=BD=BC=E8=A1=93=E5=90=91?= <48076971+Mookiepiece@users.noreply.github.com>
* ensure outside click works on Safari in iOS
When tapping on an element that is not clickable (like a div), then the
`click` and `mousedown` events will not reach the
`window.addEventListener('click')` listeners.
The only event that does that could be interesting for us is the
`pointerdown` event. The issue with this one is that we then run into
the big issue we ran in a few months ago where clicks on a scrollbar
*also* fired while a click doesn't.
This issue was not an issue in React land, the
`window.addEventListener('click')` was fired even when tapping on a
`div`. This was very very confusing, but we think this is because of the
syntethic event system, where the event listener is added to the root of
your application (E.g.: #app) and React manually bubbles the events.
Because this is done manually, it *does* reach the window as well.
The confusing part is, how does React convert a `pointerdown` event to a
`mousedown` and `click`. There is no code for that in their codebase?
Turns out they don't, and turns out the events **do** bubble, but up
until the `document`, not the `window`. But since they are manually
bubbling events it all makes sense.
So the solution? Let's switch from `window` to `document`...
* update Dialog example to use DialogPanel
* update changelog