mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-22 23:01:35 +03:00
feat: add novice guide for quick search arrow button (#1493)
This commit is contained in:
parent
7a54e97823
commit
3ef8e2db83
5
apps/web/src/atoms/first-load.ts
Normal file
5
apps/web/src/atoms/first-load.ts
Normal 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);
|
@ -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);
|
||||
}, [])}
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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 {
|
||||
|
@ -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',
|
||||
};
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -124,6 +124,7 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
|
||||
testid="sliderBar-arrowButton-collapse"
|
||||
/>
|
||||
</StyledSidebarWrapper>
|
||||
|
||||
<StyledSliderBarWrapper data-testid="sliderBar">
|
||||
<WorkspaceSelector
|
||||
currentWorkspace={currentWorkspace}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
12
apps/web/src/hooks/affine/use-is-first-load.ts
Normal file
12
apps/web/src/hooks/affine/use-is-first-load.ts
Normal 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;
|
||||
}
|
@ -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.
|
||||
|
@ -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),
|
||||
};
|
||||
});
|
||||
|
76
packages/component/src/ui/tooltip/QuickSearch-tips.tsx
Normal file
76
packages/component/src/ui/tooltip/QuickSearch-tips.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -19,7 +19,6 @@ export const Tooltip = (props: PopperProps & Omit<TooltipProps, 'title'>) => {
|
||||
return (
|
||||
<Popper
|
||||
{...props}
|
||||
showArrow={false}
|
||||
content={<StyledTooltip placement={placement}>{content}</StyledTooltip>}
|
||||
>
|
||||
{children}
|
||||
|
@ -1 +1,2 @@
|
||||
export * from './QuickSearch-tips';
|
||||
export * from './Tooltip';
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user