Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accessible Routing #433

Closed
wants to merge 11 commits into from
126 changes: 97 additions & 29 deletions text/0000-a11y-routing.md
@@ -1,4 +1,4 @@
- Start Date: (fill me in with today's date, YYYY-MM-DD)
- Start Date: (YYYY-MM-DD, updated on 2019-05-13)
- RFC PR: (leave this empty)
- Ember Issue: (leave this empty)

Expand All @@ -22,66 +22,134 @@ One of the challenges is that screen reader technology is all closed-source exce

Copy link

@Robdel12 Robdel12 Jan 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've long had a dream to take NVDA (since it's OSS) and turn make a "headless" screen reader version of it. Just like we have a headless Chrome we push around in tests, I'd like a headless AT to send commands to and asset the right thing has happened. Like "press enter on this link, now assert the spoken output of the AT is correct & it focuses the correct element in the DOM"

It's a neat idea, but I have no idea if its even possible.

## Detailed design
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This design should be explicit. As it stands this is not a concrete proposal; it cannot be implemented using the RFC as a guide.


After reviewing possible approaches to this problem, it seems like the very first step should be an implementation that, at the very _least_, returns Ember back to the starting line. We could then build on that by providing a more machine-intelligent iteration for an upscaled experience. This iterative approach would give us a better way to tackle the more difficult edge-case scenarios.
This RFC proposes two different solutions for us to consider. The solution that is determined to be the most appropriate for our use will be made into an officially supported addon, and be included in the Ember blueprints, so it will be part of every new application by default.

This RFC proposes that we could either implement a two-phase approach (outlined below) OR decide to move directly to the approach outlined in phase two.
### Solution #1 - Navigation Message

### Phase One - Return to the starting line
> "When you learn how to use a screen reader, the first thing you learn how to do, is stop it talking." - a screen reader user.

This solution proposes the simplest possible solution- implement in Ember what already exists in other traditional websites:
1. When a browser loads a website, the screen reader starts to read the content of that page
2. When the user navigates to a new page in that website, the screen reader starts to read the content of that page
3. (repeat)
One potential solution is that we don't overwhelm the screen reader user with the contents of the page, but rather inform them that they have successfully transitioned to their desired route. In doing so, we will also reset the focus for them.

Perhaps we could achieve this by adding a function to set focus on the body element when a route transition has occurred.
- A component is created that provides a screen-reader only message, informing the user that "Page transition is complete. You may now navigate as you wish." The text of this component could be internationalized.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that I've experimented with is an off screen "page change" log inside of an aria-live component. Every element in that log would provide information about what is happening, and the ability to jump to the section flagged by it. This has numerous problems:

  • It's a new paradigm.
  • It can't be done in a fully-automated way (the description for each page change area must be manually specified).
  • It doesn't support all AT use cases.

- Focus is reset to the top left of the page
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rest of your description doesn't specify position by location, but instead by DOM hierarchy. This one slipped through.


In order to not break similar solutions that may already exists in other apps or addons, I propose that the solution be implemented as an optional feature/with a flag. The default could be set to on, and if folks already have a solution in their addons/apps, they could then opt-out of this feature.
The component template:

### Phase Two - Intelligent content focus
```hbs
<div tabindex="-1" class="ember-sr-only" id="nav-message">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • How should this handle partial renders? /foo/loading => /foo/bar/loading => /foo/bar/baz
  • How do you prevent this from being read at other times?
    • If you remove it, how do you prevent this from disappearing before AT has a chance to communicate it to the user?
    • If left in the page, stale information in this component could in theory be read during a transition to something new (especially if a large portion of content gets popped out of the page). This could make it seem as if you have landed on the previous state while the next state is still transitioning in.
  • How do you provide a human-understandable page name?
    • If it requires configuration, how do you ensure that users properly configure it?
  • Ember doesn't support localization out of the box, how do you localize the message?

Navigation to {{this.currentURL}} is complete. You may now navigate as you wish.
</div>
```

It would be even better to have a more intelligent solution, since there are several ways new page content could be introduced in an Ember application.
The component js:

[Ember-self-focused](https://github.com/linkedin/self-focused/tree/master/packages/ember-self-focused) and [ember-a11y](https://github.com/ember-a11y/ember-a11y) are both examples of community-produced addons that attempt to provide a contextual focus, based on new page content.
```js
import Component from '@ember/component';
import layout from '../templates/components/navigation-narrator';
import { inject as service } from '@ember/service';
import { schedule } from '@ember/runloop';

From the [Ember-self-focused](https://github.com/linkedin/self-focused/tree/master/packages/ember-self-focused) readme:
> When UI transitions happen in a SPA (or in any dynamic web application), there is visual feedback; however, for users of screen reading software, there is no spoken feedback by default. Traditionally, screen reading software automatically reads out the content of a web page during page load or page refresh. In single page applications, the UI transitions happen by rewriting the current page, rather than loading entirely new pages from a server; this makes it difficult for screen reading software users to be aware of UI changes (e.g., clicking on the navigation bar to load a new route).
export default Component.extend({
layout,
tagName: '',
router: service(),

> If the corresponding HTML node of the dynamic content can be focused programmatically, screen reading software will start speaking the textual content of that node. Focusing the corresponding HTML node of the dynamic content can be considered guided focus management. Not only will it facilitate the announcement of the textual content of the focused HTML node, but it will also serve as a guided context switch. Any subsequent “tab” key press will focus the next focusable element within/after this context.
init() {
this._super();

The tedious, error-prone task of keeping track of HTML nodes that can/could/would/should have focus is something that Ember could do for you.
this.router.on('routeDidChange', () => {
// we need to put this inside of something async so we can make sure it really happens **after everything else**
schedule('afterRender', this, function() {
document.body.querySelector('#ember-a11y-refocus-nav-message').focus();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Can we make this use a reference from inside of Ember?
  • How does this support Ember being used in a place where it is not in control of the entire application? The idea here seems to be to focus the "first" thing, similar to default AT behavior. However, if rootElement is set this doesn't play quite as nicely.

});
})
}
});
```

A comparable from another JS Ecosystem: [Reach Router](https://reach.tech/router) is available for React apps.
The addon style (popular sr-only technique):

## How we teach this
```css
#ember-a11y-refocus-nav-message {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
```

The addon would attempt to provide a sensible resolution for all involved:
- the performance gains from `pushState` remain in place
- users with assistive technology are informed that a page transition has occurred
- the focus is reset to the message itself, which also resets the focus of the page (as is desired for the screen-reader user)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This element should probably provide a jump link back to the section of the page that changed.

- `aria-live` can remain available for things that genuinely need it (a page transition should not need it)

Some [experimentation of this approach can be seen online](https://navigator-message-test-app.netlify.com/). In this example, the message _is_ shown for demonstration purposes, but in the final product, the message would not be shown.

What do users with screen-readers think? "It works better than I thought it might."

### Solution #2 - Content Focus

What about content-level focus? [Ember-self-focused](https://github.com/linkedin/self-focused/tree/master/packages/ember-self-focused) and [ember-a11y](https://github.com/ember-a11y/ember-a11y) are both examples of community-produced addons that attempt to provide a contextual focus, based on new page content.

After some research, we discovered that we could provide content-level focus by using (currently private) API to place focus around the content of the application.

For phase one, the guides should include information about skip links, since the application author will need to add those in themselves (just as they would for a static site). For phase two, we would explain that skip links are no longer needed because we have integrated that functionality in a more machine intelligent way.
For the template, we could take one of two approaches:

### Skip Links
A skip link is an internal link at the beginning of a hypertext document that permits users to skip navigational material and quickly access the document's main content. Skip links are particularly useful for users who access a document with screen readers and users who rely on keyboards.
1. wrap the `{{outlet}}` on application.hbs with an element with the tabindex and id attributes, like this:

Directly inside the `body` element, place a skip link like this:
```hbs
<main tabindex="-1" id="ember-primary-application-outlet">
{{outlet}}
</main>
```

<a href="#maincontent">Skip to main content</a>
2. change the application.hbs outlet so it is different from the usual `{{outlet}}`, and have it already include what we need:

This allows the user with assistive technology to skip directly to the main content and is useful for not needing to repeatedly navigate through a navbar, for example.
```hbs
{{application-outlet}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I much prefer having a single outlet type. @ef4 and I have created a few over time:

  • liquid-outlet ... from liquid-fire.
  • focusing-outlet ... from ember-a11y which lifts the outlet stuff from liquid-fire.
  • animated-outlet ... in Apple Music which is a few dozen lines of code for hardcoded CSS fade transitions.

The problem with this strategy is that none of those solutions by default play nicely together: they're fighting over the same extension point. To wire them together you have to make one dependent upon another and wrap things. You end up (approximately) with:

{{! custom-outlet.hbs }}
{{#animated-outlet}}
  {{#focusing-outlet}}
    {{outlet}}
  {{/focusing-outlet}}
{{/animated-outlet}}

I feel like the API for the outlet itself should be extensible instead of needing to wrap it. I'm thinking something like element modifiers. It's a little hard to figure out what it should do since and outlet doesn't exactly have the same lifecycle hooks that a component has.

```

In this way, if an application author did not want to use the `{{application-outlet}}`, for whatever reason (maybe they are nesting apps, or maybe they already have a focus solution) they could remove the `application-` and be left with a classic outlet.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the behavior in this scenario should be opt out, not opt in. Start with outlet that does the accessible thing, opting out requires changing things in the application. Presumably the person who knows how to opt out will also be more familiar with what they're doing.


It's possible that this solution is a [spherical cow](https://en.wikipedia.org/wiki/Spherical_cow), and the complexity of route transitions in a framework deserves an accessibility solution that is is equally complex.
Either way, we'd depend on the private API `renderSettled` to allow us to focus on the application outlet after the route has changed (a little hand-wavy since it's just an idea):

```js
export default Route.extend({
router: service('router'),
init() {
this._super(...arguments);
this.router.on('routeDidChange', transition => {

if (transition.to !== null) {
emberRequire.renderSettled().then(function() {
document.body.querySelector('#ember-primary-application-outlet').focus();
});
}
});
}
});
```


## How we teach this

It's possible that enterprise Ember users have already implemented their own solution, since governments in many countries require this by law. An out of the box solution would need to be well-advertised and documented to avoid any potential conflicts for users.


## Alternatives
1. We could try to implement something like Apple’s first responder pattern: https://developer.apple.com/documentation/uikit/uiresponder/1621113-becomefirstresponder
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible to implement this, but it's a really wonky experience on the Web. To do it right you would likely need inert.

2. Wait for a SPA solution to be implemented in assistive technology. I would note that this option seems less than ideal, since this specific issue has been reported to the different assistive technologies available, and has been an on-going issue for many years now.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This, and I bet AT vendors will try and push the burden onto the browsers since AT operates at the OS level.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1


## Unresolved Questions
- how would we handle loading states?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ember-a11y works with loading states out of the box with exactly one caveat: when the loading state is below the pivot route.

- how does/could/would this fit in with the router helpers work that Chad did?

## Appendix

### Related RFCs
- [Route Helpers](https://emberjs.github.io/rfcs/0391-router-helpers.html) by Chad Hietala

### Related Reading
- Accessibility APIs
Expand Down