Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(slider): improve accessibility by correcting ARIA attributes and tab index - 17.2.x #14795

Merged
merged 7 commits into from
Oct 22, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,16 @@
#thumbFrom
type="from"
[value]="lowerLabel"
[min]="minValue"
[max]="maxValue"
[disabled]="disabled"
[continuous]="continuous"
[onPan]="onPan"
[stepDistance]="stepDistance"
[step]="step"
[templateRef]="thumbFromTemplateRef"
[context]="context"
[labels]="labels"
(thumbChange)="onThumbChange()"
(hoverChange)="onHoverChange($event)"
[deactiveState]="deactivateThumbLabel"
Expand All @@ -62,13 +65,16 @@
#thumbTo
type="to"
[value]="upperLabel"
[min]="minValue"
[max]="maxValue"
[disabled]="disabled"
[continuous]="continuous"
[onPan]="onPan"
[stepDistance]="stepDistance"
[step]="step"
[templateRef]="thumbToTemplateRef"
[context]="context"
[labels]="labels"
(thumbChange)="onThumbChange()"
(hoverChange)="onHoverChange($event)"
[deactiveState]="deactivateThumbLabel"
Expand Down
249 changes: 217 additions & 32 deletions projects/igniteui-angular/src/lib/slider/slider.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ describe('IgxSlider', () => {
SliderMinMaxComponent,
SliderTestComponent,
SliderWithLabelsComponent,
RangeSliderTestComponent,
RangeSliderWithLabelsComponent,
RangeSliderWithCustomTemplateComponent,
SliderTicksComponent,
Expand Down Expand Up @@ -897,22 +898,6 @@ describe('IgxSlider', () => {
expect(slider.upperBound).toBe(100);
expect(slider.lowerBound).toBe(0);
});

it('aria properties should be successfully applied', () => {
const sliderElement = fixture.nativeElement.querySelector('igx-slider');
const sliderRole = fixture.nativeElement.querySelector('igx-slider[role="slider"]');

expect(sliderElement).toBeDefined();
expect(sliderRole).toBeDefined();

const minValue = parseInt(sliderElement.getAttribute('aria-valuemin'), 10);
const maxValue = parseInt(sliderElement.getAttribute('aria-valuemax'), 10);
const readOnly = sliderElement.getAttribute('aria-readonly');

expect(minValue).toBe(slider.minValue);
expect(maxValue).toBe(slider.maxValue);
expect(readOnly).toBe('false');
});
});

describe('Slider type: Range - List View', () => {
Expand Down Expand Up @@ -1196,22 +1181,6 @@ describe('IgxSlider', () => {
expect(slider.upperBound).toBe(slider.maxValue);
expect(slider.lowerBound).toBe(slider.minValue);
});

it('aria properties should be successfully applied', () => {
const sliderElement = fixture.nativeElement.querySelector('igx-slider');
const sliderRole = fixture.nativeElement.querySelector('igx-slider[role="slider"]');

expect(sliderElement).toBeDefined();
expect(sliderRole).toBeDefined();

const minValue = parseInt(sliderElement.getAttribute('aria-valuemin'), 10);
const maxValue = parseInt(sliderElement.getAttribute('aria-valuemax'), 10);
const readOnly = sliderElement.getAttribute('aria-readonly');

expect(minValue).toBe(slider.minValue);
expect(maxValue).toBe(slider.maxValue);
expect(readOnly).toBe('false');
});
});

describe('General Tests', () => {
Expand Down Expand Up @@ -1849,6 +1818,211 @@ describe('IgxSlider', () => {
}));
});

describe('Accessibility: ARIA Attributes', () => {
let fixture: ComponentFixture<RangeSliderTestComponent>;
let slider: IgxSliderComponent;

beforeEach(() => {
fixture = TestBed.createComponent(RangeSliderTestComponent);
slider = fixture.componentInstance.slider;
fixture.detectChanges();
});

it('should apply all ARIA properties correctly to both thumbs', fakeAsync(() => {
fixture = TestBed.createComponent(RangeSliderTestComponent);
slider = fixture.componentInstance.slider;
fixture.detectChanges();
tick();

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

expect(thumbFrom.getAttribute('role')).toBe('slider');
expect(thumbFrom.getAttribute('tabindex')).toBe('0');
expect(parseInt(thumbFrom.getAttribute('aria-valuenow'), 10)).toBe(slider.lowerValue);
expect(parseInt(thumbFrom.getAttribute('aria-valuemin'), 10)).toBe(slider.minValue);
expect(parseInt(thumbFrom.getAttribute('aria-valuemax'), 10)).toBe(slider.maxValue);
expect(thumbFrom.getAttribute('aria-label')).toBe('Slider thumb from');
expect(thumbFrom.getAttribute('aria-orientation')).toBe('horizontal');
expect(thumbFrom.getAttribute('aria-disabled')).toBe('false');

expect(thumbTo.getAttribute('role')).toBe('slider');
expect(thumbTo.getAttribute('tabindex')).toBe('0');
expect(parseInt(thumbTo.getAttribute('aria-valuenow'), 10)).toBe(slider.upperValue);
expect(parseInt(thumbTo.getAttribute('aria-valuemin'), 10)).toBe(slider.minValue);
expect(parseInt(thumbTo.getAttribute('aria-valuemax'), 10)).toBe(slider.maxValue);
expect(thumbTo.getAttribute('aria-label')).toBe('Slider thumb to');
expect(thumbTo.getAttribute('aria-orientation')).toBe('horizontal');
expect(thumbTo.getAttribute('aria-disabled')).toBe('false');

slider.labels = ['Low', 'Medium', 'High'];
fixture.detectChanges();
tick();

expect(thumbFrom.getAttribute('aria-valuetext')).toBe('Low');
expect(thumbTo.getAttribute('aria-valuetext')).toBe('High');

slider.disabled = true;
fixture.detectChanges();
tick();

expect(thumbFrom.getAttribute('aria-disabled')).toBe('true');
expect(thumbTo.getAttribute('aria-disabled')).toBe('true');
}));

it('should apply correct tabindex to thumbs', fakeAsync(() => {
fixture = TestBed.createComponent(RangeSliderTestComponent);
slider = fixture.componentInstance.slider;
fixture.detectChanges();
tick();

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

expect(thumbFrom.getAttribute('tabindex')).toBe('0');
expect(thumbTo.getAttribute('tabindex')).toBe('0');
}));

it('should apply correct role to thumbs', fakeAsync(() => {
fixture = TestBed.createComponent(RangeSliderTestComponent);
slider = fixture.componentInstance.slider;
fixture.detectChanges();
tick();

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

expect(thumbFrom.getAttribute('role')).toBe('slider');
expect(thumbTo.getAttribute('role')).toBe('slider');
}));

it('should apply aria-valuenow, aria-valuemin, and aria-valuemax to thumbs', fakeAsync(() => {
fixture = TestBed.createComponent(RangeSliderTestComponent);
slider = fixture.componentInstance.slider;
fixture.detectChanges();
tick();

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

expect(thumbFrom.getAttribute('aria-valuenow')).toBe(String(slider.lowerValue));
expect(thumbFrom.getAttribute('aria-valuemin')).toBe(String(slider.minValue));
expect(thumbFrom.getAttribute('aria-valuemax')).toBe(String(slider.maxValue));

expect(thumbTo.getAttribute('aria-valuenow')).toBe(String(slider.upperValue));
expect(thumbTo.getAttribute('aria-valuemin')).toBe(String(slider.minValue));
expect(thumbTo.getAttribute('aria-valuemax')).toBe(String(slider.maxValue));
}));

it('should apply aria-valuenow to the thumbs', fakeAsync(() => {
fixture = TestBed.createComponent(RangeSliderTestComponent);
slider = fixture.componentInstance.slider;
fixture.detectChanges();
tick();

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

expect(thumbFrom.getAttribute('aria-valuenow')).toBe(String(slider.lowerLabel));
expect(thumbTo.getAttribute('aria-valuenow')).toBe(String(slider.upperLabel));
}));

it('should update aria-valuenow when the slider value changes', fakeAsync(() => {
fixture = TestBed.createComponent(RangeSliderTestComponent);
slider = fixture.componentInstance.slider;
fixture.detectChanges();
tick();

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

expect(thumbFrom.getAttribute('aria-valuenow')).toBe(String(slider.lowerLabel));
expect(thumbTo.getAttribute('aria-valuenow')).toBe(String(slider.upperLabel));

slider.value = {
lower: 30,
upper: 70
};
fixture.detectChanges();
tick();

expect(thumbFrom.getAttribute('aria-valuenow')).toBe('30');
expect(thumbTo.getAttribute('aria-valuenow')).toBe('70');
}));

it('should apply aria-valuetext when labels are provided', fakeAsync(() => {
fixture = TestBed.createComponent(RangeSliderTestComponent);
slider = fixture.componentInstance.slider;
fixture.detectChanges();
tick();

slider.labels = ['Low', 'Medium', 'High'];
tick();
fixture.detectChanges();

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

expect(thumbFrom.getAttribute('aria-valuetext')).toBe('Low');
expect(thumbTo.getAttribute('aria-valuetext')).toBe('High');

slider.value = {
lower: 1,
upper: 1
};
fixture.detectChanges();
tick();

expect(thumbFrom.getAttribute('aria-valuetext')).toBe('Medium');
expect(thumbTo.getAttribute('aria-valuetext')).toBe('Medium');
}));

it('should apply correct aria-label to thumbs', fakeAsync(() => {
fixture = TestBed.createComponent(RangeSliderTestComponent);
slider = fixture.componentInstance.slider;
fixture.detectChanges();
tick();

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

expect(thumbFrom.getAttribute('aria-label')).toBe('Slider thumb from');
expect(thumbTo.getAttribute('aria-label')).toBe('Slider thumb to');
}));

it('should apply correct aria-orientation to thumbs', fakeAsync(() => {
fixture = TestBed.createComponent(RangeSliderTestComponent);
slider = fixture.componentInstance.slider;
fixture.detectChanges();
tick();

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

expect(thumbFrom.getAttribute('aria-orientation')).toBe('horizontal');
expect(thumbTo.getAttribute('aria-orientation')).toBe('horizontal');
}));

it('should update aria-disabled when the slider is disabled', fakeAsync(() => {
fixture = TestBed.createComponent(RangeSliderTestComponent);
slider = fixture.componentInstance.slider;
fixture.detectChanges();
tick();

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

expect(thumbFrom.getAttribute('aria-disabled')).toBe('false');
expect(thumbTo.getAttribute('aria-disabled')).toBe('false');

slider.disabled = true;
fixture.detectChanges();
tick();

expect(thumbFrom.getAttribute('aria-disabled')).toBe('true');
expect(thumbTo.getAttribute('aria-disabled')).toBe('true');
}));
});

const verifySecondaryTicsLabelsAreHidden = (ticks, hidden) => {
const allTicks = Array.from(ticks.nativeElement.querySelectorAll(`${SLIDER_GROUP_TICKS_CLASS}`));
Expand Down Expand Up @@ -1983,6 +2157,17 @@ class SliderWithLabelsComponent {
@ViewChild(IgxSliderComponent, { read: IgxSliderComponent, static: true }) public slider: IgxSliderComponent;
}

@Component({
template: `<igx-slider #slider [type]="type">
</igx-slider>`,
standalone: true,
imports: [IgxSliderComponent]
})
class RangeSliderTestComponent {
@ViewChild(IgxSliderComponent, { static: true }) public slider: IgxSliderComponent;
public type = IgxSliderType.RANGE;
}

@Component({
template: `
<igx-slider
Expand Down
32 changes: 1 addition & 31 deletions projects/igniteui-angular/src/lib/slider/slider.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,6 @@ export class IgxSliderComponent implements
@ContentChild(IgxTickLabelTemplateDirective, { read: TemplateRef, static: false })
public tickLabelTemplateRef: TemplateRef<any>;

/**
* @hidden
*/
@HostBinding(`attr.role`)
public role = 'slider';

/**
* @hidden
*/
Expand All @@ -128,30 +122,6 @@ export class IgxSliderComponent implements
@Input()
public thumbLabelVisibilityDuration = 750;

/**
* @hidden
*/
@HostBinding(`attr.aria-valuemin`)
public get valuemin() {
return this.minValue;
}

/**
* @hidden
*/
@HostBinding(`attr.aria-valuemax`)
public get valuemax() {
return this.maxValue;
}

/**
* @hidden
*/
@HostBinding(`attr.aria-readonly`)
public get readonly() {
return this.disabled;
}

/**
* @hidden
*/
Expand Down Expand Up @@ -1353,7 +1323,7 @@ export class IgxSliderComponent implements
}

private changeThumbFocusableState(state: boolean) {
const value = state ? -1 : 1;
const value = state ? -1 : 0;

if (this.isRange) {
this.thumbFrom.tabindex = value;
Expand Down
Loading
Loading