import { html, css } from '@lion/core'; import { LionInput } from '@lion/input'; import { IsNumber, MinNumber, MaxNumber } from '@lion/form-core'; /** * `LionInputStepper` is a class for custom input-stepper element (`` web component). * * @customElement lion-input-stepper */ export class LionInputStepper extends LionInput { static get styles() { return [ ...super.styles, css` .input-group__container > .input-group__input ::slotted(.form-control) { text-align: center; } `, ]; } /** @type {any} */ static get properties() { return { min: { type: Number, reflect: true, }, max: { type: Number, reflect: true, }, step: { type: Number, reflect: true, }, }; } /** * @returns {number} */ get currentValue() { return parseFloat(this.value) || 0; } get _inputNode() { return /** @type {HTMLInputElement} */ (super._inputNode); } constructor() { super(); /** @param {string} modelValue */ this.parser = modelValue => parseFloat(modelValue); this.min = Infinity; this.max = Infinity; this.step = 1; this.values = { max: this.max, min: this.min, step: this.step, }; } connectedCallback() { super.connectedCallback(); this.values = { max: this.max, min: this.min, step: this.step, }; this.role = 'spinbutton'; this.addEventListener('keydown', this.__keyDownHandler); this._inputNode.setAttribute('inputmode', 'decimal'); this._inputNode.setAttribute('autocomplete', 'off'); this.setAttribute('aria-label', this.label); this.step = this.hasAttribute('step') ? this.step : 1; this.__setAriaLabelsAndValidator(); this.__toggleSpinnerButtonsState(); } disconnectedCallback() { super.disconnectedCallback(); this.removeEventListener('keydown', this.__keyDownHandler); } /** @param {import('@lion/core').PropertyValues } changedProperties */ updated(changedProperties) { super.updated(changedProperties); if (changedProperties.has('min')) { this._inputNode.min = `${this.min}`; this.values.min = this.min; } if (changedProperties.has('max')) { this._inputNode.max = `${this.max}`; this.values.max = this.max; } if (changedProperties.has('step')) { this._inputNode.step = `${this.step}`; this.values.step = this.step; } } get slots() { return { ...super.slots, prefix: () => this.__getDecrementButtonNode(), suffix: () => this.__getIncrementButtonNode(), }; } /** * Set aria labels and apply validators * @private */ __setAriaLabelsAndValidator() { const ariaAttributes = { 'aria-valuemax': this.values.max, 'aria-valuemin': this.values.min, }; const minMaxValidators = /** @type {(MaxNumber | MinNumber)[]} */ (Object.entries( ariaAttributes, ) .map(([key, val]) => { if (val !== Infinity) { this.setAttribute(key, `${val}`); return key === 'aria-valuemax' ? new MaxNumber(val) : new MinNumber(val); } return null; }) .filter(validator => validator !== null)); const validators = [new IsNumber(), ...minMaxValidators]; this.defaultValidators.push(...validators); } /** * Update values on keyboard arrow up and down event * @param {KeyboardEvent} e - keyboard event * @private */ __keyDownHandler(e) { if (e.key === 'ArrowUp') { this.__increment(); } if (e.key === 'ArrowDown') { this.__decrement(); } } /** * Toggle disabled state for the buttons * @private */ __toggleSpinnerButtonsState() { const { min, max } = this.values; const decrementButton = this.__getSlot('prefix'); const incrementButton = this.__getSlot('suffix'); const disableIncrementor = this.currentValue >= max && max !== Infinity; const disableDecrementor = this.currentValue <= min && min !== Infinity; decrementButton[disableDecrementor ? 'setAttribute' : 'removeAttribute']('disabled', 'true'); incrementButton[disableIncrementor ? 'setAttribute' : 'removeAttribute']('disabled', 'true'); this.setAttribute('aria-valuenow', `${this.currentValue}`); this.dispatchEvent( new CustomEvent('user-input-changed', { bubbles: true, }), ); } /** * Get slotted element * @param {String} slotName - slot name * @returns {HTMLButtonElement|Object} * @private */ __getSlot(slotName) { return ( /** @type {HTMLElement[]} */ (Array.from(this.children)).find( child => child.slot === slotName, ) || {} ); } /** * Increment the value based on given step or default step value is 1 * @private */ __increment() { const { step, max } = this.values; const newValue = this.currentValue + step; if (newValue <= max || max === Infinity) { this.value = `${newValue}`; this.__toggleSpinnerButtonsState(); } } /** * Decrement the value based on given step or default step value is 1 * @private */ __decrement() { const { step, min } = this.values; const newValue = this.currentValue - step; if (newValue >= min || min === Infinity) { this.value = `${newValue}`; this.__toggleSpinnerButtonsState(); } } /** * Get the increment button node * @returns {Element|null} * @private */ __getIncrementButtonNode() { const renderParent = document.createElement('div'); /** @type {typeof LionInputStepper} */ (this.constructor).render( this._incrementorTemplate(), renderParent, { scopeName: this.localName, eventContext: this, }, ); return renderParent.firstElementChild; } /** * Get the decrement button node * @returns {Element|null} * @private */ __getDecrementButtonNode() { const renderParent = document.createElement('div'); /** @type {typeof LionInputStepper} */ (this.constructor).render( this._decrementorTemplate(), renderParent, { scopeName: this.localName, eventContext: this, }, ); return renderParent.firstElementChild; } /** * Toggle +/- buttons on change * @override * @protected */ _onChange() { super._onChange(); this.__toggleSpinnerButtonsState(); } /** * Get the decrementor button sign template * @returns {String|import('@lion/core').TemplateResult} * @protected */ // eslint-disable-next-line class-methods-use-this _decrementorSignTemplate() { return '-'; } /** * Get the incrementor button sign template * @returns {String|import('@lion/core').TemplateResult} * @protected */ // eslint-disable-next-line class-methods-use-this _incrementorSignTemplate() { return '+'; } /** * Get the increment button template * @returns {import('@lion/core').TemplateResult} * @protected */ _decrementorTemplate() { return html` `; } /** * Get the decrement button template * @returns {import('@lion/core').TemplateResult} * @protected */ _incrementorTemplate() { return html` `; } }