fix: quick search tips follow when resize (#1580)

Co-authored-by: himself65 <himself65@outlook.com>
This commit is contained in:
Whitewater 2023-03-15 09:27:38 -07:00 committed by GitHub
parent 5ac6632276
commit bc32b07bf0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 190 additions and 122 deletions

View File

@ -1,3 +1,4 @@
import { EditorContainer } from '@blocksuite/editor';
import { assertExists } from '@blocksuite/store'; import { assertExists } from '@blocksuite/store';
import { atom, createStore } from 'jotai'; import { atom, createStore } from 'jotai';
import { atomWithStorage } from 'jotai/utils'; import { atomWithStorage } from 'jotai/utils';
@ -8,6 +9,8 @@ import { RemWorkspace, RemWorkspaceFlavour } from '../shared';
// workspace necessary atoms // workspace necessary atoms
export const currentWorkspaceIdAtom = atom<string | null>(null); export const currentWorkspaceIdAtom = atom<string | null>(null);
export const currentPageIdAtom = atom<string | null>(null); export const currentPageIdAtom = atom<string | null>(null);
export const currentEditorAtom = atom<Readonly<EditorContainer> | null>(null);
// If the workspace is locked, it means that the user maybe updating the workspace // If the workspace is locked, it means that the user maybe updating the workspace
// from local to remote or vice versa // from local to remote or vice versa
export const workspaceLockAtom = atom(false); export const workspaceLockAtom = atom(false);

View File

@ -1,6 +1,13 @@
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
import { CloseIcon } from '@blocksuite/icons'; 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 { useSidebarStatus } from '../../../hooks/affine/use-sidebar-status';
import { SidebarSwitch } from '../../affine/sidebar-switch'; import { SidebarSwitch } from '../../affine/sidebar-switch';
@ -51,50 +58,57 @@ export type HeaderProps = PropsWithChildren<{
rightItems?: HeaderRightItemNames[]; rightItems?: HeaderRightItemNames[];
}>; }>;
export const Header: React.FC<HeaderProps> = ({ export const Header = forwardRef<
rightItems = ['syncUser', 'themeModeSwitch'], HTMLDivElement,
children, HeaderProps & HTMLAttributes<HTMLDivElement>
}) => { >(
const [showWarning, setShowWarning] = useState(false); (
useEffect(() => { { rightItems = ['syncUser', 'themeModeSwitch'], children, ...props },
setShowWarning(shouldShowWarning()); ref
}, []); ) => {
const [open] = useSidebarStatus(); const [showWarning, setShowWarning] = useState(false);
const { t } = useTranslation(); useEffect(() => {
setShowWarning(shouldShowWarning());
}, []);
const [open] = useSidebarStatus();
const { t } = useTranslation();
return ( return (
<StyledHeaderContainer hasWarning={showWarning}> <StyledHeaderContainer ref={ref} hasWarning={showWarning} {...props}>
<BrowserWarning <BrowserWarning
show={showWarning} show={showWarning}
onClose={() => { onClose={() => {
setShowWarning(false); setShowWarning(false);
}} }}
/>
<StyledHeader
hasWarning={showWarning}
data-testid="editor-header-items"
data-tauri-drag-region
>
<SidebarSwitch
visible={!open}
tooltipContent={t('Expand sidebar')}
testid="sliderBar-arrowButton-expand"
/> />
<StyledHeader
hasWarning={showWarning}
data-testid="editor-header-items"
data-tauri-drag-region
>
<SidebarSwitch
visible={!open}
tooltipContent={t('Expand sidebar')}
testid="sliderBar-arrowButton-expand"
/>
{children} {children}
<StyledHeaderRightSide> <StyledHeaderRightSide>
{useMemo( {useMemo(
() => () =>
rightItems.map(itemName => { rightItems.map(itemName => {
const Item = HeaderRightItems[itemName]; const Item = HeaderRightItems[itemName];
return <Item key={itemName} />; return <Item key={itemName} />;
}), }),
[rightItems] [rightItems]
)} )}
</StyledHeaderRightSide> </StyledHeaderRightSide>
</StyledHeader> </StyledHeader>
</StyledHeaderContainer> </StyledHeaderContainer>
); );
}; }
);
Header.displayName = 'Header';
export default Header; export default Header;

View File

@ -1,13 +1,14 @@
import { QuickSearchTips } from '@affine/component'; import { PopperProps, QuickSearchTips } from '@affine/component';
import { getEnvironment } from '@affine/env'; import { getEnvironment } from '@affine/env';
import { ArrowDownSmallIcon } from '@blocksuite/icons'; import { ArrowDownSmallIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store'; import { assertExists } from '@blocksuite/store';
import { useSetAtom } from 'jotai'; import { useAtomValue, useSetAtom } from 'jotai';
import React from 'react'; 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 { useOpenTips } from '../../../hooks/affine/use-is-first-load';
import { usePageMeta } from '../../../hooks/use-page-meta'; import { usePageMeta } from '../../../hooks/use-page-meta';
import { useElementResizeEffect } from '../../../hooks/use-workspaces';
import { BlockSuiteWorkspace } from '../../../shared'; import { BlockSuiteWorkspace } from '../../../shared';
import { PageNotFoundError } from '../../affine/affine-error-eoundary'; import { PageNotFoundError } from '../../affine/affine-error-eoundary';
import { QuickSearchButton } from '../../pure/quick-search-button'; import { QuickSearchButton } from '../../pure/quick-search-button';
@ -30,32 +31,45 @@ export type BlockSuiteEditorHeaderProps = React.PropsWithChildren<{
isPreview?: boolean; isPreview?: boolean;
}>; }>;
export const BlockSuiteEditorHeader: React.FC<BlockSuiteEditorHeaderProps> = ({ export const BlockSuiteEditorHeader = forwardRef<
blockSuiteWorkspace, HTMLDivElement,
pageId, BlockSuiteEditorHeaderProps & HTMLAttributes<HTMLDivElement>
children, >(
isPublic, (
isPreview, { blockSuiteWorkspace, pageId, children, isPublic, isPreview, ...props },
}) => { ref
const page = blockSuiteWorkspace.getPage(pageId); ) => {
// fixme(himself65): remove this atom and move it to props const page = blockSuiteWorkspace.getPage(pageId);
const setOpenQuickSearch = useSetAtom(openQuickSearchModalAtom); // fixme(himself65): remove this atom and move it to props
if (!page) { const setOpenQuickSearch = useSetAtom(openQuickSearchModalAtom);
throw new PageNotFoundError(blockSuiteWorkspace, pageId); if (!page) {
} throw new PageNotFoundError(blockSuiteWorkspace, pageId);
const pageMeta = usePageMeta(blockSuiteWorkspace).find( }
meta => meta.id === pageId const pageMeta = usePageMeta(blockSuiteWorkspace).find(
); meta => meta.id === pageId
assertExists(pageMeta); );
const title = pageMeta.title; assertExists(pageMeta);
const { trash: isTrash } = pageMeta; const title = pageMeta.title;
const [openTips, setOpenTips] = useOpenTips(); const { trash: isTrash } = pageMeta;
const isMac = () => { const [openTips, setOpenTips] = useOpenTips();
const env = getEnvironment(); const isMac = () => {
return env.isBrowser && env.isMacOs; const env = getEnvironment();
}; return env.isBrowser && env.isMacOs;
const tipsContent = () => { };
return (
const popperRef: PopperProps['popperRef'] = useRef(null);
useElementResizeEffect(
useAtomValue(currentEditorAtom),
useCallback(() => {
if (!openTips || !popperRef.current) {
return;
}
popperRef.current.update();
}, [openTips])
);
const TipsContent = (
<StyledQuickSearchTipContent> <StyledQuickSearchTipContent>
<div> <div>
Click button Click button
@ -81,50 +95,56 @@ export const BlockSuiteEditorHeader: React.FC<BlockSuiteEditorHeaderProps> = ({
</StyledQuickSearchTipButton> </StyledQuickSearchTipButton>
</StyledQuickSearchTipContent> </StyledQuickSearchTipContent>
); );
};
return ( return (
<Header <Header
rightItems={ ref={ref}
// fixme(himself65): other right items not supported in public mode rightItems={
isPublic || isPreview // fixme(himself65): other right items not supported in public mode
? ['themeModeSwitch'] isPublic || isPreview
: isTrash ? ['themeModeSwitch']
? ['trashButtonGroup'] : isTrash
: ['syncUser', 'themeModeSwitch', 'editorOptionMenu'] ? ['trashButtonGroup']
} : ['syncUser', 'themeModeSwitch', 'editorOptionMenu']
> }
{children} {...props}
{!isPublic && ( >
<StyledTitleContainer data-tauri-drag-region> {children}
<StyledTitleWrapper> {!isPublic && (
<StyledSwitchWrapper> <StyledTitleContainer data-tauri-drag-region>
<EditorModeSwitch <StyledTitleWrapper>
blockSuiteWorkspace={blockSuiteWorkspace} <StyledSwitchWrapper>
pageId={pageId} <EditorModeSwitch
style={{ blockSuiteWorkspace={blockSuiteWorkspace}
marginRight: '12px', pageId={pageId}
}} style={{
/> marginRight: '12px',
</StyledSwitchWrapper>
<StyledTitle>{title || 'Untitled'}</StyledTitle>
<QuickSearchTips
data-testid="quick-search-tips"
content={tipsContent()}
placement="bottom"
open={openTips}
offset={[0, -5]}
>
<StyledSearchArrowWrapper>
<QuickSearchButton
onClick={() => {
setOpenQuickSearch(true);
}} }}
/> />
</StyledSearchArrowWrapper> </StyledSwitchWrapper>
</QuickSearchTips> <StyledTitle>{title || 'Untitled'}</StyledTitle>
</StyledTitleWrapper> <QuickSearchTips
</StyledTitleContainer> data-testid="quick-search-tips"
)} content={TipsContent}
</Header> placement="bottom"
); popperRef={popperRef}
}; open={openTips}
offset={[0, -5]}
>
<StyledSearchArrowWrapper>
<QuickSearchButton
onClick={() => {
setOpenQuickSearch(true);
}}
/>
</StyledSearchArrowWrapper>
</QuickSearchTips>
</StyledTitleWrapper>
</StyledTitleContainer>
)}
</Header>
);
}
);
BlockSuiteEditorHeader.displayName = 'BlockSuiteEditorHeader';

View File

@ -1,9 +1,11 @@
import type { EditorContainer } from '@blocksuite/editor'; import type { EditorContainer } from '@blocksuite/editor';
import { assertExists, Page } from '@blocksuite/store'; import { assertExists, Page } from '@blocksuite/store';
import { useSetAtom } from 'jotai';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import Head from 'next/head'; 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 { useBlockSuiteWorkspacePageTitle } from '../hooks/use-blocksuite-workspace-page-title';
import { usePageMeta } from '../hooks/use-page-meta'; import { usePageMeta } from '../hooks/use-page-meta';
import { BlockSuiteWorkspace } from '../shared'; import { BlockSuiteWorkspace } from '../shared';
@ -45,6 +47,7 @@ export const PageDetailEditor: React.FC<PageDetailEditorProps> = ({
const meta = usePageMeta(blockSuiteWorkspace).find( const meta = usePageMeta(blockSuiteWorkspace).find(
meta => meta.id === pageId meta => meta.id === pageId
); );
const setEditor = useSetAtom(currentEditorAtom);
assertExists(meta); assertExists(meta);
return ( return (
<> <>
@ -68,7 +71,13 @@ export const PageDetailEditor: React.FC<PageDetailEditorProps> = ({
// fixme: remove mode from meta // fixme: remove mode from meta
mode={isPublic ? 'page' : meta.mode ?? 'page'} mode={isPublic ? 'page' : meta.mode ?? 'page'}
page={page} page={page}
onInit={onInit} onInit={useCallback(
(page: Page, editor: Readonly<EditorContainer>) => {
setEditor(editor);
onInit(page, editor);
},
[onInit, setEditor]
)}
onLoad={onLoad} onLoad={onLoad}
/> />
</> </>

View File

@ -1,6 +1,6 @@
import { nanoid } from '@blocksuite/store'; import { nanoid } from '@blocksuite/store';
import { useAtomValue, useSetAtom } from 'jotai'; import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback } from 'react'; import { useCallback, useEffect } from 'react';
import { jotaiWorkspacesAtom, workspacesAtom } from '../atoms'; import { jotaiWorkspacesAtom, workspacesAtom } from '../atoms';
import { WorkspacePlugins } from '../plugins'; 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]);
};

View File

@ -19,7 +19,7 @@ test.describe('Local first favorite items ui', () => {
const cell = page.getByRole('cell', { const cell = page.getByRole('cell', {
name: 'this is a new page to favorite', name: 'this is a new page to favorite',
}); });
expect(cell).not.toBeUndefined(); await expect(cell).toBeVisible();
await cell.click(); await cell.click();
await clickPageMoreActions(page); await clickPageMoreActions(page);