diff --git a/src/components/ResizeHandler.tsx b/src/components/ResizeHandler.tsx index 43e5b65..790949c 100644 --- a/src/components/ResizeHandler.tsx +++ b/src/components/ResizeHandler.tsx @@ -2,7 +2,7 @@ import { GrabberIcon } from '@primer/octicons-react' import { Icon } from 'components/Icon' import * as React from 'react' import { ResizeState, useResizeHandler } from '../utils/hooks/useResizeHandler' -import { Size2D } from './SideBarBodyWrapper' +import { Size2D } from "./Size" type Props = { size: Size2D diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx index 044a1d3..ea50216 100644 --- a/src/components/SideBar.tsx +++ b/src/components/SideBar.tsx @@ -4,16 +4,20 @@ import { FileExplorer } from 'components/FileExplorer' import { Footer } from 'components/Footer' import { MetaBar } from 'components/MetaBar' import { Portal } from 'components/Portal' -import { SideBarBodyWrapper } from 'components/SideBarBodyWrapper' import { ToggleShowButton } from 'components/ToggleShowButton' import { useConfigs } from 'containers/ConfigsContext' import { platform } from 'platforms' import * as React from 'react' import { IIFC } from 'react-iifc' +import { useWindowSize } from 'react-use' import { cx } from 'utils/cx' import * as DOMHelper from 'utils/DOMHelper' +import * as features from 'utils/features' +import { detectBrowser } from 'utils/general' +import { useConditionalHook } from 'utils/hooks/useConditionalHook' import { useLoadedContext } from 'utils/hooks/useLoadedContext' import { useOnPJAXDone, usePJAX } from 'utils/hooks/usePJAX' +import { ResizeState } from 'utils/hooks/useResizeHandler' import * as keyHelper from 'utils/keyHelper' import { SideBarErrorContext } from '../containers/ErrorContext' import { SideBarStateContext } from '../containers/SideBarState' @@ -21,17 +25,33 @@ import { Theme } from '../containers/Theme' import { LoadingIndicator } from './LoadingIndicator' import { RoundIconButton } from './RoundIconButton' import { SettingsBarContent } from './settings/SettingsBar' +import { SideBarResizeHandler } from './SideBarResizeHandler' export function SideBar() { usePJAX() platform.usePlatformHooks?.() useMarkGitakoReadyState() + const error = useLoadedContext(SideBarErrorContext).value + const [shouldExpand, setShouldExpand, toggleShowSideBar] = useShouldExpand() - const error = useLoadedContext(SideBarErrorContext).value const configContext = useConfigs() + + const blockLeaveRef = React.useRef(false) const { sidebarToggleMode } = configContext.value + const onMouseLeaveSideBar: React.MouseEventHandler = React.useCallback(() => { + if (blockLeaveRef.current) return + if (sidebarToggleMode === 'float') setShouldExpand(false) + }, [sidebarToggleMode, setShouldExpand]) + const onResizeStateChange = React.useCallback((state: ResizeState) => { + blockLeaveRef.current = state === 'resizing' + }, []) + + const heightForSafari = useConditionalHook( + () => detectBrowser() === 'Safari', + () => useWindowSize().height, // eslint-disable-line react-hooks/rules-of-hooks + ) return ( @@ -53,16 +73,17 @@ export function SideBar() { }}
- setShouldExpand(false) : undefined} + style={{ height: heightForSafari }} + onMouseLeave={onMouseLeaveSideBar} >
-
+
{sidebarToggleMode === 'persistent' && (
- + {features.resize && } +
) diff --git a/src/components/SideBarBodyWrapper.tsx b/src/components/SideBarBodyWrapper.tsx deleted file mode 100644 index 70879bb..0000000 --- a/src/components/SideBarBodyWrapper.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { ResizeHandler } from 'components/ResizeHandler' -import { useConfigs } from 'containers/ConfigsContext' -import * as React from 'react' -import { useDebounce, useWindowSize } from 'react-use' -import { getDefaultConfigs } from 'utils/config/helper' -import { cx } from 'utils/cx' -import * as DOMHelper from 'utils/DOMHelper' -import { setCSSVariable } from 'utils/DOMHelper' -import * as features from 'utils/features' -import { detectBrowser } from 'utils/general' -import { useOnPJAXDone } from 'utils/hooks/usePJAX' -import { ResizeState } from 'utils/hooks/useResizeHandler' -import { useConditionalHook } from '../utils/hooks/useConditionalHook' - -type Size = number -export type Size2D = [Size, Size] -type Props = { - className?: string - onLeave?: React.HTMLAttributes['onMouseLeave'] -} - -const MINIMAL_CONTENT_VIEWPORT_WIDTH = 100 -const MINIMAL_WIDTH = 240 - -function getSafeSize(size: number, width: number) { - if (size > width - MINIMAL_CONTENT_VIEWPORT_WIDTH) return width - MINIMAL_CONTENT_VIEWPORT_WIDTH - if (size < MINIMAL_WIDTH) return MINIMAL_WIDTH - return size -} - -const sizeVariableMountPoint = DOMHelper.gitakoDescriptionTarget - -export function SideBarBodyWrapper({ - className, - children, - onLeave, -}: React.PropsWithChildren) { - const configContext = useConfigs() - const { sideBarWidth: baseSize } = configContext.value - const [size, setSize] = React.useState(baseSize) - - // TODO: fix sidebar flash on first load - - // TODO: verify if it is required - // React.useEffect(() => { - // setSize(baseSize) - // }, [baseSize]) - - const heightForSafari = useConditionalHook( - () => detectBrowser() === 'Safari', - () => useWindowSize().height, // eslint-disable-line react-hooks/rules-of-hooks - ) - - const { width } = useWindowSize() - React.useEffect(() => { - const safeSize = getSafeSize(size, width) - if (safeSize !== size) setSize(safeSize) - }, [width, size]) - useDebounce(() => configContext.onChange({ sideBarWidth: size }), 100, [size]) - - const applySizeToCSSVariables = React.useCallback((size: number) => { - setCSSVariable('--gitako-width', `${size}px`, sizeVariableMountPoint) - }, []) - - // Update size using useEffect would cause delay - const onResize = React.useMemo(() => { - let sizeToApply: number - let applied = true - return ([size]: number[]) => { - // do NOT merge this with the above similar effect, side bar will jump otherwise - sizeToApply = getSafeSize(size, width) - setSize(sizeToApply) - - if (applied) { - applied = false - requestAnimationFrame(() => { - applied = true - applySizeToCSSVariables(sizeToApply) - }) - } - } - }, [width, applySizeToCSSVariables]) - - const applyLatestSizeToCSSVariables = React.useCallback( - () => applySizeToCSSVariables(size), - [applySizeToCSSVariables, size], - ) - React.useLayoutEffect(applyLatestSizeToCSSVariables, [applyLatestSizeToCSSVariables]) - useOnPJAXDone(applyLatestSizeToCSSVariables) - - const blockLeaveRef = React.useRef(false) - const onMouseLeave = React.useCallback( - (e: React.MouseEvent) => { - if (blockLeaveRef.current) return - onLeave?.(e) - }, - [onLeave], - ) - const onResizeStateChange = React.useCallback((state: ResizeState) => { - blockLeaveRef.current = state === 'resizing' - }, []) - - const dummySize: [number, number] = React.useMemo(() => [size, size], [size]) - - const defaultSideBarWidth = React.useMemo(() => getDefaultConfigs().sideBarWidth, []) - - return ( -
- {children} - {features.resize && ( - { - setSize(defaultSideBarWidth) - applySizeToCSSVariables(defaultSideBarWidth) - }} - onResizeStateChange={onResizeStateChange} - size={dummySize} - /> - )} -
- ) -} diff --git a/src/components/SideBarResizeHandler.tsx b/src/components/SideBarResizeHandler.tsx new file mode 100644 index 0000000..83a0383 --- /dev/null +++ b/src/components/SideBarResizeHandler.tsx @@ -0,0 +1,85 @@ +import { useConfigs } from 'containers/ConfigsContext' +import * as React from 'react' +import { useDebounce, useWindowSize } from 'react-use' +import { getDefaultConfigs } from 'utils/config/helper' +import * as DOMHelper from 'utils/DOMHelper' +import { useOnPJAXDone } from 'utils/hooks/usePJAX' +import { ResizeHandler } from './ResizeHandler' +import { Size, Size2D } from './Size' + +const MINIMAL_CONTENT_VIEWPORT_WIDTH = 100 +const MINIMAL_WIDTH = 240 + +function getSafeWidth(width: Size, windowWidth: number) { + if (width > windowWidth - MINIMAL_CONTENT_VIEWPORT_WIDTH) + return windowWidth - MINIMAL_CONTENT_VIEWPORT_WIDTH + if (width < MINIMAL_WIDTH) return MINIMAL_WIDTH + return width +} + +function useSidebarWidth() { + const configContext = useConfigs() + + // size data flow: + // windowSize.width => width + // width => config.sideBarWidth + // width => --gitako-width // layout effect + // resize event => width + // resize event => --gitako-width // rAF + const [width, setWidth] = React.useState(configContext.value.sideBarWidth) + const { width: windowWidth } = useWindowSize() + React.useEffect(() => { + const safeSize = getSafeWidth(width, windowWidth) + if (safeSize !== width) setWidth(safeSize) + }, [windowWidth, width]) + useDebounce(() => configContext.onChange({ sideBarWidth: width }), 100, [width]) + + React.useLayoutEffect(() => DOMHelper.setGitakoWidthCSSVariable(width), [width]) + + // Keep variable when directing from PR to repo home via meta bar + useOnPJAXDone(React.useCallback(() => DOMHelper.setGitakoWidthCSSVariable(width), [width])) + + return [width, setWidth] as const +} + +export function SideBarResizeHandler({ + onResizeStateChange, +}: Pick, 'onResizeStateChange'>) { + const [width, setWidth] = useSidebarWidth() + const { width: windowWidth } = useWindowSize() + const onResize = React.useMemo(() => { + let widthToApply: Size + let pending = false + return ([width]: Size2D) => { + // do NOT merge this with the above similar effect + widthToApply = width + + if (!pending) { + pending = true + // Update size using useEffect would cause delay + requestAnimationFrame(() => { + pending = false + widthToApply = getSafeWidth(widthToApply, windowWidth) + DOMHelper.setGitakoWidthCSSVariable(widthToApply) + setWidth(widthToApply) + }) + } + } + }, [windowWidth, setWidth]) + + const onResetSize = React.useCallback( + () => setWidth(getDefaultConfigs().sideBarWidth), + [setWidth], + ) + + const dummySize: Size2D = React.useMemo(() => [width, 0], [width]) + + return ( + + ) +} diff --git a/src/components/Size.tsx b/src/components/Size.tsx new file mode 100644 index 0000000..deba4a8 --- /dev/null +++ b/src/components/Size.tsx @@ -0,0 +1,2 @@ +export type Size = number; +export type Size2D = [Size, Size]; diff --git a/src/components/ToggleShowButton.tsx b/src/components/ToggleShowButton.tsx index 180477f..5e23d19 100644 --- a/src/components/ToggleShowButton.tsx +++ b/src/components/ToggleShowButton.tsx @@ -41,7 +41,7 @@ export function ToggleShowButton({ error, className, onClick, onHover }: Props) ) // reposition on window height change, but ignores distance change - React.useEffect(() => { + React.useLayoutEffect(() => { if (ref.current) { ref.current.style.top = distance + 'px' } diff --git a/src/styles/index.scss b/src/styles/index.scss index beafa12..337ec9a 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -467,7 +467,7 @@ html[data-with-gitako-spacing='true'] { } } - .close-side-bar-button-position { + .side-bar-position-controls { float: right; z-index: 1; // prevent being covered by following elements } diff --git a/src/utils/DOMHelper.ts b/src/utils/DOMHelper.ts index 409a629..625fb3d 100644 --- a/src/utils/DOMHelper.ts +++ b/src/utils/DOMHelper.ts @@ -166,6 +166,10 @@ export function setCSSVariable(name: string, value: string | undefined, element: else element.style.setProperty(name, value) } +export const setGitakoWidthCSSVariable = (size: number) => { + setCSSVariable('--gitako-width', `${size}px`, gitakoDescriptionTarget) +} + export function formatID(id: string) { return `#${id}` } diff --git a/src/utils/hooks/useCSSVariable.ts b/src/utils/hooks/useCSSVariable.ts index 889a774..ae86814 100644 --- a/src/utils/hooks/useCSSVariable.ts +++ b/src/utils/hooks/useCSSVariable.ts @@ -1,4 +1,5 @@ import { useLayoutEffect } from 'react' +import { setCSSVariable } from 'utils/DOMHelper' export function useCSSVariable( name: string, @@ -6,6 +7,6 @@ export function useCSSVariable( element: HTMLElement = document.documentElement, ) { useLayoutEffect(() => { - element.style.setProperty(name, value) + setCSSVariable(name, value, element) }, [name, value, element]) } diff --git a/src/utils/hooks/useElementSize.ts b/src/utils/hooks/useElementSize.ts index ef87f22..10d3d90 100644 --- a/src/utils/hooks/useElementSize.ts +++ b/src/utils/hooks/useElementSize.ts @@ -1,6 +1,6 @@ import * as React from 'react' import * as features from 'utils/features' -import { Size2D } from '../../components/SideBarBodyWrapper' +import { Size2D } from "../../components/Size" export function useElementSize() { const ref = React.useRef(null) diff --git a/src/utils/hooks/useResizeHandler.ts b/src/utils/hooks/useResizeHandler.ts index 9c937d4..c123bfd 100644 --- a/src/utils/hooks/useResizeHandler.ts +++ b/src/utils/hooks/useResizeHandler.ts @@ -1,5 +1,5 @@ import * as React from 'react' -import { Size2D } from '../../components/SideBarBodyWrapper' +import { Size2D } from "../../components/Size" export type ResizeState = 'idle' | 'resizing'