962528c216300baffd2c7cda55ab2d97bff241d4
15 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d31bb5c08e |
Fix FocusTrap escape due to strange tabindex values (#2093)
* 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 |
||
|
|
b301f04c77 |
Only restore focus to the Menu.Button if necessary when activating a Menu.Option (#1782)
* 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 |
||
|
|
0e1599b464 |
Close Menu component when using tab key (#1673)
* menu should not trap focus for tab key * introduce `focusFrom` focus management utility This is internal API, and the actual API is not 100% ideal. I started refactoring this in a separate branch but it got out of hand and touches a bit more pieces of the codebase that aren't related to this PR at all. The idea of this function is just so that we can go Next/Previous but from the given element not from the document.activeElement. This is important for this feature. We also bolted this ontop of the existing code which now means that we have this API: ```js focusIn([], Focus.Previouw, true, DOMNode) ``` Luckily it's internal API only! * ensure closing via Tab works as expected Just closing the Menu isn't 100% enough. If we do this, it means that when the Menu is open, we press shift+tab, then we go to the Menu.Button because the Menu.Items were the active element. The other way is also incorrect because it can happen if you have an `<a>` element as one of the Menu.Item elements then that `<a>` will receive focus, then the `Menu` will close unmounting the focused `<a>` and now that element is gone resulting in `document.body` being the active element. To fix this, we will make sure that we consider the `Menu` as 1 coherent component. This means that using `<Tab>` will now go to the next element after the `<Menu.Button>` once the Menu is closed. Shift+Tab will go to the element before the `<Menu.Button>` even though you are currently focused on the `Menu.Items` so depending on the timing you go to the `Menu.Button` or not. Considering the Menu as a single component it makes more sense use the elements before / after the `Menu` * update changelog Co-authored-by: Enoch Riese <enoch.riese@gmail.com> |
||
|
|
e1ee36a6ea |
Simplify Popover Tab logic by using sentinel nodes instead of keydown event interception (#1440)
* improve `Popover` keyboard usage Use `TabSentinel` instead of intercepting the `Tab` keydown events. * use Buttons in Popover example * update changelog |
||
|
|
6d8235e059 |
Mimic browser select on focus when navigating via Tab (#1272)
* mimic browser select on focus When calling focusIn if the next node is selectable select all the text. * refactor browser `select` behaviour for React and Vue * update changelog Co-authored-by: Robin Malfait <malfait.robin@gmail.com> |
||
|
|
c219d87a69 |
Use ownerDocument instead of document (#1158)
* use `ownerDocument` instead of `document` This should ensure that in iframes and new windows the correct document is being used. * update changelog |
||
|
|
a63ca93aae |
Guarantee DOM sort order when performing actions (#1168)
* ensure proper sort order We already fixed a bug in the past where the order of DOM nodes wasn't stored in the correct order when performing operations (e.g.: using your keyboard to go to the next option). We fixed this by ensuring that when we register/unregister an option/item, that we sorted the list properly. This worked fine, until we introduced the Combobox components. This is because items in a Combobox are continuously filtered and because of that moved around. Moving a DOM node to a new position _doesn't_ require a full unmount/remount. This means that the sort gets messed up and the order is wrong when moving around again. To fix this, we will always perform a sort when performing actions. This could have performance drawbacks, but the alternative is to re-sort when the component gets updated. The bad part is that you can update a component via many ways (like changes on the parent), in those scenario's you probably don't care to properly re-order the internal list. Instead we do it while performing an action (`goToOption` / `goToItem`). To make things a bit more efficient, instead of querying the DOM all the time using `document.querySelectorAll`, we will keep track of the underlying DOM node instead. This does increase memory usage a bit but I think that this is a fine trade-off. Performance wise this could also be a bottleneck to perform the sorting if you have a lot of data. But this problem already exists today, therefore I consider this a complete new problem instead to solve. Maybe we don't solve it in Headless UI itself, but figure out a way to make it composable with existing virtualization libraries. * update changelog |
||
|
|
fdd2629795 |
Improve overal codebase, use modern tech like esbuild and TypeScript 4! (#1055)
* use esbuild for React instead of tsdx * remove tsdx from Vue * use consistent names * add jest and prettier * update scripts * ignore some folders for prettier * run lint script instead of tsdx lint * run prettier en-masse This has a few changes because of the new prettier version. * bump typescript to latest version * make typescript happy * cleanup playground package.json * make esbuild a dev dependency * make scripts consistent * fix husky hooks * add dedicated watch script * add `yarn playground-react` and `yarn react-playground` (alias) This will make sure to run a watcher for the actual @headlessui/react package, and start a development server in the playground-react package. * ignore formatting in the .next folder * run prettier on playground-react package * setup playground-vue Still not 100% working, but getting there! * add playground aliases in @headlessui/vue and @headlessui/react This allows you to run `yarn react playground` or `yarn vue playground` from the root. * add `clean` script * move examples folder in playground-vue to root * ensure new lines for consistency in scripts * fix typescript issue * fix typescript issues in playgrounds * make sure to run prettier on everything it can * run prettier on all files * improve error output If you minify the code, then it could happen that the errors are a bit obscure. This will hardcode the component name to improve errors. * add the `prettier-plugin-tailwindcss` plugin, party! * update changelog |
||
|
|
a37197694f |
Ensure correct order when conditionally rendering Menu.Item, Listbox.Option and RadioGroup.Option (#1045)
* ensure correct order in `Menu.Item` * Update Vue version of menu component ordering issue * ensure correct order of `Listbox.Option`s * add test to verify that RadioGroup.Option order is correct * ensure correct order of `ListboxOption`s * cleanup * add test to verify that `RadioGroupOption` order is correct * update changelog * use similar a,z signature compared to other places Co-authored-by: Jordan Pittman <jordan@cryptica.me> |
||
|
|
3bc7545169 |
Next release (#916)
* placeholder for next release * Ensure portal root exists in the DOM (#950) * ensure that the portal root is always in the DOM When using NextJS, it happens that between page transitions the portal root gets removed form the DOM. We will check the DOM when the `target` updates, and if it doesn't exist anymore, then we will re-insert it in the DOM. * update changelog * Allow `Tabs` to be controllable (#970) * feat(react): Allow Tab Component to be controlled * fix falsy bug `selectedIndex || defaultIndex` would result in the `defaultIndex` if `selectedIndex` is set to 0. This means that if you have this code: ```js <Tab.Group selectedIndex={0} defaultIndex={2} /> ``` That you will never be able to see the very first tab, unless you provided a negative value like `-1`. `selectedIndex ?? defaultIndex` fixes this, since it purely checkes for `undefined` and `null`. * implemented controllable Tabs for Vue * add dedicated test to ensure changing the defaultIndex has no effect * update changelog Co-authored-by: ChiefORZ <seb.schaffernak@gmail.com> * Fix missing key binding in examples (#1036) Co-authored-by: superDragon <xkloveme@gmail.com> * Fix slice => splice typo in Vue Tabs component (#1037) Co-authored-by: Ryan Gossiaux <ryan.gossiaux@gmail.com> * update changelog * Ensure correct DOM node order when performing focus actions (#1038) * ensure that the order of DOM nodes is correct When we are performing actions like `focusIn(list, Focus.First)` then we have to ensrue that we are working with the correct list that is properly sorted. It can happen that the list of DOM nodes is out of sync. This can happen if you have 3 Tabs, hide the second (which triggers an unmount and an `unregister` of the Tab), then re-add the second item in the middle. This will re-add the item to the end of the list instead of in the middle. We can solve this by always sorting items when we are adding / removing items, but this is a bit more error prone because it is easy to forget. Instead we will sort it when performing the actual keyboard action. If we didn't provide a list but an element, then we use a getFocusableElements(element) function, but this already gives you a correctly sorted list so we don't need to do that for this list. * add tests to prove the correct order when performing actions * cleanup code just for tests It could still happen that this internal list is not ordered correctly but that's not really a problem we just have the list to keep track of things. For our tests we now use the position from the DOM directly. * update changelog Co-authored-by: ChiefORZ <seb.schaffernak@gmail.com> Co-authored-by: superDragon <xkloveme@gmail.com> Co-authored-by: Ryan Gossiaux <ryan.gossiaux@gmail.com> |
||
|
|
084a2497d8 |
Fix incorrect nested Dialogs behaviour (#489)
* 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 |
||
|
|
ce23edeee4 |
Next release (#431)
* 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> |
||
|
|
6f8225c437 |
Next batch of Vue components (#282)
* 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) |
||
|
|
4e5190d65d |
Fix refocus button bug (#256)
* fix outside click on span inside button works as expected We have `outside click` behaviour implemented. Whenever the target element is focusable we make sure that the newly clicked/focused element stays focused. If it is not a focusable element we will make sure that the Menu/Listbox button is re-focused so that screenreader users don't get confused. This is all fine, but it turns out that when you have a button with a span, and you click on the span, then the event.target will be that span. The span itself is not focusable of course, but the button will get the focus. This results in the Menu/Listbox button being re-focused which is incorrect. For this we will introduce a FocusableMode on the `isFocusableElement`, we will have a `Strict` mode, which means the actual element should be focusable. And a `Loose` mode, which means that the actual element can be inside a focusable element. E.g.: A span within a button. * rename menu to listbox Copy paste can be fun sometimes * update changelog |
||
|
|
648a2843e6 |
Multiple new components (#220)
* 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 |