Skip to content
This repository has been archived by the owner on Apr 5, 2024. It is now read-only.

Commit

Permalink
feat: add new functionalities to useDialog
Browse files Browse the repository at this point in the history
- add ids to the dialogs to support stacking

- add optional callback
functions to the openDialog functions

- dialog options can now be
passed to the openDialog function
  • Loading branch information
nimec01 committed Nov 30, 2022
1 parent 93613a5 commit 3465855
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 98 deletions.
119 changes: 63 additions & 56 deletions src/dialog/DialogComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,48 @@
import React, { FormEvent, MouseEvent, useCallback, useEffect } from 'react';
import React, { FormEvent, MouseEvent, useCallback, useEffect, useMemo } from 'react';
import { createPortal } from 'react-dom';
import clsx from 'clsx';

import { useDialog } from './DialogHook';
import { DialogVisibility } from './DialogReducer';
import { DialogOptions } from '../types';

interface DialogComponentProps {
closeOnOffsiteClick?: boolean;
closeOnEscape?: boolean;
removeDefaultClasses?: boolean;
wrapperClassName?: string;
cardClassName?: string;
cardBodyClassName?: string;
cardActionsClassName?: string;
confirmButtonClassName?: string;
cancelButtonClassName?: string;
confirmButtonLabel?: string;
cancelButtonLabel?: string;
disableConfirmButton?: boolean;
disableCancelButton?: boolean;
}

export function DialogComponent({
closeOnOffsiteClick = true,
closeOnEscape = true,
removeDefaultClasses = false,
wrapperClassName,
cardClassName,
cardBodyClassName,
cardActionsClassName,
confirmButtonClassName,
cancelButtonClassName,
confirmButtonLabel = 'Yes',
cancelButtonLabel = 'No',
disableConfirmButton = false,
disableCancelButton = false,
}: DialogComponentProps) {
export function DialogComponent(componentOptions: DialogOptions) {
const { onConfirm, onCancel, state } = useDialog();

const dialog = useMemo(() => {
return state.dialogs.at(-1);
}, [state]);

const options = useMemo<DialogOptions>(() => {
return {
closeOnOffsiteClick:
dialog?.closeOnOffsiteClick ?? componentOptions.closeOnOffsiteClick ?? true,
closeOnEscape: dialog?.closeOnEscape ?? componentOptions.closeOnEscape ?? true,
removeDefaultClasses:
dialog?.removeDefaultClasses ?? componentOptions.removeDefaultClasses ?? false,
wrapperClassName: dialog?.wrapperClassName ?? componentOptions.wrapperClassName ?? '',
cardClassName: dialog?.cardClassName ?? componentOptions.cardClassName ?? '',
cardBodyClassName: dialog?.cardBodyClassName ?? componentOptions.cardBodyClassName ?? '',
cardActionsClassName:
dialog?.cardActionsClassName ?? componentOptions.cardActionsClassName ?? '',
confirmButtonClassName:
dialog?.confirmButtonClassName ?? componentOptions.confirmButtonClassName ?? '',
cancelButtonClassName:
dialog?.cancelButtonClassName ?? componentOptions.cancelButtonClassName ?? '',
confirmButtonLabel:
dialog?.confirmButtonLabel ?? componentOptions.confirmButtonLabel ?? 'Yes',
cancelButtonLabel: dialog?.cancelButtonLabel ?? componentOptions.cancelButtonLabel ?? 'No',
disableConfirmButton:
dialog?.disableConfirmButton ?? componentOptions.disableConfirmButton ?? false,
disableCancelButton:
dialog?.disableCancelButton ?? componentOptions.disableCancelButton ?? false,
};
}, [dialog]);

const onOffsiteClick = useCallback(
(e: MouseEvent<HTMLDivElement>) => {
const target = e.target as HTMLElement;
if (target.className !== 'dialog__wrapper') return;

onCancel();
onCancel(undefined, dialog?.id);
},
[onCancel],
);
Expand All @@ -52,7 +51,7 @@ export function DialogComponent({
(e: KeyboardEvent) => {
if (e.key !== 'Escape') return;

onCancel();
onCancel(undefined, dialog?.id);
},
[onCancel],
);
Expand All @@ -69,57 +68,65 @@ export function DialogComponent({
});

if (submitter?.dataset?.dialogAction === 'resolve') {
onConfirm(data);
onConfirm(data, dialog?.id);
} else {
onCancel(data);
onCancel(data, dialog?.id);
}
},
[onConfirm, onCancel],
);

useEffect(() => {
if (closeOnEscape) document.addEventListener('keyup', onKeyUp);
if (options.closeOnEscape) document.addEventListener('keyup', onKeyUp);

return () => {
document.removeEventListener('keyup', onKeyUp);
};
}, [closeOnEscape, onKeyUp]);
}, [options.closeOnEscape, onKeyUp]);

const component = (
<div
className={clsx(!removeDefaultClasses && 'dialog__wrapper', wrapperClassName)}
onClick={closeOnOffsiteClick ? onOffsiteClick : undefined}
className={clsx(!options.removeDefaultClasses && 'dialog__wrapper', options.wrapperClassName)}
onClick={options.closeOnOffsiteClick ? onOffsiteClick : undefined}
>
<div className={clsx(!removeDefaultClasses && 'dialog__card', cardClassName)}>
<div className={clsx(!options.removeDefaultClasses && 'dialog__card', options.cardClassName)}>
<form onSubmit={onSubmit}>
{state.content && (
<div className={clsx(!removeDefaultClasses && 'dialog__card__body', cardBodyClassName)}>
{state.content}
{dialog?.content && (
<div
className={clsx(
!options.removeDefaultClasses && 'dialog__card__body',
options.cardBodyClassName,
)}
>
{dialog?.content}
</div>
)}
<div
className={clsx(!removeDefaultClasses && 'dialog__card__actions', cardActionsClassName)}
className={clsx(
!options.removeDefaultClasses && 'dialog__card__actions',
options.cardActionsClassName,
)}
>
{!disableConfirmButton && (
{!options.disableConfirmButton && (
<button
className={clsx(
!removeDefaultClasses && 'dialog__button dialog__success',
confirmButtonClassName,
!options.removeDefaultClasses && 'dialog__button dialog__success',
options.confirmButtonClassName,
)}
data-dialog-action="resolve"
>
{confirmButtonLabel}
{options.confirmButtonLabel}
</button>
)}
{!disableCancelButton && (
{!options.disableCancelButton && (
<button
className={clsx(
!removeDefaultClasses && 'dialog__button dialog__error',
cancelButtonClassName,
!options.removeDefaultClasses && 'dialog__button dialog__error',
options.cancelButtonClassName,
)}
data-dialog-action="reject"
>
{cancelButtonLabel}
{options.cancelButtonLabel}
</button>
)}
</div>
Expand All @@ -129,7 +136,7 @@ export function DialogComponent({
);

return createPortal(
state.visibility === DialogVisibility.VISIBLE ? component : null,
state.dialogs.length > 0 ? component : null,
document.getElementById('dialog') as HTMLElement,
);
}
7 changes: 4 additions & 3 deletions src/dialog/DialogContext.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { createContext, Dispatch } from 'react';
import { DialogState, initialState } from './DialogReducer';
import { GlobalState } from '../types';
import { DialogAction, initialState } from './DialogReducer';

export interface IDialogContext {
state: DialogState;
dispatch?: Dispatch<any>;
state: GlobalState;
dispatch?: Dispatch<DialogAction>;
}

export default createContext<IDialogContext>({
Expand Down
60 changes: 44 additions & 16 deletions src/dialog/DialogHook.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useContext } from 'react';
import React, { ReactNode, useContext } from 'react';
import { DialogOptions } from '../types';
import DialogContext, { IDialogContext } from './DialogContext';
import { DialogActionType } from './DialogReducer';

Expand All @@ -7,40 +8,67 @@ interface DialogResult {
value?: any;
}

export type OpenDialogProps = string | React.ReactNode;

let resolveCallback: (flag: any) => void;
export function useDialog() {
const { state, dispatch } = useContext<IDialogContext>(DialogContext);
// eslint-disable-next-line @typescript-eslint/no-empty-function
const { state, dispatch = () => {} } = useContext<IDialogContext>(DialogContext);

const openDialog = (
content: ReactNode | string,
options?: DialogOptions,
callback?: (result: any) => any,
): Promise<DialogResult> | undefined => {
const id = Math.random()
.toString(16)
.substring(2, 8 + 2);

const openDialog = (body: OpenDialogProps): Promise<DialogResult> => {
if (typeof dispatch === 'undefined') throw new Error('DialogContext is not provided');
dispatch({
type: DialogActionType.OPEN_DIALOAG,
body,
type: DialogActionType.OPEN_DIALOG,
dialog: {
id,
content,
...options,
},
});

if (callback) {
dispatch({
type: DialogActionType.ADD_CALLBACK,
dialogId: id,
resolve: callback,
});
return;
}

return new Promise(resolve => {
resolveCallback = resolve;
dispatch({
type: DialogActionType.ADD_CALLBACK,
dialogId: id,
resolve,
});
});
};

const closeDialog = () => {
if (typeof dispatch === 'undefined') throw new Error('DialogContext is not provided');
const closeDialog = (dialogId?: string) => {
dispatch({
type: DialogActionType.CLOSE_DIALOG,
dialogId,
});
};

const onConfirm = (props?: any) => {
closeDialog();
const onConfirm = (props?: any, dialogId?: string) => {
closeDialog(dialogId);
const resolveCallback = state.resolves.find(resolve => resolve.dialogID === dialogId)?.resolve;
if (!resolveCallback) return;
resolveCallback({
confirmed: true,
value: props,
});
};

const onCancel = (props?: any) => {
closeDialog();
const onCancel = (props?: any, dialogId?: string) => {
closeDialog(dialogId);
const resolveCallback = state.resolves.find(resolve => resolve.dialogID === dialogId)?.resolve;
if (!resolveCallback) return;
resolveCallback({
confirmed: false,
value: props,
Expand Down
46 changes: 23 additions & 23 deletions src/dialog/DialogReducer.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,43 @@
import React from 'react';
import { Dialog, GlobalState } from '../types';

export enum DialogVisibility {
VISIBLE = 'visible',
HIDDEN = 'hidden',
}

export interface DialogState {
visibility: DialogVisibility;
content: string | React.ReactNode;
}

export const initialState: DialogState = {
visibility: DialogVisibility.HIDDEN,
content: '',
export const initialState: GlobalState = {
dialogs: [],
resolves: [],
};

export enum DialogActionType {
OPEN_DIALOAG = 'OPEN_DIALOG',
OPEN_DIALOG = 'OPEN_DIALOG',
CLOSE_DIALOG = 'CLOSE_DIALOG',
ADD_CALLBACK = 'ADD_CALLBACK',
}

export interface DialogAction {
type: DialogActionType;
body?: string | React.ReactNode;
dialog?: Dialog;
dialogId?: string;
resolve?: (value: any) => void;
}

export const reducer = (state: DialogState, action: DialogAction): DialogState => {
export const reducer = (state: GlobalState, action: DialogAction): GlobalState => {
switch (action.type) {
case DialogActionType.OPEN_DIALOAG:
case DialogActionType.OPEN_DIALOG:
if (!action.dialog) return state;
return {
...state,
visibility: DialogVisibility.VISIBLE,
content: action.body,
dialogs: [...state.dialogs, action.dialog],
resolves: state.resolves,
};
case DialogActionType.CLOSE_DIALOG:
return {
...state,
visibility: DialogVisibility.HIDDEN,
content: '',
dialogs: state.dialogs.filter(dialog => dialog.id !== action.dialogId),
resolves: state.resolves.filter(resolve => resolve.dialogID !== action.dialogId),
};
case DialogActionType.ADD_CALLBACK:
if (!action.resolve) return state;
if (!action.dialogId) return state;
return {
dialogs: state.dialogs,
resolves: [...state.resolves, { dialogID: action.dialogId, resolve: action.resolve }],
};
default:
return state;
Expand Down
32 changes: 32 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export interface DialogState {
id: string;
content: string | React.ReactNode;
}

export interface DialogOptions {
closeOnOffsiteClick?: boolean;
closeOnEscape?: boolean;
removeDefaultClasses?: boolean;
wrapperClassName?: string;
cardClassName?: string;
cardBodyClassName?: string;
cardActionsClassName?: string;
confirmButtonClassName?: string;
cancelButtonClassName?: string;
confirmButtonLabel?: string;
cancelButtonLabel?: string;
disableConfirmButton?: boolean;
disableCancelButton?: boolean;
}

export type Dialog = DialogState & DialogOptions;

export interface Resolve {
dialogID: string;
resolve: (value: any) => void;
}

export interface GlobalState {
dialogs: Dialog[];
resolves: Resolve[];
}

0 comments on commit 3465855

Please sign in to comment.