Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/dialog/src/DialogBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added dismissHelper here because sp-dialog-wrapper extends sp-dialog-base.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow up: we might elect to change this to a full-fledged sp-close-button that is visually hidden, instead of a plain ol' button tag.

return html`
<div class="visually-hidden">
<button
tabindex="-1"
aria-label="Dismiss"
@click=${this.close}
></button>
</div>
`;
}

protected renderDialog(): TemplateResult {
return html`
<slot></slot>
Expand Down
2 changes: 2 additions & 0 deletions packages/dialog/src/DialogWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export class DialogWrapper extends DialogBase {
}

return html`
${this.dismissHelper}
Copy link
Collaborator Author

@marissahuysentruyt marissahuysentruyt Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first dismissHelper is always shown, regardless of dismissable.

The one at the end of this render function is conditional, so that at most a dialog would only have 2 options for dismissal- a visible close button, and a visually-hidden one for screen readers.

Is that still too redundant for screen readers users when dismissable is true and there's a rendered sp-close-button?

<sp-dialog
?dismissable=${this.dismissable}
dismiss-label=${this.dismissLabel}
Expand Down Expand Up @@ -199,6 +200,7 @@ export class DialogWrapper extends DialogBase {
`
: nothing}
</sp-dialog>
${!this.dismissable ? this.dismissHelper : nothing}
`;
}
}
13 changes: 13 additions & 0 deletions packages/modal/src/modal.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,16 @@
.modal {
overflow: visible;
}

.visually-hidden {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to add the visually hidden styles to modal because I couldn't get the styles through the shadow DOM when these were in dialog.css/spectrum-dialog.css.

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;
}
49 changes: 46 additions & 3 deletions packages/tray/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be zsh now that it's the default on macOS? ✨

yarn add @spectrum-web-components/tray
```

Import the side effectful registration of `<sp-tray>` 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';
```

Expand Down Expand Up @@ -70,3 +70,46 @@ A tray has a single default `slot`.
### Accessibility

`<sp-tray>` 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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we like how this additional documentation sounds, want me to add it to the dialog-wrapper docs as well?


The `<sp-tray>` 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 [`<sp-picker>`](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
<overlay-trigger type="modal">
<sp-button slot="trigger" variant="secondary">
Toggle menu content
</sp-button>
<sp-tray slot="click-content">
<sp-menu style="width: 100%">
<sp-menu-item>Deselect</sp-menu-item>
<sp-menu-item>Select Inverse</sp-menu-item>
<sp-menu-item>Feather...</sp-menu-item>
<sp-menu-item>Select and Mask...</sp-menu-item>
</sp-menu>
</sp-tray>
</overlay-trigger>

<overlay-trigger type="modal">
<sp-button slot="trigger" variant="secondary">
Toggle dialog content
</sp-button>
<sp-tray slot="click-content">
<sp-dialog size="s">
<h2 slot="heading">New messages</h2>
You have 5 new messages.
</sp-dialog>
</sp-tray>
</overlay-trigger>
```
19 changes: 19 additions & 0 deletions packages/tray/src/Tray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
<div class="visually-hidden">
<button
tabindex="-1"
aria-label="Dismiss"
@click=${this.close}
></button>
</div>
`;
}

private dispatchClosed(): void {
this.dispatchEvent(
new Event('close', {
Expand Down Expand Up @@ -131,7 +148,9 @@ export class Tray extends SpectrumElement {
tabindex="-1"
@transitionend=${this.handleTrayTransitionend}
>
${this.dismissHelper}
<slot></slot>
${this.dismissHelper}
</div>
`;
}
Expand Down
1 change: 1 addition & 0 deletions packages/tray/src/tray.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ sp-underlay {
overscroll-behavior: contain;
}

.visually-hidden,
::slotted(.visually-hidden) {
border: 0;
clip: rect(0, 0, 0, 0);
Expand Down
Loading