fix(text-field): ensure value can overwrite defaultValue

PiperOrigin-RevId: 472984918
This commit is contained in:
Elizabeth Mitchell 2022-09-08 07:36:03 -07:00 committed by Copybara-Service
parent e77d4726fa
commit 58ae98cbc8
2 changed files with 87 additions and 2 deletions

View File

@ -10,7 +10,7 @@ import {redispatchEvent} from '@material/web/controller/events.js';
import {FormController, getFormValue} from '@material/web/controller/form-controller.js';
import {stringConverter} from '@material/web/controller/string-converter.js';
import {ariaProperty} from '@material/web/decorators/aria-property.js';
import {html, LitElement, TemplateResult} from 'lit';
import {html, LitElement, PropertyValues, TemplateResult} from 'lit';
import {property, query, queryAssignedElements, state} from 'lit/decorators.js';
import {ClassInfo, classMap} from 'lit/directives/class-map.js';
import {ifDefined} from 'lit/directives/if-defined.js';
@ -308,6 +308,19 @@ export abstract class TextField extends LitElement {
*/
@state() protected dirty = false;
@state() protected focused = false;
/**
* Returns true when the text field's `value` property has been changed from
* it's initial value.
*
* Setting `value` should always overwrite `defaultValue`, even when `value`
* is an empty string. This flag ensures that behavior.
*/
@state() protected valueHasChanged = false;
/**
* Whether or not to ignore the next `value` change when computing
* `valueHasChanged`.
*/
protected ignoreNextValueChange = false;
@query('.md3-text-field__input')
protected readonly input?: HTMLInputElement|null;
protected abstract readonly fieldTag: StaticValue;
@ -478,6 +491,8 @@ export abstract class TextField extends LitElement {
*/
reset() {
this.dirty = false;
this.valueHasChanged = false;
this.ignoreNextValueChange = true;
this.value = this.defaultValue;
}
@ -606,7 +621,8 @@ export abstract class TextField extends LitElement {
/** @soyTemplate */
protected getInputValue(): string {
return this.dirty ? this.value : this.value || this.defaultValue;
const alwaysShowValue = this.dirty || this.valueHasChanged;
return alwaysShowValue ? this.value : this.defaultValue || this.value;
}
/** @soyTemplate */
@ -682,11 +698,30 @@ export abstract class TextField extends LitElement {
return this.hasCounter() ? html`${length} / ${this.maxLength}` : html``;
}
protected override update(changedProperties: PropertyValues<TextField>) {
// Consider a value change anything that is not the initial empty string
// value.
const valueHasChanged = changedProperties.has('value') &&
changedProperties.get('value') !== undefined;
if (valueHasChanged && !this.ignoreNextValueChange) {
this.valueHasChanged = true;
}
if (this.ignoreNextValueChange) {
this.ignoreNextValueChange = false;
}
super.update(changedProperties);
}
protected override updated() {
// If a property such as `type` changes and causes the internal <input>
// value to change without dispatching an event, re-sync it.
const value = this.getInput().value;
if (this.value !== value) {
// Don't consider these updates (such as setting `defaultValue`) as
// the developer directly changing the `value`.
this.ignoreNextValueChange = true;
// Note this is typically inefficient in updated() since it schedules
// another update. However, it is needed for the <input> to fully render
// before checking its value.

View File

@ -162,6 +162,27 @@ describe('TextField', () => {
expect(harness.element.defaultValue).toBe('');
expect(harness.element.value).toBe('');
});
it('should allow defaultValue to update value again', async () => {
const {harness} = await setupTest();
// defaultValue changes value
harness.element.defaultValue = 'First default';
await env.waitForStability();
expect(harness.element.value).toBe('First default');
// Setting value programatically causes it to stick
harness.element.value = 'Value';
harness.element.defaultValue = 'Second default';
await env.waitForStability();
expect(harness.element.value).toBe('Value');
// Resetting should return to original functionality
harness.element.reset();
harness.element.defaultValue = 'Third default';
await env.waitForStability();
expect(harness.element.value).toBe('Third default');
});
});
describe('default value', () => {
@ -174,6 +195,17 @@ describe('TextField', () => {
expect(harness.element.value).toBe('Default');
});
it('should update `value` multiple times', async () => {
const {harness} = await setupTest();
harness.element.defaultValue = 'First default';
await env.waitForStability();
harness.element.defaultValue = 'Second default';
await env.waitForStability();
expect(harness.element.value).toBe('Second default');
});
it('should NOT update `value` after user input', async () => {
const {harness} = await setupTest();
@ -187,6 +219,24 @@ describe('TextField', () => {
expect(harness.element.value).toBe('Value');
});
it('should render `value` instead of `defaultValue` when `value` changes',
async () => {
const {harness, input} = await setupTest();
harness.element.defaultValue = 'Default';
await env.waitForStability();
expect(input.value).toBe('Default');
harness.element.value = 'Value';
await env.waitForStability();
expect(input.value).toBe('Value');
harness.element.value = '';
await env.waitForStability();
expect(input.value).toBe('');
expect(harness.element.defaultValue).toBe('Default');
});
});
describe('valueAsDate', () => {