Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Kyle Dodge
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Kyle Dodge@kldodge )

Giving (click) Anchor Links Tab-Access Using A Directive In Angular 7.2.15

By Ben Nadel on

Ever since reading Accessibility For Everyone by Laura Kalbag, I've become much more aware of the accessibility failings in my own user interface (UI) architecture. One of the first things that jumped out at me was the fact that anchor links <a> that use the (click) directive in Angular (and lack an href attribute) can't be accessed using the Tab key. Furthermore, they can't be invoked, ie "clicked", using the Enter or Spacebar keys. As such, I wanted to see if I could use a simple Directive in Angular 7.2.15 to unilaterally expose anchor links to Keyboard controls.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

Directives are such a subtle part of the Angular experience, it's easy to not even be cognizant of just how powerful they are. But, they are truly stunning! Directives are, in my opinion, one of the features that makes Angular a clear leader in the JavaScript Framework landscape. This becomes more obvious when you see how simple it is to retrofit your Angular application with Tab-accessible links without having to change any of your HTML markup.

To demonstrate, let's look at a simple App component. This App component has two sets of anchor links - one set that will be retrofitted with Tab-accessibility; and one set that will use the [x-no-tabbing] attribute to explicitly skip the retrofitting:

// Import the core angular services.
import { Component } from "@angular/core";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@Component({
	selector: "my-app",
	styleUrls: [ "./app.component.less" ],
	template:
	`
		<p>
			<a href="#">Native Href link</a> (experiment control)
		</p>

		<p class="actions">
			<a (click)="logClick( 'Item one' )">Item One</a>
			<a (click)="logClick( 'Item two' )">Item Two</a>
			<a (click)="logClick( 'Item three' )">Item Three</a>
			<a (click)="logClick( 'Item four' )">Item Four</a>
		</p>

		<!-- NOTE: The [x-no-tabbing] attribute will cause demo Directive to be omitted. -->
		<p class="actions">
			<a x-no-tabbing (click)="logClick( 'Item one' )">Item One</a>
			<a x-no-tabbing (click)="logClick( 'Item two' )">Item Two</a>
			<a x-no-tabbing (click)="logClick( 'Item three' )">Item Three</a>
			<a x-no-tabbing (click)="logClick( 'Item four' )">Item Four</a>
		</p>
	`
})
export class AppComponent {

	// I log the click event.
	public logClick( value: string ) : void {

		console.group( "Clicked Anchor" );
		console.log( value );
		console.groupEnd();

	}

}

As you can see, both sets of links are using the (click) Directive to trigger an action in the component - in this case, it's just logging the click event to the console. Notice, however, that there is nothing special about these links from a markup stand-point. They look just like any (click)-based links that you would create in your Angular application.

If you were to run this in the Browser and attempt to Tab-through the links, what you'd see is that the first set of links can be accessed (and invoked with the Enter and Spacebar keys); and, that the second set of links is completely skipped:

Demonstrationg that anchor links can be accessed by Tab when using the Angular Directive.

In order to expose the (click) links to the keyboard-based navigation and invocation, I added a small Directive that binds to the a element and augments the runtime functionality of the link:

// Import the core angular services.
import { Directive } from "@angular/core";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@Directive({
	selector: "a[click]:not([href]):not([role]):not([tabindex]):not([x-no-tabbing])",
	host: {
		// Adding [tabindex] allows tab-based access to this element. The "0" indicates
		// that the tabbing order should follow the native DOM element ordering.
		"tabindex": "0",
		// Adding [role] tells screen readers that this "link" is really a "button",
		// in that it triggers an action, but doesn't navigate to a new resource.
		"role": "button",
		// Adding (keydown) allows us to translate the Enter and Spacebar keys into a
		// "click" event. This is the native behavior of a Button; so, we are trying to
		// mimic that behavior for our "link button".
		// --
		// NOTE: This is perhaps a good "code smell" that we should be using a Button
		// instead of a link for this host element.
		"(keydown.enter)": "$event.preventDefault() ; $event.target.click() ;",
		"(keydown.space)": "$event.preventDefault() ; $event.target.click() ;"
	}
})
export class TabbingClickDirective {
	// ....
}

This Angular Directive is so simple, it doesn't even have any internal logic. It's just a collection of host bindings that augment the <a> instance. In this case, I'm adding the following:

  • [tabindex] - By adding the tabindex attribute, it ensures that the Element is accessible by Tab-based navigation. The 0 value tells the browser to use the natural ordering of the DOM (Document Object Model) when determining the order in which to make the Element available.

  • [role] - By adding the WAI-ARIA (Web Accessibility Initiative - Accessible Rich Internet Applications) role of button, we are telling screen readers that this link doesn't navigate to a new resource; but is, instead, used to trigger a discrete action within the application.

  • (keydown) - By adding the keydown bindings, we're translating Enter and Spacebar key-events into triggers of the link. This allows the user to both tab-to and invoke the link without having to use the mouse.

ASIDE: All of this behavior is implicitly provided by the Browser if we use a button element instead of an a element. But, that's a conversation for a follow-up post.

By adding this directive to a "Shared Module" (a common Angular practice), the entire Angular application will immediately have Tab-accessible anchor links. Of course, this may not always be the desired effect. So, I tried to use a selector that only applies the Directive if none of the augmented attributes already exist. Also, you can see that it uses :not([x-no-tabbing]), which allows for an explicit escape hatch.

Can we just stop for a second and think about how cool this is? I just used an Angular Directive to seamlessly retrofit my application with accessible links. No post-render enhancement; no global key-event binding; no "hacking"; just native Angular mechanics.

One of the most compelling features of the Angular framework is just how easy it is to extend. Extending DOM events; extending DOM Elements; adding custom Elements (ie, Components). It all just happens so seamlessly. In this case, you can see how easy it is to upgrade the link elements in Angular 7.2.15 to be Tab-accessible.

Tab Accessibility in the Firefox Browser on MacOS

Apparently - as I learned while authoring this code - the Firefox Browser doesn't support tab-based navigation on all "normal" elements by default. Instead, you have to go into the Keyboard Preferences and explicitly enable keyboard shortcuts for "All controls":

Enabling Tab access for the Firefox browser in the Keyboard preferences on MacOS.

Then, once this is enabled, you actually have to close your current Browser Tab and re-open it for the new settings to take effect.



Reader Comments

@All,

After posting this, the next question was obvious, "Should I even be using links for some of this stuff?". And, according to Marcy Sutton, the button element is more semantic and accessible for much of these discrete actions. As such, I wanted to look at styling button and a links in an Angular app:

https://www.bennadel.com/blog/3634-accessibility-and-styled-anchor-links-vs-styled-buttons-in-angular-7-2-15.htm

... what you'll see is that styling them is equally easy in both; and, the button adds much more access right out of the box!

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.