mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 12:28:42 +00:00
feat(mobile): disable swipe back gesture when there is no back in header (#8876)
close AF-1663, AF-1756 - new global `ModalConfigContext` - new logic to judge whether inside modal - render `✕` for PageHeader back if inside modal - only enable `NavigationGesture` when there is `<` in PageHeader
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Button, Modal } from '@affine/component';
|
||||
import { PageHeader } from '@affine/core/mobile/components';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
@@ -8,7 +9,6 @@ import {
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
|
||||
import { PageHeader } from '../page-header';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
interface ConfigModalProps {
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export * from './config-modal';
|
||||
export * from './page-header';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './app-tabs';
|
||||
export * from './doc-card';
|
||||
export * from './page-header';
|
||||
export * from './rename';
|
||||
export * from './search-input';
|
||||
export * from './search-result';
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { IconButton, SafeArea } from '@affine/component';
|
||||
import { ArrowLeftSmallIcon } from '@blocksuite/icons/rc';
|
||||
import { IconButton, SafeArea, useIsInsideModal } from '@affine/component';
|
||||
import { ArrowLeftSmallIcon, CloseIcon } from '@blocksuite/icons/rc';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
forwardRef,
|
||||
type HtmlHTMLAttributes,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
|
||||
import { NavigationGestureService } from '../../modules/navigation-gesture';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export interface PageHeaderProps
|
||||
@@ -60,6 +63,20 @@ 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]);
|
||||
@@ -83,7 +100,7 @@ export const PageHeader = forwardRef<HTMLDivElement, PageHeaderProps>(
|
||||
size={24}
|
||||
style={{ padding: 10 }}
|
||||
onClick={handleRouteBack}
|
||||
icon={<ArrowLeftSmallIcon />}
|
||||
icon={isInsideModal ? <CloseIcon /> : <ArrowLeftSmallIcon />}
|
||||
data-testid="page-header-back"
|
||||
/>
|
||||
) : null}
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Scrollable,
|
||||
useThemeColorMeta,
|
||||
} from '@affine/component';
|
||||
import { PageHeader } from '@affine/core/components/mobile';
|
||||
import { PageHeader } from '@affine/core/mobile/components';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { Framework } from '@toeverything/infra';
|
||||
|
||||
import { configureMobileNavigationGestureModule } from './navigation-gesture';
|
||||
import { configureMobileSearchModule } from './search';
|
||||
import { configureMobileVirtualKeyboardModule } from './virtual-keyboard';
|
||||
|
||||
export function configureMobileModules(framework: Framework) {
|
||||
configureMobileSearchModule(framework);
|
||||
configureMobileVirtualKeyboardModule(framework);
|
||||
configureMobileNavigationGestureModule(framework);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Framework } from '@toeverything/infra';
|
||||
|
||||
import { NavigationGestureProvider } from './providers/navigation-gesture';
|
||||
import { NavigationGestureService } from './services/navigation-gesture';
|
||||
|
||||
export { NavigationGestureProvider, NavigationGestureService };
|
||||
|
||||
export function configureMobileNavigationGestureModule(framework: Framework) {
|
||||
framework.service(
|
||||
NavigationGestureService,
|
||||
f => new NavigationGestureService(f.getOptional(NavigationGestureProvider))
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createIdentifier } from '@toeverything/infra';
|
||||
|
||||
export interface NavigationGestureProvider {
|
||||
isEnabled: () => Promise<boolean>;
|
||||
enable: () => Promise<void>;
|
||||
disable: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const NavigationGestureProvider =
|
||||
createIdentifier<NavigationGestureProvider>('NavigationGestureProvider');
|
||||
@@ -0,0 +1,58 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import {
|
||||
effect,
|
||||
exhaustMapWithTrailing,
|
||||
fromPromise,
|
||||
LiveData,
|
||||
Service,
|
||||
} from '@toeverything/infra';
|
||||
import { catchError, distinctUntilChanged, EMPTY, mergeMap } from 'rxjs';
|
||||
|
||||
import type { NavigationGestureProvider } from '../providers/navigation-gesture';
|
||||
|
||||
const logger = new DebugLogger('affine:navigation-gesture');
|
||||
|
||||
export class NavigationGestureService extends Service {
|
||||
public enabled$ = new LiveData(false);
|
||||
|
||||
constructor(
|
||||
private readonly navigationGestureProvider?: NavigationGestureProvider
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
setEnabled = effect(
|
||||
distinctUntilChanged<boolean>(),
|
||||
exhaustMapWithTrailing((enable: boolean) => {
|
||||
return fromPromise(async () => {
|
||||
if (!this.navigationGestureProvider) {
|
||||
return;
|
||||
}
|
||||
if (enable) {
|
||||
await this.enable();
|
||||
} else {
|
||||
await this.disable();
|
||||
}
|
||||
return;
|
||||
}).pipe(
|
||||
mergeMap(() => EMPTY),
|
||||
catchError(err => {
|
||||
logger.error('navigationGestureProvider error', err);
|
||||
return EMPTY;
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
async enable() {
|
||||
this.enabled$.next(true);
|
||||
logger.debug(`Enable navigation gesture`);
|
||||
return this.navigationGestureProvider?.enable();
|
||||
}
|
||||
|
||||
async disable() {
|
||||
this.enabled$.next(false);
|
||||
logger.debug(`Disable navigation gesture`);
|
||||
return this.navigationGestureProvider?.disable();
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,9 @@ import { useDocMetaHelper } from '@affine/core/components/hooks/use-block-suite-
|
||||
import { usePageDocumentTitle } from '@affine/core/components/hooks/use-global-state';
|
||||
import { useJournalRouteHelper } from '@affine/core/components/hooks/use-journal';
|
||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||
import { PageHeader } from '@affine/core/components/mobile';
|
||||
import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
|
||||
import { DetailPageWrapper } from '@affine/core/desktop/pages/workspace/detail-page/detail-page-wrapper';
|
||||
import { PageHeader } from '@affine/core/mobile/components';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
@@ -202,19 +202,22 @@ const DetailPageImpl = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const skeleton = (
|
||||
const getSkeleton = (back: boolean) => (
|
||||
<>
|
||||
<PageHeader back className={styles.header} />
|
||||
<PageHeader back={back} className={styles.header} />
|
||||
<PageDetailSkeleton />
|
||||
</>
|
||||
);
|
||||
|
||||
const notFound = (
|
||||
const getNotFound = (back: boolean) => (
|
||||
<>
|
||||
<PageHeader back className={styles.header} />
|
||||
<PageHeader back={back} className={styles.header} />
|
||||
Page Not Found (TODO)
|
||||
</>
|
||||
);
|
||||
const skeleton = getSkeleton(false);
|
||||
const skeletonWithBack = getSkeleton(true);
|
||||
const notFound = getNotFound(false);
|
||||
const notFoundWithBack = getNotFound(true);
|
||||
|
||||
const MobileDetailPage = ({
|
||||
pageId,
|
||||
@@ -237,12 +240,12 @@ const MobileDetailPage = ({
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<DetailPageWrapper
|
||||
skeleton={skeleton}
|
||||
notFound={notFound}
|
||||
skeleton={date ? skeleton : skeletonWithBack}
|
||||
notFound={date ? notFound : notFoundWithBack}
|
||||
pageId={pageId}
|
||||
>
|
||||
<PageHeader
|
||||
back
|
||||
back={!date}
|
||||
className={styles.header}
|
||||
suffix={
|
||||
<>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IconButton, MobileMenu } from '@affine/component';
|
||||
import { EmptyCollectionDetail } from '@affine/core/components/affine/empty';
|
||||
import { PageHeader } from '@affine/core/components/mobile';
|
||||
import { isEmptyCollection } from '@affine/core/desktop/pages/workspace/collection';
|
||||
import { PageHeader } from '@affine/core/mobile/components';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons/rc';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IconButton, MobileMenu } from '@affine/component';
|
||||
import { PageHeader } from '@affine/core/components/mobile';
|
||||
import { PageHeader } from '@affine/core/mobile/components';
|
||||
import type { Tag } from '@affine/core/modules/tag';
|
||||
import { MoreHorizontalIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData } from '@toeverything/infra';
|
||||
|
||||
Reference in New Issue
Block a user