1313import {
1414 CSSResultArray ,
1515 html ,
16+ nothing ,
1617 PropertyValues ,
1718 SpectrumElement ,
1819 TemplateResult ,
1920} from '@spectrum-web-components/base' ;
2021import {
2122 property ,
2223 query ,
24+ queryAssignedElements ,
2325} from '@spectrum-web-components/base/src/decorators.js' ;
2426import '@spectrum-web-components/underlay/sp-underlay.js' ;
2527import { firstFocusableIn } from '@spectrum-web-components/shared/src/first-focusable-in.js' ;
@@ -81,6 +83,16 @@ export class Tray extends SpectrumElement {
8183 }
8284 }
8385
86+ /**
87+ * Indicates whether the slotted content has keyboard-accessible dismiss functionality.
88+ * When set, this overrides the automatic button detection behavior.
89+ * - `true`: Content has dismiss buttons, don't render additional visually-hidden helpers
90+ * - `false`: Content lacks dismiss buttons, always render visually-hidden helpers
91+ * - `undefined` (default): Auto-detect by scanning slotted content for buttons
92+ */
93+ @property ( { type : Boolean , attribute : 'has-keyboard-dismiss' } )
94+ public hasKeyboardDismissButton ?: boolean ;
95+
8496 /**
8597 * Returns a visually hidden dismiss button for mobile screen reader accessibility.
8698 * This button is placed before and after tray content to allow mobile screen reader
@@ -98,6 +110,59 @@ export class Tray extends SpectrumElement {
98110 ` ;
99111 }
100112
113+ /**
114+ * Add a state property to track if dismiss buttons are needed
115+ * Set to false if your tray content already includes keyboard-accessible dismiss buttons.
116+ */
117+ @property ( { type : Boolean , attribute : false } )
118+ private needsDismissHelper = true ;
119+
120+ // Track slotted content
121+ @queryAssignedElements ( { flatten : true } )
122+ private slottedContent ! : HTMLElement [ ] ;
123+
124+ /**
125+ * Determines whether to show visually-hidden dismiss helpers.
126+ * Uses manual override if set, otherwise falls back to auto-detection.
127+ */
128+ private get shouldShowDismissHelper ( ) : boolean {
129+ // If explicitly set, use that value (inverted because true means "has buttons")
130+ if ( this . hasKeyboardDismissButton !== undefined ) {
131+ return ! this . hasKeyboardDismissButton ;
132+ }
133+ // Otherwise use auto-detection
134+ return this . needsDismissHelper ;
135+ }
136+
137+ // Check if slotted content has keyboard-accessible dismiss buttons
138+ private checkForDismissButtons ( ) : void {
139+ // Look for common dismiss button patterns
140+ const hasDismissButton = this . slottedContent . some ( ( element ) => {
141+ // Check for buttons at the top level
142+ if (
143+ element . tagName === 'SP-BUTTON' ||
144+ element . tagName === 'SP-CLOSE-BUTTON' ||
145+ element . tagName === 'BUTTON'
146+ )
147+ return true ;
148+
149+ // Check for buttons within the slotted content
150+ const buttons = element . querySelectorAll (
151+ 'sp-button, sp-close-button, button'
152+ ) ;
153+ if ( buttons . length > 0 ) return true ;
154+
155+ return false ;
156+ } ) ;
157+
158+ this . needsDismissHelper = ! hasDismissButton ;
159+ }
160+
161+ // Update when slot content changes
162+ private handleSlotChange ( ) : void {
163+ this . checkForDismissButtons ( ) ;
164+ }
165+
101166 private dispatchClosed ( ) : void {
102167 this . dispatchEvent (
103168 new Event ( 'close' , {
@@ -148,9 +213,9 @@ export class Tray extends SpectrumElement {
148213 tabindex ="-1 "
149214 @transitionend =${ this . handleTrayTransitionend }
150215 >
151- ${ this . dismissHelper }
152- < slot > </ slot >
153- ${ this . dismissHelper }
216+ ${ this . shouldShowDismissHelper ? this . dismissHelper : nothing }
217+ < slot @slotchange = ${ this . handleSlotChange } > </ slot >
218+ ${ this . shouldShowDismissHelper ? this . dismissHelper : nothing }
154219 </ div >
155220 ` ;
156221 }
0 commit comments