dce-reactkit 3.7.10 → 3.7.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,151 @@
1
+ /* eslint-disable react/no-unused-prop-types */
2
+ /* eslint-disable jsx-a11y/no-static-element-interactions */
3
+ /* eslint-disable jsx-a11y/click-events-have-key-events */
4
+ /* eslint-disable react/destructuring-assignment */
5
+
1
6
  /**
2
- * General use modal component
7
+ * The displayable modal component (this is the modal that's added to the
8
+ * wrapper, not the one that the programmer renders)
3
9
  * @author Gabe Abrams
4
10
  */
5
11
 
6
12
  // Import React
7
- import React, { useEffect, useRef } from 'react';
13
+ import React, { useState, useEffect, useRef } from 'react';
14
+ import ReactDOM from 'react-dom';
15
+
16
+ // Import other components
17
+ import waitMs from '../../helpers/waitMs';
8
18
 
9
- // Import shared types
19
+ // Import types
20
+ import Variant from '../../types/Variant';
21
+ import ModalButtonType from '../../types/ModalButtonType';
22
+ import ModalSize from '../../types/ModalSize';
23
+ import ModalType from '../../types/ModalType';
10
24
  import ModalProps from './ModalProps';
11
25
 
12
- // Import helpers from AppWrapper
13
- import { addModal, removeModal, updateModal } from '../AppWrapper';
26
+ // Import constants
27
+ import NUM_MODAL_PORTALS from '../../constants/NUM_MODAL_PORTALS';
28
+
29
+ // Import shared helpers
30
+ // TODO: fix dependency cycle
31
+ // eslint-disable-next-line import/no-cycle
32
+ import { isDarkModeOn } from '../../client/initClient';
33
+
34
+ /*------------------------------------------------------------------------*/
35
+ /* ------------------------------ Constants ----------------------------- */
36
+ /*------------------------------------------------------------------------*/
37
+
38
+ // Base level of z-index
39
+ const BASE_Z_INDEX = 1000000000;
40
+ const BASE_Z_INDEX_ON_TOP = 2000000000;
41
+
42
+ // Constants
43
+ const MS_TO_ANIMATE = 200; // Animation duration
44
+
45
+ // Modal type to list of buttons
46
+ const modalTypeToModalButtonTypes: {
47
+ [k: string]: ModalButtonType[]
48
+ } = {
49
+ [ModalType.Okay]: [
50
+ ModalButtonType.Okay,
51
+ ],
52
+ [ModalType.OkayCancel]: [
53
+ ModalButtonType.Okay,
54
+ ModalButtonType.Cancel,
55
+ ],
56
+ [ModalType.YesNo]: [
57
+ ModalButtonType.Yes,
58
+ ModalButtonType.No,
59
+ ],
60
+ [ModalType.YesNoCancel]: [
61
+ ModalButtonType.Yes,
62
+ ModalButtonType.No,
63
+ ModalButtonType.Cancel,
64
+ ],
65
+ [ModalType.AbandonGoBack]: [
66
+ ModalButtonType.Abandon,
67
+ ModalButtonType.GoBack,
68
+ ],
69
+ [ModalType.ImSureCancel]: [
70
+ ModalButtonType.ImSure,
71
+ ModalButtonType.Cancel,
72
+ ],
73
+ [ModalType.DeleteCancel]: [
74
+ ModalButtonType.Delete,
75
+ ModalButtonType.Cancel,
76
+ ],
77
+ [ModalType.ConfirmCancel]: [
78
+ ModalButtonType.Confirm,
79
+ ModalButtonType.Cancel,
80
+ ],
81
+ };
82
+
83
+ /**
84
+ * Get button type styling and labels
85
+ * @author Gabe Abrams
86
+ * @returns map of button type to label and variant
87
+ */
88
+ const getModalButtonTypeToLabelAndVariant = () => {
89
+ const dark = isDarkModeOn();
90
+ return {
91
+ [ModalButtonType.Okay]: {
92
+ label: 'Okay',
93
+ variant: (
94
+ dark
95
+ ? Variant.Light
96
+ : Variant.Dark
97
+ ),
98
+ },
99
+ [ModalButtonType.Cancel]: {
100
+ label: 'Cancel',
101
+ variant: Variant.Secondary,
102
+ },
103
+ [ModalButtonType.Yes]: {
104
+ label: 'Yes',
105
+ variant: (
106
+ dark
107
+ ? Variant.Light
108
+ : Variant.Dark
109
+ ),
110
+ },
111
+ [ModalButtonType.No]: {
112
+ label: 'No',
113
+ variant: Variant.Secondary,
114
+ },
115
+ [ModalButtonType.Abandon]: {
116
+ label: 'Abandon Changes',
117
+ variant: Variant.Warning,
118
+ },
119
+ [ModalButtonType.GoBack]: {
120
+ label: 'Go Back',
121
+ variant: Variant.Secondary,
122
+ },
123
+ [ModalButtonType.Continue]: {
124
+ label: 'Continue',
125
+ variant: (
126
+ dark
127
+ ? Variant.Light
128
+ : Variant.Dark
129
+ ),
130
+ },
131
+ [ModalButtonType.ImSure]: {
132
+ label: 'I am sure',
133
+ variant: Variant.Warning,
134
+ },
135
+ [ModalButtonType.Delete]: {
136
+ label: 'Yes, Delete',
137
+ variant: Variant.Danger,
138
+ },
139
+ [ModalButtonType.Confirm]: {
140
+ label: 'Confirm',
141
+ variant: (
142
+ dark
143
+ ? Variant.Light
144
+ : Variant.Dark
145
+ ),
146
+ },
147
+ };
148
+ };
14
149
 
15
150
  /*------------------------------------------------------------------------*/
16
151
  /* --------------------------- Static Helpers --------------------------- */
@@ -29,6 +164,74 @@ const getNextUniqueId = (): number => {
29
164
  return nextUniqueId++;
30
165
  };
31
166
 
167
+ /*------------------------------------------------------------------------*/
168
+ /* -------------------------------- Style ------------------------------- */
169
+ /*------------------------------------------------------------------------*/
170
+
171
+ const style = `
172
+ .Modal-backdrop {
173
+ position: fixed;
174
+ top: 0;
175
+ left: 0;
176
+ width: 100vw;
177
+ height: 200vh;
178
+ background-color: rgba(0, 0, 0, 0.7);
179
+ }
180
+ .Modal-fading-in {
181
+ animation-name: Modal-fading-in;
182
+ animation-duration: ${Math.floor(MS_TO_ANIMATE * 2)}ms;
183
+ animation-iteration-count: 1;
184
+ animation-fill-mode: both;
185
+ animation-timing-function: ease-out;
186
+ }
187
+ @keyframes Modal-fading-in {
188
+ 0% {
189
+ opacity: 0;
190
+ }
191
+ 100% {
192
+ opacity: 1;
193
+ }
194
+ }
195
+ .Modal-animating-in {
196
+ animation-name: Modal-animating-in;
197
+ animation-duration: ${MS_TO_ANIMATE}ms;
198
+ animation-iteration-count: 1;
199
+ animation-fill-mode: both;
200
+ animation-timing-function: ease-out;
201
+ }
202
+ @keyframes Modal-animating-in {
203
+ 0% {
204
+ transform: scale(1.05) translate(0, -1.5rem);
205
+ opacity: 0;
206
+ }
207
+ 100% {
208
+ transform: scale(1) translate(0, 0);
209
+ opacity: 1;
210
+ }
211
+ }
212
+ .Modal-animating-pop {
213
+ animation-name: Modal-animating-pop;
214
+ animation-duration: ${MS_TO_ANIMATE}ms;
215
+ animation-iteration-count: 1;
216
+ animation-fill-mode: both;
217
+ animation-timing-function: ease-in-out;
218
+ }
219
+ @keyframes Modal-animating-pop {
220
+ 0% {
221
+ transform: scale(1);
222
+ opacity: 1;
223
+ }
224
+ 50% {
225
+ transform: scale(1.05);
226
+ opacity: 0.9;
227
+ }
228
+ 100% {
229
+ transform: scale(1);
230
+ opacity: 1;
231
+ }
232
+ }
233
+ `;
234
+
32
235
  /*------------------------------------------------------------------------*/
33
236
  /* ------------------------------ Component ----------------------------- */
34
237
  /*------------------------------------------------------------------------*/
@@ -38,62 +241,313 @@ const Modal: React.FC<ModalProps> = (props) => {
38
241
  /* -------------------------------- Setup ------------------------------- */
39
242
  /*------------------------------------------------------------------------*/
40
243
 
244
+ /* -------------- Props ------------- */
245
+
246
+ const {
247
+ type = ModalType.NoButtons,
248
+ size = ModalSize.Large,
249
+ title,
250
+ children,
251
+ onClose,
252
+ dontAllowBackdropExit,
253
+ dontShowXButton,
254
+ onTopOfOtherModals,
255
+ } = props;
256
+
257
+ /* -------------- State ------------- */
258
+
259
+ // True if animation is in use
260
+ const [animatingIn, setAnimatingIn] = useState(true);
261
+ const [animatingPop, setAnimatingPop] = useState(false);
262
+
41
263
  /* -------------- Refs -------------- */
42
264
 
43
- // Initialize refs
44
- const id = useRef<number>(getNextUniqueId());
265
+ // Keep track of whether modal is still mounted
266
+ const mounted = useRef(false);
267
+
268
+ // Keep track of unique modal id
269
+ const id = useRef<number>(0);
270
+ if (!id.current) {
271
+ id.current = getNextUniqueId();
272
+ }
45
273
 
46
274
  /*------------------------------------------------------------------------*/
47
275
  /* ------------------------- Lifecycle Functions ------------------------ */
48
276
  /*------------------------------------------------------------------------*/
49
277
 
50
278
  /**
51
- * Mount: add to app wrapper
279
+ * Mount
52
280
  * @author Gabe Abrams
53
281
  */
54
282
  useEffect(
55
283
  () => {
56
- addModal(id.current, props);
284
+ (async () => {
285
+ // Set defaults
286
+ setAnimatingIn(true);
287
+ setAnimatingPop(false);
288
+ // Wait for animation
289
+ await waitMs(MS_TO_ANIMATE);
290
+ // Update to state after animated in
291
+ if (mounted.current) {
292
+ setAnimatingIn(false);
293
+ }
294
+ })();
295
+
296
+ return () => {
297
+ mounted.current = false;
298
+ };
57
299
  },
58
300
  [],
59
301
  );
60
302
 
61
- /**
62
- * Update: update modal props in app wrapper when props change
63
- * @author Gabe Abrams
64
- */
65
- useEffect(
66
- () => {
67
- // Update modal props
68
- updateModal(id.current, props);
69
- },
70
- [props],
71
- );
303
+ /*------------------------------------------------------------------------*/
304
+ /* ------------------------- Component Functions ------------------------ */
305
+ /*------------------------------------------------------------------------*/
72
306
 
73
307
  /**
74
- * Unmount: remove from app wrapper when unmounting
308
+ * Handles the closing of the modal
75
309
  * @author Gabe Abrams
310
+ * @param modalButtonType the button that was clicked when closing the
311
+ * modal
76
312
  */
77
- useEffect(
78
- () => {
79
- return () => {
80
- // Remove modal
81
- removeModal(id.current);
82
- };
83
- },
84
- [],
85
- );
313
+ const handleClose = async (modalButtonType: ModalButtonType) => {
314
+ // Don't close if no handler
315
+ if (!onClose) {
316
+ return;
317
+ }
318
+
319
+ onClose(modalButtonType);
320
+ };
86
321
 
87
322
  /*------------------------------------------------------------------------*/
88
323
  /* ------------------------------- Render ------------------------------- */
89
324
  /*------------------------------------------------------------------------*/
90
325
 
91
- /*----------------------------------------*/
92
- /* --------------- Main UI -------------- */
93
- /*----------------------------------------*/
326
+ // Calculate Z-index
327
+ const baseZIndex = (
328
+ onTopOfOtherModals
329
+ ? BASE_Z_INDEX_ON_TOP
330
+ : BASE_Z_INDEX
331
+ );
332
+
333
+ // Get list of buttons for this modal type
334
+ const ModalButtonTypes: ModalButtonType[] = modalTypeToModalButtonTypes[type] ?? [];
335
+
336
+ // Get map of button type to label and variant
337
+ const ModalButtonTypeToLabelAndVariant = getModalButtonTypeToLabelAndVariant();
338
+
339
+ // Create buttons
340
+ const buttons = ModalButtonTypes.map((modalButtonType: ModalButtonType, i) => {
341
+ // Get default style
342
+ let {
343
+ label,
344
+ variant,
345
+ } = ModalButtonTypeToLabelAndVariant[modalButtonType];
346
+
347
+ // Override with customizations
348
+ const newLabel = props[`${modalButtonType}Label`];
349
+ if (newLabel) {
350
+ label = newLabel;
351
+ }
352
+ const newVariant = props[`${modalButtonType}Variant`];
353
+ if (newVariant) {
354
+ variant = newVariant;
355
+ }
356
+
357
+ // Check if this button is last
358
+ const last = (i === ModalButtonTypes.length - 1);
359
+
360
+ // Create the button
361
+ return (
362
+ <button
363
+ key={modalButtonType}
364
+ type="button"
365
+ className={`Modal-${modalButtonType}-button btn btn-${variant} ${last ? '' : 'me-1'}`}
366
+ onClick={() => {
367
+ handleClose(modalButtonType);
368
+ }}
369
+ >
370
+ {label}
371
+ </button>
372
+ );
373
+ });
374
+
375
+ // Put all buttons in a footer
376
+ const footer = (
377
+ (buttons && buttons.length)
378
+ ? (
379
+ <div>
380
+ {buttons}
381
+ </div>
382
+ )
383
+ : undefined
384
+ );
385
+
386
+ // Choose an animation
387
+ let animationClass = '';
388
+ let backdropAnimationClass = '';
389
+ if (animatingIn) {
390
+ animationClass = 'Modal-animating-in';
391
+ backdropAnimationClass = 'Modal-fading-in';
392
+ } else if (animatingPop) {
393
+ animationClass = 'Modal-animating-pop';
394
+ }
395
+
396
+ // Render the modal
397
+ const contentToRender = (
398
+ <div
399
+ className="modal show"
400
+ tabIndex={-1}
401
+ style={{
402
+ zIndex: baseZIndex,
403
+ display: 'block',
404
+ margin: 'auto',
405
+ left: 0,
406
+ right: 0,
407
+ }}
408
+ >
409
+ <style>{style}</style>
410
+ <div
411
+ className={`Modal-backdrop ${backdropAnimationClass}`}
412
+ style={{
413
+ zIndex: baseZIndex + 1,
414
+ }}
415
+ onClick={async () => {
416
+ // Skip if exit via backdrop not allowed
417
+ if (dontAllowBackdropExit || !onClose) {
418
+ // Show pop animation
419
+ if (!animatingPop) {
420
+ setAnimatingPop(true);
421
+
422
+ // Wait then stop pop animation
423
+ await waitMs(MS_TO_ANIMATE);
424
+ setAnimatingPop(false);
425
+ }
426
+ return;
427
+ }
428
+ // Handle close
429
+ handleClose(ModalButtonType.Cancel);
430
+ }}
431
+ />
432
+ <div
433
+ className={`modal-dialog modal-${size} ${animationClass} modal-dialog-scrollable modal-dialog-centered`}
434
+ style={{
435
+ zIndex: baseZIndex + 2,
436
+ }}
437
+ >
438
+ <div
439
+ className="modal-content"
440
+ style={{
441
+ borderColor: (
442
+ isDarkModeOn()
443
+ ? 'gray'
444
+ : undefined
445
+ ),
446
+ }}
447
+ >
448
+ <div
449
+ className="modal-header"
450
+ style={{
451
+ color: (
452
+ isDarkModeOn()
453
+ ? 'white'
454
+ : undefined
455
+ ),
456
+ backgroundColor: (
457
+ isDarkModeOn()
458
+ ? '#444'
459
+ : undefined
460
+ ),
461
+ borderBottom: (
462
+ isDarkModeOn()
463
+ ? '0.1rem solid gray'
464
+ : undefined
465
+ ),
466
+ }}
467
+ >
468
+ <h5
469
+ className="modal-title"
470
+ style={{
471
+ fontWeight: 'bold',
472
+ }}
473
+ >
474
+ {title}
475
+ </h5>
476
+
477
+ {(onClose && !dontShowXButton) && (
478
+ <button
479
+ type="button"
480
+ className="Modal-x-button btn-close"
481
+ aria-label="Close"
482
+ style={{
483
+ backgroundColor: (
484
+ isDarkModeOn()
485
+ ? 'white'
486
+ : undefined
487
+ ),
488
+ }}
489
+ onClick={() => {
490
+ // Handle close
491
+ handleClose(ModalButtonType.Cancel);
492
+ }}
493
+ />
494
+ )}
495
+ </div>
496
+ {children && (
497
+ <div
498
+ className="modal-body"
499
+ style={{
500
+ color: (
501
+ isDarkModeOn()
502
+ ? 'white'
503
+ : undefined
504
+ ),
505
+ backgroundColor: (
506
+ isDarkModeOn()
507
+ ? '#444'
508
+ : undefined
509
+ ),
510
+ }}
511
+ >
512
+ {children}
513
+ </div>
514
+ )}
515
+ {footer && (
516
+ <div
517
+ className="modal-footer pt-1 pb-1"
518
+ style={{
519
+ color: (
520
+ isDarkModeOn()
521
+ ? 'white'
522
+ : undefined
523
+ ),
524
+ backgroundColor: (
525
+ isDarkModeOn()
526
+ ? '#444'
527
+ : undefined
528
+ ),
529
+ borderTop: (
530
+ isDarkModeOn()
531
+ ? '0.1rem solid gray'
532
+ : undefined
533
+ ),
534
+ }}
535
+ >
536
+ {footer}
537
+ </div>
538
+ )}
539
+ </div>
540
+ </div>
541
+ </div>
542
+ );
543
+
544
+ // Determine which portal to use
545
+ const portalNumber = id.current % NUM_MODAL_PORTALS;
94
546
 
95
- return (
96
- <div className="Modal-shell d-none" />
547
+ // Render in a portal
548
+ return ReactDOM.createPortal(
549
+ contentToRender,
550
+ document.getElementById(`modal-portal-${portalNumber}`) as HTMLElement,
97
551
  );
98
552
  };
99
553
 
@@ -101,5 +555,4 @@ const Modal: React.FC<ModalProps> = (props) => {
101
555
  /* ------------------------------- Wrap Up ------------------------------ */
102
556
  /*------------------------------------------------------------------------*/
103
557
 
104
- // Export component
105
558
  export default Modal;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The number of modal portals that can be rendered at once.
3
+ * @author Gabe Abrams
4
+ */
5
+ const NUM_MODAL_PORTALS = 50;
6
+
7
+ export default NUM_MODAL_PORTALS;
@@ -1,9 +0,0 @@
1
- /**
2
- * The displayable modal component (this is the modal that's added to the
3
- * wrapper, not the one that the programmer renders)
4
- * @author Gabe Abrams
5
- */
6
- import React from 'react';
7
- import ModalProps from './ModalProps';
8
- declare const Modal: React.FC<ModalProps>;
9
- export default Modal;
@@ -1,9 +0,0 @@
1
- /**
2
- * The displayable modal component (this is the modal that's added to the
3
- * wrapper, not the one that the programmer renders)
4
- * @author Gabe Abrams
5
- */
6
- import React from 'react';
7
- import ModalProps from './ModalProps';
8
- declare const Modal: React.FC<ModalProps>;
9
- export default Modal;