Skip to content

Commit

Permalink
fix(list-item): drag grid cell should be accessible via arrow keys. (#…
Browse files Browse the repository at this point in the history
…8353)

**Related Issue:** #8366

## Summary

- Set the drag handle `td` as a grid cell that can be navigated to
- add test
  • Loading branch information
driskull authored Dec 8, 2023
1 parent cef9f43 commit 2718ab3
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 22 deletions.
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) => {
// Only one cell within a list-item should be focusable at a time. Ensures the active cell is focusable.
if (tableCell === focusEl) {
tableCell.tabIndex = focusEl === focusedEl ? 0 : -1;
saveFocusIndex && focusMap.set(parentListEl, cellIndex);
} else {
tableCell.tabIndex = -1;
}
});

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

0 comments on commit 2718ab3

Please sign in to comment.