Closed LpmRaven closed 1 year ago
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.
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:
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.
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
.
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.
@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. đź‘Ť
@LpmRaven are you still using the h1 solution and being explicit with regards to the action?
@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.
@LpmRaven Hi, could you point me to information or explain shortly your advice about focus managment please?
- Your focus element should not be a landmark (main, footer etc.)
- 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..
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.
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 ref
s, <Focus component="div">
, etc.).
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)
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.
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?
@mjackson #6449
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!!!
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!
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.
For people looking for a solution for v4.x, I wrote a thing that might help: https://github.com/oaf-project/oaf-react-router
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.
@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.
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.
@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.
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?
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.
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>
)
}
@lionel-rowe thanks for the hook, I had to wrap the history listen inside a useEffect hook.
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.
@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?
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.
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.
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
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.
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();
});
}
}
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!
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?