@@ -3,15 +3,21 @@ import { styleMap } from 'lit/directives/style-map.js';
33import { classMap } from 'lit/directives/class-map.js' ;
44import { customElement } from 'lit/decorators/custom-element.js' ;
55import { property } from 'lit/decorators/property.js' ;
6- import { query } from 'lit/decorators/query.js' ;
6+ import { queryAssignedElements } from 'lit/decorators/query-assigned-elements .js' ;
77
8+ import { FloatingDOMController } from '@patternfly/pfe-core/controllers/floating-dom-controller.js' ;
9+ import { Logger } from '@patternfly/pfe-core/controllers/logger.js' ;
810import { PfDropdownItem } from './pf-dropdown-item.js' ;
911import { PfDropdownMenu } from './pf-dropdown-menu.js' ;
12+ import { getRandomId } from '@patternfly/pfe-core/functions/random.js' ;
1013
1114import '@patternfly/elements/pf-button/pf-button.js' ;
1215
1316import styles from './pf-dropdown.css' ;
14- import { FloatingDOMController } from '@patternfly/pfe-core/controllers/floating-dom-controller.js' ;
17+
18+ function canBeDisabled ( el : HTMLElement ) : el is HTMLElement & { disabled : boolean } {
19+ return 'disabled' in el ;
20+ }
1521
1622export class PfDropdownSelectEvent extends Event {
1723 constructor (
@@ -27,6 +33,8 @@ export class PfDropdownSelectEvent extends Event {
2733 * will trigger a process or navigate to a new location.
2834 *
2935 * @slot - Must contain one or more `<pf-dropdown-item>` or `<pf-dropdown-group>`
36+ * @slot trigger - Custom trigger button
37+ * @slot trigger - menu for custom trigger button
3038 *
3139 * @csspart menu - The dropdown menu wrapper
3240 *
@@ -66,85 +74,138 @@ export class PfDropdown extends LitElement {
6674 */
6775 @property ( { type : Boolean , reflect : true } ) expanded = false ;
6876
69- @query ( 'pf-dropdown-menu' )
70- private _menuElement ! : PfDropdownMenu ;
77+ @queryAssignedElements ( { slot : 'trigger' , flatten : true } )
78+ private _triggerElements ! : HTMLElement [ ] ;
7179
72- get #triggerElement( ) {
73- return this . shadowRoot ?. getElementById ( 'trigger' ) ?? null ;
74- }
80+ @queryAssignedElements ( { slot : 'menu' , flatten : true } )
81+ private _menuElements ! : HTMLElement [ ] ;
82+
83+ #logger = new Logger ( this ) ;
7584
7685 #float = new FloatingDOMController ( this , {
77- content : ( ) => this . _menuElement ,
86+ content : ( ) => this . _menuElements ?. at ( 0 ) ,
7887 } ) ;
7988
89+ protected override async getUpdateComplete ( ) : Promise < boolean > {
90+ const ps = await Promise . all ( [
91+ super . getUpdateComplete ( ) ,
92+ this . _menuElements ?. map ( x => ( x as LitElement ) . updateComplete ) ,
93+ ] ) ;
94+ return ps . every ( x => ! ! x ) ;
95+ }
96+
8097 render ( ) {
8198 const { expanded } = this ;
8299 const { anchor, alignment, styles = { } } = this . #float;
83100 const { disabled } = this ;
84101 return html `
85102 < div class ="${ classMap ( { expanded, [ anchor ?? '' ] : ! ! anchor , [ alignment ?? '' ] : ! ! alignment } ) } "
86- style ="${ styleMap ( styles ) } ">
87- < pf-button id ="trigger "
88- variant ="control "
89- aria-controls ="menu "
90- aria-haspopup ="menu "
91- aria-expanded ="${ String ( expanded ) as 'true' | 'false' } "
92- ?disabled ="${ disabled } "
93- icon ="caret-down "
94- icon-set ="fas "
95- @keydown ="${ this . #onButtonKeydown} "
96- @click ="${ ( ) => this . toggle ( ) } "> Dropdown</ pf-button >
97- < pf-dropdown-menu id ="menu "
98- part ="menu "
99- class ="${ classMap ( { show : expanded } ) } "
100- ?disabled ="${ disabled } "
101- @focusout ="${ this . #onMenuFocusout} "
102- @keydown ="${ this . #onMenuKeydown} "
103- @click ="${ this . #onSelect} ">
104- < slot > </ slot >
105- </ pf-dropdown-menu >
103+ style ="${ styleMap ( styles ) } "
104+ @slotchange ="${ this . #onSlotchange} ">
105+ < slot name ="trigger "
106+ @keydown ="${ this . #onButtonKeydown} "
107+ @click ="${ ( ) => this . toggle ( ) } ">
108+ < pf-button variant ="control "
109+ icon ="caret-down "
110+ icon-set ="fas "> Dropdown</ pf-button >
111+ </ slot >
112+ < slot name ="menu "
113+ class ="${ classMap ( { show : expanded } ) } "
114+ ?hidden ="${ ! this . expanded } "
115+ @focusout ="${ this . #onMenuFocusout} "
116+ @keydown ="${ this . #onMenuKeydown} "
117+ @click ="${ this . #onSelect} ">
118+ < pf-dropdown-menu id ="menu "
119+ part ="menu "
120+ ?disabled ="${ disabled } ">
121+ < slot > </ slot >
122+ </ pf-dropdown-menu >
123+ </ slot >
106124 </ div > ` ;
107125 }
108126
127+ override firstUpdated ( ) {
128+ this . #onSlotchange( ) ;
129+ }
130+
109131 updated ( changed : PropertyValues < this> ) {
110132 if ( changed . has ( 'expanded' ) ) {
111133 this . #expandedChanged( ) ;
112134 }
135+ if ( changed . has ( 'disabled' ) ) {
136+ this . #disabledChanged( ) ;
137+ }
138+ }
139+
140+ #onSlotchange( ) {
141+ const [ trigger ] = this . _triggerElements ;
142+ const [ menu ] = this . _menuElements ;
143+ if ( ! trigger ) {
144+ this . #logger. warn ( 'no trigger found' ) ;
145+ } else if ( ! menu ) {
146+ this . #logger. warn ( 'no menu found' ) ;
147+ } else if ( ! [ trigger , menu ] . map ( x => this . shadowRoot ?. contains ( x ) ) . every ( ( p , _ , a ) => p === a [ 0 ] ) ) {
148+ this . #logger. warn ( 'trigger and menu must be located in the same root' ) ;
149+ } else {
150+ menu . id ||= getRandomId ( 'menu' ) ;
151+ trigger . setAttribute ( 'aria-controls' , menu . id ) ;
152+ trigger . setAttribute ( 'aria-haspopup' , menu . id ) ;
153+ trigger . setAttribute ( 'aria-expanded' , String ( this . expanded ) as 'true' | 'false' ) ;
154+ }
113155 }
114156
115157 async #expandedChanged( ) {
116158 const will = this . expanded ? 'close' : 'open' ;
159+ const [ menu ] = this . _menuElements ;
160+ const [ button ] = this . _triggerElements ;
161+ button . setAttribute ( 'aria-expanded' , `${ String ( this . expanded ) as 'true' | 'false' } ` ) ;
117162 this . dispatchEvent ( new Event ( will ) ) ;
118163 if ( this . expanded ) {
119164 await this . #float. show ( ) ;
120- this . _menuElement . activeItem ?. focus ( ) ;
165+ if ( menu instanceof PfDropdownMenu ) {
166+ menu . activeItem ?. focus ( ) ;
167+ }
121168 } else {
122169 await this . #float. hide ( ) ;
123170 }
124171 }
125172
173+ #disabledChanged( ) {
174+ for ( const el of this . _triggerElements . concat ( this . _menuElements ) ) {
175+ if ( canBeDisabled ( el ) ) {
176+ el . disabled = this . disabled ;
177+ }
178+ }
179+ }
126180
127181 #onSelect( event : KeyboardEvent | Event & { target : PfDropdownItem } ) {
128- const menu = this . _menuElement ;
129- const target = event . target as PfDropdownItem || menu . activeItem ;
130- this . dispatchEvent ( new PfDropdownSelectEvent ( event , `${ target ?. value } ` ) ) ;
131- this . hide ( ) ;
182+ const [ menu ] = this . _menuElements ;
183+ if ( menu instanceof PfDropdownMenu ) {
184+ const target = event . target as PfDropdownItem || menu . activeItem ;
185+ this . dispatchEvent ( new PfDropdownSelectEvent ( event , `${ target ?. value } ` ) ) ;
186+ this . hide ( ) ;
187+ }
132188 }
133189
134190 #onButtonKeydown( event : KeyboardEvent ) {
135191 switch ( event . key ) {
136- case 'ArrowDown' :
137- this . show ( ) ;
192+ case 'ArrowDown' : {
193+ if ( ! this . disabled ) {
194+ this . show ( ) ;
195+ }
196+ }
138197 }
139198 }
140199
141200 #onMenuFocusout( event : FocusEvent ) {
142201 if ( this . expanded ) {
143202 const root = this . getRootNode ( ) ;
203+ const [ menu ] = this . _menuElements ;
144204 if ( root instanceof ShadowRoot ||
145205 root instanceof Document &&
146206 event . relatedTarget instanceof PfDropdownItem &&
147- ! this . _menuElement . items . includes ( event . relatedTarget )
207+ menu instanceof PfDropdownMenu &&
208+ ! menu . items . includes ( event . relatedTarget )
148209 ) {
149210 this . hide ( ) ;
150211 }
@@ -160,7 +221,7 @@ export class PfDropdown extends LitElement {
160221 break ;
161222 case 'Escape' :
162223 this . hide ( ) ;
163- this . #triggerElement ?. focus ( ) ;
224+ this . _triggerElements ?. at ( 0 ) ?. focus ( ) ;
164225 }
165226 }
166227
0 commit comments