Crystal Finch is a user experience engineer for productOps and in this blog tackles some of the challenges of making applications accessible to all people, regardless of disability type.

Accessibility is a critical aspect of our development pipeline especially for our clients in education and government. Every member of the team brings talent to the table, but not every member is necessarily an expert in accessibility. In a web application with many moving parts, ensuring new functionality is accessible can be a herculean effort. A module that took hours to comb through for issues could soon have new issues introduced when code is added by well-meaning teammates. If you’re like us, you want to continue to have fun coding without sacrificing quality. You also don’t want to go to sleep at night dreading the thought of your hard work being undone or becoming a tiresome accessibility watchdog.

“In a web application with many moving parts, ensuring new functionality is accessible can be a herculean effort.”

After some hours of brainstorming, we created an approach for guiding the whole team in the right direction. In recent months, we have been building an Angular application that contains hundreds of form fields. To be accessible, all fields must have an associated label. Without them, clearly understanding the form is impossible for users who depend on assistive technology. A very common oversight during development.

Now for the fix! We put together a generic component called input-field. It takes a native form input element, identified by the input-ref directive, that has an id attribute. When you instantiate the component, you pass a string to the labelText attribute. A native label is automagically output with that string and gets associated with the native form input element — the id of the input is used as the for attribute of the label. If any necessary value is missing, an error message is printed out to the console. Issues are caught right away and can be fixed before anything gets committed. Though these errors will get resolved during development, we use a special Logger class that prevents any errors from appearing in a production environment.

The input-field component also provides the option to make the label hidden. This comes in handy when using inputs in parts of the UI that do not allow space for a visible label, such as the inside of table cells. When this option is used, the labelText is applied to the native form input element as an aria-label attribute instead of a native label element.

app-logger.ts

import { environment } from '../environments/environment';

export class Logger {

  static log(msg: string | object): void {
    if (!environment.production) {
      console.log(msg);
    }
  }

  static error(msg: string): void {
    if (!environment.production) {
      console.error(msg);
    }
  }
}

app.component.html

<form>
  <input-field labelText="First Name">
    <input inputRef id="firstName" type="text" required />
  </input-field>
  <button type="submit">Submit</button>
</form>

input-field.component.html

<label *ngIf="showLabel()" [for]="input.element.id" [textContent]="labelText"></label>
<ng-content></ng-content>

input-field.component.ts

import { AfterContentInit, AfterViewInit, Component, ContentChild, Input } from '@angular/core';
import { InputRefDirective } from './input-ref.directive';
import { Logger } from './app-logger';

@Component({
  selector: 'input-field',
  templateUrl: './input-field.component.html',
})
export class InputFieldComponent implements AfterViewInit, AfterContentInit {

  @Input() labelText: string;
  @Input() labelHidden = false;
  @ContentChild(InputRefDirective) input: InputRefDirective;

  constructor() {}

  ngAfterContentInit() {
    if (!this.input) {
      Logger.error('Input required in input-field component. If input is present, add inputRef directive to element.');
    }
  }

  ngAfterViewInit() {
    if (!this.input.element.id) {
      Logger.error('An input with a unique ID is required in input-field component:');
      Logger.log(this.input);
    }

    if (!this.labelText) {
      Logger.error('labelText input required in input-field component:');
      Logger.log(this.input);
    }

    if (this.input && this.labelText && this.labelHidden) {
      this.input.element.setAttribute('aria-label', this.labelText);
    }
  }

  showLabel() {
    return this.labelText && !this.labelHidden;
  }
}

input-ref.directive.ts

import { Directive, ElementRef, HostListener, OnInit } from '@angular/core';
import { NgControl } from '@angular/forms';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';

@Directive({
  selector: '[inputRef]'
})
export class InputRefDirective implements OnInit {

  element: HTMLInputElement;
  focus = false;
  focusSubject = new BehaviorSubject<boolean>(null);
  keyUpSubject = new BehaviorSubject<KeyboardEvent>(null);

  constructor( private el: ElementRef, public control: NgControl ) { }

  ngOnInit() {
    this.element = this.el.nativeElement;
    this.focusSubject.asObservable().subscribe((value) => {
      this.focus = value;
    });
  }

  hasError() {
    return this.control.errors;
  }

  @HostListener('focus')
  handleFocus() {
    this.focusSubject.next(true);
  }

  @HostListener('blur')
  handleBlur() {
    this.focusSubject.next(false);
  }

  @HostListener('keyup', ['$event'])
  handleKeyUp(event: KeyboardEvent) {
    this.keyUpSubject.next(event);
  }
}

This approach takes the guess work out of the equation for everyone touching the application’s form fields. We maintain compliance with accessibility guidelines and give developers the guidance they need when they need it. Of course, this solution applies to only a fraction of the tasks required to build an accessible application, but even this small effort goes a long way in creating a friendly and inclusive experience for the widest possible audience.