Skip to content

Commit

Permalink
feat(list): support multiple selection using the shift key (#8301)
Browse files Browse the repository at this point in the history
**Related Issue:** #7966

## Summary

- support multiple selection using the shift key
- updates `calciteListItemSelect` event detail to specify if multiple
selection should occur by adding a `selectMultiple` property that is
true when the shiftKey is pressed.
- stores the last clicked element to reference for selecting items
in-between the next potential shift click.
- disables text selection on list content
- add e2e test
  • Loading branch information
driskull authored Nov 30, 2023
1 parent e8f6b8e commit 79538be
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
var(--calcite-list-item-spacing-indent) * var(--calcite-list-item-spacing-indent-multiplier)
);
}

.container:hover {
@apply bg-foreground-2 cursor-pointer;
}
Expand All @@ -44,6 +45,7 @@

.content-container {
@apply text-color-2
select-none
flex
flex-auto
font-sans
Expand Down
20 changes: 16 additions & 4 deletions packages/calcite-components/src/components/list-item/list-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,15 @@ export class ListItem
*/
@Event({ cancelable: false }) calciteInternalListItemSelect: EventEmitter<void>;

/**
*
* @internal
*/
@Event({ cancelable: false })
calciteInternalListItemSelectMultiple: EventEmitter<{
selectMultiple: boolean;
}>;

/**
*
* @internal
Expand Down Expand Up @@ -724,16 +733,16 @@ export class ListItem
this.open = !this.open;
};

itemClicked = (event: Event): void => {
itemClicked = (event: PointerEvent): void => {
if (event.defaultPrevented) {
return;
}

this.toggleSelected();
this.toggleSelected(event.shiftKey);
this.calciteInternalListItemActive.emit();
};

toggleSelected = (): void => {
toggleSelected = (shiftKey: boolean): void => {
const { selectionMode, selected } = this;

if (this.disabled) {
Expand All @@ -746,6 +755,9 @@ export class ListItem
this.selected = true;
}

this.calciteInternalListItemSelectMultiple.emit({
selectMultiple: shiftKey && selectionMode === "multiple",
});
this.calciteListItemSelect.emit();
};

Expand All @@ -767,7 +779,7 @@ export class ListItem
!composedPath.includes(actionsEndEl)
) {
event.preventDefault();
this.toggleSelected();
this.toggleSelected(event.shiftKey);
} else if (key === "ArrowRight") {
event.preventDefault();
const nextIndex = currentIndex + 1;
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 @@ -252,6 +252,78 @@ describe("calcite-list", () => {
expect(visibleItems.map((item) => item.id)).toEqual(["label-match", "description-match", "value-match"]);
});

it("should support shift click to select multiple items", async () => {
const clickItemContent = (item: HTMLCalciteListItemElement, selector: string) => {
item.shadowRoot.querySelector(selector).dispatchEvent(new MouseEvent("click", { bubbles: true, shiftKey: true }));
};

const page = await newE2EPage();
await page.setContent(html`<calcite-list selection-mode="multiple">
<calcite-list-item id="item-1" label="hello" description="world"></calcite-list-item>
<calcite-list-item id="item-2" label="hello 2" description="world 2"></calcite-list-item>
<calcite-list-item id="item-3" label="hello 3" description="world 3"></calcite-list-item>
<calcite-list-item id="item-4" label="hello 4" description="world 4"></calcite-list-item>
</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("selected")).toBe(false);
expect(await items[1].getProperty("selected")).toBe(false);
expect(await items[2].getProperty("selected")).toBe(false);
expect(await items[3].getProperty("selected")).toBe(false);

const eventSpy = await list.spyOnEvent("calciteListChange");

await items[0].click();

await page.waitForChanges();
await page.waitForTimeout(listDebounceTimeout);
expect(eventSpy).toHaveReceivedEventTimes(1);
expect(await list.getProperty("selectedItems")).toHaveLength(1);

expect(await items[0].getProperty("selected")).toBe(true);
expect(await items[1].getProperty("selected")).toBe(false);
expect(await items[2].getProperty("selected")).toBe(false);
expect(await items[3].getProperty("selected")).toBe(false);

await page.$eval("#item-4", clickItemContent, `.${CSS.contentContainer}`);
await page.waitForChanges();
await page.waitForTimeout(listDebounceTimeout);
expect(eventSpy).toHaveReceivedEventTimes(2);
expect(await list.getProperty("selectedItems")).toHaveLength(4);

expect(await items[0].getProperty("selected")).toBe(true);
expect(await items[1].getProperty("selected")).toBe(true);
expect(await items[2].getProperty("selected")).toBe(true);
expect(await items[3].getProperty("selected")).toBe(true);

await items[3].click();

await page.waitForChanges();
await page.waitForTimeout(listDebounceTimeout);
expect(eventSpy).toHaveReceivedEventTimes(3);
expect(await list.getProperty("selectedItems")).toHaveLength(3);

expect(await items[0].getProperty("selected")).toBe(true);
expect(await items[1].getProperty("selected")).toBe(true);
expect(await items[2].getProperty("selected")).toBe(true);
expect(await items[3].getProperty("selected")).toBe(false);

await page.$eval("#item-1", clickItemContent, `.${CSS.contentContainer}`);
await page.waitForChanges();
await page.waitForTimeout(listDebounceTimeout);
expect(eventSpy).toHaveReceivedEventTimes(4);
expect(await list.getProperty("selectedItems")).toHaveLength(0);

expect(await items[0].getProperty("selected")).toBe(false);
expect(await items[1].getProperty("selected")).toBe(false);
expect(await items[2].getProperty("selected")).toBe(false);
expect(await items[3].getProperty("selected")).toBe(false);
});

it("should update active item on init and click", async () => {
const page = await newE2EPage();
await page.setContent(html`<calcite-list selection-mode="none">
Expand Down
31 changes: 31 additions & 0 deletions packages/calcite-components/src/components/list/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,35 @@ export class List
this.updateSelectedItems();
}

@Listen("calciteInternalListItemSelectMultiple")
handleCalciteInternalListItemSelectMultiple(
event: CustomEvent<{
selectMultiple: boolean;
}>
): void {
if (!!this.parentListEl) {
return;
}

event.stopPropagation();
const { target, detail } = event;
const { enabledListItems, lastSelectedInfo } = this;
const selectedItem = target as HTMLCalciteListItemElement;

if (detail.selectMultiple && !!lastSelectedInfo) {
const currentIndex = enabledListItems.indexOf(selectedItem);
const lastSelectedIndex = enabledListItems.indexOf(lastSelectedInfo.selectedItem);
const startIndex = Math.min(lastSelectedIndex, currentIndex);
const endIndex = Math.max(lastSelectedIndex, currentIndex);

enabledListItems
.slice(startIndex, endIndex + 1)
.forEach((item) => (item.selected = lastSelectedInfo.selected));
} else {
this.lastSelectedInfo = { selectedItem, selected: selectedItem.selected };
}
}

@Listen("calciteInternalListItemChange")
handleCalciteInternalListItemChange(event: CustomEvent): void {
if (!!this.parentListEl) {
Expand Down Expand Up @@ -410,6 +439,8 @@ export class List

private ancestorOfFirstFilteredItem: HTMLCalciteListItemElement;

private lastSelectedInfo: { selectedItem: HTMLCalciteListItemElement; selected: boolean };

// --------------------------------------------------------------------------
//
// Public Methods
Expand Down

0 comments on commit 79538be

Please sign in to comment.