feat: optimize resize and enable drag on Firefox

This commit is contained in:
EnixCoda 2021-12-23 14:02:27 +08:00
parent 86a030f308
commit 193a8f48a1
3 changed files with 102 additions and 85 deletions

View file

@ -1,10 +1,10 @@
import { Icon } from 'components/Icon'
import * as React from 'react'
import { Size } from './SideBarBodyWrapper'
import { Size2D } from './SideBarBodyWrapper'
type Props = {
size: Size
onResize(size: Size): void
size: Size2D
onResize(size: Size2D): void
onResetSize?(): void
onResizeStateChange?(state: ResizeState): void
style?: React.CSSProperties
@ -12,55 +12,13 @@ type Props = {
type ResizeState = 'idle' | 'resizing'
export function HorizontalResizeHandler({
onResize,
onResetSize,
onResizeStateChange,
size,
style,
}: Props) {
const pointerDown = React.useRef(false)
const startX = React.useRef(0)
const baseSize = React.useRef(size)
const latestPropSize = React.useRef(size)
React.useEffect(() => {
latestPropSize.current = size
}, [size])
const onPointerDown = React.useCallback(({ clientX }: React.MouseEvent) => {
startX.current = clientX
pointerDown.current = true
baseSize.current = latestPropSize.current
onResizeStateChange?.('resizing')
}, [])
React.useEffect(() => {
const onPointerMove = ({ clientX }: MouseEvent) => {
if (!pointerDown.current) return
const shift = clientX - startX.current
onResize(baseSize.current + shift)
}
window.addEventListener('mousemove', onPointerMove)
return () => window.removeEventListener('mousemove', onPointerMove)
}, [onResize])
React.useEffect(() => {
const onPointerUp = () => {
if (pointerDown.current) {
pointerDown.current = false
baseSize.current = latestPropSize.current
onResizeStateChange?.('idle')
}
}
window.addEventListener('mouseup', onPointerUp)
return () => window.removeEventListener('mouseup', onPointerUp)
}, [])
export function ResizeHandler({ onResize, onResetSize, onResizeStateChange, size, style }: Props) {
const { onPointerDown } = useResizeHandler(size, onResize, { onResizeStateChange })
return (
<div
className={'gitako-resize-handler'}
onMouseDown={onPointerDown}
onPointerDown={onPointerDown}
onDoubleClick={onResetSize}
style={style}
>
@ -68,3 +26,61 @@ export function HorizontalResizeHandler({
</div>
)
}
export function useResizeHandler(
size: Size2D,
onResize: (size: Size2D) => void,
{
onResizeStateChange,
onClick,
}: Partial<{
onResizeStateChange: (state: ResizeState) => void
onClick: (e: PointerEvent) => void
}> = {},
) {
const pointerDown = React.useRef(false)
const pointerMoved = React.useRef(false)
const initialSizeRef = React.useRef([0, 0])
const baseSize = React.useRef(size)
const latestPropSize = React.useRef(size)
React.useEffect(() => {
latestPropSize.current = size
}, [size])
React.useEffect(() => {
const onPointerMove = ({ clientX, clientY }: PointerEvent) => {
if (!pointerDown.current) return
pointerMoved.current = true
const [x, y] = baseSize.current
onResize([x + clientX - initialSizeRef.current[0], y + clientY - initialSizeRef.current[1]])
}
window.addEventListener('pointermove', onPointerMove)
return () => window.removeEventListener('pointermove', onPointerMove)
}, [onResize])
React.useEffect(() => {
const onPointerUp = (e: PointerEvent) => {
if (pointerDown.current) {
pointerDown.current = false
if (!pointerMoved.current) onClick?.(e)
pointerMoved.current = false
baseSize.current = latestPropSize.current
onResizeStateChange?.('idle')
}
}
window.addEventListener('pointerup', onPointerUp)
return () => window.removeEventListener('pointerup', onPointerUp)
}, [])
const onPointerDown = React.useCallback(({ clientX, clientY }: React.PointerEvent) => {
pointerDown.current = true
initialSizeRef.current = [clientX, clientY]
baseSize.current = latestPropSize.current
onResizeStateChange?.('resizing')
}, [])
return { onPointerDown }
}

View file

@ -1,4 +1,4 @@
import { HorizontalResizeHandler } from 'components/ResizeHandler'
import { ResizeHandler } from 'components/ResizeHandler'
import { useConfigs } from 'containers/ConfigsContext'
import * as React from 'react'
import { useDebounce, useWindowSize } from 'react-use'
@ -7,7 +7,8 @@ import { cx } from 'utils/cx'
import { setCSSVariable } from 'utils/DOMHelper'
import * as features from 'utils/features'
export type Size = number
type Size = number
export type Size2D = [Size, Size]
type Props = {
baseSize: Size
className?: string
@ -67,7 +68,7 @@ export function SideBarBodyWrapper({
const onResize = React.useMemo(() => {
let sizeToApply: number,
applied = true
return (size: number) => {
return ([size]: number[]) => {
// do NOT merge this with the above similar effect, side bar will jump otherwise
sizeToApply = getSafeSize(size, width)
setSize(sizeToApply)
@ -94,6 +95,7 @@ export function SideBarBodyWrapper({
[onLeave],
)
const newLocal: [number, number] = React.useMemo(() => [size, size], [size])
return (
<div
ref={bodyWrapperRef}
@ -102,7 +104,7 @@ export function SideBarBodyWrapper({
>
<div className={'gitako-side-bar-body-wrapper-content'}>{children}</div>
{features.resize && (
<HorizontalResizeHandler
<ResizeHandler
onResize={onResize}
onResetSize={() => {
setSize(defaultConfigs.sideBarWidth)
@ -111,7 +113,7 @@ export function SideBarBodyWrapper({
onResizeStateChange={state => {
blockLeaveRef.current = state === 'resizing'
}}
size={size}
size={newLocal}
/>
)}
</div>

View file

@ -4,30 +4,31 @@ import * as React from 'react'
import { useDebounce, useWindowSize } from 'react-use'
import { cx } from 'utils/cx'
import { Icon } from './Icon'
import { useResizeHandler } from './ResizeHandler'
type Props = {
error?: string | null
className?: React.HTMLAttributes<HTMLButtonElement>['className']
onHover?: React.HTMLAttributes<HTMLButtonElement>['onMouseEnter']
} & Pick<React.HTMLAttributes<HTMLButtonElement>, 'onClick'>
onClick?: (e: PointerEvent) => void
}
const buttonHeight = 42
function getSafeDistance(y: number, height: number) {
return Math.max(0, Math.min(y, height - buttonHeight))
}
export function ToggleShowButton({ error, className, onClick, onHover }: Props) {
const ref = React.useRef<HTMLDivElement>(null)
const config = useConfigs()
const [distance, setDistance] = React.useState(config.value.toggleButtonVerticalDistance)
const { height } = useWindowSize()
const buttonHeight = 42
React.useEffect(() => {
// make sure it is inside viewport
if (height - buttonHeight < distance) {
setDistance(Math.max(0, height - buttonHeight))
}
const safeDistance = getSafeDistance(distance, height)
if (safeDistance !== distance) setDistance(safeDistance)
}, [height, distance])
React.useLayoutEffect(() => {
if (ref.current) {
ref.current.style.top = distance + 'px'
}
}, [distance])
// updating context
useDebounce(
@ -36,28 +37,34 @@ export function ToggleShowButton({ error, className, onClick, onHover }: Props)
[distance],
)
const toggleIconMode = config.value.toggleButtonContent
React.useEffect(() => {
if (ref.current) {
ref.current.style.top = distance + 'px'
}
}, [])
const { onPointerDown } = useResizeHandler(
[distance, distance],
([, y]) => {
const distance = getSafeDistance(y, height)
setDistance(distance)
if (ref.current) {
ref.current.style.top = distance + 'px'
}
},
{ onClick },
)
return (
<div ref={ref} className={cx('gitako-toggle-show-button-wrapper', className)}>
<button
className={cx('gitako-toggle-show-button', {
error,
})}
onClick={onClick}
onMouseEnter={onHover}
draggable
onDragStart={event => {
hideDragPreview(event)
}}
onDrag={e => {
if (e.clientY !== 0) {
// It will be 0 when release pointer
setDistance(e.clientY - buttonHeight / 2)
}
}}
onPointerEnter={onHover}
onPointerDown={onPointerDown}
title={'Gitako (draggable)'}
>
{toggleIconMode === 'octoface' ? (
{config.value.toggleButtonContent === 'octoface' ? (
<Icon className={'octoface-icon'} type={'octoface'} />
) : (
<img className={'tentacle'} draggable={false} src={iconURL} />
@ -67,11 +74,3 @@ export function ToggleShowButton({ error, className, onClick, onHover }: Props)
</div>
)
}
function hideDragPreview(event: React.DragEvent<HTMLButtonElement>) {
const img = new Image()
const EMPTY_IMAGE_BASE64 =
'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='
img.src = EMPTY_IMAGE_BASE64
event.dataTransfer.setDragImage(img, 0, 0)
}