Skip to content

Commit

Permalink
Add "scroll to top" button on events screen
Browse files Browse the repository at this point in the history
  • Loading branch information
brundonsmith committed Jun 19, 2024
1 parent ab60a04 commit ef12c65
Show file tree
Hide file tree
Showing 9 changed files with 74 additions and 8 deletions.
3 changes: 2 additions & 1 deletion front-end/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"simple-import-sort/imports": "error",
"no-console": ["error", { "allow": ["warn", "error"] }]
"no-console": ["error", { "allow": ["warn", "error"] }],
"no-inner-declarations": "off"
}
}
11 changes: 11 additions & 0 deletions front-end/src/components/Events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import Col from './core/Col'
import Icon from './core/Icon'
import LoadingDots from './core/LoadingDots'
import Modal from './core/Modal'
import { useSlideScroll } from './core/MultiView'
import Row from './core/Row'
import RowSelect from './core/RowSelect'
import Spacer from './core/Spacer'
import Event from './events/Event'
import EventEditor from './events/EventEditor'

export default React.memo(() => {
const { scrollToTop, scrollHeight, scrollTop } = useSlideScroll()
const showScrollButton = scrollTop > 100
const store = useStore()
const [filter, setFilter] = useState<'All' | 'Bookmarked' | 'Mine'>('All')

Expand Down Expand Up @@ -68,6 +71,14 @@ export default React.memo(() => {

<Spacer size={12} />

<div style={{ position: 'fixed', top: 0, left: 0, opacity: showScrollButton ? 1 : 0, width: '100%', padding: 10, pointerEvents: showScrollButton ? undefined : 'none', appearance: 'none', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Button onClick={scrollToTop} style={{ width: 'auto', padding: '5px 10px', borderRadius: 21 }}>
<Icon name='arrow_back' style={{ transform: 'rotate(90deg)' }} />
<Spacer size={6} />
Scroll to top
</Button>
</div>

<Events events={visibleEvents} editEvent={editEvent} />

<Modal isOpen={eventBeingEdited != null} onClose={stopEditingEvent} side='right'>
Expand Down
1 change: 1 addition & 0 deletions front-end/src/components/core/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type MaterialIconName =
| 'person'
| 'open_in_new'
| 'arrow_back_ios'
| 'arrow_back'
| 'check'
| 'content_copy'
| 'add'
Expand Down
46 changes: 43 additions & 3 deletions front-end/src/components/core/MultiView.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,64 @@
import React, { CSSProperties, ReactNode } from 'react'
import React, { CSSProperties, FC, ReactNode, useCallback, useContext, useMemo, useState } from 'react'

import useElementEvent from '../../hooks/useElementEvent'
import { doNothing } from '../../utils'

type Props<TView> = {
views: readonly { readonly name: TView, readonly content: ReactNode }[],
currentView: TView,
}

function MultiView<TView>({ views, currentView }: Props<TView>) {
const [refs, setRefs] = useState<Array<HTMLElement | null>>(views.map(() => null))
const scrollHeights = useMemo(() => refs.map(ref => ref?.scrollTop ?? 0), [refs])

const setRef = (index: number) => (ref: HTMLElement | null) => {
const newRefs = [...refs]
newRefs[index] = ref
setRefs(newRefs)
}

return (
<div className='multi-view' style={{
'--view-count': views.length,
'--current-view': views.findIndex(e => e.name === currentView)
} as CSSProperties}>
<div className='sliding-container'>
{views.map(({ name, content }) =>
<div className='slide' key={String(name)}>
<Slide key={String(name)}>
{content}
</div>)}
</Slide>)}
</div>
</div>
)
}

const Slide: FC<{ children: ReactNode }> = React.memo(({ children }) => {
const [ref, setRef] = useState<HTMLElement | null>(null)
const [scrollTop, setScrollTop] = useState(0)
const [scrollHeight, setScrollHeight] = useState(0)
const scrollToTop = useCallback(() =>
ref?.scrollTo({ top: 0, behavior: 'smooth' })
, [ref])

useElementEvent(ref, 'scroll', (_, ref) => setScrollTop(ref.scrollTop))
useElementEvent(ref, 'resize', (_, ref) => setScrollHeight(ref.scrollHeight))

const contextValue = useMemo(() => ({ scrollTop, scrollHeight, scrollToTop } as const), [scrollHeight, scrollToTop, scrollTop])

return (
<div className='slide' ref={setRef}>
<SlideScrollContext.Provider value={contextValue}>
{children}
</SlideScrollContext.Provider>
</div>
)
})

const SlideScrollContext = React.createContext({ scrollTop: 0, scrollHeight: 0, scrollToTop: doNothing })

export function useSlideScroll() {
return useContext(SlideScrollContext)
}

export default React.memo(MultiView) as typeof MultiView
13 changes: 13 additions & 0 deletions front-end/src/hooks/useElementEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useEffect } from 'react'

import { Maybe } from '../../../back-end/types/misc'

export default function useElementEvent<E extends HTMLElement, K extends keyof HTMLElementEventMap>(ref: Maybe<E>, type: K, listener: (this: void, ev: HTMLElementEventMap[K], ref: E) => unknown) {
useEffect(() => {
if (ref) {
const handle = (e: HTMLElementEventMap[K]) => listener(e, ref)
ref.addEventListener(type, handle)
return () => ref.removeEventListener(type, handle)
}
}, [listener, ref, type])
}
2 changes: 1 addition & 1 deletion front-end/src/hooks/useLocationHash.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, useState } from 'react'

import { useWindowEvent } from './utils'
import useWindowEvent from './useWindowEvent'

export default function useLocationHash() {
const [hash, setHash] = useState(window.location.hash)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect } from 'react'

export function useWindowEvent<K extends keyof WindowEventMap>(type: K, listener: (this: Window, ev: WindowEventMap[K]) => unknown) {
export default function useWindowEvent<K extends keyof WindowEventMap>(type: K, listener: (this: Window, ev: WindowEventMap[K]) => unknown) {
useEffect(() => {
window.addEventListener(type, listener)
return () => window.removeEventListener(type, listener)
Expand Down
2 changes: 1 addition & 1 deletion front-end/src/hooks/useWindowSize.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, useState } from 'react'

import { useWindowEvent } from './utils'
import useWindowEvent from './useWindowEvent'

export default function useWindowSize() {
const [size, setSize] = useState(getSize())
Expand Down
2 changes: 1 addition & 1 deletion front-end/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es6",
"lib": [
"dom",
"dom.iterable",
Expand Down

0 comments on commit ef12c65

Please sign in to comment.