State machine advent: Let the machine handle accessibility for you (18/24)
Tags: typescript, javascript, react, a11y
Published: 2019-12-19 01:07:04


When designing and building apps, we need to ensure that they are accessible to all people. Among [other things](https://a11yproject.com/checklist/), this includes managing focus and the tab order deliberately.

To give a small example of a login form where we'd want to manage focus: 

- No input field is shown initially.
- Upon clicking a button saying "click here to login": 
    - Hide the button
    - Show an input field to enter the email
    - Focus the input field so that one can type their email immediately

Solving one of the most common a11y problems in our apps like focus management can be very complicated and error-prone even with straightforward requirements as the ones above.
In React for example, we usually create a ref and manage local state that tracks whether we have already set the focus or not. We then perform some conditional logic on the variable inside an effect where we ultimately perform the `ref.currrent.focus()` action to set the focus onto the input field.  

Sounds complicated, let's put the words into code. We'll start off by adding some types for a little state machine.
 
```typescript
interface LoginStateSchema {
  states: {
    /**
     * In the `idle` state, we'll just show the button
     */
    idle: {};

    /**
     * In the `canLogin` state, we want to show the email input field
     */
    canLogin: {};
  };
}

type LoginEvent = 
 | { type: 'ENABLE_LOGIN' }
 | { type: 'SET_EMAIL', email: string };

interface LoginContext {
  email: string;
}
```

After having written the types, let's go ahead and implement our state machine.  


```typescript
import { Machine, assign } from 'xstate';

const loginOrIdleMachine = Machine<LoginContext, LoginStateSchema, LoginEvent>({
  id: 'loginOrIdle',
  initial: 'idle',
  context: {
    email: '',
  },
  states: {
    idle: {
      on: {
        'ENABLE_LOGIN': {
          target: 'canLogin',
        },        
      },
    },
    canLogin: {
      on: {
        'SET_EMAIL': {
          actions: assign({
            email: (context, event) => event.email,
          })
        }
      }  
    },
  }  
})
```

With everything we have learned up until this point, our code to manage the focus of the input field is still pretty verbose: 

```tsx
import React, { Fragment, useRef, useLayouteffect, useState } from 'react';
import { useMachine } from '@xstate/react';

const Login = () => {
  const inputRef = useRef<HTMLInputElement>(null);
  const [state, send] = useMachine(loginOrIdleMachine);
  const [hasManagedFocus, setHasManagedFocus] = useState<boolean>(false);

  useLayoutEffect(() => {
    if (state.matches('canLogin') && !hasManagedFocus){
      inputRef?.current?.focus();
      setHasManagedFocus(true);
    }
  }, [state, setHasManagedFocus, hasManagedFocus, inputRef])

  return (
    <Fragment>
      {state.matches('idle') && 
        (
          <button onClick={() => void send({type: 'ENABLE_LOGIN'}) }>
            click here to login
          </button>
        )
      }

      <input 
        onChange={e => void send({type: 'SET_EMAIL', email: e.target.value})} 
        hidden={!state.matches('canLogin')}
        placeholder="Enter email"
        value={state.context.email}
        ref={inputRef}
      />
    </Fragment>
  )
}
```

Not only is the code verbose, if we introduced a state transition from the `canLogin` state towards the `idle` state again (e.g a button saying "login later" underneath the input field), the requirements would no longer be fulfilled and we'd also have to call `setHasManagedFocus(false)` conditionally. The reason why we need to reset the `hasManagedFocus` variable is because we need to ensure that when transitioning from `idle` => `canLogin` => `idle` => `canLogin` state, the input field of the second `canLogin` state transition will be focused as well. I'm sure it is possible to create fully accessible applications by managing local variables, but it can get out of hand really quickly.

Let's quickly determine what can happen if this particular part of our code gets out of hand. Once our code gets too complex, we tend to ship less. Introducing bugs or being too afraid to touch the a11y logic of our app can literally result in us denying people access to our app or service. Needless to say, we have to get this right! 

We can simplify our code by reframing the question. Ask: "On which state transition should we perform the action to steal focus?" instead of asking in which states we should and shouldn't steal focus. Sure, when modeling our state machine, we should also think about the latter question but the worry is purely solved within the machine. As a consequence, it removes conditional logic from our code and frees our minds from a lot of cognitive overhead.

Now that we know what kind of question to ask, let's learn about one particular primitive in XState that could help us find a good answer. Each state node in XState has an `entry` property. We can call actions by their name as a value of the property and they'll be executed every time the invoked machine enters (or reenters) the state node.
To conclude our quest for the best place to put our focus action: We want to focus the input field right after entering the `canLogin` state.

```javascript
canLogin: {
  // focusInput action will be called whenever the machine enters the state node 
  entry: 'focusInput',
  on: {
    'SET_EMAIL': {
      actions: assign({
        email: (context, event) => event.email,
      })
    }
  }  
},
```

Pretty rad but how can our machine define the `focusInput` action when it doesn't know anything about the input element or the DOM to begin with?
The machine doesn't define the action. Instead, the action will be defined within our React component and passed into the machine as a configuration. 

```typescript
const [state, send] = useMachine(loginOrIdleMachine.withConfig({
  actions: {
    'focusInput': () => void inputRef?.current?.focus(),
  }
}));
```

That's right, components can pass actions and guards inside a configuration object (`withConfig`) into the machine. We can then call the guard or action by their distinct name (e.g `entry: 'focusInput'`); hence, handle focus management within the state machine. 🎉🥳

Once we put everything together, our code could look like this:

```tsx
import React, { useRef, Fragment } from 'react';
import { useMachine } from '@xstate/react';
import { Machine, assign } from 'xstate';

/** Type declarations  */
interface LoginStateSchema {
  states: {
    idle: {};
    canLogin: {};
  };
}

type LoginEvent =
 | { type: 'ENABLE_LOGIN' }
 | { type: 'SET_EMAIL'; email: string };

interface LoginContext {
  email: string;
}

/** Machine implementation  */
const loginOrIdleMachine = Machine<LoginContext, LoginStateSchema, LoginEvent>({
  id: 'loginOrIdle',
  initial: 'idle',
  context: {
    email: '',
  },
  states: {
    idle: {
      on: {
        'ENABLE_LOGIN': {
          target: 'canLogin',
        },
      },
    },
    canLogin: {
      entry: 'focusInput',
      on: {
        'SET_EMAIL': {
          actions: assign({
            email: (context, event) => event.email,
          }),
        },
      },
    },
  },
});


/** Component that invokes the machine  */
const Login = () => {
  const inputRef = useRef<HTMLInputElement>(null);
  const [state, send] = useMachine(loginOrIdleMachine.withConfig({
    actions: {
      'focusInput': () => void inputRef?.current?.focus(),
    },
  }));

  return (
    <Fragment>
      {state.matches('idle') &&
        (
          <button onClick={() => void send({type: 'ENABLE_LOGIN'}) }>
            click here to log in
          </button>
        )
      }

      <input
        onChange={e => void send({type: 'SET_EMAIL', email: e.target.value})}
        hidden={!state.matches('canLogin')}
        placeholder="Enter email"
        value={state.context.email}
        ref={inputRef}
      />
    </Fragment>
  );
};
```

Awesome! We have eliminated most of the local state (except the ref which we always need for focusing) and have refactored the React effect to an action that is called by our machine. Even if we were to introduce some more states, or a button + event to transition back to the initial `idle` state as described above, the input field will always be focused when our machine enters the `canLogin` state.

I hope this post gave you some insights on how to pass actions to a machine and also showcased the accessibility benefits by doing so.

### About this series
Throughout the first 24 days of December, I'll publish a small blog post each day teaching you about the ins and outs of state machines and statecharts.

The first couple of days will be spent on the fundamentals before we'll progress to more advanced concepts.