diff --git a/dev/multi-select-combo-box.html b/dev/multi-select-combo-box.html
new file mode 100644
index 00000000000..d9811f0bb90
--- /dev/null
+++ b/dev/multi-select-combo-box.html
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+ Multi Select Combo box
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/multi-select-combo-box/LICENSE b/packages/multi-select-combo-box/LICENSE
new file mode 100644
index 00000000000..ec6f35397a7
--- /dev/null
+++ b/packages/multi-select-combo-box/LICENSE
@@ -0,0 +1,190 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ Copyright 2022 Vaadin Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/packages/multi-select-combo-box/README.md b/packages/multi-select-combo-box/README.md
new file mode 100644
index 00000000000..a9c345b69a0
--- /dev/null
+++ b/packages/multi-select-combo-box/README.md
@@ -0,0 +1,62 @@
+# @vaadin/multi-select-combo-box
+
+> ⚠️ Work in progress, please do not use this component yet.
+
+A web component that wraps `` and allows selecting multiple items.
+
+```html
+
+
+```
+
+## Installation
+
+Install the component:
+
+```sh
+npm i @vaadin/multi-select-combo-box
+```
+
+Once installed, import the component in your application:
+
+```js
+import '@vaadin/multi-select-combo-box';
+```
+
+## Themes
+
+Vaadin components come with two built-in [themes](https://vaadin.com/docs/latest/ds/customization/using-themes), Lumo and Material.
+The [main entrypoint](https://github.com/vaadin/web-components/blob/master/packages/multi-select-combo-box/vaadin-multi-select-combo-box.js) of the package uses the Lumo theme.
+
+To use the Material theme, import the component from the `theme/material` folder:
+
+```js
+import '@vaadin/multi-select-combo-box/theme/material/vaadin-multi-select-combo-box.js';
+```
+
+You can also import the Lumo version of the component explicitly:
+
+```js
+import '@vaadin/multi-select-combo-box/theme/lumo/vaadin-multi-select-combo-box.js';
+```
+
+Finally, you can import the un-themed component from the `src` folder to get a minimal starting point:
+
+```js
+import '@vaadin/multi-select-combo-box/src/vaadin-multi-select-combo-box.js';
+```
+
+## Contributing
+
+Read the [contributing guide](https://vaadin.com/docs/latest/guide/contributing/overview) to learn about our development process, how to propose bugfixes and improvements, and how to test your changes to Vaadin components.
+
+## License
+
+Apache License 2.0
+
+Vaadin collects usage statistics at development time to improve this product.
+For details and to opt-out, see https://github.com/vaadin/vaadin-usage-statistics.
diff --git a/packages/multi-select-combo-box/package.json b/packages/multi-select-combo-box/package.json
new file mode 100644
index 00000000000..3f70ad976a2
--- /dev/null
+++ b/packages/multi-select-combo-box/package.json
@@ -0,0 +1,49 @@
+{
+ "name": "@vaadin/multi-select-combo-box",
+ "version": "23.1.0-alpha1",
+ "publishConfig": {
+ "access": "public"
+ },
+ "description": "vaadin-multi-select-combo-box",
+ "license": "Apache-2.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/vaadin/web-components.git",
+ "directory": "packages/multi-select-combo-box"
+ },
+ "author": "Vaadin Ltd",
+ "homepage": "https://vaadin.com/components",
+ "bugs": {
+ "url": "https://github.com/vaadin/web-components/issues"
+ },
+ "main": "vaadin-multi-select-combo-box.js",
+ "module": "vaadin-multi-select-combo-box.js",
+ "files": [
+ "src",
+ "theme",
+ "vaadin-*.d.ts",
+ "vaadin-*.js"
+ ],
+ "keywords": [
+ "Vaadin",
+ "multi-select-combo-box",
+ "web-components",
+ "web-component",
+ "polymer"
+ ],
+ "dependencies": {
+ "@polymer/polymer": "^3.0.0",
+ "@vaadin/combo-box": "23.1.0-alpha1",
+ "@vaadin/component-base": "23.1.0-alpha1",
+ "@vaadin/field-base": "23.1.0-alpha1",
+ "@vaadin/input-container": "23.1.0-alpha1",
+ "@vaadin/vaadin-lumo-styles": "23.1.0-alpha1",
+ "@vaadin/vaadin-material-styles": "23.1.0-alpha1",
+ "@vaadin/vaadin-themable-mixin": "23.1.0-alpha1"
+ },
+ "devDependencies": {
+ "@esm-bundle/chai": "^4.3.4",
+ "@vaadin/testing-helpers": "^0.3.2",
+ "sinon": "^9.2.0"
+ }
+}
diff --git a/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box-chip.js b/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box-chip.js
new file mode 100644
index 00000000000..c89a0381020
--- /dev/null
+++ b/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box-chip.js
@@ -0,0 +1,66 @@
+/**
+ * @license
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
+import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
+
+/**
+ * An element used by `` to display selected items.
+ *
+ * ### Styling
+ *
+ * The following shadow DOM parts are available for styling:
+ *
+ * Part name | Description
+ * -----------------|-------------
+ * `label` | Element containing the label
+ * `remove-button` | Remove button
+ *
+ * See [Styling Components](https://vaadin.com/docs/latest/ds/customization/styling-components) documentation.
+ *
+ * @extends HTMLElement
+ * @private
+ */
+class MultiSelectComboBoxChip extends ThemableMixin(PolymerElement) {
+ static get is() {
+ return 'vaadin-multi-select-combo-box-chip';
+ }
+
+ static get properties() {
+ return {
+ label: {
+ type: String
+ },
+
+ item: {
+ type: Object
+ }
+ };
+ }
+
+ static get template() {
+ return html`
+ [[label]]
+
+ `;
+ }
+
+ /** @private */
+ _onRemoveClick(event) {
+ event.stopPropagation();
+
+ this.dispatchEvent(
+ new CustomEvent('item-removed', {
+ detail: {
+ item: this.item
+ },
+ bubbles: true,
+ composed: true
+ })
+ );
+ }
+}
+
+customElements.define(MultiSelectComboBoxChip.is, MultiSelectComboBoxChip);
diff --git a/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box-container.js b/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box-container.js
new file mode 100644
index 00000000000..310341e7342
--- /dev/null
+++ b/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box-container.js
@@ -0,0 +1,52 @@
+/**
+ * @license
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+import { InputContainer } from '@vaadin/input-container/src/vaadin-input-container.js';
+import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
+
+registerStyles(
+ 'vaadin-multi-select-combo-box-container',
+ css`
+ [part='wrapper'] {
+ display: flex;
+ width: 100%;
+ }
+ `,
+ {
+ moduleId: 'vaadin-multi-select-combo-box-container-styles'
+ }
+);
+
+let memoizedTemplate;
+
+/**
+ * An element used internally by ``. Not intended to be used separately.
+ *
+ * @extends InputContainer
+ * @private
+ */
+class MultiSelectComboBoxContainer extends InputContainer {
+ static get is() {
+ return 'vaadin-multi-select-combo-box-container';
+ }
+
+ static get template() {
+ if (!memoizedTemplate) {
+ memoizedTemplate = super.template.cloneNode(true);
+ const content = memoizedTemplate.content;
+ const slots = content.querySelectorAll('slot');
+
+ const wrapper = document.createElement('div');
+ wrapper.setAttribute('part', 'wrapper');
+ content.insertBefore(wrapper, slots[2]);
+
+ wrapper.appendChild(slots[0]);
+ wrapper.appendChild(slots[1]);
+ }
+ return memoizedTemplate;
+ }
+}
+
+customElements.define(MultiSelectComboBoxContainer.is, MultiSelectComboBoxContainer);
diff --git a/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box-dropdown.js b/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box-dropdown.js
new file mode 100644
index 00000000000..4bc2ce1308e
--- /dev/null
+++ b/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box-dropdown.js
@@ -0,0 +1,38 @@
+/**
+ * @license
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+import './vaadin-multi-select-combo-box-item.js';
+import './vaadin-multi-select-combo-box-overlay.js';
+import './vaadin-multi-select-combo-box-scroller.js';
+import { html } from '@polymer/polymer/lib/utils/html-tag.js';
+import { ComboBoxDropdown } from '@vaadin/combo-box/src/vaadin-combo-box-dropdown.js';
+
+/**
+ * An element used internally by ``. Not intended to be used separately.
+ *
+ * @extends ComboBoxDropdown
+ * @private
+ */
+class MultiSelectComboBoxDropdown extends ComboBoxDropdown {
+ static get is() {
+ return 'vaadin-multi-select-combo-box-dropdown';
+ }
+
+ static get template() {
+ return html`
+
+ `;
+ }
+}
+
+customElements.define(MultiSelectComboBoxDropdown.is, MultiSelectComboBoxDropdown);
diff --git a/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box-internal.js b/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box-internal.js
new file mode 100644
index 00000000000..a38bbb0d957
--- /dev/null
+++ b/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box-internal.js
@@ -0,0 +1,129 @@
+/**
+ * @license
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+import './vaadin-multi-select-combo-box-dropdown.js';
+import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
+import { ComboBoxDataProviderMixin } from '@vaadin/combo-box/src/vaadin-combo-box-data-provider-mixin.js';
+import { ComboBoxMixin } from '@vaadin/combo-box/src/vaadin-combo-box-mixin.js';
+import { ComboBoxPlaceholder } from '@vaadin/combo-box/src/vaadin-combo-box-placeholder.js';
+import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
+
+/**
+ * An element used internally by ``. Not intended to be used separately.
+ *
+ * @extends HTMLElement
+ * @mixes ComboBoxDataProviderMixin
+ * @mixes ComboBoxMixin
+ * @mixes ThemableMixin
+ * @private
+ */
+class MultiSelectComboBoxInternal extends ComboBoxDataProviderMixin(ComboBoxMixin(ThemableMixin(PolymerElement))) {
+ static get is() {
+ return 'vaadin-multi-select-combo-box-internal';
+ }
+
+ static get template() {
+ return html`
+
+
+
+
+
+ `;
+ }
+
+ static get properties() {
+ return {
+ _target: {
+ type: Object
+ }
+ };
+ }
+
+ /**
+ * Reference to the clear button element.
+ * @protected
+ * @return {!HTMLElement}
+ */
+ get clearElement() {
+ return this.querySelector('[part="clear-button"]');
+ }
+
+ /**
+ * @protected
+ * @override
+ */
+ _getItemElements() {
+ return Array.from(this.$.dropdown._scroller.querySelectorAll('vaadin-multi-select-combo-box-item'));
+ }
+
+ /** @protected */
+ ready() {
+ super.ready();
+
+ this._target = this;
+ this._toggleElement = this.querySelector('.toggle-button');
+ }
+
+ /** @protected */
+ _isClearButton(event) {
+ return (
+ super._isClearButton(event) ||
+ (event.type === 'input' && !event.isTrusted) || // fake input event dispatched by clear button
+ event.composedPath()[0].getAttribute('part') === 'clear-button'
+ );
+ }
+
+ /**
+ * @param {!Event} event
+ * @protected
+ */
+ _onChange(event) {
+ super._onChange(event);
+
+ if (this._isClearButton(event)) {
+ this._clear();
+ }
+ }
+
+ /**
+ * @param {CustomEvent} event
+ * @protected
+ * @override
+ */
+ _overlaySelectedItemChanged(event) {
+ event.stopPropagation();
+
+ if (event.detail.item instanceof ComboBoxPlaceholder) {
+ return;
+ }
+
+ if (this.opened) {
+ this.dispatchEvent(
+ new CustomEvent('combo-box-item-selected', {
+ detail: {
+ item: event.detail.item
+ }
+ })
+ );
+ }
+ }
+}
+
+customElements.define(MultiSelectComboBoxInternal.is, MultiSelectComboBoxInternal);
diff --git a/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box-item.js b/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box-item.js
new file mode 100644
index 00000000000..6bb764ca0f3
--- /dev/null
+++ b/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box-item.js
@@ -0,0 +1,37 @@
+/**
+ * @license
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+import { ComboBoxItem } from '@vaadin/combo-box/src/vaadin-combo-box-item.js';
+
+/**
+ * An element used for items in ``.
+ *
+ * ### Styling
+ *
+ * The following shadow DOM parts are available for styling:
+ *
+ * Part name | Description
+ * ----------|-------------
+ * `content` | The element that wraps the item content
+ *
+ * The following state attributes are exposed for styling:
+ *
+ * Attribute | Description | Part name
+ * -----------|-------------------------------|-----------
+ * `selected` | Set when the item is selected | :host
+ * `focused` | Set when the item is focused | :host
+ *
+ * See [Styling Components](https://vaadin.com/docs/latest/ds/customization/styling-components) documentation.
+ *
+ * @extends ComboBoxItem
+ * @private
+ */
+class MultiSelectComboBoxItem extends ComboBoxItem {
+ static get is() {
+ return 'vaadin-multi-select-combo-box-item';
+ }
+}
+
+customElements.define(MultiSelectComboBoxItem.is, MultiSelectComboBoxItem);
diff --git a/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box-overlay.js b/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box-overlay.js
new file mode 100644
index 00000000000..4cc2bd6d8a4
--- /dev/null
+++ b/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box-overlay.js
@@ -0,0 +1,34 @@
+/**
+ * @license
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+import { ComboBoxOverlay } from '@vaadin/combo-box/src/vaadin-combo-box-overlay.js';
+import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
+
+registerStyles(
+ 'vaadin-multi-select-combo-box-overlay',
+ css`
+ #overlay {
+ width: var(
+ --vaadin-multi-select-combo-box-overlay-width,
+ var(--_vaadin-multi-select-combo-box-overlay-default-width, auto)
+ );
+ }
+ `,
+ { moduleId: 'vaadin-multi-select-combo-box-overlay-styles' }
+);
+
+/**
+ * An element used internally by ``. Not intended to be used separately.
+ *
+ * @extends ComboBoxOverlay
+ * @private
+ */
+class MultiSelectComboBoxOverlay extends ComboBoxOverlay {
+ static get is() {
+ return 'vaadin-multi-select-combo-box-overlay';
+ }
+}
+
+customElements.define(MultiSelectComboBoxOverlay.is, MultiSelectComboBoxOverlay);
diff --git a/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box-scroller.js b/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box-scroller.js
new file mode 100644
index 00000000000..b8342dcf44d
--- /dev/null
+++ b/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box-scroller.js
@@ -0,0 +1,31 @@
+/**
+ * @license
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+import { ComboBoxPlaceholder } from '@vaadin/combo-box/src/vaadin-combo-box-placeholder.js';
+import { ComboBoxScroller } from '@vaadin/combo-box/src/vaadin-combo-box-scroller.js';
+
+/**
+ * An element used internally by ``. Not intended to be used separately.
+ *
+ * @extends ComboBoxScroller
+ * @private
+ */
+class MultiSelectComboBoxScroller extends ComboBoxScroller {
+ static get is() {
+ return 'vaadin-multi-select-combo-box-scroller';
+ }
+
+ /** @private */
+ __isItemSelected(item, _selectedItem, itemIdPath) {
+ if (item instanceof ComboBoxPlaceholder) {
+ return false;
+ }
+
+ const host = this.comboBox.getRootNode().host;
+ return host._findIndex(item, host.selectedItems, itemIdPath) > -1;
+ }
+}
+
+customElements.define(MultiSelectComboBoxScroller.is, MultiSelectComboBoxScroller);
diff --git a/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box.d.ts b/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box.d.ts
new file mode 100644
index 00000000000..4a15e21e613
--- /dev/null
+++ b/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box.d.ts
@@ -0,0 +1,254 @@
+/**
+ * @license
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+import { ComboBoxDataProvider, ComboBoxDefaultItem, ComboBoxRenderer } from '@vaadin/combo-box/src/vaadin-combo-box.js';
+import { ControllerMixinClass } from '@vaadin/component-base/src/controller-mixin.js';
+import { DisabledMixinClass } from '@vaadin/component-base/src/disabled-mixin.js';
+import { ElementMixinClass } from '@vaadin/component-base/src/element-mixin.js';
+import { FocusMixinClass } from '@vaadin/component-base/src/focus-mixin.js';
+import { KeyboardMixinClass } from '@vaadin/component-base/src/keyboard-mixin.js';
+import { DelegateFocusMixinClass } from '@vaadin/field-base/src/delegate-focus-mixin.js';
+import { DelegateStateMixinClass } from '@vaadin/field-base/src/delegate-state-mixin.js';
+import { FieldMixinClass } from '@vaadin/field-base/src/field-mixin.js';
+import { InputConstraintsMixinClass } from '@vaadin/field-base/src/input-constraints-mixin.js';
+import { InputControlMixinClass } from '@vaadin/field-base/src/input-control-mixin.js';
+import { InputMixinClass } from '@vaadin/field-base/src/input-mixin.js';
+import { LabelMixinClass } from '@vaadin/field-base/src/label-mixin.js';
+import { ValidateMixinClass } from '@vaadin/field-base/src/validate-mixin.js';
+import { ThemableMixinClass } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
+
+/**
+ * Fired when the user commits a value change.
+ */
+export type MultiSelectComboBoxChangeEvent = Event & {
+ target: MultiSelectComboBox;
+};
+
+/**
+ * Fired when the user sets a custom value.
+ */
+export type MultiSelectComboBoxCustomValuesSetEvent = CustomEvent;
+
+/**
+ * Fired when the `filter` property changes.
+ */
+export type MultiSelectComboBoxFilterChangedEvent = CustomEvent<{ value: string }>;
+
+/**
+ * Fired when the `invalid` property changes.
+ */
+export type MultiSelectComboBoxInvalidChangedEvent = CustomEvent<{ value: boolean }>;
+
+/**
+ * Fired when the `selectedItems` property changes.
+ */
+export type MultiSelectComboBoxSelectedItemsChangedEvent = CustomEvent<{ value: Array }>;
+
+export interface MultiSelectComboBoxEventMap extends HTMLElementEventMap {
+ change: MultiSelectComboBoxChangeEvent;
+
+ 'custom-values-set': MultiSelectComboBoxCustomValuesSetEvent;
+
+ 'filter-changed': MultiSelectComboBoxFilterChangedEvent;
+
+ 'invalid-changed': MultiSelectComboBoxInvalidChangedEvent;
+
+ 'selected-items-changed': MultiSelectComboBoxSelectedItemsChangedEvent;
+}
+
+/**
+ * `` is a web component that wraps `` and extends
+ * its functionality to allow selecting multiple items, in addition to basic features.
+ *
+ * ```html
+ *
+ * ```
+ *
+ * ```js
+ * const comboBox = document.querySelector('#comboBox');
+ * comboBox.items = ['apple', 'banana', 'lemon', 'orange'];
+ * comboBox.selectedItems = ['lemon', 'orange'];
+ * ```
+ *
+ * ### Styling
+ *
+ * The following shadow DOM parts are available for styling:
+ *
+ * Part name | Description
+ * -----------------------|----------------
+ * `chip` | Chip shown for every selected item
+ * `label` | The label element
+ * `input-field` | The element that wraps prefix, value and suffix
+ * `clear-button` | The clear button
+ * `error-message` | The error message element
+ * `helper-text` | The helper text element wrapper
+ * `required-indicator` | The `required` state indicator element
+ * `toggle-button` | The toggle button
+ *
+ * The following state attributes are available for styling:
+ *
+ * Attribute | Description
+ * -----------------------|-----------------
+ * `disabled` | Set to a disabled element
+ * `has-value` | Set when the element has a value
+ * `has-label` | Set when the element has a label
+ * `has-helper` | Set when the element has helper text or slot
+ * `has-error-message` | Set when the element has an error message
+ * `invalid` | Set when the element is invalid
+ * `focused` | Set when the element is focused
+ * `focus-ring` | Set when the element is keyboard focused
+ * `opened` | Set when the dropdown is open
+ * `readonly` | Set to a readonly element
+ *
+ * ### Internal components
+ *
+ * In addition to `` itself, the following internal
+ * components are themable:
+ *
+ * - `` - has the same API as ``.
+ * - `` - has the same API as ``.
+ * - `` - has the same API as ``.
+ *
+ * Note: the `theme` attribute value set on `` is
+ * propagated to these components.
+ *
+ * See [Styling Components](https://vaadin.com/docs/latest/ds/customization/styling-components) documentation.
+ *
+ * @fires {Event} change - Fired when the user commits a value change.
+ * @fires {CustomEvent} custom-values-set - Fired when the user sets a custom value.
+ * @fires {CustomEvent} filter-changed - Fired when the `filter` property changes.
+ * @fires {CustomEvent} invalid-changed - Fired when the `invalid` property changes.
+ * @fires {CustomEvent} selected-items-changed - Fired when the `selectedItems` property changes.
+ */
+declare class MultiSelectComboBox extends HTMLElement {
+ /**
+ * When true, the user can input a value that is not present in the items list.
+ * @attr {boolean} allow-custom-values
+ */
+ allowCustomValues: boolean;
+
+ /**
+ * Set true to prevent the overlay from opening automatically.
+ * @attr {boolean} auto-open-disabled
+ */
+ autoOpenDisabled: boolean;
+
+ /**
+ * Function that provides items lazily. Receives two arguments:
+ *
+ * - `params` - Object with the following properties:
+ * - `params.page` Requested page index
+ * - `params.pageSize` Current page size
+ * - `params.filter` Currently applied filter
+ *
+ * - `callback(items, size)` - Callback function with arguments:
+ * - `items` Current page of items
+ * - `size` Total number of items.
+ */
+ dataProvider: ComboBoxDataProvider | null | undefined;
+
+ /**
+ * A subset of items, filtered based on the user input. Filtered items
+ * can be assigned directly to omit the internal filtering functionality.
+ * The items can be of either `String` or `Object` type.
+ */
+ filteredItems: Array | undefined;
+
+ /**
+ * Filtering string the user has typed into the input field.
+ */
+ filter: string;
+
+ /**
+ * A full set of items to filter the visible options from.
+ * The items can be of either `String` or `Object` type.
+ */
+ items: Array | undefined;
+
+ /**
+ * The item property used for a visual representation of the item.
+ * @attr {string} item-label-path
+ */
+ itemLabelPath: string;
+
+ /**
+ * Path for the id of the item, used to detect whether the item is selected.
+ * @attr {string} item-id-path
+ */
+ itemIdPath: string;
+
+ /**
+ * Path for the value of the item. If `items` is an array of objects,
+ * this property is used as a string value for the selected item.
+ * @attr {string} item-value-path
+ */
+ itemValuePath: string;
+
+ /**
+ * True if the dropdown is open, false otherwise.
+ */
+ opened: boolean;
+
+ /**
+ * Number of items fetched at a time from the data provider.
+ * @attr {number} page-size
+ */
+ pageSize: number;
+
+ /**
+ * Custom function for rendering the content of every item.
+ * Receives three arguments:
+ *
+ * - `root` The `` internal container DOM element.
+ * - `comboBox` The reference to the `` element.
+ * - `model` The object with the properties related with the rendered
+ * item, contains:
+ * - `model.index` The index of the rendered item.
+ * - `model.item` The item.
+ */
+ renderer: ComboBoxRenderer | null | undefined;
+
+ /**
+ * The list of selected items.
+ * Note: modifying the selected items creates a new array each time.
+ */
+ selectedItems: Array;
+
+ addEventListener>(
+ type: K,
+ listener: (this: MultiSelectComboBox, ev: MultiSelectComboBoxEventMap[K]) => void,
+ options?: boolean | AddEventListenerOptions
+ ): void;
+
+ removeEventListener>(
+ type: K,
+ listener: (this: MultiSelectComboBox, ev: MultiSelectComboBoxEventMap[K]) => void,
+ options?: boolean | EventListenerOptions
+ ): void;
+}
+
+interface MultiSelectComboBox
+ extends ValidateMixinClass,
+ LabelMixinClass,
+ KeyboardMixinClass,
+ InputMixinClass,
+ InputControlMixinClass,
+ InputConstraintsMixinClass,
+ FocusMixinClass,
+ FieldMixinClass,
+ DisabledMixinClass,
+ DelegateStateMixinClass,
+ DelegateFocusMixinClass,
+ ThemableMixinClass,
+ ElementMixinClass,
+ ControllerMixinClass {}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'vaadin-multi-select-combo-box': MultiSelectComboBox;
+ }
+}
+
+export { MultiSelectComboBox };
diff --git a/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box.js b/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box.js
new file mode 100644
index 00000000000..82fc7f477cb
--- /dev/null
+++ b/packages/multi-select-combo-box/src/vaadin-multi-select-combo-box.js
@@ -0,0 +1,596 @@
+/**
+ * @license
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+import './vaadin-multi-select-combo-box-chip.js';
+import './vaadin-multi-select-combo-box-container.js';
+import './vaadin-multi-select-combo-box-internal.js';
+import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
+import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
+import { processTemplates } from '@vaadin/component-base/src/templates.js';
+import { InputControlMixin } from '@vaadin/field-base/src/input-control-mixin.js';
+import { InputController } from '@vaadin/field-base/src/input-controller.js';
+import { LabelledInputController } from '@vaadin/field-base/src/labelled-input-controller.js';
+import { inputFieldShared } from '@vaadin/field-base/src/styles/input-field-shared-styles.js';
+import { css, registerStyles, ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
+
+const multiSelectComboBox = css`
+ [hidden] {
+ display: none !important;
+ }
+
+ :host([has-value]) ::slotted(input:placeholder-shown) {
+ color: transparent !important;
+ }
+
+ :host([has-value]) [class$='container'] {
+ width: auto;
+ }
+
+ ::slotted(input) {
+ box-sizing: border-box;
+ flex: 1 0 4em;
+ }
+
+ [part~='chip'] {
+ flex: 0 1 auto;
+ }
+
+ :host([readonly]) [part~='chip'] {
+ pointer-events: none;
+ }
+`;
+
+registerStyles('vaadin-multi-select-combo-box', [inputFieldShared, multiSelectComboBox], {
+ moduleId: 'vaadin-multi-select-combo-box-styles'
+});
+
+/**
+ * `` is a web component that wraps `` and extends
+ * its functionality to allow selecting multiple items, in addition to basic features.
+ *
+ * ```html
+ *
+ * ```
+ *
+ * ```js
+ * const comboBox = document.querySelector('#comboBox');
+ * comboBox.items = ['apple', 'banana', 'lemon', 'orange'];
+ * comboBox.selectedItems = ['lemon', 'orange'];
+ * ```
+ *
+ * ### Styling
+ *
+ * The following shadow DOM parts are available for styling:
+ *
+ * Part name | Description
+ * -----------------------|----------------
+ * `chip` | Chip shown for every selected item
+ * `label` | The label element
+ * `input-field` | The element that wraps prefix, value and suffix
+ * `clear-button` | The clear button
+ * `error-message` | The error message element
+ * `helper-text` | The helper text element wrapper
+ * `required-indicator` | The `required` state indicator element
+ * `toggle-button` | The toggle button
+ *
+ * The following state attributes are available for styling:
+ *
+ * Attribute | Description
+ * -----------------------|-----------------
+ * `disabled` | Set to a disabled element
+ * `has-value` | Set when the element has a value
+ * `has-label` | Set when the element has a label
+ * `has-helper` | Set when the element has helper text or slot
+ * `has-error-message` | Set when the element has an error message
+ * `invalid` | Set when the element is invalid
+ * `focused` | Set when the element is focused
+ * `focus-ring` | Set when the element is keyboard focused
+ * `opened` | Set when the dropdown is open
+ * `readonly` | Set to a readonly element
+ *
+ * ### Internal components
+ *
+ * In addition to `` itself, the following internal
+ * components are themable:
+ *
+ * - `` - has the same API as ``.
+ * - `` - has the same API as ``.
+ * - `` - has the same API as ``.
+ *
+ * Note: the `theme` attribute value set on `` is
+ * propagated to these components.
+ *
+ * See [Styling Components](https://vaadin.com/docs/latest/ds/customization/styling-components) documentation.
+ *
+ * @fires {Event} change - Fired when the user commits a value change.
+ * @fires {CustomEvent} custom-values-set - Fired when the user sets a custom value.
+ * @fires {CustomEvent} filter-changed - Fired when the `filter` property changes.
+ * @fires {CustomEvent} invalid-changed - Fired when the `invalid` property changes.
+ * @fires {CustomEvent} selected-items-changed - Fired when the `selectedItems` property changes.
+ *
+ * @extends HTMLElement
+ * @mixes ElementMixin
+ * @mixes ThemableMixin
+ * @mixes InputControlMixin
+ */
+class MultiSelectComboBox extends InputControlMixin(ThemableMixin(ElementMixin(PolymerElement))) {
+ static get is() {
+ return 'vaadin-multi-select-combo-box';
+ }
+
+ static get template() {
+ return html`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ static get properties() {
+ return {
+ /**
+ * Set true to prevent the overlay from opening automatically.
+ * @attr {boolean} auto-open-disabled
+ */
+ autoOpenDisabled: Boolean,
+
+ /**
+ * A full set of items to filter the visible options from.
+ * The items can be of either `String` or `Object` type.
+ */
+ items: {
+ type: Array
+ },
+
+ /**
+ * The item property used for a visual representation of the item.
+ * @attr {string} item-label-path
+ */
+ itemLabelPath: {
+ type: String
+ },
+
+ /**
+ * Path for the value of the item. If `items` is an array of objects,
+ * this property is used as a string value for the selected item.
+ * @attr {string} item-value-path
+ */
+ itemValuePath: {
+ type: String
+ },
+
+ /**
+ * Path for the id of the item, used to detect whether the item is selected.
+ * @attr {string} item-id-path
+ */
+ itemIdPath: {
+ type: String
+ },
+
+ /**
+ * The list of selected items.
+ * Note: modifying the selected items creates a new array each time.
+ */
+ selectedItems: {
+ type: Array,
+ value: () => [],
+ notify: true
+ },
+
+ /**
+ * True if the dropdown is open, false otherwise.
+ */
+ opened: {
+ type: Boolean,
+ notify: true,
+ value: false,
+ reflectToAttribute: true
+ },
+
+ /**
+ * Number of items fetched at a time from the data provider.
+ * @attr {number} page-size
+ */
+ pageSize: {
+ type: Number,
+ value: 50,
+ observer: '_pageSizeChanged'
+ },
+
+ /**
+ * Function that provides items lazily. Receives two arguments:
+ *
+ * - `params` - Object with the following properties:
+ * - `params.page` Requested page index
+ * - `params.pageSize` Current page size
+ * - `params.filter` Currently applied filter
+ *
+ * - `callback(items, size)` - Callback function with arguments:
+ * - `items` Current page of items
+ * - `size` Total number of items.
+ */
+ dataProvider: {
+ type: Object,
+ observer: '_dataProviderChanged'
+ },
+
+ /**
+ * When true, the user can input a value that is not present in the items list.
+ * @attr {boolean} allow-custom-values
+ */
+ allowCustomValues: {
+ type: Boolean,
+ value: false
+ },
+
+ /**
+ * Custom function for rendering the content of every item.
+ * Receives three arguments:
+ *
+ * - `root` The `` internal container DOM element.
+ * - `comboBox` The reference to the `` element.
+ * - `model` The object with the properties related with the rendered
+ * item, contains:
+ * - `model.index` The index of the rendered item.
+ * - `model.item` The item.
+ */
+ renderer: Function,
+
+ /**
+ * Filtering string the user has typed into the input field.
+ */
+ filter: {
+ type: String,
+ value: '',
+ notify: true
+ },
+
+ /**
+ * A subset of items, filtered based on the user input. Filtered items
+ * can be assigned directly to omit the internal filtering functionality.
+ * The items can be of either `String` or `Object` type.
+ */
+ filteredItems: Array,
+
+ /** @protected */
+ _hasValue: {
+ type: Boolean,
+ value: false
+ }
+ };
+ }
+
+ static get observers() {
+ return ['_selectedItemsChanged(selectedItems, selectedItems.*)'];
+ }
+
+ /**
+ * Used by `ClearButtonMixin` as a reference to the clear button element.
+ * @protected
+ * @return {!HTMLElement}
+ */
+ get clearElement() {
+ return this.$.clearButton;
+ }
+
+ /** @protected */
+ get _chips() {
+ return this.shadowRoot.querySelectorAll('[part~="chip"]');
+ }
+
+ /** @protected */
+ ready() {
+ super.ready();
+
+ this.addController(
+ new InputController(this, (input) => {
+ this._setInputElement(input);
+ this._setFocusElement(input);
+ this.stateTarget = input;
+ this.ariaTarget = input;
+ })
+ );
+ this.addController(new LabelledInputController(this.inputElement, this._labelController));
+
+ this._inputField = this.shadowRoot.querySelector('[part="input-field"]');
+ this.__updateChips();
+
+ processTemplates(this);
+ }
+
+ /**
+ * Returns true if the current input value satisfies all constraints (if any).
+ * @return {boolean}
+ */
+ checkValidity() {
+ return this.required ? this._hasValue : true;
+ }
+
+ /**
+ * Override method inherited from `DisabledMixin` to forward disabled to chips.
+ * @protected
+ * @override
+ */
+ _disabledChanged(disabled, oldDisabled) {
+ super._disabledChanged(disabled, oldDisabled);
+
+ if (disabled || oldDisabled) {
+ this._chips.forEach((chip) => {
+ chip.toggleAttribute('disabled', disabled);
+ });
+ }
+ }
+
+ /**
+ * Override method inherited from `InputMixin` to forward the input to combo-box.
+ * @protected
+ * @override
+ */
+ _inputElementChanged(input) {
+ super._inputElementChanged(input);
+
+ if (input) {
+ this.$.comboBox._setInputElement(input);
+ }
+ }
+
+ /**
+ * Override method inherited from `FocusMixin` to validate on blur.
+ * @param {boolean} focused
+ * @protected
+ */
+ _setFocused(focused) {
+ super._setFocused(focused);
+
+ if (!focused) {
+ this.validate();
+ }
+ }
+
+ /**
+ * Override method inherited from `InputMixin`
+ * to keep attribute after clearing the input.
+ * @protected
+ * @override
+ */
+ _toggleHasValue() {
+ super._toggleHasValue(this._hasValue);
+ }
+
+ /** @private */
+ _pageSizeChanged(pageSize, oldPageSize) {
+ if (Math.floor(pageSize) !== pageSize || pageSize <= 0) {
+ this.pageSize = oldPageSize;
+ console.error('"pageSize" value must be an integer > 0');
+ }
+
+ this.$.comboBox.pageSize = this.pageSize;
+ }
+
+ /** @private */
+ _selectedItemsChanged(selectedItems) {
+ this._hasValue = Boolean(selectedItems && selectedItems.length);
+
+ this._toggleHasValue();
+
+ // Re-render chips
+ this.__updateChips();
+
+ // Re-render scroller
+ this.$.comboBox.$.dropdown._scroller.__virtualizer.update();
+
+ // Wait for chips to render
+ requestAnimationFrame(() => {
+ this.$.comboBox.$.dropdown._setOverlayWidth();
+ });
+ }
+
+ /** @private */
+ _getItemLabel(item, itemLabelPath) {
+ return item && Object.prototype.hasOwnProperty.call(item, itemLabelPath) ? item[itemLabelPath] : item;
+ }
+
+ /** @private */
+ _findIndex(item, selectedItems, itemIdPath) {
+ if (itemIdPath && item) {
+ for (let index = 0; index < selectedItems.length; index++) {
+ if (selectedItems[index] && selectedItems[index][itemIdPath] === item[itemIdPath]) {
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ return selectedItems.indexOf(item);
+ }
+
+ /** @private */
+ __clearFilter() {
+ this.$.comboBox.clear();
+ }
+
+ /** @private */
+ __removeItem(item) {
+ const itemsCopy = [...this.selectedItems];
+ itemsCopy.splice(itemsCopy.indexOf(item), 1);
+ this.__updateSelection(itemsCopy);
+ }
+
+ /** @private */
+ __selectItem(item) {
+ const itemsCopy = [...this.selectedItems];
+
+ const index = this._findIndex(item, itemsCopy, this.itemIdPath);
+ if (index !== -1) {
+ itemsCopy.splice(index, 1);
+ } else {
+ itemsCopy.push(item);
+ }
+
+ this.__updateSelection(itemsCopy);
+
+ // Reset the overlay focused index.
+ this.$.comboBox._focusedIndex = -1;
+
+ // Suppress `value-changed` event.
+ this.__clearFilter();
+ }
+
+ /** @private */
+ __updateSelection(selectedItems) {
+ this.selectedItems = selectedItems;
+
+ this.validate();
+
+ this.dispatchEvent(new CustomEvent('change', { bubbles: true }));
+ }
+
+ /** @private */
+ __createChip(item) {
+ const chip = document.createElement('vaadin-multi-select-combo-box-chip');
+ chip.setAttribute('part', 'chip');
+ chip.setAttribute('slot', 'prefix');
+
+ chip.item = item;
+ chip.label = this._getItemLabel(item, this.itemLabelPath);
+ chip.toggleAttribute('disabled', this.disabled);
+
+ chip.addEventListener('item-removed', (e) => this._onItemRemoved(e));
+ chip.addEventListener('mousedown', (e) => this._preventBlur(e));
+
+ return chip;
+ }
+
+ /** @private */
+ __updateChips() {
+ if (!this._inputField) {
+ return;
+ }
+
+ this._chips.forEach((chip) => {
+ chip.remove();
+ });
+
+ const items = [...this.selectedItems];
+
+ for (let i = items.length - 1; i >= 0; i--) {
+ const chip = this.__createChip(items[i]);
+ this._inputField.insertBefore(chip, this._inputField.firstElementChild);
+ }
+ }
+
+ /**
+ * Override method inherited from `ClearButtonMixin` and clear items.
+ * @protected
+ * @override
+ */
+ _onClearButtonClick(event) {
+ event.stopPropagation();
+
+ this.__updateSelection([]);
+ }
+
+ /**
+ * Override an event listener from `KeyboardMixin`.
+ * @param {KeyboardEvent} event
+ * @protected
+ * @override
+ */
+ _onKeyDown(event) {
+ const items = this.selectedItems || [];
+ if (!this.readonly && event.key === 'Backspace' && items.length && this.inputElement.value === '') {
+ this.__removeItem(items[items.length - 1]);
+ }
+ }
+
+ /** @private */
+ _onComboBoxChange() {
+ const item = this.$.comboBox.selectedItem;
+ if (item) {
+ this.__selectItem(item);
+ }
+ }
+
+ /** @private */
+ _onComboBoxItemSelected(event) {
+ this.__selectItem(event.detail.item);
+ }
+
+ /** @private */
+ _onCustomValueSet(event) {
+ // Do not set combo-box value
+ event.preventDefault();
+
+ this.__clearFilter();
+
+ this.dispatchEvent(
+ new CustomEvent('custom-values-set', {
+ detail: event.detail,
+ composed: true,
+ bubbles: true
+ })
+ );
+ }
+
+ /** @private */
+ _onItemRemoved(event) {
+ this.__removeItem(event.detail.item);
+ }
+
+ /** @private */
+ _preventBlur(event) {
+ // Prevent mousedown event to keep the input focused
+ // and keep the overlay opened when clicking a chip.
+ event.preventDefault();
+ }
+}
+
+customElements.define(MultiSelectComboBox.is, MultiSelectComboBox);
+
+export { MultiSelectComboBox };
diff --git a/packages/multi-select-combo-box/test/.eslintrc.json b/packages/multi-select-combo-box/test/.eslintrc.json
new file mode 100644
index 00000000000..7eeefc33b66
--- /dev/null
+++ b/packages/multi-select-combo-box/test/.eslintrc.json
@@ -0,0 +1,5 @@
+{
+ "env": {
+ "mocha": true
+ }
+}
diff --git a/packages/multi-select-combo-box/test/basic.test.js b/packages/multi-select-combo-box/test/basic.test.js
new file mode 100644
index 00000000000..8e1823d9297
--- /dev/null
+++ b/packages/multi-select-combo-box/test/basic.test.js
@@ -0,0 +1,417 @@
+import { expect } from '@esm-bundle/chai';
+import { fixtureSync, nextRender } from '@vaadin/testing-helpers';
+import { sendKeys } from '@web/test-runner-commands';
+import sinon from 'sinon';
+import './not-animated-styles.js';
+import '../vaadin-multi-select-combo-box.js';
+
+describe('basic', () => {
+ let comboBox, internal, inputElement;
+
+ beforeEach(() => {
+ comboBox = fixtureSync(``);
+ comboBox.items = ['apple', 'banana', 'lemon', 'orange'];
+ internal = comboBox.$.comboBox;
+ inputElement = comboBox.inputElement;
+ });
+
+ describe('custom element definition', () => {
+ let tagName;
+
+ beforeEach(() => {
+ tagName = comboBox.tagName.toLowerCase();
+ });
+
+ it('should be defined in custom element registry', () => {
+ expect(customElements.get(tagName)).to.be.ok;
+ });
+
+ it('should have a valid static "is" getter', () => {
+ expect(customElements.get(tagName).is).to.equal(tagName);
+ });
+ });
+
+ describe('properties and attributes', () => {
+ it('should propagate inputElement property to combo-box', () => {
+ expect(internal.inputElement).to.equal(comboBox.inputElement);
+ });
+
+ it('should propagate opened property to input', () => {
+ comboBox.opened = true;
+ expect(internal.opened).to.be.true;
+
+ comboBox.opened = false;
+ expect(internal.opened).to.be.false;
+ });
+
+ it('should reflect opened property to attribute', () => {
+ comboBox.opened = true;
+ expect(comboBox.hasAttribute('opened')).to.be.true;
+
+ comboBox.opened = false;
+ expect(comboBox.hasAttribute('opened')).to.be.false;
+ });
+
+ it('should propagate placeholder property to input', () => {
+ expect(inputElement.placeholder).to.be.not.ok;
+ comboBox.placeholder = 'foo';
+ expect(inputElement.placeholder).to.be.equal('foo');
+ });
+
+ it('should propagate required property to input', () => {
+ comboBox.required = true;
+ expect(inputElement.required).to.be.true;
+
+ comboBox.required = false;
+ expect(inputElement.required).to.be.false;
+ });
+
+ it('should propagate disabled property to combo-box', () => {
+ expect(internal.disabled).to.be.false;
+ comboBox.disabled = true;
+ expect(internal.disabled).to.be.true;
+ });
+
+ it('should propagate disabled property to input', () => {
+ expect(inputElement.disabled).to.be.false;
+ comboBox.disabled = true;
+ expect(inputElement.disabled).to.be.true;
+ });
+
+ it('should propagate readonly property to combo-box', () => {
+ expect(internal.readonly).to.be.false;
+ comboBox.readonly = true;
+ expect(internal.readonly).to.be.true;
+ });
+
+ it('should reflect readonly property to attribute', () => {
+ comboBox.readonly = true;
+ expect(comboBox.hasAttribute('readonly')).to.be.true;
+ });
+
+ it('should propagate renderer property to combo-box', () => {
+ const renderer = (root, _, model) => (root.textContent = model);
+ comboBox.renderer = renderer;
+ expect(internal.renderer).to.equal(renderer);
+ });
+ });
+
+ describe('selecting items', () => {
+ beforeEach(() => {
+ inputElement.focus();
+ });
+
+ it('should update selectedItems when selecting an item on Enter', async () => {
+ await sendKeys({ down: 'ArrowDown' });
+ await sendKeys({ down: 'ArrowDown' });
+ await sendKeys({ down: 'Enter' });
+ expect(comboBox.selectedItems).to.deep.equal(['apple']);
+ });
+
+ it('should update selectedItems when selecting an item on click', async () => {
+ await sendKeys({ down: 'ArrowDown' });
+ const item = document.querySelector('vaadin-multi-select-combo-box-item');
+ item.click();
+ expect(comboBox.selectedItems).to.deep.equal(['apple']);
+ });
+
+ it('should update has-value attribute on selected items change', () => {
+ expect(comboBox.hasAttribute('has-value')).to.be.false;
+ comboBox.selectedItems = ['apple', 'banana'];
+ expect(comboBox.hasAttribute('has-value')).to.be.true;
+ });
+
+ it('should keep has-value attribute after user clears input value', async () => {
+ comboBox.selectedItems = ['apple', 'banana'];
+ await nextRender();
+ await sendKeys({ type: 'o' });
+ await sendKeys({ down: 'Backspace' });
+ expect(comboBox.hasAttribute('has-value')).to.be.true;
+ });
+
+ it('should clear last selected item on Backspace if input has no value', async () => {
+ comboBox.selectedItems = ['apple', 'banana'];
+ await nextRender();
+
+ await sendKeys({ down: 'Backspace' });
+ expect(comboBox.selectedItems).to.deep.equal(['apple']);
+
+ await sendKeys({ down: 'Backspace' });
+ expect(comboBox.selectedItems).to.deep.equal([]);
+ });
+
+ it('should not clear last selected item on Backspace if input has value', async () => {
+ comboBox.selectedItems = ['apple', 'banana'];
+ await nextRender();
+ await sendKeys({ type: 'lemon' });
+
+ await sendKeys({ down: 'Backspace' });
+ expect(comboBox.selectedItems).to.deep.equal(['apple', 'banana']);
+ });
+
+ it('should not clear last selected item on Backspace when readonly', async () => {
+ comboBox.selectedItems = ['apple', 'banana'];
+ await nextRender();
+ comboBox.readonly = true;
+
+ await sendKeys({ down: 'Backspace' });
+ expect(comboBox.selectedItems).to.deep.equal(['apple', 'banana']);
+ });
+
+ it('should clear internal combo-box value when selecting an item', async () => {
+ await sendKeys({ down: 'ArrowDown' });
+ await sendKeys({ type: 'apple' });
+ await sendKeys({ down: 'Enter' });
+ expect(internal.value).to.equal('');
+ expect(inputElement.value).to.equal('');
+ });
+
+ it('should not fire internal value-changed event when selecting an item', async () => {
+ const spy = sinon.spy();
+ internal.addEventListener('value-changed', spy);
+ await sendKeys({ down: 'ArrowDown' });
+ await sendKeys({ type: 'apple' });
+ await sendKeys({ down: 'Enter' });
+ expect(spy.calledOnce).to.be.false;
+ });
+ });
+
+ describe('pageSize', () => {
+ beforeEach(() => {
+ sinon.stub(console, 'error');
+ });
+
+ afterEach(() => {
+ console.error.restore();
+ });
+
+ it('should propagate pageSize property to combo-box', () => {
+ comboBox.pageSize = 25;
+ expect(internal.pageSize).to.equal(25);
+ });
+
+ it('should log error when incorrect pageSize is set', () => {
+ comboBox.pageSize = 0;
+ expect(internal.pageSize).to.equal(50);
+ expect(console.error.calledOnce).to.be.true;
+ });
+ });
+
+ describe('clear button', () => {
+ let clearButton;
+
+ beforeEach(() => {
+ clearButton = comboBox.$.clearButton;
+ comboBox.clearButtonVisible = true;
+ });
+
+ it('should not show clear button when disabled', () => {
+ comboBox.disabled = true;
+ expect(getComputedStyle(clearButton).display).to.equal('none');
+ });
+
+ it('should not show clear button when readonly', () => {
+ comboBox.readonly = true;
+ expect(getComputedStyle(clearButton).display).to.equal('none');
+ });
+
+ it('should not open the dropdown', () => {
+ comboBox.selectedItems = ['apple', 'orange'];
+ clearButton.click();
+ expect(internal.opened).to.be.false;
+ });
+ });
+
+ describe('chips', () => {
+ const getChips = (combo) => combo.shadowRoot.querySelectorAll('[part~="chip"]');
+
+ const getChipContent = (chip) => chip.shadowRoot.querySelector('[part="label"]').textContent;
+
+ beforeEach(async () => {
+ comboBox.selectedItems = ['orange'];
+ await nextRender();
+ });
+
+ describe('programmatic update', () => {
+ it('should re-render chips when selectedItems is updated', async () => {
+ comboBox.selectedItems = ['apple', 'banana'];
+ await nextRender();
+ const chips = getChips(comboBox);
+ expect(chips.length).to.equal(2);
+ expect(getChipContent(chips[0])).to.equal('apple');
+ expect(getChipContent(chips[1])).to.equal('banana');
+ });
+
+ it('should re-render chips when selectedItems is cleared', async () => {
+ comboBox.selectedItems = [];
+ await nextRender();
+ const chips = getChips(comboBox);
+ expect(chips.length).to.equal(0);
+ });
+ });
+
+ describe('manual selection', () => {
+ beforeEach(() => {
+ inputElement.focus();
+ });
+
+ it('should re-render chips when selecting the item', async () => {
+ await sendKeys({ down: 'ArrowDown' });
+ await sendKeys({ down: 'ArrowDown' });
+ await sendKeys({ down: 'Enter' });
+ await nextRender();
+ expect(getChips(comboBox).length).to.equal(2);
+ });
+
+ it('should re-render chips when un-selecting the item', async () => {
+ await sendKeys({ down: 'ArrowDown' });
+ await sendKeys({ type: 'orange' });
+ await sendKeys({ down: 'Enter' });
+ await nextRender();
+ expect(getChips(comboBox).length).to.equal(0);
+ });
+
+ it('should remove chip on remove button click', async () => {
+ const chip = getChips(comboBox)[0];
+ chip.shadowRoot.querySelector('[part="remove-button"]').click();
+ await nextRender();
+ expect(getChips(comboBox).length).to.equal(0);
+ });
+ });
+
+ describe('disabled', () => {
+ beforeEach(async () => {
+ comboBox.selectedItems = ['apple', 'banana'];
+ await nextRender();
+ comboBox.disabled = true;
+ });
+
+ it('should set disabled attribute on all chips when disabled', () => {
+ const chips = getChips(comboBox);
+ expect(chips[0].hasAttribute('disabled')).to.be.true;
+ expect(chips[1].hasAttribute('disabled')).to.be.true;
+ });
+
+ it('should remove disabled attribute from chips when re-enabled', () => {
+ comboBox.disabled = false;
+ const chips = getChips(comboBox);
+ expect(chips[0].hasAttribute('disabled')).to.be.false;
+ expect(chips[1].hasAttribute('disabled')).to.be.false;
+ });
+ });
+ });
+
+ describe('change event', () => {
+ let spy;
+
+ beforeEach(async () => {
+ spy = sinon.spy();
+ comboBox.addEventListener('change', spy);
+ comboBox.selectedItems = ['apple'];
+ await nextRender();
+ inputElement.focus();
+ });
+
+ it('should fire change on user arrow input commit', async () => {
+ await sendKeys({ down: 'ArrowDown' });
+ await sendKeys({ down: 'ArrowDown' });
+ await sendKeys({ down: 'Enter' });
+ expect(spy.calledOnce).to.be.true;
+ });
+
+ it('should fire change on clear button click', () => {
+ comboBox.clearButtonVisible = true;
+ comboBox.$.clearButton.click();
+ expect(spy.calledOnce).to.be.true;
+ });
+
+ it('should fire change when chip is removed', () => {
+ const chip = comboBox.shadowRoot.querySelector('[part="chip"]');
+ chip.shadowRoot.querySelector('[part="remove-button"]').click();
+ expect(spy.calledOnce).to.be.true;
+ });
+ });
+
+ describe('allowCustomValues', () => {
+ beforeEach(async () => {
+ comboBox.allowCustomValues = true;
+ comboBox.selectedItems = ['apple'];
+ await nextRender();
+ inputElement.focus();
+ });
+
+ it('should fire custom-values-set event when entering custom value', async () => {
+ const spy = sinon.spy();
+ comboBox.addEventListener('custom-values-set', spy);
+ await sendKeys({ type: 'pear' });
+ await sendKeys({ down: 'Enter' });
+ expect(spy.calledOnce).to.be.true;
+ });
+
+ it('should clear input element value after entering custom value', async () => {
+ await sendKeys({ type: 'pear' });
+ await sendKeys({ down: 'Enter' });
+ expect(internal.value).to.equal('');
+ });
+
+ it('should not add custom value to selectedItems automatically', async () => {
+ await sendKeys({ type: 'pear' });
+ await sendKeys({ down: 'Enter' });
+ expect(comboBox.selectedItems).to.deep.equal(['apple']);
+ });
+ });
+
+ describe('helper text', () => {
+ it('should set helper text content using helperText property', async () => {
+ comboBox.helperText = 'foo';
+ await nextRender();
+ expect(comboBox.querySelector('[slot="helper"]').textContent).to.eql('foo');
+ });
+
+ it('should display the helper text when slotted helper available', async () => {
+ const helper = document.createElement('div');
+ helper.setAttribute('slot', 'helper');
+ helper.textContent = 'foo';
+ comboBox.appendChild(helper);
+ await nextRender();
+ expect(comboBox.querySelector('[slot="helper"]').textContent).to.eql('foo');
+ });
+ });
+
+ describe('theme attribute', () => {
+ beforeEach(() => {
+ comboBox.setAttribute('theme', 'foo');
+ });
+
+ it('should propagate theme attribute to input container', () => {
+ const inputField = comboBox.shadowRoot.querySelector('[part="input-field"]');
+ expect(inputField.getAttribute('theme')).to.equal('foo');
+ });
+
+ it('should propagate theme attribute to combo-box', () => {
+ expect(comboBox.$.comboBox.getAttribute('theme')).to.equal('foo');
+ });
+ });
+
+ describe('required', () => {
+ beforeEach(() => {
+ comboBox.required = true;
+ });
+
+ it('should be invalid when selectedItems is empty', () => {
+ expect(comboBox.validate()).to.be.false;
+ expect(comboBox.invalid).to.be.true;
+ });
+
+ it('should be valid when selectedItems is not empty', () => {
+ comboBox.selectedItems = ['apple'];
+ expect(comboBox.validate()).to.be.true;
+ expect(comboBox.invalid).to.be.false;
+ });
+
+ it('should focus on required indicator click', () => {
+ comboBox.shadowRoot.querySelector('[part="required-indicator"]').click();
+ expect(comboBox.hasAttribute('focused')).to.be.true;
+ });
+ });
+});
diff --git a/packages/multi-select-combo-box/test/not-animated-styles.js b/packages/multi-select-combo-box/test/not-animated-styles.js
new file mode 100644
index 00000000000..a393ba476df
--- /dev/null
+++ b/packages/multi-select-combo-box/test/not-animated-styles.js
@@ -0,0 +1,13 @@
+import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
+
+registerStyles(
+ 'vaadin-multi-select-combo-box-overlay',
+ css`
+ :host([opening]),
+ :host([closing]),
+ :host([opening]) [part='overlay'],
+ :host([closing]) [part='overlay'] {
+ animation: none !important;
+ }
+ `
+);
diff --git a/packages/multi-select-combo-box/test/typings/multi-select-combo-box.types.ts b/packages/multi-select-combo-box/test/typings/multi-select-combo-box.types.ts
new file mode 100644
index 00000000000..cb357cb124b
--- /dev/null
+++ b/packages/multi-select-combo-box/test/typings/multi-select-combo-box.types.ts
@@ -0,0 +1,99 @@
+import { ComboBoxRenderer } from '@vaadin/combo-box/src/vaadin-combo-box.js';
+import { ControllerMixinClass } from '@vaadin/component-base/src/controller-mixin.js';
+import { DisabledMixinClass } from '@vaadin/component-base/src/disabled-mixin.js';
+import { ElementMixinClass } from '@vaadin/component-base/src/element-mixin.js';
+import { FocusMixinClass } from '@vaadin/component-base/src/focus-mixin.js';
+import { KeyboardMixinClass } from '@vaadin/component-base/src/keyboard-mixin.js';
+import { DelegateFocusMixinClass } from '@vaadin/field-base/src/delegate-focus-mixin.js';
+import { DelegateStateMixinClass } from '@vaadin/field-base/src/delegate-state-mixin.js';
+import { FieldMixinClass } from '@vaadin/field-base/src/field-mixin.js';
+import { InputConstraintsMixinClass } from '@vaadin/field-base/src/input-constraints-mixin.js';
+import { InputControlMixinClass } from '@vaadin/field-base/src/input-control-mixin.js';
+import { InputMixinClass } from '@vaadin/field-base/src/input-mixin.js';
+import { LabelMixinClass } from '@vaadin/field-base/src/label-mixin.js';
+import { ValidateMixinClass } from '@vaadin/field-base/src/validate-mixin.js';
+import { ThemableMixinClass } from '@vaadin/vaadin-themable-mixin';
+import {
+ MultiSelectComboBox,
+ MultiSelectComboBoxChangeEvent,
+ MultiSelectComboBoxCustomValuesSetEvent,
+ MultiSelectComboBoxFilterChangedEvent,
+ MultiSelectComboBoxInvalidChangedEvent,
+ MultiSelectComboBoxSelectedItemsChangedEvent
+} from '../../vaadin-multi-select-combo-box.js';
+
+interface TestComboBoxItem {
+ testProperty: string;
+}
+
+const assertType = (actual: TExpected) => actual;
+
+const genericComboBox = document.createElement('vaadin-multi-select-combo-box');
+assertType(genericComboBox);
+
+const narrowedComboBox = genericComboBox as MultiSelectComboBox;
+
+// Events
+narrowedComboBox.addEventListener('change', (event) => {
+ assertType>(event);
+ assertType>(event.target);
+});
+
+narrowedComboBox.addEventListener('custom-values-set', (event) => {
+ assertType(event);
+ assertType(event.detail);
+});
+
+narrowedComboBox.addEventListener('filter-changed', (event) => {
+ assertType(event);
+ assertType(event.detail.value);
+});
+
+narrowedComboBox.addEventListener('invalid-changed', (event) => {
+ assertType(event);
+ assertType(event.detail.value);
+});
+
+narrowedComboBox.addEventListener('selected-items-changed', (event) => {
+ assertType>(event);
+ assertType>(event.detail.value);
+});
+
+// Properties
+assertType<() => boolean>(narrowedComboBox.checkValidity);
+assertType<() => boolean>(narrowedComboBox.validate);
+assertType(narrowedComboBox.allowCustomValues);
+assertType(narrowedComboBox.autoOpenDisabled);
+assertType(narrowedComboBox.filter);
+assertType(narrowedComboBox.filteredItems);
+assertType(narrowedComboBox.items);
+assertType(narrowedComboBox.itemIdPath);
+assertType(narrowedComboBox.itemLabelPath);
+assertType(narrowedComboBox.itemValuePath);
+assertType | null | undefined>(narrowedComboBox.renderer);
+assertType(narrowedComboBox.invalid);
+assertType(narrowedComboBox.focusElement);
+assertType(narrowedComboBox.disabled);
+assertType(narrowedComboBox.clearButtonVisible);
+assertType(narrowedComboBox.errorMessage);
+assertType(narrowedComboBox.placeholder);
+assertType(narrowedComboBox.helperText);
+assertType(narrowedComboBox.readonly);
+assertType(narrowedComboBox.label);
+assertType(narrowedComboBox.required);
+
+// Mixins
+assertType(narrowedComboBox);
+assertType(narrowedComboBox);
+assertType(narrowedComboBox);
+assertType(narrowedComboBox);
+assertType(narrowedComboBox);
+assertType(narrowedComboBox);
+assertType(narrowedComboBox);
+assertType(narrowedComboBox);
+assertType(narrowedComboBox);
+assertType(narrowedComboBox);
+assertType(narrowedComboBox);
+assertType(narrowedComboBox);
+assertType(narrowedComboBox);
+assertType(narrowedComboBox);
diff --git a/packages/multi-select-combo-box/theme/lumo/vaadin-multi-select-combo-box-chip-styles.js b/packages/multi-select-combo-box/theme/lumo/vaadin-multi-select-combo-box-chip-styles.js
new file mode 100644
index 00000000000..8379408b425
--- /dev/null
+++ b/packages/multi-select-combo-box/theme/lumo/vaadin-multi-select-combo-box-chip-styles.js
@@ -0,0 +1,64 @@
+/**
+ * @license
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+import '@vaadin/vaadin-lumo-styles/color.js';
+import '@vaadin/vaadin-lumo-styles/font-icons.js';
+import '@vaadin/vaadin-lumo-styles/spacing.js';
+import '@vaadin/vaadin-lumo-styles/style.js';
+import '@vaadin/vaadin-lumo-styles/typography.js';
+import { fieldButton } from '@vaadin/vaadin-lumo-styles/mixins/field-button.js';
+import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
+
+const chip = css`
+ :host {
+ display: inline-flex;
+ align-items: center;
+ align-self: center;
+ font-family: var(--lumo-font-family);
+ font-size: var(--lumo-font-size-xxs);
+ line-height: 1;
+ padding: 0.3125em 0 0.3125em calc(0.5em + var(--lumo-border-radius-s) / 4);
+ border-radius: var(--lumo-border-radius-s);
+ border-radius: var(--lumo-border-radius);
+ background-color: var(--lumo-contrast-20pct);
+ cursor: var(--lumo-clickable-cursor);
+ white-space: nowrap;
+ box-sizing: border-box;
+ min-width: 0;
+ }
+
+ [part='label'] {
+ color: var(--lumo-body-text-color);
+ font-weight: 500;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ line-height: 1.25;
+ }
+
+ [part='remove-button'] {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-top: -0.3125em;
+ margin-bottom: -0.3125em;
+ width: var(--lumo-icon-size-s);
+ height: var(--lumo-icon-size-s);
+ font-size: 1.5em;
+ }
+
+ [part='remove-button']::before {
+ content: var(--lumo-icons-cross);
+ }
+
+ :host([disabled]) [part] {
+ color: var(--lumo-disabled-text-color);
+ -webkit-text-fill-color: var(--lumo-disabled-text-color);
+ pointer-events: none;
+ }
+`;
+
+registerStyles('vaadin-multi-select-combo-box-chip', [fieldButton, chip], {
+ moduleId: 'lumo-multi-select-combo-box-chip'
+});
diff --git a/packages/multi-select-combo-box/theme/lumo/vaadin-multi-select-combo-box-styles.js b/packages/multi-select-combo-box/theme/lumo/vaadin-multi-select-combo-box-styles.js
new file mode 100644
index 00000000000..1aa737b9f0d
--- /dev/null
+++ b/packages/multi-select-combo-box/theme/lumo/vaadin-multi-select-combo-box-styles.js
@@ -0,0 +1,33 @@
+/**
+ * @license
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+import '@vaadin/vaadin-lumo-styles/color.js';
+import '@vaadin/vaadin-lumo-styles/font-icons.js';
+import '@vaadin/vaadin-lumo-styles/style.js';
+import '@vaadin/vaadin-lumo-styles/typography.js';
+import { inputFieldShared } from '@vaadin/vaadin-lumo-styles/mixins/input-field-shared.js';
+import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
+
+const multiSelectComboBox = css`
+ :host([has-value]) {
+ padding-inline-start: 0;
+ }
+
+ :host([readonly]) [part~='chip'] {
+ opacity: 0.7;
+ }
+
+ [part~='chip']:not(:last-of-type) {
+ margin-inline-end: var(--lumo-space-xs);
+ }
+
+ [part='toggle-button']::before {
+ content: var(--lumo-icons-dropdown);
+ }
+`;
+
+registerStyles('vaadin-multi-select-combo-box', [inputFieldShared, multiSelectComboBox], {
+ moduleId: 'lumo-multi-select-combo-box'
+});
diff --git a/packages/multi-select-combo-box/theme/lumo/vaadin-multi-select-combo-box.js b/packages/multi-select-combo-box/theme/lumo/vaadin-multi-select-combo-box.js
new file mode 100644
index 00000000000..a046ae2a4c8
--- /dev/null
+++ b/packages/multi-select-combo-box/theme/lumo/vaadin-multi-select-combo-box.js
@@ -0,0 +1,11 @@
+/**
+ * @license
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+import '@vaadin/combo-box/theme/lumo/vaadin-combo-box-item-styles.js';
+import '@vaadin/combo-box/theme/lumo/vaadin-combo-box-dropdown-styles.js';
+import '@vaadin/input-container/theme/lumo/vaadin-input-container.js';
+import './vaadin-multi-select-combo-box-chip-styles.js';
+import './vaadin-multi-select-combo-box-styles.js';
+import '../../src/vaadin-multi-select-combo-box.js';
diff --git a/packages/multi-select-combo-box/theme/material/vaadin-multi-select-combo-box-chip-styles.js b/packages/multi-select-combo-box/theme/material/vaadin-multi-select-combo-box-chip-styles.js
new file mode 100644
index 00000000000..0c1e26f0f44
--- /dev/null
+++ b/packages/multi-select-combo-box/theme/material/vaadin-multi-select-combo-box-chip-styles.js
@@ -0,0 +1,69 @@
+/**
+ * @license
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+import '@vaadin/vaadin-material-styles/color.js';
+import '@vaadin/vaadin-material-styles/font-icons.js';
+import '@vaadin/vaadin-material-styles/typography.js';
+import { fieldButton } from '@vaadin/vaadin-material-styles/mixins/field-button.js';
+import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
+
+const chip = css`
+ :host {
+ display: flex;
+ align-items: center;
+ align-self: center;
+ box-sizing: border-box;
+ height: 1.25rem;
+ margin-inline-end: 0.25rem;
+ padding-inline-start: 0.5rem;
+ border-radius: 4px;
+ background-color: hsla(214, 53%, 23%, 0.1);
+ cursor: default;
+ white-space: nowrap;
+ font-family: var(--material-font-family);
+ }
+
+ [part='label'] {
+ font-size: var(--material-caption-font-size);
+ line-height: 1;
+ color: var(--material-body-text-color);
+ }
+
+ /* Override field button */
+ [part='remove-button'] {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ width: 20px;
+ height: 20px;
+ line-height: 20px;
+ padding: 0;
+ font-size: 0.75em;
+ }
+
+ [part='remove-button']::before {
+ content: var(--material-icons-clear);
+ }
+
+ /* Disabled */
+ :host([disabled]) [part] {
+ pointer-events: none;
+ }
+
+ :host([disabled]) [part='label'] {
+ color: var(--material-disabled-text-color);
+ -webkit-text-fill-color: var(--material-disabled-text-color);
+ }
+
+ :host([disabled]) [part='remove-button'] {
+ color: hsla(0, 0%, 100%, 0.75);
+ -webkit-text-fill-color: hsla(0, 0%, 100%, 0.75);
+ }
+`;
+
+registerStyles('vaadin-multi-select-combo-box-chip', [fieldButton, chip], {
+ moduleId: 'material-multi-select-combo-box-chip'
+});
diff --git a/packages/multi-select-combo-box/theme/material/vaadin-multi-select-combo-box-styles.js b/packages/multi-select-combo-box/theme/material/vaadin-multi-select-combo-box-styles.js
new file mode 100644
index 00000000000..95492f1def7
--- /dev/null
+++ b/packages/multi-select-combo-box/theme/material/vaadin-multi-select-combo-box-styles.js
@@ -0,0 +1,37 @@
+/**
+ * @license
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+import '@vaadin/vaadin-material-styles/color.js';
+import '@vaadin/vaadin-material-styles/font-icons.js';
+import '@vaadin/vaadin-material-styles/typography.js';
+import { inputFieldShared } from '@vaadin/vaadin-material-styles/mixins/input-field-shared.js';
+import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
+
+const multiSelectComboBox = css`
+ :host([readonly]) [part~='chip'] {
+ opacity: 0.5;
+ }
+
+ [part='input-field'] {
+ height: auto;
+ min-height: 32px;
+ }
+
+ [part='input-field'] ::slotted(input) {
+ padding: 6px 0;
+ }
+
+ [part='toggle-button']::before {
+ content: var(--material-icons-dropdown);
+ }
+
+ :host([opened]) [part='toggle-button'] {
+ transform: rotate(180deg);
+ }
+`;
+
+registerStyles('vaadin-multi-select-combo-box', [inputFieldShared, multiSelectComboBox], {
+ moduleId: 'material-multi-select-combo-box'
+});
diff --git a/packages/multi-select-combo-box/theme/material/vaadin-multi-select-combo-box.js b/packages/multi-select-combo-box/theme/material/vaadin-multi-select-combo-box.js
new file mode 100644
index 00000000000..9d66cbf49f9
--- /dev/null
+++ b/packages/multi-select-combo-box/theme/material/vaadin-multi-select-combo-box.js
@@ -0,0 +1,11 @@
+/**
+ * @license
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+import '@vaadin/combo-box/theme/material/vaadin-combo-box-item-styles.js';
+import '@vaadin/combo-box/theme/material/vaadin-combo-box-dropdown-styles.js';
+import '@vaadin/input-container/theme/material/vaadin-input-container.js';
+import './vaadin-multi-select-combo-box-chip-styles.js';
+import './vaadin-multi-select-combo-box-styles.js';
+import '../../src/vaadin-multi-select-combo-box.js';
diff --git a/packages/multi-select-combo-box/vaadin-multi-select-combo-box.d.ts b/packages/multi-select-combo-box/vaadin-multi-select-combo-box.d.ts
new file mode 100644
index 00000000000..aa994473962
--- /dev/null
+++ b/packages/multi-select-combo-box/vaadin-multi-select-combo-box.d.ts
@@ -0,0 +1 @@
+export * from './src/vaadin-multi-select-combo-box.js';
diff --git a/packages/multi-select-combo-box/vaadin-multi-select-combo-box.js b/packages/multi-select-combo-box/vaadin-multi-select-combo-box.js
new file mode 100644
index 00000000000..13ffe54cc84
--- /dev/null
+++ b/packages/multi-select-combo-box/vaadin-multi-select-combo-box.js
@@ -0,0 +1,2 @@
+import './theme/lumo/vaadin-multi-select-combo-box.js';
+export * from './src/vaadin-multi-select-combo-box.js';