From 922bc3c3d5f92e7cdd4d6b7cf2806a58fce38e2d Mon Sep 17 00:00:00 2001 From: Jeri Peier Date: Thu, 5 Dec 2024 17:12:45 +0100 Subject: [PATCH] fix(sbb-form-field): update floating label on programmatic changes (#3277) Closes #3274 --- .../form-field/form-field/form-field.spec.ts | 57 ++++++++++++++++--- .../form-field/form-field/form-field.ts | 56 +++++++++++++++++- 2 files changed, 104 insertions(+), 9 deletions(-) diff --git a/src/elements/form-field/form-field/form-field.spec.ts b/src/elements/form-field/form-field/form-field.spec.ts index 50eb9817d2..ae11694c55 100644 --- a/src/elements/form-field/form-field/form-field.spec.ts +++ b/src/elements/form-field/form-field/form-field.spec.ts @@ -416,7 +416,7 @@ describe(`sbb-form-field`, () => { expect(element).to.have.attribute('data-input-empty'); }); - it('should reset floating label when calling reset of sbb-form-field', async () => { + it('should reset floating label when changing value programmatically', async () => { const element: SbbFormFieldElement = await fixture(html` @@ -433,15 +433,58 @@ describe(`sbb-form-field`, () => { input.value = ''; await waitForLitRender(element); - // Then empty state is not updated - expect(element).not.to.have.attribute('data-input-empty'); + // Then the empty state is updated + expect(element).to.have.attribute('data-input-empty'); + }); + + it('should unpatch on input removal', async () => { + const element: SbbFormFieldElement = await fixture(html` + + `); + + const newInput = document.createElement('input'); - // When manually calling reset method - element.reset(); + const originalSetter = Object.getOwnPropertyDescriptor( + Object.getPrototypeOf(newInput), + 'value', + )!.set; + + element.appendChild(newInput); await waitForLitRender(element); - // Then empty state should be updated - expect(element).to.have.attribute('data-input-empty'); + expect(Object.getOwnPropertyDescriptor(newInput, 'value')!.set).not.to.be.equal( + originalSetter, + ); + + newInput.remove(); + await waitForLitRender(element); + + expect(Object.getOwnPropertyDescriptor(newInput, 'value')!.set).to.be.equal(originalSetter); + }); + + it('should unpatch on disconnection', async () => { + const element: SbbFormFieldElement = await fixture(html` + + `); + + const newInput = document.createElement('input'); + + const originalSetter = Object.getOwnPropertyDescriptor( + Object.getPrototypeOf(newInput), + 'value', + )!.set; + + element.appendChild(newInput); + await waitForLitRender(element); + + expect(Object.getOwnPropertyDescriptor(newInput, 'value')!.set).not.to.be.equal( + originalSetter, + ); + + element.remove(); + await waitForLitRender(element); + + expect(Object.getOwnPropertyDescriptor(newInput, 'value')!.set).to.be.equal(originalSetter); }); }); }); diff --git a/src/elements/form-field/form-field/form-field.ts b/src/elements/form-field/form-field/form-field.ts index 72f3f4aae0..1c8acbaa07 100644 --- a/src/elements/form-field/form-field/form-field.ts +++ b/src/elements/form-field/form-field/form-field.ts @@ -20,6 +20,8 @@ let nextFormFieldErrorId = 0; const supportedPopupTagNames = ['sbb-autocomplete', 'sbb-autocomplete-grid', 'sbb-select']; +const patchedInputs = new WeakMap(); + /** * It wraps an input element adding label, errors, icon, etc. * @@ -152,6 +154,9 @@ class SbbFormFieldElement extends SbbNegativeMixin(SbbHydrationMixin(LitElement) super.disconnectedCallback(); this._formFieldAttributeObserver?.disconnect(); this._inputAbortController.abort(); + if (this._input?.localName === 'input') { + this._unpatchInputValue(); + } } private _onPopupOpen({ target }: CustomEvent): void { @@ -204,11 +209,17 @@ class SbbFormFieldElement extends SbbNegativeMixin(SbbHydrationMixin(LitElement) * It is used internally to assign the attributes of `` to `_id` and `_input` and to observe the native readonly and disabled attributes. */ private _onSlotInputChange(event: Event): void { - this._input = (event.target as HTMLSlotElement) + const newInput = (event.target as HTMLSlotElement) .assignedElements() .find((e): e is HTMLElement => this._supportedInputElements.includes(e.localName)); this._assignSlots(); + if (this._input && this._input.localName === 'input' && newInput !== this._input) { + this._unpatchInputValue(); + } + + this._input = newInput; + if (!this._input) { return; } @@ -282,7 +293,9 @@ class SbbFormFieldElement extends SbbNegativeMixin(SbbHydrationMixin(LitElement) let inputFocusElement = this._input; - if (this._input.localName === 'sbb-select') { + if (this._input.localName === 'input') { + this._patchInputValue(); + } else if (this._input.localName === 'sbb-select') { this._input.addEventListener('stateChange', () => this._checkAndUpdateInputEmpty(), { signal: this._inputAbortController.signal, }); @@ -321,6 +334,45 @@ class SbbFormFieldElement extends SbbNegativeMixin(SbbHydrationMixin(LitElement) return this._input?.closest('form'); } + // We need to patch the value property of the HTMLInputElement in order + // to be able to reset the floating label in the empty state. + private _patchInputValue(): void { + const inputElement = this._input as HTMLInputElement; + const originalDescriptor = Object.getOwnPropertyDescriptor( + Object.getPrototypeOf(inputElement), + 'value', + ); + + if (!originalDescriptor || !originalDescriptor.set || !originalDescriptor.get) { + return; + } + + patchedInputs.set(inputElement, originalDescriptor); + + const { get: getter, set: setter } = originalDescriptor; + const checkAndUpdateInputEmpty = (): void => this._checkAndUpdateInputEmpty(); + + Object.defineProperty(inputElement, 'value', { + ...originalDescriptor, + get() { + return getter.call(this); + }, + set(newValue) { + setter.call(this, newValue); + checkAndUpdateInputEmpty(); + }, + }); + } + + private _unpatchInputValue(): void { + const inputElement = this._input as HTMLInputElement; + const originalDescriptor = patchedInputs.get(inputElement); + if (originalDescriptor) { + Object.defineProperty(inputElement, 'value', originalDescriptor); + patchedInputs.delete(inputElement); + } + } + private _checkAndUpdateInputEmpty(): void { this.toggleAttribute( 'data-input-empty',