mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00: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 React from 'react';
|
||||
import { ConfirmModalProvider } from '../src/ui/modal/confirm-modal';
|
||||
|
||||
export const parameters: Preview = {
|
||||
argTypes: {
|
||||
@@ -53,9 +54,11 @@ export const decorators = [
|
||||
(Story: ComponentType, context) => {
|
||||
return (
|
||||
<ThemeProvider themes={['dark', 'light']} enableSystem={true}>
|
||||
<ThemeChange />
|
||||
<Component />
|
||||
<Story {...context} />
|
||||
<ConfirmModalProvider>
|
||||
<ThemeChange />
|
||||
<Component />
|
||||
<Story {...context} />
|
||||
</ConfirmModalProvider>
|
||||
</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 clsx from 'clsx';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { createContext, useCallback, useContext, useState } from 'react';
|
||||
|
||||
import type { ButtonProps } from '../button';
|
||||
import { Button } from '../button';
|
||||
@@ -9,9 +11,11 @@ import * as styles from './styles.css';
|
||||
|
||||
export interface ConfirmModalProps extends ModalProps {
|
||||
confirmButtonOptions?: ButtonProps;
|
||||
onConfirm?: () => void;
|
||||
onConfirm?: (() => void) | (() => Promise<void>);
|
||||
onCancel?: () => void;
|
||||
cancelText?: string;
|
||||
cancelButtonOptions?: ButtonProps;
|
||||
reverseFooter?: boolean;
|
||||
}
|
||||
|
||||
export const ConfirmModal = ({
|
||||
@@ -20,7 +24,9 @@ export const ConfirmModal = ({
|
||||
// FIXME: we need i18n
|
||||
cancelText = 'Cancel',
|
||||
cancelButtonOptions,
|
||||
reverseFooter,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
width = 480,
|
||||
...props
|
||||
}: ConfirmModalProps) => {
|
||||
@@ -36,13 +42,92 @@ export const ConfirmModal = ({
|
||||
<div
|
||||
className={clsx(styles.modalFooter, {
|
||||
modalFooterWithChildren: !!children,
|
||||
reverse: reverseFooter,
|
||||
})}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button {...cancelButtonOptions}>{cancelText}</Button>
|
||||
<Button onClick={onCancel} {...cancelButtonOptions}>
|
||||
{cancelText}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<Button onClick={onConfirm} {...confirmButtonOptions}></Button>
|
||||
</div>
|
||||
</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': {
|
||||
paddingTop: '20px',
|
||||
},
|
||||
'&.reverse': {
|
||||
flexDirection: 'row-reverse',
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const confirmModalContent = style({
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-s
|
||||
export const EnableAffineCloudModal = ({
|
||||
onConfirm: propsOnConfirm,
|
||||
...props
|
||||
}: ConfirmModalProps) => {
|
||||
}: Omit<ConfirmModalProps, 'onConfirm'> & { onConfirm: () => void }) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const setAuthAtom = useSetAtom(authAtom);
|
||||
|
||||
@@ -55,7 +55,7 @@ export const ConfirmLoadingModal = ({
|
||||
onOpenChange={onOpenChange}
|
||||
onConfirm={() => {
|
||||
confirmed.current = true;
|
||||
onConfirm?.();
|
||||
onConfirm?.()?.catch(console.error);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -13,6 +13,7 @@ import * as styles from './style.css';
|
||||
|
||||
interface WorkspaceDeleteProps extends ConfirmModalProps {
|
||||
workspaceMetadata: WorkspaceMetadata;
|
||||
onConfirm?: () => void;
|
||||
}
|
||||
|
||||
export const WorkspaceDeleteModal = ({
|
||||
|
||||
Reference in New Issue
Block a user