From e46aa62faa984084b07d80cdd5552d2e80554d76 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 18 Mar 2022 18:27:53 +0100 Subject: [PATCH] fix(material/input): preserve native placeholder on non-legacy appearances (#20936) The `legacy` form field appearance has a feature where it promotes the input placeholder to the form field label which introduces a problem where screen readers will read out the placeholder twice. Some time ago we added logic to clear the placeholder, but it seems to be a bit too aggressive since it also clears the placeholder for other appearances. These changes scope the workaround only to the case when a placeholder would be promoted to a label. Fixes #20903. --- src/material-experimental/mdc-input/input.spec.ts | 14 +++++++++++++- src/material/datepicker/date-range-input.scss | 10 ++++++++++ src/material/form-field/form-field-input.scss | 6 ++++++ src/material/input/input.spec.ts | 15 +++++++++++++-- src/material/input/input.ts | 6 +++++- 5 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/material-experimental/mdc-input/input.spec.ts b/src/material-experimental/mdc-input/input.spec.ts index 5af704229ccf..584932586bcc 100644 --- a/src/material-experimental/mdc-input/input.spec.ts +++ b/src/material-experimental/mdc-input/input.spec.ts @@ -882,6 +882,17 @@ describe('MatMdcInput without forms', () => { expect(formField.classList).not.toContain('mat-mdc-form-field-type-mat-native-select'); }); + it('should preserve the native placeholder on a non-legacy appearance', fakeAsync(() => { + const fixture = createComponent(MatInputWithLabelAndPlaceholder); + fixture.componentInstance.floatLabel = 'auto'; + fixture.componentInstance.appearance = 'outline'; + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector('input').getAttribute('placeholder')).toBe( + 'Placeholder', + ); + })); + it( 'should use the native input value when determining whether ' + 'the element is empty with a custom accessor', @@ -1861,7 +1872,7 @@ class MatInputWithLabel {} @Component({ template: ` - + Label @@ -1869,6 +1880,7 @@ class MatInputWithLabel {} }) class MatInputWithLabelAndPlaceholder { floatLabel: FloatLabelType; + appearance: MatFormFieldAppearance; } @Component({ diff --git a/src/material/datepicker/date-range-input.scss b/src/material/datepicker/date-range-input.scss index 9f39d4ce45a0..2c6257ea612f 100644 --- a/src/material/datepicker/date-range-input.scss +++ b/src/material/datepicker/date-range-input.scss @@ -28,6 +28,10 @@ $date-range-input-part-max-width: calc(50% - #{$date-range-input-separator-spaci .mat-date-range-input-separator { @include _placeholder-transition(opacity); margin: 0 $date-range-input-separator-spacing; + + ._mat-animation-noopable & { + transition: none; + } } .mat-date-range-input-separator-hidden { @@ -85,6 +89,12 @@ $date-range-input-part-max-width: calc(50% - #{$date-range-input-separator-spaci } } } + + ._mat-animation-noopable & { + @include vendor-prefixes.input-placeholder { + transition: none; + } + } } // We want the start input to be flush against the separator, no matter how much text it has, but diff --git a/src/material/form-field/form-field-input.scss b/src/material/form-field/form-field-input.scss index 386a57ce0a85..b3de12a0d03d 100644 --- a/src/material/form-field/form-field-input.scss +++ b/src/material/form-field/form-field-input.scss @@ -126,6 +126,12 @@ } } } + + ._mat-animation-noopable & { + @include vendor-prefixes.input-placeholder { + transition: none; + } + } } // Prevents IE from always adding a scrollbar by default. diff --git a/src/material/input/input.spec.ts b/src/material/input/input.spec.ts index b4c029cddd72..dba0f2bcdf74 100644 --- a/src/material/input/input.spec.ts +++ b/src/material/input/input.spec.ts @@ -973,7 +973,6 @@ describe('MatInput without forms', () => { expect(container.classList).toContain('mat-form-field-hide-placeholder'); expect(container.classList).not.toContain('mat-form-field-should-float'); expect(label.textContent.trim()).toBe('Label'); - expect(input.hasAttribute('placeholder')).toBe(false); input.value = 'Value'; fixture.detectChanges(); @@ -1020,6 +1019,17 @@ describe('MatInput without forms', () => { expect(container.classList).not.toContain('mat-form-field-should-float'); }); + it('should preserve the native placeholder on a non-legacy appearance', fakeAsync(() => { + const fixture = createComponent(MatInputWithLabelAndPlaceholder); + fixture.componentInstance.floatLabel = 'auto'; + fixture.componentInstance.appearance = 'standard'; + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector('input').getAttribute('placeholder')).toBe( + 'Placeholder', + ); + })); + it('should not add the native select class if the control is not a native select', () => { const fixture = createComponent(MatInputWithId); fixture.detectChanges(); @@ -2232,7 +2242,7 @@ class MatInputWithLabel {} @Component({ template: ` - + Label @@ -2240,6 +2250,7 @@ class MatInputWithLabel {} }) class MatInputWithLabelAndPlaceholder { floatLabel: FloatLabelType; + appearance: MatFormFieldAppearance = 'legacy'; } @Component({ diff --git a/src/material/input/input.ts b/src/material/input/input.ts index 6af7c3e98379..a799f0490d27 100644 --- a/src/material/input/input.ts +++ b/src/material/input/input.ts @@ -396,7 +396,11 @@ export class MatInput // screen readers will read it out twice: once from the label and once from the attribute. // TODO: can be removed once we get rid of the `legacy` style for the form field, because it's // the only one that supports promoting the placeholder to a label. - const placeholder = this._formField?._hideControlPlaceholder?.() ? null : this.placeholder; + const formField = this._formField; + const placeholder = + formField && formField.appearance === 'legacy' && !formField._hasLabel?.() + ? null + : this.placeholder; if (placeholder !== this._previousPlaceholder) { const element = this._elementRef.nativeElement; this._previousPlaceholder = placeholder;