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';