Skip to content

feat: add select all button to multi-select-combo-box - #12716

Open
sissbruecker wants to merge 15 commits into
mainfrom
feat/mscb-select-all-items
Open

feat: add select all button to multi-select-combo-box#12716
sissbruecker wants to merge 15 commits into
mainfrom
feat/mscb-select-all-items

Conversation

@sissbruecker

@sissbruecker sissbruecker commented Sep 10, 2026

Copy link
Copy Markdown
Member

Description

Part of #4590
Related to vaadin/flow-components#4876

Adds an opt-in "Select all" button to vaadin-multi-select-combo-box:

  • When selectAllButtonVisible is set, a button is rendered at the top of the dropdown overlay, above the item list.
  • Clicking it selects every item that matches the current filter, or deselects them when they are all selected already.
  • Items that do not match the filter keep their selection state, the action does not replace the selection.
  • The label switches between "Select all" / "Deselect all" without a filter and "Select filtered" / "Deselect filtered" with one.
  • All four labels can be customized through i18n.
  • Keyboard users reach the button with Tab while the dropdown is open.
  • Focus cycles between the input and the button instead of leaving the component.
  • Escape, ArrowDown and ArrowUp return focus to the input and act as if pressed there.
  • Updating the selection announces the new selection count to screen readers.

Implementation notes

  • Added the selectAllButtonVisible property and the selectAll, deselectAll, selectFiltered and deselectFiltered i18n keys
  • Added the select-all shadow part with base, Lumo and Aura styles. It does not use select-all-button as that conflicts with existing styling rules for clear and toggle buttons.
  • The button logic lives in an internal SelectAllController in select-all-controller.js. It is not exported from the package and has no .d.ts, following the dashboard controllers. The mixin calls update() from updated() whenever a property the button depends on has changed.
  • The button is rendered in the shadow root as a sibling of the overlay slot, so it stays out of the role="listbox" scroller and does not affect aria-activedescendant handling.
  • Selection changes go through the existing __updateSelection(), so change, selected-items-changed and validation behave exactly as they do for clicking a single item.
  • Focus between the input and the button is kept inside the component by overriding _shouldSetFocus and _shouldRemoveFocus. Only the button shows a focus ring while it is focused, the host drops its focus-ring attribute but keeps focused.

Limitations

  • This PR covers the items API only. Support for dataProvider comes as a follow-up.
  • Styling is preliminary and will be improved later. As such no visual tests are added yet.

Type of change

  • Feature

@sissbruecker
sissbruecker force-pushed the feat/mscb-select-all-items branch from da79f2b to 2eab5c8 Compare September 10, 2026 09:20
@sissbruecker
sissbruecker marked this pull request as ready for review September 10, 2026 12:32

@vaadin-review-bot vaadin-review-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewed the changes — left 2 comments.

Finding
⚠️ With the dropdown open and the button visible, Tab can never leave the component
⚠️ role="application" is set on the overlay for every instance, not only when the button is used

Comment thread packages/multi-select-combo-box/src/select-all-controller.js

@web-padawan web-padawan left a comment

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.

First batch of comments, mostly focused on reviewing tests so far.

Comment thread packages/multi-select-combo-box/test/accessibility.test.js
Comment thread packages/multi-select-combo-box/test/select-all.test.js Outdated
Comment thread packages/multi-select-combo-box/test/select-all.test.js Outdated
Comment thread packages/multi-select-combo-box/test/select-all.test.js Outdated
Comment thread packages/multi-select-combo-box/test/select-all.test.js Outdated
Comment thread packages/multi-select-combo-box/test/select-all.test.js Outdated
Comment thread packages/multi-select-combo-box/test/select-all.test.js Outdated
Comment thread packages/multi-select-combo-box/test/select-all.test.js Outdated
Comment thread packages/multi-select-combo-box/test/select-all.test.js Outdated
});

it('should not submit the surrounding form on Enter', async () => {
const form = fixtureSync('<form></form>');

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.

nit: we don't generally test form submission explicitly elsewhere, could use simpler checks with event.defaultPrevented like we have in combo-box "enter key behavior" suite.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The behavior relies on applying type="button", so I replaced everything with that assertion but kept the test title as is to indicate why that attribute is there..

@sissbruecker
sissbruecker force-pushed the feat/mscb-select-all-items branch from e5eaba4 to 164ac48 Compare September 11, 2026 08:04
@sissbruecker
sissbruecker force-pushed the feat/mscb-select-all-items branch from 164ac48 to 4e16229 Compare September 11, 2026 10:23
Comment thread packages/multi-select-combo-box/test/select-all.test.js
Comment thread packages/multi-select-combo-box/test/select-all.test.js
Comment thread packages/multi-select-combo-box/src/select-all-controller.js Outdated

const { selectAll, deselectAll, selectFiltered, deselectFiltered } = host.__effectiveI18n;
if (host.filter) {
button.textContent = this.#allSelected ? deselectFiltered : selectFiltered;

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.

idea(non-blocking): while "keep everything in the controller" approach makes sense, one thing that I would personally do differently is to use Lit for rendering the button, its state and listener.

Possible implementation:

// Internal mixin getters, could be potentially beneficial for TestBench
get __selectAllVisible() {
  return this.selectAllButtonVisible && !this.readonly && !this.dataProvider;
}

get __selectAllText() {
  const { selectAll, deselectAll, selectFiltered, deselectFiltered } = this.__effectiveI18n;
  const allSelected = this.__selectAllVisible && this._selectAllController.isEveryFilteredItemSelected();

  if (this.filter) {
    return allSelected ? deselectFiltered : selectFiltered;
  }

  return allSelected ? deselectAll : selectAll;
}

// Declared in the mixin, used in MSCB `render()` 
_renderSelectAll() {
  return html`
    <button part="select-all" 
      type="button"
      ?hidden="${!this.__selectAllVisible}"
      @click="${this.__onSelectAllClick}"
    >
      ${this.__selectAllText}
    </button>
  `;
}

Some notes from prototyping this locally as an experiment:

  • No need for listing individual properties in updated() and calling _selectAllController.update().
  • The controller would need to be created in the constructor() and accept this as a host.
  • The controller still needs reference to the button - can use querySelector or ref directive.
  • The restoreFocus() would need to be called from willIUpdate() before the button gets hidden.

I'd leave it up to you as a possible optional refactor, not necessarily a big improvement.

@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants