feat(electron): mouse middle click to close tab (#7759)

fix AF-1200
This commit is contained in:
pengx17
2024-08-07 05:19:45 +00:00
parent 352ceca94b
commit b5e543c406
11 changed files with 66 additions and 66 deletions
@@ -1,9 +1,10 @@
import { Tooltip } from '@affine/component/ui/tooltip'; import { Tooltip } from '@affine/component/ui/tooltip';
import { useCatchEventCallback } from '@affine/core/hooks/use-catch-event-hook';
import { SubscriptionPlan } from '@affine/graphql'; import { SubscriptionPlan } from '@affine/graphql';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { useLiveData, useServices } from '@toeverything/infra'; import { useLiveData, useServices } from '@toeverything/infra';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { useCallback, useEffect } from 'react'; import { useEffect } from 'react';
import { openSettingModalAtom } from '../../../atoms'; import { openSettingModalAtom } from '../../../atoms';
import { import {
@@ -35,17 +36,13 @@ export const UserPlanButton = () => {
}, [subscriptionService]); }, [subscriptionService]);
const setSettingModalAtom = useSetAtom(openSettingModalAtom); const setSettingModalAtom = useSetAtom(openSettingModalAtom);
const handleClick = useCallback( const handleClick = useCatchEventCallback(() => {
(e: React.MouseEvent<HTMLDivElement>) => { setSettingModalAtom({
e.stopPropagation(); open: true,
setSettingModalAtom({ activeTab: 'plans',
open: true, scrollAnchor: 'cloudPricingPlan',
activeTab: 'plans', });
scrollAnchor: 'cloudPricingPlan', }, [setSettingModalAtom]);
});
},
[setSettingModalAtom]
);
const t = useI18n(); const t = useI18n();
@@ -6,6 +6,7 @@ import {
import { Avatar } from '@affine/component/ui/avatar'; import { Avatar } from '@affine/component/ui/avatar';
import { Button } from '@affine/component/ui/button'; import { Button } from '@affine/component/ui/button';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useCatchEventCallback } from '@affine/core/hooks/use-catch-event-hook';
import { track } from '@affine/core/mixpanel'; import { track } from '@affine/core/mixpanel';
import { SubscriptionPlan } from '@affine/graphql'; import { SubscriptionPlan } from '@affine/graphql';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
@@ -17,7 +18,7 @@ import {
useServices, useServices,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import type { FC, MouseEvent } from 'react'; import type { FC } from 'react';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { import {
@@ -53,14 +54,10 @@ export const UserAvatar = () => {
[session] [session]
); );
const handleRemoveUserAvatar = useAsyncCallback( const handleRemoveUserAvatar = useCatchEventCallback(async () => {
async (e: MouseEvent<HTMLButtonElement>) => { track.$.settingsPanel.accountSettings.removeAvatar();
track.$.settingsPanel.accountSettings.removeAvatar(); await session.removeAvatar();
e.stopPropagation(); }, [session]);
await session.removeAvatar();
},
[session]
);
return ( return (
<Upload <Upload
@@ -2,14 +2,14 @@ import { FlexWrapper, Input, notify, Wrapper } from '@affine/component';
import { Button } from '@affine/component/ui/button'; import { Button } from '@affine/component/ui/button';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar'; import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import { Upload } from '@affine/core/components/pure/file-upload'; import { Upload } from '@affine/core/components/pure/file-upload';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useCatchEventCallback } from '@affine/core/hooks/use-catch-event-hook';
import { WorkspacePermissionService } from '@affine/core/modules/permissions'; import { WorkspacePermissionService } from '@affine/core/modules/permissions';
import { validateAndReduceImage } from '@affine/core/utils/reduce-image'; import { validateAndReduceImage } from '@affine/core/utils/reduce-image';
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant'; import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { CameraIcon } from '@blocksuite/icons/rc'; import { CameraIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra'; import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
import type { KeyboardEvent, MouseEvent } from 'react'; import type { KeyboardEvent } from 'react';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import * as style from './style.css'; import * as style from './style.css';
@@ -106,13 +106,9 @@ export const ProfilePanel = () => {
handleUpdateWorkspaceName(input); handleUpdateWorkspaceName(input);
}, [handleUpdateWorkspaceName, input]); }, [handleUpdateWorkspaceName, input]);
const handleRemoveUserAvatar = useAsyncCallback( const handleRemoveUserAvatar = useCatchEventCallback(async () => {
async (e: MouseEvent<HTMLButtonElement>) => { await setWorkspaceAvatar(null);
e.stopPropagation(); }, [setWorkspaceAvatar]);
await setWorkspaceAvatar(null);
},
[setWorkspaceAvatar]
);
const handleUploadAvatar = useCallback( const handleUploadAvatar = useCallback(
(file: File) => { (file: File) => {
@@ -1,3 +1,4 @@
import { useCatchEventCallback } from '@affine/core/hooks/use-catch-event-hook';
import { track } from '@affine/core/mixpanel'; import { track } from '@affine/core/mixpanel';
import { CloseIcon, DownloadIcon } from '@blocksuite/icons/rc'; import { CloseIcon, DownloadIcon } from '@blocksuite/icons/rc';
import clsx from 'clsx'; import clsx from 'clsx';
@@ -15,7 +16,7 @@ export function AppDownloadButton({
}) { }) {
const [show, setShow] = useState(true); const [show, setShow] = useState(true);
const handleClose = useCallback(() => { const handleClose = useCatchEventCallback(() => {
setShow(false); setShow(false);
}, []); }, []);
@@ -39,13 +40,7 @@ export function AppDownloadButton({
<DownloadIcon className={styles.icon} /> <DownloadIcon className={styles.icon} />
<span className={styles.ellipsisTextOverflow}>Download App</span> <span className={styles.ellipsisTextOverflow}>Download App</span>
</div> </div>
<div <div className={styles.closeIcon} onClick={handleClose}>
className={styles.closeIcon}
onClick={e => {
e.stopPropagation();
handleClose();
}}
>
<CloseIcon /> <CloseIcon />
</div> </div>
<div className={styles.particles} aria-hidden="true"></div> <div className={styles.particles} aria-hidden="true"></div>
@@ -1,4 +1,5 @@
import { Tooltip } from '@affine/component'; import { Tooltip } from '@affine/component';
import { useCatchEventCallback } from '@affine/core/hooks/use-catch-event-hook';
import { popupWindow } from '@affine/core/utils'; import { popupWindow } from '@affine/core/utils';
import { Unreachable } from '@affine/env/constant'; import { Unreachable } from '@affine/env/constant';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
@@ -118,13 +119,9 @@ function OpenDownloadPage({ updateAvailable }: ButtonContentProps) {
function WhatsNew({ onDismissChangelog }: ButtonContentProps) { function WhatsNew({ onDismissChangelog }: ButtonContentProps) {
const t = useI18n(); const t = useI18n();
const onClickClose: React.MouseEventHandler = useCallback( const onClickClose = useCatchEventCallback(() => {
e => { onDismissChangelog();
onDismissChangelog(); }, [onDismissChangelog]);
e.stopPropagation();
},
[onDismissChangelog]
);
return ( return (
<> <>
<div className={clsx([styles.whatsNewLabel])}> <div className={clsx([styles.whatsNewLabel])}>
@@ -1,11 +1,11 @@
import { Menu } from '@affine/component'; import { Menu } from '@affine/component';
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 { 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 type { MouseEventHandler } from 'react'; import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { stopPropagation } from '../utils'; import { stopPropagation } from '../utils';
import * as styles from './page-tags.css'; import * as styles from './page-tags.css';
@@ -62,13 +62,9 @@ export const TagItem = ({
}: TagItemProps) => { }: TagItemProps) => {
const value = useLiveData(tag?.value$); const value = useLiveData(tag?.value$);
const color = useLiveData(tag?.color$); const color = useLiveData(tag?.color$);
const handleRemove: MouseEventHandler = useCallback( const handleRemove = useCatchEventCallback(() => {
e => { onRemoved?.();
e.stopPropagation(); }, [onRemoved]);
onRemoved?.();
},
[onRemoved]
);
return ( return (
<div <div
data-testid="page-tag" data-testid="page-tag"
@@ -1,4 +1,5 @@
import { Button, Input, Modal } from '@affine/component'; import { Button, Input, Modal } from '@affine/component';
import { useCatchEventCallback } from '@affine/core/hooks/use-catch-event-hook';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import type { KeyboardEvent } from 'react'; import type { KeyboardEvent } from 'react';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
@@ -74,7 +75,7 @@ export const CreateCollection = ({
} }
onConfirm(value); onConfirm(value);
}, [onConfirm, value, isNameEmpty]); }, [onConfirm, value, isNameEmpty]);
const onKeyDown = useCallback( const onKeyDown = useCatchEventCallback(
(e: KeyboardEvent<HTMLInputElement>) => { (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Escape') { if (e.key === 'Escape') {
if (isNameEmpty) { if (isNameEmpty) {
@@ -83,7 +84,6 @@ export const CreateCollection = ({
e.currentTarget.blur(); e.currentTarget.blur();
} }
} }
e.stopPropagation();
}, },
[isNameEmpty] [isNameEmpty]
); );
@@ -1,4 +1,4 @@
import React from 'react'; import React, { type DependencyList } from 'react';
export type AsyncErrorHandler = (error: Error) => void; export type AsyncErrorHandler = (error: Error) => void;
@@ -17,7 +17,7 @@ export const AsyncCallbackContext = React.createContext<AsyncErrorHandler>(
*/ */
export function useAsyncCallback<T extends any[]>( export function useAsyncCallback<T extends any[]>(
callback: (...args: T) => Promise<void>, callback: (...args: T) => Promise<void>,
deps: any[] deps: DependencyList
): (...args: T) => void { ): (...args: T) => void {
const handleAsyncError = React.useContext(AsyncCallbackContext); const handleAsyncError = React.useContext(AsyncCallbackContext);
return React.useCallback( return React.useCallback(
@@ -0,0 +1,17 @@
import { type DependencyList, type SyntheticEvent } from 'react';
import { useAsyncCallback } from './affine-async-hooks';
export const useCatchEventCallback = <E extends SyntheticEvent>(
cb: (e: E) => void | Promise<void>,
deps: DependencyList
) => {
return useAsyncCallback(
async (e: E) => {
e.stopPropagation();
await cb(e);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
deps
);
};
@@ -14,6 +14,7 @@ import {
import { appSidebarWidthAtom } from '@affine/core/components/app-sidebar/index.jotai'; import { appSidebarWidthAtom } from '@affine/core/components/app-sidebar/index.jotai';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls'; import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useCatchEventCallback } from '@affine/core/hooks/use-catch-event-hook';
import type { AffineDNDData } from '@affine/core/types/dnd'; import type { AffineDNDData } from '@affine/core/types/dnd';
import { apis, events } from '@affine/electron-api'; import { apis, events } from '@affine/electron-api';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
@@ -92,15 +93,19 @@ const WorkbenchTab = ({
}, },
[tabsHeaderService, workbench.id] [tabsHeaderService, workbench.id]
); );
const onCloseTab: MouseEventHandler = useAsyncCallback( const handleAuxClick: MouseEventHandler = useCatchEventCallback(
async e => { async e => {
e.stopPropagation(); if (e.button === 1) {
await tabsHeaderService.closeTab?.(workbench.id);
await tabsHeaderService.closeTab?.(workbench.id); }
}, },
[tabsHeaderService, workbench.id] [tabsHeaderService, workbench.id]
); );
const handleCloseTab = useCatchEventCallback(async () => {
await tabsHeaderService.closeTab?.(workbench.id);
}, [tabsHeaderService, workbench.id]);
const { dropTargetRef, closestEdge } = useDropTarget<AffineDNDData>( const { dropTargetRef, closestEdge } = useDropTarget<AffineDNDData>(
() => ({ () => ({
closestEdge: { closestEdge: {
@@ -154,6 +159,7 @@ const WorkbenchTab = ({
onContextMenu={() => { onContextMenu={() => {
onContextMenu(viewIdx); onContextMenu(viewIdx);
}} }}
onAuxClick={handleAuxClick}
onClick={e => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
onActivateView(viewIdx); onActivateView(viewIdx);
@@ -185,7 +191,7 @@ const WorkbenchTab = ({
<button <button
data-testid="close-tab-button" data-testid="close-tab-button"
className={styles.tabCloseButton} className={styles.tabCloseButton}
onClick={onCloseTab} onClick={handleCloseTab}
> >
<CloseIcon /> <CloseIcon />
</button> </button>
@@ -1,5 +1,5 @@
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useCatchEventCallback } from '@affine/core/hooks/use-catch-event-hook';
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';
@@ -21,10 +21,9 @@ export const WorkbenchLink = forwardRef<
const link = const link =
basename + basename +
(typeof to === 'string' ? to : `${to.pathname}${to.search}${to.hash}`); (typeof to === 'string' ? to : `${to.pathname}${to.search}${to.hash}`);
const handleClick = useAsyncCallback( const handleClick = useCatchEventCallback(
async (event: React.MouseEvent<HTMLAnchorElement>) => { async (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation();
if (onClick?.(event) === false) { if (onClick?.(event) === false) {
return; return;
} }