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
-2
View File
@@ -50,7 +50,6 @@
"jotai": "^2.4.0", "jotai": "^2.4.0",
"jotai-devtools": "^0.6.2", "jotai-devtools": "^0.6.2",
"lit": "^2.8.0", "lit": "^2.8.0",
"lodash.debounce": "^4.0.8",
"lottie-web": "^5.12.2", "lottie-web": "^5.12.2",
"mini-css-extract-plugin": "^2.7.6", "mini-css-extract-plugin": "^2.7.6",
"next-auth": "^4.22.1", "next-auth": "^4.22.1",
@@ -77,7 +76,6 @@
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",
"@swc/core": "^1.3.80", "@swc/core": "^1.3.80",
"@types/lodash-es": "^4.17.8", "@types/lodash-es": "^4.17.8",
"@types/lodash.debounce": "^4.0.7",
"@types/webpack-env": "^1.18.1", "@types/webpack-env": "^1.18.1",
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.8.1", "css-loader": "^6.8.1",
+5
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);
+20 -89
View File
@@ -5,112 +5,40 @@ import {
SidebarSwitch, SidebarSwitch,
} from '@affine/component/app-sidebar'; } from '@affine/component/app-sidebar';
import { isDesktop } from '@affine/env/constant'; import { isDesktop } from '@affine/env/constant';
import { useIsTinyScreen } from '@toeverything/hooks/use-is-tiny-screen';
import clsx from 'clsx'; import clsx from 'clsx';
import { useAtomValue } from 'jotai'; import { type Atom, useAtomValue } from 'jotai';
import debounce from 'lodash.debounce'; import type { ReactElement } from 'react';
import type { MutableRefObject, ReactNode } from 'react'; import { forwardRef, useRef } from 'react';
import { useEffect, useRef, useState } from 'react';
import * as style from './style.css'; import * as style from './style.css';
import { TopTip } from './top-tip'; import { TopTip } from './top-tip';
import { WindowsAppControls } from './windows-app-controls'; import { WindowsAppControls } from './windows-app-controls';
interface HeaderPros { interface HeaderPros {
left?: ReactNode; left?: ReactElement;
right?: ReactNode; right?: ReactElement;
center?: ReactNode; 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 // The Header component is used to solve the following problems
// 1. Manage layout issues independently of page or business logic // 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 // 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 sidebarSwitchRef = useRef<HTMLDivElement | null>(null);
const leftSlotRef = useRef<HTMLDivElement | null>(null); const leftSlotRef = useRef<HTMLDivElement | null>(null);
const centerSlotRef = useRef<HTMLDivElement | null>(null); const centerSlotRef = useRef<HTMLDivElement | null>(null);
const rightSlotRef = useRef<HTMLDivElement | null>(null); const rightSlotRef = useRef<HTMLDivElement | null>(null);
const windowControlsRef = useRef<HTMLDivElement | null>(null); const windowControlsRef = useRef<HTMLDivElement | null>(null);
const mainContainer = useAtomValue(mainContainerAtom);
const isTinyScreen = useIsTinyScreen({ const isTinyScreen = useIsTinyScreen({
mainContainer: document.querySelector('.main-container') || document.body, mainContainer,
leftStatic: sidebarSwitchRef, leftStatic: sidebarSwitchRef,
leftSlot: [leftSlotRef], leftSlot: [leftSlotRef],
centerDom: centerSlotRef, centerDom: centerSlotRef,
@@ -130,6 +58,7 @@ export const Header = ({ left, center, right }: HeaderPros) => {
data-open={open} data-open={open}
data-sidebar-floating={appSidebarFloating} data-sidebar-floating={appSidebarFloating}
data-testid="header" data-testid="header"
ref={ref}
> >
<div <div
className={clsx(style.headerSideContainer, { className={clsx(style.headerSideContainer, {
@@ -175,4 +104,6 @@ export const Header = ({ left, center, right }: HeaderPros) => {
</div> </div>
</> </>
); );
}; });
Header.displayName = 'Header';
+16 -2
View File
@@ -4,6 +4,7 @@ import {
SaveCollectionButton, SaveCollectionButton,
useCollectionManager, useCollectionManager,
} from '@affine/component/page-list'; } from '@affine/component/page-list';
import { Unreachable } from '@affine/env/constant';
import type { Collection } from '@affine/env/filter'; import type { Collection } from '@affine/env/filter';
import type { PropertiesMeta } from '@affine/env/filter'; import type { PropertiesMeta } from '@affine/env/filter';
import { import {
@@ -11,8 +12,10 @@ import {
type WorkspaceHeaderProps, type WorkspaceHeaderProps,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
import { WorkspaceSubPath } from '@affine/env/workspace'; import { WorkspaceSubPath } from '@affine/env/workspace';
import { useSetAtom } from 'jotai/react';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { appHeaderAtom, mainContainerAtom } from '../atoms/element';
import { useGetPageInfoById } from '../hooks/use-get-page-info'; import { useGetPageInfoById } from '../hooks/use-get-page-info';
import { useWorkspace } from '../hooks/use-workspace'; import { useWorkspace } from '../hooks/use-workspace';
import { SharePageModal } from './affine/share-page-modal'; import { SharePageModal } from './affine/share-page-modal';
@@ -76,6 +79,7 @@ export function WorkspaceHeader({
currentEntry, currentEntry,
}: WorkspaceHeaderProps<WorkspaceFlavour>) { }: WorkspaceHeaderProps<WorkspaceFlavour>) {
const setting = useCollectionManager(currentWorkspaceId); const setting = useCollectionManager(currentWorkspaceId);
const setAppHeader = useSetAtom(appHeaderAtom);
const currentWorkspace = useWorkspace(currentWorkspaceId); const currentWorkspace = useWorkspace(currentWorkspaceId);
const getPageInfoById = useGetPageInfoById( const getPageInfoById = useGetPageInfoById(
@@ -90,6 +94,8 @@ export function WorkspaceHeader({
return ( return (
<> <>
<Header <Header
mainContainerAtom={mainContainerAtom}
ref={setAppHeader}
left={ left={
<CollectionList <CollectionList
setting={setting} setting={setting}
@@ -112,7 +118,13 @@ export function WorkspaceHeader({
(currentEntry.subPath === WorkspaceSubPath.SHARED || (currentEntry.subPath === WorkspaceSubPath.SHARED ||
currentEntry.subPath === WorkspaceSubPath.TRASH) currentEntry.subPath === WorkspaceSubPath.TRASH)
) { ) {
return <Header center={<WorkspaceModeFilterTab />} />; return (
<Header
mainContainerAtom={mainContainerAtom}
ref={setAppHeader}
center={<WorkspaceModeFilterTab />}
/>
);
} }
// route in edit page // route in edit page
@@ -128,6 +140,8 @@ export function WorkspaceHeader({
) : null; ) : null;
return ( return (
<Header <Header
mainContainerAtom={mainContainerAtom}
ref={setAppHeader}
center={ center={
<BlockSuiteHeaderTitle <BlockSuiteHeaderTitle
workspace={currentWorkspace} workspace={currentWorkspace}
@@ -144,5 +158,5 @@ export function WorkspaceHeader({
); );
} }
return null; throw new Unreachable();
} }
+8 -2
View File
@@ -41,6 +41,7 @@ import {
openSettingModalAtom, openSettingModalAtom,
openWorkspacesModalAtom, openWorkspacesModalAtom,
} from '../atoms'; } from '../atoms';
import { mainContainerAtom } from '../atoms/element';
import { useAppSetting } from '../atoms/settings'; import { useAppSetting } from '../atoms/settings';
import { AdapterProviderWrapper } from '../components/adapter-worksapce-wrapper'; import { AdapterProviderWrapper } from '../components/adapter-worksapce-wrapper';
import { AppContainer } from '../components/affine/app-container'; import { AppContainer } from '../components/affine/app-container';
@@ -206,6 +207,8 @@ export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
const location = useLocation(); const location = useLocation();
const { pageId } = useParams(); const { pageId } = useParams();
const setMainContainer = useSetAtom(mainContainerAtom);
return ( return (
<> <>
{/* This DndContext is used for drag page from all-pages list into a folder in sidebar */} {/* 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} paths={pathGenerator}
/> />
</Suspense> </Suspense>
<Suspense fallback={<MainContainer />}> <Suspense fallback={<MainContainer ref={setMainContainer} />}>
<MainContainer padding={appSetting.clientBorder}> <MainContainer
ref={setMainContainer}
padding={appSetting.clientBorder}
>
{children} {children}
<ToolContainer> <ToolContainer>
<BlockHubWrapper blockHubAtom={rootBlockHubAtom} /> <BlockHubWrapper blockHubAtom={rootBlockHubAtom} />
+10 -4
View File
@@ -1,13 +1,19 @@
import type { ToastOptions } from '@affine/component'; import type { ToastOptions } from '@affine/component';
import { toast as basicToast } 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) => { export const toast = (message: string, options?: ToastOptions) => {
const mainContainer = getCurrentStore().get(mainContainerAtom);
const modal = document.querySelector( const modal = document.querySelector(
'[role=presentation]' '[role=presentation]'
) as HTMLElement | null; ) as HTMLDivElement | null;
const mainContainer = document.querySelector( assertExists(mainContainer, 'main container should exist');
'.main-container' if (modal) {
) as HTMLElement | null; assertEquals(modal.constructor, HTMLDivElement, 'modal should be div');
}
return basicToast(message, { return basicToast(message, {
portal: modal || mainContainer || document.body, portal: modal || mainContainer || document.body,
...options, ...options,
@@ -1,5 +1,6 @@
import { clsx } from 'clsx'; import { clsx } from 'clsx';
import type { HTMLAttributes, PropsWithChildren, ReactElement } from 'react'; import type { HTMLAttributes, PropsWithChildren, ReactElement } from 'react';
import { forwardRef } from 'react';
import { AppSidebarFallback } from '../app-sidebar'; import { AppSidebarFallback } from '../app-sidebar';
import { appStyle, mainContainerStyle, toolStyle } from './index.css'; import { appStyle, mainContainerStyle, toolStyle } from './index.css';
@@ -36,23 +37,27 @@ export interface MainContainerProps extends HTMLAttributes<HTMLDivElement> {
padding?: boolean; padding?: boolean;
} }
export const MainContainer = ({ export const MainContainer = forwardRef<
className, HTMLDivElement,
padding, PropsWithChildren<MainContainerProps>
children, >(function MainContainer(
...props { className, padding, children, ...props },
}: PropsWithChildren<MainContainerProps>): ReactElement => { ref
): ReactElement {
return ( return (
<div <div
{...props} {...props}
className={clsx(mainContainerStyle, 'main-container', className)} className={clsx(mainContainerStyle, className)}
data-is-macos={environment.isDesktop && environment.isMacOs} data-is-macos={environment.isDesktop && environment.isMacOs}
data-show-padding={!!padding} data-show-padding={!!padding}
ref={ref}
> >
{children} {children}
</div> </div>
); );
}; });
MainContainer.displayName = 'MainContainer';
export const ToolContainer = (props: PropsWithChildren): ReactElement => { export const ToolContainer = (props: PropsWithChildren): ReactElement => {
return <div className={toolStyle}>{props.children}</div>; return <div className={toolStyle}>{props.children}</div>;
+4 -2
View File
@@ -6,7 +6,8 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"foxact": "^0.2.20" "foxact": "^0.2.20",
"lodash.debounce": "^4.0.8"
}, },
"devDependencies": { "devDependencies": {
"@affine/env": "workspace:*", "@affine/env": "workspace:*",
@@ -16,7 +17,8 @@
"@blocksuite/editor": "0.0.0-20230828163942-e5356e86-nightly", "@blocksuite/editor": "0.0.0-20230828163942-e5356e86-nightly",
"@blocksuite/global": "0.0.0-20230828163942-e5356e86-nightly", "@blocksuite/global": "0.0.0-20230828163942-e5356e86-nightly",
"@blocksuite/lit": "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": { "peerDependencies": {
"@affine/y-provider": "workspace:*", "@affine/y-provider": "workspace:*",
+86
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;
}
+2 -2
View File
@@ -262,7 +262,6 @@ __metadata:
"@swc/core": ^1.3.80 "@swc/core": ^1.3.80
"@toeverything/components": ^0.0.24 "@toeverything/components": ^0.0.24
"@types/lodash-es": ^4.17.8 "@types/lodash-es": ^4.17.8
"@types/lodash.debounce": ^4.0.7
"@types/webpack-env": ^1.18.1 "@types/webpack-env": ^1.18.1
async-call-rpc: ^6.3.1 async-call-rpc: ^6.3.1
cmdk: ^0.2.0 cmdk: ^0.2.0
@@ -278,7 +277,6 @@ __metadata:
jotai-devtools: ^0.6.2 jotai-devtools: ^0.6.2
lit: ^2.8.0 lit: ^2.8.0
lodash-es: ^4.17.21 lodash-es: ^4.17.21
lodash.debounce: ^4.0.8
lottie-web: ^5.12.2 lottie-web: ^5.12.2
mime-types: ^2.1.35 mime-types: ^2.1.35
mini-css-extract-plugin: ^2.7.6 mini-css-extract-plugin: ^2.7.6
@@ -12662,7 +12660,9 @@ __metadata:
"@blocksuite/global": 0.0.0-20230828163942-e5356e86-nightly "@blocksuite/global": 0.0.0-20230828163942-e5356e86-nightly
"@blocksuite/lit": 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
foxact: ^0.2.20 foxact: ^0.2.20
lodash.debounce: ^4.0.8
peerDependencies: peerDependencies:
"@affine/y-provider": "workspace:*" "@affine/y-provider": "workspace:*"
"@blocksuite/block-std": "*" "@blocksuite/block-std": "*"