Further improve tabbable performance (#1750)

* improve tabbable performance

* improve tabbable performance

* add PR #

* prettier

* change to getSlottedChildrenOutsideRootElement

* prettier
pull/1766/head
Konnor Rogers 2023-12-01 12:06:16 -05:00 zatwierdzone przez GitHub
rodzic 3e38da210e
commit dd27db5196
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
2 zmienionych plików z 41 dodań i 28 usunięć

Wyświetl plik

@ -14,6 +14,8 @@ New versions of Shoelace are released as-needed and generally occur when a criti
## Next ## Next
- Fixed focus trapping not scrolling elements into view. [#1750]
- Fixed more performance issues with focus trapping performance. [#1750]
- Added the `hover-bridge` feature to `<sl-popup>` to support better tooltip accessibility [#1734] - Added the `hover-bridge` feature to `<sl-popup>` to support better tooltip accessibility [#1734]
- Fixed a bug in `<sl-input>` and `<sl-textarea>` that made it work differently from `<input>` and `<textarea>` when using defaults [#1746] - Fixed a bug in `<sl-input>` and `<sl-textarea>` that made it work differently from `<input>` and `<textarea>` when using defaults [#1746]
- Improved the accessibility of `<sl-tooltip>` so they persist when hovering over the tooltip and dismiss when pressing [[Esc]] [#1734] - Improved the accessibility of `<sl-tooltip>` so they persist when hovering over the tooltip and dismiss when pressing [[Esc]] [#1734]

Wyświetl plik

@ -1,14 +1,22 @@
// // Cached compute style calls. This is specifically for browsers that dont support `checkVisibility()`.
// This doesn't technically check visibility, it checks if the element has been rendered and can maybe possibly be tabbed // computedStyle calls are "live" so they only need to be retrieved once for an element.
// to. This is a workaround for shadow roots not having an `offsetParent`. const computedStyleMap = new WeakMap<Element, CSSStyleDeclaration>();
//
// See https://stackoverflow.com/questions/19669786/check-if-element-is-visible-in-dom function isVisible(el: HTMLElement): boolean {
// // This is the fastest check, but isn't supported in Safari.
// Previously, we used https://www.npmjs.com/package/composed-offset-position, but recursing up an entire node tree took if (typeof el.checkVisibility === 'function') {
// up a lot of CPU cycles and made focus traps unusable in Chrome / Edge. return el.checkVisibility({ checkOpacity: false });
// }
function isTakingUpSpace(elem: HTMLElement): boolean {
return Boolean(elem.offsetParent || elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length); // Fallback "polyfill" for "checkVisibility"
let computedStyle: undefined | CSSStyleDeclaration = computedStyleMap.get(el);
if (!computedStyle) {
computedStyle = window.getComputedStyle(el, null);
computedStyleMap.set(el, computedStyle);
}
return computedStyle.visibility !== 'hidden' && computedStyle.display !== 'none';
} }
/** Determines if the specified element is tabbable using heuristics inspired by https://github.com/focus-trap/tabbable */ /** Determines if the specified element is tabbable using heuristics inspired by https://github.com/focus-trap/tabbable */
@ -30,13 +38,7 @@ function isTabbable(el: HTMLElement) {
return false; return false;
} }
// Elements that are hidden have no offsetParent and are not tabbable if (!isVisible(el)) {
if (!isTakingUpSpace(el)) {
return false;
}
// Elements without visibility are not tabbable
if (window.getComputedStyle(el).visibility === 'hidden') {
return false; return false;
} }
@ -73,7 +75,17 @@ export function getTabbableBoundary(root: HTMLElement | ShadowRoot) {
return { start, end }; return { start, end };
} }
/**
* This looks funky. Basically a slot's children will always be picked up *if* they're within the `root` element.
* However, there is an edge case when, if the `root` is wrapped by another shadow DOM, it won't grab the children.
* This fixes that fun edge case.
*/
function getSlottedChildrenOutsideRootElement(slotElement: HTMLSlotElement, root: HTMLElement | ShadowRoot) {
return (slotElement.getRootNode({ composed: true }) as ShadowRoot | null)?.host !== root;
}
export function getTabbableElements(root: HTMLElement | ShadowRoot) { export function getTabbableElements(root: HTMLElement | ShadowRoot) {
const walkedEls = new WeakMap();
const tabbableElements: HTMLElement[] = []; const tabbableElements: HTMLElement[] = [];
function walk(el: HTMLElement | ShadowRoot) { function walk(el: HTMLElement | ShadowRoot) {
@ -83,19 +95,16 @@ export function getTabbableElements(root: HTMLElement | ShadowRoot) {
return; return;
} }
if (walkedEls.has(el)) {
return;
}
walkedEls.set(el, true);
if (!tabbableElements.includes(el) && isTabbable(el)) { if (!tabbableElements.includes(el) && isTabbable(el)) {
tabbableElements.push(el); tabbableElements.push(el);
} }
/** if (el instanceof HTMLSlotElement && getSlottedChildrenOutsideRootElement(el, root)) {
* This looks funky. Basically a slot's children will always be picked up *if* they're within the `root` element.
* However, there is an edge case when, if the `root` is wrapped by another shadow DOM, it won't grab the children.
* This fixes that fun edge case.
*/
const slotChildrenOutsideRootElement = (slotElement: HTMLSlotElement) =>
(slotElement.getRootNode({ composed: true }) as ShadowRoot | null)?.host !== root;
if (el instanceof HTMLSlotElement && slotChildrenOutsideRootElement(el)) {
el.assignedElements({ flatten: true }).forEach((assignedEl: HTMLElement) => { el.assignedElements({ flatten: true }).forEach((assignedEl: HTMLElement) => {
walk(assignedEl); walk(assignedEl);
}); });
@ -106,7 +115,9 @@ export function getTabbableElements(root: HTMLElement | ShadowRoot) {
} }
} }
[...el.children].forEach((e: HTMLElement) => walk(e)); for (const e of el.children) {
walk(e as HTMLElement);
}
} }
// Collect all elements including the root // Collect all elements including the root