mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-22 21:51:39 +03:00
feat(mobile): optimize home header animation (#8707)
close AF-1420 ![CleanShot 2024-11-05 at 17.17.29.gif](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/LakojjjzZNf6ogjOVwKE/a74ead2d-30ef-4bb1-8714-fdc77dd93335.gif)
This commit is contained in:
parent
fa82842cd7
commit
98bdf25844
@ -1,12 +1,21 @@
|
|||||||
import { MobileMenu } from '@affine/component';
|
import { MobileMenu } from '@affine/component';
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
import { useServiceOptional, WorkspacesService } from '@toeverything/infra';
|
import { useServiceOptional, WorkspacesService } from '@toeverything/infra';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import {
|
||||||
|
forwardRef,
|
||||||
|
type HTMLAttributes,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import { CurrentWorkspaceCard } from './current-card';
|
import { CurrentWorkspaceCard } from './current-card';
|
||||||
import { SelectorMenu } from './menu';
|
import { SelectorMenu } from './menu';
|
||||||
|
|
||||||
export const WorkspaceSelector = () => {
|
export const WorkspaceSelector = forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
HTMLAttributes<HTMLDivElement>
|
||||||
|
>(function WorkspaceSelector({ className }, ref) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const workspaceManager = useServiceOptional(WorkspacesService);
|
const workspaceManager = useServiceOptional(WorkspacesService);
|
||||||
|
|
||||||
@ -33,7 +42,11 @@ export const WorkspaceSelector = () => {
|
|||||||
style: { padding: 0 },
|
style: { padding: 0 },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CurrentWorkspaceCard onClick={openMenu} />
|
<CurrentWorkspaceCard
|
||||||
|
ref={ref}
|
||||||
|
onClick={openMenu}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
</MobileMenu>
|
</MobileMenu>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -9,7 +9,7 @@ import { useI18n } from '@affine/i18n';
|
|||||||
import { SettingsIcon } from '@blocksuite/icons/rc';
|
import { SettingsIcon } from '@blocksuite/icons/rc';
|
||||||
import { useService } from '@toeverything/infra';
|
import { useService } from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { SearchInput, WorkspaceSelector } from '../../components';
|
import { SearchInput, WorkspaceSelector } from '../../components';
|
||||||
import { searchVTScope } from '../../components/search-input/style.css';
|
import { searchVTScope } from '../../components/search-input/style.css';
|
||||||
@ -23,18 +23,12 @@ import * as styles from './styles.css';
|
|||||||
* - hide Search
|
* - hide Search
|
||||||
*/
|
*/
|
||||||
export const HomeHeader = () => {
|
export const HomeHeader = () => {
|
||||||
const t = useI18n();
|
|
||||||
const workbench = useService(WorkbenchService).workbench;
|
|
||||||
const globalDialogService = useService(GlobalDialogService);
|
const globalDialogService = useService(GlobalDialogService);
|
||||||
|
|
||||||
const [dense, setDense] = useState(false);
|
const workspaceCardRef = useRef<HTMLDivElement>(null);
|
||||||
|
const floatWorkspaceCardRef = useRef<HTMLDivElement>(null);
|
||||||
useGlobalEvent(
|
const t = useI18n();
|
||||||
'scroll',
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
useCallback(() => {
|
|
||||||
setDense(window.scrollY > 114);
|
|
||||||
}, [])
|
|
||||||
);
|
|
||||||
|
|
||||||
const navSearch = useCallback(() => {
|
const navSearch = useCallback(() => {
|
||||||
startScopedViewTransition(searchVTScope, () => {
|
startScopedViewTransition(searchVTScope, () => {
|
||||||
@ -42,33 +36,49 @@ export const HomeHeader = () => {
|
|||||||
});
|
});
|
||||||
}, [workbench]);
|
}, [workbench]);
|
||||||
|
|
||||||
|
const [dense, setDense] = useState(false);
|
||||||
|
|
||||||
|
useGlobalEvent(
|
||||||
|
'scroll',
|
||||||
|
useCallback(() => {
|
||||||
|
if (!workspaceCardRef.current || !floatWorkspaceCardRef.current) return;
|
||||||
|
const inFlowTop = workspaceCardRef.current.getBoundingClientRect().top;
|
||||||
|
const floatTop =
|
||||||
|
floatWorkspaceCardRef.current.getBoundingClientRect().top;
|
||||||
|
setDense(inFlowTop <= floatTop);
|
||||||
|
}, [])
|
||||||
|
);
|
||||||
|
|
||||||
|
const openSetting = useCallback(() => {
|
||||||
|
globalDialogService.open('setting', {
|
||||||
|
activeTab: 'appearance',
|
||||||
|
});
|
||||||
|
}, [globalDialogService]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.root, { dense })}>
|
<>
|
||||||
<SafeArea top className={styles.float}>
|
<SafeArea top className={styles.root}>
|
||||||
<div className={styles.headerAndWsSelector}>
|
<div className={styles.headerSettingRow}>
|
||||||
<div className={styles.wsSelectorWrapper}>
|
<IconButton onClick={openSetting} size={28} icon={<SettingsIcon />} />
|
||||||
<WorkspaceSelector />
|
|
||||||
</div>
|
|
||||||
<div className={styles.settingWrapper}>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => {
|
|
||||||
globalDialogService.open('setting', {
|
|
||||||
activeTab: 'appearance',
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
size="24"
|
|
||||||
style={{ padding: 10 }}
|
|
||||||
icon={<SettingsIcon />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.searchWrapper}>
|
<div className={styles.wsSelectorAndSearch}>
|
||||||
|
<WorkspaceSelector ref={workspaceCardRef} />
|
||||||
<SearchInput placeholder={t['Quick search']()} onClick={navSearch} />
|
<SearchInput placeholder={t['Quick search']()} onClick={navSearch} />
|
||||||
</div>
|
</div>
|
||||||
</SafeArea>
|
</SafeArea>
|
||||||
<SafeArea top>
|
{/* float */}
|
||||||
<div className={styles.space} />
|
<SafeArea top className={clsx(styles.root, styles.float, { dense })}>
|
||||||
|
<WorkspaceSelector
|
||||||
|
className={styles.floatWsSelector}
|
||||||
|
ref={floatWorkspaceCardRef}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
style={{ transition: 'none' }}
|
||||||
|
onClick={openSetting}
|
||||||
|
size={28}
|
||||||
|
icon={<SettingsIcon />}
|
||||||
|
/>
|
||||||
</SafeArea>
|
</SafeArea>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,80 +5,48 @@ const headerHeight = createVar('headerHeight');
|
|||||||
const wsSelectorHeight = createVar('wsSelectorHeight');
|
const wsSelectorHeight = createVar('wsSelectorHeight');
|
||||||
const searchHeight = createVar('searchHeight');
|
const searchHeight = createVar('searchHeight');
|
||||||
|
|
||||||
const searchPadding = [10, 16, 15, 16];
|
|
||||||
|
|
||||||
export const root = style({
|
export const root = style({
|
||||||
vars: {
|
vars: {
|
||||||
[headerHeight]: '44px',
|
[headerHeight]: '44px',
|
||||||
[wsSelectorHeight]: '48px',
|
[wsSelectorHeight]: '48px',
|
||||||
[searchHeight]: '44px',
|
[searchHeight]: '44px',
|
||||||
},
|
},
|
||||||
width: '100vw',
|
width: '100dvw',
|
||||||
});
|
});
|
||||||
|
export const headerSettingRow = style({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'end',
|
||||||
|
height: 44,
|
||||||
|
paddingRight: 10,
|
||||||
|
});
|
||||||
|
export const wsSelectorAndSearch = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 15,
|
||||||
|
padding: '4px 16px 15px 16px',
|
||||||
|
});
|
||||||
|
|
||||||
export const float = style({
|
export const float = style({
|
||||||
// why not 'sticky'?
|
|
||||||
// when height change, will affect scroll behavior, causing shaking
|
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
background: cssVarV2('layer/background/secondary'),
|
background: cssVarV2('layer/background/secondary'),
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
});
|
|
||||||
export const space = style({
|
|
||||||
height: `calc(${headerHeight} + ${wsSelectorHeight} + ${searchHeight} + ${searchPadding[0] + searchPadding[2]}px + 12px)`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const headerAndWsSelector = style({
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '4px 10px 4px 16px',
|
||||||
gap: 10,
|
gap: 10,
|
||||||
alignItems: 'end',
|
|
||||||
transition: 'height 0.2s',
|
|
||||||
height: `calc(${headerHeight} + ${wsSelectorHeight})`,
|
|
||||||
|
|
||||||
|
// visibility control
|
||||||
|
visibility: 'hidden',
|
||||||
selectors: {
|
selectors: {
|
||||||
[`${root}.dense &`]: {
|
'&.dense': {
|
||||||
height: wsSelectorHeight,
|
visibility: 'visible',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
export const floatWsSelector = style({
|
||||||
export const wsSelectorWrapper = style({
|
|
||||||
width: 0,
|
width: 0,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
height: wsSelectorHeight,
|
|
||||||
padding: '0 10px 0 16px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const settingWrapper = style({
|
|
||||||
width: '44px',
|
|
||||||
height: headerHeight,
|
|
||||||
transition: 'height 0.2s',
|
|
||||||
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
alignSelf: 'start',
|
|
||||||
|
|
||||||
selectors: {
|
|
||||||
[`${root}.dense &`]: {
|
|
||||||
height: wsSelectorHeight,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const searchWrapper = style({
|
|
||||||
padding: searchPadding.map(v => `${v}px`).join(' '),
|
|
||||||
width: '100%',
|
|
||||||
height: 44 + searchPadding[0] + searchPadding[2],
|
|
||||||
transition: 'all 0.2s',
|
|
||||||
overflow: 'hidden',
|
|
||||||
selectors: {
|
|
||||||
[`${root}.dense &`]: {
|
|
||||||
height: 0,
|
|
||||||
paddingTop: 0,
|
|
||||||
paddingBottom: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user