Skip to content

Commit 3526ca0

Browse files
authored
Merge pull request #68 from rafgraph/v2.2
2 parents e18d6ac + 0073d0e commit 3526ca0

File tree

4 files changed

+1951
-1377
lines changed

4 files changed

+1951
-1377
lines changed

README.md

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,27 @@ $ npm install --save react-router-hash-link
2222
```javascript
2323
// In YourComponent.js
2424
...
25-
import { HashLink as Link } from 'react-router-hash-link';
25+
import { HashLink } from 'react-router-hash-link';
2626
...
27-
// Use it just like a RRv4/5 <Link> (to can be a string or an object, see RRv4/5 api for details):
28-
<Link to="/some/path#with-hash-fragment">Link to Hash Fragment</Link>
27+
// Use it just like a RRv4/5 <Link> (to can be a string or an object, see RRv4/5 api for details)
28+
<HashLink to="/some/path#with-hash-fragment">Link to Hash Fragment</HashLink>
2929
```
3030

3131

3232
### `<NavHashLink>`
3333
```javascript
3434
// In YourComponent.js
3535
...
36-
import { NavHashLink as NavLink } from 'react-router-hash-link';
36+
import { NavHashLink } from 'react-router-hash-link';
3737
...
38-
// Use it just like a RRv4/5 <NavLink> (see RRv4/5 api for details):
39-
<NavLink
38+
// Use it just like a RRv4/5 <NavLink> (see RRv4/5 api for details)
39+
// It will be active only if both the path and hash fragment match
40+
<NavHashLink
4041
to="/some/path#with-hash-fragment"
4142
activeClassName="selected"
43+
activeStyle={{ color: 'red' }}
4244
// etc...
43-
>Link to Hash Fragment</NavLink>
45+
>Link to Hash Fragment</NavHashLink>
4446
```
4547

4648
## Scrolling API
@@ -49,22 +51,39 @@ import { NavHashLink as NavLink } from 'react-router-hash-link';
4951
- React Router Hash Link uses the native Element method `element.scrollIntoView()` for scrolling, and when the `smooth` prop is present it will call it with the smooth option, `element.scrollIntoView({ behavior: 'smooth' })`
5052
- Note that not all browsers have implemented options for `scrollIntoView` - see [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView) and [Can I Use](https://caniuse.com/#feat=scrollintoview) - there is also a browser [polyfill for smooth scrolling](https://github.com/iamdustan/smoothscroll) which you can install separately so `smooth` will work in all browsers
5153
```js
52-
import { HashLink as Link } from 'react-router-hash-link';
53-
<Link smooth to="/path#hash">Link to Hash Fragment</Link>
54+
import { HashLink } from 'react-router-hash-link';
55+
<HashLink smooth to="/path#hash">Link to Hash Fragment</HashLink>
5456
```
5557

5658
### `scroll: function`
5759
- Custom scroll function called with the element to scroll to, e.g. `const myScrollFn = element => {...}`
5860
- This allows you to do things like scroll with offset, use a specific smooth scrolling library, or pass in your own options to `scrollIntoView`
5961
```js
60-
import { HashLink as Link } from 'react-router-hash-link';
61-
<Link
62+
import { HashLink } from 'react-router-hash-link';
63+
<HashLink
6264
to="/path#hash"
6365
scroll={el => el.scrollIntoView({ behavior: 'instant', block: 'end' })}
64-
>Link to Hash Fragment</Link>
66+
>Link to Hash Fragment</HashLink>
6567
```
6668

67-
### Custom `Link`
69+
### Scroll to top of page
70+
- To scroll to the top of the page set the hash fragment to `#` (empty) or `#top`
71+
- This is inline with the [HTML spec](https://html.spec.whatwg.org/multipage/browsing-the-web.html#target-element), also see [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#Linking_to_an_element_on_the_same_page)
72+
```js
73+
import { HashLink } from 'react-router-hash-link';
74+
<HashLink to="/path#top">Link to Top of Page</HashLink>
75+
```
76+
77+
### Scroll with offset
78+
- To scroll with offset use a custom scroll function, one way of doing this can be found [here](https://github.com/rafgraph/react-router-hash-link/issues/25#issuecomment-536688104)
79+
80+
### `elementId: string`
81+
- Scroll to the element with matching id
82+
- Used instead of providing a hash fragment as part of the `to` prop, if both are present then the `elementId` will override the `to` prop's hash fragment
83+
- Note that it is generally recommended to use the `to` prop's hash fragment instead of the `elementId`
84+
85+
86+
## Custom `Link`
6887

6988
The exported components are wrapped versions of the `Link` and `NavLink` exports of react-router-dom. In some cases you may need to provide a custom `Link` implementation.
7089

package.json

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
"description": "Hash link scroll functionality for React Router v4/5",
55
"main": "lib/index.js",
66
"scripts": {
7-
"build": "rm -rf lib && babel src -d lib --presets=env,react --plugins=transform-object-rest-spread",
7+
"build": "rm -rf lib && babel src -d lib --presets=@babel/preset-env,@babel/preset-react",
88
"prepublish": "yarn build",
9-
"dev": "yarn link && babel src -d lib --watch --presets=env,react --plugins=transform-object-rest-spread"
9+
"dev": "yarn link && babel src -d lib --watch --presets=@babel/preset-env,@babel/preset-react"
1010
},
1111
"files": [
1212
"src",
@@ -33,12 +33,17 @@
3333
"react-router-dom": ">=4"
3434
},
3535
"devDependencies": {
36-
"babel-cli": "^6.26.0",
37-
"babel-plugin-transform-object-rest-spread": "^6.26.0",
38-
"babel-preset-env": "^1.6.1",
39-
"babel-preset-react": "^6.24.1"
36+
"@babel/cli": "^7.11.6",
37+
"@babel/core": "^7.11.6",
38+
"@babel/preset-env": "^7.11.5",
39+
"@babel/preset-react": "^7.10.4",
40+
"prettier": "^2.1.2"
4041
},
4142
"dependencies": {
42-
"prop-types": "^15.6.0"
43+
"prop-types": "^15.7.2"
44+
},
45+
"prettier": {
46+
"trailingComma": "all",
47+
"singleQuote": true
4348
}
4449
}

src/index.js

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,33 @@ function reset() {
1717
}
1818

1919
function getElAndScroll() {
20-
const element = document.getElementById(hashFragment);
20+
let element = null;
21+
if (hashFragment === '#') {
22+
element = document.body;
23+
} else {
24+
// check for element with matching id before assume '#top' is the top of the document
25+
// see https://html.spec.whatwg.org/multipage/browsing-the-web.html#target-element
26+
const id = hashFragment.replace('#', '');
27+
element = document.getElementById(id);
28+
if (element === null && hashFragment === '#top') {
29+
element = document.body;
30+
}
31+
}
32+
2133
if (element !== null) {
2234
scrollFunction(element);
35+
36+
// update focus to where the page is scrolled to
37+
// unfortunately this doesn't work in safari (desktop and iOS) when blur() is called
38+
let originalTabIndex = element.getAttribute('tabindex');
39+
if (originalTabIndex === null) element.setAttribute('tabindex', -1);
40+
element.focus({ preventScroll: true });
41+
// for some reason calling blur() in safari resets the focus region to where it was previously,
42+
// if blur() is not called it works in safari, but then are stuck with default focus styles
43+
// on an element that otherwise might never had focus styles applied, so not an option
44+
element.blur();
45+
if (originalTabIndex === null) element.removeAttribute('tabindex');
46+
2347
reset();
2448
return true;
2549
}
@@ -48,33 +72,39 @@ function hashLinkScroll(timeout) {
4872

4973
export function genericHashLink(As) {
5074
return React.forwardRef((props, ref) => {
75+
let linkHash = '';
76+
if (typeof props.to === 'string' && props.to.includes('#')) {
77+
linkHash = `#${props.to.split('#').slice(1).join('#')}`;
78+
} else if (
79+
typeof props.to === 'object' &&
80+
typeof props.to.hash === 'string'
81+
) {
82+
linkHash = props.to.hash;
83+
}
84+
85+
const passDownProps = {};
86+
if (As === NavLink) {
87+
passDownProps.isActive = (match, location) =>
88+
match && match.isExact && location.hash === linkHash;
89+
}
90+
5191
function handleClick(e) {
5292
reset();
93+
hashFragment = props.elementId ? `#${props.elementId}` : linkHash;
5394
if (props.onClick) props.onClick(e);
54-
if (typeof props.to === 'string') {
55-
hashFragment = props.to
56-
.split('#')
57-
.slice(1)
58-
.join('#');
59-
} else if (
60-
typeof props.to === 'object' &&
61-
typeof props.to.hash === 'string'
62-
) {
63-
hashFragment = props.to.hash.replace('#', '');
64-
}
6595
if (hashFragment !== '') {
6696
scrollFunction =
6797
props.scroll ||
68-
(el =>
98+
((el) =>
6999
props.smooth
70-
? el.scrollIntoView({ behavior: "smooth" })
100+
? el.scrollIntoView({ behavior: 'smooth' })
71101
: el.scrollIntoView());
72102
hashLinkScroll(props.timeout);
73103
}
74104
}
75-
const { scroll, smooth, timeout, ...filteredProps } = props;
105+
const { scroll, smooth, timeout, elementId, ...filteredProps } = props;
76106
return (
77-
<As {...filteredProps} onClick={handleClick} ref={ref}>
107+
<As {...passDownProps} {...filteredProps} onClick={handleClick} ref={ref}>
78108
{props.children}
79109
</As>
80110
);
@@ -90,6 +120,7 @@ const propTypes = {
90120
children: PropTypes.node,
91121
scroll: PropTypes.func,
92122
timeout: PropTypes.number,
123+
elementId: PropTypes.string,
93124
to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
94125
};
95126

0 commit comments

Comments
 (0)