diff --git a/textfield/lib/text-field.ts b/textfield/lib/text-field.ts index af798b557..1a5368d05 100644 --- a/textfield/lib/text-field.ts +++ b/textfield/lib/text-field.ts @@ -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) { + // 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 // 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 to fully render // before checking its value. diff --git a/textfield/lib/text-field_test.ts b/textfield/lib/text-field_test.ts index ed9c7c0bb..62e85391d 100644 --- a/textfield/lib/text-field_test.ts +++ b/textfield/lib/text-field_test.ts @@ -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', () => {