mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 04:09:12 +00:00
prettier (#20)
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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 || [])
|
||||
|
||||
@@ -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>', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 ', () => {
|
||||
|
||||
@@ -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 ', () => {
|
||||
|
||||
@@ -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 ', () => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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': `
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 }>;
|
||||
|
||||
|
||||
@@ -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[] }>;
|
||||
|
||||
|
||||
@@ -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 }>;
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user