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+ state ,
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' ;
@@ -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