@telekom/scale-components
Version:
Scale is the digital design system for Telekom products and experiences.
899 lines (898 loc) • 26.8 kB
JavaScript
/**
* @license
* Scale https://github.com/telekom/scale
*
* Copyright (c) 2021 Egor Kirpichev and contributors, Deutsche Telekom AG
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { Component, Prop, Event, h, Host, State, Watch, Element, } from '@stencil/core';
import classNames from 'classnames';
import statusNote from '../../utils/status-note';
import { emitEvent, generateUniqueId } from '../../utils/utils';
const SELECT_ICON = 'M20.65 7.4c-.3-.3-.75-.3-1.05 0L12 15 4.4 7.4c-.3-.3-.75-.3-1.05 0s-.3.75 0 1.05L12 17.1l8.65-8.65c.3-.25.3-.75 0-1.05z';
export class Input {
constructor() {
/** (optional) Input type */
this.type = 'text';
/** (optional) Input name */
this.name = '';
/** (optional) Input label variant */
this.variant = 'static';
/** (optional) Input label */
this.label = '';
/** (optional) Input size */
this.size = '';
/** (optional) Input helper text */
this.helperText = '';
/** @deprecated - invalid should replace status */
this.status = '';
/** (optional) Input status */
this.invalid = false;
/** (optional) Input placeHolder */
this.placeholder = '';
/** (optional) Active switch */
this.checked = false;
/** (optional) Input value */
this.value = '';
/** (optional) Makes type `select` behave as a controlled component in React */
this.controlled = false;
/** Whether the input element has focus */
this.hasFocus = false;
this.internalId = generateUniqueId();
// Handle checkbox/radio change (click on label)
this.handleCheckChange = (event) => {
this.checked = event.target.checked;
};
// Handle click on checkbox visual element
this.handleCheckboxClick = () => {
if (!this.disabled) {
this.checked = !this.checked;
}
};
// Handle change on <select> independently
// so we can allow "controlled" (React) behavior,
// in which only the `value` changing does update
// the actual <select> value, not the user's input.
this.handleSelectChange = (event) => {
const target = event.target;
if (this.controlled) {
emitEvent(this, 'scaleChange', { value: target.value });
this.selectElement.value = String(this.value);
this.forceUpdate = String(Date.now());
}
else {
this.value = target.value || '';
this.emitChange();
}
};
this.handleInput = (event) => {
const target = event.target;
if (target) {
this.value = target.value || '';
this.emitChange();
}
emitEvent(this, 'scaleInput', event);
};
this.handleChange = (event) => {
const target = event.target;
if (target) {
this.value = target.value || '';
this.emitChange();
}
};
this.handleFocus = () => {
emitEvent(this, 'scaleFocus');
this.hasFocus = true;
};
this.handleBlur = () => {
emitEvent(this, 'scaleBlur');
this.hasFocus = false;
};
this.handleKeyDown = (event) => {
emitEvent(this, 'scaleKeyDown', event);
};
}
componentWillLoad() {
if (this.inputId == null) {
this.inputId = 'input-' + this.internalId;
}
// Default icon for `select` type
if (this.type === 'select' && this.icon == null) {
this.icon = SELECT_ICON;
}
}
componentDidLoad() {
// tslint:disable-next-line:no-console
statusNote({
tag: 'deprecated',
source: this.el,
type: 'warn',
extraMessage: `Please use <${{
select: 'scale-dropdown',
checkbox: 'scale-checkbox',
radio: 'scale-radio-button',
textarea: 'scale-textarea',
}[this.type] || 'scale-text-field'}> instead.`,
});
// Keep this.value up-to-date for type="select".
// This is important also for React, where `value` is used to control the element state.
if (this.type === 'select') {
const select = this.selectElement;
const selectedValue = select.selectedIndex > -1
? select.options[select.selectedIndex].value
: null;
// If we have a `value` passed, set it on the <select> element
// Otherwise, if we have an <option selected>, set its value on `value`
if (this.value) {
select.value = String(this.value);
}
else if (selectedValue) {
this.value = selectedValue;
}
}
// This is a workaroud to prevent a bug in Stencil:
// when using slots without Shadow DOM (possible only with Stencil)
// sometimes an update in the Light DOM does not trigger a re-render
// thus causing unexpected results.
// https://gitlab.com/scale-ds/scale-telekom/-/issues/16
if (this.type === 'select' && this.selectElement) {
this.mutationObserver = new MutationObserver(() => {
this.forceUpdate = String(Date.now());
});
this.mutationObserver.observe(this.el, {
childList: true,
subtree: true,
});
}
}
componentWillUpdate() { }
componentDidRender() {
// When type `select` and `controlled` is true,
// make sure the <select> is always in sync with the value.
const value = this.value == null ? '' : this.value.toString();
if (this.type === 'select' &&
this.controlled &&
this.selectElement.value.toString() !== value) {
this.selectElement.value = value;
}
if (this.status !== '') {
statusNote({
tag: 'deprecated',
message: 'Property "status" is deprecated. Please use the "invalid" property!',
type: 'warn',
source: this.el,
});
}
}
disconnectedCallback() {
if (this.mutationObserver) {
this.mutationObserver.disconnect();
}
}
// We're not watching `value` like we used to
// because we get unwanted `scaleChange` events
// because how we keep this.value up-to-date for type="select"
// `this.value = selectedValue`
emitChange() {
emitEvent(this, 'scaleChange', {
value: this.value == null ? this.value : this.value.toString(),
});
}
checkedChanged() {
emitEvent(this, 'scaleChange', { value: this.checked });
}
render() {
const Tag = this.type === 'textarea' ? 'textarea' : 'input';
const ariaInvalidAttr = this.status === 'error' || this.invalid ? { 'aria-invalid': true } : {};
const helperTextId = `helper-message-${this.internalId}`;
const ariaDescribedByAttr = { 'aria-describedBy': helperTextId };
if (this.type === 'checkbox') {
return (h(Host, { checked: this.checked },
h("div", { class: this.getCssClassMap() },
h("input", Object.assign({ type: "checkbox", name: this.name, id: this.inputId, onChange: this.handleCheckChange, value: this.value, checked: this.checked, disabled: this.disabled }, ariaInvalidAttr, ariaDescribedByAttr)),
h("div", { class: classNames('input__checkbox-container'), onClick: this.handleCheckboxClick },
h("span", { class: classNames('input__checkbox-placeholder') }),
!!this.icon && this.checked && (h("scale-icon", { path: this.icon, size: 12 }))),
h("label", { htmlFor: this.inputId }, this.label),
!!this.helperText && (h("div", { class: "input__meta", id: helperTextId, "aria-live": "polite", "aria-relevant": "additions removals" },
h("div", { class: "input__helper-text" }, this.helperText))))));
}
if (this.type === 'radio') {
return (h(Host, null,
h("div", { class: this.getCssClassMap() },
h("input", Object.assign({ type: "radio", name: this.name, id: this.inputId, onChange: this.handleCheckChange, value: this.value, checked: this.checked, disabled: this.disabled }, ariaInvalidAttr, ariaDescribedByAttr)),
h("label", { htmlFor: this.inputId }, this.label),
!!this.helperText && (h("div", { class: "input__meta", id: helperTextId, "aria-live": "polite", "aria-relevant": "additions removals" },
h("div", { class: "input__helper-text" }, this.helperText))))));
}
return (h(Host, null,
h("div", { class: this.getCssClassMap() },
h("label", { class: "input__label", htmlFor: this.inputId }, this.label),
this.type === 'select' ? (h("div", { class: "input__select-wrapper" },
h("select", Object.assign({ ref: (el) => (this.selectElement = el), class: classNames('input__select'),
// @ts-ignore
value: this.value, onChange: this.handleSelectChange, onFocus: this.handleFocus, onBlur: this.handleBlur, onKeyDown: this.handleKeyDown, disabled: this.disabled, required: this.required, multiple: this.multiple, id: this.inputId, name: this.name, size: this.visibleSize }, ariaInvalidAttr, ariaDescribedByAttr),
h("slot", null)),
!!this.icon && h("scale-icon", { path: this.icon }))) : (h(Tag, Object.assign({ type: this.type, class: classNames(`input__${this.type === 'textarea' ? 'textarea' : 'input'}`, this.customResize && this.customResize.id), style: !!this.resize && { resize: this.resize }, value: this.value }, (!!this.name ? { name: this.name } : {}), { required: this.required, minLength: this.minLength, maxLength: this.maxLength, id: this.inputId, onInput: this.handleInput, onChange: this.handleChange, onFocus: this.handleFocus, onBlur: this.handleBlur, onKeyDown: this.handleKeyDown }, (!!this.placeholder ? { placeholder: this.placeholder } : {}), { disabled: this.disabled }, (!!this.rows ? { rows: this.rows } : {}), (!!this.cols ? { cols: this.cols } : {}), ariaInvalidAttr, ariaDescribedByAttr))),
this.type === 'textarea' && this.variant === 'animated' && (h("span", { class: "input__textarea-label-safety-background", "aria-hidden": "true" })),
(!!this.helperText || !!this.counter) && (h("div", { class: "input__meta", id: helperTextId, "aria-live": "polite", "aria-relevant": "additions removals" },
!!this.helperText && (h("div", { class: "input__helper-text" }, this.helperText)),
this.counter && (h("div", { class: "input__counter" },
!!this.value ? String(this.value).length : 0,
" /",
' ',
this.maxLength)))))));
}
getCssClassMap() {
return classNames('input', this.type && `input--type-${this.type}`, this.hasFocus && 'input--has-focus', this.checked && `input--checked`, this.resize && `input--resize-${this.resize}`, this.variant && `input--variant-${this.variant}`, this.disabled && `input--disabled`, this.transparent && 'input--transparent', this.status && `input--status-${this.status}`, this.invalid && `input--status-error`, this.size && `input--size-${this.size}`, this.value != null && this.value !== '' && 'animated');
}
static get is() { return "scale-input"; }
static get originalStyleUrls() { return {
"$": ["./input.css"]
}; }
static get styleUrls() { return {
"$": ["input.css"]
}; }
static get properties() { return {
"type": {
"type": "string",
"mutable": false,
"complexType": {
"original": "| 'email'\n | 'hidden'\n | 'number'\n | 'password'\n | 'tel'\n | 'text'\n | 'checkbox'\n | 'radio'\n | 'select'\n | 'textarea'\n | 'url'",
"resolved": "\"checkbox\" | \"email\" | \"hidden\" | \"number\" | \"password\" | \"radio\" | \"select\" | \"tel\" | \"text\" | \"textarea\" | \"url\"",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Input type"
},
"attribute": "type",
"reflect": false,
"defaultValue": "'text'"
},
"name": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Input name"
},
"attribute": "name",
"reflect": false,
"defaultValue": "''"
},
"variant": {
"type": "string",
"mutable": false,
"complexType": {
"original": "'animated' | 'static'",
"resolved": "\"animated\" | \"static\"",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Input label variant"
},
"attribute": "variant",
"reflect": false,
"defaultValue": "'static'"
},
"label": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "(optional) Input label"
},
"attribute": "label",
"reflect": false,
"defaultValue": "''"
},
"size": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Input size"
},
"attribute": "size",
"reflect": false,
"defaultValue": "''"
},
"rows": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) textarea row"
},
"attribute": "rows",
"reflect": false
},
"cols": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) textarea column"
},
"attribute": "cols",
"reflect": false
},
"helperText": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Input helper text"
},
"attribute": "helper-text",
"reflect": false,
"defaultValue": "''"
},
"status": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [{
"name": "deprecated",
"text": "- invalid should replace status"
}],
"text": ""
},
"attribute": "status",
"reflect": false,
"defaultValue": "''"
},
"invalid": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Input status"
},
"attribute": "invalid",
"reflect": false,
"defaultValue": "false"
},
"maxLength": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Input max length"
},
"attribute": "max-length",
"reflect": false
},
"minLength": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Input min length"
},
"attribute": "min-length",
"reflect": false
},
"placeholder": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Input placeHolder"
},
"attribute": "placeholder",
"reflect": false,
"defaultValue": "''"
},
"disabled": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Input disabled"
},
"attribute": "disabled",
"reflect": false
},
"required": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Input required"
},
"attribute": "required",
"reflect": false
},
"counter": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Input counter"
},
"attribute": "counter",
"reflect": false
},
"checked": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Active switch"
},
"attribute": "checked",
"reflect": true,
"defaultValue": "false"
},
"resize": {
"type": "string",
"mutable": false,
"complexType": {
"original": "'unset' | 'none' | 'vertical' | 'horizontal'",
"resolved": "\"horizontal\" | \"none\" | \"unset\" | \"vertical\"",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) textarea resize"
},
"attribute": "resize",
"reflect": false
},
"value": {
"type": "any",
"mutable": true,
"complexType": {
"original": "string | number | null",
"resolved": "number | string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Input value"
},
"attribute": "value",
"reflect": false,
"defaultValue": "''"
},
"inputId": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Input checkbox id"
},
"attribute": "input-id",
"reflect": false
},
"icon": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Input checkbox checked icon"
},
"attribute": "icon",
"reflect": false
},
"multiple": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) select multiple options"
},
"attribute": "multiple",
"reflect": false
},
"visibleSize": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) the number of visible options in a select drop-down list"
},
"attribute": "visible-size",
"reflect": false
},
"transparent": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) input background transparent"
},
"attribute": "transparent",
"reflect": false
},
"controlled": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Makes type `select` behave as a controlled component in React"
},
"attribute": "controlled",
"reflect": false,
"defaultValue": "false"
},
"styles": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "(optional) Injected CSS styles"
},
"attribute": "styles",
"reflect": false
}
}; }
static get states() { return {
"customResize": {},
"hasFocus": {},
"forceUpdate": {}
}; }
static get events() { return [{
"method": "scaleInput",
"name": "scale-input",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Emitted when a keyboard input occurred."
},
"complexType": {
"original": "KeyboardEvent",
"resolved": "KeyboardEvent",
"references": {
"KeyboardEvent": {
"location": "global"
}
}
}
}, {
"method": "scaleInputLegacy",
"name": "scaleInput",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [{
"name": "deprecated",
"text": "in v3 in favor of kebab-case event names"
}],
"text": ""
},
"complexType": {
"original": "KeyboardEvent",
"resolved": "KeyboardEvent",
"references": {
"KeyboardEvent": {
"location": "global"
}
}
}
}, {
"method": "scaleChange",
"name": "scale-change",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Emitted when the value has changed."
},
"complexType": {
"original": "InputChangeEventDetail",
"resolved": "InputChangeEventDetail",
"references": {
"InputChangeEventDetail": {
"location": "local"
}
}
}
}, {
"method": "scaleChangeLegacy",
"name": "scaleChange",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [{
"name": "deprecated",
"text": "in v3 in favor of kebab-case event names"
}],
"text": ""
},
"complexType": {
"original": "InputChangeEventDetail",
"resolved": "InputChangeEventDetail",
"references": {
"InputChangeEventDetail": {
"location": "local"
}
}
}
}, {
"method": "scaleFocus",
"name": "scale-focus",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Emitted when the input has focus."
},
"complexType": {
"original": "void",
"resolved": "void",
"references": {}
}
}, {
"method": "scaleFocusLegacy",
"name": "scaleFocus",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [{
"name": "deprecated",
"text": "in v3 in favor of kebab-case event names"
}],
"text": ""
},
"complexType": {
"original": "void",
"resolved": "void",
"references": {}
}
}, {
"method": "scaleBlur",
"name": "scale-blur",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Emitted when the input loses focus."
},
"complexType": {
"original": "void",
"resolved": "void",
"references": {}
}
}, {
"method": "scaleBlurLegacy",
"name": "scaleBlur",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [{
"name": "deprecated",
"text": "in v3 in favor of kebab-case event names"
}],
"text": ""
},
"complexType": {
"original": "void",
"resolved": "void",
"references": {}
}
}, {
"method": "scaleKeyDown",
"name": "scale-keydown",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Emitted on keydown."
},
"complexType": {
"original": "KeyboardEvent",
"resolved": "KeyboardEvent",
"references": {
"KeyboardEvent": {
"location": "global"
}
}
}
}, {
"method": "scaleKeyDownLegacy",
"name": "scaleKeyDown",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [{
"name": "deprecated",
"text": "in v3 in favor of kebab-case event names"
}],
"text": ""
},
"complexType": {
"original": "KeyboardEvent",
"resolved": "KeyboardEvent",
"references": {
"KeyboardEvent": {
"location": "global"
}
}
}
}]; }
static get elementRef() { return "el"; }
static get watchers() { return [{
"propName": "checked",
"methodName": "checkedChanged"
}]; }
}