Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
e57a387
feat(combobox): add select-all toggle
Elijbet Mar 7, 2025
86806f9
set to
Elijbet Mar 8, 2025
6153643
logic for all items to be selected/deselected when selectAll is toggled
Elijbet Mar 8, 2025
e0bd2e1
when this.selectAllChecked hide all the chips and instead display one…
Elijbet Mar 8, 2025
8fd8f6e
rewrite css in tailwind and calcite tokens
Elijbet Mar 9, 2025
63c5ebe
add this.selectAllKeyDownHandler and prevent single select and single…
Elijbet Mar 9, 2025
dcf1b0f
WIP: logic for ArrowDown
Elijbet Mar 25, 2025
81d7f65
merge dev
Elijbet Apr 4, 2025
d750d43
cleanup
Elijbet Apr 16, 2025
6b870d3
WIP: arrowDown logic
Elijbet Apr 17, 2025
705e1e7
WIP: sub custom input checkbox element with a special combobox item a…
Elijbet Apr 20, 2025
c585981
WIP: cleanup
Elijbet Apr 20, 2025
c8ef92c
this.renderAllSelectedIndicatorChip when this.selectAllEnabled
Elijbet Apr 20, 2025
c04dc1f
merge dev
Elijbet Apr 20, 2025
e6f1c4b
make id more specific
Elijbet Apr 21, 2025
b72225a
only toggle when select all is toggled
Elijbet Apr 21, 2025
9c1f5bb
correct for select all item target
Elijbet Apr 21, 2025
53dfbba
add functionality for indeterminate state
Elijbet Apr 22, 2025
4fe311a
select-all item styling
Elijbet Apr 22, 2025
bfd365e
selectAllEnabled tests: should toggle all items on and off and indete…
Elijbet Apr 23, 2025
61defb1
selectAllEnabledSelected and selectAllEnabledIndeterminate
Elijbet Apr 23, 2025
8fe7e36
cleanup
Elijbet Apr 23, 2025
3404661
cleanup
Elijbet Apr 23, 2025
9ba6ca0
WIP: modify the 2 tests to check for both click and keydown
Elijbet Apr 23, 2025
8685c9a
dry up tests for click and keydown enter press by extracting a common…
Elijbet Apr 23, 2025
065fb33
remove select-all-enabled prop and make Select All checkbox avaialble…
Elijbet Apr 23, 2025
913e4b2
cleanup test: check for prop instead of attrb
Elijbet Apr 23, 2025
617a049
revert the optional select-all-enabled toggle and match refs instead …
Elijbet Apr 24, 2025
e36076d
selectAll messages and shadow token
Elijbet Apr 24, 2025
d67aea4
shorten var names and add a shadow token
Elijbet Apr 24, 2025
dfb11e7
add html for multiple indeterminate select all enabled
Elijbet Apr 24, 2025
9130ec5
cleanup
Elijbet Apr 24, 2025
06a3f6c
deselecting checkboxes after selecting all should update the input wi…
Elijbet Apr 24, 2025
2628b23
WIP: should toggle indeterminate state to All Selected when list item…
Elijbet Apr 24, 2025
8485111
WIP: add logic for Enter key
Elijbet Apr 24, 2025
1b9534a
add updateSelectAllState function to call on initalization, should ha…
Elijbet Apr 25, 2025
70ed074
hide all chips when all selected chip is on, do not render select all…
Elijbet Apr 28, 2025
b6a9de5
use ref instead of id, change function name to reflect that it's retu…
Elijbet Apr 28, 2025
a40ce67
remove role and add label
Elijbet Apr 28, 2025
a5c4c53
cleanup
Elijbet Apr 28, 2025
6a9b2cc
cleanup
Elijbet Apr 29, 2025
41dd343
should bring back all the chips except when one item is deselected
Elijbet Apr 29, 2025
7599e24
clean up HTML
Elijbet Apr 29, 2025
7abde7d
fix label
driskull Apr 29, 2025
610603e
fixes
driskull Apr 29, 2025
c265013
bring back pointer-events-none, remove comments, z-index-sticky
Elijbet Apr 29, 2025
37b79e1
story for all selected
Elijbet Apr 30, 2025
1c72a35
use messages value in tests
Elijbet Apr 30, 2025
5ad6845
use messages value in e2e tests
Elijbet Apr 30, 2025
608cebe
use allSelected getter
Elijbet May 2, 2025
8643e83
isIndeterminate
Elijbet May 5, 2025
cc9a1eb
create a separate private var for items + selectAll item as keyboardN…
Elijbet May 5, 2025
06e0759
ensure bool on isIndeterminate, strengthen css selectors in e2e
Elijbet May 6, 2025
dcb3c64
merge conflicts
Elijbet May 6, 2025
7377df6
revert unrelated
Elijbet May 6, 2025
d3cf34f
remove templating from ICON, rename vars, prevent focus from running …
Elijbet May 6, 2025
82a430e
BEM syntax, accessible vs interactive id for select-all-enabledcheckb…
Elijbet May 7, 2025
b93440e
hidden listitem select-all to be an li tag, additional stateful props…
Elijbet May 7, 2025
d772db7
typing, dry up by using allSelected where applicable, use destructuring
Elijbet May 8, 2025
d30f035
consolidate repeated logic for renderAllSelectedIndicatorChip and ren…
Elijbet May 8, 2025
f89a3d3
simplify toggleSelection logic
Elijbet May 8, 2025
679f133
docs, filtered items as a getter that always excludes hidden
Elijbet May 8, 2025
baa0a9b
cleanup
Elijbet May 8, 2025
9bbf0fa
should update aria-selected on items when toggling 'Select All
Elijbet May 8, 2025
e23ccf2
refactors and cleanups
Elijbet May 9, 2025
f816680
cleanup
Elijbet May 9, 2025
53f7cef
no longer needed to filter out the selectAll ref from selectedItems, …
Elijbet May 9, 2025
5de43a8
use localized message for renderListBoxOptions title
Elijbet May 9, 2025
b6ede7a
bring back ariaLabel to fix the failing test
Elijbet May 9, 2025
18a59b3
add allSelected class for the chip to query in tests and small refactors
Elijbet May 9, 2025
efa765b
renderAllSelectedIndicatorChip to use compactSelectionDisplay
Elijbet May 11, 2025
418b88e
cleanups and refactors
Elijbet May 11, 2025
0586822
cleanup
Elijbet May 11, 2025
1b02ded
cleanup
Elijbet May 12, 2025
eab03c1
delay render() until the strings are ready to make strings available …
Elijbet May 12, 2025
a6af93a
cleanup to pass accessiblity tests
Elijbet May 12, 2025
b3e4f95
adjust test to pass
Elijbet May 12, 2025
4a742e9
refactor - make sure selectAllComboboxItem is not undefined before ad…
Elijbet May 12, 2025
c0301c1
refactor continued
Elijbet May 12, 2025
2308e6a
remove blocking for messages
Elijbet May 12, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
*
* These properties can be overridden using the component's tag as selector.
*
* @prop --calcite-combobox-item-border-color: Specifies the component's border color.
Comment thread
Elijbet marked this conversation as resolved.
* @prop --calcite-combobox-text-color: Specifies the component's text and `icon` color.
* @prop --calcite-combobox-text-color-hover: Specifies the component's text and `icon` color when hovered.
* @prop --calcite-combobox-item-background-color-active: Specifies the component's background color when active.
* @prop --calcite-combobox-item-background-color-hover: Specifies the component's background color when hovered.
* @prop --calcite-combobox-item-shadow: Specifies the component's shadow.

* @prop --calcite-combobox-selected-icon-color: Specifies the component's selected indicator icon color.
* @prop --calcite-combobox-description-text-color: Specifies the component's `description` and `shortHeading` text color.
* @prop --calcite-combobox-description-text-color-press: Specifies the component's `description` and `shortHeading` text color when hovered.
Expand Down Expand Up @@ -118,7 +121,8 @@ ul:focus {
color: var(--calcite-color-border-input);
}

:host([selected]) .icon {
:host([selected]) .icon,
:host([indeterminate]) .icon {
color: var(--calcite-combobox-selected-icon-color, var(--calcite-color-brand));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { getIconScale, warnIfMissingRequiredProp } from "../../utils/component";
import { IconNameOrString } from "../icon/interfaces";
import { slotChangeHasContent } from "../../utils/dom";
import { highlightText } from "../../utils/text";
import { CSS, SLOTS } from "./resources";
import { CSS, ICONS, SLOTS } from "./resources";
import { styles } from "./combobox-item.scss";

declare global {
Expand Down Expand Up @@ -158,6 +158,13 @@ export class ComboboxItem extends LitElement implements InteractiveComponent {
* */
@property({ reflect: true }) itemHidden = false;

/**
* When `selectionMode` is `"multiple"` or `"ancestors"` and one or more, but not all `calcite-combobox-item`s are selected, displays an indeterminate "select all" checkbox.
*
* @private
*/
@property({ reflect: true }) indeterminate = false;

//#endregion

//#region Events
Expand Down Expand Up @@ -279,14 +286,16 @@ export class ComboboxItem extends LitElement implements InteractiveComponent {
shortHeading,
} = this;
const isSingleSelect = isSingleLike(this.selectionMode);
const icon = disabled || isSingleSelect ? undefined : "check-square-f";
const icon = disabled || isSingleSelect ? undefined : ICONS.checked;
const selectionIcon = isSingleSelect
? this.selected
? "circle-inset-large"
: "circle"
: this.selected
? "check-square-f"
: "square";
? ICONS.selectedSingle
: ICONS.circle
: this.indeterminate
? ICONS.indeterminate
: this.selected
? ICONS.checked
: ICONS.unchecked;
const headingText = heading || textLabel;
const itemLabel = label || value;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ export const CSS = {
heading: "heading",
};

export const ICONS = {
checked: "check-square-f",
circle: "circle",
indeterminate: "minus-square-f",
selectedSingle: "circle-inset-large",
unchecked: "square",
};

export const SLOTS = {
contentEnd: "content-end",
contentStart: "content-start",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"all": "All",
"allSelected": "All selected",
"selectAll": "Select All",
"clear": "Clear value",
"removeTag": "Remove tag",
"selected": "selected"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"all": "All",
"allSelected": "All selected",
"selectAll": "Select All",
"clear": "Clear value",
"removeTag": "Remove tag",
"selected": "selected"
Expand Down
231 changes: 229 additions & 2 deletions packages/calcite-components/src/components/combobox/combobox.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,13 @@ describe("calcite-combobox", () => {
</calcite-combobox>`,
);

const item = await page.find("calcite-combobox-item");
await page.waitForChanges();
Comment thread
Elijbet marked this conversation as resolved.

const combobox = await page.find("calcite-combobox");
await combobox.callMethod("componentOnReady");
Comment thread
Elijbet marked this conversation as resolved.
expect(combobox).not.toBeNull();
Comment thread
Elijbet marked this conversation as resolved.

const item = await page.find("calcite-combobox-item#item-0");
let a11yItem = await page.find(`calcite-combobox >>> ul.${CSS.screenReadersOnly} li`);

expect(a11yItem).not.toBeNull();
Expand Down Expand Up @@ -794,7 +800,7 @@ describe("calcite-combobox", () => {
item.setProperty("disabled", true);
await page.waitForChanges();
await page.waitForTimeout(DEBOUNCE.nextTick);
a11yItem = await page.find(`calcite-combobox >>> ul.${CSS.screenReadersOnly} li`);
a11yItem = await page.find(`calcite-combobox >>> ul.${CSS.screenReadersOnly} li:nth-of-type(2)`);
Comment thread
Elijbet marked this conversation as resolved.

expect(a11yItem).toBeNull();
});
Expand Down Expand Up @@ -2975,6 +2981,227 @@ describe("calcite-combobox", () => {
expect((await combobox.getProperty("selectedItems")).length).toBe(1);
});

describe("selectAllEnabled", async () => {
let page: E2EPage;

beforeEach(async () => {
page = await newE2EPage();
await page.setContent(
html`<calcite-combobox selection-mode="multiple" select-all-enabled>
<calcite-combobox-item value="Trees" text-label="Trees">
<calcite-combobox-item value="Pine" text-label="Pine">
<calcite-combobox-item value="Pine Nested" text-label="Pine Nested"></calcite-combobox-item>
</calcite-combobox-item>
<calcite-combobox-item value="Sequoia" text-label="Sequoia"></calcite-combobox-item>
</calcite-combobox-item>
<calcite-combobox-item value="Flowers" text-label="Flowers">
<calcite-combobox-item value="Daffodil" text-label="Daffodil"></calcite-combobox-item>
<calcite-combobox-item value="Nasturtium" text-label="Nasturtium"></calcite-combobox-item>
</calcite-combobox-item>
</calcite-combobox>`,
);
await page.waitForChanges();
});

async function testToggleAllItems(
page: E2EPage,
toggleAction: ([selectAll, combobox]: [E2EElement, E2EElement]) => Promise<void>,
): Promise<void> {
const combobox = await page.find("calcite-combobox");
await combobox.click();
expect(await combobox.getProperty("open")).toBe(true);

const selectAll = await page.find(`calcite-combobox >>> .${CSS.selectAll}`);
await toggleAction([selectAll, combobox]);

let allComboboxItems = await findAll(page, "calcite-combobox-item");
for (const item of allComboboxItems) {
expect(await item.getProperty("selected")).toBe(true);
}
expect(await page.find(`calcite-combobox >>> calcite-chip.${CSS.allSelected}`)).toBeDefined();

await toggleAction([selectAll, combobox]);

allComboboxItems = await findAll(page, "calcite-combobox-item");
for (const item of allComboboxItems) {
expect(await item.getProperty("selected")).toBe(false);
}

const chip = await page.find(`calcite-combobox >>> calcite-chip.${CSS.allSelected}`);
expect(chip.classList.contains(`${CSS.chipInvisible}`)).toBe(true);
}

it("should toggle all items on and off with a click", async () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
await testToggleAllItems(page, async ([selectAll, _combobox]) => {
await selectAll.click();
});
});

it("should toggle all items on and off with KeyDown press `enter`", async () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
await testToggleAllItems(page, async ([_selectAll, combobox]) => {
await combobox.press("Enter");
});
});

it("indeterminate state", async () => {
const combobox = await page.find("calcite-combobox");
await combobox.click();
expect(await combobox.getProperty("open")).toBe(true);

await (await combobox.find("calcite-combobox-item[value=Sequoia]")).click();

const selectAll = await page.find(`calcite-combobox >>> calcite-combobox-item.${CSS.selectAll}`);
expect(await selectAll.getProperty("indeterminate")).toBe(true);
expect(await page.find(`calcite-combobox >>> calcite-chip[value=Sequoia]`)).toBeDefined();

await (await combobox.find("calcite-combobox-item[value=Flowers]")).click();

expect(await selectAll.getProperty("indeterminate")).toBe(true);
expect(await page.find(`calcite-combobox >>> calcite-chip[value=Flowers]`)).toBeDefined();

const chip = await page.find(`calcite-combobox >>> calcite-chip.${CSS.allSelected}`);
expect(chip.classList.contains(`${CSS.chipInvisible}`)).toBe(true);

await selectAll.click();
expect(await selectAll.getProperty("indeterminate")).toBe(false);
expect(await selectAll.getProperty("selected")).toBe(true);

expect(await page.find(`calcite-combobox >>> calcite-chip.${CSS.allSelected}`)).toBeDefined();
expect(await page.find(`calcite-combobox >>> calcite-chip[value=Sequoia]`)).toBeNull();
expect(await page.find(`calcite-combobox >>> calcite-chip[value=Flowers]`)).toBeNull();

const allComboboxItems = await findAll(page, "calcite-combobox-item");
for (const item of allComboboxItems) {
expect(await item.getProperty("selected")).toBe(true);
}
});

async function testToggleListItems(
page: E2EPage,
toggleAction: ([listItem, combobox]: [E2EElement, E2EElement]) => Promise<void>,
): Promise<void> {
const messages = await import("./assets/t9n/messages.json");
const combobox = await page.find("calcite-combobox");
await combobox.click();
expect(await combobox.getProperty("open")).toBe(true);

const allComboboxItems = await findAll(page, "calcite-combobox-item");
for (const item of allComboboxItems) {
item.setProperty("selected", true);
}
await page.waitForChanges();
expect(await page.find(`calcite-combobox >>> calcite-chip[value="${messages.allSelected}"]`)).toBeDefined();
Comment thread
Elijbet marked this conversation as resolved.

const listItem = await combobox.find("calcite-combobox-item[value=Sequoia]");
await toggleAction([listItem, combobox]);

const selectAll = await page.find(`calcite-combobox >>> calcite-combobox-item.${CSS.selectAll}`);
expect(await selectAll.getProperty("indeterminate")).toBe(true);
expect(await page.find(`calcite-combobox >>> calcite-chip[value=Sequoia]`)).toBeDefined();

await toggleAction([listItem, combobox]);

expect(await selectAll.getProperty("indeterminate")).toBe(false);
expect(await selectAll.getProperty("selected")).toBe(true);
expect(await page.find(`calcite-combobox >>> calcite-chip[value=Sequoia]`)).toBeNull();

await toggleAction([listItem, combobox]);

expect(await selectAll.getProperty("indeterminate")).toBe(true);
expect(await page.find(`calcite-combobox >>> calcite-chip[value=Sequoia]`)).toBeDefined();
}

it("should toggle indeterminate state to `All Selected` when list items are toggled with a click", async () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
await testToggleListItems(page, async ([listItem, _combobox]) => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not disable this rule. You don't need to specify _combobox if it's not being used:

 await testToggleAllItems(page, async ([selectAll]) => { /* ... */ });

Applies to similar unused var lines.

Copy link
Copy Markdown
Contributor Author

@Elijbet Elijbet May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm passing both because I'm using either/or, right? I have 2 it blocks that call testToggleListItems, to test the click I need the listItem, to test the Enter key, I need the combobox.

await listItem.click();
});
});

it("should toggle indeterminate state to `All Selected` when list items are toggled with a keydown `Enter`", async () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not disable this rule. You don't need to specify _listItem if it's not being used.

 await testToggleAllItems(page, async ([, combobox]) => { /* ... */ });

Applies to similar unused var lines.

await testToggleAllItems(page, async ([_listItem, combobox]) => {
await combobox.press("Enter");
});
});

it("should have indeterminate state when some items are initialized selected", async () => {
page = await newE2EPage();
await page.setContent(
html`<calcite-combobox selection-mode="multiple" select-all-enabled>
<calcite-combobox-item value="Trees" text-label="Trees" selected>
<calcite-combobox-item value="Pine" text-label="Pine" />
</calcite-combobox-item>
</calcite-combobox>`,
);
await page.waitForChanges();
const selectAll = await page.find(`calcite-combobox >>> calcite-combobox-item.${CSS.selectAll}`);
expect(await selectAll.getProperty("indeterminate")).toBe(true);
});

it("should have selectAll state true when all items are initialized selected", async () => {
page = await newE2EPage();
await page.setContent(
html`<calcite-combobox selection-mode="multiple" select-all-enabled>
<calcite-combobox-item value="Trees" text-label="Trees" selected>
<calcite-combobox-item value="Pine" text-label="Pine" selected />
</calcite-combobox-item>
</calcite-combobox>`,
);
await page.waitForChanges();
const selectAll = await page.find(`calcite-combobox >>> calcite-combobox-item.${CSS.selectAll}`);
expect(await selectAll.getProperty("selected")).toBe(true);
});

it("should bring back all the chips except `All Selected` when one item is deselected", async () => {
page = await newE2EPage();
await page.setContent(
html`<calcite-combobox selection-mode="multiple" select-all-enabled>
<calcite-combobox-item value="Trees" text-label="Trees" selected>
<calcite-combobox-item value="Pine" text-label="Maple" selected />
<calcite-combobox-item value="Pine" text-label="Pine" selected />
</calcite-combobox-item>
</calcite-combobox>`,
);
await page.waitForChanges();
const messages = await import("./assets/t9n/messages.json");

const combobox = await page.find("calcite-combobox");
await combobox.click();
await page.waitForChanges();

const listItem = await combobox.find("calcite-combobox-item[value=Pine]");
await listItem.click();

expect(await page.find(`calcite-combobox >>> calcite-chip[value="Trees"]`)).toBeDefined();
expect(await page.find(`calcite-combobox >>> calcite-chip[value="Maple"]`)).toBeDefined();
expect(await page.find(`calcite-combobox >>> calcite-chip[value="${messages.allSelected}"]`)).toBeNull();
});

it("should update aria-selected on items when toggling 'Select All'", async () => {
const combobox = await page.find("calcite-combobox");
await combobox.click();

const selectAll = await page.find(`calcite-combobox >>> calcite-combobox-item.${CSS.selectAll}`);
await selectAll.click();
await page.waitForChanges();

let a11yItem = await page.find(`calcite-combobox >>> ul.${CSS.screenReadersOnly} li:nth-of-type(2)`);
expect(await a11yItem.getProperty("ariaSelected")).toBe("true");

a11yItem = await page.find(`calcite-combobox >>> ul.${CSS.screenReadersOnly} li:nth-of-type(3)`);
expect(await a11yItem.getProperty("ariaSelected")).toBe("true");

await selectAll.click();
await page.waitForChanges();

a11yItem = await page.find(`calcite-combobox >>> ul.${CSS.screenReadersOnly} li:nth-of-type(2)`);
expect(await a11yItem.getProperty("ariaSelected")).toBe("false");
});
});

describe("theme", () => {
describe("default", () => {
const comboboxHTML = html`<calcite-combobox label="test" max-items="6" open>
Expand Down
14 changes: 12 additions & 2 deletions packages/calcite-components/src/components/combobox/combobox.scss
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@
gap: var(--calcite-internal-combobox-spacing-unit-s);
margin-inline-end: var(--calcite-internal-combobox-spacing-unit-s);

&.selection-display-fit,
&.selection-display-single {
&.selection-display--fit,
&.selection-display--single {
@apply flex-nowrap overflow-hidden;
}
}
Expand Down Expand Up @@ -236,6 +236,16 @@ calcite-chip {
@apply block;
}

.select-all {
background-color: var(--calcite-combobox-item-background-color-active, var(--calcite-color-foreground-1));
border-block-end-color: var(--calcite-combobox-item-border-color, var(--calcite-color-border-3));
border-block-end-style: solid;
border-block-end-width: var(--calcite-border-width-sm);
inset-block-start: 0;
position: sticky;
z-index: var(--calcite-z-index-sticky);
}

@include disabled();
@include x-button(
$background-color: "var(--calcite-close-background-color, var(--calcite-color-foreground-2))",
Expand Down
Loading
Loading