Skip to content

Commit 26e6c1f

Browse files
authored
fix(cdk-experimental/menu): simplify radio and checkbox item APIs (#24720)
* fix(cdk-experimental/menu): simplify radio and checkbox item APIs * fix(cdk-experimental/menu): Add aria-controls attr to triggers * fixup! fix(cdk-experimental/menu): Add aria-controls attr to triggers * fixup! fix(cdk-experimental/menu): Add aria-controls attr to triggers
1 parent 3a94415 commit 26e6c1f

21 files changed

+125
-511
lines changed

src/cdk-experimental/menu/menu-bar.spec.ts

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -69,39 +69,6 @@ describe('MenuBar', () => {
6969
});
7070
});
7171

72-
describe('radiogroup change events', () => {
73-
let fixture: ComponentFixture<MenuBarRadioGroup>;
74-
let menuItems: CdkMenuItemRadio[];
75-
76-
beforeEach(waitForAsync(() => {
77-
TestBed.configureTestingModule({
78-
imports: [CdkMenuModule],
79-
declarations: [MenuBarRadioGroup],
80-
}).compileComponents();
81-
82-
fixture = TestBed.createComponent(MenuBarRadioGroup);
83-
84-
fixture.detectChanges();
85-
86-
menuItems = fixture.debugElement
87-
.queryAll(By.directive(CdkMenuItemRadio))
88-
.map(element => element.injector.get(CdkMenuItemRadio));
89-
}));
90-
91-
it('should emit on click', () => {
92-
const spy = jasmine.createSpy('cdkMenu change spy');
93-
fixture.debugElement
94-
.query(By.directive(CdkMenuBar))
95-
.injector.get(CdkMenuBar)
96-
.change.subscribe(spy);
97-
98-
menuItems[0].trigger();
99-
100-
expect(spy).toHaveBeenCalledTimes(1);
101-
expect(spy).toHaveBeenCalledWith(menuItems[0]);
102-
});
103-
});
104-
10572
describe('Keyboard handling', () => {
10673
describe('(with ltr layout)', () => {
10774
let fixture: ComponentFixture<MultiMenuWithSubmenu>;
@@ -1145,7 +1112,7 @@ describe('MenuBar', () => {
11451112
template: `
11461113
<ul cdkMenuBar>
11471114
<li role="none">
1148-
<button checked="true" cdkMenuItemRadio>
1115+
<button cdkMenuItemChecked="true" cdkMenuItemRadio>
11491116
first
11501117
</button>
11511118
</li>

src/cdk-experimental/menu/menu-base.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
Directive,
1414
ElementRef,
1515
Inject,
16+
Input,
1617
OnDestroy,
1718
Optional,
1819
QueryList,
@@ -26,9 +27,12 @@ import {MENU_STACK, MenuStack, MenuStackItem} from './menu-stack';
2627
import {Menu} from './menu-interface';
2728
import {PointerFocusTracker} from './pointer-focus-tracker';
2829

30+
let nextId = 0;
31+
2932
@Directive({
3033
host: {
3134
'[tabindex]': '_isInline ? (_hasFocus ? -1 : 0) : null',
35+
'[id]': 'id',
3236
'[attr.aria-orientation]': 'orientation',
3337
'(focus)': 'focusFirstItem()',
3438
'(focusin)': 'menuStack.setHasFocus(true)',
@@ -39,6 +43,8 @@ export abstract class CdkMenuBase
3943
extends CdkMenuGroup
4044
implements Menu, AfterContentInit, OnDestroy
4145
{
46+
@Input() id = `cdk-menu-${nextId++}`;
47+
4248
/**
4349
* Sets the aria-orientation attribute and determines where menus will be opened.
4450
* Does not affect styling/layout.
@@ -73,16 +79,14 @@ export abstract class CdkMenuBase
7379
super();
7480
}
7581

76-
override ngAfterContentInit() {
77-
super.ngAfterContentInit();
82+
ngAfterContentInit() {
7883
this._setKeyManager();
7984
this._subscribeToHasFocus();
8085
this._subscribeToMenuOpen();
8186
this._subscribeToMenuStackClosed();
8287
}
8388

84-
override ngOnDestroy() {
85-
super.ngOnDestroy();
89+
ngOnDestroy() {
8690
this.destroyed.next();
8791
this.destroyed.complete();
8892
}

src/cdk-experimental/menu/menu-group.spec.ts

Lines changed: 5 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {Component, ViewChild} from '@angular/core';
22
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
33
import {By} from '@angular/platform-browser';
44
import {CdkMenuModule} from './menu-module';
5-
import {CdkMenuGroup} from './menu-group';
65
import {CdkMenuItemCheckbox} from './menu-item-checkbox';
76
import {CdkMenuItemRadio} from './menu-item-radio';
87
import {CdkMenuItem} from './menu-item';
@@ -58,14 +57,14 @@ describe('MenuGroup', () => {
5857
}));
5958

6059
it('should change state of sibling menuitemradio in same group', () => {
61-
menuItems[1].trigger();
60+
menuItems[1].trigger({keepOpen: true});
6261

6362
expect(menuItems[1].checked).toBeTrue();
6463
expect(menuItems[0].checked).toBeFalse();
6564
});
6665

6766
it('should not change state of menuitemradio in sibling group', () => {
68-
menuItems[3].trigger();
67+
menuItems[3].trigger({keepOpen: true});
6968

7069
expect(menuItems[3].checked).toBeTrue();
7170
expect(menuItems[0].checked).toBeTrue();
@@ -74,65 +73,12 @@ describe('MenuGroup', () => {
7473
it('should not change radiogroup state with disabled button', () => {
7574
menuItems[1].disabled = true;
7675

77-
menuItems[1].trigger();
76+
menuItems[1].trigger({keepOpen: true});
7877

7978
expect(menuItems[0].checked).toBeTrue();
8079
expect(menuItems[1].checked).toBeFalse();
8180
});
8281
});
83-
84-
describe('change events', () => {
85-
let fixture: ComponentFixture<MenuWithMenuItemsAndRadioGroups>;
86-
let menuItems: CdkMenuItemRadio[];
87-
88-
beforeEach(waitForAsync(() => {
89-
TestBed.configureTestingModule({
90-
imports: [CdkMenuModule],
91-
declarations: [MenuWithMenuItemsAndRadioGroups],
92-
}).compileComponents();
93-
94-
fixture = TestBed.createComponent(MenuWithMenuItemsAndRadioGroups);
95-
fixture.detectChanges();
96-
97-
fixture.componentInstance.trigger.getMenuTrigger()?.toggle();
98-
fixture.detectChanges();
99-
100-
menuItems = fixture.debugElement
101-
.queryAll(By.directive(CdkMenuItemRadio))
102-
.map(element => element.injector.get(CdkMenuItemRadio));
103-
}));
104-
105-
it('should emit from enclosing radio group only', () => {
106-
const spies: jasmine.Spy[] = [];
107-
108-
fixture.debugElement.queryAll(By.directive(CdkMenuGroup)).forEach((group, index) => {
109-
const spy = jasmine.createSpy(`cdkMenuGroup ${index} change spy`);
110-
spies.push(spy);
111-
group.injector.get(CdkMenuGroup).change.subscribe(spy);
112-
});
113-
114-
menuItems[0].trigger();
115-
116-
expect(spies[2]).toHaveBeenCalledTimes(1);
117-
expect(spies[2]).toHaveBeenCalledWith(menuItems[0]);
118-
expect(spies[3]).not.toHaveBeenCalled();
119-
expect(spies[4]).not.toHaveBeenCalled();
120-
});
121-
122-
it('should not emit with click on disabled button', () => {
123-
const spy = jasmine.createSpy('cdkMenuGroup change spy');
124-
125-
fixture.debugElement
126-
.queryAll(By.directive(CdkMenuGroup))[1]
127-
.injector.get(CdkMenuGroup)
128-
.change.subscribe(spy);
129-
menuItems[0].disabled = true;
130-
131-
menuItems[0].trigger();
132-
133-
expect(spy).not.toHaveBeenCalled();
134-
});
135-
});
13682
});
13783

13884
@Component({
@@ -145,7 +91,7 @@ describe('MenuGroup', () => {
14591
<li role="none">
14692
<ul cdkMenuGroup>
14793
<li #first role="none">
148-
<button checked="true" cdkMenuItemCheckbox>
94+
<button cdkMenuItemChecked="true" cdkMenuItemCheckbox>
14995
one
15096
</button>
15197
</li>
@@ -174,7 +120,7 @@ class CheckboxMenu {
174120
<li role="none">
175121
<ul cdkMenuGroup>
176122
<li role="none">
177-
<button checked="true" cdkMenuItemRadio>
123+
<button cdkMenuItemChecked="true" cdkMenuItemRadio>
178124
one
179125
</button>
180126
</li>
@@ -206,45 +152,3 @@ class CheckboxMenu {
206152
class MenuWithMultipleRadioGroups {
207153
@ViewChild(CdkMenuItem) readonly trigger: CdkMenuItem;
208154
}
209-
210-
@Component({
211-
template: `
212-
<div cdkMenuBar>
213-
<button cdkMenuItem [cdkMenuTriggerFor]="panel"></button>
214-
</div>
215-
<ng-template #panel>
216-
<ul cdkMenu>
217-
<li role="none">
218-
<ul cdkMenuGroup>
219-
<li role="none">
220-
<button cdkMenuItemRadio>
221-
one
222-
</button>
223-
</li>
224-
</ul>
225-
</li>
226-
<li role="none">
227-
<ul cdkMenuGroup>
228-
<li role="none">
229-
<button cdkMenuItemRadio>
230-
two
231-
</button>
232-
</li>
233-
</ul>
234-
</li>
235-
<li role="none">
236-
<ul cdkMenuGroup>
237-
<li role="none">
238-
<button cdkMenuItem>
239-
three
240-
</button>
241-
</li>
242-
</ul>
243-
</li>
244-
</ul>
245-
</ng-template>
246-
`,
247-
})
248-
class MenuWithMenuItemsAndRadioGroups {
249-
@ViewChild(CdkMenuItem) readonly trigger: CdkMenuItem;
250-
}

src/cdk-experimental/menu/menu-group.ts

Lines changed: 2 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {
10-
AfterContentInit,
11-
ContentChildren,
12-
Directive,
13-
EventEmitter,
14-
OnDestroy,
15-
Output,
16-
QueryList,
17-
} from '@angular/core';
9+
import {Directive} from '@angular/core';
1810
import {UniqueSelectionDispatcher} from '@angular/cdk/collections';
19-
import {takeUntil} from 'rxjs/operators';
20-
import {CdkMenuItemSelectable} from './menu-item-selectable';
21-
import {CdkMenuItem} from './menu-item';
2211

2312
/**
2413
* Directive which acts as a grouping container for `CdkMenuItem` instances with
@@ -33,43 +22,4 @@ import {CdkMenuItem} from './menu-item';
3322
},
3423
providers: [{provide: UniqueSelectionDispatcher, useClass: UniqueSelectionDispatcher}],
3524
})
36-
export class CdkMenuGroup implements AfterContentInit, OnDestroy {
37-
/** Emits the element when checkbox or radiobutton state changed */
38-
@Output() readonly change: EventEmitter<CdkMenuItem> = new EventEmitter();
39-
40-
/** List of menuitemcheckbox or menuitemradio elements which reside in this group */
41-
@ContentChildren(CdkMenuItemSelectable, {descendants: true})
42-
private readonly _selectableItems: QueryList<CdkMenuItemSelectable>;
43-
44-
/** Emits when the _selectableItems QueryList triggers a change */
45-
private readonly _selectableChanges: EventEmitter<void> = new EventEmitter();
46-
47-
ngAfterContentInit() {
48-
this._registerMenuSelectionListeners();
49-
}
50-
51-
ngOnDestroy() {
52-
this._selectableChanges.next();
53-
this._selectableChanges.complete();
54-
}
55-
56-
/**
57-
* Register the child selectable elements with the change emitter and ensure any new child
58-
* elements do so as well.
59-
*/
60-
private _registerMenuSelectionListeners() {
61-
this._selectableItems.forEach(selectable => this._registerClickListener(selectable));
62-
63-
this._selectableItems.changes.subscribe((selectableItems: QueryList<CdkMenuItemSelectable>) => {
64-
this._selectableChanges.next();
65-
selectableItems.forEach(selectable => this._registerClickListener(selectable));
66-
});
67-
}
68-
69-
/** Register each selectable to emit on the change Emitter when clicked */
70-
private _registerClickListener(selectable: CdkMenuItemSelectable) {
71-
selectable.toggled
72-
.pipe(takeUntil(this._selectableChanges))
73-
.subscribe(() => this.change.next(selectable));
74-
}
75-
}
25+
export class CdkMenuGroup {}

src/cdk-experimental/menu/menu-interface.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export const CDK_MENU = new InjectionToken<Menu>('cdk-menu');
1515

1616
/** Interface which specifies Menu operations and used to break circular dependency issues */
1717
export interface Menu extends MenuStackItem {
18+
id: string;
19+
1820
/** The element the Menu directive is placed on. */
1921
_elementRef: ElementRef<HTMLElement>;
2022

src/cdk-experimental/menu/menu-item-checkbox.spec.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,12 @@ describe('MenuItemCheckbox', () => {
4848
expect(checkboxElement.getAttribute('aria-disabled')).toBe('true');
4949
});
5050

51-
it('should be a button type', () => {
52-
expect(checkboxElement.getAttribute('type')).toBe('button');
53-
});
54-
5551
it('should not have a menu', () => {
5652
expect(checkbox.hasMenu()).toBeFalse();
5753
});
5854

5955
it('should toggle the aria checked attribute', () => {
60-
expect(checkboxElement.getAttribute('aria-checked')).toBeNull();
56+
expect(checkboxElement.getAttribute('aria-checked')).toBe('false');
6157

6258
checkboxElement.click();
6359
fixture.detectChanges();
@@ -84,7 +80,7 @@ describe('MenuItemCheckbox', () => {
8480

8581
it('should emit on clicked emitter when triggered', () => {
8682
const spy = jasmine.createSpy('cdkMenuItemCheckbox clicked spy');
87-
checkbox.toggled.subscribe(spy);
83+
checkbox.triggered.subscribe(spy);
8884

8985
checkbox.trigger();
9086

@@ -93,7 +89,7 @@ describe('MenuItemCheckbox', () => {
9389

9490
it('should not emit on clicked emitter when disabled', () => {
9591
const spy = jasmine.createSpy('cdkMenuItemCheckbox clicked spy');
96-
checkbox.toggled.subscribe(spy);
92+
checkbox.triggered.subscribe(spy);
9793
checkbox.disabled = true;
9894

9995
checkbox.trigger();

src/cdk-experimental/menu/menu-item-checkbox.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,7 @@ import {CdkMenuItem} from './menu-item';
1818
selector: '[cdkMenuItemCheckbox]',
1919
exportAs: 'cdkMenuItemCheckbox',
2020
host: {
21-
'[tabindex]': '_tabindex',
22-
'type': 'button',
2321
'role': 'menuitemcheckbox',
24-
'[attr.aria-checked]': 'checked || null',
25-
'[attr.aria-disabled]': 'disabled || null',
2622
},
2723
providers: [
2824
{provide: CdkMenuItemSelectable, useExisting: CdkMenuItemCheckbox},
@@ -31,8 +27,8 @@ import {CdkMenuItem} from './menu-item';
3127
})
3228
export class CdkMenuItemCheckbox extends CdkMenuItemSelectable {
3329
/** Toggle the checked state of the checkbox. */
34-
override trigger() {
35-
super.trigger();
30+
override trigger(options?: {keepOpen: boolean}) {
31+
super.trigger(options);
3632

3733
if (!this.disabled) {
3834
this.checked = !this.checked;

0 commit comments

Comments
 (0)