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
@@ -86,7 +86,7 @@ export function registerAffineSettingsCommands({
})
);
//Font styles
// Font styles
unsubs.push(
registerAffineCommand({
id: 'affine:change-font-style-to-sans',
@@ -144,7 +144,7 @@ export function registerAffineSettingsCommands({
})
);
//Display Language
// Display Language
languagesList.forEach(language => {
unsubs.push(
registerAffineCommand({
@@ -162,7 +162,7 @@ export function registerAffineSettingsCommands({
);
});
//Layout Style
// Layout Style
unsubs.push(
registerAffineCommand({
id: `affine:change-client-border-style`,
@@ -13,9 +13,10 @@ export const mainContainer = style({
height: '100%',
position: 'relative',
flexDirection: 'column',
width: '100%',
minWidth: 0,
overflow: 'hidden',
selectors: {
[`${root}[data-client-border] &`]: {
[`${root}[data-client-border=true] &`]: {
borderRadius: '4px',
},
},
@@ -27,68 +28,31 @@ export const editorContainer = style({
flexDirection: 'column',
flex: 1,
overflow: 'hidden',
zIndex: 0, // it will create stacking context to limit layer of child elements and be lower than after auto zIndex
});
export const resizeHandle = style({
width: '1px',
position: 'relative',
backgroundColor: 'var(--affine-border-color)',
selectors: {
'&[data-collapsed=true]': {
display: 'none',
},
[`${root}[data-client-border] &`]: {
width: '8px',
backgroundColor: 'transparent',
},
},
});
export const resizeHandleInner = style({
height: '100%',
width: '10px', // this is the real hit box
position: 'absolute',
transform: 'translateX(-50%)',
zIndex: 10,
transition: 'all 0.2s ease-in-out',
display: 'flex',
justifyContent: 'center',
'::before': {
content: '""',
width: '0px',
height: '100%',
borderRadius: '2px',
transition: 'all 0.2s ease-in-out',
},
selectors: {
[`${root}[data-client-border] &`]: {
transform: 'translateX(-1px)',
},
[`:is(${resizeHandle}:hover, ${resizeHandle}[data-resize-handle-active]) &::before`]:
{
width: '2px',
backgroundColor: 'var(--affine-primary-color)',
},
[`${resizeHandle}[data-resize-handle-active] &::before`]: {
width: '4px',
borderRadius: '4px',
},
},
zIndex: 0,
});
export const sidebarContainer = style({
transition: 'flex 0.2s ease-in-out',
display: 'flex',
flexShrink: 0,
height: '100%',
selectors: {
[`${root}[data-client-border=true] &`]: {
paddingLeft: 9,
},
[`${root}[data-client-border=false] &`]: {
borderLeft: '1px solid var(--affine-border-color)',
},
},
});
export const sidebarContainerInner = style({
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
height: '100%',
width: '100%',
selectors: {
[`${resizeHandle}[data-resize-handle-active] + &`]: {
transition: 'none',
},
[`${root}[data-disable-animation] &`]: {
transition: 'none',
},
[`${root}[data-client-border] &`]: {
[`${root}[data-client-border=true] &`]: {
borderRadius: '4px',
},
},
@@ -3,6 +3,7 @@ import {
createTagFilter,
useCollectionManager,
} from '@affine/component/page-list';
import { ResizePanel } from '@affine/component/resize-panel';
import { WorkspaceSubPath } from '@affine/env/workspace';
import { globalBlockSuiteSchema } from '@affine/workspace/manager';
import { SyncEngineStep } from '@affine/workspace/providers';
@@ -21,16 +22,8 @@ import {
type ReactNode,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import type { PanelOnResize } from 'react-resizable-panels';
import {
type ImperativePanelHandle,
Panel,
PanelGroup,
PanelResizeHandle,
} from 'react-resizable-panels';
import { type LoaderFunction, useParams } from 'react-router-dom';
import type { Map as YMap } from 'yjs';
@@ -56,6 +49,7 @@ import { DetailPageHeader, RightSidebarHeader } from './detail-page-header';
import {
EditorSidebar,
editorSidebarOpenAtom,
editorSidebarResizingAtom,
editorSidebarStateAtom,
editorSidebarWidthAtom,
} from './editor-sidebar';
@@ -67,16 +61,8 @@ interface DetailPageLayoutProps {
sidebar: ReactNode;
}
// disable animation to avoid UI flash
function useEnableAnimation() {
const [enable, setEnable] = useState(false);
useEffect(() => {
window.setTimeout(() => {
setEnable(true);
}, 500);
}, []);
return enable;
}
const MIN_SIDEBAR_WIDTH = 320;
const MAX_SIDEBAR_WIDTH = 800;
// todo: consider move to a shared place if we also want to reuse the layout for other routes
const DetailPageLayout = ({
@@ -87,85 +73,39 @@ const DetailPageLayout = ({
}: DetailPageLayoutProps): ReactElement => {
const sidebarState = useAtomValue(editorSidebarStateAtom);
const setSidebarWidth = useSetAtom(editorSidebarWidthAtom);
const setSidebarOpen = useSetAtom(editorSidebarOpenAtom);
const { clientBorder } = useAtomValue(appSettingAtom);
const sidebarRef = useRef<ImperativePanelHandle>(null);
const onExpandSidebar = useCallback(() => {
setSidebarOpen(true);
}, [setSidebarOpen]);
const onCollapseSidebar = useCallback(() => {
setSidebarOpen(false);
}, [setSidebarOpen]);
const onResize: PanelOnResize = useCallback(
e => {
if (e.sizePixels > 0) {
setSidebarWidth(e.sizePixels);
}
},
[setSidebarWidth]
);
useEffect(() => {
const panelHandle = sidebarRef.current;
if (!panelHandle) {
return;
}
if (sidebarState.isOpen) {
panelHandle.expand();
} else {
panelHandle.collapse();
}
}, [sidebarState.isOpen]);
const enableAnimation = useEnableAnimation();
const setResizing = useSetAtom(editorSidebarResizingAtom);
const setOpen = useSetAtom(editorSidebarOpenAtom);
return (
<PanelGroup
direction="horizontal"
<div
className={styles.root}
dataAttributes={{
'data-disable-animation': !enableAnimation ? 'true' : undefined,
'data-client-border': clientBorder ? 'true' : undefined,
}}
data-client-border={clientBorder && sidebarState.isOpen}
>
<Panel id="editor" className={styles.mainContainer}>
<div className={styles.mainContainer}>
{header}
{main}
{footer}
</Panel>
</div>
{sidebar ? (
<>
<PanelResizeHandle
dataAttributes={{
'data-collapsed': !sidebarState.isOpen,
}}
className={styles.resizeHandle}
>
<div className={styles.resizeHandleInner} />
</PanelResizeHandle>
<Panel
id="editor-sidebar"
className={styles.sidebarContainer}
onResize={onResize}
collapsedSizePixels={0}
collapsible
onCollapse={onCollapseSidebar}
onExpand={onExpandSidebar}
ref={sidebarRef}
defaultSizePixels={Math.max(sidebarState.width, 240)}
minSizePixels={sidebarState.isOpen ? 240 : 0}
maxSizePercentage={50}
>
{sidebar}
</Panel>
</>
<ResizePanel
enableAnimation={false}
resizeHandlePos="left"
resizeHandleOffset={clientBorder ? 4 : 0}
width={sidebarState.width}
className={styles.sidebarContainer}
onResizing={setResizing}
resizing={sidebarState.resizing}
open={sidebarState.isOpen}
onOpen={setOpen}
onWidthChange={setSidebarWidth}
minWidth={MIN_SIDEBAR_WIDTH}
maxWidth={MAX_SIDEBAR_WIDTH}
>
{sidebar}
</ResizePanel>
) : null}
</PanelGroup>
</div>
);
};
@@ -262,10 +202,10 @@ const DetailPageImpl = ({ page }: { page: Page }) => {
footer={isInTrash ? <TrashPageFooter pageId={page.id} /> : null}
sidebar={
!isInTrash ? (
<>
<div className={styles.sidebarContainerInner}>
<RightSidebarHeader />
<EditorSidebar />
</>
</div>
) : null
}
/>
@@ -13,12 +13,14 @@ export const extensions: EditorExtension[] = [outlineExtension];
export interface EditorSidebarState {
isOpen: boolean;
width: number;
resizing: boolean;
activeExtension?: EditorExtension;
extensions: EditorExtension[];
}
const baseStateAtom = atom<EditorSidebarState>({
isOpen: false,
resizing: false,
width: 300, // todo: should be resizable
activeExtension: extensions[0],
extensions: extensions, // todo: maybe should be dynamic (by feature flag?)
@@ -27,6 +29,7 @@ const baseStateAtom = atom<EditorSidebarState>({
export const editorSidebarStateAtom = atom(get => get(baseStateAtom));
const isOpenAtom = selectAtom(baseStateAtom, state => state.isOpen);
const resizingAtom = selectAtom(baseStateAtom, state => state.resizing);
const activeExtensionAtom = selectAtom(
baseStateAtom,
state => state.activeExtension
@@ -48,6 +51,16 @@ export const editorSidebarOpenAtom = atom(
}
);
// get/set sidebar resizing state
export const editorSidebarResizingAtom = atom(
get => get(resizingAtom),
(_, set, resizing: boolean) => {
set(baseStateAtom, prev => {
return { ...prev, resizing };
});
}
);
// get/set active extension
export const editorSidebarActiveExtensionAtom = atom(
get => get(activeExtensionAtom),
@@ -7,10 +7,5 @@ export const EditorSidebar = () => {
const sidebarState = useAtomValue(editorSidebarStateAtom);
const Component = sidebarState.activeExtension?.Component;
// do we need this?
if (!sidebarState.isOpen) {
return null;
}
return <div className={styles.root}>{Component ? <Component /> : null}</div>;
};
@@ -39,7 +39,7 @@ export const button = style({
selectors: {
'&[data-active=true]': {
color: 'var(--affine-primary-color)',
pointerEvents: 'none',
},
},
});