Skip to content

Commit

Permalink
feat(select): implement select all (#746)
Browse files Browse the repository at this point in the history
Closes #520
  • Loading branch information
ogunb authored Dec 15, 2023
1 parent b251702 commit caf155e
Show file tree
Hide file tree
Showing 4 changed files with 289 additions and 15 deletions.
24 changes: 24 additions & 0 deletions src/components/select/bl-select.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
--popover-position: var(--bl-popover-position, fixed);
}

:host([multiple]:not([hide-select-all])) .select-wrapper {
--menu-height: 290px;
}

:host([size="large"]) .select-wrapper {
--height: var(--bl-size-3xl);
--padding-vertical: var(--bl-size-xs);
Expand Down Expand Up @@ -337,3 +341,23 @@ legend span {
.dirty.invalid .help-text {
display: none;
}

.select-all {
position: sticky;
top: 0;
padding: var(--bl-size-xs) 0;
background: var(--background-color);
z-index: 1;
font: var(--bl-font-title-3-regular);

/* Make sure option focus doesn't overflow */
box-shadow: 10px 0 0 var(--background-color), -10px 0 0 var(--background-color);
}

.select-all::after {
position: absolute;
content: "";
width: 100%;
bottom: 0;
border-bottom: 1px solid var(--bl-color-neutral-lighter);
}
35 changes: 34 additions & 1 deletion src/components/select/bl-select.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,16 @@ export const SelectTemplate = (args) => html`<bl-select
?required=${args.required}
?disabled=${args.disabled}
?success=${args.success}
?hide-select-all=${args.hideSelectAll}
select-all-text=${ifDefined(args.selectAllText)}
size=${ifDefined(args.size)}
help-text=${ifDefined(args.helpText)}
invalid-text=${ifDefined(args.customInvalidText)}
placeholder=${ifDefined(args.placeholder)}
.value=${ifDefined(args.value)}>${
(args.options || defaultOptions).map((option) => html`
<bl-select-option value=${ifDefined(option.value)} ?selected=${
( args.selected || []).includes(option.value) }>${option.label}</bl-select-option>`
( args.selected || []).includes(option.value) } ?disabled=${option.disabled}>${option.label}</bl-select-option>`
)}
</bl-select>`

Expand Down Expand Up @@ -159,6 +161,37 @@ Selected options will be visible on input seperated by commas.
</Story>
</Canvas>

## Select All

The Select component features a 'Select All' option, which is automatically displayed when the `multiple` attribute is enabled. If you wish to hide this option, you can do so by adding the `hide-select-all` attribute to the Select component. Additionally, the text for the 'Select All' option can be customized by using the `select-all-text` attribute. Also 'Select All' feature will not have any effect on disabled options.

<Canvas>
<Story
name="Select All"
args={{ placeholder: "Choose countries", multiple: true, selectAllText: 'Select All Countries', options: [{
label: 'United States',
value: 'us',
}, ...defaultOptions] }}
play={selectOpener}
>
{SelectTemplate.bind({})}
</Story>
<Story
name="Select All with Disabled Options"
args={{ placeholder: "Choose countries", multiple: true, selectAllText: 'Select All Countries', options: [{
label: 'United States',
value: 'us',
disabled: true
}, ...defaultOptions] }}
play={selectOpener}
>
{SelectTemplate.bind({})}
</Story>
<Story name="Select All Hidden" args={{ placeholder: "Choose countries", value: ['nl'], multiple: true, hideSelectAll: true }} play={selectOpener}>
{SelectTemplate.bind({})}
</Story>
</Canvas>

## Clear Button

The select component includes a clear button. Clear button can be displayed by passing `clearable` attribute to the Select component.
Expand Down
140 changes: 140 additions & 0 deletions src/components/select/bl-select.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { sendKeys } from "@web/test-runner-commands";
import BlSelect from "./bl-select";
import BlButton from "../button/bl-button";
import BlCheckbox from "../checkbox-group/checkbox/bl-checkbox";
import "../checkbox-group/checkbox/bl-checkbox";
import type BlSelectOption from "./option/bl-select-option";

describe("bl-select", () => {
Expand Down Expand Up @@ -181,6 +182,31 @@ describe("bl-select", () => {
expect(el.selectedOptions.length).to.equal(0);
expect(el.value).to.null;
});
it("should keep selected disabled options when remove all clicked", async () => {
const el = await fixture<BlSelect>(html`<bl-select multiple>
<bl-select-option value="1" disabled selected>Option 1</bl-select-option>
<bl-select-option value="2" selected>Option 2</bl-select-option>
</bl-select>`);

const removeAll = el.shadowRoot?.querySelector<BlButton>(".remove-all");

setTimeout(() => removeAll?.click());

const event = await oneEvent(el, "bl-select");

expect(el.shadowRoot?.querySelector<BlButton>(".remove-all")).to.not.exist;
expect(event).to.exist;
expect(event.detail).to.deep.eq([
{
selected: true,
text: "Option 1",
value: "1",
}
]);
expect(el.options.length).to.equal(2);
expect(el.selectedOptions.length).to.equal(1);
expect(el.value).to.deep.eq(["1"]);
});
it("should hide remove icon button on single required selection", async () => {
const el = await fixture<BlSelect>(html`<bl-select required>
<bl-select-option value="1">Option 1</bl-select-option>
Expand Down Expand Up @@ -510,4 +536,118 @@ describe("bl-select", () => {
expect((document.activeElement as BlSelectOption).value).to.equal(firstOption?.value);
});
});

describe("select all", () => {
it("should select all options", async () => {
const el = await fixture<BlSelect>(html`<bl-select multiple>
<bl-select-option value="1">Option 1</bl-select-option>
<bl-select-option value="2">Option 2</bl-select-option>
<bl-select-option value="3">Option 3</bl-select-option>
<bl-select-option value="4">Option 4</bl-select-option>
<bl-select-option value="5">Option 5</bl-select-option>
</bl-select>`);


const selectAll = el.shadowRoot!.querySelector<BlCheckbox>(".select-all")!;

setTimeout(() => selectAll.dispatchEvent(
new CustomEvent("bl-checkbox-change", { detail: true }))
);
const event = await oneEvent(el, "bl-select");

expect(event).to.exist;
expect(event.detail.length).to.equal(5);
expect(el.selectedOptions.length).to.equal(5);
});

it("should deselect all options", async () => {
const el = await fixture<BlSelect>(html`<bl-select multiple .value=${["1", "2", "3", "4", "5"]}>
<bl-select-option value="1">Option 1</bl-select-option>
<bl-select-option value="2">Option 2</bl-select-option>
<bl-select-option value="3">Option 3</bl-select-option>
<bl-select-option value="4">Option 4</bl-select-option>
<bl-select-option value="5">Option 5</bl-select-option>
</bl-select>`);

expect(el.selectedOptions.length).to.equal(5);

const selectAll = el.shadowRoot!.querySelector<BlCheckbox>(".select-all")!;

setTimeout(() => selectAll.dispatchEvent(
new CustomEvent("bl-checkbox-change", { detail: false }))
);

const event = await oneEvent(el, "bl-select");

expect(event).to.exist;
expect(event.detail.length).to.equal(0);
expect(el.selectedOptions.length).to.equal(0);
});

it("should not act on disabled options", async () => {
const el = await fixture<BlSelect>(html`<bl-select multiple>
<bl-select-option value="1" disabled>Option 1</bl-select-option>
<bl-select-option value="2">Option 2</bl-select-option>
<bl-select-option value="3">Option 3</bl-select-option>
<bl-select-option value="4">Option 4</bl-select-option>
<bl-select-option value="5">Option 5</bl-select-option>
</bl-select>`);

const selectAll = el.shadowRoot!.querySelector<BlCheckbox>(".select-all")!;

setTimeout(() => selectAll.dispatchEvent(
new CustomEvent("bl-checkbox-change", { detail: true }))
);

const event = await oneEvent(el, "bl-select");

expect(event).to.exist;
expect(event.detail.length).to.equal(4);
expect(el.selectedOptions.length).to.equal(4);
expect(el.selectedOptions[0].value).to.equal("2");
});

it("should display indeterminate state when some options are selected", async () => {
const el = await fixture<BlSelect>(html`<bl-select multiple>
<bl-select-option value="1" selected>Option 1</bl-select-option>
<bl-select-option value="2">Option 2</bl-select-option>
<bl-select-option value="3">Option 3</bl-select-option>
<bl-select-option value="4">Option 4</bl-select-option>
<bl-select-option value="5">Option 5</bl-select-option>
</bl-select>`);

const selectAll = el.shadowRoot!.querySelector<BlCheckbox>(".select-all")!;

expect(selectAll.indeterminate).to.be.true;
expect(selectAll.checked).to.be.false;
});

it('should uncheck "select all" checkbox when all available options are selected', async () => {
const el = await fixture<BlSelect>(html`<bl-select multiple>
<bl-select-option value="1" disabled>Option 1</bl-select-option>
<bl-select-option value="2" selected>Option 2</bl-select-option>
<bl-select-option value="3" selected>Option 3</bl-select-option>
<bl-select-option value="4" selected>Option 4</bl-select-option>
<bl-select-option value="5" selected>Option 5</bl-select-option>
</bl-select>`);

const selectAll = el.shadowRoot!.querySelector<BlCheckbox>(".select-all")!;

expect(selectAll.indeterminate).to.be.true;
expect(selectAll.checked).to.be.false;

setTimeout(() => selectAll.dispatchEvent(
new CustomEvent("bl-checkbox-change", { detail: true }))
);

const event = await oneEvent(el, "bl-select");

expect(event).to.exist;
expect(event.detail.length).to.equal(0);
expect(el.selectedOptions.length).to.equal(0);

expect(selectAll.indeterminate).to.be.false;
expect(selectAll.checked).to.be.false;
});
});
});
Loading

0 comments on commit caf155e

Please sign in to comment.