refactor(core): use element atom (#4026)

This commit is contained in:
Alex Yang
2023-08-29 18:59:39 -05:00
committed by GitHub
parent 591bfc3320
commit 4aabe2ea5e
10 changed files with 164 additions and 111 deletions

View File

@@ -50,7 +50,6 @@
"jotai": "^2.4.0",
"jotai-devtools": "^0.6.2",
"lit": "^2.8.0",
"lodash.debounce": "^4.0.8",
"lottie-web": "^5.12.2",
"mini-css-extract-plugin": "^2.7.6",
"next-auth": "^4.22.1",
@@ -77,7 +76,6 @@
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.3.80",
"@types/lodash-es": "^4.17.8",
"@types/lodash.debounce": "^4.0.7",
"@types/webpack-env": "^1.18.1",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.8.1",

View File

@@ -0,0 +1,5 @@
import { atom } from 'jotai/vanilla';
export const appHeaderAtom = atom<HTMLDivElement | null>(null);
export const mainContainerAtom = atom<HTMLDivElement | null>(null);

View File

@@ -5,112 +5,40 @@ import {
SidebarSwitch,
} from '@affine/component/app-sidebar';
import { isDesktop } from '@affine/env/constant';
import { useIsTinyScreen } from '@toeverything/hooks/use-is-tiny-screen';
import clsx from 'clsx';
import { useAtomValue } from 'jotai';
import debounce from 'lodash.debounce';
import type { MutableRefObject, ReactNode } from 'react';
import { useEffect, useRef, useState } from 'react';
import { type Atom, useAtomValue } from 'jotai';
import type { ReactElement } from 'react';
import { forwardRef, useRef } from 'react';
import * as style from './style.css';
import { TopTip } from './top-tip';
import { WindowsAppControls } from './windows-app-controls';
interface HeaderPros {
left?: ReactNode;
right?: ReactNode;
center?: ReactNode;
left?: ReactElement;
right?: ReactElement;
center?: ReactElement;
mainContainerAtom: Atom<HTMLDivElement | null>;
}
const useIsTinyScreen = ({
mainContainer,
leftStatic,
leftSlot,
centerDom,
rightStatic,
rightSlot,
}: {
mainContainer: HTMLElement;
leftStatic: MutableRefObject<HTMLElement | null>;
leftSlot: MutableRefObject<HTMLElement | null>[];
centerDom: MutableRefObject<HTMLElement | null>;
rightStatic: MutableRefObject<HTMLElement | null>;
rightSlot: MutableRefObject<HTMLElement | null>[];
}) => {
const [isTinyScreen, setIsTinyScreen] = useState(false);
useEffect(() => {
const handleResize = debounce(() => {
if (!centerDom.current) {
return;
}
const leftStaticWidth = leftStatic.current?.clientWidth || 0;
const leftSlotWidth = leftSlot.reduce((accWidth, dom) => {
return accWidth + (dom.current?.clientWidth || 0);
}, 0);
const rightStaticWidth = rightStatic.current?.clientWidth || 0;
const rightSlotWidth = rightSlot.reduce((accWidth, dom) => {
return accWidth + (dom.current?.clientWidth || 0);
}, 0);
if (!leftSlotWidth && !rightSlotWidth) {
if (isTinyScreen) {
setIsTinyScreen(false);
}
return;
}
const containerRect = mainContainer.getBoundingClientRect();
const centerRect = centerDom.current.getBoundingClientRect();
if (
leftStaticWidth + leftSlotWidth + containerRect.left >=
centerRect.left ||
containerRect.right - centerRect.right <=
rightSlotWidth + rightStaticWidth
) {
setIsTinyScreen(true);
} else {
setIsTinyScreen(false);
}
}, 100);
handleResize();
const resizeObserver = new ResizeObserver(() => {
handleResize();
});
resizeObserver.observe(mainContainer);
return () => {
resizeObserver.disconnect();
};
}, [
centerDom,
isTinyScreen,
leftSlot,
leftStatic,
mainContainer,
rightSlot,
rightStatic,
]);
return isTinyScreen;
};
// The Header component is used to solve the following problems
// 1. Manage layout issues independently of page or business logic
// 2. Dynamic centered middle element (relative to the main-container), when the middle element is detected to collide with the two elements, the line wrapping process is performed
export const Header = ({ left, center, right }: HeaderPros) => {
export const Header = forwardRef<HTMLDivElement, HeaderPros>(function Header(
{ left, center, right, mainContainerAtom },
ref
) {
const sidebarSwitchRef = useRef<HTMLDivElement | null>(null);
const leftSlotRef = useRef<HTMLDivElement | null>(null);
const centerSlotRef = useRef<HTMLDivElement | null>(null);
const rightSlotRef = useRef<HTMLDivElement | null>(null);
const windowControlsRef = useRef<HTMLDivElement | null>(null);
const mainContainer = useAtomValue(mainContainerAtom);
const isTinyScreen = useIsTinyScreen({
mainContainer: document.querySelector('.main-container') || document.body,
mainContainer,
leftStatic: sidebarSwitchRef,
leftSlot: [leftSlotRef],
centerDom: centerSlotRef,
@@ -130,6 +58,7 @@ export const Header = ({ left, center, right }: HeaderPros) => {
data-open={open}
data-sidebar-floating={appSidebarFloating}
data-testid="header"
ref={ref}
>
<div
className={clsx(style.headerSideContainer, {
@@ -175,4 +104,6 @@ export const Header = ({ left, center, right }: HeaderPros) => {
</div>
</>
);
};
});
Header.displayName = 'Header';

View File

@@ -4,6 +4,7 @@ import {
SaveCollectionButton,
useCollectionManager,
} from '@affine/component/page-list';
import { Unreachable } from '@affine/env/constant';
import type { Collection } from '@affine/env/filter';
import type { PropertiesMeta } from '@affine/env/filter';
import {
@@ -11,8 +12,10 @@ import {
type WorkspaceHeaderProps,
} from '@affine/env/workspace';
import { WorkspaceSubPath } from '@affine/env/workspace';
import { useSetAtom } from 'jotai/react';
import { useCallback } from 'react';
import { appHeaderAtom, mainContainerAtom } from '../atoms/element';
import { useGetPageInfoById } from '../hooks/use-get-page-info';
import { useWorkspace } from '../hooks/use-workspace';
import { SharePageModal } from './affine/share-page-modal';
@@ -76,6 +79,7 @@ export function WorkspaceHeader({
currentEntry,
}: WorkspaceHeaderProps<WorkspaceFlavour>) {
const setting = useCollectionManager(currentWorkspaceId);
const setAppHeader = useSetAtom(appHeaderAtom);
const currentWorkspace = useWorkspace(currentWorkspaceId);
const getPageInfoById = useGetPageInfoById(
@@ -90,6 +94,8 @@ export function WorkspaceHeader({
return (
<>
<Header
mainContainerAtom={mainContainerAtom}
ref={setAppHeader}
left={
<CollectionList
setting={setting}
@@ -112,7 +118,13 @@ export function WorkspaceHeader({
(currentEntry.subPath === WorkspaceSubPath.SHARED ||
currentEntry.subPath === WorkspaceSubPath.TRASH)
) {
return <Header center={<WorkspaceModeFilterTab />} />;
return (
<Header
mainContainerAtom={mainContainerAtom}
ref={setAppHeader}
center={<WorkspaceModeFilterTab />}
/>
);
}
// route in edit page
@@ -128,6 +140,8 @@ export function WorkspaceHeader({
) : null;
return (
<Header
mainContainerAtom={mainContainerAtom}
ref={setAppHeader}
center={
<BlockSuiteHeaderTitle
workspace={currentWorkspace}
@@ -144,5 +158,5 @@ export function WorkspaceHeader({
);
}
return null;
throw new Unreachable();
}

View File

@@ -41,6 +41,7 @@ import {
openSettingModalAtom,
openWorkspacesModalAtom,
} from '../atoms';
import { mainContainerAtom } from '../atoms/element';
import { useAppSetting } from '../atoms/settings';
import { AdapterProviderWrapper } from '../components/adapter-worksapce-wrapper';
import { AppContainer } from '../components/affine/app-container';
@@ -206,6 +207,8 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
const location = useLocation();
const { pageId } = useParams();
const setMainContainer = useSetAtom(mainContainerAtom);
return (
<>
{/* This DndContext is used for drag page from all-pages list into a folder in sidebar */}
@@ -234,8 +237,11 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
paths={pathGenerator}
/>
</Suspense>
<Suspense fallback={<MainContainer />}>
<MainContainer padding={appSetting.clientBorder}>
<Suspense fallback={<MainContainer ref={setMainContainer} />}>
<MainContainer
ref={setMainContainer}
padding={appSetting.clientBorder}
>
{children}
<ToolContainer>
<BlockHubWrapper blockHubAtom={rootBlockHubAtom} />

View File

@@ -1,13 +1,19 @@
import type { ToastOptions } from '@affine/component';
import { toast as basicToast } from '@affine/component';
import { assertEquals, assertExists } from '@blocksuite/global/utils';
import { getCurrentStore } from '@toeverything/infra/atom';
import { mainContainerAtom } from '../atoms/element';
export const toast = (message: string, options?: ToastOptions) => {
const mainContainer = getCurrentStore().get(mainContainerAtom);
const modal = document.querySelector(
'[role=presentation]'
) as HTMLElement | null;
const mainContainer = document.querySelector(
'.main-container'
) as HTMLElement | null;
) as HTMLDivElement | null;
assertExists(mainContainer, 'main container should exist');
if (modal) {
assertEquals(modal.constructor, HTMLDivElement, 'modal should be div');
}
return basicToast(message, {
portal: modal || mainContainer || document.body,
...options,

View File

@@ -1,5 +1,6 @@
import { clsx } from 'clsx';
import type { HTMLAttributes, PropsWithChildren, ReactElement } from 'react';
import { forwardRef } from 'react';
import { AppSidebarFallback } from '../app-sidebar';
import { appStyle, mainContainerStyle, toolStyle } from './index.css';
@@ -36,23 +37,27 @@ export interface MainContainerProps extends HTMLAttributes<HTMLDivElement> {
padding?: boolean;
}
export const MainContainer = ({
className,
padding,
children,
...props
}: PropsWithChildren<MainContainerProps>): ReactElement => {
export const MainContainer = forwardRef<
HTMLDivElement,
PropsWithChildren<MainContainerProps>
>(function MainContainer(
{ className, padding, children, ...props },
ref
): ReactElement {
return (
<div
{...props}
className={clsx(mainContainerStyle, 'main-container', className)}
className={clsx(mainContainerStyle, className)}
data-is-macos={environment.isDesktop && environment.isMacOs}
data-show-padding={!!padding}
ref={ref}
>
{children}
</div>
);
};
});
MainContainer.displayName = 'MainContainer';
export const ToolContainer = (props: PropsWithChildren): ReactElement => {
return <div className={toolStyle}>{props.children}</div>;

View File

@@ -6,7 +6,8 @@
},
"private": true,
"dependencies": {
"foxact": "^0.2.20"
"foxact": "^0.2.20",
"lodash.debounce": "^4.0.8"
},
"devDependencies": {
"@affine/env": "workspace:*",
@@ -16,7 +17,8 @@
"@blocksuite/editor": "0.0.0-20230828163942-e5356e86-nightly",
"@blocksuite/global": "0.0.0-20230828163942-e5356e86-nightly",
"@blocksuite/lit": "0.0.0-20230828163942-e5356e86-nightly",
"@blocksuite/store": "0.0.0-20230828163942-e5356e86-nightly"
"@blocksuite/store": "0.0.0-20230828163942-e5356e86-nightly",
"@types/lodash.debounce": "^4.0.7"
},
"peerDependencies": {
"@affine/y-provider": "workspace:*",

View File

@@ -0,0 +1,86 @@
import 'foxact/use-debounced-state';
import debounce from 'lodash.debounce';
import { type RefObject, useEffect, useState } from 'react';
export function useIsTinyScreen({
mainContainer,
leftStatic,
leftSlot,
centerDom,
rightStatic,
rightSlot,
}: {
mainContainer: HTMLElement | null;
leftStatic: RefObject<HTMLElement>;
leftSlot: RefObject<HTMLElement>[];
centerDom: RefObject<HTMLElement>;
rightStatic: RefObject<HTMLElement>;
rightSlot: RefObject<HTMLElement>[];
}) {
const [isTinyScreen, setIsTinyScreen] = useState(false);
useEffect(() => {
if (!mainContainer) {
return;
}
const handleResize = debounce(() => {
if (!centerDom.current) {
return;
}
const leftStaticWidth = leftStatic.current?.clientWidth || 0;
const leftSlotWidth = leftSlot.reduce((accWidth, dom) => {
return accWidth + (dom.current?.clientWidth || 0);
}, 0);
const rightStaticWidth = rightStatic.current?.clientWidth || 0;
const rightSlotWidth = rightSlot.reduce((accWidth, dom) => {
return accWidth + (dom.current?.clientWidth || 0);
}, 0);
if (!leftSlotWidth && !rightSlotWidth) {
if (isTinyScreen) {
setIsTinyScreen(false);
}
return;
}
const containerRect = mainContainer.getBoundingClientRect();
const centerRect = centerDom.current.getBoundingClientRect();
if (
leftStaticWidth + leftSlotWidth + containerRect.left >=
centerRect.left ||
containerRect.right - centerRect.right <=
rightSlotWidth + rightStaticWidth
) {
setIsTinyScreen(true);
} else {
setIsTinyScreen(false);
}
}, 100);
handleResize();
const resizeObserver = new ResizeObserver(() => {
handleResize();
});
resizeObserver.observe(mainContainer);
return () => {
resizeObserver.disconnect();
};
}, [
centerDom,
isTinyScreen,
leftSlot,
leftStatic,
mainContainer,
rightSlot,
rightStatic,
]);
return isTinyScreen;
}

View File

@@ -262,7 +262,6 @@ __metadata:
"@swc/core": ^1.3.80
"@toeverything/components": ^0.0.24
"@types/lodash-es": ^4.17.8
"@types/lodash.debounce": ^4.0.7
"@types/webpack-env": ^1.18.1
async-call-rpc: ^6.3.1
cmdk: ^0.2.0
@@ -278,7 +277,6 @@ __metadata:
jotai-devtools: ^0.6.2
lit: ^2.8.0
lodash-es: ^4.17.21
lodash.debounce: ^4.0.8
lottie-web: ^5.12.2
mime-types: ^2.1.35
mini-css-extract-plugin: ^2.7.6
@@ -12662,7 +12660,9 @@ __metadata:
"@blocksuite/global": 0.0.0-20230828163942-e5356e86-nightly
"@blocksuite/lit": 0.0.0-20230828163942-e5356e86-nightly
"@blocksuite/store": 0.0.0-20230828163942-e5356e86-nightly
"@types/lodash.debounce": ^4.0.7
foxact: ^0.2.20
lodash.debounce: ^4.0.8
peerDependencies:
"@affine/y-provider": "workspace:*"
"@blocksuite/block-std": "*"