-
-
Notifications
You must be signed in to change notification settings - Fork 10.7k
Add <Focus> component to react-router-dom #6449
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,26 @@ | ||
{ | ||
"esm/react-router-dom.js": { | ||
"bundled": 7978, | ||
"minified": 4880, | ||
"gzipped": 1618, | ||
"bundled": 10547, | ||
"minified": 6376, | ||
"gzipped": 2040, | ||
"treeshaked": { | ||
"rollup": { | ||
"code": 1250, | ||
"import_statements": 417 | ||
"code": 1285, | ||
"import_statements": 440 | ||
}, | ||
"webpack": { | ||
"code": 3322 | ||
"code": 3336 | ||
} | ||
} | ||
}, | ||
"umd/react-router-dom.js": { | ||
"bundled": 159709, | ||
"minified": 57597, | ||
"gzipped": 16540 | ||
"bundled": 162216, | ||
"minified": 58840, | ||
"gzipped": 16893 | ||
}, | ||
"umd/react-router-dom.min.js": { | ||
"bundled": 97476, | ||
"minified": 34651, | ||
"gzipped": 10216 | ||
"bundled": 99449, | ||
"minified": 35481, | ||
"gzipped": 10400 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
# <Focus> | ||
|
||
Provides a way for an application to add [focus management](https://developers.google.com/web/fundamentals/accessibility/focus/using-tabindex#managing_focus_at_the_page_level) after navigation for better accessibility. | ||
|
||
```jsx | ||
import { Focus } from 'react-router-dom' | ||
|
||
<Focus> | ||
{ref => ( | ||
<main tabIndex={-1} ref={ref}> | ||
{/* ... */} | ||
</main> | ||
)} | ||
</Focus> | ||
``` | ||
|
||
`Focus` uses a render prop to provide a `ref`. The `ref` should be passed to the element that will be focused. | ||
|
||
In order for `Focus` to work, the component type for the focused element needs to either be natively focusable (like an `<input>` or a `<button>`) or be given a `tabIndex` of `-1`. If you do not do this, then the document's `<body>` will be focused instead. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If an element isn't natively focusable, could we perhaps do a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think that My personal preference would be to require the user to set the tab index. It's a little more work for the dev than RR injecting the attribute, but the End of the day, (assuming setting the attribute works like I think it would) I can make the switch if you would prefer. |
||
|
||
Focusing a DOM element will give it an outline; you can style it with `outline: none;` to hide this outline. | ||
pshrmn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
**Note:** Only the element that is passed the `ref` should have the `outline: none` style. A global `outline: none` rule should **not** be used because it will make your application inaccessible to users who navigate the page using their keyboard. You also should not use `outline: none` style if you are attaching the `ref` to a natively focusable element, like an `input`, `a`, or `button`. | ||
|
||
```jsx | ||
<Focus> | ||
{ref => ( | ||
<main tabIndex={-1} ref={ref} style={{ outline: "none" }}> | ||
{/* ... */} | ||
</main> | ||
)} | ||
</Focus> | ||
``` | ||
|
||
## children: function | ||
|
||
The `children` function will be called with a [`ref`](https://reactjs.org/docs/refs-and-the-dom.html) and should return valid React elements. | ||
|
||
```jsx | ||
<Focus> | ||
{ref => ( | ||
<main tabIndex={-1} ref={ref}> | ||
{/* ... */} | ||
</main> | ||
)} | ||
</Focus> | ||
``` | ||
|
||
## preserve: bool | ||
|
||
When `true`, if one of the focused element's children is already focused (e.g. uses `autofocus`), then the element will not steal the focus from the child. Defaults to `false`. | ||
|
||
```jsx | ||
<Focus preserve={true}> | ||
{ref => ...} | ||
</Focus> | ||
``` | ||
|
||
## preventScroll: bool | ||
|
||
When `true`, the application will not scroll to the element when it is focused. Defaults to `false`. | ||
|
||
**Note:** This is experimental functionality that does not work in all browsers. | ||
|
||
```jsx | ||
<Focus preventScroll={true}> | ||
{ref => ...} | ||
</Focus> | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import React from "react"; | ||
import { __RouterContext as RouterContext } from "react-router"; | ||
import warning from "tiny-warning"; | ||
import PropTypes from "prop-types"; | ||
import { locationsAreEqual } from "history"; | ||
|
||
class FocusWithLocation extends React.Component { | ||
setRef = element => { | ||
this.eleToFocus = element; | ||
}; | ||
|
||
componentDidMount() { | ||
this.focus(); | ||
} | ||
|
||
componentDidUpdate(prevProps) { | ||
// only re-focus when the location changes | ||
if (!locationsAreEqual(this.props.location, prevProps.location)) { | ||
this.focus(); | ||
} | ||
} | ||
|
||
focus() { | ||
const { preserve, preventScroll } = this.props; | ||
// https://developers.google.com/web/fundamentals/accessibility/focus/using-tabindex#managing_focus_at_the_page_level | ||
if (this.eleToFocus != null) { | ||
warning( | ||
this.eleToFocus.hasAttribute("tabIndex") || | ||
this.eleToFocus.tabIndex !== -1, | ||
'The ref must be assigned an element with the "tabIndex" attribute or be focusable by default in order to be focused. ' + | ||
"Otherwise, the document's <body> will be focused instead." | ||
); | ||
|
||
if (preserve && this.eleToFocus.contains(document.activeElement)) { | ||
return; | ||
} | ||
|
||
setTimeout(() => { | ||
this.eleToFocus.focus({ preventScroll }); | ||
}); | ||
} else { | ||
warning( | ||
false, | ||
"There is no element to focus. Did you forget to add the ref to an element?" | ||
); | ||
} | ||
} | ||
|
||
render() { | ||
const { children } = this.props; | ||
return children(this.setRef); | ||
} | ||
} | ||
|
||
const Focus = props => ( | ||
<RouterContext.Consumer> | ||
{context => <FocusWithLocation location={context.location} {...props} />} | ||
</RouterContext.Consumer> | ||
); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we add some I'm kinda on the fence about There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can build your modules with a Babel plugin that strips prop types in production mode. That's what I do for RNWeb There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I meant to do this. This code was adapted from TypeScript, so I stripped out all of type references and then completely forgot to add prop types. I would not miss prop types. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the tip, @necolas :) We used to use that plugin, but we use the When I mentioned bundle size, what I meant was that I'm not sure if our |
||
Focus.defaultProps = { | ||
preventScroll: false | ||
}; | ||
|
||
if (__DEV__) { | ||
Focus.propTypes = { | ||
preventScroll: PropTypes.bool, | ||
preserve: PropTypes.bool, | ||
children: PropTypes.func | ||
}; | ||
} | ||
|
||
export default Focus; |
Uh oh!
There was an error while loading. Please reload this page.