diff --git a/packages/calcite-components/src/components/list-item/list-item.tsx b/packages/calcite-components/src/components/list-item/list-item.tsx index 2a640e48c65..aba22ad1ddd 100644 --- a/packages/calcite-components/src/components/list-item/list-item.tsx +++ b/packages/calcite-components/src/components/list-item/list-item.tsx @@ -86,6 +86,11 @@ export class ListItem /** When `true`, hides the component. */ @Prop({ reflect: true, mutable: true }) closed = false; + @Watch("closed") + handleClosedChange(): void { + this.emitCalciteInternalListItemChange(); + } + /** * A description for the component. Displays below the label text. */ @@ -96,6 +101,11 @@ export class ListItem */ @Prop({ reflect: true }) disabled = false; + @Watch("disabled") + handleDisabledChange(): void { + this.emitCalciteInternalListItemChange(); + } + /** * The label text of the component. Displays above the description text. */ @@ -210,6 +220,12 @@ export class ListItem */ @Event({ cancelable: false }) calciteInternalFocusPreviousItem: EventEmitter; + /** + * + * @internal + */ + @Event({ cancelable: false }) calciteInternalListItemChange: EventEmitter; + // -------------------------------------------------------------------------- // // Private Properties @@ -541,6 +557,10 @@ export class ListItem // // -------------------------------------------------------------------------- + private emitCalciteInternalListItemChange(): void { + this.calciteInternalListItemChange.emit(); + } + closeClickHandler = (): void => { this.closed = true; this.calciteListItemClose.emit(); diff --git a/packages/calcite-components/src/components/list/list.e2e.ts b/packages/calcite-components/src/components/list/list.e2e.ts index 4f37de97f68..72b562be576 100755 --- a/packages/calcite-components/src/components/list/list.e2e.ts +++ b/packages/calcite-components/src/components/list/list.e2e.ts @@ -5,6 +5,7 @@ import { newE2EPage } from "@stencil/core/testing"; import { debounceTimeout } from "./resources"; import { CSS } from "../list-item/resources"; import { DEBOUNCE_TIMEOUT as FILTER_DEBOUNCE_TIMEOUT } from "../filter/resources"; +import { isElementFocused } from "../../tests/utils"; const placeholder = placeholderImage({ width: 140, @@ -209,13 +210,14 @@ describe("calcite-list", () => { await page.waitForChanges(); await page.waitForTimeout(listDebounceTimeout); + const list = await page.find("calcite-list"); const items = await page.findAll("calcite-list-item"); expect(await items[0].getProperty("active")).toBe(true); expect(await items[1].getProperty("active")).toBe(false); expect(await items[2].getProperty("active")).toBe(false); - const eventSpy = await page.spyOnEvent("calciteInternalListItemActive"); + const eventSpy = await list.spyOnEvent("calciteInternalListItemActive"); await items[1].click(); @@ -229,13 +231,12 @@ describe("calcite-list", () => { }); it("should prevent de-selection of selected item in single-persist mode", async () => { - const page = await newE2EPage({ - html: html` - - - - ` - }); + const page = await newE2EPage(); + await page.setContent(html` + + + + `); await page.waitForChanges(); await page.waitForTimeout(listDebounceTimeout); @@ -260,13 +261,12 @@ describe("calcite-list", () => { }); it("should correctly allow de-selection and change of selected item in single mode", async () => { - const page = await newE2EPage({ - html: html` - - - - ` - }); + const page = await newE2EPage(); + await page.setContent(html` + + + + `); await page.waitForChanges(); await page.waitForTimeout(listDebounceTimeout); @@ -301,14 +301,13 @@ describe("calcite-list", () => { }); it("should emit calciteListChange on selection change", async () => { - const page = await newE2EPage({ - html: html` - - - - - ` - }); + const page = await newE2EPage(); + await page.setContent(html` + + + + + `); await page.waitForChanges(); const list = await page.find("calcite-list"); const listItemOne = await page.find(`calcite-list-item[value=one]`); @@ -339,4 +338,79 @@ describe("calcite-list", () => { expect(await listItemOne.getProperty("selected")).toBe(false); expect(await list.getProperty("selectedItems")).toHaveLength(0); }); + + describe("keyboard navigation", () => { + it("should navigate via ArrowUp, ArrowDown, Home, and End", async () => { + const page = await newE2EPage(); + await page.setContent(html` + + + + + + + `); + await page.waitForChanges(); + const list = await page.find("calcite-list"); + await list.callMethod("setFocus"); + await page.waitForChanges(); + + await isElementFocused(page, "#one"); + + await list.press("ArrowDown"); + + await isElementFocused(page, "#two"); + + await list.press("ArrowDown"); + + await isElementFocused(page, "#two"); + + await list.press("ArrowUp"); + + await isElementFocused(page, "#one"); + + await list.press("ArrowDown"); + + await isElementFocused(page, "#two"); + + const listItemThree = await page.find("#three"); + listItemThree.setProperty("disabled", false); + await page.waitForChanges(); + await page.waitForTimeout(listDebounceTimeout); + + await list.press("ArrowDown"); + + await isElementFocused(page, "#three"); + + const listItemFour = await page.find("#four"); + listItemFour.setProperty("closed", false); + await page.waitForChanges(); + await page.waitForTimeout(listDebounceTimeout); + + await list.press("ArrowDown"); + + await isElementFocused(page, "#four"); + + await list.press("Home"); + + await isElementFocused(page, "#one"); + + await list.press("End"); + + await isElementFocused(page, "#four"); + }); + }); }); diff --git a/packages/calcite-components/src/components/list/list.tsx b/packages/calcite-components/src/components/list/list.tsx index 427d41eb922..3f8d8cebda3 100755 --- a/packages/calcite-components/src/components/list/list.tsx +++ b/packages/calcite-components/src/components/list/list.tsx @@ -175,6 +175,7 @@ export class List implements InteractiveComponent, LoadableComponent { @Listen("calciteInternalListItemActive") handleCalciteInternalListItemActive(event: CustomEvent): void { + event.stopPropagation(); const target = event.target as HTMLCalciteListItemElement; const { listItems } = this; @@ -190,6 +191,7 @@ export class List implements InteractiveComponent, LoadableComponent { @Listen("calciteInternalListItemSelect") handleCalciteInternalListItemSelect(event: CustomEvent): void { + event.stopPropagation(); const target = event.target as HTMLCalciteListItemElement; const { listItems, selectionMode } = this; @@ -200,8 +202,9 @@ export class List implements InteractiveComponent, LoadableComponent { this.updateSelectedItems(); } - @Listen("calciteListItemClose") - handleCalciteListItemClose(): void { + @Listen("calciteInternalListItemChange") + handleCalciteInternalListItemChange(event: CustomEvent): void { + event.stopPropagation(); this.updateListItems(true); }