Skip to content

Commit caf155e

Browse files
authored
feat(select): implement select all (#746)
Closes #520
1 parent b251702 commit caf155e

File tree

4 files changed

+289
-15
lines changed

4 files changed

+289
-15
lines changed

src/components/select/bl-select.css

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
--popover-position: var(--bl-popover-position, fixed);
3030
}
3131

32+
:host([multiple]:not([hide-select-all])) .select-wrapper {
33+
--menu-height: 290px;
34+
}
35+
3236
:host([size="large"]) .select-wrapper {
3337
--height: var(--bl-size-3xl);
3438
--padding-vertical: var(--bl-size-xs);
@@ -337,3 +341,23 @@ legend span {
337341
.dirty.invalid .help-text {
338342
display: none;
339343
}
344+
345+
.select-all {
346+
position: sticky;
347+
top: 0;
348+
padding: var(--bl-size-xs) 0;
349+
background: var(--background-color);
350+
z-index: 1;
351+
font: var(--bl-font-title-3-regular);
352+
353+
/* Make sure option focus doesn't overflow */
354+
box-shadow: 10px 0 0 var(--background-color), -10px 0 0 var(--background-color);
355+
}
356+
357+
.select-all::after {
358+
position: absolute;
359+
content: "";
360+
width: 100%;
361+
bottom: 0;
362+
border-bottom: 1px solid var(--bl-color-neutral-lighter);
363+
}

src/components/select/bl-select.stories.mdx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,16 @@ export const SelectTemplate = (args) => html`<bl-select
9898
?required=${args.required}
9999
?disabled=${args.disabled}
100100
?success=${args.success}
101+
?hide-select-all=${args.hideSelectAll}
102+
select-all-text=${ifDefined(args.selectAllText)}
101103
size=${ifDefined(args.size)}
102104
help-text=${ifDefined(args.helpText)}
103105
invalid-text=${ifDefined(args.customInvalidText)}
104106
placeholder=${ifDefined(args.placeholder)}
105107
.value=${ifDefined(args.value)}>${
106108
(args.options || defaultOptions).map((option) => html`
107109
<bl-select-option value=${ifDefined(option.value)} ?selected=${
108-
( args.selected || []).includes(option.value) }>${option.label}</bl-select-option>`
110+
( args.selected || []).includes(option.value) } ?disabled=${option.disabled}>${option.label}</bl-select-option>`
109111
)}
110112
</bl-select>`
111113

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

164+
## Select All
165+
166+
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.
167+
168+
<Canvas>
169+
<Story
170+
name="Select All"
171+
args={{ placeholder: "Choose countries", multiple: true, selectAllText: 'Select All Countries', options: [{
172+
label: 'United States',
173+
value: 'us',
174+
}, ...defaultOptions] }}
175+
play={selectOpener}
176+
>
177+
{SelectTemplate.bind({})}
178+
</Story>
179+
<Story
180+
name="Select All with Disabled Options"
181+
args={{ placeholder: "Choose countries", multiple: true, selectAllText: 'Select All Countries', options: [{
182+
label: 'United States',
183+
value: 'us',
184+
disabled: true
185+
}, ...defaultOptions] }}
186+
play={selectOpener}
187+
>
188+
{SelectTemplate.bind({})}
189+
</Story>
190+
<Story name="Select All Hidden" args={{ placeholder: "Choose countries", value: ['nl'], multiple: true, hideSelectAll: true }} play={selectOpener}>
191+
{SelectTemplate.bind({})}
192+
</Story>
193+
</Canvas>
194+
162195
## Clear Button
163196

164197
The select component includes a clear button. Clear button can be displayed by passing `clearable` attribute to the Select component.

src/components/select/bl-select.test.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { sendKeys } from "@web/test-runner-commands";
33
import BlSelect from "./bl-select";
44
import BlButton from "../button/bl-button";
55
import BlCheckbox from "../checkbox-group/checkbox/bl-checkbox";
6+
import "../checkbox-group/checkbox/bl-checkbox";
67
import type BlSelectOption from "./option/bl-select-option";
78

89
describe("bl-select", () => {
@@ -181,6 +182,31 @@ describe("bl-select", () => {
181182
expect(el.selectedOptions.length).to.equal(0);
182183
expect(el.value).to.null;
183184
});
185+
it("should keep selected disabled options when remove all clicked", async () => {
186+
const el = await fixture<BlSelect>(html`<bl-select multiple>
187+
<bl-select-option value="1" disabled selected>Option 1</bl-select-option>
188+
<bl-select-option value="2" selected>Option 2</bl-select-option>
189+
</bl-select>`);
190+
191+
const removeAll = el.shadowRoot?.querySelector<BlButton>(".remove-all");
192+
193+
setTimeout(() => removeAll?.click());
194+
195+
const event = await oneEvent(el, "bl-select");
196+
197+
expect(el.shadowRoot?.querySelector<BlButton>(".remove-all")).to.not.exist;
198+
expect(event).to.exist;
199+
expect(event.detail).to.deep.eq([
200+
{
201+
selected: true,
202+
text: "Option 1",
203+
value: "1",
204+
}
205+
]);
206+
expect(el.options.length).to.equal(2);
207+
expect(el.selectedOptions.length).to.equal(1);
208+
expect(el.value).to.deep.eq(["1"]);
209+
});
184210
it("should hide remove icon button on single required selection", async () => {
185211
const el = await fixture<BlSelect>(html`<bl-select required>
186212
<bl-select-option value="1">Option 1</bl-select-option>
@@ -510,4 +536,118 @@ describe("bl-select", () => {
510536
expect((document.activeElement as BlSelectOption).value).to.equal(firstOption?.value);
511537
});
512538
});
539+
540+
describe("select all", () => {
541+
it("should select all options", async () => {
542+
const el = await fixture<BlSelect>(html`<bl-select multiple>
543+
<bl-select-option value="1">Option 1</bl-select-option>
544+
<bl-select-option value="2">Option 2</bl-select-option>
545+
<bl-select-option value="3">Option 3</bl-select-option>
546+
<bl-select-option value="4">Option 4</bl-select-option>
547+
<bl-select-option value="5">Option 5</bl-select-option>
548+
</bl-select>`);
549+
550+
551+
const selectAll = el.shadowRoot!.querySelector<BlCheckbox>(".select-all")!;
552+
553+
setTimeout(() => selectAll.dispatchEvent(
554+
new CustomEvent("bl-checkbox-change", { detail: true }))
555+
);
556+
const event = await oneEvent(el, "bl-select");
557+
558+
expect(event).to.exist;
559+
expect(event.detail.length).to.equal(5);
560+
expect(el.selectedOptions.length).to.equal(5);
561+
});
562+
563+
it("should deselect all options", async () => {
564+
const el = await fixture<BlSelect>(html`<bl-select multiple .value=${["1", "2", "3", "4", "5"]}>
565+
<bl-select-option value="1">Option 1</bl-select-option>
566+
<bl-select-option value="2">Option 2</bl-select-option>
567+
<bl-select-option value="3">Option 3</bl-select-option>
568+
<bl-select-option value="4">Option 4</bl-select-option>
569+
<bl-select-option value="5">Option 5</bl-select-option>
570+
</bl-select>`);
571+
572+
expect(el.selectedOptions.length).to.equal(5);
573+
574+
const selectAll = el.shadowRoot!.querySelector<BlCheckbox>(".select-all")!;
575+
576+
setTimeout(() => selectAll.dispatchEvent(
577+
new CustomEvent("bl-checkbox-change", { detail: false }))
578+
);
579+
580+
const event = await oneEvent(el, "bl-select");
581+
582+
expect(event).to.exist;
583+
expect(event.detail.length).to.equal(0);
584+
expect(el.selectedOptions.length).to.equal(0);
585+
});
586+
587+
it("should not act on disabled options", async () => {
588+
const el = await fixture<BlSelect>(html`<bl-select multiple>
589+
<bl-select-option value="1" disabled>Option 1</bl-select-option>
590+
<bl-select-option value="2">Option 2</bl-select-option>
591+
<bl-select-option value="3">Option 3</bl-select-option>
592+
<bl-select-option value="4">Option 4</bl-select-option>
593+
<bl-select-option value="5">Option 5</bl-select-option>
594+
</bl-select>`);
595+
596+
const selectAll = el.shadowRoot!.querySelector<BlCheckbox>(".select-all")!;
597+
598+
setTimeout(() => selectAll.dispatchEvent(
599+
new CustomEvent("bl-checkbox-change", { detail: true }))
600+
);
601+
602+
const event = await oneEvent(el, "bl-select");
603+
604+
expect(event).to.exist;
605+
expect(event.detail.length).to.equal(4);
606+
expect(el.selectedOptions.length).to.equal(4);
607+
expect(el.selectedOptions[0].value).to.equal("2");
608+
});
609+
610+
it("should display indeterminate state when some options are selected", async () => {
611+
const el = await fixture<BlSelect>(html`<bl-select multiple>
612+
<bl-select-option value="1" selected>Option 1</bl-select-option>
613+
<bl-select-option value="2">Option 2</bl-select-option>
614+
<bl-select-option value="3">Option 3</bl-select-option>
615+
<bl-select-option value="4">Option 4</bl-select-option>
616+
<bl-select-option value="5">Option 5</bl-select-option>
617+
</bl-select>`);
618+
619+
const selectAll = el.shadowRoot!.querySelector<BlCheckbox>(".select-all")!;
620+
621+
expect(selectAll.indeterminate).to.be.true;
622+
expect(selectAll.checked).to.be.false;
623+
});
624+
625+
it('should uncheck "select all" checkbox when all available options are selected', async () => {
626+
const el = await fixture<BlSelect>(html`<bl-select multiple>
627+
<bl-select-option value="1" disabled>Option 1</bl-select-option>
628+
<bl-select-option value="2" selected>Option 2</bl-select-option>
629+
<bl-select-option value="3" selected>Option 3</bl-select-option>
630+
<bl-select-option value="4" selected>Option 4</bl-select-option>
631+
<bl-select-option value="5" selected>Option 5</bl-select-option>
632+
</bl-select>`);
633+
634+
const selectAll = el.shadowRoot!.querySelector<BlCheckbox>(".select-all")!;
635+
636+
expect(selectAll.indeterminate).to.be.true;
637+
expect(selectAll.checked).to.be.false;
638+
639+
setTimeout(() => selectAll.dispatchEvent(
640+
new CustomEvent("bl-checkbox-change", { detail: true }))
641+
);
642+
643+
const event = await oneEvent(el, "bl-select");
644+
645+
expect(event).to.exist;
646+
expect(event.detail.length).to.equal(0);
647+
expect(el.selectedOptions.length).to.equal(0);
648+
649+
expect(selectAll.indeterminate).to.be.false;
650+
expect(selectAll.checked).to.be.false;
651+
});
652+
});
513653
});

0 commit comments

Comments
 (0)