1- import { booleanAttribute , Directive , ElementRef , HostBinding , Input , OnDestroy , OnInit } from '@angular/core' ;
2- import { Subscription } from 'rxjs' ;
1+ import {
2+ AfterContentInit ,
3+ ContentChildren ,
4+ DestroyRef ,
5+ Directive ,
6+ ElementRef ,
7+ forwardRef ,
8+ HostBinding ,
9+ HostListener ,
10+ inject ,
11+ Input ,
12+ OnInit ,
13+ QueryList
14+ } from '@angular/core' ;
15+ import { FocusKeyManager } from '@angular/cdk/a11y' ;
16+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop' ;
17+ import { tap } from 'rxjs/operators' ;
18+
19+ import { ThemeDirective } from '../../shared/theme.directive' ;
320import { DropdownService } from '../dropdown.service' ;
21+ import { DropdownItemDirective } from '../dropdown-item/dropdown-item.directive' ;
422
523@Directive ( {
624 selector : '[cDropdownMenu]' ,
725 exportAs : 'cDropdownMenu' ,
8- standalone : true
26+ standalone : true ,
27+ hostDirectives : [ { directive : ThemeDirective , inputs : [ 'dark' ] } ]
928} )
10- export class DropdownMenuDirective implements OnInit , OnDestroy {
29+ export class DropdownMenuDirective implements OnInit , AfterContentInit {
1130
12- constructor (
13- public elementRef : ElementRef ,
14- private dropdownService : DropdownService
15- ) { }
31+ readonly #destroyRef: DestroyRef = inject ( DestroyRef ) ;
32+ public readonly elementRef : ElementRef = inject ( ElementRef ) ;
33+ readonly # dropdownService: DropdownService = inject ( DropdownService ) ;
34+ #focusKeyManager ! : FocusKeyManager < DropdownItemDirective > ;
1635
1736 /**
1837 * Set alignment of dropdown menu.
@@ -22,55 +41,81 @@ export class DropdownMenuDirective implements OnInit, OnDestroy {
2241
2342 /**
2443 * Toggle the visibility of dropdown menu component.
25- */
26- @Input ( ) visible = false ;
27-
28- /**
29- * Sets a darker color scheme to match a dark navbar.
3044 * @type boolean
3145 */
32- @Input ( { transform : booleanAttribute } ) dark : string | boolean = false ;
33-
34- private dropdownStateSubscription ! : Subscription ;
46+ @Input ( ) visible : boolean = false ;
3547
36- @HostBinding ( 'class' )
37- get hostClasses ( ) : any {
48+ @HostBinding ( 'class' ) get hostClasses ( ) : any {
3849 return {
39- 'dropdown-menu' : true ,
40- 'dropdown-menu-dark' : this . dark ,
41- [ `dropdown-menu-${ this . alignment } ` ] : ! ! this . alignment ,
42- show : this . visible
50+ 'dropdown-menu' : true , [ `dropdown-menu-${ this . alignment } ` ] : ! ! this . alignment , show : this . visible
4351 } ;
4452 }
4553
46- @HostBinding ( 'style' )
47- get hostStyles ( ) {
54+ @HostBinding ( 'style' ) get hostStyles ( ) {
4855 // workaround for popper position calculate (see also: dropdown.component)
4956 return {
50- visibility : this . visible ? null : '' ,
51- display : this . visible ? null : ''
57+ visibility : this . visible ? null : '' , display : this . visible ? null : ''
5258 } ;
5359 }
5460
55- ngOnInit ( ) : void {
56- this . dropdownStateSubscribe ( ) ;
61+ @HostListener ( 'keydown' , [ '$event' ] ) onKeyDown ( $event : KeyboardEvent ) : void {
62+ if ( ! this . visible ) {
63+ return ;
64+ }
65+ if ( [ 'Space' , 'ArrowDown' ] . includes ( $event . code ) ) {
66+ $event . preventDefault ( ) ;
67+ }
68+ this . #focusKeyManager. onKeydown ( $event ) ;
5769 }
5870
59- ngOnDestroy ( ) : void {
60- this . dropdownStateSubscribe ( false ) ;
71+ @HostListener ( 'keyup' , [ '$event' ] ) onKeyUp ( $event : KeyboardEvent ) : void {
72+ if ( ! this . visible ) {
73+ return ;
74+ }
75+ if ( [ 'Tab' ] . includes ( $event . key ) ) {
76+ if ( this . #focusKeyManager. activeItem ) {
77+ $event . shiftKey ? this . #focusKeyManager. setPreviousItemActive ( ) : this . #focusKeyManager. setNextItemActive ( ) ;
78+ } else {
79+ this . #focusKeyManager. setFirstItemActive ( ) ;
80+ }
81+ }
6182 }
6283
63- private dropdownStateSubscribe ( subscribe : boolean = true ) : void {
64- if ( subscribe ) {
65- this . dropdownStateSubscription =
66- this . dropdownService . dropdownState$ . subscribe ( ( state ) => {
84+ @ContentChildren ( forwardRef ( ( ) => DropdownItemDirective ) , { descendants : true } ) dropdownItemsContent ! : QueryList < DropdownItemDirective > ;
85+
86+ ngAfterContentInit ( ) : void {
87+ this . focusKeyManagerInit ( ) ;
88+
89+ this . dropdownItemsContent . changes
90+ . pipe (
91+ tap ( ( change ) => {
92+ this . focusKeyManagerInit ( ) ;
93+ } ) ,
94+ takeUntilDestroyed ( this . #destroyRef)
95+ ) . subscribe ( ) ;
96+ }
97+
98+ ngOnInit ( ) : void {
99+ this . #dropdownService. dropdownState$
100+ . pipe (
101+ tap ( ( state ) => {
67102 if ( 'visible' in state ) {
68- this . visible =
69- state . visible === 'toggle' ? ! this . visible : state . visible ;
103+ this . visible = state . visible === 'toggle' ? ! this . visible : state . visible ;
104+ if ( ! this . visible ) {
105+ this . #focusKeyManager?. setActiveItem ( - 1 ) ;
106+ }
70107 }
71- } ) ;
72- } else {
73- this . dropdownStateSubscription ?. unsubscribe ( ) ;
74- }
108+ } ) ,
109+ takeUntilDestroyed ( this . #destroyRef)
110+ ) . subscribe ( ) ;
111+ }
112+
113+ private focusKeyManagerInit ( ) : void {
114+ this . #focusKeyManager = new FocusKeyManager ( this . dropdownItemsContent )
115+ . withHomeAndEnd ( )
116+ . withPageUpDown ( )
117+ . withWrap ( )
118+ . skipPredicate ( ( dropdownItem ) => ( dropdownItem . disabled === true ) ) ;
75119 }
120+
76121}
0 commit comments