diff --git a/packages/form-core/src/FormatMixin.js b/packages/form-core/src/FormatMixin.js index 644f913a8..ac2d6bbdc 100644 --- a/packages/form-core/src/FormatMixin.js +++ b/packages/form-core/src/FormatMixin.js @@ -122,6 +122,23 @@ const FormatMixinImplementation = superclass => } } + get value() { + return (this._inputNode && this._inputNode.value) || this.__value || ''; + } + + // We don't delegate, because we want to preserve caret position via _setValueAndPreserveCaret + /** @type {string} */ + set value(value) { + // if not yet connected to dom can't change the value + if (this._inputNode) { + this._inputNode.value = value; + /** @type {string | undefined} */ + this.__value = undefined; + } else { + this.__value = value; + } + } + /** * Converts formattedValue to modelValue * For instance, a localized date to a Date Object diff --git a/packages/form-core/src/LionField.js b/packages/form-core/src/LionField.js index 185df721e..208c2dea3 100644 --- a/packages/form-core/src/LionField.js +++ b/packages/form-core/src/LionField.js @@ -38,10 +38,6 @@ export class LionField extends FormControlMixin( }; } - get _inputNode() { - return /** @type {HTMLElement} */ (super._inputNode); // casts type - } - constructor() { super(); this.name = ''; diff --git a/packages/form-core/src/ValueMixin.js b/packages/form-core/src/NativeTextFieldMixin.js similarity index 61% rename from packages/form-core/src/ValueMixin.js rename to packages/form-core/src/NativeTextFieldMixin.js index cbd226171..84d618425 100644 --- a/packages/form-core/src/ValueMixin.js +++ b/packages/form-core/src/NativeTextFieldMixin.js @@ -1,18 +1,50 @@ import { dedupeMixin } from '@lion/core'; /** - * @typedef {import('../types/ValueMixinTypes').ValueMixin} ValueMixin - * @type {ValueMixin} - * @param {import('@open-wc/dedupe-mixin').Constructor} superclass} superclass + * @typedef {import('../types/NativeTextFieldMixinTypes').NativeTextFieldMixin} NativeTextFieldMixin + * @type {NativeTextFieldMixin} + * @param {import('@open-wc/dedupe-mixin').Constructor} superclass} superclass */ -const ValueMixinImplementation = superclass => - class ValueMixin extends superclass { +const NativeTextFieldMixinImplementation = superclass => + class NativeTextFieldMixin extends superclass { + /** @type {number} */ + get selectionStart() { + const native = this._inputNode; + if (native && native.selectionStart) { + return native.selectionStart; + } + return 0; + } + + set selectionStart(value) { + const native = this._inputNode; + if (native && native.selectionStart) { + native.selectionStart = value; + } + } + + /** @type {number} */ + get selectionEnd() { + const native = this._inputNode; + if (native && native.selectionEnd) { + return native.selectionEnd; + } + return 0; + } + + set selectionEnd(value) { + const native = this._inputNode; + if (native && native.selectionEnd) { + native.selectionEnd = value; + } + } + get value() { return (this._inputNode && this._inputNode.value) || this.__value || ''; } // We don't delegate, because we want to preserve caret position via _setValueAndPreserveCaret - /** @type {string} */ + /** @param {string} value */ set value(value) { // if not yet connected to dom can't change the value if (this._inputNode) { @@ -54,4 +86,4 @@ const ValueMixinImplementation = superclass => } }; -export const ValueMixin = dedupeMixin(ValueMixinImplementation); +export const NativeTextFieldMixin = dedupeMixin(NativeTextFieldMixinImplementation); diff --git a/packages/form-core/src/form-group/FormGroupMixin.js b/packages/form-core/src/form-group/FormGroupMixin.js index b5936d062..41f1a05ed 100644 --- a/packages/form-core/src/form-group/FormGroupMixin.js +++ b/packages/form-core/src/form-group/FormGroupMixin.js @@ -125,6 +125,9 @@ const FormGroupMixinImplementation = superclass => constructor() { super(); + // inputNode = this, which always requires a value prop + this.value = ''; + this.disabled = false; this.submitted = false; this.dirty = false; diff --git a/packages/form-core/types/FormControlMixinTypes.d.ts b/packages/form-core/types/FormControlMixinTypes.d.ts index f3214e3ac..6af5a4e5f 100644 --- a/packages/form-core/types/FormControlMixinTypes.d.ts +++ b/packages/form-core/types/FormControlMixinTypes.d.ts @@ -6,6 +6,10 @@ import { DisabledHost } from '@lion/core/types/DisabledMixinTypes'; import { LionValidationFeedback } from '../src/validate/LionValidationFeedback'; import { FormRegisteringHost } from './registration/FormRegisteringMixinTypes'; +declare interface HTMLElementWithValue extends HTMLElement { + value: string; +} + export class FormControlHost { static get styles(): CSSResult | CSSResult[]; /** @@ -52,7 +56,7 @@ export class FormControlHost { get fieldName(): string; __fieldName: string | undefined; get slots(): SlotsMap; - get _inputNode(): HTMLElement; + get _inputNode(): HTMLElementWithValue; get _labelNode(): HTMLElement; get _helpTextNode(): HTMLElement; get _feedbackNode(): LionValidationFeedback | undefined; diff --git a/packages/form-core/types/FormatMixinTypes.d.ts b/packages/form-core/types/FormatMixinTypes.d.ts index 5b4ac86ab..f40272159 100644 --- a/packages/form-core/types/FormatMixinTypes.d.ts +++ b/packages/form-core/types/FormatMixinTypes.d.ts @@ -9,7 +9,6 @@ export declare class FormatHost { serializedValue: string; formatOn: string; formatOptions: FormatNumberOptions; - value: string; __preventRecursiveTrigger: boolean; __isHandlingUserInput: boolean; @@ -18,6 +17,9 @@ export declare class FormatHost { serializer(v: unknown): string; deserializer(v: string): unknown; + get value(): string; + set value(value: string); + _calculateValues(opts: { source: 'model' | 'serialized' | 'formatted' | null }): void; __callParser(value: string | undefined): object; __callFormatter(): string; diff --git a/packages/form-core/types/NativeTextFieldMixinTypes.d.ts b/packages/form-core/types/NativeTextFieldMixinTypes.d.ts new file mode 100644 index 000000000..987dc92cc --- /dev/null +++ b/packages/form-core/types/NativeTextFieldMixinTypes.d.ts @@ -0,0 +1,19 @@ +import { Constructor } from '@open-wc/dedupe-mixin'; +import { LionField } from '@lion/form-core/src/LionField'; + +export declare class NativeTextField extends LionField { + get _inputNode(): HTMLTextAreaElement | HTMLInputElement; +} + +export declare class NativeTextFieldHost { + get selectionStart(): number; + set selectionStart(value: number); + get selectionEnd(): number; + set selectionEnd(value: number); +} + +export declare function NativeTextFieldImplementation>( + superclass: T, +): T & Constructor & NativeTextFieldHost; + +export type NativeTextFieldMixin = typeof NativeTextFieldImplementation; diff --git a/packages/form-core/types/ValueMixinTypes.d.ts b/packages/form-core/types/ValueMixinTypes.d.ts deleted file mode 100644 index 7b667d909..000000000 --- a/packages/form-core/types/ValueMixinTypes.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Constructor } from '@open-wc/dedupe-mixin'; -import { LionField } from '@lion/form-core/src/LionField'; - -export declare class LionFieldWithValue extends LionField { - get _inputNode(): HTMLTextAreaElement | HTMLInputElement | HTMLSelectElement; -} - -export declare class ValueHost { - get value(): string; - set value(value: string); - _setValueAndPreserveCaret(newValue: string): void; -} - -export declare function ValueImplementation>( - superclass: T, -): T & Constructor & ValueHost; - -export type ValueMixin = typeof ValueImplementation; diff --git a/packages/input-amount/src/LionInputAmount.js b/packages/input-amount/src/LionInputAmount.js index 2c4a33f56..bb4248990 100644 --- a/packages/input-amount/src/LionInputAmount.js +++ b/packages/input-amount/src/LionInputAmount.js @@ -53,14 +53,11 @@ export class LionInputAmount extends LocalizeMixin(LionInput) { } static get styles() { - return [ - super.styles, - css` - .input-group__container > .input-group__input ::slotted(.form-control) { - text-align: right; - } - `, - ]; + return css` + .input-group__container > .input-group__input ::slotted(.form-control) { + text-align: right; + } + `; } constructor() { diff --git a/packages/input-stepper/src/LionInputStepper.js b/packages/input-stepper/src/LionInputStepper.js index 42d09ddb0..8108a71eb 100644 --- a/packages/input-stepper/src/LionInputStepper.js +++ b/packages/input-stepper/src/LionInputStepper.js @@ -10,14 +10,11 @@ import { IsNumber, MinNumber, MaxNumber } from '@lion/form-core'; // @ts-expect-error false positive for incompatible static get properties. Lit-element merges super properties already for you. export class LionInputStepper extends LionInput { static get styles() { - return [ - super.styles, - css` - .input-group__container > .input-group__input ::slotted(.form-control) { - text-align: center; - } - `, - ]; + return css` + .input-group__container > .input-group__input ::slotted(.form-control) { + text-align: center; + } + `; } static get properties() { diff --git a/packages/input/src/LionInput.js b/packages/input/src/LionInput.js index 18caf8557..cfc971278 100644 --- a/packages/input/src/LionInput.js +++ b/packages/input/src/LionInput.js @@ -1,14 +1,13 @@ import { LionField } from '@lion/form-core'; -import { ValueMixin } from '@lion/form-core/src/ValueMixin'; +import { NativeTextFieldMixin } from '@lion/form-core/src/NativeTextFieldMixin'; /** * LionInput: extension of lion-field with native input element in place and user friendly API. * * @customElement lion-input - * @extends {LionField} */ // @ts-expect-error false positive for incompatible static get properties. Lit-element merges super properties already for you. -export class LionInput extends ValueMixin(LionField) { +export class LionInput extends NativeTextFieldMixin(LionField) { static get properties() { return { /** @@ -54,38 +53,6 @@ export class LionInput extends ValueMixin(LionField) { return /** @type {HTMLInputElement} */ (super._inputNode); // casts type } - /** @type {number} */ - get selectionStart() { - const native = this._inputNode; - if (native && native.selectionStart) { - return native.selectionStart; - } - return 0; - } - - set selectionStart(value) { - const native = this._inputNode; - if (native && native.selectionStart) { - native.selectionStart = value; - } - } - - /** @type {number} */ - get selectionEnd() { - const native = this._inputNode; - if (native && native.selectionEnd) { - return native.selectionEnd; - } - return 0; - } - - set selectionEnd(value) { - const native = this._inputNode; - if (native && native.selectionEnd) { - native.selectionEnd = value; - } - } - constructor() { super(); this.readOnly = false; diff --git a/packages/listbox/src/ListboxMixin.js b/packages/listbox/src/ListboxMixin.js index e969e094e..0b0deace6 100644 --- a/packages/listbox/src/ListboxMixin.js +++ b/packages/listbox/src/ListboxMixin.js @@ -9,6 +9,7 @@ import { LionOptions } from './LionOptions.js'; // list items that can be found via MutationObserver or registration (.formElements) /** + * @typedef {import('@lion/form-core/types/FormControlMixinTypes').HTMLElementWithValue} HTMLElementWithValue * @typedef {import('./LionOption').LionOption} LionOption * @typedef {import('../types/ListboxMixinTypes').ListboxMixin} ListboxMixin * @typedef {import('../types/ListboxMixinTypes').ListboxHost} ListboxHost @@ -131,15 +132,15 @@ const ListboxMixinImplementation = superclass => * @configure FormControlMixin */ get _inputNode() { - return /** @type {HTMLElement} */ (this.querySelector('[slot="input"]')); + return /** @type {HTMLElementWithValue} */ (this.querySelector('[slot="input"]')); } /** * @overridable - * @type {LionOptions} */ get _listboxNode() { - return /** @type {LionOptions} */ (this._inputNode); + // Cast to unknown first, since HTMLElementWithValue is not compatible with LionOptions + return /** @type {LionOptions} */ (/** @type {unknown} */ (this._inputNode)); } /** diff --git a/packages/select/src/LionSelect.js b/packages/select/src/LionSelect.js index 152d8a8a3..9ae6bd108 100644 --- a/packages/select/src/LionSelect.js +++ b/packages/select/src/LionSelect.js @@ -1,6 +1,5 @@ /* eslint-disable max-classes-per-file */ import { LionField } from '@lion/form-core'; -import { ValueMixin } from '@lion/form-core/src/ValueMixin'; class LionFieldWithSelect extends LionField { /** @@ -38,14 +37,48 @@ class LionFieldWithSelect extends LionField { * usability for keyboard and screen reader users. * * @customElement lion-select - * @extends {LionField} */ -export class LionSelect extends ValueMixin(LionFieldWithSelect) { +export class LionSelect extends LionFieldWithSelect { connectedCallback() { super.connectedCallback(); this._inputNode.addEventListener('change', this._proxyChangeEvent); } + // FIXME: For some reason we have to override this FormatMixin getter/setter pair for the tests to pass + get value() { + return (this._inputNode && this._inputNode.value) || this.__value || ''; + } + + // We don't delegate, because we want to preserve caret position via _setValueAndPreserveCaret + /** @type {string} */ + set value(value) { + // if not yet connected to dom can't change the value + if (this._inputNode) { + this._inputNode.value = value; + /** @type {string | undefined} */ + this.__value = undefined; + } else { + this.__value = value; + } + } + + /** @param {import('lit-element').PropertyValues } changedProperties */ + updated(changedProperties) { + super.updated(changedProperties); + if (changedProperties.has('disabled')) { + this._inputNode.disabled = this.disabled; + this.validate(); + } + + if (changedProperties.has('name')) { + this._inputNode.name = this.name; + } + + if (changedProperties.has('autocomplete')) { + this._inputNode.autocomplete = /** @type {string} */ (this.autocomplete); + } + } + disconnectedCallback() { super.disconnectedCallback(); this._inputNode.removeEventListener('change', this._proxyChangeEvent); diff --git a/packages/switch/src/LionSwitchButton.js b/packages/switch/src/LionSwitchButton.js index ee6b73c68..faad4cdbb 100644 --- a/packages/switch/src/LionSwitchButton.js +++ b/packages/switch/src/LionSwitchButton.js @@ -72,6 +72,9 @@ export class LionSwitchButton extends DisabledWithTabIndexMixin(LitElement) { constructor() { super(); + // inputNode = this, which always requires a value prop + this.value = ''; + this.role = 'switch'; this.checked = false; this.__toggleChecked = this.__toggleChecked.bind(this); diff --git a/packages/textarea/src/LionTextarea.js b/packages/textarea/src/LionTextarea.js index 2bf6a8409..7e1231ff5 100644 --- a/packages/textarea/src/LionTextarea.js +++ b/packages/textarea/src/LionTextarea.js @@ -3,7 +3,7 @@ import autosize from 'autosize/src/autosize.js'; import { LionField } from '@lion/form-core'; import { css } from '@lion/core'; -import { ValueMixin } from '@lion/form-core/src/ValueMixin'; +import { NativeTextFieldMixin } from '@lion/form-core/src/NativeTextFieldMixin'; class LionFieldWithTextArea extends LionField { /** @@ -22,7 +22,7 @@ class LionFieldWithTextArea extends LionField { * @customElement lion-textarea */ // @ts-expect-error false positive, parent properties get merged by lit-element already -export class LionTextarea extends ValueMixin(LionFieldWithTextArea) { +export class LionTextarea extends NativeTextFieldMixin(LionFieldWithTextArea) { static get properties() { return { maxRows: { @@ -78,6 +78,15 @@ export class LionTextarea extends ValueMixin(LionFieldWithTextArea) { /** @param {import('lit-element').PropertyValues } changedProperties */ updated(changedProperties) { super.updated(changedProperties); + + if (changedProperties.has('name')) { + this._inputNode.name = this.name; + } + + if (changedProperties.has('autocomplete')) { + this._inputNode.autocomplete = /** @type {string} */ (this.autocomplete); + } + if (changedProperties.has('disabled')) { this._inputNode.disabled = this.disabled; this.validate();