Skip to content

Commit 5d1e987

Browse files
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
1 parent 36dea1c commit 5d1e987

File tree

1 file changed

+96
-3
lines changed

1 file changed

+96
-3
lines changed

packages/tray/src/Tray.ts

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@
1313
import {
1414
CSSResultArray,
1515
html,
16+
nothing,
1617
PropertyValues,
1718
SpectrumElement,
1819
TemplateResult,
1920
} from '@spectrum-web-components/base';
2021
import {
2122
property,
2223
query,
24+
state,
2325
} from '@spectrum-web-components/base/src/decorators.js';
2426
import '@spectrum-web-components/underlay/sp-underlay.js';
2527
import { firstFocusableIn } from '@spectrum-web-components/shared/src/first-focusable-in.js';
@@ -55,6 +57,9 @@ export class Tray extends SpectrumElement {
5557
@query('.tray')
5658
private tray!: HTMLDivElement;
5759

60+
@query('slot')
61+
private slot!: HTMLSlotElement;
62+
5863
public override focus(): void {
5964
const firstFocusable = firstFocusableIn(this);
6065
if (firstFocusable) {
@@ -81,6 +86,16 @@ export class Tray extends SpectrumElement {
8186
}
8287
}
8388

89+
/**
90+
* When set, prevents the tray from rendering visually-hidden dismiss helpers.
91+
* Use this if your slotted content has custom keyboard-accessible dismiss functionality
92+
* that the auto-detection doesn't recognize.
93+
*
94+
* By default, the tray automatically detects buttons in slotted content.
95+
*/
96+
@property({ type: Boolean, attribute: 'has-keyboard-dismiss' })
97+
public hasKeyboardDismissButton = false;
98+
8499
/**
85100
* Returns a visually hidden dismiss button for mobile screen reader accessibility.
86101
* This button is placed before and after tray content to allow mobile screen reader
@@ -98,6 +113,74 @@ export class Tray extends SpectrumElement {
98113
`;
99114
}
100115

116+
/**
117+
* Internal state tracking whether dismiss helpers are needed.
118+
* Automatically updated when slotted content changes.
119+
*/
120+
@state()
121+
private needsDismissHelper = true;
122+
123+
/**
124+
* Check if slotted content has keyboard-accessible dismiss buttons.
125+
* Looks for buttons in light DOM and checks for known components with built-in dismiss.
126+
*/
127+
private checkForDismissButtons(): void {
128+
if (!this.slot) {
129+
this.needsDismissHelper = true;
130+
return;
131+
}
132+
133+
const slottedElements = this.slot.assignedElements({ flatten: true });
134+
135+
if (slottedElements.length === 0) {
136+
this.needsDismissHelper = true;
137+
return;
138+
}
139+
140+
const hasDismissButton = slottedElements.some((element) => {
141+
// Check if element is a button itself
142+
if (
143+
element.tagName === 'SP-BUTTON' ||
144+
element.tagName === 'SP-CLOSE-BUTTON' ||
145+
element.tagName === 'BUTTON'
146+
) {
147+
return true;
148+
}
149+
150+
// Check for dismissable dialog (has built-in dismiss button in shadow DOM)
151+
if (
152+
element.tagName === 'SP-DIALOG' &&
153+
element.hasAttribute('dismissable')
154+
) {
155+
return true;
156+
}
157+
158+
// Check for dismissable dialog-wrapper
159+
if (
160+
element.tagName === 'SP-DIALOG-WRAPPER' &&
161+
element.hasAttribute('dismissable')
162+
) {
163+
return true;
164+
}
165+
166+
// Check for buttons in light DOM (won't see shadow DOM)
167+
const buttons = element.querySelectorAll(
168+
'sp-button, sp-close-button, button'
169+
);
170+
if (buttons.length > 0) {
171+
return true;
172+
}
173+
174+
return false;
175+
});
176+
177+
this.needsDismissHelper = !hasDismissButton;
178+
}
179+
180+
private handleSlotChange(): void {
181+
this.checkForDismissButtons();
182+
}
183+
101184
private dispatchClosed(): void {
102185
this.dispatchEvent(
103186
new Event('close', {
@@ -119,6 +202,12 @@ export class Tray extends SpectrumElement {
119202
}
120203
}
121204

205+
protected override firstUpdated(changes: PropertyValues<this>): void {
206+
super.firstUpdated(changes);
207+
// Run initial button detection
208+
this.checkForDismissButtons();
209+
}
210+
122211
protected override update(changes: PropertyValues<this>): void {
123212
if (
124213
changes.has('open') &&
@@ -148,9 +237,13 @@ export class Tray extends SpectrumElement {
148237
tabindex="-1"
149238
@transitionend=${this.handleTrayTransitionend}
150239
>
151-
${this.dismissHelper}
152-
<slot></slot>
153-
${this.dismissHelper}
240+
${!this.hasKeyboardDismissButton && this.needsDismissHelper
241+
? this.dismissHelper
242+
: nothing}
243+
<slot @slotchange=${this.handleSlotChange}></slot>
244+
${!this.hasKeyboardDismissButton && this.needsDismissHelper
245+
? this.dismissHelper
246+
: nothing}
154247
</div>
155248
`;
156249
}

0 commit comments

Comments
 (0)