Skip to content

Commit 02d0897

Browse files
fix(slider): enhance accessibility with ARIA attributes
1 parent 906efe0 commit 02d0897

File tree

4 files changed

+194
-63
lines changed

4 files changed

+194
-63
lines changed

projects/igniteui-angular/src/lib/slider/slider.component.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,16 @@
3737
#thumbFrom
3838
type="from"
3939
[value]="lowerLabel"
40+
[min]="minValue"
41+
[max]="maxValue"
4042
[disabled]="disabled"
4143
[continuous]="continuous"
4244
[onPan]="onPan"
4345
[stepDistance]="stepDistance"
4446
[step]="step"
4547
[templateRef]="thumbFromTemplateRef"
4648
[context]="context"
49+
[labels]="labels"
4750
(thumbChange)="onThumbChange()"
4851
(hoverChange)="onHoverChange($event)"
4952
[deactiveState]="deactivateThumbLabel"
@@ -62,13 +65,16 @@
6265
#thumbTo
6366
type="to"
6467
[value]="upperLabel"
68+
[min]="minValue"
69+
[max]="maxValue"
6570
[disabled]="disabled"
6671
[continuous]="continuous"
6772
[onPan]="onPan"
6873
[stepDistance]="stepDistance"
6974
[step]="step"
7075
[templateRef]="thumbToTemplateRef"
7176
[context]="context"
77+
[labels]="labels"
7278
(thumbChange)="onThumbChange()"
7379
(hoverChange)="onHoverChange($event)"
7480
[deactiveState]="deactivateThumbLabel"

projects/igniteui-angular/src/lib/slider/slider.component.spec.ts

Lines changed: 144 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1819,45 +1819,100 @@ describe('IgxSlider', () => {
18191819
});
18201820

18211821
describe('Accessibility: ARIA Attributes', () => {
1822-
let fixture: ComponentFixture<SliderInitializeTestComponent>;
1822+
let fixture: ComponentFixture<RangeSliderTestComponent>;
18231823
let slider: IgxSliderComponent;
18241824

18251825
beforeEach(() => {
1826-
fixture = TestBed.createComponent(SliderInitializeTestComponent);
1826+
fixture = TestBed.createComponent(RangeSliderTestComponent);
18271827
slider = fixture.componentInstance.slider;
18281828
fixture.detectChanges();
18291829
});
18301830

1831-
it('aria properties should be successfully applied', () => {
1832-
const sliderElement = fixture.nativeElement.querySelector('igx-slider');
1833-
const sliderRole = fixture.nativeElement.querySelector('igx-slider[role="slider"]');
1831+
it('should apply all ARIA properties correctly to both thumbs', fakeAsync(() => {
1832+
fixture = TestBed.createComponent(RangeSliderTestComponent);
1833+
slider = fixture.componentInstance.slider;
1834+
fixture.detectChanges();
1835+
tick();
18341836

1835-
expect(sliderElement).toBeDefined();
1836-
expect(sliderRole).toBeDefined();
1837+
const thumbFrom = fixture.debugElement.query(By.css(THUMB_FROM_CLASS)).nativeElement;
1838+
const thumbTo = fixture.debugElement.query(By.css(THUMB_TO_CLASS)).nativeElement;
18371839

1838-
const minValue = parseInt(sliderElement.getAttribute('aria-valuemin'), 10);
1839-
const maxValue = parseInt(sliderElement.getAttribute('aria-valuemax'), 10);
1840-
const readOnly = sliderElement.getAttribute('aria-readonly');
1840+
expect(thumbFrom.getAttribute('role')).toBe('slider');
1841+
expect(thumbFrom.getAttribute('tabindex')).toBe('0');
1842+
expect(parseInt(thumbFrom.getAttribute('aria-valuenow'), 10)).toBe(slider.lowerValue);
1843+
expect(parseInt(thumbFrom.getAttribute('aria-valuemin'), 10)).toBe(slider.minValue);
1844+
expect(parseInt(thumbFrom.getAttribute('aria-valuemax'), 10)).toBe(slider.maxValue);
1845+
expect(thumbFrom.getAttribute('aria-label')).toBe('Slider thumb from');
1846+
expect(thumbFrom.getAttribute('aria-orientation')).toBe('horizontal');
1847+
expect(thumbFrom.getAttribute('aria-disabled')).toBe('false');
1848+
1849+
expect(thumbTo.getAttribute('role')).toBe('slider');
1850+
expect(thumbTo.getAttribute('tabindex')).toBe('0');
1851+
expect(parseInt(thumbTo.getAttribute('aria-valuenow'), 10)).toBe(slider.upperValue);
1852+
expect(parseInt(thumbTo.getAttribute('aria-valuemin'), 10)).toBe(slider.minValue);
1853+
expect(parseInt(thumbTo.getAttribute('aria-valuemax'), 10)).toBe(slider.maxValue);
1854+
expect(thumbTo.getAttribute('aria-label')).toBe('Slider thumb to');
1855+
expect(thumbTo.getAttribute('aria-orientation')).toBe('horizontal');
1856+
expect(thumbTo.getAttribute('aria-disabled')).toBe('false');
1857+
1858+
slider.labels = ['Low', 'Medium', 'High'];
1859+
fixture.detectChanges();
1860+
tick();
18411861

1842-
expect(minValue).toBe(slider.minValue);
1843-
expect(maxValue).toBe(slider.maxValue);
1844-
expect(readOnly).toBe('false');
1845-
});
1862+
expect(thumbFrom.getAttribute('aria-valuetext')).toBe('Low');
1863+
expect(thumbTo.getAttribute('aria-valuetext')).toBe('High');
18461864

1847-
it('should maintain accessibility attributes when slider is disabled', () => {
1848-
const sliderElement = fixture.nativeElement.querySelector('igx-slider');
1849-
const sliderRole = fixture.nativeElement.querySelector('igx-slider[role="slider"]');
1865+
slider.disabled = true;
1866+
fixture.detectChanges();
1867+
tick();
18501868

1851-
expect(sliderElement).toBeDefined();
1852-
expect(sliderRole).toBeDefined();
1869+
expect(thumbFrom.getAttribute('aria-disabled')).toBe('true');
1870+
expect(thumbTo.getAttribute('aria-disabled')).toBe('true');
1871+
}));
18531872

1854-
slider.disabled = true;
1873+
it('should apply correct tabindex to thumbs', fakeAsync(() => {
1874+
fixture = TestBed.createComponent(RangeSliderTestComponent);
1875+
slider = fixture.componentInstance.slider;
18551876
fixture.detectChanges();
1877+
tick();
18561878

1857-
const disabled = sliderElement.getAttribute('aria-readonly');
1879+
const thumbFrom = fixture.debugElement.query(By.css(THUMB_FROM_CLASS)).nativeElement;
1880+
const thumbTo = fixture.debugElement.query(By.css(THUMB_TO_CLASS)).nativeElement;
18581881

1859-
expect(disabled).toBe('true');
1860-
});
1882+
expect(thumbFrom.getAttribute('tabindex')).toBe('0');
1883+
expect(thumbTo.getAttribute('tabindex')).toBe('0');
1884+
}));
1885+
1886+
it('should apply correct role to thumbs', fakeAsync(() => {
1887+
fixture = TestBed.createComponent(RangeSliderTestComponent);
1888+
slider = fixture.componentInstance.slider;
1889+
fixture.detectChanges();
1890+
tick();
1891+
1892+
const thumbFrom = fixture.debugElement.query(By.css(THUMB_FROM_CLASS)).nativeElement;
1893+
const thumbTo = fixture.debugElement.query(By.css(THUMB_TO_CLASS)).nativeElement;
1894+
1895+
expect(thumbFrom.getAttribute('role')).toBe('slider');
1896+
expect(thumbTo.getAttribute('role')).toBe('slider');
1897+
}));
1898+
1899+
it('should apply aria-valuenow, aria-valuemin, and aria-valuemax to thumbs', fakeAsync(() => {
1900+
fixture = TestBed.createComponent(RangeSliderTestComponent);
1901+
slider = fixture.componentInstance.slider;
1902+
fixture.detectChanges();
1903+
tick();
1904+
1905+
const thumbFrom = fixture.debugElement.query(By.css(THUMB_FROM_CLASS)).nativeElement;
1906+
const thumbTo = fixture.debugElement.query(By.css(THUMB_TO_CLASS)).nativeElement;
1907+
1908+
expect(thumbFrom.getAttribute('aria-valuenow')).toBe(String(slider.lowerValue));
1909+
expect(thumbFrom.getAttribute('aria-valuemin')).toBe(String(slider.minValue));
1910+
expect(thumbFrom.getAttribute('aria-valuemax')).toBe(String(slider.maxValue));
1911+
1912+
expect(thumbTo.getAttribute('aria-valuenow')).toBe(String(slider.upperValue));
1913+
expect(thumbTo.getAttribute('aria-valuemin')).toBe(String(slider.minValue));
1914+
expect(thumbTo.getAttribute('aria-valuemax')).toBe(String(slider.maxValue));
1915+
}));
18611916

18621917
it('should apply aria-valuenow to the thumbs', fakeAsync(() => {
18631918
fixture = TestBed.createComponent(RangeSliderTestComponent);
@@ -1895,16 +1950,78 @@ describe('IgxSlider', () => {
18951950
expect(thumbTo.getAttribute('aria-valuenow')).toBe('70');
18961951
}));
18971952

1898-
it('should have correct aria-labelledby references for each thumb', () => {
1953+
it('should apply aria-valuetext when labels are provided', fakeAsync(() => {
18991954
fixture = TestBed.createComponent(RangeSliderTestComponent);
1955+
slider = fixture.componentInstance.slider;
1956+
fixture.detectChanges();
1957+
tick();
1958+
1959+
slider.labels = ['Low', 'Medium', 'High'];
1960+
tick();
19001961
fixture.detectChanges();
19011962

19021963
const thumbFrom = fixture.debugElement.query(By.css(THUMB_FROM_CLASS)).nativeElement;
19031964
const thumbTo = fixture.debugElement.query(By.css(THUMB_TO_CLASS)).nativeElement;
19041965

1905-
expect(thumbFrom.getAttribute('aria-labelledby')).toBe('slider-label-from');
1906-
expect(thumbTo.getAttribute('aria-labelledby')).toBe('slider-label-to');
1907-
});
1966+
expect(thumbFrom.getAttribute('aria-valuetext')).toBe('Low');
1967+
expect(thumbTo.getAttribute('aria-valuetext')).toBe('High');
1968+
1969+
slider.value = {
1970+
lower: 1,
1971+
upper: 1
1972+
};
1973+
fixture.detectChanges();
1974+
tick();
1975+
1976+
expect(thumbFrom.getAttribute('aria-valuetext')).toBe('Medium');
1977+
expect(thumbTo.getAttribute('aria-valuetext')).toBe('Medium');
1978+
}));
1979+
1980+
it('should apply correct aria-label to thumbs', fakeAsync(() => {
1981+
fixture = TestBed.createComponent(RangeSliderTestComponent);
1982+
slider = fixture.componentInstance.slider;
1983+
fixture.detectChanges();
1984+
tick();
1985+
1986+
const thumbFrom = fixture.debugElement.query(By.css(THUMB_FROM_CLASS)).nativeElement;
1987+
const thumbTo = fixture.debugElement.query(By.css(THUMB_TO_CLASS)).nativeElement;
1988+
1989+
expect(thumbFrom.getAttribute('aria-label')).toBe('Slider thumb from');
1990+
expect(thumbTo.getAttribute('aria-label')).toBe('Slider thumb to');
1991+
}));
1992+
1993+
it('should apply correct aria-orientation to thumbs', fakeAsync(() => {
1994+
fixture = TestBed.createComponent(RangeSliderTestComponent);
1995+
slider = fixture.componentInstance.slider;
1996+
fixture.detectChanges();
1997+
tick();
1998+
1999+
const thumbFrom = fixture.debugElement.query(By.css(THUMB_FROM_CLASS)).nativeElement;
2000+
const thumbTo = fixture.debugElement.query(By.css(THUMB_TO_CLASS)).nativeElement;
2001+
2002+
expect(thumbFrom.getAttribute('aria-orientation')).toBe('horizontal');
2003+
expect(thumbTo.getAttribute('aria-orientation')).toBe('horizontal');
2004+
}));
2005+
2006+
it('should update aria-disabled when the slider is disabled', fakeAsync(() => {
2007+
fixture = TestBed.createComponent(RangeSliderTestComponent);
2008+
slider = fixture.componentInstance.slider;
2009+
fixture.detectChanges();
2010+
tick();
2011+
2012+
const thumbFrom = fixture.debugElement.query(By.css(THUMB_FROM_CLASS)).nativeElement;
2013+
const thumbTo = fixture.debugElement.query(By.css(THUMB_TO_CLASS)).nativeElement;
2014+
2015+
expect(thumbFrom.getAttribute('aria-disabled')).toBe('false');
2016+
expect(thumbTo.getAttribute('aria-disabled')).toBe('false');
2017+
2018+
slider.disabled = true;
2019+
fixture.detectChanges();
2020+
tick();
2021+
2022+
expect(thumbFrom.getAttribute('aria-disabled')).toBe('true');
2023+
expect(thumbTo.getAttribute('aria-disabled')).toBe('true');
2024+
}));
19082025
});
19092026

19102027
const verifySecondaryTicsLabelsAreHidden = (ticks, hidden) => {

projects/igniteui-angular/src/lib/slider/slider.component.ts

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,6 @@ export class IgxSliderComponent implements
9696
@ContentChild(IgxTickLabelTemplateDirective, { read: TemplateRef, static: false })
9797
public tickLabelTemplateRef: TemplateRef<any>;
9898

99-
/**
100-
* @hidden
101-
*/
102-
@HostBinding(`attr.role`)
103-
public role = 'slider';
104-
10599
/**
106100
* @hidden
107101
*/
@@ -128,30 +122,6 @@ export class IgxSliderComponent implements
128122
@Input()
129123
public thumbLabelVisibilityDuration = 750;
130124

131-
/**
132-
* @hidden
133-
*/
134-
@HostBinding(`attr.aria-valuemin`)
135-
public get valuemin() {
136-
return this.minValue;
137-
}
138-
139-
/**
140-
* @hidden
141-
*/
142-
@HostBinding(`attr.aria-valuemax`)
143-
public get valuemax() {
144-
return this.maxValue;
145-
}
146-
147-
/**
148-
* @hidden
149-
*/
150-
@HostBinding(`attr.aria-readonly`)
151-
public get readonly() {
152-
return this.disabled;
153-
}
154-
155125
/**
156126
* @hidden
157127
*/
@@ -1353,7 +1323,7 @@ export class IgxSliderComponent implements
13531323
}
13541324

13551325
private changeThumbFocusableState(state: boolean) {
1356-
const value = state ? -1 : 1;
1326+
const value = state ? -1 : 0;
13571327

13581328
if (this.isRange) {
13591329
this.thumbFrom.tabindex = value;

projects/igniteui-angular/src/lib/slider/thumb/thumb-slider.component.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ export class IgxSliderThumbComponent implements OnInit, OnDestroy {
6060
@Input({ transform: booleanAttribute })
6161
public deactiveState: boolean;
6262

63+
@Input()
64+
public min: number;
65+
66+
@Input()
67+
public max: number;
68+
69+
@Input()
70+
public labels: any[];
71+
6372
@Output()
6473
public thumbValueChange = new EventEmitter<number>();
6574

@@ -75,19 +84,48 @@ export class IgxSliderThumbComponent implements OnInit, OnDestroy {
7584
@HostBinding('attr.tabindex')
7685
public tabindex = 0;
7786

78-
@HostBinding('attr.z-index')
79-
public zIndex = 0;
87+
@HostBinding('attr.role')
88+
public role = 'slider';
8089

8190
@HostBinding('attr.aria-valuenow')
8291
public get ariaValueNow() {
8392
return this.value;
8493
}
8594

86-
@HostBinding('attr.aria-labelledby')
87-
public get ariaLabelledbyAttr() {
88-
return this.type === SliderHandle.FROM ? 'slider-label-from' : 'slider-label-to';
95+
@HostBinding('attr.aria-valuemin')
96+
public get ariaValueMin() {
97+
return this.min;
98+
}
99+
100+
@HostBinding('attr.aria-valuemax')
101+
public get ariaValueMax() {
102+
return this.max;
103+
}
104+
105+
@HostBinding('attr.aria-valuetext')
106+
public get ariaValueText() {
107+
if (this.labels && this.labels[this.value] !== undefined) {
108+
return this.labels[this.value];
109+
}
110+
return this.value;
111+
}
112+
113+
@HostBinding('attr.aria-label')
114+
public get ariaLabelAttr() {
115+
return `Slider thumb ${this.type}`;
89116
}
90117

118+
@HostBinding('attr.aria-orientation')
119+
public ariaOrientation = 'horizontal';
120+
121+
@HostBinding(`attr.aria-disabled`)
122+
public get ariaDisabled() {
123+
return this.disabled;
124+
}
125+
126+
@HostBinding('attr.z-index')
127+
public zIndex = 0;
128+
91129
@HostBinding('class.igx-slider-thumb-to--focused')
92130
public focused = false;
93131

0 commit comments

Comments
 (0)