Calculate topbar and composer height dynamically (#362)

* dynamically calculate topbar and composer height

* simplify scroll position calculation

* update AnchorActions component

* update initial content scroll position

* add if condition
This commit is contained in:
Pavel 2023-03-31 16:45:43 +02:00 committed by GitHub
parent dda3cf1dfe
commit 00f97e4d3b
No known key found for this signature in database
GPG Key ID: 0EB8D75C775AB6F1
9 changed files with 135 additions and 157 deletions

View File

@ -15,7 +15,8 @@
"@tamagui/core": "1.7.7", "@tamagui/core": "1.7.7",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-native-web": "^0.18.12" "react-native-web": "^0.18.12",
"use-resize-observer": "^9.1.0"
}, },
"devDependencies": { "devDependencies": {
"@tamagui/vite-plugin": "1.7.7", "@tamagui/vite-plugin": "1.7.7",

View File

@ -1,4 +1,4 @@
import { useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { import {
AnchorActions, AnchorActions,
@ -11,7 +11,9 @@ import {
useAppDispatch, useAppDispatch,
useAppState, useAppState,
} from '@status-im/components' } from '@status-im/components'
import { useBlur } from '@status-im/components/hooks' import useResizeObserver from 'use-resize-observer'
import { useScrollPosition } from './hooks/use-scroll-position'
const COMMUNITY = { const COMMUNITY = {
name: 'Rarible', name: 'Rarible',
@ -22,15 +24,13 @@ const COMMUNITY = {
'https://images.unsplash.com/photo-1574786527860-f2e274867c91?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1764&q=80', 'https://images.unsplash.com/photo-1574786527860-f2e274867c91?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1764&q=80',
} }
const updateProperty = (property: string, value: number) => {
document.documentElement.style.setProperty(property, `${value}px`)
}
function App() { function App() {
const [showMembers, setShowMembers] = useState(false) const [showMembers, setShowMembers] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const { shouldBlurTop, shouldBlurBottom } = useBlur({
ref: containerRef,
})
const appState = useAppState() const appState = useAppState()
const appDispatch = useAppDispatch() const appDispatch = useAppDispatch()
@ -45,6 +45,32 @@ function App() {
} }
}, [appState.channelId]) }, [appState.channelId])
const topbarRef = useRef<HTMLDivElement>(null)
const contentRef = useRef<HTMLDivElement>(null)
const composerRef = useRef<HTMLDivElement>(null)
useResizeObserver<HTMLDivElement>({
ref: topbarRef,
onResize({ height }) {
updateProperty('--topbar-height', height)
},
})
useResizeObserver<HTMLDivElement>({
ref: composerRef,
onResize({ height }) {
updateProperty('--composer-height', height)
},
})
const scrollPosition = useScrollPosition({
ref: contentRef,
})
useEffect(() => {
contentRef.current.scrollTop = contentRef.current.scrollHeight
}, [selectedChannel])
return ( return (
<div id="app"> <div id="app">
<div id="sidebar" style={{ zIndex: 200 }}> <div id="sidebar" style={{ zIndex: 200 }}>
@ -58,23 +84,28 @@ function App() {
</div> </div>
<main id="main"> <main id="main">
<div id="topbar" ref={topbarRef}>
<Topbar <Topbar
blur={shouldBlurTop} blur={scrollPosition !== 'top'}
channel={selectedChannel} channel={selectedChannel}
showMembers={showMembers} showMembers={showMembers}
onMembersPress={() => setShowMembers(show => !show)} onMembersPress={() => setShowMembers(show => !show)}
/> />
</div>
<div id="content" ref={containerRef}> <div id="content" ref={contentRef}>
<div id="messages"> <div id="messages">
<Messages /> <Messages />
</div> </div>
<div id="composer"> </div>
<div id="composer" ref={composerRef}>
{scrollPosition !== 'bottom' && (
<div id="anchor-actions"> <div id="anchor-actions">
<AnchorActions scrolled={shouldBlurBottom} /> <AnchorActions />
</div>
<Composer blur={shouldBlurBottom} />
</div> </div>
)}
<Composer blur={scrollPosition !== 'bottom'} />
</div> </div>
</main> </main>

View File

@ -0,0 +1,49 @@
import { useEffect, useRef, useState } from 'react'
type Position = 'top' | 'middle' | 'bottom'
type Options = {
ref: React.RefObject<HTMLElement>
}
export function useScrollPosition(options: Options) {
const { ref } = options
const [position, setPosition] = useState<Position>('bottom')
const positionRef = useRef(position)
// Using ref for storing position because don't want to recreate the event listener
positionRef.current = position
useEffect(() => {
const node = ref.current
const handleScroll = () => {
if (!node) return
const { scrollTop, scrollHeight, clientHeight } = node
if (scrollTop === 0) {
setPosition('top')
return
}
if (scrollTop + clientHeight === scrollHeight) {
setPosition('bottom')
return
}
if (positionRef.current !== 'middle') {
setPosition('middle')
}
}
node.addEventListener('scroll', handleScroll, { passive: true })
return () => {
node.removeEventListener('scroll', handleScroll)
}
}, [ref])
return position
}

View File

@ -1,3 +1,8 @@
:root {
--topbar-height: 56px;
--composer-height: 100px;
}
html, html,
body, body,
#root { #root {
@ -19,37 +24,30 @@ body,
#main { #main {
position: relative; position: relative;
display: grid;
grid-template-rows: 96px 1fr 100px; /* 56px 1fr 100px without pinned messages */
height: 100vh; height: 100vh;
} }
/* #main,
#sidebar,
#members,
#main > div {
border: 1px solid rgba(0, 0, 0, 0.1);
} */
.composer-input::placeholder {
transition: color 0.2s ease-in-out;
}
#sidebar { #sidebar {
overflow: auto; overflow: auto;
height: 100vh; height: 100vh;
} }
#topbar {
position: absolute;
inset: 0 0 auto;
z-index: 100;
}
#content { #content {
position: relative; position: relative;
overflow: auto; overflow: auto;
padding: 40px 0px 0px 0px; padding-top: var(--topbar-height);
padding-bottom: var(--composer-height);
height: 100vh; height: 100vh;
margin-top: -96px; /* -56px without pinned messages */
} }
#messages { #messages {
padding: 32px 8px; padding: 8px;
} }
#anchor-actions { #anchor-actions {
@ -59,9 +57,8 @@ body,
} }
#composer { #composer {
position: sticky; position: absolute;
bottom: 0; inset: auto 0 0;
left: 0;
z-index: 100; z-index: 100;
} }

View File

@ -1,3 +1 @@
export { useBlur } from './use-blur'
export { useImageUpload } from './use-image-uploader' export { useImageUpload } from './use-image-uploader'
export { useThrottle } from './use-throttle'

View File

@ -1,68 +0,0 @@
import { useEffect, useState } from 'react'
import { useThrottle } from './use-throttle'
interface UseBlurReturn {
shouldBlurBottom: boolean
shouldBlurTop: boolean
}
type UseBlurProps = {
ref: React.RefObject<HTMLDivElement>
marginBlurBottom?: number
heightTop?: number
throttle?: number
}
const useBlur = (props: UseBlurProps): UseBlurReturn => {
const {
marginBlurBottom = 32,
heightTop = 96,
throttle = 100,
ref,
} = props || {}
const [shouldBlurTop, setShouldBlurTop] = useState(false)
const [shouldBlurBottom, setShouldBlurBottom] = useState(false)
const handleScroll = useThrottle(() => {
const scrollPosition = ref.current!.scrollTop
const elementHeight = ref.current!.clientHeight
const scrollHeight = ref.current?.scrollHeight || 0
if (scrollPosition >= heightTop) {
setShouldBlurTop(true)
}
if (scrollPosition < heightTop) {
setShouldBlurTop(false)
}
if (scrollPosition < scrollHeight - (elementHeight + marginBlurBottom)) {
setShouldBlurBottom(true)
}
if (scrollPosition >= scrollHeight - (elementHeight + marginBlurBottom)) {
setShouldBlurBottom(false)
}
}, throttle)
useEffect(() => {
const element = props.ref
if (!element.current) {
throw new Error('useBlur ref not set correctly')
}
element.current!.addEventListener('scroll', handleScroll, { passive: true })
handleScroll()
return () => {
element.current!.removeEventListener('scroll', handleScroll)
}
}, [handleScroll, props.ref])
return { shouldBlurBottom, shouldBlurTop }
}
export { useBlur }

View File

@ -1,38 +0,0 @@
import { useMemo } from 'react'
const throttle = <Args extends unknown[]>(
fn: (...args: Args) => void,
cooldown: number
) => {
let lastArgs: Args | undefined
const run = () => {
if (lastArgs) {
fn(...lastArgs)
lastArgs = undefined
}
}
const throttled = (...args: Args) => {
const isOnCooldown = !!lastArgs
lastArgs = args
if (isOnCooldown) {
return
}
window.setTimeout(run, cooldown)
}
return throttled
}
const useThrottle = <Args extends unknown[]>(
cb: (...args: Args) => void,
cooldown: number
) => {
return useMemo(() => throttle(cb, cooldown), [cb, cooldown])
}
export { useThrottle }

View File

@ -2,17 +2,13 @@ import { Stack } from 'tamagui'
import { DynamicButton } from '../dynamic-button' import { DynamicButton } from '../dynamic-button'
type Props = { // type Props = {}
scrolled: boolean
}
const AnchorActions = (props: Props) => {
const { scrolled } = props
const AnchorActions = () => {
return ( return (
<Stack flexDirection="row" space={8}> <Stack flexDirection="row" space={8}>
{scrolled && <DynamicButton type="mention" count={1} />} <DynamicButton type="mention" count={1} />
{scrolled && <DynamicButton type="notification" count={0} />} <DynamicButton type="notification" count={0} />
</Stack> </Stack>
) )
} }

View File

@ -2648,6 +2648,11 @@
"@jridgewell/resolve-uri" "3.1.0" "@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14" "@jridgewell/sourcemap-codec" "1.4.14"
"@juggle/resize-observer@^3.3.1":
version "3.4.0"
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
"@leichtgewicht/base64-codec@^1.0.0": "@leichtgewicht/base64-codec@^1.0.0":
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/@leichtgewicht/base64-codec/-/base64-codec-1.0.0.tgz#f5d730be74bd41564cf23c6d332044ae88fc31d8" resolved "https://registry.yarnpkg.com/@leichtgewicht/base64-codec/-/base64-codec-1.0.0.tgz#f5d730be74bd41564cf23c6d332044ae88fc31d8"
@ -6359,7 +6364,7 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
"@types/react-dom@18.0.11", "@types/react-dom@^18.0.11": "@types/react-dom@^18.0.11":
version "18.0.11" version "18.0.11"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.11.tgz#321351c1459bc9ca3d216aefc8a167beec334e33" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.11.tgz#321351c1459bc9ca3d216aefc8a167beec334e33"
integrity sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw== integrity sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==
@ -6373,7 +6378,7 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react@*", "@types/react@18.0.28", "@types/react@>=16", "@types/react@^18.0.28": "@types/react@*", "@types/react@>=16", "@types/react@^18.0.28":
version "18.0.28" version "18.0.28"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065"
integrity sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew== integrity sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==
@ -16685,6 +16690,13 @@ use-latest-callback@^0.1.5:
resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.5.tgz#a4a836c08fa72f6608730b5b8f4bbd9c57c04f51" resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.5.tgz#a4a836c08fa72f6608730b5b8f4bbd9c57c04f51"
integrity sha512-HtHatS2U4/h32NlkhupDsPlrbiD27gSH5swBdtXbCAlc6pfOFzaj0FehW/FO12rx8j2Vy4/lJScCiJyM01E+bQ== integrity sha512-HtHatS2U4/h32NlkhupDsPlrbiD27gSH5swBdtXbCAlc6pfOFzaj0FehW/FO12rx8j2Vy4/lJScCiJyM01E+bQ==
use-resize-observer@^9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/use-resize-observer/-/use-resize-observer-9.1.0.tgz#14735235cf3268569c1ea468f8a90c5789fc5c6c"
integrity sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==
dependencies:
"@juggle/resize-observer" "^3.3.1"
use-sidecar@^1.1.2: use-sidecar@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2"