dce-reactkit 3.6.28 → 3.7.1-beta.2

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.
@@ -3,14 +3,9 @@
3
3
  * @author Gabe Abrams
4
4
  */
5
5
  import React from 'react';
6
- import { IconProp } from '@fortawesome/fontawesome-svg-core';
7
6
  type Props = {
8
- icon?: IconProp;
9
7
  text: string;
10
- children: React.ReactNode;
11
- wide?: boolean;
12
- thin?: boolean;
13
- containerIsDisplayBlock?: boolean;
8
+ children: JSX.Element;
14
9
  };
15
10
  declare const Tooltip: React.FC<Props>;
16
11
  export default Tooltip;
package/dist/index.d.ts CHANGED
@@ -588,12 +588,8 @@ declare const DBEntryManagerPanel: React.FC<Props$4>;
588
588
  */
589
589
 
590
590
  type Props$3 = {
591
- icon?: IconProp;
592
591
  text: string;
593
- children: React.ReactNode;
594
- wide?: boolean;
595
- thin?: boolean;
596
- containerIsDisplayBlock?: boolean;
592
+ children: JSX.Element;
597
593
  };
598
594
  declare const Tooltip: React.FC<Props$3>;
599
595
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dce-reactkit",
3
- "version": "3.6.28",
3
+ "version": "3.7.1-beta.2",
4
4
  "description": "Shared components for Harvard DCE apps",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -40,6 +40,7 @@
40
40
  "@rollup/plugin-json": "^4.1.0",
41
41
  "@rollup/plugin-node-resolve": "^13.2.1",
42
42
  "@rollup/plugin-typescript": "^8.3.2",
43
+ "@types/bootstrap": "^5.2.8",
43
44
  "@types/express": "^4.17.17",
44
45
  "@types/jest": "^28.1.7",
45
46
  "@types/node": "^20.2.5",
@@ -632,16 +632,54 @@ const AppWrapper: React.FC<Props> = (props: Props): React.ReactElement => {
632
632
  body = children;
633
633
  }
634
634
 
635
+ /* ------------- Tooltip ------------ */
636
+
637
+ const tooltipStyle = (
638
+ isDarkModeOn()
639
+ ? `
640
+ .tooltip-inner {
641
+ background-color: white;
642
+ color: black;
643
+ border: 0.1rem solid black;
644
+ }
645
+ div[data-popper-placement="top"] .tooltip-arrow::before {
646
+ border-top-color: white !important;
647
+ transform: translate(0, -0.05rem);
648
+ }
649
+ `
650
+ : `
651
+ .tooltip-inner {
652
+ background-color: black;
653
+ color: black;
654
+ border: 0.1rem solid white;
655
+ }
656
+ div[data-popper-placement="top"] .tooltip-arrow::before {
657
+ border-top-color: black !important;
658
+ transform: translate(0, -0.05rem);
659
+ }
660
+ `
661
+ );
662
+
635
663
  /*----------------------------------------*/
636
664
  /* --------------- Main UI -------------- */
637
665
  /*----------------------------------------*/
638
666
 
639
667
  return (
640
668
  <>
669
+ {/* Style */}
641
670
  <style>
642
671
  {style}
643
672
  </style>
673
+
674
+ {/* Tooltip Style */}
675
+ <style>
676
+ {tooltipStyle}
677
+ </style>
678
+
679
+ {/* Modal */}
644
680
  {modal}
681
+
682
+ {/* Body */}
645
683
  {body}
646
684
  </>
647
685
  );
@@ -4,12 +4,10 @@
4
4
  */
5
5
 
6
6
  // Import React
7
- import React, { useEffect, useReducer, useRef } from 'react';
7
+ import React, { useEffect, useRef } from 'react';
8
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';
9
+ // Import Bootstrap
10
+ import { Tooltip as BSTooltip } from 'bootstrap';
13
11
 
14
12
  /*------------------------------------------------------------------------*/
15
13
  /* -------------------------------- Types ------------------------------- */
@@ -17,309 +15,10 @@ import { faCaretDown } from '@fortawesome/free-solid-svg-icons';
17
15
 
18
16
  // Props definition
19
17
  type Props = {
20
- // Icon to show next to the text
21
- icon?: IconProp,
22
18
  // Text of the tooltip
23
19
  text: string,
24
20
  // 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
- // If true, the tooltip container should be display: block
31
- containerIsDisplayBlock?: boolean,
32
- };
33
-
34
- /*------------------------------------------------------------------------*/
35
- /* ------------------------------ Constants ----------------------------- */
36
- /*------------------------------------------------------------------------*/
37
-
38
- // Tooltip widths
39
- const TOOLTIP_WIDTH_THIN_REM = 5;
40
- const TOOLTIP_WIDTH_NORMAL_REM = 7;
41
- const TOOLTIP_WIDTH_WIDE_REM = 10;
42
-
43
- // Tooltip border radius
44
- const TOOLTIP_BORDER_RADIUS_REM = 0.3;
45
-
46
- /*------------------------------------------------------------------------*/
47
- /* -------------------------------- Style ------------------------------- */
48
- /*------------------------------------------------------------------------*/
49
-
50
- const style = `
51
- /* Container for contents and tooltip */
52
- .Tooltip-outer-container {
53
- position: relative;
54
- }
55
-
56
- /* Container for just tooltip */
57
- .Tooltip-tooltip-container {
58
- /* Position in middle but take no space */
59
- /* (so layout of page is unaffected) */
60
- position: absolute;
61
- display: inline-block;
62
- top: 0;
63
- left: 50%;
64
- height: 0;
65
- width: 0;
66
- overflow: visible;
67
- }
68
-
69
- /* Tooltip */
70
- .Tooltip-tooltip {
71
- /* Position above item */
72
- display: inline-block;
73
- position: absolute;
74
- top: -2.3rem;
75
- height: 1.8rem;
76
- z-index: 1;
77
-
78
- /* Use standard black font */
79
- font-size: 1rem;
80
- color: black;
81
-
82
- /* Non-interactive */
83
- pointer-events: none;
84
- }
85
-
86
- /* Tooltip box (the rectangle surrounding the text) */
87
- .Tooltip-box,
88
- .Tooltip-box-shadow,
89
- .Tooltip-box-highlight {
90
- /* Fill all space with rectangle */
91
- position: absolute;
92
- left: 0;
93
- top: 0;
94
- display: inline-block;
95
- width: 100%;
96
- height: 100%;
97
-
98
- /* Rounded border */
99
- border: 0.15rem solid black;
100
- border-radius: ${TOOLTIP_BORDER_RADIUS_REM}rem;
101
- }
102
-
103
- /* Tooltip highlight (the bright background to the text) */
104
- .Tooltip-box-highlight {
105
- /* Bootstrap warning variant background */
106
- background-color: rgb(255, 193, 7);
107
- border-radius: ${TOOLTIP_BORDER_RADIUS_REM}rem;
108
-
109
- /* Place on top */
110
- z-index: -1;
111
- }
112
-
113
- /* Tooltip box (the black rectangle) */
114
- .Tooltip-box {
115
- /* Plain black background */
116
- background-color: black;
117
-
118
- /* Place in middle */
119
- z-index: -2;
120
- }
121
-
122
- /* Tooltip box shadow (the white shadow beneath the box) */
123
- .Tooltip-box-shadow {
124
- /* White shadow */
125
- box-shadow: 0 0 0.3rem white;
126
-
127
- /* Place beneath */
128
- z-index: -3;
129
- }
130
-
131
- /* Tooltip contents (the text and icon) */
132
- .Tooltip-contents {
133
- /* Center text, don't allow overflow */
134
- white-space: nowrap;
135
- overflow: hidden;
136
- text-align: center;
137
- text-overflow: ellipsis;
138
- display: flex;
139
- align-items: center;
140
- justify-content: center;
141
-
142
- /* Space within parent */
143
- z-index: 1;
144
- padding-left: 0.2rem;
145
- padding-right: 0.2rem;
146
- height: 100%;
147
- }
148
-
149
- /* Tooltip text */
150
- .Tooltip-text {
151
- /* Center text, add ellipsis */
152
- overflow: hidden;
153
- text-align: center;
154
- text-overflow: ellipsis;
155
- }
156
-
157
- /* Classes to switch between tooltip visible/hidden */
158
- .Tooltip-tooltip-visible-true {
159
- opacity: 1;
160
- }
161
- .Tooltip-tooltip-visible-false {
162
- opacity: 0;
163
- }
164
-
165
- /* Classes to switch between tooltip sizes */
166
- .Tooltip-size-thin {
167
- width: ${TOOLTIP_WIDTH_THIN_REM}rem;
168
- left: calc(50% - ${TOOLTIP_WIDTH_THIN_REM / 2}rem);
169
- }
170
- .Tooltip-size-normal {
171
- width: ${TOOLTIP_WIDTH_NORMAL_REM}rem;
172
- left: calc(50% - ${TOOLTIP_WIDTH_NORMAL_REM / 2}rem);
173
- }
174
- .Tooltip-size-wide {
175
- width: ${TOOLTIP_WIDTH_WIDE_REM}rem;
176
- left: calc(50% - ${TOOLTIP_WIDTH_WIDE_REM / 2}rem);
177
- }
178
-
179
- /* Arrow (beneath tooltip) */
180
- .Tooltip-arrow,
181
- .Tooltip-arrow-shadow {
182
- /* Position beneath tooltip */
183
- position: absolute;
184
- top: -1.5rem;
185
- font-size: 1.5rem;
186
- left: calc(50% - 0.5rem);
187
- width: 0;
188
- display: inline-block;
189
-
190
- /* Non-interactive */
191
- pointer-events: none;
192
- }
193
-
194
- /* Arrow itself */
195
- .Tooltip-arrow {
196
- color: black;
197
- z-index: 4;
198
- }
199
-
200
- /* Arrow shadow */
201
- .Tooltip-arrow-shadow {
202
- z-index: -1;
203
-
204
- /* Blur a white version of the caret as a shadow */
205
- color: white;
206
- filter: blur(0.2rem);
207
- }
208
-
209
- /* Classes to switch between arrow visible/hidden */
210
- .Tooltip-arrow-visible-true {
211
- opacity: 1;
212
- }
213
- .Tooltip-arrow-visible-false {
214
- opacity: 0;
215
- }
216
- `;
217
-
218
- /*------------------------------------------------------------------------*/
219
- /* -------------------------------- State ------------------------------- */
220
- /*------------------------------------------------------------------------*/
221
-
222
- /* -------- State Definition -------- */
223
-
224
- type State = {
225
- // If true, the tooltip is visible
226
- isVisible: boolean,
227
- // Number of pixels to nudge the tooltip
228
- nudgePx: number,
229
- // If true, bottom left corner of tooltip should not be rounded
230
- squareBottomLeft?: boolean,
231
- // If true, bottom right corner of tooltip should not be rounded
232
- squareBottomRight?: boolean,
233
- };
234
-
235
- /* ------------- Actions ------------ */
236
-
237
- // Types of actions
238
- enum ActionType {
239
- // Make the tooltip visible
240
- Show = 'Show',
241
- // Make the tooltip invisible
242
- Hide = 'Hide',
243
- // Set nudge pixels
244
- SetNudgePx = 'SetNudgePx',
245
- // Set whether bottom left corner should be square
246
- SetSquareBottomLeft = 'SetSquareBottomLeft',
247
- // Set whether bottom right corner should be square
248
- SetSquareBottomRight = 'SetSquareBottomRight',
249
- }
250
-
251
- // Action definitions
252
- type Action = (
253
- | {
254
- // Action type
255
- type: ActionType.SetNudgePx,
256
- // Number of pixels to nudge the tooltip
257
- nudgePx: number,
258
- }
259
- | {
260
- // Action type
261
- type: ActionType.SetSquareBottomLeft,
262
- // If true, bottom left corner of tooltip should not be rounded
263
- squareBottomLeft: boolean,
264
- }
265
- | {
266
- // Action type
267
- type: ActionType.SetSquareBottomRight,
268
- // If true, bottom right corner of tooltip should not be rounded
269
- squareBottomRight: boolean,
270
- }
271
- | {
272
- // Action type
273
- type: (
274
- | ActionType.Show
275
- | ActionType.Hide
276
- ),
277
- }
278
- );
279
-
280
- /**
281
- * Reducer that executes actions
282
- * @author Gabe Abrams
283
- * @param state current state
284
- * @param action action to execute
285
- */
286
- const reducer = (state: State, action: Action): State => {
287
- switch (action.type) {
288
- case ActionType.Show: {
289
- return {
290
- ...state,
291
- isVisible: true,
292
- };
293
- }
294
- case ActionType.Hide: {
295
- return {
296
- ...state,
297
- isVisible: false,
298
- nudgePx: 0,
299
- };
300
- }
301
- case ActionType.SetNudgePx: {
302
- return {
303
- ...state,
304
- nudgePx: action.nudgePx,
305
- };
306
- }
307
- case ActionType.SetSquareBottomLeft: {
308
- return {
309
- ...state,
310
- squareBottomLeft: action.squareBottomLeft,
311
- };
312
- }
313
- case ActionType.SetSquareBottomRight: {
314
- return {
315
- ...state,
316
- squareBottomRight: action.squareBottomRight,
317
- };
318
- }
319
- default: {
320
- return state;
321
- }
322
- }
21
+ children: JSX.Element,
323
22
  };
324
23
 
325
24
  /*------------------------------------------------------------------------*/
@@ -335,38 +34,14 @@ const Tooltip: React.FC<Props> = (props) => {
335
34
 
336
35
  // Destructure all props
337
36
  const {
338
- icon,
339
37
  text,
340
38
  children,
341
- wide,
342
- thin,
343
- containerIsDisplayBlock,
344
39
  } = props;
345
40
 
346
- /* -------------- State ------------- */
347
-
348
- // Initial state
349
- const initialState: State = {
350
- isVisible: false,
351
- nudgePx: 0,
352
- };
353
-
354
- // Initialize state
355
- const [state, dispatch] = useReducer(reducer, initialState);
356
-
357
- // Destructure common state
358
- const {
359
- isVisible,
360
- nudgePx,
361
- squareBottomLeft,
362
- squareBottomRight,
363
- } = state;
364
-
365
41
  /* -------------- Refs -------------- */
366
42
 
367
- // Initialize refs
368
- const tooltipContainer = useRef<HTMLDivElement | null>(null);
369
- const arrowElement = useRef<HTMLDivElement | null>(null);
43
+ // Child ref
44
+ const childRef = useRef<Element>(undefined as unknown as Element);
370
45
 
371
46
  /*------------------------------------------------------------------------*/
372
47
  /* ------------------------- Lifecycle Functions ------------------------ */
@@ -378,72 +53,27 @@ const Tooltip: React.FC<Props> = (props) => {
378
53
  */
379
54
  useEffect(
380
55
  () => {
381
- // Update tooltip container position
382
- if (tooltipContainer.current && isVisible) {
383
- // Get the tooltip container location
384
- const tooltipContainerRect = (
385
- tooltipContainer
386
- .current
387
- .getBoundingClientRect()
388
- );
389
-
390
- // Get the window width
391
- const windowWidth = window.innerWidth;
392
-
393
- // Calculate new nudge value
394
- let newNudgePx: number;
395
- if (tooltipContainerRect.left < 0) {
396
- // Tooltip is off the left side of the screen
397
- newNudgePx = -1 * tooltipContainerRect.left;
398
- } else if (tooltipContainerRect.right > windowWidth) {
399
- // Tooltip is off the right side of the screen
400
- newNudgePx = -1 * (tooltipContainerRect.right - windowWidth);
401
- } else {
402
- // Tooltip is in the middle of the screen
403
- newNudgePx = 0;
404
- }
405
-
406
- // Update state
407
- dispatch({
408
- type: ActionType.SetNudgePx,
409
- nudgePx: newNudgePx,
410
- });
56
+ // Skip if no child
57
+ if (!childRef.current) {
58
+ return;
411
59
  }
412
60
 
413
- // Update border rounding
414
- if (arrowElement.current && isVisible) {
415
- // Get the arrow location
416
- const arrowRect = (
417
- arrowElement
418
- .current
419
- .getBoundingClientRect()
420
- );
421
-
422
- // REM in pixels given font size
423
- const remInPixels = Number.parseInt(
424
- getComputedStyle(document.documentElement).fontSize,
425
- 10,
426
- );
427
-
428
- // Set bottom left corner rounding
429
- dispatch({
430
- type: ActionType.SetSquareBottomLeft,
431
- squareBottomLeft: (
432
- arrowRect.left < (TOOLTIP_BORDER_RADIUS_REM * remInPixels)
433
- ),
434
- });
435
-
436
- // Set bottom right corner rounding
437
- dispatch({
438
- type: ActionType.SetSquareBottomRight,
439
- squareBottomRight: (
440
- arrowRect.right
441
- > window.innerWidth - (TOOLTIP_BORDER_RADIUS_REM * remInPixels)
442
- ),
443
- });
444
- }
61
+ // Initialize tooltip
62
+ const t = new BSTooltip(
63
+ childRef.current,
64
+ {
65
+ title: text,
66
+ placement: 'top',
67
+ trigger: 'hover',
68
+ },
69
+ );
70
+
71
+ // Clean up tooltip
72
+ return () => {
73
+ t.dispose();
74
+ };
445
75
  },
446
- [isVisible],
76
+ [text],
447
77
  );
448
78
 
449
79
  /*------------------------------------------------------------------------*/
@@ -454,110 +84,10 @@ const Tooltip: React.FC<Props> = (props) => {
454
84
  /* --------------- Main UI -------------- */
455
85
  /*----------------------------------------*/
456
86
 
457
- // Determine size
458
- let size = 'normal';
459
- if (wide) {
460
- size = 'wide';
461
- } else if (thin) {
462
- size = 'thin';
463
- }
464
-
465
- // Render
466
- return (
467
- <div
468
- className={`Tooltip-outer-container ${containerIsDisplayBlock ? 'd-block' : 'd-inline-block'}}`}
469
- aria-label={text}
470
- onMouseEnter={() => {
471
- dispatch({
472
- type: ActionType.Show,
473
- });
474
- }}
475
- onMouseLeave={() => {
476
- dispatch({
477
- type: ActionType.Hide,
478
- });
479
- }}
480
- onFocus={() => {
481
- dispatch({
482
- type: ActionType.Show,
483
- });
484
- }}
485
- onBlur={() => {
486
- dispatch({
487
- type: ActionType.Hide,
488
- });
489
- }}
490
- >
491
- {/* Style */}
492
- <style>
493
- {style}
494
- </style>
495
-
496
- {/* Children */}
497
- {children}
498
-
499
- {/* Arrow */}
500
- <div
501
- className={`Tooltip-arrow Tooltip-arrow-visible-${isVisible ? 'true' : 'false'}`}
502
- ref={arrowElement}
503
- >
504
- <FontAwesomeIcon
505
- icon={faCaretDown}
506
- />
507
- </div>
508
- <div className={`Tooltip-arrow-shadow Tooltip-arrow-visible-${isVisible ? 'true' : 'false'}`}>
509
- <FontAwesomeIcon
510
- icon={faCaretDown}
511
- />
512
- </div>
513
-
514
- {/* Tooltip */}
515
- <div
516
- className="Tooltip-tooltip-container"
517
- >
518
- <div
519
- className={`Tooltip-tooltip Tooltip-tooltip-visible-${isVisible ? 'true' : 'false'} Tooltip-size-${size}`}
520
- role="tooltip"
521
- ref={tooltipContainer}
522
- style={{
523
- transform: `translateX(${nudgePx}px)`,
524
- }}
525
- >
526
- {/* Box */}
527
- <div className="Tooltip-box-highlight" />
528
- <div
529
- className="Tooltip-box"
530
- style={{
531
- borderBottomLeftRadius: squareBottomLeft ? 0 : undefined,
532
- borderBottomRightRadius: squareBottomRight ? 0 : undefined,
533
- }}
534
- />
535
- <div
536
- className="Tooltip-box-shadow"
537
- style={{
538
- borderBottomLeftRadius: squareBottomLeft ? 0 : undefined,
539
- borderBottomRightRadius: squareBottomRight ? 0 : undefined,
540
- }}
541
- />
542
-
543
- {/* Contents */}
544
- <div className="Tooltip-contents">
545
- {/* Icon */}
546
- {icon && (
547
- <FontAwesomeIcon
548
- icon={icon}
549
- className="me-1"
550
- />
551
- )}
552
-
553
- {/* Text */}
554
- <span className="Tooltip-text">
555
- {text}
556
- </span>
557
- </div>
558
- </div>
559
- </div>
560
- </div>
87
+ // Clone child and add ref
88
+ return React.cloneElement(
89
+ children,
90
+ { ref: childRef },
561
91
  );
562
92
  };
563
93