refactor(core): side bar resizing (#5280)

Rewrite sidebar panel using a customized react-resizable-panels version that supports sidebar pixel sizing (not using flex percentages).

Now the left & right sidebar using the same `ResizePanel` impl.

fix https://github.com/toeverything/AFFiNE/issues/5271
fix TOV-163
fix TOV-146
fix TOV-168
fix TOV-109
fix TOV-165
This commit is contained in:
Peng Xiao
2023-12-13 07:52:01 +00:00
parent 2a9a6855f4
commit ce64685176
16 changed files with 406 additions and 386 deletions
@@ -0,0 +1 @@
export * from './resize-panel';
@@ -0,0 +1,100 @@
import { createVar, style } from '@vanilla-extract/css';
export const panelWidthVar = createVar('panel-width');
export const resizeHandleOffsetVar = createVar('resize-handle-offset');
export const resizeHandleVerticalPadding = createVar(
'resize-handle-vertical-padding'
);
export const root = style({
vars: {
[panelWidthVar]: '256px',
[resizeHandleOffsetVar]: '0',
},
position: 'relative',
width: panelWidthVar,
minWidth: 0,
height: '100%',
selectors: {
'&[data-is-floating="true"]': {
position: 'absolute',
width: `calc(${panelWidthVar})`,
zIndex: 4,
},
'&[data-open="true"]': {
maxWidth: '50%',
},
'&[data-open="false"][data-handle-position="right"]': {
marginLeft: `calc(${panelWidthVar} * -1)`,
},
'&[data-open="false"][data-handle-position="left"]': {
marginRight: `calc(${panelWidthVar} * -1)`,
},
'&[data-enable-animation="true"]': {
transition: 'margin-left .3s .05s, margin-right .3s .05s, width .3s .05s',
},
'&[data-is-floating="false"][data-transparent=true]': {
backgroundColor: 'transparent',
},
},
});
export const panelContent = style({
position: 'relative',
height: '100%',
overflow: 'auto',
});
export const resizeHandleContainer = style({
position: 'absolute',
right: resizeHandleOffsetVar,
top: resizeHandleVerticalPadding,
bottom: resizeHandleVerticalPadding,
width: 16,
zIndex: '1',
transform: 'translateX(50%)',
backgroundColor: 'transparent',
opacity: 0,
display: 'flex',
justifyContent: 'center',
cursor: 'ew-resize',
'@media': {
'(max-width: 600px)': {
// do not allow resizing on small screen
display: 'none',
},
},
transition: 'opacity 0.15s ease 0.1s',
selectors: {
'&[data-resizing="true"], &:hover': {
opacity: 1,
},
'&[data-open="false"]': {
display: 'none',
},
'&[data-open="open"]': {
display: 'block',
},
'&[data-handle-position="left"]': {
left: resizeHandleOffsetVar,
right: 'auto',
transform: 'translateX(-50%)',
},
},
});
export const resizerInner = style({
position: 'absolute',
height: '100%',
width: '2px',
borderRadius: '2px',
backgroundColor: 'var(--affine-primary-color)',
transition: 'all 0.2s ease-in-out',
transform: 'translateX(0.5px)',
selectors: {
[`${resizeHandleContainer}[data-resizing="true"] &`]: {
width: '4px',
borderRadius: '4px',
},
},
});
@@ -0,0 +1,181 @@
import { assertExists } from '@blocksuite/global/utils';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import clsx from 'clsx';
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
import * as styles from './resize-panel.css';
export interface ResizeHandleProps
extends React.HtmlHTMLAttributes<HTMLDivElement> {
resizing: boolean;
open: boolean;
minWidth: number;
maxWidth: number;
resizeHandlePos: 'left' | 'right';
resizeHandleOffset?: number;
resizeHandleVerticalPadding?: number;
onOpen: (open: boolean) => void;
onResizing: (resizing: boolean) => void;
onWidthChange: (width: number) => void;
}
export interface ResizePanelProps
extends React.HtmlHTMLAttributes<HTMLDivElement> {
resizing: boolean;
open: boolean;
floating?: boolean;
minWidth: number;
maxWidth: number;
resizeHandlePos: 'left' | 'right';
resizeHandleOffset?: number;
resizeHandleVerticalPadding?: number;
enableAnimation?: boolean;
width: number;
onOpen: (open: boolean) => void;
onResizing: (resizing: boolean) => void;
onWidthChange: (width: number) => void;
}
const ResizeHandle = ({
className,
resizing,
minWidth,
maxWidth,
resizeHandlePos,
resizeHandleOffset,
resizeHandleVerticalPadding,
open,
onOpen,
onResizing,
onWidthChange,
...rest
}: ResizeHandleProps) => {
const ref = useRef<HTMLDivElement>(null);
const onResizeStart = useCallback(() => {
let resized = false;
const panelContainer = ref.current?.parentElement;
assertExists(
panelContainer,
'parent element not found for resize indicator'
);
const { left: anchorLeft, right: anchorRight } =
panelContainer.getBoundingClientRect();
function onMouseMove(e: MouseEvent) {
e.preventDefault();
if (!panelContainer) return;
const newWidth = Math.min(
maxWidth,
Math.max(
resizeHandlePos === 'right'
? e.clientX - anchorLeft
: anchorRight - e.clientX,
minWidth
)
);
onWidthChange(newWidth);
onResizing(true);
resized = true;
}
document.addEventListener('mousemove', onMouseMove);
document.addEventListener(
'mouseup',
() => {
// if not resized, toggle sidebar
if (!resized) {
onOpen(false);
}
onResizing(false);
document.removeEventListener('mousemove', onMouseMove);
},
{ once: true }
);
}, [maxWidth, resizeHandlePos, minWidth, onWidthChange, onResizing, onOpen]);
return (
<div
{...rest}
data-testid="resize-handle"
ref={ref}
style={assignInlineVars({
[styles.resizeHandleOffsetVar]: `${resizeHandleOffset ?? 0}px`,
[styles.resizeHandleVerticalPadding]: `${
resizeHandleVerticalPadding ?? 0
}px`,
})}
className={clsx(styles.resizeHandleContainer, className)}
data-handle-position={resizeHandlePos}
data-resizing={resizing}
data-open={open}
onMouseDown={onResizeStart}
>
<div className={styles.resizerInner} />
</div>
);
};
function useEnableAnimation() {
const [enable, setEnable] = useState(false);
useEffect(() => {
window.setTimeout(() => {
setEnable(true);
}, 500);
}, []);
return enable;
}
export const ResizePanel = forwardRef<HTMLDivElement, ResizePanelProps>(
function ResizePanel(
{
children,
className,
resizing,
minWidth,
maxWidth,
width,
floating,
enableAnimation: _enableAnimation = true,
open,
onOpen,
onResizing,
onWidthChange,
resizeHandlePos,
resizeHandleOffset,
resizeHandleVerticalPadding,
...rest
},
ref
) {
const enableAnimation = useEnableAnimation() && _enableAnimation;
return (
<div
{...rest}
ref={ref}
style={assignInlineVars({
[styles.panelWidthVar]: `${width}px`,
})}
className={clsx(className, styles.root)}
data-open={open}
data-is-floating={floating}
data-handle-position={resizeHandlePos}
data-enable-animation={enableAnimation && !resizing}
>
{children}
<ResizeHandle
resizeHandlePos={resizeHandlePos}
resizeHandleOffset={resizeHandleOffset}
resizeHandleVerticalPadding={resizeHandleVerticalPadding}
maxWidth={maxWidth}
minWidth={minWidth}
onOpen={onOpen}
onResizing={onResizing}
onWidthChange={onWidthChange}
open={open}
resizing={resizing}
/>
</div>
);
}
);