From 4bacaa49e25ce3869399ebf6bf933649893004c5 Mon Sep 17 00:00:00 2001 From: Marissa Huysentruyt Date: Fri, 17 Oct 2025 16:41:46 -0400 Subject: [PATCH 01/19] feat(tray): add dismissHelper function --- 1st-gen/packages/tray/src/Tray.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/1st-gen/packages/tray/src/Tray.ts b/1st-gen/packages/tray/src/Tray.ts index 901910c9380..d2a73407b1d 100644 --- a/1st-gen/packages/tray/src/Tray.ts +++ b/1st-gen/packages/tray/src/Tray.ts @@ -81,6 +81,23 @@ export class Tray extends SpectrumElement { } } + /** + * Returns a visually hidden dismiss button for mobile screen reader accessibility. + * This button is placed before and after tray content to allow mobile screen reader + * users (particularly VoiceOver on iOS) to easily dismiss the overlay. + */ + protected get dismissHelper(): TemplateResult { + return html` +
+ +
+ `; + } + private dispatchClosed(): void { this.dispatchEvent( new Event('close', { @@ -131,7 +148,9 @@ export class Tray extends SpectrumElement { tabindex="-1" @transitionend=${this.handleTrayTransitionend} > + ${this.dismissHelper} + ${this.dismissHelper} `; } From 1d100df8d74b6c9d9f8c1fc9df389faf642baa36 Mon Sep 17 00:00:00 2001 From: Marissa Huysentruyt Date: Fri, 17 Oct 2025 16:42:36 -0400 Subject: [PATCH 02/19] refactor(tray): expand visually-hidden selector for dismiss buttons --- 1st-gen/packages/tray/src/tray.css | 1 + 1 file changed, 1 insertion(+) diff --git a/1st-gen/packages/tray/src/tray.css b/1st-gen/packages/tray/src/tray.css index d6bd646b275..33ec07f30b4 100644 --- a/1st-gen/packages/tray/src/tray.css +++ b/1st-gen/packages/tray/src/tray.css @@ -30,6 +30,7 @@ sp-underlay { overscroll-behavior: contain; } +.visually-hidden, ::slotted(.visually-hidden) { border: 0; clip: rect(0, 0, 0, 0); From 713b3cf78e5bd550d29927d13ae493a8914aa08b Mon Sep 17 00:00:00 2001 From: Marissa Huysentruyt Date: Fri, 17 Oct 2025 16:43:58 -0400 Subject: [PATCH 03/19] docs(tray): add tray docs with visually hidden dismiss --- 1st-gen/packages/tray/README.md | 49 +++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/1st-gen/packages/tray/README.md b/1st-gen/packages/tray/README.md index 3bbdaadb38d..85bca444c92 100644 --- a/1st-gen/packages/tray/README.md +++ b/1st-gen/packages/tray/README.md @@ -7,19 +7,19 @@ [![See it on NPM!](https://img.shields.io/npm/v/@spectrum-web-components/tray?style=for-the-badge)](https://www.npmjs.com/package/@spectrum-web-components/tray) [![How big is this package in your project?](https://img.shields.io/bundlephobia/minzip/@spectrum-web-components/tray?style=for-the-badge)](https://bundlephobia.com/result?p=@spectrum-web-components/tray) -``` +```bash yarn add @spectrum-web-components/tray ``` Import the side effectful registration of `` via: -``` +```js import '@spectrum-web-components/tray/sp-tray.js'; ``` When looking to leverage the `Tray` base class as a type and/or for extension purposes, do so via: -``` +```js import { Tray } from '@spectrum-web-components/tray'; ``` @@ -70,3 +70,46 @@ A tray has a single default `slot`. ### Accessibility `` presents a page blocking experience and should be opened with the `Overlay` API using the `modal` interaction to ensure that the content appropriately manages the presence of other content in the tab order of the page and the availability of that content for a screen reader. + +#### Mobile screen reader support + +The `` component automatically includes visually hidden dismiss buttons before and after its content to support mobile screen readers. This is particularly important for VoiceOver on iOS, where users navigate through interactive elements sequentially. + +These built-in dismiss buttons: + +- Are visually hidden but accessible to screen readers +- Use `tabindex="-1"` to prevent keyboard tab navigation interference +- Allow mobile screen reader users to easily dismiss the tray from either the beginning or end of the content +- Are labeled "Dismiss" for clear screen reader announcements + +This dismiss helper pattern is also implemented in the [``](https://opensource.adobe.com/spectrum-web-components/components/picker/) component, which uses the same approach when rendering menu content in a tray on mobile devices. + +Simply place your content inside the tray - the dismiss buttons are automatically rendered: + +```html + + + Toggle menu content + + + + Deselect + Select Inverse + Feather... + Select and Mask... + + + + + + + Toggle dialog content + + + +

New messages

+ You have 5 new messages. +
+
+
+``` From 4a83701da2aa05e2534c0ffdd75dbb2157c69497 Mon Sep 17 00:00:00 2001 From: Marissa Huysentruyt Date: Tue, 21 Oct 2025 14:33:33 -0400 Subject: [PATCH 04/19] feat(dialog-base): add dismissHelper for visually-hidden buttons --- 1st-gen/packages/dialog/src/DialogBase.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/1st-gen/packages/dialog/src/DialogBase.ts b/1st-gen/packages/dialog/src/DialogBase.ts index 2973c20ac69..7d35e8a584d 100644 --- a/1st-gen/packages/dialog/src/DialogBase.ts +++ b/1st-gen/packages/dialog/src/DialogBase.ts @@ -194,6 +194,23 @@ export class DialogBase extends FocusVisiblePolyfillMixin(SpectrumElement) { super.update(changes); } + /** + * Returns a visually hidden dismiss button for mobile screen reader accessibility. + * This button is placed before and after dialog content to allow mobile screen reader + * users (particularly VoiceOver on iOS) to easily dismiss the overlay. + */ + protected get dismissHelper(): TemplateResult { + return html` +
+ +
+ `; + } + protected renderDialog(): TemplateResult { return html` From 5692d4ad7eaeb5f47110e2efcc3d5209780d324d Mon Sep 17 00:00:00 2001 From: Marissa Huysentruyt Date: Tue, 21 Oct 2025 14:36:32 -0400 Subject: [PATCH 05/19] feat(dialog-wrapper): implement conditional dismissHelper buttons --- 1st-gen/packages/dialog/src/DialogWrapper.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/1st-gen/packages/dialog/src/DialogWrapper.ts b/1st-gen/packages/dialog/src/DialogWrapper.ts index f6d6953fe9b..d46df91f638 100644 --- a/1st-gen/packages/dialog/src/DialogWrapper.ts +++ b/1st-gen/packages/dialog/src/DialogWrapper.ts @@ -125,6 +125,7 @@ export class DialogWrapper extends DialogBase { } return html` + ${this.dismissHelper} + ${!this.dismissable ? this.dismissHelper : nothing} `; } } From 864b45a2259436ffad6f00b1ee9caaeef65d6da2 Mon Sep 17 00:00:00 2001 From: Marissa Huysentruyt Date: Tue, 21 Oct 2025 14:46:02 -0400 Subject: [PATCH 06/19] feat(modal): add visually-hidden styles for dialog dismissHelpers --- 1st-gen/packages/modal/src/modal.css | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/1st-gen/packages/modal/src/modal.css b/1st-gen/packages/modal/src/modal.css index 996d22d3d6d..5c632d34982 100644 --- a/1st-gen/packages/modal/src/modal.css +++ b/1st-gen/packages/modal/src/modal.css @@ -24,3 +24,16 @@ .modal { overflow: visible; } + +.visually-hidden { + border: 0; + clip: rect(0, 0, 0, 0); + clip-path: inset(50%); + height: 1px; + margin: 0 -1px -1px 0; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + white-space: nowrap; +} From 7fa46158f9c2f10626a3884144dfaee746f2bb8f Mon Sep 17 00:00:00 2001 From: Marissa Huysentruyt Date: Wed, 22 Oct 2025 15:15:22 -0400 Subject: [PATCH 07/19] revert: feat(modal): add visually-hidden styles for dialog dismissHelpers we do not need to provide a close/visually hidden button for non-dismissible diaologs --- 1st-gen/packages/modal/src/modal.css | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/1st-gen/packages/modal/src/modal.css b/1st-gen/packages/modal/src/modal.css index 5c632d34982..996d22d3d6d 100644 --- a/1st-gen/packages/modal/src/modal.css +++ b/1st-gen/packages/modal/src/modal.css @@ -24,16 +24,3 @@ .modal { overflow: visible; } - -.visually-hidden { - border: 0; - clip: rect(0, 0, 0, 0); - clip-path: inset(50%); - height: 1px; - margin: 0 -1px -1px 0; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; - white-space: nowrap; -} From 970b87cc1c4e44a42d7849bcc3f6c652df7f273c Mon Sep 17 00:00:00 2001 From: Marissa Huysentruyt Date: Wed, 22 Oct 2025 15:16:47 -0400 Subject: [PATCH 08/19] revert: feat(dialog-wrapper): implement conditional dismissHelper buttons we do not need to provide a close/visually hidden button for non-dismissible dialogs --- 1st-gen/packages/dialog/src/DialogWrapper.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/1st-gen/packages/dialog/src/DialogWrapper.ts b/1st-gen/packages/dialog/src/DialogWrapper.ts index d46df91f638..f6d6953fe9b 100644 --- a/1st-gen/packages/dialog/src/DialogWrapper.ts +++ b/1st-gen/packages/dialog/src/DialogWrapper.ts @@ -125,7 +125,6 @@ export class DialogWrapper extends DialogBase { } return html` - ${this.dismissHelper} - ${!this.dismissable ? this.dismissHelper : nothing} `; } } From f8a86040f31fc9ca9a1398fd9a7c17e2abe1cc57 Mon Sep 17 00:00:00 2001 From: Marissa Huysentruyt Date: Wed, 22 Oct 2025 15:17:38 -0400 Subject: [PATCH 09/19] revert: feat(dialog-base): add dismissHelper for visually-hidden buttons we do not need to provide a close/visually hidden button for non-dismissible dialogs --- 1st-gen/packages/dialog/src/DialogBase.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/1st-gen/packages/dialog/src/DialogBase.ts b/1st-gen/packages/dialog/src/DialogBase.ts index 7d35e8a584d..2973c20ac69 100644 --- a/1st-gen/packages/dialog/src/DialogBase.ts +++ b/1st-gen/packages/dialog/src/DialogBase.ts @@ -194,23 +194,6 @@ export class DialogBase extends FocusVisiblePolyfillMixin(SpectrumElement) { super.update(changes); } - /** - * Returns a visually hidden dismiss button for mobile screen reader accessibility. - * This button is placed before and after dialog content to allow mobile screen reader - * users (particularly VoiceOver on iOS) to easily dismiss the overlay. - */ - protected get dismissHelper(): TemplateResult { - return html` -
- -
- `; - } - protected renderDialog(): TemplateResult { return html` From 361d5864e63f673da6b21cb84e44018445d7f877 Mon Sep 17 00:00:00 2001 From: Marissa Huysentruyt Date: Wed, 22 Oct 2025 15:48:59 -0400 Subject: [PATCH 10/19] feat(tray): expand API for dismissible behavior - adds several properties and methods to tray API to support more flexibile dismissible behavior. - queries for keyboard-accessible dismiss buttons in the tray's slot content - adds a state property to track if dismiss buttons are needed - adds manual override for dismissible behavior --- 1st-gen/packages/tray/src/Tray.ts | 99 ++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 3 deletions(-) diff --git a/1st-gen/packages/tray/src/Tray.ts b/1st-gen/packages/tray/src/Tray.ts index d2a73407b1d..0a622153525 100644 --- a/1st-gen/packages/tray/src/Tray.ts +++ b/1st-gen/packages/tray/src/Tray.ts @@ -13,6 +13,7 @@ import { CSSResultArray, html, + nothing, PropertyValues, SpectrumElement, TemplateResult, @@ -20,6 +21,7 @@ import { import { property, query, + state, } from '@spectrum-web-components/base/src/decorators.js'; import '@spectrum-web-components/underlay/sp-underlay.js'; import { firstFocusableIn } from '@spectrum-web-components/shared/src/first-focusable-in.js'; @@ -55,6 +57,9 @@ export class Tray extends SpectrumElement { @query('.tray') private tray!: HTMLDivElement; + @query('slot') + private slot!: HTMLSlotElement; + public override focus(): void { const firstFocusable = firstFocusableIn(this); if (firstFocusable) { @@ -81,6 +86,16 @@ export class Tray extends SpectrumElement { } } + /** + * When set, prevents the tray from rendering visually-hidden dismiss helpers. + * Use this if your slotted content has custom keyboard-accessible dismiss functionality + * that the auto-detection doesn't recognize. + * + * By default, the tray automatically detects buttons in slotted content. + */ + @property({ type: Boolean, attribute: 'has-keyboard-dismiss' }) + public hasKeyboardDismissButton = false; + /** * Returns a visually hidden dismiss button for mobile screen reader accessibility. * This button is placed before and after tray content to allow mobile screen reader @@ -98,6 +113,74 @@ export class Tray extends SpectrumElement { `; } + /** + * Internal state tracking whether dismiss helpers are needed. + * Automatically updated when slotted content changes. + */ + @state() + private needsDismissHelper = true; + + /** + * Check if slotted content has keyboard-accessible dismiss buttons. + * Looks for buttons in light DOM and checks for known components with built-in dismiss. + */ + private checkForDismissButtons(): void { + if (!this.slot) { + this.needsDismissHelper = true; + return; + } + + const slottedElements = this.slot.assignedElements({ flatten: true }); + + if (slottedElements.length === 0) { + this.needsDismissHelper = true; + return; + } + + const hasDismissButton = slottedElements.some((element) => { + // Check if element is a button itself + if ( + element.tagName === 'SP-BUTTON' || + element.tagName === 'SP-CLOSE-BUTTON' || + element.tagName === 'BUTTON' + ) { + return true; + } + + // Check for dismissable dialog (has built-in dismiss button in shadow DOM) + if ( + element.tagName === 'SP-DIALOG' && + element.hasAttribute('dismissable') + ) { + return true; + } + + // Check for dismissable dialog-wrapper + if ( + element.tagName === 'SP-DIALOG-WRAPPER' && + element.hasAttribute('dismissable') + ) { + return true; + } + + // Check for buttons in light DOM (won't see shadow DOM) + const buttons = element.querySelectorAll( + 'sp-button, sp-close-button, button' + ); + if (buttons.length > 0) { + return true; + } + + return false; + }); + + this.needsDismissHelper = !hasDismissButton; + } + + private handleSlotChange(): void { + this.checkForDismissButtons(); + } + private dispatchClosed(): void { this.dispatchEvent( new Event('close', { @@ -119,6 +202,12 @@ export class Tray extends SpectrumElement { } } + protected override firstUpdated(changes: PropertyValues): void { + super.firstUpdated(changes); + // Run initial button detection + this.checkForDismissButtons(); + } + protected override update(changes: PropertyValues): void { if ( changes.has('open') && @@ -148,9 +237,13 @@ export class Tray extends SpectrumElement { tabindex="-1" @transitionend=${this.handleTrayTransitionend} > - ${this.dismissHelper} - - ${this.dismissHelper} + ${!this.hasKeyboardDismissButton && this.needsDismissHelper + ? this.dismissHelper + : nothing} + + ${!this.hasKeyboardDismissButton && this.needsDismissHelper + ? this.dismissHelper + : nothing} `; } From 12ecf8b64b69db0482dbd39bf82bd0db4f4555ae Mon Sep 17 00:00:00 2001 From: Marissa Huysentruyt Date: Thu, 23 Oct 2025 09:13:14 -0400 Subject: [PATCH 11/19] docs(tray): update README with new properties/examples --- 1st-gen/packages/tray/README.md | 45 +++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/1st-gen/packages/tray/README.md b/1st-gen/packages/tray/README.md index 85bca444c92..d435db13af0 100644 --- a/1st-gen/packages/tray/README.md +++ b/1st-gen/packages/tray/README.md @@ -7,7 +7,7 @@ [![See it on NPM!](https://img.shields.io/npm/v/@spectrum-web-components/tray?style=for-the-badge)](https://www.npmjs.com/package/@spectrum-web-components/tray) [![How big is this package in your project?](https://img.shields.io/bundlephobia/minzip/@spectrum-web-components/tray?style=for-the-badge)](https://bundlephobia.com/result?p=@spectrum-web-components/tray) -```bash +```zsh yarn add @spectrum-web-components/tray ``` @@ -71,9 +71,9 @@ A tray has a single default `slot`. `` presents a page blocking experience and should be opened with the `Overlay` API using the `modal` interaction to ensure that the content appropriately manages the presence of other content in the tab order of the page and the availability of that content for a screen reader. -#### Mobile screen reader support +#### Auto-detection behavior -The `` component automatically includes visually hidden dismiss buttons before and after its content to support mobile screen readers. This is particularly important for VoiceOver on iOS, where users navigate through interactive elements sequentially. +By default, `` automatically detects whether its slotted content includes keyboard-accessible dismiss buttons (like ``, ``, or HTML ` + + + `); + await elementUpdated(el); + + const helpers = el.shadowRoot.querySelectorAll('.visually-hidden'); + expect(helpers.length).to.equal(0); + }); + + it('does not render dismiss helpers with has-keyboard-dismiss attribute', async () => { + const el = await fixture(html` + +

Custom content with custom dismiss handling

+
+ `); + await elementUpdated(el); + + expect(el.hasKeyboardDismissButton).to.be.true; + + const helpers = el.shadowRoot.querySelectorAll('.visually-hidden'); + expect(helpers.length).to.equal(0); + }); + + it('renders dismiss helpers after slot content changes to remove buttons', async () => { + const el = await fixture(html` + + Close + + `); + await elementUpdated(el); + + // Should not have helpers initially + let helpers = el.shadowRoot.querySelectorAll('.visually-hidden'); + expect(helpers.length).to.equal(0); + + // Remove the button + const button = el.querySelector('sp-button'); + button?.remove(); + await elementUpdated(el); + + // Should now have helpers + helpers = el.shadowRoot.querySelectorAll('.visually-hidden'); + expect(helpers.length).to.equal(2); + }); + + it('removes dismiss helpers after slot content changes to add buttons', async () => { + const el = await fixture(html` + +

Some content

+
+ `); + await elementUpdated(el); + + // Should have helpers initially + let helpers = el.shadowRoot.querySelectorAll('.visually-hidden'); + expect(helpers.length).to.equal(2); + + // Add a button + const button = document.createElement('sp-button'); + button.textContent = 'Close'; + el.appendChild(button); + await elementUpdated(el); + + // Should no longer have helpers + helpers = el.shadowRoot.querySelectorAll('.visually-hidden'); + expect(helpers.length).to.equal(0); + }); + + it('dismiss helper buttons trigger close when clicked', async () => { + const el = await fixture(html` + +

Content without buttons

+
+ `); + await elementUpdated(el); + + expect(el.open).to.be.true; + + const dismissButton = el.shadowRoot.querySelector( + '.visually-hidden button' + ) as HTMLButtonElement; + expect(dismissButton).to.exist; + + const closed = oneEvent(el, 'close'); + dismissButton.click(); + await closed; + + expect(el.open).to.be.false; + }); + }); }); From 93b829637459b9311adb77a0edd60c22daf4cce1 Mon Sep 17 00:00:00 2001 From: Marissa Huysentruyt Date: Thu, 23 Oct 2025 12:59:18 -0400 Subject: [PATCH 13/19] chore(tray): add changeset --- .changeset/every-worlds-push.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/every-worlds-push.md diff --git a/.changeset/every-worlds-push.md b/.changeset/every-worlds-push.md new file mode 100644 index 00000000000..938db1eb553 --- /dev/null +++ b/.changeset/every-worlds-push.md @@ -0,0 +1,11 @@ +--- +'@spectrum-web-components/tray': minor +--- + +**Added**: Automatic dismiss button detection and visually-hidden helpers for screen reader accessibility + +- **Added**: `` now automatically detects keyboard-accessible dismiss buttons (like ``, ``, or HTML ` + `; } From c563c2dacda4b2a5f4882558377f33c74a2b5444 Mon Sep 17 00:00:00 2001 From: Marissa Huysentruyt Date: Fri, 24 Oct 2025 12:19:41 -0400 Subject: [PATCH 17/19] test(tray): add keyboard accessible test assertion --- 1st-gen/packages/tray/test/tray.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/1st-gen/packages/tray/test/tray.test.ts b/1st-gen/packages/tray/test/tray.test.ts index 9e1781edae0..4eb929afb9b 100644 --- a/1st-gen/packages/tray/test/tray.test.ts +++ b/1st-gen/packages/tray/test/tray.test.ts @@ -106,7 +106,7 @@ describe('Tray', () => { }); describe('Dismiss helpers', () => { - it('renders visually-hidden dismiss helpers when no buttons detected', async () => { + it('renders visually-hidden, keyboard-accessible dismiss helpers when no buttons detected', async () => { const el = await fixture(html` @@ -125,7 +125,7 @@ describe('Tray', () => { ); expect(buttons.length).to.equal(2); expect(buttons[0].getAttribute('aria-label')).to.equal('Dismiss'); - expect(buttons[0].getAttribute('tabindex')).to.equal('-1'); + expect(buttons[0].getAttribute('tabindex')).to.be.null; }); it('does not render dismiss helpers when sp-button is detected', async () => { From 6662f31948afbaddcb9b59c91f332140889102c0 Mon Sep 17 00:00:00 2001 From: Marissa Huysentruyt Date: Fri, 24 Oct 2025 12:21:18 -0400 Subject: [PATCH 18/19] docs(tray): update docs with keyboard nav info --- 1st-gen/packages/tray/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/1st-gen/packages/tray/README.md b/1st-gen/packages/tray/README.md index d435db13af0..e8ad8ca004e 100644 --- a/1st-gen/packages/tray/README.md +++ b/1st-gen/packages/tray/README.md @@ -78,7 +78,6 @@ By default, `` automatically detects whether its slotted content includ These built-in dismiss buttons: - Are visually hidden but accessible to screen readers -- Use `tabindex="-1"` to prevent keyboard tab navigation interference - Allow mobile screen reader users to easily dismiss the tray from either the beginning or end of the content - Are labeled "Dismiss" for clear screen reader announcements @@ -88,7 +87,7 @@ This dismiss helper pattern is also implemented in the [``](https://o Content has no buttons -This example shows the default behavior where the tray automatically detects that the menu content lacks dismiss buttons and renders visually hidden helpers. Screen readers will announce them as "Dismiss, button." +This example shows the default behavior where the tray automatically detects that the menu content lacks dismiss buttons and renders visually hidden helpers. Screen readers will announce them as "Dismiss, button" and these helpers are keyboard accessible. ```html From a084f4e2244083c41cfbed4c38aa5727a2e74ba4 Mon Sep 17 00:00:00 2001 From: Marissa Huysentruyt Date: Fri, 24 Oct 2025 12:48:59 -0400 Subject: [PATCH 19/19] test(tray): add tab and close() keyboard tests --- 1st-gen/packages/tray/test/tray.test.ts | 62 ++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/1st-gen/packages/tray/test/tray.test.ts b/1st-gen/packages/tray/test/tray.test.ts index 4eb929afb9b..e8057320ed4 100644 --- a/1st-gen/packages/tray/test/tray.test.ts +++ b/1st-gen/packages/tray/test/tray.test.ts @@ -106,7 +106,7 @@ describe('Tray', () => { }); describe('Dismiss helpers', () => { - it('renders visually-hidden, keyboard-accessible dismiss helpers when no buttons detected', async () => { + it('renders visually-hidden dismiss helpers when no buttons detected', async () => { const el = await fixture(html` @@ -128,6 +128,66 @@ describe('Tray', () => { expect(buttons[0].getAttribute('tabindex')).to.be.null; }); + it('allows focusing dismiss helper buttons', async () => { + const el = await fixture(html` + +

Content without buttons

+
+ `); + await elementUpdated(el); + + const dismissButton = el.shadowRoot.querySelector( + '.visually-hidden button' + ) as HTMLButtonElement; + expect(dismissButton).to.exist; + + dismissButton.focus(); + await elementUpdated(el); + + expect(document.activeElement).to.equal(el); + expect( + el.shadowRoot.activeElement, + 'dismiss button is focused in shadow root' + ).to.equal(dismissButton); + }); + + it('closes tray when Enter key is pressed on dismiss button', async () => { + const test = await fixture(html` + + +

Content without buttons

+
+
+ `); + + const el = test.querySelector('sp-tray') as Tray; + await nextFrame(); // allows for animation to complete + await nextFrame(); + await elementUpdated(el); + expect(el.open).to.be.true; + + const dismissButton = el.shadowRoot.querySelector( + '.visually-hidden button' + ) as HTMLButtonElement; + + dismissButton.focus(); + await elementUpdated(el); + + const closed = oneEvent(el, 'close'); + const enterEvent = new KeyboardEvent('keydown', { + key: 'Enter', + code: 'Enter', + bubbles: true, + cancelable: true, + }); + dismissButton.dispatchEvent(enterEvent); + // Trigger the default button behavior + dismissButton.click(); + await closed; + + expect(el.open).to.be.false; + }); + it('does not render dismiss helpers when sp-button is detected', async () => { const el = await fixture(html`