dce-reactkit 3.5.3 → 3.5.5

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.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * A toggle switch that toggles on or off
3
+ * @author Alessandra De Lucas
4
+ * @author Gabe Abrams
5
+ */
6
+
7
+ // Import React
8
+ import React from 'react';
9
+
10
+ // Import FontAwesome
11
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
12
+ import { faCircle } from '@fortawesome/free-solid-svg-icons';
13
+
14
+ // Import shared types
15
+ import Variant from '../types/Variant';
16
+
17
+ /*------------------------------------------------------------------------*/
18
+ /* -------------------------------- Types ------------------------------- */
19
+ /*------------------------------------------------------------------------*/
20
+
21
+ // Props definition
22
+ type Props = {
23
+ // If true, the toggle switch is in the on position
24
+ isOn: boolean,
25
+ /**
26
+ * A handler to call when the switch is toggled
27
+ * @param isOn Updated value for isOn
28
+ */
29
+ onToggle: (isOn: boolean) => void,
30
+ // Unique ID for the toggle switch
31
+ id?: string,
32
+ // A description for what it means when the toggle switch is on
33
+ // E.g. "airplane mode is active"
34
+ // Description will be placed into a sentence with the structure:
35
+ // `If on, ${description}. Currently on. Click to turn off.`
36
+ description: string,
37
+ // Custom background variant for the "on" state (defaults to Variant.Info)
38
+ backgroundVariantWhenOn?: Variant,
39
+ };
40
+
41
+ /*------------------------------------------------------------------------*/
42
+ /* ------------------------------ Component ----------------------------- */
43
+ /*------------------------------------------------------------------------*/
44
+
45
+ const ToggleSwitch: React.FC<Props> = (props) => {
46
+ /*------------------------------------------------------------------------*/
47
+ /* -------------------------------- Setup ------------------------------- */
48
+ /*------------------------------------------------------------------------*/
49
+
50
+ /* -------------- Props ------------- */
51
+
52
+ // Destructure all props
53
+ const {
54
+ isOn,
55
+ onToggle,
56
+ id,
57
+ description,
58
+ backgroundVariantWhenOn = Variant.Info,
59
+ } = props;
60
+
61
+ /*------------------------------------------------------------------------*/
62
+ /* ------------------------------- Render ------------------------------- */
63
+ /*------------------------------------------------------------------------*/
64
+
65
+ /*----------------------------------------*/
66
+ /* --------------- Main UI -------------- */
67
+ /*----------------------------------------*/
68
+
69
+ const backgroundVariant = (
70
+ isOn
71
+ ? backgroundVariantWhenOn
72
+ : 'secondary'
73
+ );
74
+
75
+ return (
76
+ <button
77
+ id={id}
78
+ aria-label={`If on, ${description}. Currently ${isOn ? 'on' : 'off'}. Click to turn ${isOn ? 'off' : 'on'}.`}
79
+ className={`alert alert-${backgroundVariant} bg-${backgroundVariant} mb-0 rounded-pill d-inline-block pt-0 pb-0 px-3`}
80
+ onClick={() => {
81
+ onToggle(!isOn);
82
+ }}
83
+ type="button"
84
+ >
85
+ <FontAwesomeIcon
86
+ icon={faCircle}
87
+ className="text-light"
88
+ style={{
89
+ transform: `translateX(${isOn ? '' : '-'}0.7rem)`,
90
+ transition: '0.3s transform ease-in-out',
91
+ }}
92
+ />
93
+ </button>
94
+ );
95
+ };
96
+
97
+ /*------------------------------------------------------------------------*/
98
+ /* ------------------------------- Wrap Up ------------------------------ */
99
+ /*------------------------------------------------------------------------*/
100
+
101
+ // Export component
102
+ export default ToggleSwitch;
@@ -0,0 +1,567 @@
1
+ /**
2
+ * Simple tooltip component
3
+ * @author Gabe Abrams
4
+ */
5
+
6
+ // Import React
7
+ import React, { useEffect, useReducer, useRef } from 'react';
8
+
9
+ // Import FontAwesome
10
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
11
+ import { IconProp } from '@fortawesome/fontawesome-svg-core';
12
+ import { faCaretDown } from '@fortawesome/free-solid-svg-icons';
13
+
14
+ /*------------------------------------------------------------------------*/
15
+ /* -------------------------------- Types ------------------------------- */
16
+ /*------------------------------------------------------------------------*/
17
+
18
+ // Props definition
19
+ type Props = {
20
+ // Icon to show next to the text
21
+ icon?: IconProp,
22
+ // Text of the tooltip
23
+ text: string,
24
+ // Children to wrap in the tooltip
25
+ children: React.ReactNode,
26
+ // If true, tooltip is wider
27
+ wide?: boolean,
28
+ // If true, tooltip is thinner
29
+ thin?: boolean,
30
+ };
31
+
32
+ /*------------------------------------------------------------------------*/
33
+ /* ------------------------------ Constants ----------------------------- */
34
+ /*------------------------------------------------------------------------*/
35
+
36
+ // Tooltip widths
37
+ const TOOLTIP_WIDTH_THIN_REM = 5;
38
+ const TOOLTIP_WIDTH_NORMAL_REM = 7;
39
+ const TOOLTIP_WIDTH_WIDE_REM = 10;
40
+
41
+ // Tooltip border radius
42
+ const TOOLTIP_BORDER_RADIUS_REM = 0.3;
43
+
44
+ /*------------------------------------------------------------------------*/
45
+ /* -------------------------------- Style ------------------------------- */
46
+ /*------------------------------------------------------------------------*/
47
+
48
+ const style = `
49
+ /* Container for contents and tooltip */
50
+ .Tooltip-outer-container {
51
+ display: inline-block;
52
+ position: relative;
53
+ }
54
+
55
+ /* Container for just tooltip */
56
+ .Tooltip-tooltip-container {
57
+ /* Position in middle but take no space */
58
+ /* (so layout of page is unaffected) */
59
+ position: absolute;
60
+ display: inline-block;
61
+ top: 0;
62
+ left: 50%;
63
+ height: 0;
64
+ width: 0;
65
+ overflow: visible;
66
+ }
67
+
68
+ /* Tooltip */
69
+ .Tooltip-tooltip {
70
+ /* Position above item */
71
+ display: inline-block;
72
+ position: absolute;
73
+ top: -2.3rem;
74
+ height: 1.8rem;
75
+ z-index: 1;
76
+
77
+ /* Use standard black font */
78
+ font-size: 1rem;
79
+ color: black;
80
+
81
+ /* Non-interactive */
82
+ pointer-events: none;
83
+ }
84
+
85
+ /* Tooltip box (the rectangle surrounding the text) */
86
+ .Tooltip-box,
87
+ .Tooltip-box-shadow,
88
+ .Tooltip-box-highlight {
89
+ /* Fill all space with rectangle */
90
+ position: absolute;
91
+ left: 0;
92
+ top: 0;
93
+ display: inline-block;
94
+ width: 100%;
95
+ height: 100%;
96
+
97
+ /* Rounded border */
98
+ border: 0.15rem solid black;
99
+ border-radius: ${TOOLTIP_BORDER_RADIUS_REM}rem;
100
+ }
101
+
102
+ /* Tooltip highlight (the bright background to the text) */
103
+ .Tooltip-box-highlight {
104
+ /* Bootstrap warning variant background */
105
+ background-color: rgb(255, 193, 7);
106
+ border-radius: ${TOOLTIP_BORDER_RADIUS_REM}rem;
107
+
108
+ /* Place on top */
109
+ z-index: -1;
110
+ }
111
+
112
+ /* Tooltip box (the black rectangle) */
113
+ .Tooltip-box {
114
+ /* Plain black background */
115
+ background-color: black;
116
+
117
+ /* Place in middle */
118
+ z-index: -2;
119
+ }
120
+
121
+ /* Tooltip box shadow (the white shadow beneath the box) */
122
+ .Tooltip-box-shadow {
123
+ /* White shadow */
124
+ box-shadow: 0 0 0.3rem white;
125
+
126
+ /* Place beneath */
127
+ z-index: -3;
128
+ }
129
+
130
+ /* Tooltip contents (the text and icon) */
131
+ .Tooltip-contents {
132
+ /* Center text, don't allow overflow */
133
+ white-space: nowrap;
134
+ overflow: hidden;
135
+ text-align: center;
136
+ text-overflow: ellipsis;
137
+ display: flex;
138
+ align-items: center;
139
+ justify-content: center;
140
+
141
+ /* Space within parent */
142
+ z-index: 1;
143
+ padding-left: 0.2rem;
144
+ padding-right: 0.2rem;
145
+ height: 100%;
146
+ }
147
+
148
+ /* Tooltip text */
149
+ .Tooltip-text {
150
+ /* Center text, add ellipsis */
151
+ overflow: hidden;
152
+ text-align: center;
153
+ text-overflow: ellipsis;
154
+ }
155
+
156
+ /* Classes to switch between tooltip visible/hidden */
157
+ .Tooltip-tooltip-visible-true {
158
+ opacity: 1;
159
+ }
160
+ .Tooltip-tooltip-visible-false {
161
+ opacity: 0;
162
+ }
163
+
164
+ /* Classes to switch between tooltip sizes */
165
+ .Tooltip-size-thin {
166
+ width: ${TOOLTIP_WIDTH_THIN_REM}rem;
167
+ left: calc(50% - ${TOOLTIP_WIDTH_THIN_REM / 2}rem);
168
+ }
169
+ .Tooltip-size-normal {
170
+ width: ${TOOLTIP_WIDTH_NORMAL_REM}rem;
171
+ left: calc(50% - ${TOOLTIP_WIDTH_NORMAL_REM / 2}rem);
172
+ }
173
+ .Tooltip-size-wide {
174
+ width: ${TOOLTIP_WIDTH_WIDE_REM}rem;
175
+ left: calc(50% - ${TOOLTIP_WIDTH_WIDE_REM / 2}rem);
176
+ }
177
+
178
+ /* Arrow (beneath tooltip) */
179
+ .Tooltip-arrow,
180
+ .Tooltip-arrow-shadow {
181
+ /* Position beneath tooltip */
182
+ position: absolute;
183
+ top: -1.5rem;
184
+ font-size: 1.5rem;
185
+ left: calc(50% - 0.5rem);
186
+ width: 0;
187
+ display: inline-block;
188
+
189
+ /* Non-interactive */
190
+ pointer-events: none;
191
+ }
192
+
193
+ /* Arrow itself */
194
+ .Tooltip-arrow {
195
+ color: black;
196
+ z-index: 4;
197
+ }
198
+
199
+ /* Arrow shadow */
200
+ .Tooltip-arrow-shadow {
201
+ z-index: -1;
202
+
203
+ /* Blur a white version of the caret as a shadow */
204
+ color: white;
205
+ filter: blur(0.2rem);
206
+ }
207
+
208
+ /* Classes to switch between arrow visible/hidden */
209
+ .Tooltip-arrow-visible-true {
210
+ opacity: 1;
211
+ }
212
+ .Tooltip-arrow-visible-false {
213
+ opacity: 0;
214
+ }
215
+ `;
216
+
217
+ /*------------------------------------------------------------------------*/
218
+ /* -------------------------------- State ------------------------------- */
219
+ /*------------------------------------------------------------------------*/
220
+
221
+ /* -------- State Definition -------- */
222
+
223
+ type State = {
224
+ // If true, the tooltip is visible
225
+ isVisible: boolean,
226
+ // Number of pixels to nudge the tooltip
227
+ nudgePx: number,
228
+ // If true, bottom left corner of tooltip should not be rounded
229
+ squareBottomLeft?: boolean,
230
+ // If true, bottom right corner of tooltip should not be rounded
231
+ squareBottomRight?: boolean,
232
+ };
233
+
234
+ /* ------------- Actions ------------ */
235
+
236
+ // Types of actions
237
+ enum ActionType {
238
+ // Make the tooltip visible
239
+ Show = 'Show',
240
+ // Make the tooltip invisible
241
+ Hide = 'Hide',
242
+ // Set nudge pixels
243
+ SetNudgePx = 'SetNudgePx',
244
+ // Set whether bottom left corner should be square
245
+ SetSquareBottomLeft = 'SetSquareBottomLeft',
246
+ // Set whether bottom right corner should be square
247
+ SetSquareBottomRight = 'SetSquareBottomRight',
248
+ }
249
+
250
+ // Action definitions
251
+ type Action = (
252
+ | {
253
+ // Action type
254
+ type: ActionType.SetNudgePx,
255
+ // Number of pixels to nudge the tooltip
256
+ nudgePx: number,
257
+ }
258
+ | {
259
+ // Action type
260
+ type: ActionType.SetSquareBottomLeft,
261
+ // If true, bottom left corner of tooltip should not be rounded
262
+ squareBottomLeft: boolean,
263
+ }
264
+ | {
265
+ // Action type
266
+ type: ActionType.SetSquareBottomRight,
267
+ // If true, bottom right corner of tooltip should not be rounded
268
+ squareBottomRight: boolean,
269
+ }
270
+ | {
271
+ // Action type
272
+ type: (
273
+ | ActionType.Show
274
+ | ActionType.Hide
275
+ ),
276
+ }
277
+ );
278
+
279
+ /**
280
+ * Reducer that executes actions
281
+ * @author Gabe Abrams
282
+ * @param state current state
283
+ * @param action action to execute
284
+ */
285
+ const reducer = (state: State, action: Action): State => {
286
+ switch (action.type) {
287
+ case ActionType.Show: {
288
+ return {
289
+ ...state,
290
+ isVisible: true,
291
+ };
292
+ }
293
+ case ActionType.Hide: {
294
+ return {
295
+ ...state,
296
+ isVisible: false,
297
+ nudgePx: 0,
298
+ };
299
+ }
300
+ case ActionType.SetNudgePx: {
301
+ return {
302
+ ...state,
303
+ nudgePx: action.nudgePx,
304
+ };
305
+ }
306
+ case ActionType.SetSquareBottomLeft: {
307
+ return {
308
+ ...state,
309
+ squareBottomLeft: action.squareBottomLeft,
310
+ };
311
+ }
312
+ case ActionType.SetSquareBottomRight: {
313
+ return {
314
+ ...state,
315
+ squareBottomRight: action.squareBottomRight,
316
+ };
317
+ }
318
+ default: {
319
+ return state;
320
+ }
321
+ }
322
+ };
323
+
324
+ /*------------------------------------------------------------------------*/
325
+ /* ------------------------------ Component ----------------------------- */
326
+ /*------------------------------------------------------------------------*/
327
+
328
+ const Tooltip: React.FC<Props> = (props) => {
329
+ /*------------------------------------------------------------------------*/
330
+ /* -------------------------------- Setup ------------------------------- */
331
+ /*------------------------------------------------------------------------*/
332
+
333
+ /* -------------- Props ------------- */
334
+
335
+ // Destructure all props
336
+ const {
337
+ icon,
338
+ text,
339
+ children,
340
+ wide,
341
+ thin,
342
+ } = props;
343
+
344
+ /* -------------- State ------------- */
345
+
346
+ // Initial state
347
+ const initialState: State = {
348
+ isVisible: false,
349
+ nudgePx: 0,
350
+ };
351
+
352
+ // Initialize state
353
+ const [state, dispatch] = useReducer(reducer, initialState);
354
+
355
+ // Destructure common state
356
+ const {
357
+ isVisible,
358
+ nudgePx,
359
+ squareBottomLeft,
360
+ squareBottomRight,
361
+ } = state;
362
+
363
+ /* -------------- Refs -------------- */
364
+
365
+ // Initialize refs
366
+ const tooltipContainer = useRef<HTMLDivElement | null>(null);
367
+ const arrowElement = useRef<HTMLDivElement | null>(null);
368
+
369
+ /*------------------------------------------------------------------------*/
370
+ /* ------------------------- Lifecycle Functions ------------------------ */
371
+ /*------------------------------------------------------------------------*/
372
+
373
+ /**
374
+ * Update (also called on mount)
375
+ * @author Gabe Abrams
376
+ */
377
+ useEffect(
378
+ () => {
379
+ // Update tooltip container position
380
+ if (tooltipContainer.current && isVisible) {
381
+ // Get the tooltip container location
382
+ const tooltipContainerRect = (
383
+ tooltipContainer
384
+ .current
385
+ .getBoundingClientRect()
386
+ );
387
+
388
+ // Get the window width
389
+ const windowWidth = window.innerWidth;
390
+
391
+ // Calculate new nudge value
392
+ let newNudgePx: number;
393
+ if (tooltipContainerRect.left < 0) {
394
+ // Tooltip is off the left side of the screen
395
+ newNudgePx = -1 * tooltipContainerRect.left;
396
+ } else if (tooltipContainerRect.right > windowWidth) {
397
+ // Tooltip is off the right side of the screen
398
+ newNudgePx = -1 * (tooltipContainerRect.right - windowWidth);
399
+ } else {
400
+ // Tooltip is in the middle of the screen
401
+ newNudgePx = 0;
402
+ }
403
+
404
+ // Update state
405
+ dispatch({
406
+ type: ActionType.SetNudgePx,
407
+ nudgePx: newNudgePx,
408
+ });
409
+ }
410
+
411
+ // Update border rounding
412
+ if (arrowElement.current && isVisible) {
413
+ // Get the arrow location
414
+ const arrowRect = (
415
+ arrowElement
416
+ .current
417
+ .getBoundingClientRect()
418
+ );
419
+
420
+ // REM in pixels given font size
421
+ const remInPixels = Number.parseInt(
422
+ getComputedStyle(document.documentElement).fontSize,
423
+ 10,
424
+ );
425
+
426
+ // Set bottom left corner rounding
427
+ dispatch({
428
+ type: ActionType.SetSquareBottomLeft,
429
+ squareBottomLeft: (
430
+ arrowRect.left < (TOOLTIP_BORDER_RADIUS_REM * remInPixels)
431
+ ),
432
+ });
433
+
434
+ // Set bottom right corner rounding
435
+ dispatch({
436
+ type: ActionType.SetSquareBottomRight,
437
+ squareBottomRight: (
438
+ arrowRect.right
439
+ > window.innerWidth - (TOOLTIP_BORDER_RADIUS_REM * remInPixels)
440
+ ),
441
+ });
442
+ }
443
+ },
444
+ [isVisible],
445
+ );
446
+
447
+ /*------------------------------------------------------------------------*/
448
+ /* ------------------------------- Render ------------------------------- */
449
+ /*------------------------------------------------------------------------*/
450
+
451
+ /*----------------------------------------*/
452
+ /* --------------- Main UI -------------- */
453
+ /*----------------------------------------*/
454
+
455
+ // Determine size
456
+ let size = 'normal';
457
+ if (wide) {
458
+ size = 'wide';
459
+ } else if (thin) {
460
+ size = 'thin';
461
+ }
462
+
463
+ // Render
464
+ return (
465
+ <div
466
+ className="Tooltip-outer-container"
467
+ aria-label={text}
468
+ onMouseEnter={() => {
469
+ dispatch({
470
+ type: ActionType.Show,
471
+ });
472
+ }}
473
+ onMouseLeave={() => {
474
+ dispatch({
475
+ type: ActionType.Hide,
476
+ });
477
+ }}
478
+ onFocus={() => {
479
+ dispatch({
480
+ type: ActionType.Show,
481
+ });
482
+ }}
483
+ onBlur={() => {
484
+ dispatch({
485
+ type: ActionType.Hide,
486
+ });
487
+ }}
488
+ >
489
+ {/* Style */}
490
+ <style>
491
+ {style}
492
+ </style>
493
+
494
+ {/* Children */}
495
+ {children}
496
+
497
+ {/* Arrow */}
498
+ <div
499
+ className={`Tooltip-arrow Tooltip-arrow-visible-${isVisible ? 'true' : 'false'}`}
500
+ ref={arrowElement}
501
+ >
502
+ <FontAwesomeIcon
503
+ icon={faCaretDown}
504
+ />
505
+ </div>
506
+ <div className={`Tooltip-arrow-shadow Tooltip-arrow-visible-${isVisible ? 'true' : 'false'}`}>
507
+ <FontAwesomeIcon
508
+ icon={faCaretDown}
509
+ />
510
+ </div>
511
+
512
+ {/* Tooltip */}
513
+ <div
514
+ className="Tooltip-tooltip-container"
515
+ >
516
+ <div
517
+ className={`Tooltip-tooltip Tooltip-tooltip-visible-${isVisible ? 'true' : 'false'} Tooltip-size-${size}`}
518
+ role="tooltip"
519
+ ref={tooltipContainer}
520
+ style={{
521
+ transform: `translateX(${nudgePx}px)`,
522
+ }}
523
+ >
524
+ {/* Box */}
525
+ <div className="Tooltip-box-highlight" />
526
+ <div
527
+ className="Tooltip-box"
528
+ style={{
529
+ borderBottomLeftRadius: squareBottomLeft ? 0 : undefined,
530
+ borderBottomRightRadius: squareBottomRight ? 0 : undefined,
531
+ }}
532
+ />
533
+ <div
534
+ className="Tooltip-box-shadow"
535
+ style={{
536
+ borderBottomLeftRadius: squareBottomLeft ? 0 : undefined,
537
+ borderBottomRightRadius: squareBottomRight ? 0 : undefined,
538
+ }}
539
+ />
540
+
541
+ {/* Contents */}
542
+ <div className="Tooltip-contents">
543
+ {/* Icon */}
544
+ {icon && (
545
+ <FontAwesomeIcon
546
+ icon={icon}
547
+ className="me-1"
548
+ />
549
+ )}
550
+
551
+ {/* Text */}
552
+ <span className="Tooltip-text">
553
+ {text}
554
+ </span>
555
+ </div>
556
+ </div>
557
+ </div>
558
+ </div>
559
+ );
560
+ };
561
+
562
+ /*------------------------------------------------------------------------*/
563
+ /* ------------------------------- Wrap Up ------------------------------ */
564
+ /*------------------------------------------------------------------------*/
565
+
566
+ // Export component
567
+ export default Tooltip;
package/src/index.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  // Import components
2
- import AppWrapper, { alert, confirm, showFatalError } from './components/AppWrapper';
2
+ import AppWrapper, {
3
+ alert,
4
+ confirm,
5
+ showFatalError,
6
+ addFatalErrorHandler,
7
+ } from './components/AppWrapper';
3
8
  import LoadingSpinner from './components/LoadingSpinner';
4
9
  import ErrorBox from './components/ErrorBox';
5
10
  import Modal from './components/Modal';
@@ -18,6 +23,8 @@ import LogReviewer from './components/LogReviewer';
18
23
  import IntelliTable from './components/IntelliTable';
19
24
  import CSVDownloadButton from './components/CSVDownloadButton';
20
25
  import DBEntryManagerPanel from './components/DBEntryManagerPanel';
26
+ import Tooltip from './components/Tooltip';
27
+ import ToggleSwitch from './components/ToggleSwitch';
21
28
 
22
29
  // Import errors
23
30
  import ErrorWithCode from './errors/ErrorWithCode';
@@ -115,6 +122,8 @@ export {
115
122
  IntelliTable,
116
123
  CSVDownloadButton,
117
124
  DBEntryManagerPanel,
125
+ Tooltip,
126
+ ToggleSwitch,
118
127
  // Global functions
119
128
  alert,
120
129
  confirm,
@@ -162,6 +171,7 @@ export {
162
171
  initClient,
163
172
  visitServerEndpoint,
164
173
  logClientEvent,
174
+ addFatalErrorHandler,
165
175
  // Server helpers
166
176
  initServer,
167
177
  genRouteHandler,