Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(sbb-form-field): update floating label on programmatic changes #3277

Merged
merged 3 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@

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 @@ -155,6 +157,9 @@
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 @@ -207,11 +212,17 @@
* 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 @@ -285,7 +296,9 @@

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 @@ -337,6 +350,45 @@
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;
}

Check warning on line 364 in src/elements/form-field/form-field/form-field.ts

View check run for this annotation

Codecov / codecov/patch

src/elements/form-field/form-field/form-field.ts#L363-L364

Added lines #L363 - L364 were not covered by tests

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
Loading