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:
CatsJuice 2024-11-12 07:28:10 +00:00
parent fa82842cd7
commit 98bdf25844
No known key found for this signature in database
GPG Key ID: 1C1E76924FAFDDE4
3 changed files with 81 additions and 90 deletions

View File

@ -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>
); );
}; });

View File

@ -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> </>
); );
}; };

View File

@ -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,
},
},
}); });