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' ;
3
20
import { DropdownService } from '../dropdown.service' ;
21
+ import { DropdownItemDirective } from '../dropdown-item/dropdown-item.directive' ;
4
22
5
23
@Directive ( {
6
24
selector : '[cDropdownMenu]' ,
7
25
exportAs : 'cDropdownMenu' ,
8
- standalone : true
26
+ standalone : true ,
27
+ hostDirectives : [ { directive : ThemeDirective , inputs : [ 'dark' ] } ]
9
28
} )
10
- export class DropdownMenuDirective implements OnInit , OnDestroy {
29
+ export class DropdownMenuDirective implements OnInit , AfterContentInit {
11
30
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 > ;
16
35
17
36
/**
18
37
* Set alignment of dropdown menu.
@@ -22,55 +41,81 @@ export class DropdownMenuDirective implements OnInit, OnDestroy {
22
41
23
42
/**
24
43
* 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.
30
44
* @type boolean
31
45
*/
32
- @Input ( { transform : booleanAttribute } ) dark : string | boolean = false ;
33
-
34
- private dropdownStateSubscription ! : Subscription ;
46
+ @Input ( ) visible : boolean = false ;
35
47
36
- @HostBinding ( 'class' )
37
- get hostClasses ( ) : any {
48
+ @HostBinding ( 'class' ) get hostClasses ( ) : any {
38
49
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
43
51
} ;
44
52
}
45
53
46
- @HostBinding ( 'style' )
47
- get hostStyles ( ) {
54
+ @HostBinding ( 'style' ) get hostStyles ( ) {
48
55
// workaround for popper position calculate (see also: dropdown.component)
49
56
return {
50
- visibility : this . visible ? null : '' ,
51
- display : this . visible ? null : ''
57
+ visibility : this . visible ? null : '' , display : this . visible ? null : ''
52
58
} ;
53
59
}
54
60
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 ) ;
57
69
}
58
70
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
+ }
61
82
}
62
83
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 ) => {
67
102
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
+ }
70
107
}
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 ) ) ;
75
119
}
120
+
76
121
}
0 commit comments