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:
CatsJuice
2024-03-27 13:30:30 +00:00
parent 39facba92e
commit d412635f6b
7 changed files with 138 additions and 7 deletions

View File

@@ -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>
);
},

View File

@@ -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>;
};

View File

@@ -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,
};
};

View File

@@ -65,6 +65,10 @@ export const modalFooter = style({
'&.modalFooterWithChildren': {
paddingTop: '20px',
},
'&.reverse': {
flexDirection: 'row-reverse',
justifyContent: 'flex-start',
},
},
});
export const confirmModalContent = style({

View File

@@ -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);

View File

@@ -55,7 +55,7 @@ export const ConfirmLoadingModal = ({
onOpenChange={onOpenChange}
onConfirm={() => {
confirmed.current = true;
onConfirm?.();
onConfirm?.()?.catch(console.error);
}}
{...props}
>

View File

@@ -13,6 +13,7 @@ import * as styles from './style.css';
interface WorkspaceDeleteProps extends ConfirmModalProps {
workspaceMetadata: WorkspaceMetadata;
onConfirm?: () => void;
}
export const WorkspaceDeleteModal = ({