mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-21 04:41:29 +03:00
feat(mobile): replace search with create in app tab (#8934)
- Remove search on AppTab, replaced with `create doc` - Always show AppTab for editor page - Extract `NavigationBack` from `PageHeader`
This commit is contained in:
parent
5e92d6cfe9
commit
6a71b28a61
@ -0,0 +1,51 @@
|
||||
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import track from '@affine/track';
|
||||
import { EditIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
DocsService,
|
||||
useLiveData,
|
||||
useServices,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { tabItem } from './styles.css';
|
||||
|
||||
export const AppTabCreate = () => {
|
||||
const { docsService, workbenchService, workspaceService, journalService } =
|
||||
useServices({
|
||||
DocsService,
|
||||
WorkbenchService,
|
||||
WorkspaceService,
|
||||
JournalService,
|
||||
});
|
||||
const workbench = workbenchService.workbench;
|
||||
const currentWorkspace = workspaceService.workspace;
|
||||
const location = useLiveData(workbench.location$);
|
||||
const pageHelper = usePageHelper(currentWorkspace.docCollection);
|
||||
|
||||
const maybeDocId = location.pathname.split('/')[1].split('?')[0];
|
||||
const doc = useLiveData(docsService.list.doc$(maybeDocId));
|
||||
const journalDate = useLiveData(journalService.journalDate$(maybeDocId));
|
||||
const isActive = !!doc && !journalDate;
|
||||
|
||||
const createPage = useCallback(() => {
|
||||
if (isActive) return;
|
||||
pageHelper.createPage(undefined, true);
|
||||
track.$.navigationPanel.$.createDoc();
|
||||
}, [isActive, pageHelper]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={tabItem}
|
||||
data-active={isActive}
|
||||
role="tab"
|
||||
aria-label="New Page"
|
||||
onClick={createPage}
|
||||
>
|
||||
<EditIcon />
|
||||
</div>
|
||||
);
|
||||
};
|
@ -3,13 +3,14 @@ import {
|
||||
WorkbenchLink,
|
||||
WorkbenchService,
|
||||
} from '@affine/core/modules/workbench';
|
||||
import { AllDocsIcon, MobileHomeIcon, SearchIcon } from '@blocksuite/icons/rc';
|
||||
import { AllDocsIcon, MobileHomeIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import React from 'react';
|
||||
import type { Location } from 'react-router-dom';
|
||||
|
||||
import { VirtualKeyboardService } from '../../modules/virtual-keyboard/services/virtual-keyboard';
|
||||
import { AppTabCreate } from './create';
|
||||
import { AppTabJournal } from './journal';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
@ -48,9 +49,8 @@ const routes: Route[] = [
|
||||
node: <AppTabJournal />,
|
||||
},
|
||||
{
|
||||
key: 'search',
|
||||
to: '/search',
|
||||
Icon: SearchIcon,
|
||||
key: 'new',
|
||||
node: <AppTabCreate />,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
export * from './app-tabs';
|
||||
export * from './doc-card';
|
||||
export * from './navigation-back';
|
||||
export * from './page-header';
|
||||
export * from './rename';
|
||||
export * from './search-input';
|
||||
|
@ -0,0 +1,54 @@
|
||||
import {
|
||||
IconButton,
|
||||
type IconButtonProps,
|
||||
useIsInsideModal,
|
||||
} from '@affine/component';
|
||||
import { ArrowLeftSmallIcon, CloseIcon } from '@blocksuite/icons/rc';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { NavigationGestureService } from '../../modules/navigation-gesture';
|
||||
|
||||
export interface NavigationBackButtonProps extends IconButtonProps {
|
||||
backAction?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A button to control the back behavior of the mobile app, as well as manage navigation gesture
|
||||
*/
|
||||
export const NavigationBackButton = ({
|
||||
backAction,
|
||||
style: propsStyle,
|
||||
...otherProps
|
||||
}: NavigationBackButtonProps) => {
|
||||
const navigationGesture = useService(NavigationGestureService);
|
||||
const isInsideModal = useIsInsideModal();
|
||||
|
||||
const handleRouteBack = useCallback(() => {
|
||||
backAction ? backAction() : history.back();
|
||||
}, [backAction]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInsideModal) return;
|
||||
|
||||
const prev = navigationGesture.enabled$.value;
|
||||
navigationGesture.setEnabled(true);
|
||||
|
||||
return () => {
|
||||
navigationGesture.setEnabled(prev);
|
||||
};
|
||||
}, [isInsideModal, navigationGesture]);
|
||||
|
||||
const style = useMemo(() => ({ padding: 10, ...propsStyle }), [propsStyle]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size={24}
|
||||
style={style}
|
||||
onClick={handleRouteBack}
|
||||
icon={isInsideModal ? <CloseIcon /> : <ArrowLeftSmallIcon />}
|
||||
data-testid="page-header-back"
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,16 +1,8 @@
|
||||
import { IconButton, SafeArea, useIsInsideModal } from '@affine/component';
|
||||
import { ArrowLeftSmallIcon, CloseIcon } from '@blocksuite/icons/rc';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { SafeArea } from '@affine/component';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
forwardRef,
|
||||
type HtmlHTMLAttributes,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import { forwardRef, type HtmlHTMLAttributes, type ReactNode } from 'react';
|
||||
|
||||
import { NavigationGestureService } from '../../modules/navigation-gesture';
|
||||
import { NavigationBackButton } from '../navigation-back';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export interface PageHeaderProps
|
||||
@ -74,24 +66,6 @@ export const PageHeader = forwardRef<HTMLDivElement, PageHeaderProps>(
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const navigationGesture = useService(NavigationGestureService);
|
||||
const isInsideModal = useIsInsideModal();
|
||||
|
||||
useEffect(() => {
|
||||
if (isInsideModal) return;
|
||||
|
||||
const prev = navigationGesture.enabled$.value;
|
||||
navigationGesture.setEnabled(!!back);
|
||||
|
||||
return () => {
|
||||
navigationGesture.setEnabled(prev);
|
||||
};
|
||||
}, [back, isInsideModal, navigationGesture]);
|
||||
|
||||
const handleRouteBack = useCallback(() => {
|
||||
backAction ? backAction() : history.back();
|
||||
}, [backAction]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SafeArea
|
||||
@ -106,15 +80,7 @@ export const PageHeader = forwardRef<HTMLDivElement, PageHeaderProps>(
|
||||
className={clsx(styles.prefix, prefixClassName)}
|
||||
style={prefixStyle}
|
||||
>
|
||||
{back ? (
|
||||
<IconButton
|
||||
size={24}
|
||||
style={{ padding: 10 }}
|
||||
onClick={handleRouteBack}
|
||||
icon={isInsideModal ? <CloseIcon /> : <ArrowLeftSmallIcon />}
|
||||
data-testid="page-header-back"
|
||||
/>
|
||||
) : null}
|
||||
{back ? <NavigationBackButton backAction={backAction} /> : null}
|
||||
{prefix}
|
||||
</section>
|
||||
|
||||
|
@ -169,7 +169,7 @@ const DetailPageImpl = () => {
|
||||
|
||||
editor.bindEditorContainer(
|
||||
editorContainer,
|
||||
null,
|
||||
(editorContainer as any).docTitle, // set from proxy
|
||||
scrollViewportRef.current
|
||||
);
|
||||
|
||||
@ -272,9 +272,7 @@ const MobileDetailPage = ({
|
||||
) : null}
|
||||
</PageHeader>
|
||||
<DetailPageImpl />
|
||||
{date ? (
|
||||
<AppTabs background={cssVarV2('layer/background/primary')} />
|
||||
) : null}
|
||||
<AppTabs background={cssVarV2('layer/background/primary')} />
|
||||
</DetailPageWrapper>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,10 +1,15 @@
|
||||
import { SafeArea, useThemeColorV2 } from '@affine/component';
|
||||
import {
|
||||
SafeArea,
|
||||
startScopedViewTransition,
|
||||
useThemeColorV2,
|
||||
} from '@affine/component';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import {
|
||||
type QuickSearchItem,
|
||||
QuickSearchTagIcon,
|
||||
} from '@affine/core/modules/quicksearch';
|
||||
import { TagService } from '@affine/core/modules/tag';
|
||||
import { sleep } from '@blocksuite/affine/global/utils';
|
||||
import { ViewLayersIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
LiveData,
|
||||
@ -14,7 +19,12 @@ import {
|
||||
} from '@toeverything/infra';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { AppTabs, SearchInput, SearchResLabel } from '../../components';
|
||||
import {
|
||||
NavigationBackButton,
|
||||
SearchInput,
|
||||
SearchResLabel,
|
||||
} from '../../components';
|
||||
import { searchVTScope } from '../../components/search-input/style.css';
|
||||
import { MobileSearchService } from '../../modules/search';
|
||||
import { SearchResults } from '../../views/search/search-results';
|
||||
import * as styles from '../../views/search/style.css';
|
||||
@ -133,11 +143,20 @@ export const Component = () => {
|
||||
]
|
||||
);
|
||||
|
||||
const transitionBack = useCallback(() => {
|
||||
startScopedViewTransition(searchVTScope, async () => {
|
||||
history.back();
|
||||
await sleep(10);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SafeArea top>
|
||||
<div className={styles.searchHeader} data-testid="search-header">
|
||||
<NavigationBackButton backAction={transitionBack} />
|
||||
<SearchInput
|
||||
className={styles.searchInput}
|
||||
debounce={300}
|
||||
autoFocus={!searchInput}
|
||||
value={searchInput}
|
||||
@ -147,7 +166,6 @@ export const Component = () => {
|
||||
</div>
|
||||
</SafeArea>
|
||||
{searchInput ? <WithQueryList /> : <RecentList />}
|
||||
<AppTabs />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -4,6 +4,14 @@ import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const searchHeader = style({
|
||||
padding: 16,
|
||||
paddingLeft: 8,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
});
|
||||
export const searchInput = style({
|
||||
width: 0,
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
export const resTitle = style([
|
||||
|
@ -229,7 +229,15 @@ export class Editor extends Entity {
|
||||
const title = docTitle?.querySelector<
|
||||
HTMLElement & { inlineEditor: InlineEditor | null }
|
||||
>('rich-text');
|
||||
title?.inlineEditor?.focusEnd();
|
||||
// Only focus on the title when it's empty on mobile edition.
|
||||
if (BUILD_CONFIG.isMobileEdition) {
|
||||
const titleText = this.doc.title$.value;
|
||||
if (!titleText?.length) {
|
||||
title?.inlineEditor?.focusEnd();
|
||||
}
|
||||
} else {
|
||||
title?.inlineEditor?.focusEnd();
|
||||
}
|
||||
} else {
|
||||
const selection = editorContainer.host?.std.selection;
|
||||
|
||||
|
@ -13,7 +13,8 @@ test('app tabs is visible', async ({ page }) => {
|
||||
|
||||
await expect(tabs.getByRole('tab', { name: 'home' })).toBeVisible();
|
||||
await expect(tabs.getByRole('tab', { name: 'all' })).toBeVisible();
|
||||
await expect(tabs.getByRole('tab', { name: 'search' })).toBeVisible();
|
||||
await expect(tabs.getByRole('tab', { name: 'journal' })).toBeVisible();
|
||||
await expect(tabs.getByRole('tab', { name: 'new' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('recent docs', async ({ page }) => {
|
||||
@ -49,15 +50,3 @@ test('all tab', async ({ page }) => {
|
||||
const todayDocs = page.getByTestId('doc-card');
|
||||
expect(await todayDocs.count()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('search tab', async ({ page }) => {
|
||||
const searchTab = page
|
||||
.locator('#app-tabs')
|
||||
.getByRole('tab', { name: 'search' });
|
||||
await expect(searchTab).toBeVisible();
|
||||
|
||||
await searchTab.click();
|
||||
|
||||
const searchInput = page.getByTestId('search-header').getByRole('textbox');
|
||||
await expect(searchInput).toBeVisible();
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user