Skip to content
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

fix(list-item): drag grid cell should be accessible via arrow keys. #8353

Merged
merged 5 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
59 changes: 37 additions & 22 deletions packages/calcite-components/src/components/list-item/list-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,8 @@ export class ListItem

actionsEndEl: HTMLTableCellElement;

handleGridEl: HTMLTableCellElement;

defaultSlotEl: HTMLSlotElement;

// --------------------------------------------------------------------------
Expand Down Expand Up @@ -365,11 +367,11 @@ export class ListItem
@Method()
async setFocus(): Promise<void> {
await componentFocusable(this);
const { containerEl, contentEl, actionsStartEl, actionsEndEl, parentListEl } = this;
const { containerEl, parentListEl } = this;
const focusIndex = focusMap.get(parentListEl);

if (typeof focusIndex === "number") {
const cells = [actionsStartEl, contentEl, actionsEndEl].filter((el) => el && !el.hidden);
const cells = this.getGridCells();
if (cells[focusIndex]) {
this.focusCell(cells[focusIndex]);
} else {
Expand Down Expand Up @@ -411,13 +413,22 @@ export class ListItem
}

renderDragHandle(): VNode {
return this.dragHandle ? (
<td class={CSS.dragContainer} key="drag-handle-container">
const { label, dragHandle, dragDisabled, setPosition, setSize } = this;

return dragHandle ? (
<td
aria-label={label}
class={CSS.dragContainer}
key="drag-handle-container"
role="gridcell"
// eslint-disable-next-line react/jsx-sort-props -- ref should be last so node attrs/props are in sync (see https://github.com/Esri/calcite-design-system/pull/6530)
ref={(el) => (this.handleGridEl = el)}
>
<calcite-handle
disabled={this.dragDisabled}
label={this.label}
setPosition={this.setPosition}
setSize={this.setSize}
disabled={dragDisabled}
label={label}
setPosition={setPosition}
setSize={setSize}
/>
</td>
) : null;
Expand Down Expand Up @@ -761,16 +772,22 @@ export class ListItem
this.calciteListItemSelect.emit();
};

getGridCells(): HTMLTableCellElement[] {
return [this.handleGridEl, this.actionsStartEl, this.contentEl, this.actionsEndEl].filter(
(el) => el && !el.hidden
);
}

handleItemKeyDown = (event: KeyboardEvent): void => {
if (event.defaultPrevented) {
return;
}

const { key } = event;
const composedPath = event.composedPath();
const { containerEl, contentEl, actionsStartEl, actionsEndEl, open, openable } = this;
const { containerEl, actionsStartEl, actionsEndEl, open, openable } = this;

const cells = [actionsStartEl, contentEl, actionsEndEl].filter((el) => el && !el.hidden);
const cells = this.getGridCells();
const currentIndex = cells.findIndex((cell) => composedPath.includes(cell));

if (
Expand Down Expand Up @@ -817,25 +834,23 @@ export class ListItem
};

focusCell = (focusEl: HTMLTableCellElement, saveFocusIndex = true): void => {
const { contentEl, actionsStartEl, actionsEndEl, parentListEl } = this;
const { parentListEl } = this;

if (saveFocusIndex) {
focusMap.set(parentListEl, null);
}

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);
}
});
this.getGridCells().forEach((tableCell, cellIndex) => {
const tabIndexAttr = "tabindex";
if (tableCell === focusEl) {
focusEl === focusedEl && tableCell.setAttribute(tabIndexAttr, "0");
saveFocusIndex && focusMap.set(parentListEl, cellIndex);
} else {
tableCell.removeAttribute(tabIndexAttr);
}
});

focusedEl?.focus();
};
Expand Down
72 changes: 72 additions & 0 deletions packages/calcite-components/src/components/list/list.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,78 @@ describe("calcite-list", () => {
expect(await isElementFocused(page, "#one")).toBe(true);
expect(await one.getProperty("open")).toBe(false);
});

it("should navigate a draggable list via ArrowRight and ArrowLeft", async () => {
const page = await newE2EPage();
await page.setContent(html`
<calcite-list drag-enabled>
<calcite-list-item id="one" value="one" label="One" description="hello world">
<calcite-action
appearance="transparent"
icon="ellipsis"
text="menu"
label="menu"
slot="actions-end"
></calcite-action>
<calcite-list>
<calcite-list-item id="two" value="two" label="Two" description="hello world">
<calcite-action
appearance="transparent"
icon="ellipsis"
text="menu"
label="menu"
slot="actions-end"
></calcite-action
></calcite-list-item>
</calcite-list>
</calcite-list-item>
</calcite-list>
`);
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, `calcite-handle`, { shadowed: true })).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, `calcite-handle`, { 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", () => {
Expand Down