Skip to content

Commit

Permalink
fix(material/input): preserve native placeholder on non-legacy appear…
Browse files Browse the repository at this point in the history
…ances (angular#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 angular#20903.
  • Loading branch information
crisbeto authored and forsti0506 committed Apr 3, 2022
1 parent 25a8101 commit e46aa62
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 4 deletions.
14 changes: 13 additions & 1 deletion src/material-experimental/mdc-input/input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -1861,14 +1872,15 @@ class MatInputWithLabel {}

@Component({
template: `
<mat-form-field [floatLabel]="floatLabel">
<mat-form-field [floatLabel]="floatLabel" [appearance]="appearance">
<mat-label>Label</mat-label>
<input matInput placeholder="Placeholder">
</mat-form-field>
`,
})
class MatInputWithLabelAndPlaceholder {
floatLabel: FloatLabelType;
appearance: MatFormFieldAppearance;
}

@Component({
Expand Down
10 changes: 10 additions & 0 deletions src/material/datepicker/date-range-input.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/material/form-field/form-field-input.scss
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@
}
}
}

._mat-animation-noopable & {
@include vendor-prefixes.input-placeholder {
transition: none;
}
}
}

// Prevents IE from always adding a scrollbar by default.
Expand Down
15 changes: 13 additions & 2 deletions src/material/input/input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -2232,14 +2242,15 @@ class MatInputWithLabel {}

@Component({
template: `
<mat-form-field [floatLabel]="floatLabel">
<mat-form-field [floatLabel]="floatLabel" [appearance]="appearance">
<mat-label>Label</mat-label>
<input matInput placeholder="Placeholder">
</mat-form-field>
`,
})
class MatInputWithLabelAndPlaceholder {
floatLabel: FloatLabelType;
appearance: MatFormFieldAppearance = 'legacy';
}

@Component({
Expand Down
6 changes: 5 additions & 1 deletion src/material/input/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit e46aa62

Please sign in to comment.