feat: mid click links to open in new tab (#7784)

fix AF-1200
This commit is contained in:
pengx17 2024-08-08 09:43:35 +00:00
parent fd6e198295
commit 7fca13076a
No known key found for this signature in database
GPG Key ID: 23F23D9E8B3971ED
22 changed files with 163 additions and 78 deletions

View File

@ -247,7 +247,8 @@ const config = {
'react-hooks/exhaustive-deps': [ 'react-hooks/exhaustive-deps': [
'warn', 'warn',
{ {
additionalHooks: '(useAsyncCallback|useDraggable|useDropTarget)', additionalHooks:
'(useAsyncCallback|useCatchEventCallback|useDraggable|useDropTarget)',
}, },
], ],
}, },

View File

@ -106,7 +106,6 @@ export function AffinePageReference({
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
peekView.open(ref.current).catch(console.error); peekView.open(ref.current).catch(console.error);
return false; // means this click is handled
} }
if (isInPeekView) { if (isInPeekView) {
peekView.close(); peekView.close();

View File

@ -29,6 +29,7 @@ export function AddPageButton({
style={style} style={style}
className={clsx([styles.root, className])} className={clsx([styles.root, className])}
onClick={onClick} onClick={onClick}
onAuxClick={onClick}
> >
<PlusIcon /> <PlusIcon />
</IconButton> </IconButton>

View File

@ -1,13 +1,14 @@
import { Checkbox, useDraggable } from '@affine/component'; import { Checkbox, useDraggable } from '@affine/component';
import { WorkbenchLink } from '@affine/core/modules/workbench'; import { WorkbenchLink } from '@affine/core/modules/workbench';
import type { AffineDNDData } from '@affine/core/types/dnd'; import type { AffineDNDData } from '@affine/core/types/dnd';
import { stopPropagation } from '@affine/core/utils';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import type { ForwardedRef, PropsWithChildren } from 'react'; import type { ForwardedRef, PropsWithChildren } from 'react';
import { forwardRef, useCallback, useMemo } from 'react'; import { forwardRef, useCallback, useMemo } from 'react';
import { selectionStateAtom, useAtom } from '../scoped-atoms'; import { selectionStateAtom, useAtom } from '../scoped-atoms';
import type { CollectionListItemProps, PageListItemProps } from '../types'; import type { CollectionListItemProps, PageListItemProps } from '../types';
import { ColWrapper, stopPropagation } from '../utils'; import { ColWrapper } from '../utils';
import * as styles from './collection-list-item.css'; import * as styles from './collection-list-item.css';
const ListTitleCell = ({ const ListTitleCell = ({

View File

@ -35,6 +35,7 @@ export const CreateNewPagePopup = ({
desc={t['com.affine.write_with_a_blank_page']()} desc={t['com.affine.write_with_a_blank_page']()}
right={<PageIcon width={20} height={20} />} right={<PageIcon width={20} height={20} />}
onClick={createNewPage} onClick={createNewPage}
onAuxClick={createNewPage}
data-testid="new-page-button-in-all-page" data-testid="new-page-button-in-all-page"
/> />
<BlockCard <BlockCard
@ -42,6 +43,7 @@ export const CreateNewPagePopup = ({
desc={t['com.affine.draw_with_a_blank_whiteboard']()} desc={t['com.affine.draw_with_a_blank_whiteboard']()}
right={<EdgelessIcon width={20} height={20} />} right={<EdgelessIcon width={20} height={20} />}
onClick={createNewEdgeless} onClick={createNewEdgeless}
onAuxClick={createNewEdgeless}
data-testid="new-edgeless-button-in-all-page" data-testid="new-edgeless-button-in-all-page"
/> />
{importFile ? ( {importFile ? (
@ -117,6 +119,7 @@ export const NewPageButton = ({
<DropdownButton <DropdownButton
size={size} size={size}
onClick={handleCreateNewPage} onClick={handleCreateNewPage}
onAuxClick={handleCreateNewPage}
onClickDropDown={useCallback(() => setOpen(open => !open), [])} onClickDropDown={useCallback(() => setOpen(open => !open), [])}
> >
{children} {children}

View File

@ -10,6 +10,7 @@ import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import { track } from '@affine/core/mixpanel'; import { track } from '@affine/core/mixpanel';
import type { Tag } from '@affine/core/modules/tag'; import type { Tag } from '@affine/core/modules/tag';
import { TagService } from '@affine/core/modules/tag'; import { TagService } from '@affine/core/modules/tag';
import { isNewTabTrigger } from '@affine/core/utils';
import type { Collection } from '@affine/env/filter'; import type { Collection } from '@affine/env/filter';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { import {
@ -66,12 +67,9 @@ export const PageListHeader = () => {
size="small" size="small"
testId="new-page-button-trigger" testId="new-page-button-trigger"
onCreateEdgeless={e => onCreateEdgeless={e =>
// todo: abstract this for ctrl check createEdgeless(isNewTabTrigger(e) ? 'new-tab' : true)
createEdgeless(e?.metaKey || e?.ctrlKey ? 'new-tab' : true)
}
onCreatePage={e =>
createPage(e?.metaKey || e?.ctrlKey ? 'new-tab' : true)
} }
onCreatePage={e => createPage(isNewTabTrigger(e) ? 'new-tab' : true)}
onImportFile={onImportFile} onImportFile={onImportFile}
> >
<div className={styles.buttonText}>{t['New Page']()}</div> <div className={styles.buttonText}>{t['New Page']()}</div>

View File

@ -1,6 +1,7 @@
import { Checkbox, Tooltip, useDraggable } from '@affine/component'; import { Checkbox, Tooltip, useDraggable } from '@affine/component';
import { TagService } from '@affine/core/modules/tag'; import { TagService } from '@affine/core/modules/tag';
import type { AffineDNDData } from '@affine/core/types/dnd'; import type { AffineDNDData } from '@affine/core/types/dnd';
import { stopPropagation } from '@affine/core/utils';
import { i18nTime } from '@affine/i18n'; import { i18nTime } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import type { ForwardedRef, PropsWithChildren } from 'react'; import type { ForwardedRef, PropsWithChildren } from 'react';
@ -15,7 +16,7 @@ import {
} from '../scoped-atoms'; } from '../scoped-atoms';
import type { PageListItemProps } from '../types'; import type { PageListItemProps } from '../types';
import { useAllDocDisplayProperties } from '../use-all-doc-display-properties'; import { useAllDocDisplayProperties } from '../use-all-doc-display-properties';
import { ColWrapper, stopPropagation } from '../utils'; import { ColWrapper } from '../utils';
import * as styles from './page-list-item.css'; import * as styles from './page-list-item.css';
import { PageTags } from './page-tags'; import { PageTags } from './page-tags';
@ -310,18 +311,18 @@ const PageListItemWrapper = forwardRef(
if (!selectionState.selectable) { if (!selectionState.selectable) {
return; return;
} }
stopPropagation(e); e.stopPropagation();
const currentIndex = pageIds.indexOf(pageId); const currentIndex = pageIds.indexOf(pageId);
if (e.shiftKey) { if (e.shiftKey) {
e.preventDefault();
if (!selectionState.selectionActive) { if (!selectionState.selectionActive) {
setSelectionActive(true); setSelectionActive(true);
setAnchorIndex(currentIndex); setAnchorIndex(currentIndex);
onClick?.(); onClick?.();
return false; } else {
handleShiftClick(currentIndex);
} }
handleShiftClick(currentIndex);
return false;
} else { } else {
setAnchorIndex(undefined); setAnchorIndex(undefined);
setRangeIds([]); setRangeIds([]);

View File

@ -1,13 +1,13 @@
import { Menu } from '@affine/component'; import { Menu } from '@affine/component';
import { useCatchEventCallback } from '@affine/core/hooks/use-catch-event-hook'; import { useCatchEventCallback } from '@affine/core/hooks/use-catch-event-hook';
import type { Tag } from '@affine/core/modules/tag'; import type { Tag } from '@affine/core/modules/tag';
import { stopPropagation } from '@affine/core/utils';
import { CloseIcon, MoreHorizontalIcon } from '@blocksuite/icons/rc'; import { CloseIcon, MoreHorizontalIcon } from '@blocksuite/icons/rc';
import { LiveData, useLiveData } from '@toeverything/infra'; import { LiveData, useLiveData } from '@toeverything/infra';
import { assignInlineVars } from '@vanilla-extract/dynamic'; import { assignInlineVars } from '@vanilla-extract/dynamic';
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { stopPropagation } from '../utils';
import * as styles from './page-tags.css'; import * as styles from './page-tags.css';
export interface PageTagsProps { export interface PageTagsProps {

View File

@ -9,6 +9,7 @@ import {
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper'; import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper'; import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
import { useCatchEventCallback } from '@affine/core/hooks/use-catch-event-hook';
import { track } from '@affine/core/mixpanel'; import { track } from '@affine/core/mixpanel';
import { FavoriteService } from '@affine/core/modules/favorite'; import { FavoriteService } from '@affine/core/modules/favorite';
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties'; import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
@ -277,18 +278,30 @@ export const TrashOperationCell = ({
const t = useI18n(); const t = useI18n();
const { openConfirmModal } = useConfirmModal(); const { openConfirmModal } = useConfirmModal();
const onConfirmPermanentlyDelete = useCallback(() => { const onConfirmPermanentlyDelete = useCatchEventCallback(
openConfirmModal({ e => {
title: `${t['com.affine.trashOperation.deletePermanently']()}?`, e.preventDefault();
description: t['com.affine.trashOperation.deleteDescription'](), openConfirmModal({
cancelText: t['Cancel'](), title: `${t['com.affine.trashOperation.deletePermanently']()}?`,
confirmText: t['com.affine.trashOperation.delete'](), description: t['com.affine.trashOperation.deleteDescription'](),
confirmButtonOptions: { cancelText: t['Cancel'](),
variant: 'error', confirmText: t['com.affine.trashOperation.delete'](),
}, confirmButtonOptions: {
onConfirm: onPermanentlyDeletePage, variant: 'error',
}); },
}, [onPermanentlyDeletePage, openConfirmModal, t]); onConfirm: onPermanentlyDeletePage,
});
},
[onPermanentlyDeletePage, openConfirmModal, t]
);
const handleRestorePage = useCatchEventCallback(
e => {
e.preventDefault();
onRestorePage();
},
[onRestorePage]
);
return ( return (
<ColWrapper flex={1}> <ColWrapper flex={1}>
@ -297,9 +310,7 @@ export const TrashOperationCell = ({
tooltipOptions={tooltipSideTop} tooltipOptions={tooltipSideTop}
data-testid="restore-page-button" data-testid="restore-page-button"
style={{ marginRight: '12px' }} style={{ marginRight: '12px' }}
onClick={() => { onClick={handleRestorePage}
onRestorePage();
}}
size="20" size="20"
> >
<ResetIcon /> <ResetIcon />

View File

@ -1,5 +1,6 @@
import type { CheckboxProps } from '@affine/component'; import type { CheckboxProps } from '@affine/component';
import { Checkbox } from '@affine/component'; import { Checkbox } from '@affine/component';
import { useCatchEventCallback } from '@affine/core/hooks/use-catch-event-hook';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { MultiSelectIcon } from '@blocksuite/icons/rc'; import { MultiSelectIcon } from '@blocksuite/icons/rc';
import clsx from 'clsx'; import clsx from 'clsx';
@ -19,7 +20,6 @@ import {
useAtomValue, useAtomValue,
} from './scoped-atoms'; } from './scoped-atoms';
import type { HeaderColDef, ListItem } from './types'; import type { HeaderColDef, ListItem } from './types';
import { stopPropagation } from './utils';
// the checkbox on the header has three states: // the checkbox on the header has three states:
// when list selectable = true, the checkbox will be presented // when list selectable = true, the checkbox will be presented
@ -28,23 +28,19 @@ import { stopPropagation } from './utils';
const ListHeaderCheckbox = () => { const ListHeaderCheckbox = () => {
const [selectionState, setSelectionState] = useAtom(selectionStateAtom); const [selectionState, setSelectionState] = useAtom(selectionStateAtom);
const items = useAtomValue(itemsAtom); const items = useAtomValue(itemsAtom);
const onActivateSelection: MouseEventHandler = useCallback( const onActivateSelection: MouseEventHandler = useCatchEventCallback(() => {
e => { setSelectionState(true);
stopPropagation(e); }, [setSelectionState]);
setSelectionState(true);
},
[setSelectionState]
);
const handlers = useAtomValue(listHandlersAtom); const handlers = useAtomValue(listHandlersAtom);
const onChange: NonNullable<CheckboxProps['onChange']> = useCallback( const onChange: NonNullable<CheckboxProps['onChange']> =
(e, checked) => { useCatchEventCallback(
stopPropagation(e); (_e, checked) => {
handlers.onSelectedIdsChange?.( handlers.onSelectedIdsChange?.(
checked ? (items ?? []).map(i => i.id) : [] checked ? (items ?? []).map(i => i.id) : []
); );
}, },
[handlers, items] [handlers, items]
); );
if (!selectionState.selectable) { if (!selectionState.selectable) {
return null; return null;

View File

@ -1,13 +1,14 @@
import { Checkbox, useDraggable } from '@affine/component'; import { Checkbox, useDraggable } from '@affine/component';
import { WorkbenchLink } from '@affine/core/modules/workbench'; import { WorkbenchLink } from '@affine/core/modules/workbench';
import type { AffineDNDData } from '@affine/core/types/dnd'; import type { AffineDNDData } from '@affine/core/types/dnd';
import { stopPropagation } from '@affine/core/utils';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import type { ForwardedRef, PropsWithChildren } from 'react'; import type { ForwardedRef, PropsWithChildren } from 'react';
import { forwardRef, useCallback, useMemo } from 'react'; import { forwardRef, useCallback, useMemo } from 'react';
import { selectionStateAtom, useAtom } from '../scoped-atoms'; import { selectionStateAtom, useAtom } from '../scoped-atoms';
import type { TagListItemProps } from '../types'; import type { TagListItemProps } from '../types';
import { ColWrapper, stopPropagation } from '../utils'; import { ColWrapper } from '../utils';
import * as styles from './tag-list-item.css'; import * as styles from './tag-list-item.css';
const TagListTitleCell = ({ const TagListTitleCell = ({

View File

@ -1,5 +1,4 @@
import clsx from 'clsx'; import clsx from 'clsx';
import type { BaseSyntheticEvent } from 'react';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import * as styles from './list.css'; import * as styles from './list.css';
@ -58,14 +57,6 @@ export const betweenDaysAgo = (
return !withinDaysAgo(date, days0) && withinDaysAgo(date, days1); return !withinDaysAgo(date, days0) && withinDaysAgo(date, days1);
}; };
export function stopPropagation(event: BaseSyntheticEvent) {
event.stopPropagation();
event.preventDefault();
}
export function stopPropagationWithoutPrevent(event: BaseSyntheticEvent) {
event.stopPropagation();
}
// credit: https://github.com/facebook/fbjs/blob/main/packages/fbjs/src/core/shallowEqual.js // credit: https://github.com/facebook/fbjs/blob/main/packages/fbjs/src/core/shallowEqual.js
export function shallowEqual(objA: any, objB: any) { export function shallowEqual(objA: any, objB: any) {
if (Object.is(objA, objB)) { if (Object.is(objA, objB)) {

View File

@ -9,6 +9,7 @@ import {
} from '@affine/core/modules/explorer'; } from '@affine/core/modules/explorer';
import { ExplorerTags } from '@affine/core/modules/explorer/views/sections/tags'; import { ExplorerTags } from '@affine/core/modules/explorer/views/sections/tags';
import { CMDKQuickSearchService } from '@affine/core/modules/quicksearch/services/cmdk'; import { CMDKQuickSearchService } from '@affine/core/modules/quicksearch/services/cmdk';
import { isNewTabTrigger } from '@affine/core/utils';
import { events } from '@affine/electron-api'; import { events } from '@affine/electron-api';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { AllDocsIcon, SettingsIcon } from '@blocksuite/icons/rc'; import { AllDocsIcon, SettingsIcon } from '@blocksuite/icons/rc';
@ -85,9 +86,7 @@ export const RootAppSidebar = (): ReactElement => {
const onClickNewPage = useAsyncCallback( const onClickNewPage = useAsyncCallback(
async (e?: MouseEvent) => { async (e?: MouseEvent) => {
const page = pageHelper.createPage( const page = pageHelper.createPage(isNewTabTrigger(e) ? 'new-tab' : true);
e?.ctrlKey || e?.metaKey ? 'new-tab' : true
);
page.load(); page.load();
track.$.navigationPanel.$.createDoc(); track.$.navigationPanel.$.createDoc();
}, },

View File

@ -1,13 +1,15 @@
import { useCatchEventCallback } from '@affine/core/hooks/use-catch-event-hook';
import { import {
useJournalInfoHelper, useJournalInfoHelper,
useJournalRouteHelper, useJournalRouteHelper,
} from '@affine/core/hooks/use-journal'; } from '@affine/core/hooks/use-journal';
import { WorkbenchService } from '@affine/core/modules/workbench'; import { WorkbenchService } from '@affine/core/modules/workbench';
import type { DocCollection } from '@affine/core/shared'; import type { DocCollection } from '@affine/core/shared';
import { isNewTabTrigger } from '@affine/core/utils';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { TodayIcon, TomorrowIcon, YesterdayIcon } from '@blocksuite/icons/rc'; import { TodayIcon, TomorrowIcon, YesterdayIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { type MouseEvent, useCallback } from 'react'; import { type MouseEvent } from 'react';
import { MenuItem } from '../app-sidebar'; import { MenuItem } from '../app-sidebar';
@ -27,9 +29,9 @@ export const AppSidebarJournalButton = ({
location.pathname.split('/')[1] location.pathname.split('/')[1]
); );
const handleOpenToday = useCallback( const handleOpenToday = useCatchEventCallback(
(e: MouseEvent) => { (e: MouseEvent) => {
openToday(e.ctrlKey || e.metaKey); openToday(isNewTabTrigger(e));
}, },
[openToday] [openToday]
); );
@ -48,6 +50,7 @@ export const AppSidebarJournalButton = ({
data-testid="slider-bar-journals-button" data-testid="slider-bar-journals-button"
active={isJournal} active={isJournal}
onClick={handleOpenToday} onClick={handleOpenToday}
onAuxClick={handleOpenToday}
icon={<Icon />} icon={<Icon />}
> >
{t['com.affine.journal.app-sidebar-title']()} {t['com.affine.journal.app-sidebar-title']()}

View File

@ -2,14 +2,17 @@ import { type DependencyList, type SyntheticEvent } from 'react';
import { useAsyncCallback } from './affine-async-hooks'; import { useAsyncCallback } from './affine-async-hooks';
export const useCatchEventCallback = <E extends SyntheticEvent>( export const useCatchEventCallback = <
cb: (e: E) => void | Promise<void>, E extends SyntheticEvent,
Args extends any[],
>(
cb: (e: E, ...args: Args) => void | Promise<void>,
deps: DependencyList deps: DependencyList
) => { ) => {
return useAsyncCallback( return useAsyncCallback(
async (e: E) => { async (e: E, ...args: Args) => {
e.stopPropagation(); e.stopPropagation();
await cb(e); await cb(e, ...args);
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
deps deps

View File

@ -15,6 +15,7 @@ import {
} from '@affine/core/modules/favorite'; } from '@affine/core/modules/favorite';
import { WorkbenchService } from '@affine/core/modules/workbench'; import { WorkbenchService } from '@affine/core/modules/workbench';
import type { AffineDNDData } from '@affine/core/types/dnd'; import type { AffineDNDData } from '@affine/core/types/dnd';
import { isNewTabTrigger } from '@affine/core/utils';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { PlusIcon } from '@blocksuite/icons/rc'; import { PlusIcon } from '@blocksuite/icons/rc';
import { DocsService, useLiveData, useServices } from '@toeverything/infra'; import { DocsService, useLiveData, useServices } from '@toeverything/infra';
@ -81,7 +82,7 @@ export const ExplorerFavorites = () => {
favoriteService.favoriteList.indexAt('before') favoriteService.favoriteList.indexAt('before')
); );
workbenchService.workbench.openDoc(newDoc.id, { workbenchService.workbench.openDoc(newDoc.id, {
at: e.ctrlKey || e.metaKey ? 'new-tab' : 'active', at: isNewTabTrigger(e) ? 'new-tab' : 'active',
}); });
explorerSection.setCollapsed(false); explorerSection.setCollapsed(false);
}, },
@ -173,6 +174,7 @@ export const ExplorerFavorites = () => {
data-event-props="$.navigationPanel.favorites.createDoc" data-event-props="$.navigationPanel.favorites.createDoc"
data-event-args-control="addFavorite" data-event-args-control="addFavorite"
onClick={handleCreateNewFavoriteDoc} onClick={handleCreateNewFavoriteDoc}
onAuxClick={handleCreateNewFavoriteDoc}
size="16" size="16"
tooltip={t[ tooltip={t[
'com.affine.rootAppSidebar.explorer.fav-section-add-tooltip' 'com.affine.rootAppSidebar.explorer.fav-section-add-tooltip'

View File

@ -1,5 +1,6 @@
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
import { useCatchEventCallback } from '@affine/core/hooks/use-catch-event-hook'; import { useCatchEventCallback } from '@affine/core/hooks/use-catch-event-hook';
import { isNewTabTrigger } from '@affine/core/utils';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { type To } from 'history'; import { type To } from 'history';
import { forwardRef, type MouseEvent } from 'react'; import { forwardRef, type MouseEvent } from 'react';
@ -11,7 +12,7 @@ export const WorkbenchLink = forwardRef<
React.PropsWithChildren< React.PropsWithChildren<
{ {
to: To; to: To;
onClick?: (e: MouseEvent) => boolean | void; // return false to stop propagation onClick?: (e: MouseEvent) => void;
} & React.HTMLProps<HTMLAnchorElement> } & React.HTMLProps<HTMLAnchorElement>
> >
>(function WorkbenchLink({ to, onClick, ...other }, ref) { >(function WorkbenchLink({ to, onClick, ...other }, ref) {
@ -23,26 +24,33 @@ export const WorkbenchLink = forwardRef<
(typeof to === 'string' ? to : `${to.pathname}${to.search}${to.hash}`); (typeof to === 'string' ? to : `${to.pathname}${to.search}${to.hash}`);
const handleClick = useCatchEventCallback( const handleClick = useCatchEventCallback(
async (event: React.MouseEvent<HTMLAnchorElement>) => { async (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault(); onClick?.(event);
if (onClick?.(event) === false) { if (event.defaultPrevented) {
return; return;
} }
const at = (() => { const at = (() => {
if (event.ctrlKey || event.metaKey) { if (isNewTabTrigger(event)) {
return event.altKey && appSettings.enableMultiView return event.altKey && appSettings.enableMultiView
? 'tail' ? 'tail'
: 'new-tab'; : 'new-tab';
} }
return 'active'; return 'active';
})(); })();
workbench.open(to, { at }); workbench.open(to, { at });
event.preventDefault();
}, },
[appSettings.enableMultiView, onClick, to, workbench] [appSettings.enableMultiView, onClick, to, workbench]
); );
// eslint suspicious runtime error // eslint suspicious runtime error
// eslint-disable-next-line react/no-danger-with-children // eslint-disable-next-line react/no-danger-with-children
return <a {...other} ref={ref} href={link} onClick={handleClick} />; return (
<a
{...other}
ref={ref}
href={link}
onClick={handleClick}
onAuxClick={handleClick}
/>
);
}); });

View File

@ -8,6 +8,7 @@ import { Header } from '@affine/core/components/pure/header';
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab'; import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { track } from '@affine/core/mixpanel'; import { track } from '@affine/core/mixpanel';
import { isNewTabTrigger } from '@affine/core/utils';
import type { Filter } from '@affine/env/filter'; import type { Filter } from '@affine/env/filter';
import { PlusIcon } from '@blocksuite/icons/rc'; import { PlusIcon } from '@blocksuite/icons/rc';
import { useService, WorkspaceService } from '@toeverything/infra'; import { useService, WorkspaceService } from '@toeverything/infra';
@ -60,10 +61,10 @@ export const AllPageHeader = ({
!showCreateNew && styles.headerCreateNewButtonHidden !showCreateNew && styles.headerCreateNewButtonHidden
)} )}
onCreateEdgeless={e => onCreateEdgeless={e =>
createEdgeless(e?.metaKey || e?.ctrlKey ? 'new-tab' : true) createEdgeless(isNewTabTrigger(e) ? 'new-tab' : true)
} }
onCreatePage={e => onCreatePage={e =>
createPage(e?.metaKey || e?.ctrlKey ? 'new-tab' : true) createPage(isNewTabTrigger(e) ? 'new-tab' : true)
} }
onImportFile={onImportFile} onImportFile={onImportFile}
> >

View File

@ -0,0 +1,14 @@
import type { BaseSyntheticEvent } from 'react';
export function stopPropagation(event: BaseSyntheticEvent) {
event.stopPropagation();
}
export function stopEvent(event: BaseSyntheticEvent) {
event.stopPropagation();
event.preventDefault();
}
export function isNewTabTrigger(event?: React.MouseEvent) {
return event ? event.ctrlKey || event.metaKey || event.button === 1 : false;
}

View File

@ -1,4 +1,5 @@
export * from './create-emotion-cache'; export * from './create-emotion-cache';
export * from './event';
export * from './fractional-indexing'; export * from './fractional-indexing';
export * from './popup'; export * from './popup';
export * from './string2color'; export * from './string2color';

View File

@ -10,7 +10,7 @@ import {
import { waitForLogMessage } from '@affine-test/kit/utils/utils'; import { waitForLogMessage } from '@affine-test/kit/utils/utils';
import { expect } from '@playwright/test'; import { expect } from '@playwright/test';
test('New a page and open it ,then favorite it', async ({ test('New a page and open it, then favorite it', async ({
page, page,
workspace, workspace,
}) => { }) => {

View File

@ -8,7 +8,7 @@ import {
} from '@affine-test/kit/utils/page-logic'; } from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test'; import { expect } from '@playwright/test';
test('click btn bew page and open in tab', async ({ page, workspace }) => { test('click btn new page and open in tab', async ({ page, workspace }) => {
await openHomePage(page); await openHomePage(page);
await waitForEditorLoad(page); await waitForEditorLoad(page);
await clickNewPageButton(page); await clickNewPageButton(page);
@ -31,3 +31,54 @@ test('click btn bew page and open in tab', async ({ page, workspace }) => {
expect(currentWorkspace.meta.flavour).toContain('local'); expect(currentWorkspace.meta.flavour).toContain('local');
}); });
test('switch between new page and all page', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
const title = 'this is a new page';
await clickNewPageButton(page, title);
await page.getByTestId('all-pages').click();
const cell = page.getByTestId('page-list-item').getByText(title);
await expect(cell).toBeVisible();
await cell.click();
await expect(getBlockSuiteEditorTitle(page)).toHaveText(title);
await page.getByTestId('all-pages').click();
await expect(cell).toBeVisible();
});
test('ctrl click all page and open in new tab', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
const [newTabPage] = await Promise.all([
page.waitForEvent('popup'),
page.getByTestId('all-pages').click({
modifiers: ['ControlOrMeta'],
}),
]);
await expect(newTabPage).toHaveURL(/\/all/, {
timeout: 15000,
});
});
test('mid click all page and open in new tab', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
const [newTabPage] = await Promise.all([
page.waitForEvent('popup'),
page.getByTestId('all-pages').click({
button: 'middle',
}),
]);
await expect(newTabPage).toHaveURL(/\/all/, {
timeout: 15000,
});
});