mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-09 21:25:45 +08:00
feat(component): new hook to open confirm modal (#6342)
new exports from `@affine/component`:
```ts
import { ConfirmModalProvider, useConfirmModal } from "@affine/component"
```
Open confirm modal with hook:
```ts
const Component = () => {
const { openConfirmModal } = useConformModal();
const open = () => {
openConfirmModal({
// props of ConfirmModal
/**
* will show loading state when confirm clicked, and close after onConfirm finished
*/
onConfirm: async () => {
await new Promise((r) => setTimeout(r, 2000));
},
});
}
return <Button onClick={open}>Open</Button>
}
```
This commit is contained in:
@@ -8,6 +8,7 @@ import { useDarkMode } from 'storybook-dark-mode';
|
|||||||
|
|
||||||
import type { Preview } from '@storybook/react';
|
import type { Preview } from '@storybook/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { ConfirmModalProvider } from '../src/ui/modal/confirm-modal';
|
||||||
|
|
||||||
export const parameters: Preview = {
|
export const parameters: Preview = {
|
||||||
argTypes: {
|
argTypes: {
|
||||||
@@ -53,9 +54,11 @@ export const decorators = [
|
|||||||
(Story: ComponentType, context) => {
|
(Story: ComponentType, context) => {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider themes={['dark', 'light']} enableSystem={true}>
|
<ThemeProvider themes={['dark', 'light']} enableSystem={true}>
|
||||||
<ThemeChange />
|
<ConfirmModalProvider>
|
||||||
<Component />
|
<ThemeChange />
|
||||||
<Story {...context} />
|
<Component />
|
||||||
|
<Story {...context} />
|
||||||
|
</ConfirmModalProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import type { Meta } from '@storybook/react';
|
||||||
|
|
||||||
|
import { Button } from '../button';
|
||||||
|
import {
|
||||||
|
ConfirmModal,
|
||||||
|
type ConfirmModalProps,
|
||||||
|
useConfirmModal,
|
||||||
|
} from './confirm-modal';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'UI/Modal/Confirm Modal',
|
||||||
|
component: ConfirmModal,
|
||||||
|
argTypes: {},
|
||||||
|
} satisfies Meta<ConfirmModalProps>;
|
||||||
|
|
||||||
|
export const UsingHook = () => {
|
||||||
|
const { openConfirmModal } = useConfirmModal();
|
||||||
|
|
||||||
|
const onConfirm = () =>
|
||||||
|
new Promise<void>(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
const showConfirm = () => {
|
||||||
|
openConfirmModal({
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
confirmButtonOptions: {
|
||||||
|
children: 'Confirm',
|
||||||
|
},
|
||||||
|
title: 'Confirm Modal',
|
||||||
|
children: 'Are you sure you want to confirm?',
|
||||||
|
onConfirm,
|
||||||
|
onCancel: () => {
|
||||||
|
console.log('Cancelled');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Button onClick={showConfirm}>Show confirm</Button>;
|
||||||
|
};
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { DialogTrigger } from '@radix-ui/react-dialog';
|
import { DialogTrigger } from '@radix-ui/react-dialog';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import { createContext, useCallback, useContext, useState } from 'react';
|
||||||
|
|
||||||
import type { ButtonProps } from '../button';
|
import type { ButtonProps } from '../button';
|
||||||
import { Button } from '../button';
|
import { Button } from '../button';
|
||||||
@@ -9,9 +11,11 @@ import * as styles from './styles.css';
|
|||||||
|
|
||||||
export interface ConfirmModalProps extends ModalProps {
|
export interface ConfirmModalProps extends ModalProps {
|
||||||
confirmButtonOptions?: ButtonProps;
|
confirmButtonOptions?: ButtonProps;
|
||||||
onConfirm?: () => void;
|
onConfirm?: (() => void) | (() => Promise<void>);
|
||||||
|
onCancel?: () => void;
|
||||||
cancelText?: string;
|
cancelText?: string;
|
||||||
cancelButtonOptions?: ButtonProps;
|
cancelButtonOptions?: ButtonProps;
|
||||||
|
reverseFooter?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConfirmModal = ({
|
export const ConfirmModal = ({
|
||||||
@@ -20,7 +24,9 @@ export const ConfirmModal = ({
|
|||||||
// FIXME: we need i18n
|
// FIXME: we need i18n
|
||||||
cancelText = 'Cancel',
|
cancelText = 'Cancel',
|
||||||
cancelButtonOptions,
|
cancelButtonOptions,
|
||||||
|
reverseFooter,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
width = 480,
|
width = 480,
|
||||||
...props
|
...props
|
||||||
}: ConfirmModalProps) => {
|
}: ConfirmModalProps) => {
|
||||||
@@ -36,13 +42,92 @@ export const ConfirmModal = ({
|
|||||||
<div
|
<div
|
||||||
className={clsx(styles.modalFooter, {
|
className={clsx(styles.modalFooter, {
|
||||||
modalFooterWithChildren: !!children,
|
modalFooterWithChildren: !!children,
|
||||||
|
reverse: reverseFooter,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button {...cancelButtonOptions}>{cancelText}</Button>
|
<Button onClick={onCancel} {...cancelButtonOptions}>
|
||||||
|
{cancelText}
|
||||||
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<Button onClick={onConfirm} {...confirmButtonOptions}></Button>
|
<Button onClick={onConfirm} {...confirmButtonOptions}></Button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface ConfirmModalContextProps {
|
||||||
|
modalProps: ConfirmModalProps;
|
||||||
|
openConfirmModal: (props?: ConfirmModalProps) => void;
|
||||||
|
closeConfirmModal: () => void;
|
||||||
|
}
|
||||||
|
const ConfirmModalContext = createContext<ConfirmModalContextProps>({
|
||||||
|
modalProps: { open: false },
|
||||||
|
openConfirmModal: () => {},
|
||||||
|
closeConfirmModal: () => {},
|
||||||
|
});
|
||||||
|
export const ConfirmModalProvider = ({ children }: PropsWithChildren) => {
|
||||||
|
const [modalProps, setModalProps] = useState<ConfirmModalProps>({
|
||||||
|
open: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const setLoading = useCallback((value: boolean) => {
|
||||||
|
setModalProps(prev => ({
|
||||||
|
...prev,
|
||||||
|
confirmButtonOptions: {
|
||||||
|
...prev.confirmButtonOptions,
|
||||||
|
loading: value,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeConfirmModal = useCallback(() => {
|
||||||
|
setModalProps({ open: false });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const openConfirmModal = useCallback(
|
||||||
|
(props?: ConfirmModalProps) => {
|
||||||
|
if (!props) {
|
||||||
|
setModalProps({ open: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { onConfirm: _onConfirm, ...otherProps } = props;
|
||||||
|
|
||||||
|
const onConfirm = () => {
|
||||||
|
setLoading(true);
|
||||||
|
_onConfirm?.()
|
||||||
|
?.catch(console.error)
|
||||||
|
?.finally(() => closeConfirmModal());
|
||||||
|
};
|
||||||
|
setModalProps({ ...otherProps, onConfirm, open: true });
|
||||||
|
},
|
||||||
|
[closeConfirmModal, setLoading]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onOpenChange = useCallback((open: boolean) => {
|
||||||
|
setModalProps(props => ({ ...props, open }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmModalContext.Provider
|
||||||
|
value={{ openConfirmModal, closeConfirmModal, modalProps }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{/* TODO: multi-instance support(unnecessary for now) */}
|
||||||
|
<ConfirmModal onOpenChange={onOpenChange} {...modalProps} />
|
||||||
|
</ConfirmModalContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const useConfirmModal = () => {
|
||||||
|
const context = useContext(ConfirmModalContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useConfirmModal must be used within a ConfirmModalProvider'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
openConfirmModal: context.openConfirmModal,
|
||||||
|
closeConfirmModal: context.closeConfirmModal,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ export const modalFooter = style({
|
|||||||
'&.modalFooterWithChildren': {
|
'&.modalFooterWithChildren': {
|
||||||
paddingTop: '20px',
|
paddingTop: '20px',
|
||||||
},
|
},
|
||||||
|
'&.reverse': {
|
||||||
|
flexDirection: 'row-reverse',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
export const confirmModalContent = style({
|
export const confirmModalContent = style({
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-s
|
|||||||
export const EnableAffineCloudModal = ({
|
export const EnableAffineCloudModal = ({
|
||||||
onConfirm: propsOnConfirm,
|
onConfirm: propsOnConfirm,
|
||||||
...props
|
...props
|
||||||
}: ConfirmModalProps) => {
|
}: Omit<ConfirmModalProps, 'onConfirm'> & { onConfirm: () => void }) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const loginStatus = useCurrentLoginStatus();
|
const loginStatus = useCurrentLoginStatus();
|
||||||
const setAuthAtom = useSetAtom(authAtom);
|
const setAuthAtom = useSetAtom(authAtom);
|
||||||
|
|||||||
+1
-1
@@ -55,7 +55,7 @@ export const ConfirmLoadingModal = ({
|
|||||||
onOpenChange={onOpenChange}
|
onOpenChange={onOpenChange}
|
||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
confirmed.current = true;
|
confirmed.current = true;
|
||||||
onConfirm?.();
|
onConfirm?.()?.catch(console.error);
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|||||||
+1
@@ -13,6 +13,7 @@ import * as styles from './style.css';
|
|||||||
|
|
||||||
interface WorkspaceDeleteProps extends ConfirmModalProps {
|
interface WorkspaceDeleteProps extends ConfirmModalProps {
|
||||||
workspaceMetadata: WorkspaceMetadata;
|
workspaceMetadata: WorkspaceMetadata;
|
||||||
|
onConfirm?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WorkspaceDeleteModal = ({
|
export const WorkspaceDeleteModal = ({
|
||||||
|
|||||||
Reference in New Issue
Block a user