Skip to content

Commit

Permalink
Merge pull request #14793 from IgniteUI/ganastasov/fix-14262-master
Browse files Browse the repository at this point in the history
fix(slider): improve accessibility by correcting ARIA attributes and tab index - master
  • Loading branch information
kacheshmarova authored Oct 22, 2024
2 parents 1ddfd26 + 9953c57 commit f3b10d3
Show file tree
Hide file tree
Showing 4 changed files with 272 additions and 63 deletions.
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

0 comments on commit f3b10d3

Please sign in to comment.