@universal-material/web
Version:
Material web components
222 lines • 8.02 kB
JavaScript
import { __decorate } from "tslib";
import { html, render, svg } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { map } from 'lit/directives/map.js';
import { html as staticHtml } from 'lit/static-html.js';
import { UmTextFieldBase } from '../shared/text-field-base/text-field-base.js';
import { UmOption } from './option.js';
import { SelectNavigationController } from './select-navigation-controller.js';
import { styles } from './select.styles.js';
import './option.js';
let UmSelect = class UmSelect extends UmTextFieldBase {
static { this.styles = [UmTextFieldBase.styles, styles]; }
#list;
#navigationController;
#resizeObserver;
#connected;
/**
* The `value` of the selected option
*/
get value() {
return this._nativeSelect.value;
}
set value(value) {
this._nativeSelect.value = value;
if (!this.#connected) {
return;
}
this.elementInternals.setFormValue(value);
for (const option of this._options) {
option._writeNativeSelected();
}
}
/**
* The index of the selected option. When there's no selected option the value is `-1`.
*/
get selectedIndex() {
return this._nativeSelect.selectedIndex;
}
set selectedIndex(index) {
this._nativeSelect.selectedIndex = index;
}
/**
* An `Array` containing the selected `UmOption` or empty if there's no selected option. Multiple selection is not supported.
*/
get selectedOptions() {
return this._nativeSelect.selectedOptions.length
? [this._options[this._nativeSelect.selectedIndex]]
: [];
}
get _options() {
const options = Array.from(this.querySelectorAll('u-option'));
return options.filter(o => o instanceof UmOption);
}
constructor() {
super();
this._nativeSelect = document.createElement('select');
this.#list = (() => {
const list = document.createElement('div');
list.role = 'listbox';
list.id = 'list';
list.className = 'list';
return list;
})();
this.#navigationController = new SelectNavigationController(this);
this.#resizeObserver = new ResizeObserver(() => this.#setMenuWidthProperty());
this.#connected = false;
this.menuPositioning = 'relative';
this.#handleClick = (e) => {
this._menu.toggle();
if (!this._menu.open || !this.selectedOptions.length) {
return;
}
this.#navigationController.focusMenu(this.selectedOptions[0], 0, e.detail === 0, false);
};
this.#handleMenuOpen = () => {
this._button.setAttribute('aria-expanded', 'true');
};
this.#handleMenuOpened = () => {
if (!this.selectedOptions.length) {
return;
}
const option = this.selectedOptions[0];
option.scrollIntoView({ block: 'nearest' });
};
this.#handleMenuClose = () => {
this._button.setAttribute('aria-expanded', 'false');
this.#navigationController.blurMenu();
};
this._nativeSelect.setAttribute('tabindex', '-1');
this._nativeSelect.setAttribute('part', 'select');
}
#setMenuWidthProperty() {
this.style.setProperty('--_menu-width', `${this.clientWidth}px`);
}
renderControl() {
return staticHtml `
<button
class="button"
role="combobox"
aria-expanded="false"
aria-owns="select"
?disabled=${this.disabled}></button>
<div class="input"></div>`;
}
renderAfterContent() {
return html `
<u-menu positioning="${this.menuPositioning}">
<slot =${this._renderOptionRelatedElements}></slot>
</u-menu>
`;
}
renderDefaultTrailingIcon() {
return svg `
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 -960 960 960" width="1em" fill="currentColor">
<path d="M480-360 280-560h400L480-360Z"/>
</svg>`;
}
updated(changedProperties) {
super.updated(changedProperties);
this.empty = !this.selectedOptions[0]?.textContent?.trim();
this.elementInternals.setFormValue(this._nativeSelect.value || null);
}
attributeChangedCallback(name, _old, value) {
super.attributeChangedCallback(name, _old, value);
if (name !== 'disabled') {
return;
}
this._nativeSelect.disabled = value === null;
}
connectedCallback() {
super.connectedCallback();
this.#connected = true;
this.#attach();
}
disconnectedCallback() {
super.disconnectedCallback();
this.#detach();
}
#handleClick;
#handleMenuClick(e) {
e.stopPropagation();
}
#handleMenuOpen;
#handleMenuOpened;
#handleMenuClose;
async #attach() {
this.#resizeObserver.observe(this);
this._renderOptionRelatedElements();
await this.updateComplete;
this._nativeSelect.disabled = this.hasAttribute('disabled');
this.#navigationController.attach(this);
this._input.appendChild(this._nativeSelect);
this._input.appendChild(this.#list);
this._button.addEventListener('click', this.#handleClick);
this._menu.anchorElement = this._container;
this._menu.addEventListener('click', this.#handleMenuClick);
this._menu.addEventListener('open', this.#handleMenuOpen);
this._menu.addEventListener('opened', this.#handleMenuOpened);
this._menu.addEventListener('close', this.#handleMenuClose);
}
#detach() {
this.#resizeObserver.disconnect();
this._nativeSelect.remove();
this.#list.remove();
this.#navigationController.detach();
this.#connected = false;
this._button.removeEventListener('click', this.#handleClick);
this._menu.removeEventListener('click', this.#handleMenuClick);
this._menu.removeEventListener('open', this.#handleMenuOpen);
this._menu.removeEventListener('opened', this.#handleMenuOpened);
this._menu.removeEventListener('close', this.#handleMenuClose);
}
_renderOptionRelatedElements() {
this.#renderNativeOptions();
this.#renderAccessibilityList();
this._updateEmpty();
this._syncSelectedOptions();
}
_updateEmpty() {
this.empty = !this.selectedOptions[0]?.textContent?.trim();
}
get _menuItems() {
return this._options;
}
#renderNativeOptions() {
render(map(this._options, option => html `<option value=${option.value} ?selected=${option.selected}>${option.textContent}</option>`), this._nativeSelect);
}
#renderAccessibilityList() {
render(map(this._options, (option, index) => html `<div role="option" id="${`item-${index + 1}`}">${option.textContent}</div>`), this.#list);
}
_syncSelectedOptions() {
for (let i = 0; i < this._options.length; i++) {
const option = this._options[i];
const nativeOption = this._nativeSelect.children[i];
option.selected = nativeOption.selected;
option._nativeOption = nativeOption;
}
}
};
__decorate([
state()
], UmSelect.prototype, "value", null);
__decorate([
query('u-menu', true)
], UmSelect.prototype, "_menu", void 0);
__decorate([
query('.button', true)
], UmSelect.prototype, "_button", void 0);
__decorate([
query('.input', true)
], UmSelect.prototype, "_input", void 0);
__decorate([
property({ reflect: true, attribute: 'menu-positioning' })
], UmSelect.prototype, "menuPositioning", void 0);
__decorate([
state()
], UmSelect.prototype, "selectedIndex", null);
UmSelect = __decorate([
customElement('u-select')
], UmSelect);
export { UmSelect };
//# sourceMappingURL=select.js.map