Skip to content

Commit

Permalink
feat(progress-bar): set aria attributes and allow custom ranges (#1205)
Browse files Browse the repository at this point in the history
* feat(progress-bar): add a11y attributes

* feat(progress-bar): enable custom ranges

* feat(progress-indicator): set aria values for progressbar properly

* fix: simplify inputs to use native aria attributes

* fix: fix a11y test

* fix: restore aria inputs

* fix: simplify input attributes

* fix: simplify input parameter

* Update projects/ng-aquila/documentation/examples/progressbar/progressbar-custom-range/progressbar-custom-range-example.ts

Co-authored-by: Philipp Fahrenschon <[email protected]>

* fix: simplify input parameters

* fix: simplify input parameters

* fix: simplify input parameters

* fix: simplify input parameters

* docs: add documentation

* docs: fix aria attributes names

---------

Co-authored-by: [email protected] <[email protected]>
Co-authored-by: Philipp Fahrenschon <[email protected]>
  • Loading branch information
3 people authored and GitHub Enterprise committed Jul 15, 2024
1 parent edb6909 commit e7fa382
Show file tree
Hide file tree
Showing 16 changed files with 139 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ export class ModalUnsavedExampleComponent {
}

onSubmit(): void {
console.log('form submitted', this.formGroup.get('text')?.value);
this.formGroup.reset();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<nx-single-stepper currentStepLabel="Left label" nextLabel="Right label">
<nx-single-stepper
currentStepLabel="Left label"
nextLabel="Right label"
[progressbarAriaLabel]="'Progress towards your new coverage'"
>
@for (step of testStepsData; track step) {
<nx-step [label]="step">
<div class="nx-margin-y-s">content for {{step}}</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<nx-single-stepper currentStepLabel="Step">
<nx-single-stepper
currentStepLabel="Step"
[progressbarAriaLabel]="'Progress to file your claim'"
>
<nx-label>My Title</nx-label>
@for (step of testStepsData; track step) {
<nx-step [label]="step">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<nx-single-stepper currentStepLabel="Step">
<nx-single-stepper
currentStepLabel="Step"
[progressbarAriaLabel]="'Progress to file your claim'"
>
<nx-step label="Address">
<div class="nx-margin-top-s">
<p>This is the first step</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<nx-progressbar
ariaLabel="Some aria label"
aria-valuetext="Some value description"
[value]="value"
[min]="min"
[max]="max"
></nx-progressbar>
value: {{value}} | completed: {{getCompletedPercentage()}}%
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Component, OnInit } from '@angular/core';

import { NxProgressbarComponent } from '@aposin/ng-aquila/progressbar';

/**
* @title Progress Bar Example
*/
@Component({
selector: 'progressbar-custom-range-example',
templateUrl: './progressbar-custom-range-example.html',
styleUrls: ['./progressbar-custom-range-example.css'],
imports: [NxProgressbarComponent],
standalone: true,
})
export class ProgressbarCustomRangeExample implements OnInit {
value = 15;
min = 5;
max = 80;

ngOnInit(): void {
(async () => {
while (this.value !== 80) {
await new Promise(resolve => setTimeout(resolve, 2000));

this.value = (this.value + 10) % this.max;
}
})();
}

getCompletedPercentage() {
return Math.round(
((this.value - this.min) / (this.max - this.min)) * 100,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { NxProgressbarModule } from '@aposin/ng-aquila/progressbar';

import { ProgressbarExampleComponent } from './progressbar/progressbar-example';
import { ProgressbarBasicExampleComponent } from './progressbar-basic/progressbar-basic-example';
import { ProgressbarCustomRangeExample } from './progressbar-custom-range/progressbar-custom-range-example';

const EXAMPLES = [
ProgressbarExampleComponent,
ProgressbarBasicExampleComponent,
ProgressbarCustomRangeExample,
];

@NgModule({
Expand All @@ -18,6 +20,7 @@ export class ProgressbarExamplesModule {
return {
progressbar: ProgressbarExampleComponent,
'progressbar-basic': ProgressbarBasicExampleComponent,
'progressbar-custom-range': ProgressbarCustomRangeExample,
};
}
}
5 changes: 5 additions & 0 deletions projects/ng-aquila/src/progress-stepper/progress-stepper.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,8 @@ There are two possible approaches. One is using a single form for the indicator,
You can force the user to complete a form before continuing. To make a indicator aware of it you have to enable linear progress with the property `linear` on any indicator and you have to assign the involved form group to the step through the `[stepControl]` Input.

<!-- example(progress-stepper-progress) -->

### Accessibility

In case of the Single Indicator also make sure to set the appropriate `progressbarAriaLabel` or `progressbarAriaLabeledBy` for your use case.
This will set the associated aria attributes on the nested <a href="./documentation/progressbar/overview">NxProgressbarComponent</a>
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<div class="nx-subprogress__label">{{ step.stepLabel || step.label }}</div>
@if (selectedIndex === i) {
<div class="nx-subprogress__progress">
<nx-progressbar [value]="progress"></nx-progressbar>
<nx-progressbar [value]="progress" [ariaLabel]="progressbarAriaLabel" [ariaLabelledBy]="progressbarAriaLabeledBy"></nx-progressbar>
</div>
}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import { NxProgressStepperDirective } from '../progress-stepper.component';
imports: [NxProgressbarModule, NgTemplateOutlet],
})
export class NxProgressStepperComponent extends NxProgressStepperDirective {
/** Overrides the `aria-label` of the nx-progressbar. Defaults to "Progress" */
@Input() progressbarAriaLabel: string | undefined = 'Progress';

/** Sets the `aria-labelledby` of the nx-progressbar */
@Input() progressbarAriaLabeledBy: string | undefined;

/** Sets the current value/progress of the progress bar. */
@Input() set progress(value: number) {
this._progress = clamp(value || 0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<ng-content select="nx-label"></ng-content>
</span>

<nx-progressbar [value]="progress"></nx-progressbar>
<nx-progressbar [value]="progress" [ariaLabel]="progressbarAriaLabel" [ariaLabelledBy]="progressbarAriaLabeledBy"></nx-progressbar>

<div class="nx-single-step__infos">
<div class="nx-single-step__current">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ export class NxSingleStepperComponent extends NxProgressStepperDirective {
}
private _rightLabel = 'Next step:';

/** Overrides the `aria-label` of the nx-progressbar. Defaults to "Progress" */
@Input() progressbarAriaLabel: string | undefined = 'Progress';

/** Sets the `aria-labelledby` of the nx-progressbar */
@Input('progressbarAriaLabeledBy') progressbarAriaLabeledBy: string | undefined;

/** @docs-private */
get progress() {
return (this.selectedIndex + 1) / this.count;
Expand Down
31 changes: 30 additions & 1 deletion projects/ng-aquila/src/progressbar/progressbar.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Component, Directive, ElementRef, Type, ViewChild } from '@angular/core';
import { Component, DebugElement, Directive, ElementRef, Type, ViewChild } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';

import { NxProgressbarComponent } from './progressbar.component';
import { NxProgressbarModule } from './progressbar.module';
Expand All @@ -16,13 +17,15 @@ describe('NxProgressbarComponent', () => {
let testInstance: ProgressBarTest;
let componentInstance: NxProgressbarComponent;
let componentInstanceRef: ElementRef;
let barElement: DebugElement;

function createTestComponent(component: Type<ProgressBarTest>) {
fixture = TestBed.createComponent(component);
fixture.detectChanges();
testInstance = fixture.componentInstance;
componentInstance = testInstance.componentInstance;
componentInstanceRef = testInstance.componentInstanceRef;
barElement = fixture.debugElement.query(By.css('nx-progressbar'));
}

beforeEach(waitForAsync(() => {
Expand All @@ -39,12 +42,32 @@ describe('NxProgressbarComponent', () => {
it('value should default to 0', fakeAsync(() => {
createTestComponent(ProgressBarBasicComponent);
expect(componentInstance.value).toBe(0);

expect(barElement.nativeElement.getAttribute('aria-valuenow')).toBe('0');
expect(barElement.nativeElement.getAttribute('aria-valuemin')).toBe('0');
expect(barElement.nativeElement.getAttribute('aria-valuemax')).toBe('1');
}));

it('value should reflect binding', fakeAsync(() => {
createTestComponent(ProgressBarValueComponent);
expect(componentInstance.value).toBe(0.5);
expect(barElement.nativeElement.getAttribute('aria-valuenow')).toBe('0.5');
expect(barElement.nativeElement.getAttribute('aria-valuemin')).toBe('0');
expect(barElement.nativeElement.getAttribute('aria-valuemax')).toBe('1');
}));

it('value should reflect custom range', fakeAsync(() => {
createTestComponent(ProgressBarCustomRangeComponent);
expect(componentInstance.value).toBe(15);
expect(barElement.nativeElement.getAttribute('aria-valuenow')).toBe('15');
expect(barElement.nativeElement.getAttribute('aria-valuemin')).toBe('12');
expect(barElement.nativeElement.getAttribute('aria-valuemax')).toBe('33');
}));

it('has no accessibility violations', async () => {
createTestComponent(ProgressBarBasicComponent);
await expectAsync(fixture.nativeElement).toBeAccessible();
});
});

@Component({
Expand All @@ -60,3 +83,9 @@ class ProgressBarBasicComponent extends ProgressBarTest {}
imports: [NxProgressbarModule, FormsModule],
})
class ProgressBarValueComponent extends ProgressBarTest {}
@Component({
template: `<nx-progressbar value="15" min="12" max="33"></nx-progressbar>`,
standalone: true,
imports: [NxProgressbarModule, FormsModule],
})
class ProgressBarCustomRangeComponent extends ProgressBarTest {}
36 changes: 20 additions & 16 deletions projects/ng-aquila/src/progressbar/progressbar.component.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { NumberInput } from '@angular/cdk/coercion';
import { NgStyle } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { clamp } from '@aposin/ng-aquila/utils';

let progressbarId = 0;
import { ChangeDetectionStrategy, Component, Input, numberAttribute } from '@angular/core';

@Component({
selector: 'nx-progressbar',
Expand All @@ -12,25 +8,33 @@ let progressbarId = 0;
styleUrls: ['./progressbar.component.scss'],
host: {
'[attr.aria-valuenow]': 'value',
'[attr.role]': '"progressbar"',
'[attr.aria-valuemax]': 'this.max',
'[attr.aria-valuemin]': 'this.min',
'[attr.aria-label]': 'this.ariaLabel',
'[attr.aria-labelledby]': 'this.ariaLabelledBy',
},
standalone: true,
imports: [NgStyle],
})
export class NxProgressbarComponent {
/** @docs-private */
progressbarId = `nx-progress-bar-${progressbarId++}`;
/** Overrides the `aria-label` of the nx-progressbar. Defaults to "Progress" */
@Input() ariaLabel: string | undefined = 'Progress';

/** Sets the value of the progress bar. Defaults to zero. Mirrored to aria-value now. */
@Input() set value(value: NumberInput) {
this._value = clamp((value as any) || 0); // TODO properly coerce input value
}
get value(): number {
return this._value;
}
private _value = 0;
/** Sets the `aria-labelledby` of the nx-progressbar */
@Input() ariaLabelledBy: string | undefined;

/** Sets the value of the progress bar. Defaults to zero. Mirrored to aria-valuenow. */
@Input({ transform: numberAttribute }) value = 0;

/** The minimum value of the progress bar. Used for percentage calculation and mirrored to `aria-valuemin`. Defaults to 0 */
@Input({ transform: numberAttribute }) min = 0;

/** The maximum value of the progress bar. Used for percentage calculation and mirrored to `aria-valuemax`. Defaults to 1 */
@Input({ transform: numberAttribute }) max = 1;

_primaryTransform() {
const scale = this.value;
const scale = (this.value - this.min) / (this.max - this.min);
return { transform: `scaleX(${scale})` };
}
}
11 changes: 11 additions & 0 deletions projects/ng-aquila/src/progressbar/progressbar.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,14 @@ The progress bar module provides a simple horizontal bar to show progress.
Bind a variable to `value` input to set the progress bar value.

<!-- example(progressbar-basic) -->

Use custom ranges with `min` and `max`. When using those, `value` has to be `min <= value <= max` to have a meaningful progress bar.

<!-- example(progressbar-custom-range) -->

### Accessibility

For more verbose value texts you can use `aria-valuetext`.

If the progress changes dynamically and a `aria-label` with additional `aria-description` is set, some browser <-> screen reader combinations will not announce the full information.
Try to keep your descriptions and labels short to avoid that or try to make use of `aria-valuetext`. `aria-valuetext` was more reliable in our testing for those cases.

0 comments on commit e7fa382

Please sign in to comment.