Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(component): new hook to open confirm modal #6342

Merged
merged 1 commit into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions packages/frontend/component/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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>
);
},
Expand Down
38 changes: 38 additions & 0 deletions packages/frontend/component/src/ui/modal/confirm-modal.stories.tsx
Original file line number Diff line number Diff line change
@@ -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>;
};
89 changes: 87 additions & 2 deletions packages/frontend/component/src/ui/modal/confirm-modal.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 = ({
Expand All @@ -20,7 +24,9 @@ export const ConfirmModal = ({
// FIXME: we need i18n
cancelText = 'Cancel',
cancelButtonOptions,
reverseFooter,
onConfirm,
onCancel,
width = 480,
...props
}: ConfirmModalProps) => {
Expand All @@ -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,
CatsJuice marked this conversation as resolved.
Show resolved Hide resolved
closeConfirmModal: context.closeConfirmModal,
};
};
4 changes: 4 additions & 0 deletions packages/frontend/component/src/ui/modal/styles.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ export const modalFooter = style({
'&.modalFooterWithChildren': {
paddingTop: '20px',
},
'&.reverse': {
flexDirection: 'row-reverse',
justifyContent: 'flex-start',
},
},
});
export const confirmModalContent = style({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const ConfirmLoadingModal = ({
onOpenChange={onOpenChange}
onConfirm={() => {
confirmed.current = true;
onConfirm?.();
onConfirm?.()?.catch(console.error);
}}
{...props}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as styles from './style.css';

interface WorkspaceDeleteProps extends ConfirmModalProps {
workspaceMetadata: WorkspaceMetadata;
onConfirm?: () => void;
}

export const WorkspaceDeleteModal = ({
Expand Down
Loading