Skip to content

feat(cdk-experimental/menu): Implement grouping logic for menuitems #19628

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jun 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions goldens/ts-circular-deps.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@
"src/cdk-experimental/dialog/dialog-config.ts",
"src/cdk-experimental/dialog/dialog-container.ts"
],
[
"src/cdk-experimental/menu/menu-item.ts",
"src/cdk-experimental/menu/menu-panel.ts",
"src/cdk-experimental/menu/menu.ts"
],
[
"src/cdk-experimental/popover-edit/edit-event-dispatcher.ts",
"src/cdk-experimental/popover-edit/edit-ref.ts"
Expand Down
19 changes: 18 additions & 1 deletion src/cdk-experimental/menu/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
load("//tools:defaults.bzl", "ng_module")
load("//tools:defaults.bzl", "ng_module", "ng_test_library", "ng_web_test_suite")

package(default_visibility = ["//visibility:public"])

Expand All @@ -15,3 +15,20 @@ ng_module(
"@npm//rxjs",
],
)

ng_test_library(
name = "unit_test_sources",
srcs = glob(
["**/*.spec.ts"],
exclude = ["**/*.e2e.spec.ts"],
),
deps = [
":menu",
"@npm//@angular/platform-browser",
],
)

ng_web_test_suite(
name = "unit_tests",
deps = [":unit_test_sources"],
)
88 changes: 88 additions & 0 deletions src/cdk-experimental/menu/menu-bar.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {ComponentFixture, TestBed, async} from '@angular/core/testing';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized you test a lot of the same functionality in the tests for menu-group. Since menu-bar extends menu-group, I don't think you need to repeat the same tests here -- this suite can focus on tests specific to menu-bar's extra functionality.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The menu and menu-bar tests are more of a sanity check to ensure that it's wired up correctly The meat of the logic is in the menu-group which contains most of the tests.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, just one or two sanity-check tests should be enough -- I don't think you need to go over checkboxes and radio buttons separately. (That said, I initially thought there were more tests in this file, for some reason. Now that I look again, there doesn't seem to be as much overlap. I'm still not used to reviewing on Github...)

import {Component} from '@angular/core';
import {CdkMenuBar} from './menu-bar';
import {CdkMenuModule} from './menu-module';
import {CdkMenuItem} from './menu-item';
import {By} from '@angular/platform-browser';

describe('MenuBar', () => {
describe('as radio group', () => {
let fixture: ComponentFixture<MenuBarRadioGroup>;
let menuItems: CdkMenuItem[];

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [CdkMenuModule],
declarations: [MenuBarRadioGroup],
}).compileComponents();

fixture = TestBed.createComponent(MenuBarRadioGroup);
fixture.detectChanges();

menuItems = fixture.debugElement
.queryAll(By.directive(CdkMenuItem))
.map(element => element.injector.get(CdkMenuItem));
}));

it('should toggle menuitemradio items', () => {
expect(menuItems[0].checked).toBeTrue();
expect(menuItems[1].checked).toBeFalse();

menuItems[1].trigger();

expect(menuItems[0].checked).toBeFalse();
expect(menuItems[1].checked).toBeTrue();
});
});

describe('radiogroup change events', () => {
let fixture: ComponentFixture<MenuBarRadioGroup>;
let menuItems: CdkMenuItem[];

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [CdkMenuModule],
declarations: [MenuBarRadioGroup],
}).compileComponents();

fixture = TestBed.createComponent(MenuBarRadioGroup);

fixture.detectChanges();

menuItems = fixture.debugElement
.queryAll(By.directive(CdkMenuItem))
.map(element => element.injector.get(CdkMenuItem));
}));

it('should emit on click', () => {
const spy = jasmine.createSpy('cdkMenu change spy');
fixture.debugElement
.query(By.directive(CdkMenuBar))
.injector.get(CdkMenuBar)
.change.subscribe(spy);

menuItems[0].trigger();

expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith(menuItems[0]);
});
});
});

@Component({
template: `
<ul cdkMenuBar>
<li role="none">
<button checked="true" role="menuitemradio" cdkMenuItem>
first
</button>
</li>
<li role="none">
<button role="menuitemradio" cdkMenuItem>
second
</button>
</li>
</ul>
`,
})
class MenuBarRadioGroup {}
4 changes: 3 additions & 1 deletion src/cdk-experimental/menu/menu-bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import {Directive, Input} from '@angular/core';
import {CdkMenuGroup} from './menu-group';

/**
* Directive applied to an element which configures it as a MenuBar by setting the appropriate
Expand All @@ -21,8 +22,9 @@ import {Directive, Input} from '@angular/core';
'role': 'menubar',
'[attr.aria-orientation]': 'orientation',
},
providers: [{provide: CdkMenuGroup, useExisting: CdkMenuBar}],
})
export class CdkMenuBar {
export class CdkMenuBar extends CdkMenuGroup {
/**
* Sets the aria-orientation attribute and determines where sub-menus will be opened.
* Does not affect styling/layout.
Expand Down
245 changes: 245 additions & 0 deletions src/cdk-experimental/menu/menu-group.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import {Component} from '@angular/core';
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
import {CdkMenuModule} from './menu-module';
import {CdkMenuGroup} from './menu-group';
import {CdkMenuItem} from './menu-item';
import {CdkMenu} from './menu';
import {By} from '@angular/platform-browser';

describe('MenuGroup', () => {
describe('with MenuItems as checkbox', () => {
let fixture: ComponentFixture<CheckboxMenu>;
let menuItems: CdkMenuItem[];

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [CdkMenuModule],
declarations: [CheckboxMenu],
}).compileComponents();

fixture = TestBed.createComponent(CheckboxMenu);
fixture.detectChanges();

menuItems = fixture.debugElement
.queryAll(By.directive(CdkMenuItem))
.map(e => e.injector.get(CdkMenuItem));
}));

it('should not change state of sibling checked menuitemcheckbox', () => {
menuItems[1].trigger();

expect(menuItems[0].checked).toBeTrue();
});
});

describe('with MenuItems as radio button', () => {
let fixture: ComponentFixture<MenuWithMultipleRadioGroups>;
let menuItems: CdkMenuItem[];

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [CdkMenuModule],
declarations: [MenuWithMultipleRadioGroups],
}).compileComponents();

fixture = TestBed.createComponent(MenuWithMultipleRadioGroups);
fixture.detectChanges();

menuItems = fixture.debugElement
.queryAll(By.directive(CdkMenuItem))
.map(e => e.injector.get(CdkMenuItem));
}));

it('should change state of sibling menuitemradio in same group', () => {
menuItems[1].trigger();

expect(menuItems[1].checked).toBeTrue();
expect(menuItems[0].checked).toBeFalse();
});

it('should not change state of menuitemradio in sibling group', () => {
menuItems[3].trigger();

expect(menuItems[3].checked).toBeTrue();
expect(menuItems[0].checked).toBeTrue();
});

it('should not change radiogroup state with disabled button', () => {
menuItems[1].disabled = true;

menuItems[1].trigger();

expect(menuItems[0].checked).toBeTrue();
expect(menuItems[1].checked).toBeFalse();
});
});

describe('change events', () => {
let fixture: ComponentFixture<MenuWithMenuItemsAndRadioGroups>;
let menu: CdkMenu;
let menuItems: CdkMenuItem[];

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [CdkMenuModule],
declarations: [MenuWithMenuItemsAndRadioGroups],
}).compileComponents();

fixture = TestBed.createComponent(MenuWithMenuItemsAndRadioGroups);
fixture.detectChanges();

menu = fixture.debugElement.query(By.directive(CdkMenu)).injector.get(CdkMenu);

menuItems = fixture.debugElement
.queryAll(By.directive(CdkMenuItem))
.map(element => element.injector.get(CdkMenuItem));
}));

it('should not emit from root menu with nested groups', () => {
const spy = jasmine.createSpy('changeSpy for root menu');
menu.change.subscribe(spy);

menuItems.forEach(menuItem => menuItem.trigger());

expect(spy).toHaveBeenCalledTimes(0);
});

it('should emit from enclosing radio group only', () => {
const spies: jasmine.Spy[] = [];

fixture.debugElement.queryAll(By.directive(CdkMenuGroup)).forEach((group, index) => {
const spy = jasmine.createSpy(`cdkMenuGroup ${index} change spy`);
spies.push(spy);
group.injector.get(CdkMenuGroup).change.subscribe(spy);
});

menuItems[0].trigger();

expect(spies[1]).toHaveBeenCalledTimes(1);
expect(spies[1]).toHaveBeenCalledWith(menuItems[0]);
expect(spies[2]).toHaveBeenCalledTimes(0);
expect(spies[3]).toHaveBeenCalledTimes(0);
});

it('should not emit with click on disabled button', () => {
const spy = jasmine.createSpy('cdkMenuGroup change spy');

fixture.debugElement
.queryAll(By.directive(CdkMenuGroup))[1]
.injector.get(CdkMenuGroup)
.change.subscribe(spy);
menuItems[0].disabled = true;

menuItems[0].trigger();

expect(spy).toHaveBeenCalledTimes(0);
});

it('should not emit on menuitem click', () => {
const spies: jasmine.Spy[] = [];

fixture.debugElement.queryAll(By.directive(CdkMenuGroup)).forEach((group, index) => {
const spy = jasmine.createSpy(`cdkMenuGroup ${index} change spy`);
spies.push(spy);
group.injector.get(CdkMenuGroup).change.subscribe(spy);
});

menuItems[2].trigger();

spies.forEach(spy => expect(spy).toHaveBeenCalledTimes(0));
});
});
});

@Component({
template: `
<ul cdkMenu>
<li role="none">
<ul cdkMenuGroup>
<li #first role="none">
<button checked="true" role="menuitemcheckbox" cdkMenuItem>
one
</button>
</li>
<li role="none">
<button role="menuitemcheckbox" cdkMenuItem>
two
</button>
</li>
</ul>
</li>
</ul>
`,
})
class CheckboxMenu {}

@Component({
template: `
<ul cdkMenu>
<li role="none">
<ul cdkMenuGroup>
<li role="none">
<button checked="true" role="menuitemradio" cdkMenuItem>
one
</button>
</li>
<li role="none">
<button role="menuitemradio" cdkMenuItem>
two
</button>
</li>
</ul>
</li>
<li role="none">
<ul cdkMenuGroup>
<li role="none">
<button role="menuitemradio" cdkMenuItem>
three
</button>
</li>
<li role="none">
<button role="menuitemradio" cdkMenuItem>
four
</button>
</li>
</ul>
</li>
</ul>
`,
})
class MenuWithMultipleRadioGroups {}

@Component({
template: `
<ul cdkMenu>
<li role="none">
<ul cdkMenuGroup>
<li role="none">
<button role="menuitemradio" cdkMenuItem>
one
</button>
</li>
</ul>
</li>
<li role="none">
<ul cdkMenuGroup>
<li role="none">
<button role="menuitemradio" cdkMenuItem>
two
</button>
</li>
</ul>
</li>
<li role="none">
<ul cdkMenuGroup>
<li role="none">
<button cdkMenuItem>
three
</button>
</li>
</ul>
</li>
</ul>
`,
})
class MenuWithMenuItemsAndRadioGroups {}
Loading