From 41e69a6a2bef5fccd188edced893bf718b1cdcdf Mon Sep 17 00:00:00 2001 From: Ogun Babacan Date: Thu, 30 Nov 2023 17:58:03 +0300 Subject: [PATCH] feat(select): implement select all #520 --- src/components/select/bl-select.css | 23 ++++++ src/components/select/bl-select.stories.mdx | 25 +++++- src/components/select/bl-select.test.ts | 86 +++++++++++++++++++++ src/components/select/bl-select.ts | 50 ++++++++++++ 4 files changed, 183 insertions(+), 1 deletion(-) diff --git a/src/components/select/bl-select.css b/src/components/select/bl-select.css index 42ffc5a3..ab6b1d1f 100644 --- a/src/components/select/bl-select.css +++ b/src/components/select/bl-select.css @@ -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); @@ -337,3 +341,22 @@ 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; + + /* 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); +} diff --git a/src/components/select/bl-select.stories.mdx b/src/components/select/bl-select.stories.mdx index efc559ed..2da6c289 100644 --- a/src/components/select/bl-select.stories.mdx +++ b/src/components/select/bl-select.stories.mdx @@ -98,6 +98,8 @@ export const SelectTemplate = (args) => html` html`${ (args.options || defaultOptions).map((option) => html` ${option.label}` + ( args.selected || []).includes(option.value) } ?disabled=${option.disabled}>${option.label}` )} ` @@ -159,6 +161,27 @@ Selected options will be visible on input seperated by commas. +### 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. + + + + {SelectTemplate.bind({})} + + + {SelectTemplate.bind({})} + + + ## Clear Button The select component includes a clear button. Clear button can be displayed by passing `clearable` attribute to the Select component. diff --git a/src/components/select/bl-select.test.ts b/src/components/select/bl-select.test.ts index 4290466a..a58fd23e 100644 --- a/src/components/select/bl-select.test.ts +++ b/src/components/select/bl-select.test.ts @@ -510,4 +510,90 @@ 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(html` + Option 1 + Option 2 + Option 3 + Option 4 + Option 5 + `); + + + const selectAll = el.shadowRoot!.querySelector(".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(html` + Option 1 + Option 2 + Option 3 + Option 4 + Option 5 + `); + + expect(el.selectedOptions.length).to.equal(5); + + const selectAll = el.shadowRoot!.querySelector(".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(html` + Option 1 + Option 2 + Option 3 + Option 4 + Option 5 + `); + + const selectAll = el.shadowRoot!.querySelector(".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(html` + Option 1 + Option 2 + Option 3 + Option 4 + Option 5 + `); + + const selectAll = el.shadowRoot!.querySelector(".select-all")!; + + expect(selectAll.indeterminate).to.be.true; + expect(selectAll.checked).to.be.false; + }); + }); }); diff --git a/src/components/select/bl-select.ts b/src/components/select/bl-select.ts index 3beac77b..76bc1d00 100644 --- a/src/components/select/bl-select.ts +++ b/src/components/select/bl-select.ts @@ -143,6 +143,18 @@ export default class BlSelect extends Form @property({ type: String, attribute: "invalid-text", reflect: true }) customInvalidText?: string; + /** + * Hides select all option in multiple select + */ + @property({ type: Boolean, attribute: "hide-select-all" }) + hideSelectAll = false; + + /** + * Sets select all text in multiple select + */ + @property({ type: String, attribute: "select-all-text" }) + selectAllText = "Select All"; + /* Declare internal reactive properties */ @state() private _isPopoverOpen = false; @@ -204,6 +216,10 @@ export default class BlSelect extends Form return this._additionalSelectedOptionCount; } + get isAllSelected() { + return this._selectedOptions.length === this._connectedOptions.length; + } + validityCallback(): string | void { if (this.customInvalidText) { return this.customInvalidText; @@ -273,6 +289,20 @@ export default class BlSelect extends Form }); } + private _handleSelectAll(e: CustomEvent) { + const checked = e.detail; + + this._connectedOptions.forEach(option => { + if (option.disabled) { + return; + } + + option.selected = checked; + }); + + this._handleMultipleSelect(); + } + connectedCallback(): void { super.connectedCallback(); @@ -332,6 +362,25 @@ export default class BlSelect extends Form `; } + selectAllTemplate() { + if (!this.multiple || this.hideSelectAll) { + return null; + } + + const isAnySelected = this._selectedOptions.length > 0; + + return html` + ${this.selectAllText} + `; + } + render() { const invalidMessage = !this.checkValidity() ? html`

@@ -362,6 +411,7 @@ export default class BlSelect extends Form aria-multiselectable="${this.multiple}" aria-labelledby="label" > + ${this.selectAllTemplate()}

${invalidMessage} ${helpMessage}