Create a JavaScript library. Add keyboard controls and improve accessibility support
Tags: javascript, typescript, webdev, a11y
Published: 2020-07-15 11:04:27

Today I'll continue to improve the library and suggest discussing another important topic - the accessibility and usability of your interface.

{% youtube VFzBbb1DvaM %}

## What is accessibility?

An accessible site is a site, access to the contents of which can be obtained regardless of any violations by the user, and the functioning of which can be carried out by a wide variety of users.

Remember that accessibility is not a tool to make your interface convenient for a certain group of users, it is an opportunity to make your interface convenient for all your users.

As the [web.dev](https://web.dev/what-is-accessibility/#vision) blog writes accessibility concerns can be split into four broad categories:
* Vision
* Motor/dexterity
* Auditory
* Cognitive

If you think that your project does not have users from the described groups, you're very wrong. For example, any user will be grateful if, as an alternative, you give the opportunity to control the interface from the keyboard.

## Keyboard controls

Keyboard control can be convenient not only for users with restrictions since this is a more efficient way to move around the site (if everything is done on the site for this).

**Focus** on an element (for example, a link, button, or input field) shows which element is currently ready for keyboard input. To change the focus on an element you need to use the **TAB** key or the **SHIFT + TAB** key combination.

Just try to do it

{% codepen https://codepen.io/alexandrshy/pen/GRoGPMR %}

Depending on your browser, the interactive elements on which the focus is located will be highlighted. This is very important, without this it'll be impossible to use the keyboard to navigate the site. If you don’t like how it looks by default, you can style it with the **:focus** CSS pseudo-class.

The second important point that you need to pay attention to is the order of your elements when navigating from the keyboard.

If the order of focus seems wrong, you should reorder the elements in the DOM to make it more natural. If you want something to appear earlier on the screen, move it earlier in the DOM.

It'll look like the normal focus order:

{% codepen https://codepen.io/alexandrshy/pen/xxZJEVW %}

And it'll look like an order changed using styles:

{% codepen https://codepen.io/alexandrshy/pen/LYGBRyg %}

Now let's look at this issue when using the library. If we add a list of interactive elements and add a layout of the modal window after this list, the order will correspond to the layout, but not to our ideas about the correct focus. For example, it might look like this:

{% codepen https://codepen.io/alexandrshy/pen/wvMxzPM %}

This is how it looks now.

![Focus for buttons](https://user-images.githubusercontent.com/14329906/87538349-1f993e80-c6ad-11ea-8793-95fa39178a4f.gif)

We have 4 buttons, one of them opens the modal window by clicking, but after opening the modal window, the focus remains on the buttons. It would be much more convenient if the focus automatically moved us to the modal window.

## Implementing autofocus

First of all, for automatic focusing inside the modal window, we need a list of elements that can get the focus state after opening the modal window

```js
export const FOCUSING_ELEMENTS = [
    'a[href]',
    'area[href]',
    'button:not([disabled]):not([aria-hidden])',
    'input:not([disabled]):not([aria-hidden])',
    'select:not([disabled]):not([aria-hidden])',
    'textarea:not([disabled]):not([aria-hidden])',
    '[tabindex]:not([tabindex^="-"])',
];
```

Now we need a method to get all the interactive elements for the active modal window

```js
/**
 * Get a list of node elements that may be in focus
 *
 * @returns {Array<HTMLElement>} list of nodes
 */
getFocusNodesList(): HTMLElement[] {
    if (!this.$modal) return [];
    const nodes = this.$modal.querySelectorAll<HTMLElement>(FOCUSING_ELEMENTS.join(', '));
    return Array.from(nodes);
}
```

The method for autofocus will have the following algorithm: 
1. We get a list of interactive elements if the list is empty (so empty that there is no button to close inside the modal window) we simply interrupt the method;
2. If the list has several interactive elements, we do `.focus()` on the first of an element that isn't a close button;
3. If the modal window has only a close button, then do `.focus()` on this button.

```js
/**
 * Set focus on an element inside a modal
 */
setFocus() {
    const nodesList = this.getFocusNodesList();
    if (!nodesList.length) return;
    const filteredNodesList = nodesList.filter(
        (item) => !item.hasAttribute(this.closeAttribute),
    );
    (filteredNodesList.length ? filteredNodesList[0] : nodesList[0]).focus();
}
```

Since we are trying to change the default logic of the focus operation, we should not do this as a mandatory part of the library, so we'll add a new option `isAssignFocus` which will be responsible for autofocus (with the default value of `true`). Call the "setFocus" function after opening the modal window

```js
preparationOpeningModal(event?: Event) {
    if (this.hasAnimation) {
        ...
        const handler = () => {
            if (this.isAssignFocus) this.setFocus();
            ...
        };
        this.$modal?.addEventListener('animationend', handler);
    } else {
        if (this.isAssignFocus) this.setFocus();
        ...
    }
}
```

If the modal window has an animation when opened, we will focus on the element only after the animation is complete.

That's what we got

{% codepen https://codepen.io/alexandrshy/pen/oNbMWeX %}

![Autofocus inside the modal window](https://dev-to-uploads.s3.amazonaws.com/i/isv8tyw74spgjxl7874o.gif)

## Focus control inside a complex element

For complex elements, you need to work extra to make it easy to control with the keyboard. Part can be done using the only markup for this `tabindex` is suitable. The native interactive element has focus, but `tabindex` makes any UI element available for focus.

There are three types of `tabindex` values:

* A **negative value** (usually `tabindex="-1"`) means that the item is not accessible via sequential keyboard navigation, but can be focused with JavaScript or a visual click
* `tabindex="0"` means that the element must be focused when sequentially navigating the keyboard
* A **positive value** (`tabindex="1"`, `tabindex="2"`, `tabindex="100"`) means that the element must be focused in sequential navigation using the keyboard, with its order determined by the value of the number. This completely contradicts the natural order of focus that we discussed earlier and is the antipattern

## Focus control

Earlier we implemented autofocus, but this showed another problem, after all the interactive elements (or elements with `tabindex="0"`) are over, focusing continues on the following elements outside the modal window. This is usually not a problem if you use a modal window as a hint that doesn't block the main content. But if we use a full-size modal window (also with a scroll lock), this isn't the behavior that we expect to see.

Let's give users the opportunity to choose whether they want to control the focus inside the modal window or not.

First of all, we need to get a list of interactive elements

```js
/**
 * Leaves focus control inside a modal
 *
 * @param {KeyboardEvent} event - Event data
 */
controlFocus(event: KeyboardEvent) {
    const nodesList = this.getFocusNodesList();
    if (!nodesList.length) return;
}
```

After that we filter all the hidden elements and then we determine whether the modal window is the currently active element, if not, we set the focus on the first element in order. If the active element is already in the modal window, we get the index of the active element and, depending on the index and the keys pressed, decide which element will be focused next. Here we have two special cases that we need to handle on our own:
1. If the `SHIFT` key is pressed and we're focused on the first element, then we need to next focus on the last interactive element inside the modal window
2. If the `SHIFT` key isn't pressed and we're focused on the last element, then we need to next focus on the first interactive element inside the modal window

```js
controlFocus(event: KeyboardEvent) {
    ...
    const filteredNodesList = nodesList.filter(({offsetParent}) => offsetParent !== null);
    if (!this.$modal?.contains(document.activeElement)) {
        filteredNodesList[0].focus();
    } else {
        const index = filteredNodesList.indexOf(document.activeElement as HTMLElement);
        const length = filteredNodesList.length;

        if (event.shiftKey && index === 0) {
            filteredNodesList[length - 1].focus();
            event.preventDefault();
        }

        if (!event.shiftKey && length && index === length - 1) {
            filteredNodesList[0].focus();
            event.preventDefault();
        }
    }
}
```

Now add a handler for clicking on `TAB` and individual options in the configuration file

```js
/**
 * Modal constructor
 *
 * @param {ConfigType} param - Config
 */
constructor({
    ...
    isAssignFocus = true,
    isFocusInside = true,
}: ConfigType) {
    ...
    this.isAssignFocus = isAssignFocus;
    this.isFocusInside = isFocusInside;

    this.onKeydown = this.onKeydown.bind(this);
}

/**
 * Event keydown handler
 *
 * @param {KeyboardEvent} event - Event data
 */
onKeydown(event: KeyboardEvent) {
    if (event.key === KEY.TAB) this.controlFocus(event);
}

/**
 * Add event listeners for an open modal
 */
addEventListeners() {
    ...
    if (this.isFocusInside) document.addEventListener('keydown', this.onKeydown);
}

/**
 * Remove event listener for an open modal
 */
removeEventListeners() {
    ...
    if (this.isFocusInside) document.removeEventListener('keydown', this.onKeydown);
}
```

Our current result:

{% codepen https://codepen.io/alexandrshy/pen/RwrYVzK %}

![Focus for buttons](https://dev-to-uploads.s3.amazonaws.com/i/2vz90fb0ns8shiayx4fi.gif)

## Semantic information

Sometimes semantic markup is not enough to fully convey the information that your content carries. In such cases, you can use [ARIA](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) attributes. `ARIA` is a set of special attributes that can add a description to your markup. This is a separate large section with its good practices and recommendations. For the library, we'll consider only a part (and perhaps we'll return to this topic in the future when we write modal window templates).

Adding [aria-hidden="true"](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-hidden_attribute) to an element removes this element and all its children from the accessibility tree. This can improve the assistive technology user experience. In our case, this attribute should hide the modal window at the moment when it is not active and show when the `open` method is called. Add `aria-hidden="true"` to our markup and implement attribute control in libraries.

```js
/**
 * Set value for aria-hidden
 *
 * @param {boolean} value - aria-hidden value
 */
setAriaHidden(value: boolean) {
    this.$modal?.setAttribute('aria-hidden', String(value));
}

open(event?: Event) {
    ...
    this.setAriaHidden(false);
    ...
}

close(event?: Event) {
    ...
    this.setAriaHidden(true);
    ...
}
```

The modal window is already a familiar element of any site, but the browser will not be able to understand that part of the content that appears is a modal window, but we can suggest using [role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles). `role="dialog"` is great for our markup. `role="dialog"` is used to mark up an HTML-based application dialog box or a window that separates the content or interface from the rest of the web application or page. Dialogs are usually placed on top of the rest of the page using overlay. As you can see, the role simply tells the browser what kind of component is in front of it.

That's all for now, this time we touched on a very interesting and important topic. The library itself you can find on GitHub will be glad to your ⭐

{% github Alexandrshy/keukenhof %}

___

This is the final part of writing the basic functionality (this doesn't mean that I'll no longer work on the side project, there are still a lot of steps that I want to implement, for example, write documentation, write a project page, publish my project on Product Hunt and much more). Now I can use the library to create templates because I miss `HTML`/`CSS` 😄 Thank you all for your interest in the article. See you soon 👋