mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-22 17:21:48 +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 { track } from '@affine/track';
|
||||
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 { SelectorMenu } from './menu';
|
||||
|
||||
export const WorkspaceSelector = () => {
|
||||
export const WorkspaceSelector = forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLAttributes<HTMLDivElement>
|
||||
>(function WorkspaceSelector({ className }, ref) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const workspaceManager = useServiceOptional(WorkspacesService);
|
||||
|
||||
@ -33,7 +42,11 @@ export const WorkspaceSelector = () => {
|
||||
style: { padding: 0 },
|
||||
}}
|
||||
>
|
||||
<CurrentWorkspaceCard onClick={openMenu} />
|
||||
<CurrentWorkspaceCard
|
||||
ref={ref}
|
||||
onClick={openMenu}
|
||||
className={className}
|
||||
/>
|
||||
</MobileMenu>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -9,7 +9,7 @@ import { useI18n } from '@affine/i18n';
|
||||
import { SettingsIcon } from '@blocksuite/icons/rc';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { SearchInput, WorkspaceSelector } from '../../components';
|
||||
import { searchVTScope } from '../../components/search-input/style.css';
|
||||
@ -23,18 +23,12 @@ import * as styles from './styles.css';
|
||||
* - hide Search
|
||||
*/
|
||||
export const HomeHeader = () => {
|
||||
const t = useI18n();
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
|
||||
const [dense, setDense] = useState(false);
|
||||
|
||||
useGlobalEvent(
|
||||
'scroll',
|
||||
useCallback(() => {
|
||||
setDense(window.scrollY > 114);
|
||||
}, [])
|
||||
);
|
||||
const workspaceCardRef = useRef<HTMLDivElement>(null);
|
||||
const floatWorkspaceCardRef = useRef<HTMLDivElement>(null);
|
||||
const t = useI18n();
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
|
||||
const navSearch = useCallback(() => {
|
||||
startScopedViewTransition(searchVTScope, () => {
|
||||
@ -42,33 +36,49 @@ export const HomeHeader = () => {
|
||||
});
|
||||
}, [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 (
|
||||
<div className={clsx(styles.root, { dense })}>
|
||||
<SafeArea top className={styles.float}>
|
||||
<div className={styles.headerAndWsSelector}>
|
||||
<div className={styles.wsSelectorWrapper}>
|
||||
<WorkspaceSelector />
|
||||
</div>
|
||||
<div className={styles.settingWrapper}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'appearance',
|
||||
});
|
||||
}}
|
||||
size="24"
|
||||
style={{ padding: 10 }}
|
||||
icon={<SettingsIcon />}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<SafeArea top className={styles.root}>
|
||||
<div className={styles.headerSettingRow}>
|
||||
<IconButton onClick={openSetting} size={28} icon={<SettingsIcon />} />
|
||||
</div>
|
||||
<div className={styles.searchWrapper}>
|
||||
<div className={styles.wsSelectorAndSearch}>
|
||||
<WorkspaceSelector ref={workspaceCardRef} />
|
||||
<SearchInput placeholder={t['Quick search']()} onClick={navSearch} />
|
||||
</div>
|
||||
</SafeArea>
|
||||
<SafeArea top>
|
||||
<div className={styles.space} />
|
||||
{/* float */}
|
||||
<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>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -5,80 +5,48 @@ const headerHeight = createVar('headerHeight');
|
||||
const wsSelectorHeight = createVar('wsSelectorHeight');
|
||||
const searchHeight = createVar('searchHeight');
|
||||
|
||||
const searchPadding = [10, 16, 15, 16];
|
||||
|
||||
export const root = style({
|
||||
vars: {
|
||||
[headerHeight]: '44px',
|
||||
[wsSelectorHeight]: '48px',
|
||||
[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({
|
||||
// why not 'sticky'?
|
||||
// when height change, will affect scroll behavior, causing shaking
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
width: '100%',
|
||||
background: cssVarV2('layer/background/secondary'),
|
||||
zIndex: 1,
|
||||
});
|
||||
export const space = style({
|
||||
height: `calc(${headerHeight} + ${wsSelectorHeight} + ${searchHeight} + ${searchPadding[0] + searchPadding[2]}px + 12px)`,
|
||||
});
|
||||
|
||||
export const headerAndWsSelector = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '4px 10px 4px 16px',
|
||||
gap: 10,
|
||||
alignItems: 'end',
|
||||
transition: 'height 0.2s',
|
||||
height: `calc(${headerHeight} + ${wsSelectorHeight})`,
|
||||
|
||||
// visibility control
|
||||
visibility: 'hidden',
|
||||
selectors: {
|
||||
[`${root}.dense &`]: {
|
||||
height: wsSelectorHeight,
|
||||
'&.dense': {
|
||||
visibility: 'visible',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const wsSelectorWrapper = style({
|
||||
export const floatWsSelector = style({
|
||||
width: 0,
|
||||
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