Explorations in the Virtual DOM: How React.js Impacts Accessibility

By Marcy Sutton / @MarcySutton
Senior Front-End Engineer, aXe-core team
Deque Systems

The JavaScript Hotness

JavaScript frameworks over the years: each one is 'so hot right now'

What made React.js different

  • Virtual DOM
  • Component lifecycle
  • One-way data flow
  • Timing in framework land
  • Developer ergonomics

Accessibility in React Apps

https://reactjs.org/docs/accessibility.html

  • Virtual DOM
  • HTML in JS
  • User-input events
  • Focus management
  • Testing

Virtual DOM

An in-memory representation of the
Document Object Model
(browser render tree)

Virtual DOM diagram

Source

axe-core & Virtual DOM

https://github.com/dequelabs/axe-core/blob/develop/doc/developer-guide.md

  var element = document.body;
  axe.utils.getFlattenedTree(element, shadowId)

  //returns:
  [{
    actualNode: body,
    children: [virtualNodes],
    shadowId: undefined
  }]
					

Component Lyfe

https://reactjs.org/docs/react-component.html
  import React from 'react';
  class AdoptAPetViewer extends React.Component {
    componentDidMount: function() {},
    componentWillUnMount: function() {},
    componentDidUpdate: function() {},
    componentWillReceiveProps: function() {},
    render() {
      return (<div>Your Sweet Accessible App</div>)
    }
  }
  window.addEventListener('DOMContentLoaded', function() {
    React.render(<AdoptAPetViewer />, document.getElementById('root'))
  })
	        	

HTML in JS

https://github.com/davidtheclark/react-aria-menubutton
  import { Wrapper, Button, Menu, MenuItem } from '../../src';

  render() {
    return (
      
    {menuItemElements}
); }

POUR me a coffee ☕️

https://www.w3.org/WAI/WCAG21/Understanding

  • Perceivable
  • Operable
  • Understandable
  • Robust

ARIA Authoring Practices 1.1

User-Input Events in React

  • Virtual DOM
  • Dynamic HTML w/ JS
  • Multiple contexts

That’s a job for: Synthetic Event Delegation

George Castanza has an ah-ha moment

React Synthetic Event documentation

Binding Events in React

 class PizzaButton extends React.Component {
  handleClick(e) {}
  handleKeyDown(e) {}

  render() {
    return (
      <button
        onClick={this.handleClick}
        onKeyDown={this.handleKeyDown}
      >
        Get Pizza
      </button>
    )
  }
 })

 export default PizzaButton
	        	

SyntheticEvent Things to watch out for:

  • SyntheticEvent is a wrapper utility
  • Bind to keyboard-accessible widgets
  • Capturing & non-React events

https://www.youtube.com/watch?v=dRo_egw7tBc

Focus Management

Critical in client-rendered apps

https://www.smashingmagazine.com/2015/05/client-rendered-accessibility
  • Layers/Modals
  • Sidenavs
  • Tabs
  • Menus

aXe React Devtools Demo

Code Example 1

https://facebook.github.io/react/docs/refs-and-the-dom.html
  focus() {
    this.textInput.focus();
  }

  render() {
    return (
      <input
        type="text"
        ref={(input) => { this.textInput = input; }}
      />
    );
  }
}
					

Code Example 2

By Ryan Florence

https://github.com/facebook/react/issues/1791
 this.setState({ gotEmail: true }, () => {
   this.refs.thanks.getDOMNode().focus();
  
   // display a "thank you" for two seconds
   setTimeout(() => {
     var focusHasNotMoved =
       (this.refs.thanks.getDOMNode() ===
        document.activeElement);
     this.setState({ gotEmail: false }, () => {
       if (focusHasNotMoved)
         this.getDOMNode().focus();
     });
   }, 2000);
 });
					

Code Example 3

Attest DevTools Extension
  componentDidUpdate () {
    const ourNode = ReactDOM.findDOMNode(this)
    if (document.activeElement &&
    	ourNode.contains(document.activeElement)) {
      // if the focussed element is a child of our DOM, return
      return
    }
    // focus one of our elements
    ourNode.querySelector('button').focus()
  }
		        

There are multiple
focus management strategies

💁‍♀
  • User input event + focus on refs
  • Assign focus via component state callback
  • Focus on component mount/update*
  • Use context
  • Focus layer system (smarter)

Testing and Accessibility

  • Unit tests
  • Integration tests
  • Tools

React Unit Tests

  • Rendering with props/state
  • Interaction APIs
  • Snapshot testing

Shallow Rendering

https://facebook.github.io/react/docs/test-utils.html

Lets you render a component "one level deep" and test what its render method returns, without worrying about child components, which are not instantiated or rendered. Does not require a DOM.

Shallow Rendering Example

https://github.com/davidtheclark/react-aria-menubutton
 var shallow = require('enzyme').shallow;
 
 // more code

 it('escape key', function() {
  var wrapper = shallow(el(Button, null, 'foo'), shallowOptions);
  wrapper.simulate('keyDown', escapeEvent);

  expect(ambManager.handleMenuKey).toHaveBeenCalledTimes(1);
  expect(ambManager.handleMenuKey.mock.calls[0][0].key).toBe('Escape');
 });
				    

Tests Requiring More than Shallow DOM

  • Computed name and role
  • Color contrast
  • Visible focus state
  • Focus mgmt with refs
  • ARIA support*
light bulb

Mounted Rendering

…attaching code to the DOM


  import {mount} from 'enzyme';
  let wrapper = mount(app, { attachTo: div })
			    

Enzyme axe-core test ~ 1 of 2

http://bit.ly/a11yHelper
import App from '../app/components/App';
import a11yHelper from "./a11yHelper";

describe('Accessibility', function () {
  this.timeout(10000);

  it('Has no errors', function () {
    let config = {
      "rules": {
        "color-contrast": { enabled: false }
      }
    };
    a11yHelper.testEnzymeComponent(<App/>, config, (error, results) => {
      expect(results.violations.length).to.equal(0);
    });
  });
});
		        

Enzyme axe-core test ~ 2 of 2

http://bit.ly/a11yHelper
 a11yHelper.testEnzymeComponent = (app, config, callback) => {
    let div = document.createElement('div');
    document.body.appendChild(div);

    let wrapper = mount(app, { attachTo: div });
    let node = findDOMNode(wrapper.component);

    if (typeof config === 'function') {
      config = {};
    }
    this.runAxe(node, config, callback);

    document.body.removeChild(div);
 }

 a11yHelper.runAxe = (node, config, callback) => {
   var oldNode = global.Node;
   global.Node = node.ownerDocument.defaultView.Node;

   axeCore.run(node, config, (error, results) => {
     global.Node = oldNode;
     callback(results);
   });
 }
		        

Other Unit Test Considerations

  • Test individual units
  • JSDOM platform gaps
  • Events on inaccessible elements

The dreaded documentElement error

Command line Mocha test stack trace saying documentElement exploded!

The Fix

http://bit.ly/jsdom-helper
 import { jsdom } from 'jsdom'
 import 'jsdom-global/register'

 export function jsdomSetup () {
  if (global.document && global.window) {
    global.document = jsdom('<!doctype html><html><body></body></html>')
    global.window = global.document.defaultView
    return
  }

  // Make all window properties available on the mocha global
  Object.keys(global.window)
  .filter(key => !(key in global))
  .forEach(key => {
    global[key] = global.window[key]
  })
 }
		        

Integration Tests

“Frontend bugs have occurred in the boundary between components or layers of our app – for example, between the API and the data layer, the data layer and our components, or between a parent and child component.”

Integration Test Considerations

  • Test components together
  • Slower, more processor heavy
  • Real browser, accurate results
  • Webdriver docs are hard to use

Testing with axe-webdriverjs

http://bit.ly/a11y-demo-app
 const WebDriver = require('selenium-webdriver'),
       AxeBuilder = require('axe-webdriverjs');

 describe('Accessibility', function() {
    this.timeout(10000);
    let driver;

    beforeEach(function(done) {
      driver = new WebDriver.Builder()
        .forBrowser('chrome')
        .build();
      driver
        .get(`http://localhost:${port}`)
        .then(function() {
          done();
        });
    });

    afterEach(function(done) {
      driver.quit().then(function() {
        done();
      });
    });

    it('should find no violations in the modal', function(done) {
      driver
        .findElement(WebDriver.By.css('.signup-btn'))
        .then(function(element) {
          element.sendKeys(WebDriver.Key.ENTER)
      })
      .then(function() {
        new AxeBuilder(driver)
          .analyze(function (results) {
            TestUtils.printViolations(results.violations);
            assert.equal(results.violations.length, 0);
            done();
          });
      });
    });
 });
		        

Accessibility Testing APIs

Wrap It Up

  • A11y + React.js ❤️
  • The basics matter
  • Get skilled at focus management
  • Test, test, test!

https://egghead.io/courses/start-building-accessible-web-applications-today

Questions?

twitter.com/marcysutton

github.com/marcysutton