Reputation: 7270
I have a fixed positioned header that "slides up to hide" when users scroll down similar to the one on Medium (example: https://medium.com/@sunknudsen/brute-forcing-your-very-own-vanity-onion-address-at-11-646mh-s-9a9c1fa93663#368a).
Problem is on iOS, when the browser scrolls to hash on page load (#368a
on Medium for example), some content (How to get your very own vanity onion address?
on Medium for example) appears under the header.
I wish to handle (override) the default scroll to hash logic (something to the likes of event.preventDefault()
) to have more control on where to scroll, accounting for the height of the header among other things.
I am able to handle in-page navigation on React using the following logic.
const scrollWithOffset = (element: HTMLElement, offset:number) => {
let offsetTop = element.getBoundingClientRect().top;
const elementPosition = offsetTop - offset - 20;
window.scroll({
top: elementPosition,
left: 0,
behavior: 'smooth'
});
};
<HashLink
to={props.href}
scroll={el => scrollWithOffset(el, 104)}
smooth
>{props.children}</HashLink>
Would love to apply the same logic to the default scroll to hash behavior on page load.
Upvotes: 2
Views: 2233
Reputation: 7270
Thanks to the precious feedback of @T.J.Crowder and to this question, I was able to put together a React component that overrides the browser’s default scroll to hash behavior on page load.
The behavior of this component is inspired by Medium. The page initially loads without scrolling to the element and, after a short delay (once everything has had time to load), it initiates the scroll. See https://medium.com/@sunknudsen/brute-forcing-your-very-own-vanity-onion-address-at-11-646mh-s-9a9c1fa93663#368a. I like how this delay helps contextualize the scroll.
The secret sauce is to quickly swap the id
of the element to which the hash refers to (which disables the default scroll to hash) and to revert back to the real id
once the DOM has had time to load.
export const scrollToWithOffset = (element: HTMLElement, offset:number, smooth?: boolean) => {
let offsetTop = element.getBoundingClientRect().top + window.pageYOffset;
let maxScrollTop = document.body.scrollHeight - document.body.clientHeight;
let options: ScrollToOptions = {
top: Math.min(offsetTop - offset, maxScrollTop),
left: 0,
};
if (smooth === true) {
options.behavior = 'smooth';
}
window.scroll(options);
};
interface ScrollToHashProps {}
interface ScrollToHashState {
element?: HTMLElement;
}
export class ScrollToHash extends Component<ScrollToHashProps, ScrollToHashState> {
constructor(props: ScrollToHashProps) {
super(props);
this.handleScroll = this.handleScroll.bind(this);
}
handleScroll() {
setTimeout(() => {
if (this.state.element) {
this.state.element.id = this.state.element.id.replace('-sth', '');
setTimeout(() => {
if (this.state.element) {
scrollToWithOffset(this.state.element, 0);
}
}, 1000);
}
}, 0);
}
componentDidMount() {
if (window.location.hash !== '') {
let element = document.getElementById(window.location.hash.replace('#', ''));
if (element) {
element.id += '-sth';
this.setState({
element: element
});
if (document.readyState === 'loading') {
window.addEventListener('DOMContentLoaded', this.handleScroll);
} else {
this.handleScroll();
}
}
}
}
componentWillUnmount() {
window.removeEventListener('DOMContentLoaded', this.handleScroll);
}
render() {
return null;
}
}
Upvotes: 0
Reputation: 1074365
I would expect that your slides-up-to-hide header would slide up and hide in this case as part of how it's defined.
If not, you could respond to the DOMContentLoaded
and window
load
events by looking at window.scrollY
and triggering the slide-up-to-hide behavior at that point.
If your goal is for the browser not to scroll to the element, this works for me (iOS Safari, Chrome, and Brave):
window.addEventListener("DOMContentLoaded", function() {
if (location.hash === "#foo") {
setTimeout(function() {
window.scrollTo(0,0);
}, 0);
}
});
It leaves the hash in the URL, but scrolls the window to top. I don't see a flash in my experiment, but your mileage may vary.
Upvotes: 2