@microsoft/focusgroup-polyfill
v1.2.1
Published
A polyfill for the focusgroup HTML attribute.
Downloads
601
Readme
Focusgroup Polyfill
Applications can adopt focusgroup before it is widely available in the Web Platform.
The polyfill follows these principles:
- Performance: Small in terms of bytes and fast in terms of runtime execution.
- Correctness: Where performance is in conflict with correctness and spec-compliance, we will favor a correct, spec-compliant implementation.
- Updates: Ship updates as the spec evolves so the latest version of the polyfill implements the latest version of the spec.
- Migration: Make it easy to migrate to new updates if/when the spec evolves.
For more information about focusgroup itself, see: https://open-ui.org/components/scoped-focusgroup.explainer/
Usage
# npm
npm install @microsoft/focusgroup-polyfill
# yarn
yarn add @microsoft/focusgroup-polyfillimport { polyfill } from "@microsoft/focusgroup-polyfill";
// Polyfill the entire document
polyfill();
// Polyfill a subtree of the document
const myElement = document.querySelector(".my-element");
polyfill(myElement);If your project doesn’t need Shadow DOM support, you can use the “shadowless” bundle, which has smaller file size:
import { polyfill } from "@microsoft/focusgroup-polyfill/shadowless";
// Polyfill the entire document
polyfill();
// Polyfill a subtree of the document
const myElement = document.querySelector(".my-element");
polyfill(myElement);Alternatively, you can polyfill the entire document.body and observe any DOM changes, if new focusgroup elements are added to the light DOM, they will be automatically polyfilled:
import { polyfillBodyAndObserve } from "@microsoft/focusgroup-polyfill";
polyfillBodyAndObserve();The same function also exists in the “shadowless” bundle.
Limitations
While one of the polyfill’s principles is “correctness”, it’s still bound to the web platform’s limitations, especially when considering performance.
- The
focusgroupspec is still evolving, the polyfill may not reflect the latest spec changes and the native implementations, check the CHANGELOG.md for details - Doesn’t support
focusgroupproperty- If the polyfill adds the
focusgroupproperty toHTMLElement.prototype, it will break feature detection whenpolyfill()is called again. But if it only adds thefocusgroupproperty to individual HTML elements, it can’t change thefocusgroupproperty’s value on an arbitrary element
- If the polyfill adds the
- Polyfill explicitly manipulates
tabindexattributes- It does save the attribute’s value that the author defined, so that it can restore the attribute to its original value when needed
- Polyfill doesn’t work with CSS’s
reading-flow - Role inferring
- Polyfill only considers
div,span, and custom elements to have agenericrole, hence only infers roles on these elements, plusbuttonfor item role inferring - Polyfill will not be able to avoid role inference on items that have non-generic roles defined via their
ElementInternals
- Polyfill only considers
- When a focusgroup item has arrow/home/end key handlers, polyfill is inconsistent with the native implementation because it can’t access author event listeners:
- Custom element: unless the custom element calls
event.preventDefault(), adding arrow/home/end key handler will not stop the polyfill from handling directional navigation - Built-in elements that have key conflict (e.g.
<input>): cancelling default arrow/home/end key behavior won’t enable the polyfill to add directional navigation
- Custom element: unless the custom element calls
- If an item’s keyboard focusability is changed by a method that doesn’t reflect on any attribute changes, polyfill will not be able to exclude the item from continuing participating the directional navigation
- For example, a custom element (as a focusgroup item) gets disabled through a property change or other mechanism without reflecting its disabled state in a
disabledattribute
- For example, a custom element (as a focusgroup item) gets disabled through a property change or other mechanism without reflecting its disabled state in a
- After initial polyfill, visibility changes on elements that matter to the focusgroup’s behaviors will not automatically update the group’s behavior:
- For example, if a menu has a submenu that is initially hidden, because the submenu is hidden, it will not segment the top-level menu items. When the submenu becomes visible (e.g. hitting
ArrowRightkey on its parent menu item), it still will not segment the top-level menu items, as a result, hittingTabkey will not move the focus to the next top-level item, but out of the menu instead - This is because the polyfill only observes changes to the child element lists and their attributes, we may add support for visibility changes in the future
- For example, if a menu has a submenu that is initially hidden, because the submenu is hidden, it will not segment the top-level menu items. When the submenu becomes visible (e.g. hitting
Common patterns
Using with custom elements that have nested custom elements
If you have a custom element that is a focusgroup, and its items are also custom elements, you may run into issues that the children aren’t ready when focusgroup polyfill kicks in. While the polyfill does observe mutations, there might be racing conditions between the polyfill and child elements, we’d recommend to call the polyfill() function inside a queueMicrotask() callback function:
import { polyfill } from "@microsoft/focusgroup-polyfill";
class MyTablist extends HTMLElement {
connectedCallback() {
// Other tasks for initiating the tablist component
queueMicrotask(() => {
polyfill(this);
});
}
}Toggling visibilities of focusgroup items
Currently the polyfill doesn’t support observing visibility changes on items or nested groups. As a workaround, you should add tabindex="0" to all items, and if an item is nested inside a hidden container, or itself is hidden, also add focusgroup="none" to opt out of the directional navigation. When the item becomes visible, remove the focusgroup="none" attribute.
You may want to make the focusgroup="none" attribute changes after the visibility changes and inside a requestAnimationFrame callback function.
Selecting focusgroup items
In some UI design patterns, e.g. tab list and tabs, you’ll not only need to handle directional navigation, but also changing an item’s selection state, e.g. activating a tab and deactivating another. Since focusgroup is designed to only handle directional navigation, as a developer, you’ll still have to handle selection by yourself.
Take the tab list and tab patterns as an example, to manage selection, here are some recommendations:
- Set tab list’s
focusgroupwith no memory - Update selection in a
focusinevent handler - Add
focusgroupstartattribute to the selected tab (and remove from the deselected one)
<my-tablist focusgroup="tablist nomemory">
<my-tab>tab 1</my-tab>
<my-tab>tab 2</my-tab>
<my-tab>tab 3</my-tab>
<my-tab>tab 4</my-tab>
</my-tablist>class MyTablist extends HTMLElement {
// ...
// This method listens to the `focusin` event on the tab list element itself.
handleFocusInEvent(event) {
const { target } = event;
// Assuming no nested focusable element inside each tab element.
if (!isTab(target) || target.disabled) {
return;
}
const currentSelected = this.querySelector("my-tab[aria-selected='true']");
if (currentSelected) {
currentSelected.removeAttribute("aria-selected");
currentSelected.removeAttribute("focusgroupstart");
// Hide the associated tab panel.
}
target.setAttribute("aria-selected", "true");
target.setAttribute("focusgroupstart", "");
// Show the associated tab panel.
}
}If the focusgroup’s reentry point doesn’t need to align with the element that has the selected state within a group, you can skip the focusgroupstart attribute manipulation and feel free to remove nomemory as you see fit, but it’s still recommended to use focusin event on the focusgroup owner element to update selection states.
Testing
See TESTING.md for details on how to test this project.
Contributing
See CONTRIBUTING.md.
Support
See SUPPORT.md.
