Skip to content

Commit 7050f17

Browse files
committed
fix(dropdown): add aria-expanded attribute, refactor
1 parent 3150f32 commit 7050f17

File tree

2 files changed

+42
-29
lines changed

2 files changed

+42
-29
lines changed

projects/coreui-angular/src/lib/dropdown/dropdown/dropdown.component.spec.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ describe('DropdownComponent', () => {
1212
beforeEach(async () => {
1313
await TestBed.configureTestingModule({
1414
imports: [DropdownComponent]
15-
})
16-
.compileComponents();
15+
}).compileComponents();
1716
});
1817

1918
beforeEach(() => {
@@ -41,7 +40,6 @@ class MockElementRef extends ElementRef {}
4140
class TestComponent {}
4241

4342
describe('DropdownToggleDirective', () => {
44-
4543
let component: TestComponent;
4644
let fixture: ComponentFixture<TestComponent>;
4745
let elementRef: DebugElement;
@@ -66,7 +64,9 @@ describe('DropdownToggleDirective', () => {
6664
});
6765

6866
it('should create an instance', () => {
69-
const directive = new DropdownToggleDirective(elementRef, service);
70-
expect(directive).toBeTruthy();
67+
TestBed.runInInjectionContext(() => {
68+
const directive = new DropdownToggleDirective();
69+
expect(directive).toBeTruthy();
70+
});
7171
});
7272
});

projects/coreui-angular/src/lib/dropdown/dropdown/dropdown.component.ts

+37-24
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,31 @@
1+
import { DOCUMENT } from '@angular/common';
12
import {
23
AfterContentInit,
34
AfterViewInit,
45
booleanAttribute,
56
ChangeDetectorRef,
67
Component,
78
ContentChild,
9+
DestroyRef,
810
Directive,
911
ElementRef,
1012
EventEmitter,
1113
forwardRef,
1214
HostBinding,
1315
HostListener,
16+
inject,
1417
Inject,
1518
Input,
1619
NgZone,
1720
OnChanges,
1821
OnDestroy,
1922
OnInit,
20-
Optional,
2123
Output,
2224
Renderer2,
25+
signal,
2326
SimpleChanges
2427
} from '@angular/core';
25-
import { DOCUMENT } from '@angular/common';
28+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
2629
import { Subscription } from 'rxjs';
2730
import { filter } from 'rxjs/operators';
2831

@@ -42,12 +45,11 @@ export abstract class DropdownToken {}
4245
standalone: true
4346
})
4447
export class DropdownToggleDirective implements AfterViewInit {
45-
46-
constructor(
47-
public elementRef: ElementRef,
48-
private dropdownService: DropdownService,
49-
@Optional() public dropdown?: DropdownToken
50-
) {}
48+
// injections
49+
readonly #destroyRef = inject(DestroyRef);
50+
public readonly elementRef = inject(ElementRef);
51+
#dropdownService = inject(DropdownService);
52+
public dropdown = inject(DropdownToken, { optional: true });
5153

5254
/**
5355
* Toggle the disabled state for the toggler.
@@ -70,7 +72,8 @@ export class DropdownToggleDirective implements AfterViewInit {
7072
@Input() caret = true;
7173

7274
/**
73-
* Create split button dropdowns with virtually the same markup as single button dropdowns, but with the addition of `.dropdown-toggle-split` class for proper spacing around the dropdown caret.
75+
* Create split button dropdowns with virtually the same markup as single button dropdowns,
76+
* but with the addition of `.dropdown-toggle-split` class for proper spacing around the dropdown caret.
7477
* @type boolean
7578
* @default false
7679
*/
@@ -85,16 +88,29 @@ export class DropdownToggleDirective implements AfterViewInit {
8588
};
8689
}
8790

91+
#ariaExpanded = signal(false);
92+
93+
@HostBinding('attr.aria-expanded')
94+
get ariaExpanded() {
95+
return this.#ariaExpanded();
96+
}
97+
8898
@HostListener('click', ['$event'])
8999
public onClick($event: MouseEvent): void {
90100
$event.preventDefault();
91-
!this.disabled && this.dropdownService.toggle({ visible: 'toggle', dropdown: this.dropdown });
101+
!this.disabled && this.#dropdownService.toggle({ visible: 'toggle', dropdown: this.dropdown });
92102
}
93103

94104
ngAfterViewInit(): void {
95105
if (this.dropdownComponent) {
96106
this.dropdown = this.dropdownComponent;
97-
this.dropdownService = this.dropdownComponent?.dropdownService;
107+
this.#dropdownService = this.dropdownComponent?.dropdownService;
108+
}
109+
if (this.dropdown) {
110+
const dropdown = <DropdownComponent>this.dropdown;
111+
dropdown?.visibleChange?.pipe(takeUntilDestroyed(this.#destroyRef)).subscribe((visible) => {
112+
this.#ariaExpanded.set(visible);
113+
});
98114
}
99115
}
100116
}
@@ -109,7 +125,6 @@ export class DropdownToggleDirective implements AfterViewInit {
109125
hostDirectives: [{ directive: ThemeDirective, inputs: ['dark'] }]
110126
})
111127
export class DropdownComponent implements AfterContentInit, OnChanges, OnDestroy, OnInit {
112-
113128
constructor(
114129
@Inject(DOCUMENT) private document: Document,
115130
private elementRef: ElementRef,
@@ -136,7 +151,8 @@ export class DropdownComponent implements AfterContentInit, OnChanges, OnDestroy
136151
@Input() direction?: 'center' | 'dropup' | 'dropup-center' | 'dropend' | 'dropstart';
137152

138153
/**
139-
* Describes the placement of your component after Popper.js has applied all the modifiers that may have flipped or altered the originally provided placement property.
154+
* Describes the placement of your component after Popper.js has applied all the modifiers
155+
* that may have flipped or altered the originally provided placement property.
140156
* @type Placement
141157
*/
142158
@Input() placement: Placement = 'bottom-start';
@@ -155,7 +171,7 @@ export class DropdownComponent implements AfterContentInit, OnChanges, OnDestroy
155171
@Input()
156172
set popperOptions(value: Partial<Options>) {
157173
this._popperOptions = { ...this._popperOptions, ...value };
158-
};
174+
}
159175

160176
get popperOptions(): Partial<Options> {
161177
let placement = this.placement;
@@ -237,12 +253,10 @@ export class DropdownComponent implements AfterContentInit, OnChanges, OnDestroy
237253
@HostBinding('class')
238254
get hostClasses(): any {
239255
return {
240-
dropdown:
241-
(this.variant === 'dropdown' || this.variant === 'nav-item') &&
242-
!this.direction,
256+
dropdown: (this.variant === 'dropdown' || this.variant === 'nav-item') && !this.direction,
243257
[`${this.direction}`]: !!this.direction,
244258
[`${this.variant}`]: !!this.variant,
245-
'dropup': this.direction === 'dropup' || this.direction === 'dropup-center',
259+
dropup: this.direction === 'dropup' || this.direction === 'dropup-center',
246260
show: this.visible
247261
};
248262
}
@@ -262,16 +276,15 @@ export class DropdownComponent implements AfterContentInit, OnChanges, OnDestroy
262276

263277
dropdownStateSubscribe(subscribe: boolean = true): void {
264278
if (subscribe) {
265-
this.dropdownStateSubscription =
266-
this.dropdownService.dropdownState$.pipe(
279+
this.dropdownStateSubscription = this.dropdownService.dropdownState$
280+
.pipe(
267281
filter((state) => {
268282
return this === state.dropdown;
269283
})
270-
).subscribe((state) => {
284+
)
285+
.subscribe((state) => {
271286
if ('visible' in state) {
272-
state?.visible === 'toggle'
273-
? this.toggleDropdown()
274-
: (this.visible = state.visible);
287+
state?.visible === 'toggle' ? this.toggleDropdown() : (this.visible = state.visible);
275288
}
276289
});
277290
} else {

0 commit comments

Comments
 (0)