the browser-facing portion of osu!
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
2// See the LICENCE file in the repository root for full licence text.
3
4import { action, makeObservable, observable } from 'mobx';
5import { observer } from 'mobx-react';
6import * as React from 'react';
7import { trans } from 'utils/lang';
8
9interface Props {
10 anchor?: React.RefObject<HTMLElement>;
11}
12
13@observer
14export default class BackToTop extends React.Component<Props> {
15 @observable private lastScrollY: number | null = null;
16 private observer: IntersectionObserver | null = null;
17
18 constructor(props: Props) {
19 super(props);
20
21 makeObservable(this);
22 }
23
24 componentWillUnmount() {
25 document.removeEventListener('scroll', this.onScroll);
26 this.removeObserver();
27 }
28
29 render() {
30 return (
31 <button
32 className='floating-toolbar-button'
33 data-tooltip-float='fixed'
34 onClick={this.onClick}
35 title={trans(this.lastScrollY == null ? 'common.buttons.back_to_top' : 'common.buttons.back_to_previous')}
36 >
37 <span className={this.lastScrollY == null ? 'fas fa-angle-up' : 'fas fa-angle-down'} />
38 </button>
39 );
40 }
41
42 @action
43 readonly reset = () => {
44 this.lastScrollY = null;
45 };
46
47 // Workaround Firefox scrollTo and setTimeout(fn, 0) not being dispatched serially.
48 private mountObserver() {
49 // anchor to body if none specified; assumes body's top is 0.
50 const target = this.props.anchor?.current ?? document.body;
51
52 this.observer = new IntersectionObserver((entries) => {
53 for (const entry of entries) {
54 if (entry.target === target && entry.boundingClientRect.top === 0) {
55 document.addEventListener('scroll', this.onScroll);
56 break;
57 }
58 }
59 });
60
61 this.observer.observe(target);
62 }
63
64 @action
65 private readonly onClick = () => {
66 if (this.lastScrollY == null) {
67 const scrollY = this.props.anchor?.current == null ? 0 : ($(this.props.anchor.current).offset()?.top ?? 0);
68 if (window.pageYOffset > scrollY) {
69 this.lastScrollY = window.pageYOffset;
70
71 window.scrollTo(window.pageXOffset, scrollY);
72 this.mountObserver();
73 }
74 } else {
75 window.scrollTo(window.pageXOffset, this.lastScrollY);
76
77 this.lastScrollY = null;
78 }
79 };
80
81 @action
82 private readonly onScroll = () => {
83 this.lastScrollY = null;
84 document.removeEventListener('scroll', this.onScroll);
85 this.removeObserver();
86 };
87
88 private removeObserver() {
89 if (this.observer == null) return;
90
91 this.observer.disconnect();
92 this.observer = null;
93 }
94}