@telekom/scale-components
Version:
Scale is the digital design system for Telekom products and experiences.
675 lines (674 loc) • 20.7 kB
JavaScript
import { Component, h, Host, Prop, Element, State, Watch, Event, } from '@stencil/core';
import classNames from 'classnames';
import statusNote from '../../utils/status-note';
import { computePosition } from '@floating-ui/dom';
import { emitEvent, generateUniqueId } from '../../utils/utils';
var Actions;
(function (Actions) {
Actions["Close"] = "Close";
Actions["CloseSelect"] = "CloseSelect";
Actions["First"] = "First";
Actions["Last"] = "Last";
Actions["Next"] = "Next";
Actions["Open"] = "Open";
Actions["PageDown"] = "PageDown";
Actions["PageUp"] = "PageUp";
Actions["Previous"] = "Previous";
Actions["Select"] = "Select";
Actions["Type"] = "Type";
})(Actions || (Actions = {}));
const DEFAULT_ICON_SIZE = 20;
const isElementValue = (x) => typeof x.value === 'string';
const readValue = (element) => isElementValue(element) ? element.value : null;
const readOptions = (hostElement) => {
return Array.from(hostElement.children).map((x) => {
var _a;
return ({
label: x.textContent.trim(),
value: (_a = x.getAttribute('value')) !== null && _a !== void 0 ? _a : readValue(x),
ItemElement: h("span", { innerHTML: x.outerHTML }),
});
});
};
function getActionFromKey(event, open) {
const { key, altKey, ctrlKey, metaKey } = event;
if (!open && ['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(key)) {
return Actions['Open'];
}
if (key === 'Home') {
return Actions['First'];
}
if (key === 'End') {
return Actions['Last'];
}
if (['Backspace', 'Clear'].includes(key) ||
(key.length === 1 && key !== ' ' && !altKey && !ctrlKey && !metaKey)) {
return Actions['Type'];
}
if (!open) {
return;
}
if (key === 'ArrowUp' && altKey) {
return Actions['CloseSelect'];
}
if (key === 'ArrowDown' && !altKey) {
return Actions['Next'];
}
switch (key) {
case 'ArrowUp':
return Actions['Previous'];
case 'PageUp':
return Actions['PageUp'];
case 'PageDown':
return Actions['PageDown'];
case 'Escape':
return Actions['Close'];
case 'Enter':
return Actions['CloseSelect'];
case ' ':
return Actions['CloseSelect'];
}
}
function jumpToIndex(currentIndex, maxIndex, action) {
const JUMP_SIZE = 10;
switch (action) {
case Actions['First']:
return 0;
case Actions['Last']:
return maxIndex;
case Actions['Previous']:
return Math.max(0, currentIndex - 1);
case Actions['Next']:
return Math.min(maxIndex, currentIndex + 1);
case Actions['PageUp']:
return Math.max(0, currentIndex - JUMP_SIZE);
case Actions['PageDown']:
return Math.min(maxIndex, currentIndex + JUMP_SIZE);
default:
return currentIndex;
}
}
function matchOptions(options = [], filter) {
return options.filter((option) => option.toLowerCase().indexOf(filter.toLowerCase()) === 0);
}
function getIndexByChar(values, filter, startIndex = 0) {
const sortedValues = [
...values.slice(startIndex),
...values.slice(0, startIndex),
];
const firstHit = matchOptions(sortedValues, filter)[0];
const allMatchingChars = (array) => array.every((char) => char === array[0]);
if (firstHit) {
return values.indexOf(firstHit);
}
if (allMatchingChars(filter.split(''))) {
const hits = matchOptions(sortedValues, filter[0]);
return values.indexOf(hits[0]);
}
return -1;
}
function keepInView(activeElement, scrollParent) {
const { offsetHeight, offsetTop } = activeElement;
const { offsetHeight: parentOffsetHeight, scrollTop } = scrollParent;
const isAboveParent = offsetTop < scrollTop;
const isBelowParent = offsetTop + offsetHeight > scrollTop + parentOffsetHeight;
if (isBelowParent) {
return (scrollParent.scrollTop =
offsetTop + offsetHeight - parentOffsetHeight);
}
if (isAboveParent) {
return (scrollParent.scrollTop = offsetTop);
}
}
function hasOverflow(element) {
return element && element.clientHeight < element.scrollHeight;
}
function isInView(element) {
const rect = element.getBoundingClientRect();
const parentRect = {
top: 0,
left: 0,
right: window.innerWidth || document.documentElement.clientWidth,
bottom: window.innerHeight || document.documentElement.clientHeight,
};
return (rect.top >= parentRect.top &&
rect.left >= parentRect.left &&
rect.bottom <= parentRect.bottom &&
rect.right <= parentRect.right);
}
export class DropdownSelect {
constructor() {
this.comboboxId = 'combobox';
this.helperText = '';
this.invalid = false;
this.variant = 'informational';
/** @see {@url https://floating-ui.com/docs/computePosition#strategy} */
this.floatingStrategy = 'absolute';
/** (optional) to hide the label */
this.hideLabelVisually = false;
/** (optional) Screen reader text appended to the selected element */
this.ariaLabelSelected = 'selected';
/** (optional) Text displayed in high contrast mode only to indicate disabled state */
this.hcmLabelDisabled = 'this field is disabled';
this.options = '';
this.open = false;
this.currentIndex = -1;
this.queryString = '';
this.queryTimeout = null;
this.ignoreBlur = false;
this.hasFocus = false;
this.selectOption = (index) => {
this.currentIndex = index;
this.value = readOptions(this.hostElement)[index].value;
emitEvent(this, 'scaleChange', { value: this.value });
};
this.handleKeyDown = (event) => {
const { key } = event;
const max = readOptions(this.hostElement).length - 1;
const action = getActionFromKey(event, this.open);
emitEvent(this, 'scaleKeydown', event);
switch (action) {
case Actions['Last']:
case Actions['First']:
this.setOpen(true);
case Actions['Next']:
case Actions['Previous']:
case Actions['PageUp']:
case Actions['PageDown']:
event.preventDefault();
return this.handleOptionChange(jumpToIndex(this.currentIndex, max, action));
case Actions['CloseSelect']:
event.preventDefault();
this.selectOption(this.currentIndex);
case Actions['Close']:
event.preventDefault();
return this.setOpen(false);
case Actions['Type']:
return this.buildQueryString(key);
case Actions['Open']:
event.preventDefault();
return this.setOpen(true);
}
};
this.handleBlur = () => {
if (this.ignoreBlur) {
this.ignoreBlur = false;
return;
}
this.setOpen(false);
emitEvent(this, 'scaleBlur');
};
this.handleFocus = () => {
emitEvent(this, 'scaleFocus');
};
this.handleClick = () => {
this.setOpen(!this.open);
const indexOfValue = readOptions(this.hostElement).findIndex(({ value }) => value === this.value);
if (indexOfValue > -1) {
setTimeout(() => {
this.bringIntoView(indexOfValue);
});
}
};
}
valueChange(newValue) {
this.currentIndex = readOptions(this.hostElement).findIndex(({ value }) => value === newValue);
}
connectedCallback() {
if (this.hostElement.closest('scale-tab-panel')) {
this.ignoreBlur = true;
}
statusNote({ source: this.hostElement, tag: 'beta' });
this.currentIndex =
readOptions(this.hostElement).findIndex(({ value }) => value === this.value) || -1;
}
componentDidRender() {
if (!this.open) {
return;
}
if (this.floatingStrategy === 'fixed') {
this.listboxPadEl.style.width = `${this.comboEl.getBoundingClientRect().width}px`;
}
computePosition(this.comboEl, this.listboxPadEl, {
placement: 'bottom',
strategy: this.floatingStrategy,
}).then(({ x, y }) => {
Object.assign(this.listboxPadEl.style, {
left: `${x}px`,
top: `${y}px`,
});
});
}
handleOptionChange(index) {
this.currentIndex = index;
this.bringIntoView(index);
}
bringIntoView(index) {
const options = this.listboxEl.querySelectorAll('[role=option]');
if (hasOverflow(this.listboxEl)) {
keepInView(options[index], this.listboxEl);
}
if (!isInView(options[index])) {
options[index].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
setOpen(open) {
if (this.open === open) {
return;
}
if (this.disabled) {
return;
}
this.open = open;
if (!this.open) {
this.comboEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
this.comboEl.focus();
this.currentIndex = -1;
}
}
handleOptionClick(event, index) {
event.stopPropagation();
this.handleOptionChange(index);
this.selectOption(index);
this.setOpen(false);
}
getSearchString(char) {
if (typeof this.queryTimeout === 'number') {
window.clearTimeout(this.queryTimeout);
}
this.queryTimeout = window.setTimeout(() => {
this.queryString = '';
}, 500);
this.queryString += char;
return this.queryString;
}
buildQueryString(char) {
this.setOpen(true);
const queryString = this.getSearchString(char);
const queryIndex = getIndexByChar(readOptions(this.hostElement).map(({ label }) => label), queryString, this.currentIndex + 1);
if (queryIndex >= 0) {
this.handleOptionChange(queryIndex);
}
else {
window.clearTimeout(this.queryTimeout);
this.queryString = '';
}
}
render() {
const ValueElement = (readOptions(this.hostElement).find(({ value }) => value === this.value) ||
{}).ItemElement;
const hasEmptyValueElement = (readOptions(this.hostElement).find(({ value }) => value === this.value) || {}).value === ''
? true
: false;
const helperTextId = `helper-message-${generateUniqueId()}`;
const ariaDescribedByAttr = { 'aria-describedBy': helperTextId };
return (h(Host, null,
h("div", { part: this.getBasePartMap() },
h("div", { part: "combobox-container" },
h("label", { id: `${this.comboboxId}-label`, part: "label" }, this.label),
h("div", Object.assign({ ref: (el) => (this.comboEl = el), "aria-controls": `${this.comboboxId}-listbox`, "aria-expanded": this.open ? 'true' : 'false', "aria-haspopup": "listbox", "aria-labelledby": `${this.comboboxId}-label`, id: this.comboboxId, part: "combobox", role: "combobox", tabindex: "0", onBlur: this.handleBlur, onFocus: this.handleFocus, onClick: this.handleClick, onKeyDown: this.handleKeyDown }, (this.open
? {
'aria-activedescendant': (readOptions(this.hostElement)[this.currentIndex] ||
{}).value,
}
: {}), (this.helperText ? ariaDescribedByAttr : {}), (this.invalid ? { 'aria-invalid': 'true' } : {})),
h("span", { part: "combobox-value" }, hasEmptyValueElement ? '' : ValueElement)),
h("div", { part: "listbox-pad", ref: (el) => (this.listboxPadEl = el) },
h("div", { part: "listbox-scroll-container" },
h("div", { ref: (el) => (this.listboxEl = el), part: "listbox", role: "listbox", id: `${this.comboboxId}-listbox`, "aria-labelledby": `${this.comboboxId}-label`, tabindex: "-1" }, readOptions(this.hostElement).map(({ value, ItemElement }, index) => (h("div", Object.assign({ role: "option", part: `option${index === this.currentIndex ? ' current' : ''}`, id: value, onClick: (event) => {
this.handleOptionClick(event, index);
}, onMouseDown: () => {
this.ignoreBlur = true;
} }, (value === this.value
? { 'aria-selected': 'true' }
: {})),
ItemElement,
value === this.value ? (h("div", null,
h("scale-icon-action-checkmark", { size: 16 }),
h("span", { class: "sr-only" }, this.ariaLabelSelected))) : null)))))),
h("div", { part: "icon" }, this.open ? (h("scale-icon-navigation-collapse-up", { decorative: true, size: DEFAULT_ICON_SIZE })) : (h("scale-icon-navigation-collapse-down", { decorative: true, size: DEFAULT_ICON_SIZE })))),
this.helperText && (h("scale-helper-text", { helperText: this.helperText, variant: this.invalid ? 'danger' : this.variant, id: helperTextId })),
this.disabled && (h("div", { class: "hcm-disabled" }, this.hcmLabelDisabled)))));
}
getBasePartMap() {
const animated = this.value != null && this.value !== '';
return classNames('select', this.open && `open`, this.disabled && `disabled`, this.readonly && `readonly`, this.transparent && 'transparent', this.invalid && `invalid`, this.currentIndex > -1 && `steal-focus`, animated && 'animated', this.helperText && 'has-helper-text', this.floatingStrategy && `strategy-${this.floatingStrategy}`, this.hideLabelVisually && 'hide-label');
}
static get is() { return "scale-dropdown-select"; }
static get encapsulation() { return "shadow"; }
static get originalStyleUrls() { return {
"$": ["dropdown-select.css"]
}; }
static get styleUrls() { return {
"$": ["dropdown-select.css"]
}; }
static get properties() { return {
"comboboxId": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "combobox-id",
"reflect": false,
"defaultValue": "'combobox'"
},
"label": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": ""
},
"attribute": "label",
"reflect": false
},
"name": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "name",
"reflect": false
},
"helperText": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "helper-text",
"reflect": false,
"defaultValue": "''"
},
"disabled": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "disabled",
"reflect": false
},
"readonly": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "readonly",
"reflect": false
},
"transparent": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "transparent",
"reflect": false
},
"invalid": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "invalid",
"reflect": false,
"defaultValue": "false"
},
"variant": {
"type": "string",
"mutable": false,
"complexType": {
"original": "'informational' | 'warning' | 'danger' | 'success'",
"resolved": "\"danger\" | \"informational\" | \"success\" | \"warning\"",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": ""
},
"attribute": "variant",
"reflect": false,
"defaultValue": "'informational'"
},
"value": {
"type": "any",
"mutable": true,
"complexType": {
"original": "any",
"resolved": "any",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": ""
},
"attribute": "value",
"reflect": true
},
"floatingStrategy": {
"type": "string",
"mutable": false,
"complexType": {
"original": "'absolute' | 'fixed'",
"resolved": "\"absolute\" | \"fixed\"",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [{
"name": "see",
"text": "{"
}, {
"name": "url",
"text": "https://floating-ui.com/docs/computePosition#strategy}"
}],
"text": ""
},
"attribute": "floating-strategy",
"reflect": false,
"defaultValue": "'absolute'"
},
"hideLabelVisually": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) to hide the label"
},
"attribute": "hide-label-visually",
"reflect": false,
"defaultValue": "false"
},
"ariaLabelSelected": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Screen reader text appended to the selected element"
},
"attribute": "aria-label-selected",
"reflect": false,
"defaultValue": "'selected'"
},
"hcmLabelDisabled": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Text displayed in high contrast mode only to indicate disabled state"
},
"attribute": "hcm-label-disabled",
"reflect": false,
"defaultValue": "'this field is disabled'"
}
}; }
static get states() { return {
"options": {},
"open": {},
"currentIndex": {},
"queryString": {},
"queryTimeout": {},
"ignoreBlur": {},
"hasFocus": {}
}; }
static get events() { return [{
"method": "scaleChange",
"name": "scale-change",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": ""
},
"complexType": {
"original": "void",
"resolved": "void",
"references": {}
}
}, {
"method": "scaleFocus",
"name": "scale-focus",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": ""
},
"complexType": {
"original": "void",
"resolved": "void",
"references": {}
}
}, {
"method": "scaleBlur",
"name": "scale-blur",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": ""
},
"complexType": {
"original": "void",
"resolved": "void",
"references": {}
}
}, {
"method": "scaleKeydown",
"name": "scale-keydown",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": ""
},
"complexType": {
"original": "void",
"resolved": "void",
"references": {}
}
}]; }
static get elementRef() { return "hostElement"; }
static get watchers() { return [{
"propName": "value",
"methodName": "valueChange"
}]; }
}