From 97b90d7a8873c883817a03b951973ce547933e04 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 26 Oct 2020 18:15:25 +0100 Subject: [PATCH] fix(material/slider): some screen readers announcing long decimal values It looks like some screen readers announce the value of a slider by calculating the percentage themselves using the `aria-valuemin`, `aria-valuemax` and `aria-valuenow`. The problem is that they don't round down the decimals so for a slider between 0 and 1 with a step of 0.1, they end up reading out values like 0.20000068. These changes work around the issue by setting `aria-valuetext` to the same value that we shown in the thumb which we truncate ourselves. Fixes #20719. --- src/material/slider/slider.spec.ts | 34 ++++++++++++++++++++- src/material/slider/slider.ts | 10 ++++++ tools/public_api_guard/material/slider.d.ts | 3 +- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/material/slider/slider.spec.ts b/src/material/slider/slider.spec.ts index cb064373776b..286099894ce7 100644 --- a/src/material/slider/slider.spec.ts +++ b/src/material/slider/slider.spec.ts @@ -507,6 +507,37 @@ describe('MatSlider', () => { expect(sliderInstance.value).toBe(0.3); }); + it('should set the truncated value to the aria-valuetext', () => { + fixture.componentInstance.step = 0.1; + fixture.detectChanges(); + + dispatchSlideEventSequence(sliderNativeElement, 0, 0.333333); + fixture.detectChanges(); + + expect(sliderNativeElement.getAttribute('aria-valuetext')).toBe('33'); + }); + + it('should be able to override the aria-valuetext', () => { + fixture.componentInstance.step = 0.1; + fixture.componentInstance.valueText = 'custom'; + fixture.detectChanges(); + + dispatchSlideEventSequence(sliderNativeElement, 0, 0.333333); + fixture.detectChanges(); + + expect(sliderNativeElement.getAttribute('aria-valuetext')).toBe('custom'); + }); + + it('should be able to clear aria-valuetext', () => { + fixture.componentInstance.valueText = ''; + fixture.detectChanges(); + + dispatchSlideEventSequence(sliderNativeElement, 0, 0.333333); + fixture.detectChanges(); + + expect(sliderNativeElement.getAttribute('aria-valuetext')).toBeFalsy(); + }); + }); describe('slider with auto ticks', () => { @@ -1494,11 +1525,12 @@ class SliderWithMinAndMax { class SliderWithValue { } @Component({ - template: ``, + template: ``, styles: [styles], }) class SliderWithStep { step = 25; + valueText: string; } @Component({ diff --git a/src/material/slider/slider.ts b/src/material/slider/slider.ts index d06c9b77241c..18d858c654a8 100644 --- a/src/material/slider/slider.ts +++ b/src/material/slider/slider.ts @@ -134,6 +134,13 @@ const _MatSliderMixinBase: '[attr.aria-valuemax]': 'max', '[attr.aria-valuemin]': 'min', '[attr.aria-valuenow]': 'value', + + // NVDA and Jaws appear to announce the `aria-valuenow` by calculating its percentage based + // on its value between `aria-valuemin` and `aria-valuemax`. Due to how decimals are handled, + // it can cause the slider to read out a very long value like 0.20000068 if the current value + // is 0.2 with a min of 0 and max of 1. We work around the issue by setting `aria-valuetext` + // to the same value that we set on the slider's thumb which will be truncated. + '[attr.aria-valuetext]': 'valueText == null ? displayValue : valueText', '[attr.aria-orientation]': 'vertical ? "vertical" : "horizontal"', '[class.mat-slider-disabled]': 'disabled', '[class.mat-slider-has-ticks]': 'tickInterval', @@ -268,6 +275,9 @@ export class MatSlider extends _MatSliderMixinBase */ @Input() displayWith: (value: number) => string | number; + /** Text corresponding to the slider's value. Used primarily for improved accessibility. */ + @Input() valueText: string; + /** Whether the slider is vertical. */ @Input() get vertical(): boolean { return this._vertical; } diff --git a/tools/public_api_guard/material/slider.d.ts b/tools/public_api_guard/material/slider.d.ts index a5beb01f501f..bcfb19f00def 100644 --- a/tools/public_api_guard/material/slider.d.ts +++ b/tools/public_api_guard/material/slider.d.ts @@ -26,6 +26,7 @@ export declare class MatSlider extends _MatSliderMixinBase implements ControlVal get value(): number | null; set value(v: number | null); readonly valueChange: EventEmitter; + valueText: string; get vertical(): boolean; set vertical(value: boolean); constructor(elementRef: ElementRef, _focusMonitor: FocusMonitor, _changeDetectorRef: ChangeDetectorRef, _dir: Directionality, tabIndex: string, _ngZone: NgZone, _document: any, _animationMode?: string | undefined); @@ -71,7 +72,7 @@ export declare class MatSlider extends _MatSliderMixinBase implements ControlVal static ngAcceptInputType_tickInterval: NumberInput; static ngAcceptInputType_value: NumberInput; static ngAcceptInputType_vertical: BooleanInput; - static ɵcmp: i0.ɵɵComponentDefWithMeta; + static ɵcmp: i0.ɵɵComponentDefWithMeta; static ɵfac: i0.ɵɵFactoryDef; }