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 961b39793f7..49afdd5ed6c 100644 --- a/packages/calcite-components/src/components/list-item/list-item.tsx +++ b/packages/calcite-components/src/components/list-item/list-item.tsx @@ -12,7 +12,12 @@ import { VNode, Watch, } from "@stencil/core"; -import { getElementDir, slotChangeHasAssignedElement, toAriaBoolean } from "../../utils/dom"; +import { + getElementDir, + getFirstTabbable, + slotChangeHasAssignedElement, + toAriaBoolean, +} from "../../utils/dom"; import { connectInteractive, disconnectInteractive, @@ -344,7 +349,7 @@ export class ListItem const focusIndex = focusMap.get(parentListEl); if (typeof focusIndex === "number") { - const cells = [actionsStartEl, contentEl, actionsEndEl].filter(Boolean); + const cells = [actionsStartEl, contentEl, actionsEndEl].filter((el) => el && !el.hidden); if (cells[focusIndex]) { this.focusCell(cells[focusIndex]); } else { @@ -737,7 +742,7 @@ export class ListItem const composedPath = event.composedPath(); const { containerEl, contentEl, actionsStartEl, actionsEndEl, open, openable } = this; - const cells = [actionsStartEl, contentEl, actionsEndEl].filter(Boolean); + const cells = [actionsStartEl, contentEl, actionsEndEl].filter((el) => el && !el.hidden); const currentIndex = cells.findIndex((cell) => composedPath.includes(cell)); if ( @@ -790,16 +795,20 @@ export class ListItem focusMap.set(parentListEl, null); } - [actionsStartEl, contentEl, actionsEndEl].filter(Boolean).forEach((tableCell, cellIndex) => { - const tabIndexAttr = "tabindex"; - if (tableCell === focusEl) { - tableCell.setAttribute(tabIndexAttr, "0"); - saveFocusIndex && focusMap.set(parentListEl, cellIndex); - } else { - tableCell.removeAttribute(tabIndexAttr); - } - }); + const focusedEl = getFirstTabbable(focusEl); + + [actionsStartEl, contentEl, actionsEndEl] + .filter((el) => el && !el.hidden) + .forEach((tableCell, cellIndex) => { + const tabIndexAttr = "tabindex"; + if (tableCell === focusEl) { + focusEl === focusedEl && tableCell.setAttribute(tabIndexAttr, "0"); + saveFocusIndex && focusMap.set(parentListEl, cellIndex); + } else { + tableCell.removeAttribute(tabIndexAttr); + } + }); - focusEl?.focus(); + focusedEl?.focus(); }; } diff --git a/packages/calcite-components/src/components/list/list.e2e.ts b/packages/calcite-components/src/components/list/list.e2e.ts index d875c5bd247..2a581e52dfc 100755 --- a/packages/calcite-components/src/components/list/list.e2e.ts +++ b/packages/calcite-components/src/components/list/list.e2e.ts @@ -504,6 +504,70 @@ describe("calcite-list", () => { expect(await isElementFocused(page, "calcite-filter", { shadowed: true })).toBe(true); }); + + it("should navigate via ArrowRight and ArrowLeft", 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(); + + const one = await page.find("#one"); + expect(await one.getProperty("open")).toBe(false); + + expect(await isElementFocused(page, "#one")).toBe(true); + + await list.press("ArrowRight"); + + expect(await isElementFocused(page, "#one")).toBe(true); + expect(await one.getProperty("open")).toBe(true); + + await list.press("ArrowRight"); + + expect(await isElementFocused(page, `.${CSS.contentContainer}`, { shadowed: true })).toBe(true); + + await list.press("ArrowRight"); + + expect(await isElementFocused(page, "calcite-action")).toBe(true); + + await list.press("ArrowLeft"); + + expect(await isElementFocused(page, `.${CSS.contentContainer}`, { shadowed: true })).toBe(true); + + await list.press("ArrowLeft"); + + expect(await isElementFocused(page, "#one")).toBe(true); + expect(await one.getProperty("open")).toBe(true); + + await list.press("ArrowLeft"); + + expect(await isElementFocused(page, "#one")).toBe(true); + expect(await one.getProperty("open")).toBe(false); + }); }); describe("drag and drop", () => { diff --git a/packages/calcite-components/src/utils/dom.ts b/packages/calcite-components/src/utils/dom.ts index c6e6e987eba..d6ab2dec7e6 100644 --- a/packages/calcite-components/src/utils/dom.ts +++ b/packages/calcite-components/src/utils/dom.ts @@ -285,16 +285,26 @@ export async function focusElement(el: FocusableElement): Promise { } /** - * Helper to focus the first tabbable element. + * Helper to get the first tabbable element. * * @param {HTMLElement} element The html element containing tabbable elements. + * @returns the first tabbable element. */ -export function focusFirstTabbable(element: HTMLElement): void { +export function getFirstTabbable(element: HTMLElement): HTMLElement { if (!element) { return; } - (tabbable(element, tabbableOptions)[0] || element).focus(); + return (tabbable(element, tabbableOptions)[0] ?? element) as HTMLElement; +} + +/** + * Helper to focus the first tabbable element. + * + * @param {HTMLElement} element The html element containing tabbable elements. + */ +export function focusFirstTabbable(element: HTMLElement): void { + getFirstTabbable(element)?.focus(); } interface GetSlottedOptions {