From 6a71b28a618d10af7f35c2e6412f1b8b0966555a Mon Sep 17 00:00:00 2001 From: CatsJuice Date: Mon, 2 Dec 2024 02:55:04 +0000 Subject: [PATCH] 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` --- .../src/mobile/components/app-tabs/create.tsx | 51 ++++++++++++++++++ .../src/mobile/components/app-tabs/index.tsx | 8 +-- .../core/src/mobile/components/index.ts | 1 + .../components/navigation-back/index.tsx | 54 +++++++++++++++++++ .../mobile/components/page-header/index.tsx | 42 ++------------- .../workspace/detail/mobile-detail-page.tsx | 6 +-- .../src/mobile/pages/workspace/search.tsx | 24 +++++++-- .../core/src/mobile/views/search/style.css.ts | 8 +++ .../src/modules/editor/entities/editor.ts | 10 +++- tests/affine-mobile/e2e/home.spec.ts | 15 +----- 10 files changed, 156 insertions(+), 63 deletions(-) create mode 100644 packages/frontend/core/src/mobile/components/app-tabs/create.tsx create mode 100644 packages/frontend/core/src/mobile/components/navigation-back/index.tsx diff --git a/packages/frontend/core/src/mobile/components/app-tabs/create.tsx b/packages/frontend/core/src/mobile/components/app-tabs/create.tsx new file mode 100644 index 000000000..657541363 --- /dev/null +++ b/packages/frontend/core/src/mobile/components/app-tabs/create.tsx @@ -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 ( +
+ +
+ ); +}; diff --git a/packages/frontend/core/src/mobile/components/app-tabs/index.tsx b/packages/frontend/core/src/mobile/components/app-tabs/index.tsx index 9e3507f81..cd8f19b3b 100644 --- a/packages/frontend/core/src/mobile/components/app-tabs/index.tsx +++ b/packages/frontend/core/src/mobile/components/app-tabs/index.tsx @@ -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: , }, { - key: 'search', - to: '/search', - Icon: SearchIcon, + key: 'new', + node: , }, ]; diff --git a/packages/frontend/core/src/mobile/components/index.ts b/packages/frontend/core/src/mobile/components/index.ts index 18dd7f3f6..81bb7db8a 100644 --- a/packages/frontend/core/src/mobile/components/index.ts +++ b/packages/frontend/core/src/mobile/components/index.ts @@ -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'; diff --git a/packages/frontend/core/src/mobile/components/navigation-back/index.tsx b/packages/frontend/core/src/mobile/components/navigation-back/index.tsx new file mode 100644 index 000000000..b29efe9f9 --- /dev/null +++ b/packages/frontend/core/src/mobile/components/navigation-back/index.tsx @@ -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 ( + : } + data-testid="page-header-back" + {...otherProps} + /> + ); +}; diff --git a/packages/frontend/core/src/mobile/components/page-header/index.tsx b/packages/frontend/core/src/mobile/components/page-header/index.tsx index 2ff0ce6a9..70ae57ed4 100644 --- a/packages/frontend/core/src/mobile/components/page-header/index.tsx +++ b/packages/frontend/core/src/mobile/components/page-header/index.tsx @@ -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( }, 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 ( <> ( className={clsx(styles.prefix, prefixClassName)} style={prefixStyle} > - {back ? ( - : } - data-testid="page-header-back" - /> - ) : null} + {back ? : null} {prefix} diff --git a/packages/frontend/core/src/mobile/pages/workspace/detail/mobile-detail-page.tsx b/packages/frontend/core/src/mobile/pages/workspace/detail/mobile-detail-page.tsx index ae0f7823a..67f65d143 100644 --- a/packages/frontend/core/src/mobile/pages/workspace/detail/mobile-detail-page.tsx +++ b/packages/frontend/core/src/mobile/pages/workspace/detail/mobile-detail-page.tsx @@ -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} - {date ? ( - - ) : null} + ); diff --git a/packages/frontend/core/src/mobile/pages/workspace/search.tsx b/packages/frontend/core/src/mobile/pages/workspace/search.tsx index 8c379f9d3..5219545a2 100644 --- a/packages/frontend/core/src/mobile/pages/workspace/search.tsx +++ b/packages/frontend/core/src/mobile/pages/workspace/search.tsx @@ -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 ( <>
+ {
{searchInput ? : } - ); }; diff --git a/packages/frontend/core/src/mobile/views/search/style.css.ts b/packages/frontend/core/src/mobile/views/search/style.css.ts index 3123021be..d1628904a 100644 --- a/packages/frontend/core/src/mobile/views/search/style.css.ts +++ b/packages/frontend/core/src/mobile/views/search/style.css.ts @@ -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([ diff --git a/packages/frontend/core/src/modules/editor/entities/editor.ts b/packages/frontend/core/src/modules/editor/entities/editor.ts index dd5921a33..7955e196c 100644 --- a/packages/frontend/core/src/modules/editor/entities/editor.ts +++ b/packages/frontend/core/src/modules/editor/entities/editor.ts @@ -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; diff --git a/tests/affine-mobile/e2e/home.spec.ts b/tests/affine-mobile/e2e/home.spec.ts index c6591fdcb..1f4b4cfe2 100644 --- a/tests/affine-mobile/e2e/home.spec.ts +++ b/tests/affine-mobile/e2e/home.spec.ts @@ -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(); -});