Refactor scrollarea component for overlayscrollbars
This commit is contained in:
parent
3d6f5a2748
commit
c3d8791455
9 changed files with 95 additions and 99 deletions
|
@ -1,6 +1,6 @@
|
||||||
import { useRef } from 'react';
|
|
||||||
import { Flex, FlexProps } from '@mantine/core';
|
import { Flex, FlexProps } from '@mantine/core';
|
||||||
import { AnimatePresence, motion, Variants } from 'framer-motion';
|
import { AnimatePresence, motion, Variants } from 'framer-motion';
|
||||||
|
import { useRef } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { useShouldPadTitlebar, useTheme } from '/@/renderer/hooks';
|
import { useShouldPadTitlebar, useTheme } from '/@/renderer/hooks';
|
||||||
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
||||||
|
@ -64,6 +64,7 @@ const BackgroundImageOverlay = styled.div<{ theme: 'light' | 'dark' }>`
|
||||||
|
|
||||||
export interface PageHeaderProps
|
export interface PageHeaderProps
|
||||||
extends Omit<FlexProps, 'onAnimationStart' | 'onDragStart' | 'onDragEnd' | 'onDrag'> {
|
extends Omit<FlexProps, 'onAnimationStart' | 'onDragStart' | 'onDragEnd' | 'onDrag'> {
|
||||||
|
animated?: boolean;
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
height?: string;
|
height?: string;
|
||||||
|
@ -79,12 +80,19 @@ const TitleWrapper = styled(motion.div)`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const variants: Variants = {
|
const variants: Variants = {
|
||||||
animate: { opacity: 1 },
|
animate: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
duration: 0.3,
|
||||||
|
ease: 'easeIn',
|
||||||
|
},
|
||||||
|
},
|
||||||
exit: { opacity: 0 },
|
exit: { opacity: 0 },
|
||||||
initial: { opacity: 0 },
|
initial: { opacity: 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageHeader = ({
|
export const PageHeader = ({
|
||||||
|
animated,
|
||||||
position,
|
position,
|
||||||
height,
|
height,
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
|
@ -109,17 +117,15 @@ export const PageHeader = ({
|
||||||
$isHidden={isHidden}
|
$isHidden={isHidden}
|
||||||
$padRight={padRight}
|
$padRight={padRight}
|
||||||
>
|
>
|
||||||
<AnimatePresence initial={false}>
|
<AnimatePresence initial={animated ?? false}>
|
||||||
{!isHidden && (
|
<TitleWrapper
|
||||||
<TitleWrapper
|
animate="animate"
|
||||||
animate="animate"
|
exit="exit"
|
||||||
exit="exit"
|
initial="initial"
|
||||||
initial="initial"
|
variants={variants}
|
||||||
variants={variants}
|
>
|
||||||
>
|
{children}
|
||||||
{children}
|
</TitleWrapper>
|
||||||
</TitleWrapper>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</Header>
|
</Header>
|
||||||
{backgroundColor && (
|
{backgroundColor && (
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { forwardRef, Ref, useEffect, useRef, useState } from 'react';
|
|
||||||
import type { ScrollAreaProps as MantineScrollAreaProps } from '@mantine/core';
|
import type { ScrollAreaProps as MantineScrollAreaProps } from '@mantine/core';
|
||||||
import { ScrollArea as MantineScrollArea } from '@mantine/core';
|
import { ScrollArea as MantineScrollArea } from '@mantine/core';
|
||||||
import { useMergedRef, useTimeout } from '@mantine/hooks';
|
import { useMergedRef } from '@mantine/hooks';
|
||||||
import { motion, useScroll } from 'framer-motion';
|
import { useInView } from 'framer-motion';
|
||||||
|
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||||
|
import { forwardRef, Ref, useEffect, useRef, useState } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { PageHeader, PageHeaderProps } from '/@/renderer/components/page-header';
|
import { PageHeader, PageHeaderProps } from '/@/renderer/components/page-header';
|
||||||
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
import { useWindowSettings } from '/@/renderer/store/settings.store';
|
||||||
|
@ -30,25 +31,6 @@ const StyledScrollArea = styled(MantineScrollArea)`
|
||||||
|
|
||||||
const StyledNativeScrollArea = styled.div<{ scrollBarOffset?: string; windowBarStyle?: Platform }>`
|
const StyledNativeScrollArea = styled.div<{ scrollBarOffset?: string; windowBarStyle?: Platform }>`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
margin-top: ${(props) =>
|
|
||||||
props.windowBarStyle === Platform.WINDOWS ||
|
|
||||||
props.windowBarStyle === Platform.MACOS ||
|
|
||||||
props.windowBarStyle === Platform.LINUX
|
|
||||||
? '0px'
|
|
||||||
: props.scrollBarOffset || '65px'};
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
margin-top: ${(props) =>
|
|
||||||
props.windowBarStyle === Platform.WINDOWS ||
|
|
||||||
props.windowBarStyle === Platform.MACOS ||
|
|
||||||
props.windowBarStyle === Platform.LINUX
|
|
||||||
? '0px'
|
|
||||||
: props.scrollBarOffset || '65px'};
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ScrollArea = forwardRef(({ children, ...props }: ScrollAreaProps, ref: Ref<any>) => {
|
export const ScrollArea = forwardRef(({ children, ...props }: ScrollAreaProps, ref: Ref<any>) => {
|
||||||
|
@ -87,90 +69,84 @@ export const NativeScrollArea = forwardRef(
|
||||||
ref: Ref<HTMLDivElement>,
|
ref: Ref<HTMLDivElement>,
|
||||||
) => {
|
) => {
|
||||||
const { windowBarStyle } = useWindowSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
const [hideScrollbar, setHideScrollbar] = useState(false);
|
|
||||||
const [hideHeader, setHideHeader] = useState(true);
|
|
||||||
const { start, clear } = useTimeout(
|
|
||||||
() => setHideScrollbar(true),
|
|
||||||
scrollHideDelay !== undefined ? scrollHideDelay * 1000 : 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
const mergedRef = useMergedRef(ref, containerRef);
|
const [isPastOffset, setIsPastOffset] = useState(false);
|
||||||
|
|
||||||
const { scrollYProgress } = useScroll({
|
// useInView initializes as false, so we need to track this to properly render the header
|
||||||
container: containerRef,
|
const isInViewInitializedRef = useRef<boolean | null>(null);
|
||||||
offset: pageHeaderProps?.offset || ['center start', 'end start'],
|
|
||||||
target: pageHeaderProps?.target,
|
const isInView = useInView({
|
||||||
|
current: pageHeaderProps?.target?.current,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Automatically hide the scrollbar after the timeout duration
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
start();
|
if (!isInViewInitializedRef.current && isInView) {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
isInViewInitializedRef.current = true;
|
||||||
}, []);
|
}
|
||||||
|
}, [isInView]);
|
||||||
|
|
||||||
|
const [initialize] = useOverlayScrollbars({
|
||||||
|
defer: true,
|
||||||
|
|
||||||
|
events: {
|
||||||
|
scroll: (_instance, e) => {
|
||||||
|
if (!pageHeaderProps?.offset) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = pageHeaderProps?.offset;
|
||||||
|
const scrollTop = (e?.target as HTMLDivElement)?.scrollTop;
|
||||||
|
|
||||||
|
if (scrollTop > offset && isPastOffset === false) {
|
||||||
|
setIsPastOffset(true);
|
||||||
|
} else if (scrollTop <= offset && isPastOffset === true) {
|
||||||
|
setIsPastOffset(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
overflow: { x: 'hidden', y: 'scroll' },
|
||||||
|
scrollbars: {
|
||||||
|
autoHide: 'move',
|
||||||
|
autoHideDelay: 500,
|
||||||
|
pointers: ['mouse', 'pen', 'touch'],
|
||||||
|
theme: 'feishin',
|
||||||
|
visibility: 'visible',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const setHeaderVisibility = (v: number) => {
|
if (containerRef.current) {
|
||||||
if (v === 1) {
|
initialize(containerRef.current as HTMLDivElement);
|
||||||
return setHideHeader(false);
|
}
|
||||||
}
|
}, [initialize]);
|
||||||
|
|
||||||
if (hideHeader === false) {
|
// console.log('isPastOffsetRef.current', isPastOffsetRef.current);
|
||||||
return setHideHeader(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
const mergedRef = useMergedRef(ref, containerRef);
|
||||||
};
|
|
||||||
|
|
||||||
const unsubscribe = scrollYProgress.on('change', setHeaderVisibility);
|
const shouldShowHeader =
|
||||||
|
!noHeader && (isPastOffset || (isInViewInitializedRef.current && !isInView));
|
||||||
return () => {
|
|
||||||
unsubscribe();
|
|
||||||
};
|
|
||||||
}, [hideHeader, scrollYProgress]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!noHeader && (
|
{shouldShowHeader && (
|
||||||
<PageHeader
|
<PageHeader
|
||||||
isHidden={hideHeader}
|
animated
|
||||||
|
isHidden={false}
|
||||||
position="absolute"
|
position="absolute"
|
||||||
style={{ opacity: scrollYProgress as any }}
|
|
||||||
{...pageHeaderProps}
|
{...pageHeaderProps}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<StyledNativeScrollArea
|
<StyledNativeScrollArea
|
||||||
ref={mergedRef}
|
ref={mergedRef}
|
||||||
className={hideScrollbar ? 'hide-scrollbar' : undefined}
|
|
||||||
scrollBarOffset={scrollBarOffset}
|
scrollBarOffset={scrollBarOffset}
|
||||||
windowBarStyle={windowBarStyle}
|
windowBarStyle={windowBarStyle}
|
||||||
onMouseEnter={() => {
|
|
||||||
setHideScrollbar(false);
|
|
||||||
clear();
|
|
||||||
}}
|
|
||||||
onMouseLeave={() => {
|
|
||||||
start();
|
|
||||||
}}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</StyledNativeScrollArea>
|
</StyledNativeScrollArea>
|
||||||
{debugScrollPosition && (
|
|
||||||
<motion.div
|
|
||||||
style={{
|
|
||||||
background: 'red',
|
|
||||||
height: '10px',
|
|
||||||
left: 0,
|
|
||||||
position: 'fixed',
|
|
||||||
right: 0,
|
|
||||||
scaleX: scrollYProgress,
|
|
||||||
top: 0,
|
|
||||||
transformOrigin: '0%',
|
|
||||||
width: '100%',
|
|
||||||
zIndex: 5000,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -50,10 +50,6 @@ const DetailContainer = styled.div`
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 1rem 2rem 5rem;
|
padding: 1rem 2rem 5rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.ag-theme-alpine-dark {
|
|
||||||
--ag-header-background-color: rgba(0, 0, 0, 0%) !important;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface AlbumDetailContentProps {
|
interface AlbumDetailContentProps {
|
||||||
|
|
|
@ -139,7 +139,7 @@ const HomeRoute = () => {
|
||||||
<LibraryHeaderBar.Title>Home</LibraryHeaderBar.Title>
|
<LibraryHeaderBar.Title>Home</LibraryHeaderBar.Title>
|
||||||
</LibraryHeaderBar>
|
</LibraryHeaderBar>
|
||||||
),
|
),
|
||||||
offset: ['0px', '200px'],
|
offset: 200,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack
|
<Stack
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { App } from './app';
|
import { App } from './app';
|
||||||
import { queryClient } from './lib/react-query';
|
import { queryClient } from './lib/react-query';
|
||||||
|
import 'overlayscrollbars/overlayscrollbars.css';
|
||||||
|
|
||||||
const container = document.getElementById('root')! as HTMLElement;
|
const container = document.getElementById('root')! as HTMLElement;
|
||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.ag-header-fixed-margin {
|
.ag-header-fixed-margin {
|
||||||
margin-top: 43px !important;
|
margin-top: 36px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ag-header-cell-comp-wrapper {
|
.ag-header-cell-comp-wrapper {
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
@use '../themes/dark.scss';
|
@use '../themes/dark.scss';
|
||||||
@use '../themes/light.scss';
|
@use '../themes/light.scss';
|
||||||
@use './ag-grid.scss';
|
@use './ag-grid.scss';
|
||||||
|
@use './overlayscrollbars.scss';
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
15
src/renderer/styles/overlayscrollbars.scss
Normal file
15
src/renderer/styles/overlayscrollbars.scss
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
.feishin.os-scrollbar {
|
||||||
|
--os-size: var(--scrollbar-size);
|
||||||
|
--os-handle-bg: var(--scrollbar-thumb-bg);
|
||||||
|
--os-handle-bg-hover: var(--scrollbar-thumb-bg-hover);
|
||||||
|
--os-handle-bg-active: var(--scrollbar-thumb-bg-hover);
|
||||||
|
|
||||||
|
--os-track-bg: var(--scrollbar-track-bg);
|
||||||
|
--os-track-bg-hover: var(--scrollbar-track-bg);
|
||||||
|
--os-track-bg-active: var(--scrollbar-track-bg);
|
||||||
|
|
||||||
|
--os-padding-perpendicular: 0;
|
||||||
|
--os-padding-axis: 0;
|
||||||
|
--os-track-border-radius: 0;
|
||||||
|
--os-handle-border-radius: 0;
|
||||||
|
}
|
|
@ -43,6 +43,7 @@
|
||||||
--tooltip-bg: #ffffff;
|
--tooltip-bg: #ffffff;
|
||||||
--tooltip-fg: #000000;
|
--tooltip-fg: #000000;
|
||||||
|
|
||||||
|
--scrollbar-size: 12px;
|
||||||
--scrollbar-track-bg: transparent;
|
--scrollbar-track-bg: transparent;
|
||||||
--scrollbar-thumb-bg: rgba(160, 160, 160, 0.3);
|
--scrollbar-thumb-bg: rgba(160, 160, 160, 0.3);
|
||||||
--scrollbar-thumb-bg-hover: rgba(160, 160, 160, 0.6);
|
--scrollbar-thumb-bg-hover: rgba(160, 160, 160, 0.6);
|
||||||
|
|
Reference in a new issue