Skip to content

Commit

Permalink
refactored Triggerable Modal
Browse files Browse the repository at this point in the history
  • Loading branch information
ridz1208 committed Nov 1, 2024
1 parent 38266d4 commit a72e0db
Show file tree
Hide file tree
Showing 4 changed files with 303 additions and 81 deletions.
21 changes: 21 additions & 0 deletions jsx/Loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
interface LoaderProps {
size?: number;
}

/**
* Loader component renders a spinner wheel of a specified size.
*
* @param {LoaderProps} props - The properties for the Loader component
* @returns {JSX.Element} A div representing the loading spinner
*/
const Loader = ({size = 120}: LoaderProps) => {
const loaderStyle = {
width: size,
height: size,
borderWidth: size/15,
};

return <div className='loader' style={loaderStyle}/>;
};

export default Loader;
223 changes: 223 additions & 0 deletions jsx/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import {useState, PropsWithChildren, CSSProperties} from 'react';
import Swal from 'sweetalert2';
import Loader from './Loader';
import {
ButtonElement,
} from 'jsx/Form';

type ModalProps = PropsWithChildren<{
throwWarning?: boolean;
show: boolean;
onClose: () => void;
onSubmit?: () => Promise<any>;
onSuccess?: (data: any) => void;
title?: string;
}>;

/**
* Modal Component
*
* A React functional component that renders a modal dialog with optional
* form submission and loading indicators. Supports asynchronous form submission
* with loading and success feedback.
*
* @param {ModalProps} props - Properties for the modal component
* @returns {JSX.Element} - A modal dialog box w/ optional submit functionality
*/
const Modal = ({
throwWarning = false,
show = false,
onClose,
onSubmit,
onSuccess,
title,
children,
}: ModalProps) => {
const [loading, setLoading] = useState(false); // Tracks loading during submit
const [success, setSuccess] = useState(false); // Tracks success after submit

/**
* Handles modal close event. Shows a confirmation if `throwWarning` is true.
*/
const handleClose = () => {
if (throwWarning) { // Display warning if enabled
Swal.fire({
title: 'Are You Sure?',
text: 'Leaving the form will result in the loss of any information ' +
'entered.',
type: 'warning',
showCancelButton: true,
confirmButtonText: 'Proceed',
cancelButtonText: 'Cancel',
}).then((result) => result.value && onClose());
} else {
onClose(); // Close immediately if no warning
}
};

/**
* Manages form submission with loading and success states, calling
* `onSubmit` and handling modal state based on success or failure.
*/
const submit = async () => {
if (!onSubmit) return; // Ensure onSubmit exists

setLoading(true); // Show loader

try {
const data = await onSubmit();
setLoading(false);
setSuccess(true); // Show success

await new Promise((resolve) => setTimeout(resolve, 2000)); // Close delay

setSuccess(false); // Reset success state
onClose(); // Close modal
onSuccess?.(data); // call onSuccess if defined
} catch {
setLoading(false);
}
};

/**
* Renders submit button if `onSubmit` is provided and no loading or success.
*
* @returns {JSX.Element | undefined} - The submit button if conditions are met
*/
const submitButton = () => {
if (onSubmit && !(loading || success)) { // Show button if conditions met
return (
<div style={submitStyle}>
<ButtonElement onUserInput={submit}/>
</div>
);
}
};

const headerStyle: CSSProperties = {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
height: '40px',
borderTopRightRadius: '10',
fontSize: 24,
padding: 35,
borderBottom: '1px solid #DDDDDD',
};

const glyphStyle: CSSProperties = {
marginLeft: 'auto',
cursor: 'pointer',
};

const bodyStyle: CSSProperties = {
padding: success ? 0 : '15px 15px',
maxHeight: success ? 0 : '75vh',
overflow: 'scroll',
opacity: success ? 0 : 1,
transition: '1s ease, opacity 0.3s',
};

const modalContainer: CSSProperties = {
display: 'block',
position: 'fixed',
zIndex: 9999,
paddingTop: '100px',
paddingBottom: '100px',
left: 0,
top: 0,
width: '100%',
height: '100%',
overflow: 'auto',
backgroundColor: 'rgba(0,0,0,0.7)',
visibility: show ? 'visible' : 'hidden',
};

const modalContent: CSSProperties = {
opacity: show ? 1 : 0,
top: show ? 0 : '-300px',
position: 'relative',
backgroundColor: '#fefefe',
borderRadius: '7px',
margin: 'auto',
padding: 0,
border: '1px solid #888',
width: '700px',
boxShadow: '0 4px 8px 0 rbga(0,0,0,0.2), 0 6px 20px 0 rgba(0,0,0,0.19)',
transition: '0.4s ease',
};

/**
* Renders the modal children if `show` is true.
*
* @returns {JSX.Element | null} - The children to render or null if hidden
*/
const renderChildren = () => show && children;

const footerStyle: CSSProperties = {
borderTop: '1px solid #DDDDDD',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
height: '40px',
padding: '35px',
backgroundColor: success ? '#e0ffec' : undefined,
};

const submitStyle: CSSProperties = {
marginLeft: 'auto',
marginRight: '20px',
};

const processStyle: CSSProperties = {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-evenly',
margin: '0px auto',
width: '90px',
};

/**
* Loader element displayed during form submission.
*/
const loader = loading && (
<div style={processStyle}>
<Loader size={20}/>
<h5 className='animate-flicker'>Saving</h5>
</div>
);

/**
* Success display element shown after successful form submission.
*/
const successDisplay = success && (
<div style={processStyle}>
<span
style={{color: 'green', marginBottom: '2px'}}
className='glyphicon glyphicon-ok-circle'
/>
<h5>Success!</h5>
</div>
);

return (
<div style={modalContainer} onClick={handleClose}>
<div style={modalContent} onClick={(e) => e.stopPropagation()}>
<div style={headerStyle}>
{title}
<span style={glyphStyle} onClick={handleClose}>×</span>
</div>
<div>
<div style={bodyStyle}>{renderChildren()}</div>
<div style={footerStyle}>
{loader}
{successDisplay}
{submitButton()}
</div>
</div>
</div>
</div>
);
};

export default Modal;
81 changes: 0 additions & 81 deletions jsx/TriggerableModal.js

This file was deleted.

59 changes: 59 additions & 0 deletions jsx/TriggerableModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React, {useState, ElementType} from 'react';
import Modal, {ModalProps} from 'Modal';
import Form from 'jsx/Form';

interface TriggerableModalProps extends Omit<ModalProps, 'show'> {
label: string; // Label for the default CTA trigger button
onUserInput?: () => void; // Optional callback when the trigger is activated
TriggerTag?: ElementType; // Custom component for the modal trigger
}

/**
* TriggerableModal Component
*
* Renders a modal triggered by a custom or default CTA component, with `show`
* controlled internally.
*
* @param {TriggerableModalProps} props - The properties for the component.
* @returns {JSX.Element} The rendered TriggerableModal component.
*/
const TriggerableModal = ({
label,
onUserInput,
TriggerTag = Form.CTA, // Default trigger component is CTA
...modalProps // Spread other modal-related props to pass to Modal
}: TriggerableModalProps) => {
const [open, setOpen] = useState(false);

/**
* Handles closing the modal by updating the state and calling the optional
* `onClose` callback provided in props.
*/
const handleClose = () => {
setOpen(false);
modalProps.onClose?.(); // Call onClose if it exists
};

/**
* Trigger element to open the modal. Uses `TriggerTag` for a custom
* trigger component, defaults to CTA.
*/
const trigger = (
<TriggerTag
label={label}
onUserInput={() => {
onUserInput?.(); // Call onUserInput if it exists
setOpen(true); // Open the modal
}}
/>
);

return (
<>
{trigger}
<Modal {...modalProps} show={open} onClose={handleClose} />
</>
);
};

export default TriggerableModal;

0 comments on commit a72e0db

Please sign in to comment.