Skip to content

Commit

Permalink
feat: add radio group functionality to menu items (microsoft#4208)
Browse files Browse the repository at this point in the history
* base radio group functionality

* add menu item tests

* add radio group to storybook

* update scenarios

* add menu nav test

* add checkbox and radio menu item tests

* remove unused attribute

* only emit change when checked changes on radio/checkbox
  • Loading branch information
Stephane Comeau authored Jan 4, 2021
1 parent 0445dd7 commit 89a3930
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const fastMenuItemDefinition: WebComponentDefinition = {
},
{
name: "role",
type: DataType.boolean,
type: DataType.string,
description: "The role attribute",
default: MenuItemRole.menuitem,
values: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,13 @@ <h4>With radio buttons and checkboxes</h4>
<fast-menu-item>Menu item 2</fast-menu-item>
<fast-menu-item>Menu item 3</fast-menu-item>
<fast-divider></fast-divider>
<fast-menu-item role="menuitemradio">Menu item 4</fast-menu-item>
<fast-menu-item role="menuitemradio">Menu item 5</fast-menu-item>
<fast-menu-item role="menuitemcheckbox">Checkbox 1</fast-menu-item>
<fast-menu-item role="menuitemcheckbox">Checkbox 2</fast-menu-item>
<fast-divider></fast-divider>
<fast-menu-item role="menuitemcheckbox">Menu item 4</fast-menu-item>
<fast-menu-item role="menuitemcheckbox">Menu item 5</fast-menu-item>
<fast-menu-item role="menuitemradio">Radio 1.1</fast-menu-item>
<fast-menu-item role="menuitemradio">Radio 1.2</fast-menu-item>
<fast-divider></fast-divider>
<fast-menu-item role="menuitemradio">Radio 2.1</fast-menu-item>
<fast-menu-item role="menuitemradio">Radio 2.2</fast-menu-item>
</fast-menu>
</fast-design-system-provider>
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@
</fast-menu>
</template>

<template title="With checkboxes and radios">
<fast-menu>
<fast-menu-item>Menu item 1</fast-menu-item>
<fast-menu-item>Menu item 2</fast-menu-item>
<fast-divider></fast-divider>
<fast-menu-item role="menuitemcheckbox">Checkbox 1</fast-menu-item>
<fast-menu-item role="menuitemcheckbox">Checkbox 2</fast-menu-item>
<fast-divider></fast-divider>
<fast-menu-item role="menuitemradio">Radio 1.1</fast-menu-item>
<fast-menu-item role="menuitemradio">Radio 1.2</fast-menu-item>
<fast-divider></fast-divider>
<fast-menu-item role="menuitemradio">Radio 2.1</fast-menu-item>
<fast-menu-item role="menuitemradio">Radio 2.2</fast-menu-item>
</fast-menu>
</template>

<template title="With icons">
<fast-menu>
<fast-menu-item>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,56 @@ describe("Menu item", () => {
await disconnect();
});

it("should toggle the aria-checked attribute of checkbox item when clicked", async () => {
const { element, connect, disconnect } = await setup();
element.role = MenuItemRole.menuitemcheckbox;

await connect();

await DOM.nextUpdate();

expect(element.getAttribute("aria-checked")).to.equal(null);

element.click();

await DOM.nextUpdate();

expect(element.getAttribute("aria-checked")).to.equal("true");

element.click();

await DOM.nextUpdate();

expect(element.getAttribute("aria-checked")).to.equal("false");

await disconnect();
});

it("should aria-checked attribute of radio item to true when clicked", async () => {
const { element, connect, disconnect } = await setup();
element.role = MenuItemRole.menuitemradio;

await connect();

await DOM.nextUpdate();

expect(element.getAttribute("aria-checked")).to.equal(null);

element.click();

await DOM.nextUpdate();

expect(element.getAttribute("aria-checked")).to.equal("true");

element.click();

await DOM.nextUpdate();

expect(element.getAttribute("aria-checked")).to.equal("true");

await disconnect();
});

describe("events", () => {
it("should fire an event on click", async () => {
const { element, connect, disconnect } = await setup();
Expand Down
18 changes: 15 additions & 3 deletions packages/web-components/fast-foundation/src/menu-item/menu-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ export class MenuItem extends FASTElement {
*/
@attr
public checked: boolean;
private checkedChanged(oldValue, newValue): void {
if (this.$fastController.isConnected) {
this.$emit("change");
}
}

/**
* @internal
Expand Down Expand Up @@ -81,12 +86,19 @@ export class MenuItem extends FASTElement {

switch (this.role) {
case MenuItemRole.menuitemcheckbox:
case MenuItemRole.menuitemradio:
this.checked = !this.checked;
break;
}

this.$emit("change");
case MenuItemRole.menuitemradio:
if (!this.checked) {
this.checked = true;
}
break;

case MenuItemRole.menuitem:
this.$emit("change");
break;
}
};
}

Expand Down
196 changes: 195 additions & 1 deletion packages/web-components/fast-foundation/src/menu/menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Menu, MenuTemplate as template } from "./index";
import { MenuItem, MenuItemTemplate as itemTemplate } from "../menu-item";
import { fixture } from "../fixture";
import { DOM, customElement, html } from "@microsoft/fast-element";
import { KeyCodes } from "@microsoft/fast-web-utilities";

@customElement({
name: "fast-menu",
Expand All @@ -16,7 +17,18 @@ class FASTMenu extends Menu {}
})
class FASTMenuItem extends MenuItem {}

// TODO: Add tests for keyboard handling
const arrowUpEvent = new KeyboardEvent("keydown", {
key: "ArrowUp",
keyCode: KeyCodes.arrowUp,
bubbles: true,
} as KeyboardEventInit);

const arrowDownEvent = new KeyboardEvent("keydown", {
key: "ArrowDown",
keyCode: KeyCodes.arrowDown,
bubbles: true,
} as KeyboardEventInit);

describe("Menu", () => {
it("should include a role of menu", async () => {
const { element, connect, disconnect } = await fixture<Menu>("fast-menu");
Expand Down Expand Up @@ -115,4 +127,186 @@ describe("Menu", () => {

await disconnect();
});

it("should navigate the menu on arrow up/down keys", async () => {
const { element, connect, disconnect } = await fixture(html<FASTMenu>`
<fast-menu>
<fast-menu-item id="id1">One</fast-menu-item>
<fast-menu-item role="menuitem" id="id2">Two</fast-menu-item>
<div>I put a div in my menu</div>
<fast-menu-item role="menuitemradio" id="id3">Three</fast-menu-item>
<div>I put a div in my menu</div>
<fast-menu-item role="menuitemcheckbox" id="id4">Four</fast-menu-item>
</fast-menu>
`);

await connect();
await DOM.nextUpdate();

const item1 = element.querySelector('[id="id1"]');
(item1 as HTMLElement).focus();
expect(document.activeElement?.id).to.equal("id1");

document.activeElement?.dispatchEvent(arrowDownEvent);
expect(document.activeElement?.id).to.equal("id2");

document.activeElement?.dispatchEvent(arrowDownEvent);
expect(document.activeElement?.id).to.equal("id3");

document.activeElement?.dispatchEvent(arrowDownEvent);
expect(document.activeElement?.id).to.equal("id4");

document.activeElement?.dispatchEvent(arrowDownEvent);
expect(document.activeElement?.id).to.equal("id4");

document.activeElement?.dispatchEvent(arrowUpEvent);
expect(document.activeElement?.id).to.equal("id3");

document.activeElement?.dispatchEvent(arrowUpEvent);
expect(document.activeElement?.id).to.equal("id2");

document.activeElement?.dispatchEvent(arrowUpEvent);
expect(document.activeElement?.id).to.equal("id1");

document.activeElement?.dispatchEvent(arrowUpEvent);
expect(document.activeElement?.id).to.equal("id1");

await disconnect();
});

it("should treat all checkbox menu items as individually selectable items", async () => {
const { element, connect, disconnect } = await fixture(html<FASTMenu>`
<fast-menu>
<fast-menu-item role="menuitemcheckbox" id="id1">One</fast-menu-item>
<fast-menu-item role="menuitemcheckbox" id="id2">Two</fast-menu-item>
<fast-menu-item role="menuitemcheckbox" id="id3">Three</fast-menu-item>
</fast-menu>
`);

await connect();
await DOM.nextUpdate();

const item1 = element.querySelector('[id="id1"]');
const item2 = element.querySelector('[id="id2"]');
const item3 = element.querySelector('[id="id3"]');

expect(item1?.getAttribute("aria-checked")).to.equal(null);
expect(item2?.getAttribute("aria-checked")).to.equal(null);
expect(item3?.getAttribute("aria-checked")).to.equal(null);

(item1 as HTMLElement).click();
await DOM.nextUpdate();
expect(item1?.getAttribute("aria-checked")).to.equal("true");
expect(item2?.getAttribute("aria-checked")).to.equal(null);
expect(item3?.getAttribute("aria-checked")).to.equal(null);

(item2 as HTMLElement).click();
await DOM.nextUpdate();
expect(item1?.getAttribute("aria-checked")).to.equal("true");
expect(item2?.getAttribute("aria-checked")).to.equal("true");
expect(item3?.getAttribute("aria-checked")).to.equal(null);

(item3 as HTMLElement).click();
await DOM.nextUpdate();
expect(item1?.getAttribute("aria-checked")).to.equal("true");
expect(item2?.getAttribute("aria-checked")).to.equal("true");
expect(item3?.getAttribute("aria-checked")).to.equal("true");

(item3 as HTMLElement).click();
await DOM.nextUpdate();
expect(item1?.getAttribute("aria-checked")).to.equal("true");
expect(item2?.getAttribute("aria-checked")).to.equal("true");
expect(item3?.getAttribute("aria-checked")).to.equal("false");

await disconnect();
});

it("should treat all radio menu items as a 'radio group' and limit selection to one item within the group by default", async () => {
const { element, connect, disconnect } = await fixture(html<FASTMenu>`
<fast-menu>
<fast-menu-item role="menuitemradio" id="id1">One</fast-menu-item>
<fast-menu-item role="menuitemradio" id="id2">Two</fast-menu-item>
<fast-menu-item role="menuitemradio" id="id3">Three</fast-menu-item>
</fast-menu>
`);

await connect();
await DOM.nextUpdate();

const item1 = element.querySelector('[id="id1"]');
const item2 = element.querySelector('[id="id2"]');
const item3 = element.querySelector('[id="id3"]');

expect(item1?.getAttribute("aria-checked")).to.equal(null);
expect(item2?.getAttribute("aria-checked")).to.equal(null);
expect(item3?.getAttribute("aria-checked")).to.equal(null);

(item1 as HTMLElement).click();
await DOM.nextUpdate();
expect(item1?.getAttribute("aria-checked")).to.equal("true");
expect(item2?.getAttribute("aria-checked")).to.equal("false");
expect(item3?.getAttribute("aria-checked")).to.equal("false");

(item2 as HTMLElement).click();
await DOM.nextUpdate();
expect(item1?.getAttribute("aria-checked")).to.equal("false");
expect(item2?.getAttribute("aria-checked")).to.equal("true");
expect(item3?.getAttribute("aria-checked")).to.equal("false");

(item3 as HTMLElement).click();
await DOM.nextUpdate();
expect(item1?.getAttribute("aria-checked")).to.equal("false");
expect(item2?.getAttribute("aria-checked")).to.equal("false");
expect(item3?.getAttribute("aria-checked")).to.equal("true");

await disconnect();
});

it("should use elements with role='separator' to divide radio menu items into different radio groups ", async () => {
const { element, connect, disconnect } = await fixture(html<FASTMenu>`
<fast-menu>
<fast-menu-item role="menuitemradio" id="id1">One</fast-menu-item>
<fast-menu-item role="menuitemradio" id="id2">Two</fast-menu-item>
<div role="separator"></div>
<fast-menu-item role="menuitemradio" id="id3">Three</fast-menu-item>
<fast-menu-item role="menuitemradio" id="id4">Four</fast-menu-item>
</fast-menu>
`);

await connect();
await DOM.nextUpdate();

const item1 = element.querySelector('[id="id1"]');
const item2 = element.querySelector('[id="id2"]');
const item3 = element.querySelector('[id="id3"]');
const item4 = element.querySelector('[id="id4"]');

expect(item1?.getAttribute("aria-checked")).to.equal(null);
expect(item2?.getAttribute("aria-checked")).to.equal(null);
expect(item3?.getAttribute("aria-checked")).to.equal(null);
expect(item4?.getAttribute("aria-checked")).to.equal(null);

(item1 as HTMLElement).click();
await DOM.nextUpdate();
expect(item1?.getAttribute("aria-checked")).to.equal("true");
expect(item2?.getAttribute("aria-checked")).to.equal("false");
expect(item3?.getAttribute("aria-checked")).to.equal(null);
expect(item4?.getAttribute("aria-checked")).to.equal(null);

(item2 as HTMLElement).click();
await DOM.nextUpdate();
expect(item1?.getAttribute("aria-checked")).to.equal("false");
expect(item2?.getAttribute("aria-checked")).to.equal("true");
expect(item3?.getAttribute("aria-checked")).to.equal(null);
expect(item4?.getAttribute("aria-checked")).to.equal(null);

(item3 as HTMLElement).click();
await DOM.nextUpdate();
expect(item1?.getAttribute("aria-checked")).to.equal("false");
expect(item2?.getAttribute("aria-checked")).to.equal("true");
expect(item3?.getAttribute("aria-checked")).to.equal("true");
expect(item4?.getAttribute("aria-checked")).to.equal("false");

await disconnect();
});
});
Loading

0 comments on commit 89a3930

Please sign in to comment.