All Downloads are FREE. Search and download functionalities are using the official Maven repository.

package.src.vaadin-multi-select-combo-box.js Maven / Gradle / Ivy

There is a newer version: 24.4.10
Show newest version
/**
 * @license
 * Copyright (c) 2021 - 2024 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 { announce } from '@vaadin/a11y-base/src/announce.js';
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
import { processTemplates } from '@vaadin/component-base/src/templates.js';
import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.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`
  :host {
    --input-min-width: var(--vaadin-multi-select-combo-box-input-min-width, 4em);
    --_chip-min-width: var(--vaadin-multi-select-combo-box-chip-min-width, 50px);
  }

  #chips {
    display: flex;
    align-items: center;
  }

  ::slotted(input) {
    box-sizing: border-box;
    flex: 1 0 var(--input-min-width);
  }

  ::slotted([slot='chip']),
  ::slotted([slot='overflow']) {
    flex: 0 1 auto;
  }

  ::slotted([slot='chip']) {
    overflow: hidden;
  }

  :host(:is([readonly], [disabled])) ::slotted(input) {
    flex-grow: 0;
    flex-basis: 0;
    padding: 0;
  }

  :host([auto-expand-vertically]) #chips {
    display: contents;
  }

  :host([auto-expand-horizontally]) [class$='container'] {
    width: auto;
  }
`;

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
 * -----------------------|----------------
 * `chips`                | The element that wraps slotted chips for selected items
 * `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
 * `loading`              | Set when loading items from the data provider
 * `opened`               | Set when the dropdown is open
 * `readonly`             | Set to a readonly element
 *
 * The following custom CSS properties are available for styling:
 *
 * Custom property                                      | Description                | Default
 * -----------------------------------------------------|----------------------------|--------
 * `--vaadin-field-default-width`                       | Default width of the field | `12em`
 * `--vaadin-multi-select-combo-box-overlay-width`      | Width of the overlay       | `auto`
 * `--vaadin-multi-select-combo-box-overlay-max-height` | Max height of the overlay  | `65vh`
 * `--vaadin-multi-select-combo-box-chip-min-width`     | Min width of the chip      | `50px`
 * `--vaadin-multi-select-combo-box-input-min-width`    | Min width of the input     | `4em`
 *
 * ### 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/styling/styling-components) documentation.
 *
 * @fires {Event} change - Fired when the user commits a value change.
 * @fires {CustomEvent} custom-value-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} opened-changed - Fired when the `opened` property changes.
 * @fires {CustomEvent} selected-items-changed - Fired when the `selectedItems` property changes.
 * @fires {CustomEvent} validated - Fired whenever the field is validated.
 *
 * @customElement
 * @extends HTMLElement
 * @mixes ElementMixin
 * @mixes ThemableMixin
 * @mixes InputControlMixin
 * @mixes ResizeMixin
 */
class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(ElementMixin(PolymerElement)))) {
  static get is() {
    return 'vaadin-multi-select-combo-box';
  }

  static get template() {
    return html`
      
`; } static get properties() { return { /** * Set to true to auto expand horizontally, causing input field to * grow until max width is reached. * @attr {boolean} auto-expand-horizontally */ autoExpandHorizontally: { type: Boolean, value: false, reflectToAttribute: true, observer: '_autoExpandHorizontallyChanged', }, /** * Set to true to not collapse selected items chips into the overflow * chip and instead always expand vertically, causing input field to * wrap into multiple lines when width is limited. * @attr {boolean} auto-expand-vertically */ autoExpandVertically: { type: Boolean, value: false, reflectToAttribute: true, observer: '_autoExpandVerticallyChanged', }, /** * Set true to prevent the overlay from opening automatically. * @attr {boolean} auto-open-disabled */ autoOpenDisabled: Boolean, /** * Set to true to display the clear icon which clears the input. * @attr {boolean} clear-button-visible */ clearButtonVisible: { type: Boolean, reflectToAttribute: true, observer: '_clearButtonVisibleChanged', value: false, }, /** * 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, value: 'label', }, /** * 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, value: 'value', }, /** * Path for the id of the item, used to detect whether the item is selected. * @attr {string} item-id-path */ itemIdPath: { type: String, }, /** * The object used to localize this component. * To change the default localization, replace the entire * _i18n_ object or just the property you want to modify. * * The object has the following JSON structure and default values: * ``` * { * // Screen reader announcement on clear button click. * cleared: 'Selection cleared', * // Screen reader announcement when a chip is focused. * focused: ' focused. Press Backspace to remove', * // Screen reader announcement when item is selected. * selected: 'added to selection', * // Screen reader announcement when item is deselected. * deselected: 'removed from selection', * // Screen reader announcement of the selected items count. * // {count} is replaced with the actual count of items. * total: '{count} items selected', * } * ``` * @type {!MultiSelectComboBoxI18n} * @default {English/US} */ i18n: { type: Object, value: () => { return { cleared: 'Selection cleared', focused: 'focused. Press Backspace to remove', selected: 'added to selection', deselected: 'removed from selection', total: '{count} items selected', }; }, }, /** * When true, filter string isn't cleared after selecting an item. */ keepFilter: { type: Boolean, value: false, }, /** * True when loading items from the data provider, false otherwise. */ loading: { type: Boolean, value: false, reflectToAttribute: true, }, /** * A space-delimited list of CSS class names to set on the overlay element. * * @attr {string} overlay-class */ overlayClass: { type: String, }, /** * When present, it specifies that the field is read-only. */ readonly: { type: Boolean, value: false, observer: '_readonlyChanged', reflectToAttribute: true, }, /** * 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, }, /** * Total number of items. */ size: { type: Number, }, /** * 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, }, /** * When true, the user can input a value that is not present in the items list. * @attr {boolean} allow-custom-value */ allowCustomValue: { type: Boolean, value: false, }, /** * A hint to the user of what can be entered in the control. * The placeholder will be only displayed in the case when * there is no item selected. */ placeholder: { type: String, observer: '_placeholderChanged', }, /** * 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, /** * Set to true to group selected items at the top of the overlay. * @attr {boolean} selected-items-on-top */ selectedItemsOnTop: { type: Boolean, value: false, }, /** @private */ value: { type: String, }, /** @private */ _overflowItems: { type: Array, value: () => [], }, /** @private */ _focusedChipIndex: { type: Number, value: -1, observer: '_focusedChipIndexChanged', }, /** @private */ _lastFilter: { type: String, }, /** @private */ _topGroup: { type: Array, }, }; } static get observers() { return [ '_selectedItemsChanged(selectedItems, selectedItems.*)', '__updateOverflowChip(_overflow, _overflowItems, disabled, readonly)', '__updateTopGroup(selectedItemsOnTop, selectedItems, opened)', ]; } /** @protected */ get slotStyles() { const tag = this.localName; return [ ...super.slotStyles, ` ${tag}[has-value] input::placeholder { color: transparent !important; forced-color-adjust: none; } `, ]; } /** * Used by `InputControlMixin` as a reference to the clear button element. * @protected * @return {!HTMLElement} */ get clearElement() { return this.$.clearButton; } /** @protected */ get _chips() { return [...this.querySelectorAll('[slot="chip"]')]; } /** * Override a getter from `InputMixin` to compute * the presence of value based on `selectedItems`. * * @protected * @override */ get _hasValue() { return this.selectedItems && this.selectedItems.length > 0; } /** @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._tooltipController = new TooltipController(this); this.addController(this._tooltipController); this._tooltipController.setPosition('top'); this._tooltipController.setAriaTarget(this.inputElement); this._tooltipController.setShouldShow((target) => !target.opened); this._inputField = this.shadowRoot.querySelector('[part="input-field"]'); this._overflowController = new SlotController(this, 'overflow', 'vaadin-multi-select-combo-box-chip', { initializer: (chip) => { chip.addEventListener('mousedown', (e) => this._preventBlur(e)); this._overflow = chip; }, }); this.addController(this._overflowController); this.__updateChips(); processTemplates(this); } /** * Returns true if the current input value satisfies all constraints (if any). * @return {boolean} */ checkValidity() { return this.required && !this.readonly ? this._hasValue : true; } /** * Clears the selected items. */ clear() { this.__updateSelection([]); announce(this.i18n.cleared); } /** * Clears the cached pages and reloads data from data provider when needed. */ clearCache() { if (this.$ && this.$.comboBox) { this.$.comboBox.clearCache(); } } /** * Requests an update for the content of items. * While performing the update, it invokes the renderer (passed in the `renderer` property) once an item. * * It is not guaranteed that the update happens immediately (synchronously) after it is requested. */ requestContentUpdate() { if (this.$ && this.$.comboBox) { this.$.comboBox.requestContentUpdate(); } } /** * Override method inherited from `DisabledMixin` to forward disabled to chips. * @protected * @override */ _disabledChanged(disabled, oldDisabled) { super._disabledChanged(disabled, oldDisabled); if (disabled || oldDisabled) { this.__updateChips(); } } /** * 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); // Do not validate when focusout is caused by document // losing focus, which happens on browser tab switch. if (!focused && document.hasFocus()) { this._focusedChipIndex = -1; this.validate(); } } /** * Implement callback from `ResizeMixin` to update chips. * @protected * @override */ _onResize() { this.__updateChips(); } /** * Override method from `DelegateStateMixin` to set required state * using `aria-required` attribute instead of `required`, in order * to prevent screen readers from announcing "invalid entry". * @protected * @override */ _delegateAttribute(name, value) { if (!this.stateTarget) { return; } if (name === 'required') { this._delegateAttribute('aria-required', value ? 'true' : false); return; } super._delegateAttribute(name, value); } /** @private */ _autoExpandHorizontallyChanged(autoExpand, oldAutoExpand) { if (autoExpand || oldAutoExpand) { this.__updateChips(); } } /** @private */ _autoExpandVerticallyChanged(autoExpand, oldAutoExpand) { if (autoExpand || oldAutoExpand) { this.__updateChips(); } } /** * Setting clear button visible reduces total space available * for rendering chips, and making it hidden increases it. * @private */ _clearButtonVisibleChanged(visible, oldVisible) { if (visible || oldVisible) { this.__updateChips(); } } /** * Implement two-way binding for the `filteredItems` property * that can be set on the internal combo-box element. * * @param {CustomEvent} event * @private */ _onFilteredItemsChanged(event) { const { value } = event.detail; if (Array.isArray(value) || value == null) { this.filteredItems = value; } } /** @private */ _readonlyChanged(readonly, oldReadonly) { if (readonly || oldReadonly) { this.__updateChips(); } if (this.dataProvider) { this.clearCache(); } } /** @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 */ _placeholderChanged(placeholder) { const tmpPlaceholder = this.__tmpA11yPlaceholder; // Do not store temporary placeholder if (tmpPlaceholder !== placeholder) { this.__savedPlaceholder = placeholder; if (tmpPlaceholder) { this.placeholder = tmpPlaceholder; } } } /** @private */ _selectedItemsChanged(selectedItems) { this._toggleHasValue(this._hasValue); // Use placeholder for announcing items if (this._hasValue) { const tmpPlaceholder = this._mergeItemLabels(selectedItems); if (this.__tmpA11yPlaceholder === undefined) { this.__savedPlaceholder = this.placeholder; } this.__tmpA11yPlaceholder = tmpPlaceholder; this.placeholder = tmpPlaceholder; } else if (this.__tmpA11yPlaceholder !== undefined) { delete this.__tmpA11yPlaceholder; this.placeholder = this.__savedPlaceholder; } // Re-render chips this.__updateChips(); // Update selected for dropdown items this.requestContentUpdate(); if (this.opened) { this.$.comboBox.$.overlay._updateOverlayWidth(); } } /** @private */ _getItemLabel(item) { return this.$.comboBox._getItemLabel(item); } /** @private */ _mergeItemLabels(items) { return items.map((item) => this._getItemLabel(item)).join(', '); } /** @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); } /** * Clear the internal combo box value and filter. Filter will not be cleared * when the `keepFilter` option is enabled. Using `force` can enforce clearing * the filter. * @param {boolean} force overrides the keepFilter option * @private */ __clearInternalValue(force = false) { if (!this.keepFilter || force) { // Clear both combo box value and filter. this.filter = ''; this.$.comboBox.clear(); } else { // Only clear combo box value. This effectively resets _lastCommittedValue // which allows toggling the same item multiple times via keyboard. this.$.comboBox.clear(); // Restore input to the filter value. Needed when items are // navigated with keyboard, which overrides the input value // with the item label. this._inputElementValue = this.filter; } } /** @private */ __announceItem(itemLabel, isSelected, itemCount) { const state = isSelected ? 'selected' : 'deselected'; const total = this.i18n.total.replace('{count}', itemCount || 0); announce(`${itemLabel} ${this.i18n[state]} ${total}`); } /** @private */ __removeItem(item) { const itemsCopy = [...this.selectedItems]; itemsCopy.splice(itemsCopy.indexOf(item), 1); this.__updateSelection(itemsCopy); const itemLabel = this._getItemLabel(item); this.__announceItem(itemLabel, false, itemsCopy.length); } /** @private */ __selectItem(item) { const itemsCopy = [...this.selectedItems]; const index = this._findIndex(item, itemsCopy, this.itemIdPath); const itemLabel = this._getItemLabel(item); let isSelected = false; if (index !== -1) { const lastFilter = this._lastFilter; // Do not unselect when manually typing and committing an already selected item. if (lastFilter && lastFilter.toLowerCase() === itemLabel.toLowerCase()) { this.__clearInternalValue(); return; } itemsCopy.splice(index, 1); } else { itemsCopy.push(item); isSelected = true; } this.__updateSelection(itemsCopy); // Suppress `value-changed` event. this.__clearInternalValue(); this.__announceItem(itemLabel, isSelected, itemsCopy.length); } /** @private */ __updateSelection(selectedItems) { this.selectedItems = selectedItems; this.validate(); this.dispatchEvent(new CustomEvent('change', { bubbles: true })); } /** @private */ __updateTopGroup(selectedItemsOnTop, selectedItems, opened) { if (!selectedItemsOnTop) { this._topGroup = []; } else if (!opened) { this._topGroup = [...selectedItems]; } } /** @private */ __createChip(item) { const chip = document.createElement('vaadin-multi-select-combo-box-chip'); chip.setAttribute('slot', 'chip'); chip.item = item; chip.disabled = this.disabled; chip.readonly = this.readonly; const label = this._getItemLabel(item); chip.label = label; chip.setAttribute('title', label); chip.addEventListener('item-removed', (e) => this._onItemRemoved(e)); chip.addEventListener('mousedown', (e) => this._preventBlur(e)); return chip; } /** @private */ __getOverflowWidth() { const chip = this._overflow; chip.style.visibility = 'hidden'; chip.removeAttribute('hidden'); const count = chip.getAttribute('count'); // Detect max possible width of the overflow chip // by measuring it with widest number (2 digits) chip.setAttribute('count', '99'); const overflowStyle = getComputedStyle(chip); const overflowWidth = chip.clientWidth + parseInt(overflowStyle.marginInlineStart); chip.setAttribute('count', count); chip.setAttribute('hidden', ''); chip.style.visibility = ''; return overflowWidth; } /** @private */ __updateChips() { if (!this._inputField || !this.inputElement) { return; } // Clear all chips except the overflow this._chips.forEach((chip) => { chip.remove(); }); const items = [...this.selectedItems]; // Detect available remaining width for chips const totalWidth = this._inputField.$.wrapper.clientWidth; const inputWidth = parseInt(getComputedStyle(this.inputElement).flexBasis); let remainingWidth = totalWidth - inputWidth; if (items.length > 1) { remainingWidth -= this.__getOverflowWidth(); } const chipMinWidth = parseInt(getComputedStyle(this).getPropertyValue('--_chip-min-width')); if (this.autoExpandHorizontally) { const chips = []; // First, add all chips to make the field fully expand for (let i = items.length - 1, refNode = null; i >= 0; i--) { const chip = this.__createChip(items[i]); this.insertBefore(chip, refNode); refNode = chip; chips.unshift(chip); } const overflowItems = []; const availableWidth = this._inputField.$.wrapper.clientWidth - this.$.chips.clientWidth; // When auto expanding vertically, no need to measure width if (!this.autoExpandVertically && availableWidth < inputWidth) { // Always show at least last item as a chip while (chips.length > 1) { const lastChip = chips.pop(); lastChip.remove(); overflowItems.unshift(items.pop()); // Remove chips until there is enough width for the input element to fit const neededWidth = overflowItems.length > 0 ? inputWidth + this.__getOverflowWidth() : inputWidth; if (this._inputField.$.wrapper.clientWidth - this.$.chips.clientWidth >= neededWidth) { break; } } if (chips.length === 1) { chips[0].style.maxWidth = `${Math.max(chipMinWidth, remainingWidth)}px`; } } this._overflowItems = overflowItems; return; } // Add chips until remaining width is exceeded for (let i = items.length - 1, refNode = null; i >= 0; i--) { const chip = this.__createChip(items[i]); this.insertBefore(chip, refNode); // When auto expanding vertically, no need to measure remaining width if (!this.autoExpandVertically && this.$.chips.clientWidth > remainingWidth) { // Always show at least last selected item as a chip if (refNode === null) { chip.style.maxWidth = `${Math.max(chipMinWidth, remainingWidth)}px`; } else { chip.remove(); break; } } items.pop(); refNode = chip; } this._overflowItems = items; } /** @private */ __updateOverflowChip(overflow, items, disabled, readonly) { if (overflow) { const count = items.length; overflow.label = `${count}`; overflow.setAttribute('count', `${count}`); overflow.setAttribute('title', this._mergeItemLabels(items)); overflow.toggleAttribute('hidden', count === 0); overflow.disabled = disabled; overflow.readonly = readonly; } } /** @private */ _onClearButtonTouchend(event) { // Cancel the following click and focus events event.preventDefault(); // Prevent default combo box behavior which can otherwise unnecessarily // clear the input and filter event.stopPropagation(); this.clear(); } /** * Override method inherited from `InputControlMixin` and clear items. * @protected * @override */ _onClearButtonClick(event) { event.stopPropagation(); this.clear(); } /** * Override an event listener from `InputControlMixin` to * stop the change event re-targeted from the input. * * @param {!Event} event * @protected * @override */ _onChange(event) { event.stopPropagation(); } /** * Override an event listener from `KeyboardMixin`. * Do not call `super` in order to override clear * button logic defined in `InputControlMixin`. * * @param {!KeyboardEvent} event * @protected * @override */ _onEscape(event) { if (this.clearButtonVisible && this.selectedItems && this.selectedItems.length) { event.stopPropagation(); this.selectedItems = []; } } /** * Override an event listener from `KeyboardMixin`. * @param {KeyboardEvent} event * @protected * @override */ _onKeyDown(event) { super._onKeyDown(event); const chips = this._chips; if (!this.readonly && chips.length > 0) { switch (event.key) { case 'Backspace': this._onBackSpace(chips); break; case 'ArrowLeft': this._onArrowLeft(chips, event); break; case 'ArrowRight': this._onArrowRight(chips, event); break; default: this._focusedChipIndex = -1; break; } } } /** @private */ _onArrowLeft(chips, event) { if (this.inputElement.selectionStart !== 0) { return; } const idx = this._focusedChipIndex; if (idx !== -1) { event.preventDefault(); } let newIdx; if (!this.__isRTL) { if (idx === -1) { // Focus last chip newIdx = chips.length - 1; } else if (idx > 0) { // Focus prev chip newIdx = idx - 1; } } else if (idx === chips.length - 1) { // Blur last chip newIdx = -1; } else if (idx > -1) { // Focus next chip newIdx = idx + 1; } if (newIdx !== undefined) { this._focusedChipIndex = newIdx; } } /** @private */ _onArrowRight(chips, event) { if (this.inputElement.selectionStart !== 0) { return; } const idx = this._focusedChipIndex; if (idx !== -1) { event.preventDefault(); } let newIdx; if (this.__isRTL) { if (idx === -1) { // Focus last chip newIdx = chips.length - 1; } else if (idx > 0) { // Focus prev chip newIdx = idx - 1; } } else if (idx === chips.length - 1) { // Blur last chip newIdx = -1; } else if (idx > -1) { // Focus next chip newIdx = idx + 1; } if (newIdx !== undefined) { this._focusedChipIndex = newIdx; } } /** @private */ _onBackSpace(chips) { if (this.inputElement.selectionStart !== 0) { return; } const idx = this._focusedChipIndex; if (idx === -1) { this._focusedChipIndex = chips.length - 1; } else { this.__removeItem(chips[idx].item); this._focusedChipIndex = -1; } } /** @private */ _focusedChipIndexChanged(focusedIndex, oldFocusedIndex) { if (focusedIndex > -1 || oldFocusedIndex > -1) { const chips = this._chips; chips.forEach((chip, index) => { chip.toggleAttribute('focused', index === focusedIndex); }); // Announce focused chip if (focusedIndex > -1) { const item = chips[focusedIndex].item; const itemLabel = this._getItemLabel(item); announce(`${itemLabel} ${this.i18n.focused}`); } } } /** @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(); // Stop the original event event.stopPropagation(); this.__clearInternalValue(true); this.dispatchEvent( new CustomEvent('custom-value-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(); } /** * Fired when the user sets a custom value. * @event custom-value-set * @param {string} detail the custom value */ } defineCustomElement(MultiSelectComboBox); export { MultiSelectComboBox };




© 2015 - 2024 Weber Informatics LLC | Privacy Policy