Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,7 @@ To recreate this `react-router-hash-link` does the following:
- For focusable elements, it calls `element.focus()` and leaves focus on the target element.

Note that you may find it useful to leave focus on non-interactive elements (by adding a `tabindex` of `-1`) to augment the navigation action with a visual focus indicator.

### `preventFocusHandling: boolean`

- Optionally prevent the default focus handling
44 changes: 24 additions & 20 deletions src/HashLink.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ function isInteractiveElement(element) {
const linkTags = ['A', 'AREA'];
return (
(formTags.includes(element.tagName) && !element.hasAttribute('disabled')) ||
(linkTags.includes(element.tagName) && element.hasAttribute('href'))
(linkTags.includes(element.tagName) && element.hasAttribute('href')) ||
element.isContentEditable
);
}

function getElAndScroll() {
function getElAndScroll(preventFocusHandling) {
let element = null;
if (hashFragment === '#') {
// use document.body instead of document.documentElement because of a bug in smoothscroll-polyfill in safari
Expand All @@ -47,19 +48,21 @@ function getElAndScroll() {
if (element !== null) {
scrollFunction(element);

// update focus to where the page is scrolled to
// unfortunately this doesn't work in safari (desktop and iOS) when blur() is called
let originalTabIndex = element.getAttribute('tabindex');
if (originalTabIndex === null && !isInteractiveElement(element)) {
element.setAttribute('tabindex', -1);
}
element.focus({ preventScroll: true });
if (originalTabIndex === null && !isInteractiveElement(element)) {
// for some reason calling blur() in safari resets the focus region to where it was previously,
// if blur() is not called it works in safari, but then are stuck with default focus styles
// on an element that otherwise might never had focus styles applied, so not an option
element.blur();
element.removeAttribute('tabindex');
if (!preventFocusHandling) {
// update focus to where the page is scrolled to
// unfortunately this doesn't work in safari (desktop and iOS) when blur() is called
let originalTabIndex = element.getAttribute('tabindex');
if (originalTabIndex === null && !isInteractiveElement(element)) {
element.setAttribute('tabindex', -1);
}
element.focus({ preventScroll: true });
if (originalTabIndex === null && !isInteractiveElement(element)) {
// for some reason calling blur() in safari resets the focus region to where it was previously,
// if blur() is not called it works in safari, but then are stuck with default focus styles
// on an element that otherwise might never had focus styles applied, so not an option
element.blur();
element.removeAttribute('tabindex');
}
}

reset();
Expand All @@ -68,12 +71,12 @@ function getElAndScroll() {
return false;
}

function hashLinkScroll(timeout) {
function hashLinkScroll(timeout, preventFocusHandling) {
// Push onto callback queue so it runs after the DOM is updated
window.setTimeout(() => {
if (getElAndScroll() === false) {
if (getElAndScroll(preventFocusHandling) === false) {
if (observer === null) {
observer = new MutationObserver(getElAndScroll);
observer = new MutationObserver(() => getElAndScroll(preventFocusHandling));
}
observer.observe(document, {
attributes: true,
Expand Down Expand Up @@ -125,10 +128,10 @@ export function genericHashLink(As) {
props.smooth
? el.scrollIntoView({ behavior: 'smooth' })
: el.scrollIntoView());
hashLinkScroll(props.timeout);
hashLinkScroll(props.timeout, props.preventFocusHandling);
}
}
const { scroll, smooth, timeout, elementId, ...filteredProps } = props;
const { scroll, smooth, timeout, elementId, preventFocusHandling, ...filteredProps } = props;
return (
<As {...passDownProps} {...filteredProps} onClick={handleClick} ref={ref}>
{props.children}
Expand All @@ -152,6 +155,7 @@ if (process.env.NODE_ENV !== 'production') {
timeout: PropTypes.number,
elementId: PropTypes.string,
to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
preventFocusHandling: PropTypes.bool,
};

HashLink.propTypes = propTypes;
Expand Down