Skip to content

Commit

Permalink
fix(sbb-form-field): update floating label on programmatic changes (#…
Browse files Browse the repository at this point in the history
…3277)

Closes #3274
  • Loading branch information
jeripeierSBB authored and github-actions committed Dec 5, 2024
1 parent e8cf377 commit 922bc3c
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 9 deletions.
57 changes: 50 additions & 7 deletions src/elements/form-field/form-field/form-field.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
<sbb-form-field floating-label>
<input />
Expand All @@ -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`
<sbb-form-field floating-label></sbb-form-field>
`);

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`
<sbb-form-field floating-label></sbb-form-field>
`);

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);
});
});
});
56 changes: 54 additions & 2 deletions src/elements/form-field/form-field/form-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ let nextFormFieldErrorId = 0;

const supportedPopupTagNames = ['sbb-autocomplete', 'sbb-autocomplete-grid', 'sbb-select'];

const patchedInputs = new WeakMap<HTMLInputElement, PropertyDescriptor>();

/**
* It wraps an input element adding label, errors, icon, etc.
*
Expand Down Expand Up @@ -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>): void {
Expand Down Expand Up @@ -204,11 +209,17 @@ class SbbFormFieldElement extends SbbNegativeMixin(SbbHydrationMixin(LitElement)
* It is used internally to assign the attributes of `<input>` 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;
}
Expand Down Expand Up @@ -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,
});
Expand Down Expand Up @@ -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',
Expand Down

0 comments on commit 922bc3c

Please sign in to comment.