Skip to content

Commit

Permalink
feat(select): implement select all
Browse files Browse the repository at this point in the history
  • Loading branch information
ogunb committed Nov 30, 2023
1 parent cb12aa6 commit 41e69a6
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 1 deletion.
23 changes: 23 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,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);
}
25 changes: 24 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,27 @@ 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',
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
86 changes: 86 additions & 0 deletions src/components/select/bl-select.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<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;
});
});
});
50 changes: 50 additions & 0 deletions src/components/select/bl-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,18 @@ export default class BlSelect<ValueType extends FormValue = string> 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;
Expand Down Expand Up @@ -204,6 +216,10 @@ export default class BlSelect<ValueType extends FormValue = string> extends Form
return this._additionalSelectedOptionCount;
}

get isAllSelected() {
return this._selectedOptions.length === this._connectedOptions.length;
}

validityCallback(): string | void {
if (this.customInvalidText) {
return this.customInvalidText;
Expand Down Expand Up @@ -273,6 +289,20 @@ export default class BlSelect<ValueType extends FormValue = string> 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();

Expand Down Expand Up @@ -332,6 +362,25 @@ export default class BlSelect<ValueType extends FormValue = string> extends Form
</fieldset>`;
}

selectAllTemplate() {
if (!this.multiple || this.hideSelectAll) {
return null;
}

const isAnySelected = this._selectedOptions.length > 0;

return html`<bl-checkbox
class="select-all"
.checked="${this.isAllSelected}"
.indeterminate="${isAnySelected && !this.isAllSelected}"
role="option"
aria-selected="${this.isAllSelected}"
@bl-checkbox-change="${this._handleSelectAll}"
>
${this.selectAllText}
</bl-checkbox>`;
}

render() {
const invalidMessage = !this.checkValidity()
? html`<p id="errorMessage" aria-live="polite" class="invalid-text">
Expand Down Expand Up @@ -362,6 +411,7 @@ export default class BlSelect<ValueType extends FormValue = string> extends Form
aria-multiselectable="${this.multiple}"
aria-labelledby="label"
>
${this.selectAllTemplate()}
<slot></slot>
</div>
<div class="hint">${invalidMessage} ${helpMessage}</div>
Expand Down

0 comments on commit 41e69a6

Please sign in to comment.