feat: add novice guide for quick search arrow button (#1493)

This commit is contained in:
JimmFly 2023-03-10 14:17:26 +08:00 committed by GitHub
parent 7a54e97823
commit 3ef8e2db83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 238 additions and 34 deletions

View File

@ -0,0 +1,5 @@
import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
export const isFirstLoadAtom = atomWithStorage<boolean>('isFirstLoad', true);
export const openTipsAtom = atom<boolean>(false);

View File

@ -1,7 +1,11 @@
import { Tooltip } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import React, { useCallback, useState } from 'react';
import { useCallback, useState } from 'react';
import {
useIsFirstLoad,
useOpenTips,
} from '../../../hooks/affine/use-is-first-load';
import { useSidebarStatus } from '../../../hooks/affine/use-sidebar-status';
import { SidebarSwitchIcon } from './icons';
import { StyledSidebarSwitch } from './style';
@ -17,6 +21,8 @@ export const SidebarSwitch = ({
}: SidebarSwitchProps) => {
const [open, setOpen] = useSidebarStatus();
const [tooltipVisible, setTooltipVisible] = useState(false);
const [isFirstLoad, setIsFirstLoad] = useIsFirstLoad();
const [, setOpenTips] = useOpenTips();
const { t } = useTranslation();
tooltipContent =
tooltipContent || (open ? t('Collapse sidebar') : t('Expand sidebar'));
@ -34,7 +40,13 @@ export const SidebarSwitch = ({
onClick={useCallback(() => {
setOpen(!open);
setTooltipVisible(false);
}, [open, setOpen])}
if (isFirstLoad) {
setIsFirstLoad(false);
setTimeout(() => {
setOpenTips(true);
}, 200);
}
}, [isFirstLoad, open, setIsFirstLoad, setOpen, setOpenTips])}
onMouseEnter={useCallback(() => {
setTooltipVisible(true);
}, [])}

View File

@ -1,9 +1,12 @@
import { Content } from '@affine/component';
import { Content, 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 { openQuickSearchModalAtom } from '../../../atoms';
import { useOpenTips } from '../../../hooks/affine/use-is-first-load';
import { usePageMeta } from '../../../hooks/use-page-meta';
import { BlockSuiteWorkspace } from '../../../shared';
import { PageNotFoundError } from '../../affine/affine-error-eoundary';
@ -11,6 +14,8 @@ import { EditorModeSwitch } from './editor-mode-switch';
import Header from './header';
import { QuickSearchButton } from './quick-search-button';
import {
StyledQuickSearchTipButton,
StyledQuickSearchTipContent,
StyledSearchArrowWrapper,
StyledSwitchWrapper,
StyledTitle,
@ -43,6 +48,39 @@ export const BlockSuiteEditorHeader: React.FC<BlockSuiteEditorHeaderProps> = ({
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 (
<StyledQuickSearchTipContent>
<div>
Click button
{
<span
style={{
fontSize: '24px',
verticalAlign: 'middle',
}}
>
<ArrowDownSmallIcon />
</span>
}
or use
{isMac() ? ' ⌘ + K' : ' Ctrl + K'} to activate Quick Search. Then you
can search keywords or quickly open recently viewed pages.
</div>
<StyledQuickSearchTipButton
data-testid="quick-search-got-it"
onClick={() => setOpenTips(false)}
>
Got it
</StyledQuickSearchTipButton>
</StyledQuickSearchTipContent>
);
};
return (
<Header
rightItems={
@ -68,13 +106,21 @@ export const BlockSuiteEditorHeader: React.FC<BlockSuiteEditorHeaderProps> = ({
/>
</StyledSwitchWrapper>
<Content ellipsis={true}>{title || 'Untitled'}</Content>
<StyledSearchArrowWrapper>
<QuickSearchButton
onClick={() => {
setOpenQuickSearch(true);
}}
/>
</StyledSearchArrowWrapper>
<QuickSearchTips
data-testid="quick-search-tips"
content={tipsContent()}
placement="bottom"
open={openTips}
offset={[0, -5]}
>
<StyledSearchArrowWrapper>
<QuickSearchButton
onClick={() => {
setOpenQuickSearch(true);
}}
/>
</StyledSearchArrowWrapper>
</QuickSearchTips>
</StyledTitleWrapper>
</StyledTitle>
)}

View File

@ -1,7 +1,6 @@
import { IconButton, IconButtonProps } from '@affine/component';
import { styled } from '@affine/component';
import { ArrowDownSmallIcon } from '@blocksuite/icons';
import React from 'react';
const StyledIconButtonWithAnimate = styled(IconButton)(({ theme }) => {
return {

View File

@ -113,3 +113,24 @@ export const StyledPageListTittleWrapper = styled(StyledTitle)(({ theme }) => {
},
};
});
export const StyledQuickSearchTipButton = styled('div')(({ theme }) => {
return {
...displayFlex('center', 'center'),
marginTop: '12px',
color: '#FFFFFF',
width: '60px',
height: ' 26px',
fontSize: theme.font.sm,
lineHeight: '22px',
background: theme.colors.primaryColor,
borderRadius: '8px',
textAlign: 'center',
cursor: 'pointer',
};
});
export const StyledQuickSearchTipContent = styled('div')(({ theme }) => {
return {
...displayFlex('center', 'flex-end'),
flexDirection: 'column',
};
});

View File

@ -92,8 +92,8 @@ export const QuickSearchModal: React.FC<QuickSearchModalProps> = ({
width={620}
style={{
maxHeight: '80vh',
minHeight: isPublicAndNoQuery() ? '72px' : '350px',
top: '12vh',
minHeight: isPublicAndNoQuery() ? '72px' : '412px',
top: '80px',
}}
>
<Command

View File

@ -2,8 +2,8 @@ import { displayFlex, styled } from '@affine/component';
export const StyledContent = styled('div')(({ theme }) => {
return {
minHeight: '220px',
maxHeight: '55vh',
minHeight: '280px',
maxHeight: '70vh',
width: '100%',
overflow: 'auto',
marginBottom: '10px',

View File

@ -124,6 +124,7 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
testid="sliderBar-arrowButton-collapse"
/>
</StyledSidebarWrapper>
<StyledSliderBarWrapper data-testid="sliderBar">
<WorkspaceSelector
currentWorkspace={currentWorkspace}

View File

@ -26,6 +26,7 @@ import {
LocalWorkspace,
RemWorkspaceFlavour,
} from '../../shared';
import { useIsFirstLoad, useOpenTips } from '../affine/use-is-first-load';
import {
useRecentlyViewed,
useSyncRecentViewsWithRouter,
@ -314,3 +315,21 @@ describe('useRecentlyViewed', () => {
]);
});
});
describe('useIsFirstLoad', () => {
test('basic', async () => {
const firstLoad = renderHook(() => useIsFirstLoad());
const setFirstLoad = firstLoad.result.current[1];
expect(firstLoad.result.current[0]).toEqual(true);
setFirstLoad(false);
firstLoad.rerender();
expect(firstLoad.result.current[0]).toEqual(false);
});
test('useOpenTips', async () => {
const openTips = renderHook(() => useOpenTips());
const setOpenTips = openTips.result.current[1];
expect(openTips.result.current[0]).toEqual(false);
setOpenTips(true);
openTips.rerender();
expect(openTips.result.current[0]).toEqual(true);
});
});

View File

@ -0,0 +1,12 @@
import { useAtom } from 'jotai';
import { isFirstLoadAtom, openTipsAtom } from '../../atoms/first-load';
export function useIsFirstLoad() {
const [isFirstLoad, setIsFirstLoad] = useAtom(isFirstLoadAtom);
return [isFirstLoad, setIsFirstLoad] as const;
}
export function useOpenTips() {
const [openTips, setOpenTips] = useAtom(openTipsAtom);
return [openTips, setOpenTips] as const;
}

View File

@ -1,12 +1,10 @@
# Welcome to AFFiNE
👋 Quick Start
==============
# 👋 Quick Start
> Your content should be as feature-rich as you like. We offer you an easy and simple approach to writing your content, with advanced tools that stay out of the way when you don't need them.
AFFiNE - not just a note taking app
-----------------------------------
## AFFiNE - not just a note taking app
There are lots of apps out there, so why choose AFFiNE?
@ -18,18 +16,16 @@ AFFiNE is **privacy focused** - with a local-first approach, keep control of you
AFFiNE is **open source** - you can check us out on [GitHub: toeverything/AFFiNE](https://github.com/toeverything/affine)
Let's get started!
------------------
## Let's get started!
* Create a new page and begin editing - in `Paper` or `Edgeless`
- Create a new page and begin editing - in `Paper` or `Edgeless`
![](https://cdn.affine.pro/38928b822e78a8cf6cc1278c3cd7563967dfefcf744c2517a60c7bfe.gif)
* Head over to `Workspace Settings` to find your `General` settings, enable multiplayer with `Collaboration`, share your docs with `Publish` and download your data with `Export`
- Head over to `Workspace Settings` to find your `General` settings, enable multiplayer with `Collaboration`, share your docs with `Publish` and download your data with `Export`
![](https://cdn.affine.pro/34043644b47f7bab79255a80e1b11d6cb173ec9d2eb0e3208c7badd2.png)
Looking for more support?
-------------
## Looking for more support?
Why not come and join the awesome [AFFiNE Community](https://community.affine.pro)? Whether you want to share new ideas or interact with other like minded individuals - we look forward to having you.

View File

@ -1,4 +1,4 @@
import { forwardRef } from 'react';
import { CSSProperties, forwardRef } from 'react';
import { styled } from '../../styles';
import { PopperArrowProps } from './interface';
@ -9,7 +9,10 @@ export const PopperArrow = forwardRef<HTMLElement, PopperArrowProps>(
}
);
const getArrowStyle = (placement: PopperArrowProps['placement']) => {
const getArrowStyle = (
placement: PopperArrowProps['placement'],
backgroundColor: CSSProperties['backgroundColor']
) => {
if (placement.indexOf('bottom') === 0) {
return {
top: 0,
@ -19,7 +22,7 @@ const getArrowStyle = (placement: PopperArrowProps['placement']) => {
height: '1em',
'&::before': {
borderWidth: '0 1em 1em 1em',
borderColor: `transparent transparent #98ACBD transparent`,
borderColor: `transparent transparent ${backgroundColor} transparent`,
},
};
}
@ -33,7 +36,7 @@ const getArrowStyle = (placement: PopperArrowProps['placement']) => {
height: '1em',
'&::before': {
borderWidth: '1em 1em 0 1em',
borderColor: `#98ACBD transparent transparent transparent`,
borderColor: `${backgroundColor} transparent transparent transparent`,
},
};
}
@ -45,7 +48,7 @@ const getArrowStyle = (placement: PopperArrowProps['placement']) => {
width: '1em',
'&::before': {
borderWidth: '1em 0 1em 1em',
borderColor: `transparent transparent transparent #98ACBD`,
borderColor: `transparent transparent transparent ${backgroundColor}`,
},
};
}
@ -57,7 +60,7 @@ const getArrowStyle = (placement: PopperArrowProps['placement']) => {
width: '1em',
'&::before': {
borderWidth: '1em 1em 1em 0',
borderColor: `transparent #98ACBD transparent transparent`,
borderColor: `transparent ${backgroundColor} transparent transparent`,
},
};
}
@ -69,7 +72,7 @@ const getArrowStyle = (placement: PopperArrowProps['placement']) => {
const StyledArrow = styled('span')<{
placement: PopperArrowProps['placement'];
}>(({ placement }) => {
}>(({ placement, theme }) => {
return {
position: 'absolute',
fontSize: '7px',
@ -88,6 +91,6 @@ const StyledArrow = styled('span')<{
bottom: 0,
},
...getArrowStyle(placement),
...getArrowStyle(placement, theme.colors.tooltipBackground),
};
});

View File

@ -0,0 +1,76 @@
import type { TooltipProps } from '@mui/material';
import { css, displayFlex, styled } from '../../styles';
import { Popper, type PopperProps } from '../popper';
import StyledPopperContainer from '../shared/Container';
const StyledTooltip = styled(StyledPopperContainer)(({ theme }) => {
return {
width: '390px',
minHeight: '92px',
boxShadow: theme.shadow.tooltip,
padding: '12px',
backgroundColor: theme.colors.hoverBackground,
transform: 'all 0.15s',
color: theme.colors.primaryColor,
...displayFlex('center', 'center'),
border: `1px solid ${theme.colors.primaryColor}`,
fontSize: theme.font.sm,
lineHeight: '22px',
fontWeight: 500,
};
});
const StyledCircleContainer = styled('div')(({ theme }) => {
return css`
position: relative;
content: '';
top: 50%;
left: 50%;
transform: translate(0%, 0%);
width: 0;
height: 40px;
border-right: 1px solid ${theme.colors.primaryColor};
&::after {
content: '';
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, -100%);
width: 12px;
height: 12px;
border-radius: 50%;
border: 1px solid ${theme.colors.primaryColor};
}
&::before {
content: '';
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, -150%);
width: 6px;
height: 6px;
border-radius: 50%;
background-color: ${theme.colors.primaryColor};
border: 1px solid ${theme.colors.primaryColor};
}
`;
});
export const QuickSearchTips = (
props: PopperProps & Omit<TooltipProps, 'title'>
) => {
const { content, placement = 'top', children } = props;
return (
<Popper
{...props}
content={
<div>
<StyledCircleContainer />
<StyledTooltip placement={placement}>{content}</StyledTooltip>
</div>
}
>
{children}
</Popper>
);
};

View File

@ -19,7 +19,6 @@ export const Tooltip = (props: PopperProps & Omit<TooltipProps, 'title'>) => {
return (
<Popper
{...props}
showArrow={false}
content={<StyledTooltip placement={placement}>{content}</StyledTooltip>}
>
{children}

View File

@ -1 +1,2 @@
export * from './QuickSearch-tips';
export * from './Tooltip';

View File

@ -150,3 +150,17 @@ test.describe('Focus event for quick search', () => {
await titleIsFocused(page);
});
});
test.describe('Novice guidance for quick search', () => {
test('When opening the website for the first time, the first folding sidebar will appear novice guide', async ({
page,
}) => {
const quickSearchTips = page.locator('[data-testid=quick-search-tips]');
await expect(quickSearchTips).not.toBeVisible();
await page.getByTestId('sliderBar-arrowButton-collapse').click();
const sliderBarArea = page.getByTestId('sliderBar');
await expect(sliderBarArea).not.toBeVisible();
await expect(quickSearchTips).toBeVisible();
await page.locator('[data-testid=quick-search-got-it]').click();
await expect(quickSearchTips).not.toBeVisible();
});
});