diff --git a/apps/web/src/atoms/index.ts b/apps/web/src/atoms/index.ts index ebfee8ae06..200a085c83 100644 --- a/apps/web/src/atoms/index.ts +++ b/apps/web/src/atoms/index.ts @@ -1,3 +1,4 @@ +import { EditorContainer } from '@blocksuite/editor'; import { assertExists } from '@blocksuite/store'; import { atom, createStore } from 'jotai'; import { atomWithStorage } from 'jotai/utils'; @@ -8,6 +9,8 @@ import { RemWorkspace, RemWorkspaceFlavour } from '../shared'; // workspace necessary atoms export const currentWorkspaceIdAtom = atom(null); export const currentPageIdAtom = atom(null); +export const currentEditorAtom = atom | null>(null); + // If the workspace is locked, it means that the user maybe updating the workspace // from local to remote or vice versa export const workspaceLockAtom = atom(false); diff --git a/apps/web/src/components/blocksuite/header/header.tsx b/apps/web/src/components/blocksuite/header/header.tsx index 97f66c8538..6ca5e920d2 100644 --- a/apps/web/src/components/blocksuite/header/header.tsx +++ b/apps/web/src/components/blocksuite/header/header.tsx @@ -1,6 +1,13 @@ import { useTranslation } from '@affine/i18n'; import { CloseIcon } from '@blocksuite/icons'; -import React, { PropsWithChildren, useEffect, useMemo, useState } from 'react'; +import React, { + forwardRef, + HTMLAttributes, + PropsWithChildren, + useEffect, + useMemo, + useState, +} from 'react'; import { useSidebarStatus } from '../../../hooks/affine/use-sidebar-status'; import { SidebarSwitch } from '../../affine/sidebar-switch'; @@ -51,50 +58,57 @@ export type HeaderProps = PropsWithChildren<{ rightItems?: HeaderRightItemNames[]; }>; -export const Header: React.FC = ({ - rightItems = ['syncUser', 'themeModeSwitch'], - children, -}) => { - const [showWarning, setShowWarning] = useState(false); - useEffect(() => { - setShowWarning(shouldShowWarning()); - }, []); - const [open] = useSidebarStatus(); - const { t } = useTranslation(); +export const Header = forwardRef< + HTMLDivElement, + HeaderProps & HTMLAttributes +>( + ( + { rightItems = ['syncUser', 'themeModeSwitch'], children, ...props }, + ref + ) => { + const [showWarning, setShowWarning] = useState(false); + useEffect(() => { + setShowWarning(shouldShowWarning()); + }, []); + const [open] = useSidebarStatus(); + const { t } = useTranslation(); - return ( - - { - setShowWarning(false); - }} - /> - - + { + setShowWarning(false); + }} /> + + - {children} - - {useMemo( - () => - rightItems.map(itemName => { - const Item = HeaderRightItems[itemName]; - return ; - }), - [rightItems] - )} - - - - ); -}; + {children} + + {useMemo( + () => + rightItems.map(itemName => { + const Item = HeaderRightItems[itemName]; + return ; + }), + [rightItems] + )} + + + + ); + } +); + +Header.displayName = 'Header'; export default Header; diff --git a/apps/web/src/components/blocksuite/header/index.tsx b/apps/web/src/components/blocksuite/header/index.tsx index 8819747c4f..b4c8ac2e3e 100644 --- a/apps/web/src/components/blocksuite/header/index.tsx +++ b/apps/web/src/components/blocksuite/header/index.tsx @@ -1,13 +1,14 @@ -import { QuickSearchTips } from '@affine/component'; +import { PopperProps, QuickSearchTips } from '@affine/component'; import { getEnvironment } from '@affine/env'; import { ArrowDownSmallIcon } from '@blocksuite/icons'; import { assertExists } from '@blocksuite/store'; -import { useSetAtom } from 'jotai'; -import React from 'react'; +import { useAtomValue, useSetAtom } from 'jotai'; +import { forwardRef, HTMLAttributes, useCallback, useRef } from 'react'; -import { openQuickSearchModalAtom } from '../../../atoms'; +import { currentEditorAtom, openQuickSearchModalAtom } from '../../../atoms'; import { useOpenTips } from '../../../hooks/affine/use-is-first-load'; import { usePageMeta } from '../../../hooks/use-page-meta'; +import { useElementResizeEffect } from '../../../hooks/use-workspaces'; import { BlockSuiteWorkspace } from '../../../shared'; import { PageNotFoundError } from '../../affine/affine-error-eoundary'; import { QuickSearchButton } from '../../pure/quick-search-button'; @@ -30,32 +31,45 @@ export type BlockSuiteEditorHeaderProps = React.PropsWithChildren<{ isPreview?: boolean; }>; -export const BlockSuiteEditorHeader: React.FC = ({ - blockSuiteWorkspace, - pageId, - children, - isPublic, - isPreview, -}) => { - const page = blockSuiteWorkspace.getPage(pageId); - // fixme(himself65): remove this atom and move it to props - const setOpenQuickSearch = useSetAtom(openQuickSearchModalAtom); - if (!page) { - throw new PageNotFoundError(blockSuiteWorkspace, pageId); - } - const pageMeta = usePageMeta(blockSuiteWorkspace).find( - meta => meta.id === pageId - ); - assertExists(pageMeta); - const title = pageMeta.title; - const { trash: isTrash } = pageMeta; - const [openTips, setOpenTips] = useOpenTips(); - const isMac = () => { - const env = getEnvironment(); - return env.isBrowser && env.isMacOs; - }; - const tipsContent = () => { - return ( +export const BlockSuiteEditorHeader = forwardRef< + HTMLDivElement, + BlockSuiteEditorHeaderProps & HTMLAttributes +>( + ( + { blockSuiteWorkspace, pageId, children, isPublic, isPreview, ...props }, + ref + ) => { + const page = blockSuiteWorkspace.getPage(pageId); + // fixme(himself65): remove this atom and move it to props + const setOpenQuickSearch = useSetAtom(openQuickSearchModalAtom); + if (!page) { + throw new PageNotFoundError(blockSuiteWorkspace, pageId); + } + const pageMeta = usePageMeta(blockSuiteWorkspace).find( + meta => meta.id === pageId + ); + assertExists(pageMeta); + const title = pageMeta.title; + const { trash: isTrash } = pageMeta; + const [openTips, setOpenTips] = useOpenTips(); + const isMac = () => { + const env = getEnvironment(); + return env.isBrowser && env.isMacOs; + }; + + const popperRef: PopperProps['popperRef'] = useRef(null); + + useElementResizeEffect( + useAtomValue(currentEditorAtom), + useCallback(() => { + if (!openTips || !popperRef.current) { + return; + } + popperRef.current.update(); + }, [openTips]) + ); + + const TipsContent = (
Click button @@ -81,50 +95,56 @@ export const BlockSuiteEditorHeader: React.FC = ({ ); - }; - return ( -
- {children} - {!isPublic && ( - - - - - - {title || 'Untitled'} - - - { - setOpenQuickSearch(true); + + return ( +
+ {children} + {!isPublic && ( + + + + - - - - - )} -
- ); -}; + + {title || 'Untitled'} + + + { + setOpenQuickSearch(true); + }} + /> + + +
+
+ )} +
+ ); + } +); + +BlockSuiteEditorHeader.displayName = 'BlockSuiteEditorHeader'; diff --git a/apps/web/src/components/page-detail-editor.tsx b/apps/web/src/components/page-detail-editor.tsx index 835c01af7a..d835ff2dd6 100644 --- a/apps/web/src/components/page-detail-editor.tsx +++ b/apps/web/src/components/page-detail-editor.tsx @@ -1,9 +1,11 @@ import type { EditorContainer } from '@blocksuite/editor'; import { assertExists, Page } from '@blocksuite/store'; +import { useSetAtom } from 'jotai'; import dynamic from 'next/dynamic'; import Head from 'next/head'; -import React from 'react'; +import React, { useCallback } from 'react'; +import { currentEditorAtom } from '../atoms'; import { useBlockSuiteWorkspacePageTitle } from '../hooks/use-blocksuite-workspace-page-title'; import { usePageMeta } from '../hooks/use-page-meta'; import { BlockSuiteWorkspace } from '../shared'; @@ -45,6 +47,7 @@ export const PageDetailEditor: React.FC = ({ const meta = usePageMeta(blockSuiteWorkspace).find( meta => meta.id === pageId ); + const setEditor = useSetAtom(currentEditorAtom); assertExists(meta); return ( <> @@ -68,7 +71,13 @@ export const PageDetailEditor: React.FC = ({ // fixme: remove mode from meta mode={isPublic ? 'page' : meta.mode ?? 'page'} page={page} - onInit={onInit} + onInit={useCallback( + (page: Page, editor: Readonly) => { + setEditor(editor); + onInit(page, editor); + }, + [onInit, setEditor] + )} onLoad={onLoad} /> diff --git a/apps/web/src/hooks/use-workspaces.ts b/apps/web/src/hooks/use-workspaces.ts index 9d09e31801..46d6aa9955 100644 --- a/apps/web/src/hooks/use-workspaces.ts +++ b/apps/web/src/hooks/use-workspaces.ts @@ -1,6 +1,6 @@ import { nanoid } from '@blocksuite/store'; import { useAtomValue, useSetAtom } from 'jotai'; -import { useCallback } from 'react'; +import { useCallback, useEffect } from 'react'; import { jotaiWorkspacesAtom, workspacesAtom } from '../atoms'; import { WorkspacePlugins } from '../plugins'; @@ -71,3 +71,25 @@ export function useWorkspacesHelper() { ), }; } + +export const useElementResizeEffect = ( + element: Element | null, + fn: () => void | (() => () => void), + // TODO: add throttle + throttle = 0 +) => { + useEffect(() => { + if (!element) { + return; + } + let dispose: void | (() => void); + const resizeObserver = new ResizeObserver(entries => { + dispose = fn(); + }); + resizeObserver.observe(element); + return () => { + dispose?.(); + resizeObserver.disconnect(); + }; + }, [element, fn]); +}; diff --git a/tests/local-first-favorites-items.spec.ts b/tests/local-first-favorites-items.spec.ts index e290fa9c1b..b32ec3c84d 100644 --- a/tests/local-first-favorites-items.spec.ts +++ b/tests/local-first-favorites-items.spec.ts @@ -19,7 +19,7 @@ test.describe('Local first favorite items ui', () => { const cell = page.getByRole('cell', { name: 'this is a new page to favorite', }); - expect(cell).not.toBeUndefined(); + await expect(cell).toBeVisible(); await cell.click(); await clickPageMoreActions(page);