Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

SPA Accessibility - focus not reset when route changed #5210

Closed
LpmRaven opened this issue Jun 5, 2017 · 35 comments
Closed

SPA Accessibility - focus not reset when route changed #5210

LpmRaven opened this issue Jun 5, 2017 · 35 comments
Labels

Comments

@LpmRaven
Copy link

LpmRaven commented Jun 5, 2017

I am using a screen reader to interact with my application.

Moving between routes does not reset the focus, which would occur on a server-side render. Is there a fix to reset the focus on route changes?

@pshrmn
Copy link
Contributor

pshrmn commented Jun 5, 2017

Just to make sure that we're on the same page, you're referring to the same thing as this article discusses, correct? https://sarbbottam.github.io/blog/2016/10/22/focus-reset-and-guided-focus-management Whenever the location changes, you want to reset the focus to some initial position (like a full page refresh would produce).

I think that what you would want to do is to have a root component (inside of your <Router>) that uses a ref to focus after the component updates.

class Refocus extends React.Component {

  componentDidUpdate() {
    if (this.node) {
      this.node.focus();
    }
  }

  render() {
    return <div ref={n => this.node = n} tabIndex={-1}>{this.props.children}</div>;
  }
}

// usage
ReactDOM.render((
  <BrowserRouter>
    <Refocus>
      <App />
    </Refocus>
  </BrowserRouter>
), holder);

If I'm completely off, please let me know.

@LpmRaven
Copy link
Author

LpmRaven commented Jun 8, 2017

Yes focus management for SPAs using react is what I am looking for, thanks for the suggestion @pshrmn! I did some extra digging myself, Angular seems to focus on the first h1 on a client side change, which is a pretty nice user experience. Testing the problem, the issue is not just the focus management but letting an assistive technology (screen reader) user know when a page has changed.

My current solution:

An aria-live div at the top of page (only re-renders on server side) which will announce to AT when I change its contents. (Visually hidden class, non-focusable)

<div aria-live="polite" className="gs-u-vh" id="accessibility-message"></div>

Then to control the contents of the aria-live div, I have a function that changes its contents when my h1 changes. (As the aria-live div is not re-rendered client side, it will announce its new contents)

export function pageChangeAnnouncement() {
	let accessibilityHookMessage;
	let myH1;
	if (typeof window !== 'undefined') {
		myH1 = document.getElementsByTagName("h1")[0].innerHTML;
		accessibilityHookMessage = `${myH1} , View loaded`;
		document.getElementById("accessibility-message").innerHTML = accessibilityHookMessage;
	} else {
		accessibilityHookMessage = `page change, View loaded`;
	}
}

I appreciate your suggestion @pshrmn, it sent my down the right path but when I tested it, it did not behave as expected. This way does look a bit old school JS, but it works.

let ignoreFirstLoad = true;

if (ignoreFirstLoad) {
	ignoreFirstLoad = false;
} else {
	let resetFocusItem = document.getElementsByTagName("h1")[0];
	resetFocusItem.setAttribute("tabindex", "-1");
	resetFocusItem.style.outline = "none";
	resetFocusItem.focus();
}

A few notes to anyone else trying to manage focus:

  1. Your focus element should not be a landmark (main, footer etc.)
  2. If you set your element to a container div, it will read the contents. (Confusing to users)
  3. You need to give your focus element a tabindex of -1. (To allow focus)
  4. You should not focus on a link or button (as that will remote it from the default focus order when you give it a tabindex of -1).

@LpmRaven LpmRaven changed the title Accessibility - focus not reset when route changed SPA Accessibility - focus not reset when route changed Jun 23, 2017
@bmancini42
Copy link

This seems like a great idea, but not working for me for some reason. I have tried a number of variations of focus, blur, etc. and it doesn't seem to be taking effect. I can see that the document.activeElement is changing, but the 'outline' / tab position in the page remains the same as it was.

@bmancini42
Copy link

Nevermind. My problem was that I was doing it in componentWillReceiveProps. I was blurring and focusing on a DOM that was about to disappear. Moving it into componentWillUpdate fixed it. (the updating field here was withRouter's location.pathname.

@marcysutton
Copy link

I'd like to see more emphasis on an easy-to-use focus management solution in React Router instead of a live region, because functionally it is helpful to put the user's focus in a specific part of the page rather than leaving it where it is (on a Router link). I can use an onClick on each router link and send focus to a ref, but it would be super repetitive. There has to be a better way! I'm thinking of trying event delegation bound to a wrapper element and triggered by each individual link.

The problem with focusing automatically when a component renders is you might be focusing at the wrong time, especially if components get reused. Setting focus when the user initiates the action by activating a link is a better approach, I've found.

@LpmRaven
Copy link
Author

@marcysutton I agree, setting focus when the user initiates an action seems the correct approach. In my approach, I reset focus to the h1 when the page changes and announce the new page name.
${myH1} , View loaded;
I wasn't completely sure where to reset focus to. What I have done is quite basic but I would quite like to see something hooked into the router link. Any solution you come up with would be great to see. 👍

@jimthedev
Copy link
Contributor

@LpmRaven are you still using the h1 solution and being explicit with regards to the action?

@LpmRaven
Copy link
Author

@jimthedev still using this, I haven't seen any other solutions since opening this issue. If anyone has any new information it would be great to hear.

@sladiri
Copy link

sladiri commented Jun 3, 2018

@LpmRaven Hi, could you point me to information or explain shortly your advice about focus managment please?

  1. Your focus element should not be a landmark (main, footer etc.)
  2. If you set your element to a container div, it will read the contents. (Confusing to users)

What would be a good focus target instead? I think you are using a heading inside the landmark, but are not sure about it? Why is a landmark a bad idea (edit: Maybe you mean the iOS bug, where dynamic content inside a focussed landmark is not re-read)? I tried out NVDA briefly, and focussing on a heading element seemed OK, but I am not a screen reader user..

@krivaten
Copy link

krivaten commented Jul 5, 2018

In the land of Ember we have an addon called Ember A11y, which on route change focuses the browser on a div that wraps the main content (There is more to it but that's the gist). Example:

<div class="ember-view focusing-outlet" tabindex="-1" role="group">
    <h1>About Page</h1>
    ...
</div>

As I just started really digging in to React I am hoping to find or create a similar solution as it works rather nicely for the Ember apps I work on.

@pshrmn
Copy link
Contributor

pshrmn commented Jul 5, 2018

A <Focus> component could be added (for reference, I wrote one for my router and the code would pretty similar for React Router).

Whether this exists as part of core React Router DOM is mostly up to @mjackson. Arguments for including it in RRD would be to extend its reach (:wink:). Arguments for having it in its own package would be that it isn't core functionality and that different packages could choose different implementations (render-invoked props to pass refs, <Focus component="div">, etc.).

@afeld
Copy link

afeld commented Jul 6, 2018

My solution (simplified example):

class App extends React.Component {
  constructor(props) {
    super(props)
    this.sectionFocusEl = React.createRef()
  }

  componentDidUpdate(prevProps) {
    // https://stackoverflow.com/a/44410281/358804
    if (this.props.location.pathname !== prevProps.location.pathname) {
      this.sectionFocusEl.focus()
    }
  }

  render() {
    return <div ref={this.sectionFocusEl}/>;
  }
}

export default withRouter(App)

@Anthony-YY
Copy link

Here is a scenario we need to reset focus and currently don't have proper and generic solution to reset focus.
Saying that we have a list, a list item(Link Component) clicked, item details panel opened and route changed to '/list/ds8723/details'. So at this point if we click panel close button we return to previous route, and focus should return to that list item(Link Component).This makes focus order logical. How to satisfy requirements like this.
I am not sure i make myself understood. Hope to get some ideas from you.

@mjackson
Copy link
Member

mjackson commented Nov 2, 2018

I'm very keen to include something like this in core @pshrmn. I've been focusing a lot of work on other areas, so I haven't had a bunch of time to really think about this yet. But I'm open to suggestions. Your <Refocus> component seems like a good idea.

Is there any consensus yet on what a good solution looks like?

@pshrmn
Copy link
Contributor

pshrmn commented Nov 2, 2018

@mjackson #6449

@justsml
Copy link
Contributor

justsml commented Nov 12, 2018

Just ran into this, and I agree @mjackson - I'd love to see this in core.

Setting these sort of priorities sends the right message: a11y isn't optional 👍

I'll try the PR, and report back if I have issues.

Awesome work y'all!!!

@LpmRaven
Copy link
Author

LpmRaven commented Dec 3, 2018

I noticed @gatsbyjs have switched their router to reach/router because it supports screen readers on a SPA, progress! It's great that accessibility is being put at the forefront of some prominent js projects.

"Without the help of a router, managing focus on route transitions requires a lot effort and knowledge on your part. Reach Router provides out-of-the-box focus management so your apps are significantly more accessible without you breaking a sweat."

Great stuff!

@i5ar
Copy link

i5ar commented Mar 3, 2019

In v4.3 I have the opposite problem. When I change the option from the menu the focus resets.
Because I'm using hashed routes (from a dropdown menu) I don't want the focus to reset. Is there an option to disable the focus management? Thank you.

Just to clarify: I'm using ReactRouterDOM.withRouter(Dropdown) in the App component and this.props.history.push(`/#/${this.props._value}`) in the Dropdown component.

Before to use the router I was able to change the selected option from the arrow keys.

@danielnixon
Copy link

For people looking for a solution for v4.x, I wrote a thing that might help: https://github.com/oaf-project/oaf-react-router

@mjackson mjackson mentioned this issue Aug 26, 2019
23 tasks
@stale stale bot added stale and removed stale labels Sep 10, 2019
@stale stale bot added stale and removed stale labels Nov 9, 2019
@stale stale bot added the stale label Jan 9, 2020
@timdorr timdorr removed the stale label Jan 9, 2020
@remix-run remix-run deleted a comment from dantman Jan 9, 2020
@remix-run remix-run deleted a comment from stale bot Jan 9, 2020
@frastlin
Copy link

I am not experiencing a focus change using React-router-dom 5.1.2.
I would like to see something similar to what reach/router has out of the box, as there is never a circumstance when you would not want the focus to jump to the new content from a Link, NavLink, or redirect.

@i5ar If you are using the keyboard, you can press alt+down arrow to navigate through the combobox without focusing the page. You can also just have a list of page names, and perform a redirect when the user selects a page.

@LpmRaven
Copy link
Author

@frastlin I still don't think this issue has been fixed. Its been 3 years. I would hope to see something similar to reach/router (which I now use). Marcy Sutton (at GatsbyJS) did some great work last year, user testing of accessible client-side routing techniques. I would suggest everyone read that blog post.

@frastlin
Copy link

frastlin commented Jan 30, 2020

That article is amazing. If there is a way for users to specify an element to target, with updated content as a fallback, that would be ideal. Now, when a screen reader user clicks on a link, nothing happens, which violates a fundamental design principle.

reach/router is being slowly deprecated as seen in:
https://reacttraining.com/blog/reach-react-router-future/

So this needs to be fixed in react-router now.

@LpmRaven
Copy link
Author

@frastlin I think there needs to be consistency across websites as to where the focus moves on route change which is why this is important to be implemented by the router rather than custom code (which I was initially trying to do in this issue).

I hope this will be fixed soon, it does mention this issue on in the features list: Automatic focus management on route transitions and this issue is sitting in the roadmap backlog #6885

Whoever implements it should be referring to @marcysutton 's article (in my previous comment). If anyone finds anymore tested research on this issue I would appreciate hearing about it.

@mjackson
Copy link
Member

mjackson commented Feb 4, 2020

I've been hesitant to make any recommendations here since I don't want to cause more harm than good. I'm grateful for the pioneering work that was done in reach/router, but according to Marcy's article the approach it takes is just the beginning of a really comprehensive solution. It seems like the "best practice" for managing focus with a client-side router continues to evolve (the last update to the article was less than 4 months ago).

From that article:

The advice now looks like this:

  • Provide a skip link that takes focus on a route change within the site, with a label that indicates what the link will do when activated: e.g. “skip to main navigation”.
  • Include an ARIA Live Region on page load. On a route change, append text to it indicating the current page, e.g. “Portfolio page”.

She also says:

The most accessible and best performing pattern will likely be an opt-in component where the developer can specify where the control should go in the DOM and how it should be labeled. But it’s worth pointing out that if a solution can be handled automatically, it would have a wider impact amongst developers who aren’t prioritizing accessibility.

This reminds me of the approach taken by @pshrmn in #6449. It's not an automatic solution, so people are free to disregard it entirely. But at least it provides a starting point. Maybe if we did have a <Focus>-style component (as demo'd in the PR), we could build something more automatic on top of that primitive in the future?

@frastlin
Copy link

frastlin commented Feb 4, 2020

Well now we have nothing, so anything is better than what we have now, as long as it doesn't significantly break.
With the focus component, what happens if there are multiple components on the page?
Marcy did say the best experience is jumping to the h1 on page load, or just jumping to the changed content. I would recommend looking for the first H1, then follow that up with jumping to the new content on the page if there is no H1 (which there should be).
The developer should also be able to disable the focus jump so they can jump the focus to where they would like the screen reader user to be.

@lionel-rowe
Copy link

Here's a variation of @pshrmn's refocus component, using hooks and TypeScript:

let prevPathName: string | null = null

const FocusOnRouteChange: React.FC = ({ children }) => {
    const history = useHistory()

    const ref = useRef<HTMLDivElement>(null)

    history.listen(({ pathname }) => {
        // don't refocus if only the query params/hash have changed
        if (pathname !== prevPathName) {
            ref.current?.focus()

            // prevent jank if focusing causes page to scroll
            window.scrollTo(0, 0)

            prevPathName = pathname
        }
    })

    return (
        <div ref={ref} tabIndex={-1} style={{ outline: 'none' }}>
            {children}
        </div>
    )
}

@jafin
Copy link

jafin commented Aug 25, 2020

@lionel-rowe thanks for the hook, I had to wrap the history listen inside a useEffect hook.

@marcysutton
Copy link

It's a bummer that React Router still doesn't handle focus at all. Is that going to be addressed soon? Sending focus to a wrapper element or a heading would be better than doing nothing, even if a comprehensive skip link solution isn't in the cards right now.

@shayc
Copy link

shayc commented Dec 19, 2020

@marcysutton from what I've just read online: reach-router (handles focus) will be combined with react-router-dom :

https://reacttraining.com/blog/reach-react-router-future/

The article is from 2019 though, so I wouldn't get your hopes up. Maybe we can help with this?

@marcysutton
Copy link

That post is from May 2019, and it's almost 2021–hence my comment. Given @ryanflorence's past commitment to accessibility including Reach UI and Reach Router, I'd hope the React Training team in its new rendition could meet this requirement and not leave it to the community to handle. It otherwise doesn't send a great signal to the community that accessibility is being taken seriously at all.

@ptamarit
Copy link

ptamarit commented May 8, 2021

For those looking for a solution until this ticket is closed, the article "Accessible page title in a single-page React application" by Kitty Giraudel describes a really good solution based on React Router and React Helmet.

@mwmcode
Copy link

mwmcode commented May 10, 2021

This is what I ended up doing (inspired by Kitty's article).

const ref = useRef<HTMLSpanElement>(null);
const { pathname } = useLocation();
 
// remove once react-router accessibility issue is fixed
// https://github.com/ReactTraining/react-router/issues/5210
useEffect(() => {
  ref.current?.focus();
}, [pathname]);

return (
    <>
      <span ref={ref} tabIndex={-1} />
      <a href="#navigation" className={className}>
        Go to navigation
      </a>
      <a href="#main" className={className}>
        Go to content
      </a>
    </>
)

when pathname changes, the next key tab will focus skip links

@chaance
Copy link
Collaborator

chaance commented Sep 4, 2021

Looking to revive this and get some default focus management on the table. Lots of great progress has been made in this area since the issue was first opened, much of it from contributors to this thread, and more than enough for us to act. I think we can offer a reasonable default in both v5 and v6 in the coming weeks.

I'll follow up here with a few proposals for those interested in offering feedback.

@ajaykarwal
Copy link

This thread came up in my search for a solution to the same issue with Angular.
I took inspiration from the solution by @mwmcode and came up with this working solution for Angular if anyone else requires it.

app.component.ts

import { Component, ElementRef, ViewChild } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
@Component({
	selector: 'app-root',
	template: `
		<span #focusReset tabIndex="-1"></span>
		<router-outlet></router-outlet>
	`
})
export class AppComponent implements OnInit {
	@ViewChild('focusReset') public focusReset: ElementRef;
	constructor(private router: Router) {
		this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => {
			this.focusReset.nativeElement.focus();
		});
	}
}

@brophdawg11
Copy link
Contributor

I'm going to convert this to a discussion so it can go through our new Open Development process. Please upvote the new Proposal if you'd like to see this considered!

@remix-run remix-run locked and limited conversation to collaborators Jan 9, 2023
@brophdawg11 brophdawg11 converted this issue into discussion #9863 Jan 9, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
Projects
None yet
Development

No branches or pull requests