-
Notifications
You must be signed in to change notification settings - Fork 10.3k
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 #19290
Accessible Routing #19290
Conversation
I wonder if instead of "element id for page name", we could design API bit differently so there is less disconnect when users work on their pages. Example idea that I'm not sure if technically could work - what if user could use import React from "react"
import { Announce } from "gatsby"
export default () => (
<>
<Announce>
<h1>Title</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc faucibus eleifend libero in sollicitudin. Nulla facilisi. Pellentesque ultricies ultricies mauris. Etiam commodo tortor bibendum tortor varius mattis. Fusce sollicitudin rhoncus metus, ac vehicula elit hendrerit vel. Cras aliquet, mauris et maximus mollis, nulla est iaculis massa, vitae viverra nulla nunc et dolor. Donec fermentum arcu eu quam viverra malesuada. Suspendisse potenti. Suspendisse fringilla eget mauris facilisis egestas. Duis erat risus, dictum vitae mi nec, scelerisque tristique velit. Ut sed metus est.</p>
</Announce>
<p>Maecenas lacinia justo purus, nec semper nunc cursus at. Vivamus pellentesque nec risus vel varius. Fusce hendrerit, dolor non dapibus feugiat, urna justo bibendum leo, ut facilisis velit ipsum et felis. Maecenas fringilla vehicula vestibulum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec volutpat accumsan sem ut molestie. Sed porta tristique est, sit amet posuere nibh mollis id. Donec orci sem, efficitur a enim ut, suscipit vehicula arcu. Vestibulum hendrerit justo eu accumsan semper. Curabitur viverra ipsum sit amet dui auctor, ut posuere orci pharetra. Maecenas sit amet placerat urna, in fringilla dui. Nunc fermentum ornare velit, vitae tempor libero aliquam id.</p>
</>
) Which would announce everything within our |
This is an interesting idea! I worry that adding a component might make the it more cumbersome for developers and also might make it too easy to include too much content and overwhelm screen reader users. Definitely something to think more on. |
</RouteUpdates> | ||
)} | ||
</EnsureResources> | ||
) | ||
} | ||
} | ||
|
||
const RouteAnnouncer = () => { | ||
const [announcement, setAnnouncement] = useState(``) | ||
useEffect(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should useEffect
run on every update or it can be only for example when location
is changed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think ultimately we would need to use old lifecycle methods anyway (and not hooks) because we declare support for react@^16.4.2
(which is few versions before hooks were introduced):
gatsby/packages/gatsby/package.json
Lines 183 to 186 in 5d78258
"peerDependencies": { | |
"react": "^16.4.2", | |
"react-dom": "^16.4.2" | |
}, |
That's why we don't use any hooks in gatsby runtime right now (which is a bummer).
While this is in work in progress, I don't want to focus on implementation too much yet and first focus on high level ideas/questions:
- use live-region to do announcements
- use short content for page change announcements (title, h1, etc) instead of full new page content
- how users will be able to customize the behaviour (and what should they be able to customize)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what depends to push react dependency to current react version?
Should the name also get it from some |
I like the idea of a built-in component as that'd give fine-grained control to those that want it. @madalynrose we could constrain what people pass in so they can't add too long of messages e.g. < 200 characters or whatever. |
This is exciting! Only question I had is will this work with react-helmet as that only sets new titles after the initial render of the new page so it could get tricky coordinating with it. |
packages/gatsby/cache-dir/root.js
Outdated
@@ -52,15 +52,19 @@ const RouteHandler = props => ( | |||
</BaseContext.Provider> | |||
) | |||
|
|||
let locationListener |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the purpose of this? I can't see how it's used at the moment
const onRouteUpdate = (location, prevLocation) => { | ||
const onRouteUpdate = (location, prevLocation, announceLocation) => { | ||
if (!maybeRedirect(location.pathname)) { | ||
apiRunner(`onRouteUpdate`, { location, prevLocation }) | ||
|
||
if (announceLocation) { | ||
announceLocation() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this logic be moved to the RouteUpdates
component?
My initial thought was that it may be easier to maintain if moved there, since its props will always update when the location changes, meaning if we use this API in other places then we wouldn't have to duplicate any logic
Though on second thoughts, it looks like the onRouteUpdate
API is only called from the RouteUpdates
component, so maybe it wouldn't make a difference either way?
This is awesome!! Left some comments + I second @KyleAMathews' question about React Helmet |
We would still fallback to
This is also true if we would look for specific
I think the additional I was also wondering on how to best provide way for customization (other than pages defining what should be announced) and maybe we could package all of this as a plugin (as we rely on Additional benefit is that users could also just add this plugin to older Gatsby version if they don't want to (or can't) update gatsby core version (I imagine that's a stretch here, and we probably won't see this happening) Lastly - we should release canary version of gatsby with it, as well as publish demo site using those changes for people to try out |
Co-Authored-By: David Bailey <4248177+davidbailey00@users.noreply.github.com>
I see this PR try to fix this:
|
…f hooks, use pathName instead of vague string as default announcement
@@ -30,6 +30,7 @@ window.___loader = publicLoader | |||
navigationInit() | |||
|
|||
apiRunnerAsync(`onClientEntry`).then(() => { | |||
console.log(`HELLO`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Stray console log.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why HELLO to you too
@@ -94,6 +94,69 @@ apiRunnerAsync(`onClientEntry`).then(() => { | |||
} | |||
} | |||
|
|||
// class RouteAnnouncer extends React.Component { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this can be removed now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
></div> | ||
) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this component doesnt render any dynamic data, I wonder if we could add a
shouldComponentUpdate() { return false; }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is still getting used to using refs like this
does this.announcementRef.current.innerText
not count as dynamic data being rendered?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nope. On initial render, the ref will be passed to React, who will attach it to the node. It happens asynchronous and does not affect component lifecycles.
Changing the ref will not cause any re-renders ever!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay I'm trying to leverage my new-ness to React as something useful and ask my potentially trivial questions out in the open 🙈
Our changes happen to the announcement ref happen within the componentDidUpdate
lifecycle method. Wouldn't returning false
inside shouldComponentUpdate
prevent it from being invoked?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great questions! And you know what, I'm wrong. 😆 Since you are doing it in DidUpdate, that occurs after a render. So stopping update stops that cycle. Let's just ship this, I think there are some changes that would better align with React, but this works. So let's ship and we can see if there are better iterations later :)
constructor(props) { | ||
super(props) | ||
this.announcementRef = React.createRef() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ES6 tip (feel free to do this if you want). But you can ditch the constructor here so we're just instantiating a class variable. Do it like this instead:
class Router {
announcementRef = React.createRef()
}
It'll achieve the same result! (if this doesn't work ignore, it does require a specific babel setup to make it work and maybe we don't have that)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'm going to leave this for a later iteration. I'm ready to get this thing OUT!
pageName = pageHeadings[0].textContent | ||
} | ||
const newAnnouncement = `Navigated to ${pageName}` | ||
const oldAnnouncement = this.announcementRef.current.innerText |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, one thing I just thought of. We should add a bailout check if the ref isn't attached yet. It feels unlikely to occur, but technically ref's type is {current: HTMLElement | null}
. It's possible this could come up later on with suspense. All you need to do is at the top of this rAF, put a if (!this.announcementRef.current) return
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@blainekasten Suspense won’t change anything here.
It’s impossible for the ref to be null during componentDidUpdate, though note that because of the rAF call it is possible (though probably unlikely) for the component to unmount before this code runs, so a check is appropriate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@blainekasten I think this check is still missing in the code. Just talked to someone on Discord getting Cannot read property 'innerText' of null
on this line. Don't know if that's the cause of his problem, or a side-effect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm getting this error while trying to debug problems with Ms Edge (14). Happens when I try to navigate to a new page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is awesome! I'm in approval!!
Thanks for letting us know, @wKovacs64! Could you create an issue and add more about your setup (or point to your repo)? I'll dive into this first thing on Tuesday. |
return ( | ||
<div | ||
id="gatsby-announcer" | ||
style={{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please extract object to const outside of class scope, or React.useMemo hook
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please add eslint-plugin-react-perf
, to escape this kind of performance implications.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a very minor perf issue. Creating one object on re-renders (which only happen on route change). It's probably easier to just implement a shouldComponentUpdate: () => false
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Easier - does not mean best way. Counting that react moving to functions, instead of components, and original advise do not use shouldComponentUpdate, this style object already a symptom that there could be much more performance issues in gatsby.js project itself. A lot of very minor issues adds up pretty darn fast
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Former React team manager here – I can guarantee this will never be a performance problem and there is nothing wrong with writing the style object like this. I’d recommend you stop worrying about microoptimizing code.
if (pageHeadings && pageHeadings.length) { | ||
pageName = pageHeadings[0].textContent | ||
} | ||
const newAnnouncement = `Navigated to ${pageName}` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how about internationalization support?
Opened #20697, thanks. |
Just noticed the Any way to turn this off for apps that don't use routing? Or not render/apply those styles when the element is empty? |
@luukdv Thanks for letting us know! Could you open an issue for this and add more about the browser/platform you're seeing this on, possibly with a link to a page where you can reproduce the issue? I'll be focusing on bug fixes first thing on Tuesday. |
# Route Announcements ## Summary This PR improves the accessibility of NextJS's client-side navigation by announcing route changes to screen readers. ## Context When a user who is sighted clicks on a link, they can see the content change. It's an affirmation that what the user intended to do by clicking a link actually worked! Users navigating the page via a screen-reader will not get this feedback on NextJS sites (This is an issue on many SPA-like architectures). https://user-images.githubusercontent.com/4213649/103017382-63b02b00-44f8-11eb-9940-fb530d2d3018.mov ## Solution Whenever there is a route change, the new `<RouteAnnouncer />` will look for a name to give the new page and then announce it! The name is found by first looking for an `h1`, falling back to `document.title`, and lastly to `pathname`. `<RouteAnnouncer />` is a visually hidden component placed within the `<AppContainer />`. ## Demo https://user-images.githubusercontent.com/4213649/103017401-6ad73900-44f8-11eb-8050-b3e9a7e0c3f2.mov ## Inspiration First and foremost, this PR was inspired by @marcysutton's studies and writing, [What we learned from user testing of accessible client-side routing techniques with Fable Tech Labs ](https://www.gatsbyjs.com/blog/2019-07-11-user-testing-accessible-client-routing/) as well as @madalynrose's [Accessible Routing](gatsbyjs/gatsby#19290) PR for Gatsby. There were also learnings gleaned from the conversations within #7681. ### Related Issues & PRs - Resolves #7681 - Relates to #19963
Description
MVP for announcing route changes in Gatsby
h1
,document.title
, hardcoded to"new page"
Tested and works with:
Chrome + VoiceOver
NVDA + IE11
Narrator + Edge
Safari + VoiceOver
NVDA + Firefox
(bug where it announces multiple times)Safari + VoiceOver (iOS)
Chrome + TalkBack (Android) announces twice reliably (not sure if this is fixable)
TODO: