Skip to content

Commit ace5b61

Browse files
feat: add progress modal cmponent in explorer
1 parent 17be395 commit ace5b61

File tree

1 file changed

+226
-0
lines changed

1 file changed

+226
-0
lines changed
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import { Fragment, ReactNode } from "react";
2+
import { Dialog, Transition } from "@headlessui/react";
3+
import { CheckIcon, XMarkIcon } from "@heroicons/react/24/solid";
4+
import { ProgressStatus } from "../api/types";
5+
6+
export default function ProgressModal({
7+
isOpen,
8+
heading = "Processing...",
9+
subheading = "Please hold while your operation is in progress.",
10+
children,
11+
...props
12+
}: ProgressModalProps) {
13+
return (
14+
<Transition.Root show={isOpen} as={Fragment}>
15+
<Dialog
16+
as="div"
17+
data-testid="progress-modal"
18+
className="relative z-10"
19+
onClose={() => {
20+
/* Don't close the dialog when clicking the backdrop */
21+
}}
22+
>
23+
<Transition.Child
24+
as={Fragment}
25+
enter="ease-out duration-300"
26+
enterFrom="opacity-0"
27+
enterTo="opacity-100"
28+
leave="ease-in duration-200"
29+
leaveFrom="opacity-100"
30+
leaveTo="opacity-0"
31+
>
32+
<div className="fixed inset-0 bg-grey-400 bg-opacity-75 transition-opacity" />
33+
</Transition.Child>
34+
35+
<div className="fixed z-10 inset-0 overflow-y-auto">
36+
<div className="flex items-end sm:items-center justify-center min-h-full p-4 text-center sm:p-0">
37+
<Transition.Child
38+
as={Fragment}
39+
enter="ease-out duration-300"
40+
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
41+
enterTo="opacity-100 translate-y-0 sm:scale-100"
42+
leave="ease-in duration-200"
43+
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
44+
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
45+
>
46+
<Dialog.Panel className="relative bg-white px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:max-w-lg sm:w-full sm:p-6">
47+
<div className="sm:flex sm:items-start">
48+
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
49+
<Dialog.Title
50+
as="h3"
51+
className="text-base leading-6 font-semibold text-grey-500"
52+
>
53+
{heading}
54+
</Dialog.Title>
55+
<div className="mt-2">
56+
<p className="text-sm text-grey-400">{subheading}</p>
57+
</div>
58+
</div>
59+
</div>
60+
<nav aria-label="Progress" className="ml-4 mt-11 mb-6">
61+
<ol className="overflow-hidden">
62+
{props.steps.map((step, stepIdx) => (
63+
<li
64+
key={stepIdx}
65+
className={`relative ${
66+
stepIdx !== props.steps.length - 1 && "pb-10"
67+
}`}
68+
data-testid={`${step.name}-${step.status}`}
69+
>
70+
{step.status === ProgressStatus.IS_SUCCESS ? (
71+
<ModalStep
72+
step={step}
73+
icon={
74+
<span
75+
className="relative z-10 w-8 h-8 flex items-center justify-center bg-teal-500 rounded-full"
76+
data-testid={`${step.name}-complete-icon`}
77+
>
78+
<CheckIcon
79+
className="w-5 h-5 text-white"
80+
aria-hidden="true"
81+
/>
82+
</span>
83+
}
84+
line={
85+
<div
86+
className="-ml-px absolute mt-0.5 top-4 left-4 w-0.5 h-full bg-teal-500"
87+
aria-hidden="true"
88+
/>
89+
}
90+
nameColor={"text-grey-500"}
91+
descriptionColor={"text-grey-500"}
92+
isLastStep={stepIdx === props.steps.length - 1}
93+
/>
94+
) : step.status === ProgressStatus.IN_PROGRESS ? (
95+
<ModalStep
96+
step={step}
97+
icon={
98+
<span className="relative z-10 w-8 h-8 flex items-center justify-center bg-white border-2 border-violet-500 rounded-full">
99+
<span
100+
className="h-2.5 w-2.5 bg-violet-500 rounded-full animate-pulse-scale"
101+
data-testid={`${step.name}-current-icon`}
102+
/>
103+
</span>
104+
}
105+
line={
106+
<div
107+
className="-ml-px absolute mt-0.5 top-4 left-4 w-0.5 h-full bg-grey-200"
108+
aria-hidden="true"
109+
/>
110+
}
111+
nameColor="text-violet-500"
112+
isLastStep={stepIdx === props.steps.length - 1}
113+
/>
114+
) : step.status === ProgressStatus.IS_ERROR ? (
115+
<ModalStep
116+
step={step}
117+
icon={
118+
<span className="relative z-10 w-8 h-8 flex items-center justify-center border-2 bg-white border-pink-500 rounded-full">
119+
<XMarkIcon
120+
className="w-5 h-5 text-pink-500"
121+
data-testid={`${step.name}-error-icon`}
122+
/>
123+
</span>
124+
}
125+
line={
126+
<div
127+
className="-ml-px absolute mt-0.5 top-4 left-4 w-0.5 h-full bg-grey-300"
128+
aria-hidden="true"
129+
/>
130+
}
131+
isLastStep={stepIdx === props.steps.length - 1}
132+
nameColor="text-grey-500"
133+
/>
134+
) : step.status === ProgressStatus.NOT_STARTED ? (
135+
<ModalStep
136+
step={step}
137+
icon={
138+
<span
139+
className="relative z-10 w-8 h-8 flex items-center justify-center bg-white border-2 rounded-full border-grey-400"
140+
data-testid={`${step.name}-upcoming-icon`}
141+
></span>
142+
}
143+
line={
144+
<div
145+
className="-ml-px absolute mt-0.5 top-4 left-4 w-0.5 h-full bg-grey-300"
146+
aria-hidden="true"
147+
/>
148+
}
149+
isLastStep={stepIdx === props.steps.length - 1}
150+
nameColor="text-grey-400"
151+
/>
152+
) : (
153+
<></>
154+
)}
155+
</li>
156+
))}
157+
</ol>
158+
</nav>
159+
</Dialog.Panel>
160+
</Transition.Child>
161+
</div>
162+
</div>
163+
{/* Adding invisible button as modal needs to be displayed with a button */}
164+
<button className="h-0 w-0 overflow-hidden" />
165+
{children}
166+
</Dialog>
167+
</Transition.Root>
168+
);
169+
}
170+
171+
function ModalStep(props: {
172+
step: Step;
173+
icon: JSX.Element;
174+
line: JSX.Element;
175+
nameColor: string;
176+
descriptionColor?: string;
177+
isAriaHidden?: boolean;
178+
isAriaCurrent?: boolean;
179+
isLastStep?: boolean;
180+
}) {
181+
return (
182+
<>
183+
{!props.isLastStep ? props.line : null}
184+
<div
185+
className="relative flex items-start group"
186+
aria-current={!!props.isAriaCurrent}
187+
>
188+
<span
189+
className="h-9 flex items-center"
190+
aria-hidden={!!props.isAriaHidden}
191+
>
192+
{props.icon}
193+
</span>
194+
<span className="ml-4 min-w-0 flex flex-col">
195+
<span
196+
className={`text-xs font-semibold tracking-wide uppercase ${props.nameColor}`}
197+
>
198+
{props.step.name}
199+
</span>
200+
<span
201+
className={`text-sm ${props.descriptionColor ?? "text-grey-400"}`}
202+
>
203+
{props.step.description}
204+
</span>
205+
</span>
206+
</div>
207+
</>
208+
);
209+
}
210+
211+
export type Step = {
212+
name: string;
213+
description: string;
214+
status: ProgressStatus;
215+
};
216+
217+
export interface ProgressModalProps {
218+
isOpen: boolean;
219+
steps: Step[];
220+
heading?: string;
221+
subheading?: string;
222+
redirectUrl?: string;
223+
children?: ReactNode;
224+
}
225+
226+
export const errorModalDelayMs = 3000;

0 commit comments

Comments
 (0)