prettier (#20)

This commit is contained in:
Konnor Rogers
2023-12-08 15:09:34 -05:00
committed by GitHub
parent d8b6db8c5b
commit 3f604fcee1
39 changed files with 196 additions and 126 deletions

View File

@@ -22,6 +22,8 @@ New versions of Web Awesome are released as-needed and generally occur when a cr
## Next
- Fixed files that did not have `.js` extensions. [#1770]
- Fixed `<sl-dialog>` not accounting for elements with hidden dialog controls like `<video>` [#1755]
- 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-select>` that prevented it from closing when tabbing to another select inside a shadow root [#1763]

View File

@@ -25,10 +25,10 @@ for await (const component of components) {
const componentFile = path.join(componentDir, 'index.ts');
const importPath = component.path.replace(/\.js$/, '.component.js');
const eventImports = (component.events || [])
.map(event => `import type { ${event.eventName} } from '../../events/events';`)
.map(event => `import type { ${event.eventName} } from '../../events/events.js';`)
.join('\n');
const eventExports = (component.events || [])
.map(event => `export type { ${event.eventName} } from '../../events/events';`)
.map(event => `export type { ${event.eventName} } from '../../events/events.js';`)
.join('\n');
const eventNameImport = (component.events || []).length > 0 ? `import { type EventName } from '@lit/react';` : ``;
const events = (component.events || [])

View File

@@ -1,5 +1,6 @@
import '../../../dist/webawesome.js';
import { aTimeout, expect, fixture, html, oneEvent } from '@open-wc/testing';
import { aTimeout, expect, fixture, oneEvent } from '@open-wc/testing';
import { html } from 'lit';
import type WaAnimation from './animation.js';
describe('<wa-animation>', () => {

View File

@@ -2,9 +2,9 @@ import '../../../dist/webawesome.js';
// cspell:dictionaries lorem-ipsum
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon';
import type { WaHideEvent } from '../../events/wa-hide';
import type { WaShowEvent } from '../../events/wa-show';
import type WaDetails from './details';
import type { WaHideEvent } from '../../events/wa-hide.js';
import type { WaShowEvent } from '../../events/wa-show.js';
import type WaDetails from './details.js';
describe('<wa-details>', () => {
describe('accessibility', () => {

View File

@@ -299,9 +299,9 @@ export default class WaDialog extends WebAwesomeElement {
`
: ''}
${
'' /* The tabindex="-1" is here because the body is technically scrollable if overflowing. However, if there's no focusable elements inside, you won't actually be able to scroll it via keyboard. */
'' /* The tabindex="-1" is here because the body is technically scrollable if overflowing. However, if there's no focusable elements inside, you won't actually be able to scroll it via keyboard. Previously this was just a <slot>, but tabindex="-1" on the slot causes children to not be focusable. https://github.com/shoelace-style/shoelace/issues/1753#issuecomment-1836803277 */
}
<slot part="body" class="dialog__body" tabindex="-1"></slot>
<div part="body" class="dialog__body" tabindex="-1"><slot></slot></div>
<footer part="footer" class="dialog__footer">
<slot name="footer"></slot>

View File

@@ -1,10 +1,10 @@
import '../../../dist/webawesome.js';
// cspell:dictionaries lorem-ipsum
import { aTimeout, elementUpdated, expect, fixture, html, waitUntil } from '@open-wc/testing';
import { LitElement } from 'lit';
import { aTimeout, elementUpdated, expect, fixture, waitUntil } from '@open-wc/testing';
import { html, LitElement } from 'lit';
import { sendKeys } from '@web/test-runner-commands';
import sinon from 'sinon';
import type WaDialog from './dialog';
import type WaDialog from './dialog.js';
describe('<wa-dialog>', () => {
it('should be visible with the open attribute', async () => {

View File

@@ -3,7 +3,7 @@ import '../../../dist/webawesome.js';
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import { sendKeys } from '@web/test-runner-commands';
import sinon from 'sinon';
import type WaDrawer from './drawer';
import type WaDrawer from './drawer.js';
describe('<wa-drawer>', () => {
it('should be visible with the open attribute', async () => {

View File

@@ -1,6 +1,6 @@
import '../../../dist/webawesome.js';
import { elementUpdated, expect, fixture, html } from '@open-wc/testing';
import type WaFormatBytes from './format-bytes';
import type WaFormatBytes from './format-bytes.js';
describe('<wa-format-bytes>', () => {
describe('defaults ', () => {

View File

@@ -1,7 +1,7 @@
import '../../../dist/webawesome.js';
import { expect, fixture, html } from '@open-wc/testing';
import sinon from 'sinon';
import type WaFormatDate from './format-date';
import type WaFormatDate from './format-date.js';
describe('<wa-format-date>', () => {
describe('defaults ', () => {

View File

@@ -1,6 +1,6 @@
import '../../../dist/webawesome.js';
import { expect, fixture, html } from '@open-wc/testing';
import type WaFormatNumber from './format-number';
import type WaFormatNumber from './format-number.js';
describe('<wa-format-number>', () => {
describe('defaults ', () => {

View File

@@ -1,7 +1,7 @@
import '../../../dist/webawesome.js';
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon';
import type WaIconButton from './icon-button';
import type WaIconButton from './icon-button.js';
type LinkTarget = '_self' | '_blank' | '_parent' | '_top';

View File

@@ -1,8 +1,8 @@
import { aTimeout, elementUpdated, expect, fixture, html, oneEvent } from '@open-wc/testing';
import { registerIconLibrary } from '../../../dist/webawesome.js';
import type { WaErrorEvent } from '../../events/wa-error';
import type { WaLoadEvent } from '../../events/wa-load';
import type WaIcon from './icon';
import type { WaErrorEvent } from '../../events/wa-error.js';
import type { WaLoadEvent } from '../../events/wa-load.js';
import type WaIcon from './icon.js';
const testLibraryIcons = {
'test-icon1': `

View File

@@ -1,7 +1,7 @@
import '../../../dist/webawesome.js';
import { expect, fixture, html } from '@open-wc/testing';
import sinon from 'sinon';
import type WaImageComparer from './image-comparer';
import type WaImageComparer from './image-comparer.js';
describe('<wa-image-comparer>', () => {
it('should render a basic before/after', async () => {

View File

@@ -1,7 +1,7 @@
import '../../../dist/webawesome.js';
import { aTimeout, expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon';
import type WaInclude from './include';
import type WaInclude from './include.js';
const stubbedFetchResponse: Response = {
headers: new Headers(),

View File

@@ -4,7 +4,7 @@ import { getFormControls, serialize } from '../../../dist/webawesome.js';
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests.js';
import { sendKeys } from '@web/test-runner-commands'; // must come from the same module
import sinon from 'sinon';
import type WaInput from './input';
import type WaInput from './input.js';
describe('<wa-input>', () => {
it('should pass accessibility tests', async () => {

View File

@@ -2,8 +2,8 @@ import '../../../dist/webawesome.js';
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import { sendKeys } from '@web/test-runner-commands';
import sinon from 'sinon';
import type { WaSelectEvent } from '../../events/wa-select';
import type WaMenuItem from './menu-item';
import type { WaSelectEvent } from '../../events/wa-select.js';
import type WaMenuItem from './menu-item.js';
describe('<wa-menu-item>', () => {
it('should pass accessibility tests', async () => {

View File

@@ -1,6 +1,6 @@
import '../../../dist/webawesome.js';
import { expect, fixture, html } from '@open-wc/testing';
import type WaMenuLabel from './menu-label';
import type WaMenuLabel from './menu-label.js';
describe('<wa-menu-label>', () => {
it('passes accessibility test', async () => {

View File

@@ -4,8 +4,8 @@ import { expect, fixture } from '@open-wc/testing';
import { html } from 'lit';
import { sendKeys } from '@web/test-runner-commands';
import sinon from 'sinon';
import type { WaSelectEvent } from '../../events/wa-select';
import type WaMenu from './menu';
import type { WaSelectEvent } from '../../events/wa-select.js';
import type WaMenu from './menu.js';
describe('<wa-menu>', () => {
it('emits wa-select with the correct event detail when clicking an item', async () => {

View File

@@ -1,7 +1,7 @@
import '../../../dist/webawesome.js';
import { aTimeout, expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon';
import type WaOption from './option';
import type WaOption from './option.js';
describe('<wa-option>', () => {
it('passes accessibility test', async () => {

View File

@@ -1,6 +1,6 @@
import '../../../dist/webawesome.js';
import { expect, fixture, html } from '@open-wc/testing';
import type WaProgressBar from './progress-bar';
import type WaProgressBar from './progress-bar.js';
describe('<wa-progress-bar>', () => {
let el: WaProgressBar;

View File

@@ -1,6 +1,6 @@
import '../../../dist/webawesome.js';
import { expect, fixture, html } from '@open-wc/testing';
import type WaProgressRing from './progress-ring';
import type WaProgressRing from './progress-ring.js';
describe('<wa-progress-ring>', () => {
let el: WaProgressRing;

View File

@@ -51,7 +51,7 @@ export default class WaQrCode extends WebAwesomeElement {
return;
}
QrCreator.render(
(QrCreator as unknown as typeof QrCreator.default).render(
{
text: this.value,
radius: this.radius,

View File

@@ -1,6 +1,6 @@
import '../../../dist/webawesome.js';
import { expect, fixture, html } from '@open-wc/testing';
import type WaQrCode from './qr-code';
import type WaQrCode from './qr-code.js';
const getCanvas = (qrCode: WaQrCode): HTMLCanvasElement => {
const possibleCanvas = qrCode.shadowRoot?.querySelector<HTMLCanvasElement>('.qr-code');

View File

@@ -1,7 +1,7 @@
import '../../../dist/webawesome.js';
import { expect, fixture, html } from '@open-wc/testing';
import type WaRadioButton from './radio-button';
import type WaRadioGroup from '../radio-group/radio-group';
import type WaRadioButton from './radio-button.js';
import type WaRadioGroup from '../radio-group/radio-group.js';
describe('<wa-radio-button>', () => {
it('should not get checked when disabled', async () => {

View File

@@ -1,7 +1,7 @@
import '../../../dist/webawesome.js';
import { expect, fixture, html } from '@open-wc/testing';
import type WaRadio from './radio';
import type WaRadioGroup from '../radio-group/radio-group';
import type WaRadio from './radio.js';
import type WaRadioGroup from '../radio-group/radio-group.js';
describe('<wa-radio>', () => {
it('should not get checked when disabled', async () => {

View File

@@ -1,6 +1,6 @@
import '../../../dist/webawesome.js';
import { expect, fixture, html } from '@open-wc/testing';
import type WaSkeleton from './skeleton';
import type WaSkeleton from './skeleton.js';
describe('<wa-skeleton>', () => {
it('should render default skeleton', async () => {

View File

@@ -1,6 +1,6 @@
import '../../../dist/webawesome.js';
import { expect, fixture, html } from '@open-wc/testing';
import type WaSpinner from './spinner';
import type WaSpinner from './spinner.js';
describe('<wa-spinner>', () => {
describe('when provided no parameters', () => {

View File

@@ -1,6 +1,6 @@
import '../../../dist/webawesome.js';
import { aTimeout, expect, fixture, html } from '@open-wc/testing';
import type WaTabPanel from './tab-panel';
import type WaTabPanel from './tab-panel.js';
describe('<wa-tab-panel>', () => {
it('passes accessibility test', async () => {

View File

@@ -1,9 +1,9 @@
import '../../../dist/webawesome.js';
import { expect, fixture, html } from '@open-wc/testing';
import sinon from 'sinon';
import type WaIconButton from '../icon-button/icon-button';
import type WaTab from './tab';
import type WaTabGroup from '../tab-group/tab-group';
import type WaIconButton from '../icon-button/icon-button.js';
import type WaTab from './tab.js';
import type WaTabGroup from '../tab-group/tab-group.js';
describe('<wa-tab>', () => {
it('passes accessibility test', async () => {

View File

@@ -1,7 +1,7 @@
import '../../../dist/webawesome.js';
import { expect, fixture, html } from '@open-wc/testing';
import sinon from 'sinon';
import type WaTag from './tag';
import type WaTag from './tag.js';
describe('<wa-tag>', () => {
it('should render default tag', async () => {

View File

@@ -1,7 +1,7 @@
import '../../../dist/webawesome.js';
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon';
import type WaTooltip from './tooltip';
import type WaTooltip from './tooltip.js';
describe('<wa-tooltip>', () => {
it('should be visible with the open attribute', async () => {

View File

@@ -1,7 +1,7 @@
import '../../../dist/webawesome.js';
import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
import sinon from 'sinon';
import type WaTreeItem from './tree-item';
import type WaTreeItem from './tree-item.js';
describe('<wa-tree-item>', () => {
let leafItem: WaTreeItem;

View File

@@ -1,35 +1,35 @@
export type { WaAfterCollapseEvent } from './wa-after-collapse';
export type { WaAfterExpandEvent } from './wa-after-expand';
export type { WaAfterHideEvent } from './wa-after-hide';
export type { WaAfterShowEvent } from './wa-after-show';
export type { WaBlurEvent } from './wa-blur';
export type { WaCancelEvent } from './wa-cancel';
export type { WaChangeEvent } from './wa-change';
export type { WaClearEvent } from './wa-clear';
export type { WaCloseEvent } from './wa-close';
export type { WaCollapseEvent } from './wa-collapse';
export type { WaCopyEvent } from './wa-copy';
export type { WaErrorEvent } from './wa-error';
export type { WaExpandEvent } from './wa-expand';
export type { WaFinishEvent } from './wa-finish';
export type { WaFocusEvent } from './wa-focus';
export type { WaHideEvent } from './wa-hide';
export type { WaHoverEvent } from './wa-hover';
export type { WaInitialFocusEvent } from './wa-initial-focus';
export type { WaInputEvent } from './wa-input';
export type { WaInvalidEvent } from './wa-invalid';
export type { WaLazyChangeEvent } from './wa-lazy-change';
export type { WaLazyLoadEvent } from './wa-lazy-load';
export type { WaLoadEvent } from './wa-load';
export type { WaMutationEvent } from './wa-mutation';
export type { WaRemoveEvent } from './wa-remove';
export type { WaRepositionEvent } from './wa-reposition';
export type { WaRequestCloseEvent } from './wa-request-close';
export type { WaResizeEvent } from './wa-resize';
export type { WaSelectEvent } from './wa-select';
export type { WaSelectionChangeEvent } from './wa-selection-change';
export type { WaShowEvent } from './wa-show';
export type { WaSlideChangeEvent } from './wa-slide-change';
export type { WaStartEvent } from './wa-start';
export type { WaTabHideEvent } from './wa-tab-hide';
export type { WaTabShowEvent } from './wa-tab-show';
export type { WaAfterCollapseEvent } from './wa-after-collapse.js';
export type { WaAfterExpandEvent } from './wa-after-expand.js';
export type { WaAfterHideEvent } from './wa-after-hide.js';
export type { WaAfterShowEvent } from './wa-after-show.js';
export type { WaBlurEvent } from './wa-blur.js';
export type { WaCancelEvent } from './wa-cancel.js';
export type { WaChangeEvent } from './wa-change.js';
export type { WaClearEvent } from './wa-clear.js';
export type { WaCloseEvent } from './wa-close.js';
export type { WaCollapseEvent } from './wa-collapse.js';
export type { WaCopyEvent } from './wa-copy.js';
export type { WaErrorEvent } from './wa-error.js';
export type { WaExpandEvent } from './wa-expand.js';
export type { WaFinishEvent } from './wa-finish.js';
export type { WaFocusEvent } from './wa-focus.js';
export type { WaHideEvent } from './wa-hide.js';
export type { WaHoverEvent } from './wa-hover.js';
export type { WaInitialFocusEvent } from './wa-initial-focus.js';
export type { WaInputEvent } from './wa-input.js';
export type { WaInvalidEvent } from './wa-invalid.js';
export type { WaLazyChangeEvent } from './wa-lazy-change.js';
export type { WaLazyLoadEvent } from './wa-lazy-load.js';
export type { WaLoadEvent } from './wa-load.js';
export type { WaMutationEvent } from './wa-mutation.js';
export type { WaRemoveEvent } from './wa-remove.js';
export type { WaRepositionEvent } from './wa-reposition.js';
export type { WaRequestCloseEvent } from './wa-request-close.js';
export type { WaResizeEvent } from './wa-resize.js';
export type { WaSelectEvent } from './wa-select.js';
export type { WaSelectionChangeEvent } from './wa-selection-change.js';
export type { WaShowEvent } from './wa-show.js';
export type { WaSlideChangeEvent } from './wa-slide-change.js';
export type { WaStartEvent } from './wa-start.js';
export type { WaTabHideEvent } from './wa-tab-hide.js';
export type { WaTabShowEvent } from './wa-tab-show.js';

View File

@@ -1,4 +1,4 @@
import type WaMenuItem from '../components/menu-item/menu-item';
import type WaMenuItem from '../components/menu-item/menu-item.js';
export type WaSelectEvent = CustomEvent<{ item: WaMenuItem }>;

View File

@@ -1,4 +1,4 @@
import type WaTreeItem from '../components/tree-item/tree-item';
import type WaTreeItem from '../components/tree-item/tree-item.js';
export type WaSelectionChangeEvent = CustomEvent<{ selection: WaTreeItem[] }>;

View File

@@ -1,4 +1,4 @@
import type WaCarouselItem from '../components/carousel-item/carousel-item';
import type WaCarouselItem from '../components/carousel-item/carousel-item.js';
export type WaSlideChangeEvent = CustomEvent<{ index: number; slide: WaCarouselItem }>;

View File

@@ -8,9 +8,13 @@ export default class Modal {
isExternalActivated: boolean;
tabDirection: 'forward' | 'backward' = 'forward';
currentFocus: HTMLElement | null;
previousFocus: HTMLElement | null;
elementsWithTabbableControls: string[];
constructor(element: HTMLElement) {
this.element = element;
this.elementsWithTabbableControls = ['iframe'];
}
/** Activates focus trapping. */
@@ -56,39 +60,61 @@ export default class Modal {
if (typeof target?.focus === 'function') {
this.currentFocus = target;
target.focus({ preventScroll: true });
target.focus({ preventScroll: false });
}
}
}
}
private handleFocusIn = () => {
if (!this.isActive()) return;
this.checkFocus();
};
private possiblyHasTabbableChildren(element: HTMLElement) {
return (
this.elementsWithTabbableControls.includes(element.tagName.toLowerCase()) || element.hasAttribute('controls')
// Should we add a data-attribute for people to set just in case they have an element where we don't know if it has possibly tabbable elements?
);
}
private handleKeyDown = (event: KeyboardEvent) => {
if (event.key !== 'Tab' || this.isExternalActivated) return;
if (!this.isActive()) return;
// Because sometimes focus can actually be taken over from outside sources,
// we don't want to rely on `this.currentFocus`. Instead we check the actual `activeElement` and
// recurse through shadowRoots.
const currentActiveElement = getDeepestActiveElement();
this.previousFocus = currentActiveElement as HTMLElement | null;
if (this.previousFocus && this.possiblyHasTabbableChildren(this.previousFocus)) {
return;
}
if (event.shiftKey) {
this.tabDirection = 'backward';
} else {
this.tabDirection = 'forward';
}
event.preventDefault();
const tabbableElements = getTabbableElements(this.element);
// Because sometimes focus can actually be taken over from outside sources,
// we don't want to rely on `this.currentFocus`. Instead we check the actual `activeElement` and
// recurse through shadowRoots.
const currentActiveElement = getDeepestActiveElement();
let currentFocusIndex = tabbableElements.findIndex(el => el === currentActiveElement);
this.previousFocus = this.currentFocus;
if (currentFocusIndex === -1) {
this.currentFocus = tabbableElements[0];
this.currentFocus?.focus({ preventScroll: true });
// We don't call event.preventDefault() here because it messes with tabbing to the <iframe> controls.
// We just wait until the current focus is no longer an element with possible hidden controls.
if (Boolean(this.previousFocus) && this.possiblyHasTabbableChildren(this.previousFocus!)) {
return;
}
event.preventDefault();
this.currentFocus?.focus({ preventScroll: false });
return;
}
@@ -102,7 +128,23 @@ export default class Modal {
currentFocusIndex += addition;
}
this.currentFocus = tabbableElements[currentFocusIndex];
this.previousFocus = this.currentFocus;
const nextFocus = /** @type {HTMLElement} */ tabbableElements[currentFocusIndex];
// This is a special case. We need to make sure we're not calling .focus() if we're already focused on an element
// that possibly has "controls"
if (this.tabDirection === 'backward') {
if (this.previousFocus && this.possiblyHasTabbableChildren(this.previousFocus)) {
return;
}
}
if (nextFocus && this.possiblyHasTabbableChildren(nextFocus)) {
return;
}
event.preventDefault();
this.currentFocus = nextFocus;
this.currentFocus?.focus({ preventScroll: true });
setTimeout(() => this.checkFocus());

View File

@@ -1,22 +1,44 @@
//
// This doesn't technically check visibility, it checks if the element has been rendered and can maybe possibly be tabbed
// to. This is a workaround for shadow roots not having an `offsetParent`.
//
// See https://stackoverflow.com/questions/19669786/check-if-element-is-visible-in-dom
//
// Previously, we used https://www.npmjs.com/package/composed-offset-position, but recursing up an entire node tree took
// up a lot of CPU cycles and made focus traps unusable in Chrome / Edge.
//
function isTakingUpSpace(elem: HTMLElement): boolean {
return Boolean(elem.offsetParent || elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length);
// Cached compute style calls. This is specifically for browsers that dont support `checkVisibility()`.
// computedStyle calls are "live" so they only need to be retrieved once for an element.
const computedStyleMap = new WeakMap<Element, CSSStyleDeclaration>();
function isVisible(el: HTMLElement): boolean {
// This is the fastest check, but isn't supported in Safari.
if (typeof el.checkVisibility === 'function') {
// Opacity is focusable, visibility is not.
return el.checkVisibility({ checkOpacity: false, checkVisibilityCSS: true });
}
// 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 */
function isTabbable(el: HTMLElement) {
const tag = el.tagName.toLowerCase();
// Elements with a -1 tab index are not tabbable
if (el.getAttribute('tabindex') === '-1') {
const tabindex = Number(el.getAttribute('tabindex'));
const hasTabindex = el.hasAttribute('tabindex');
// elements with a tabindex attribute that is either NaN or <= -1 are not tabbable
if (hasTabindex && (isNaN(tabindex) || tabindex <= -1)) {
return false;
}
// Elements with a disabled attribute are not tabbable
if (el.hasAttribute('disabled')) {
return false;
}
// If any parents have "inert", we aren't "tabbable"
if (el.closest('[inert]')) {
return false;
}
@@ -30,13 +52,7 @@ function isTabbable(el: HTMLElement) {
return false;
}
// Elements that are hidden have no offsetParent and are not tabbable
if (!isTakingUpSpace(el)) {
return false;
}
// Elements without visibility are not tabbable
if (window.getComputedStyle(el).visibility === 'hidden') {
if (!isVisible(el)) {
return false;
}
@@ -56,7 +72,7 @@ function isTabbable(el: HTMLElement) {
}
// At this point, the following elements are considered tabbable
return ['button', 'input', 'select', 'textarea', 'a', 'audio', 'video', 'summary'].includes(tag);
return ['button', 'input', 'select', 'textarea', 'a', 'audio', 'video', 'summary', 'iframe'].includes(tag);
}
/**
@@ -73,29 +89,36 @@ export function getTabbableBoundary(root: HTMLElement | ShadowRoot) {
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) {
const walkedEls = new WeakMap();
const tabbableElements: HTMLElement[] = [];
function walk(el: HTMLElement | ShadowRoot) {
if (el instanceof Element) {
// if the element has "inert" we can just no-op it.
if (el.hasAttribute('inert')) {
if (el.hasAttribute('inert') || el.closest('[inert]')) {
return;
}
if (walkedEls.has(el)) {
return;
}
walkedEls.set(el, true);
if (!tabbableElements.includes(el) && isTabbable(el)) {
tabbableElements.push(el);
}
/**
* 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)) {
if (el instanceof HTMLSlotElement && getSlottedChildrenOutsideRootElement(el, root)) {
el.assignedElements({ flatten: true }).forEach((assignedEl: HTMLElement) => {
walk(assignedEl);
});
@@ -106,7 +129,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

View File

@@ -3,7 +3,8 @@
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
"target": "es2017",
"module": "es2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": [
"dom",
"dom.Iterable",
@@ -20,7 +21,6 @@
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"esModuleInterop": true,
"experimentalDecorators": true,
"useDefineForClassFields": false, /* See https://lit.dev/docs/components/properties/#avoiding-issues-with-class-fields */