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,