dce-reactkit 3.0.42 → 3.0.45

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.
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ import React from 'react';
7
7
  * @author Gabe Abrams
8
8
  */
9
9
 
10
- declare type Props$b = {
10
+ declare type Props$c = {
11
11
  children: React.ReactNode;
12
12
  sendRequest: SendRequestFunction;
13
13
  dark?: boolean;
@@ -52,7 +52,7 @@ declare const confirm: (title: string, text: string) => Promise<boolean>;
52
52
  * @param [errorTitle] title of the error box
53
53
  */
54
54
  declare const showFatalError: (error: any, errorTitle?: string) => undefined;
55
- declare const AppWrapper: React.FC<Props$b>;
55
+ declare const AppWrapper: React.FC<Props$c>;
56
56
 
57
57
  /**
58
58
  * Loading spinner/indicator
@@ -65,12 +65,12 @@ declare const LoadingSpinner: () => JSX.Element;
65
65
  * @author Gabe Abrams
66
66
  */
67
67
 
68
- declare type Props$a = {
68
+ declare type Props$b = {
69
69
  error: any;
70
70
  title?: string;
71
71
  onClose?: () => void;
72
72
  };
73
- declare const ErrorBox: React.FC<Props$a>;
73
+ declare const ErrorBox: React.FC<Props$b>;
74
74
 
75
75
  /**
76
76
  * Bootstrap variants
@@ -136,7 +136,7 @@ declare enum ModalType {
136
136
  * @author Gabe Abrams
137
137
  */
138
138
 
139
- declare type Props$9 = {
139
+ declare type Props$a = {
140
140
  type?: ModalType;
141
141
  size?: ModalSize;
142
142
  title?: React.ReactNode;
@@ -165,27 +165,27 @@ declare type Props$9 = {
165
165
  confirmVariant?: Variant;
166
166
  onTopOfOtherModals?: boolean;
167
167
  };
168
- declare const Modal: React.FC<Props$9>;
168
+ declare const Modal: React.FC<Props$a>;
169
169
 
170
170
  /**
171
171
  * A box with a tab on the top that holds buttons and other content
172
172
  * @author Gabe Abrams
173
173
  */
174
174
 
175
- declare type Props$8 = {
175
+ declare type Props$9 = {
176
176
  title: React.ReactNode;
177
177
  children: React.ReactNode;
178
178
  noBottomMargin?: boolean;
179
179
  noBottomPadding?: boolean;
180
180
  };
181
- declare const TabBox: React.FC<Props$8>;
181
+ declare const TabBox: React.FC<Props$9>;
182
182
 
183
183
  /**
184
184
  * A radio selection button
185
185
  * @author Gabe Abrams
186
186
  */
187
187
 
188
- declare type Props$7 = {
188
+ declare type Props$8 = {
189
189
  text: string;
190
190
  onSelected: () => void;
191
191
  ariaLabel: string;
@@ -197,14 +197,14 @@ declare type Props$7 = {
197
197
  unselectedVariant?: Variant;
198
198
  small?: boolean;
199
199
  };
200
- declare const RadioButton: React.FC<Props$7>;
200
+ declare const RadioButton: React.FC<Props$8>;
201
201
 
202
202
  /**
203
203
  * A checkbox button
204
204
  * @author Gabe Abrams
205
205
  */
206
206
 
207
- declare type Props$6 = {
207
+ declare type Props$7 = {
208
208
  text: string;
209
209
  onChanged: (checked: boolean) => void;
210
210
  ariaLabel: string;
@@ -216,26 +216,26 @@ declare type Props$6 = {
216
216
  uncheckedVariant?: Variant;
217
217
  small?: boolean;
218
218
  };
219
- declare const CheckboxButton: React.FC<Props$6>;
219
+ declare const CheckboxButton: React.FC<Props$7>;
220
220
 
221
221
  /**
222
222
  * Input group with a title and space for buttons
223
223
  * @author Gabe Abrams
224
224
  */
225
225
 
226
- declare type Props$5 = {
226
+ declare type Props$6 = {
227
227
  label: string;
228
228
  minLabelWidth?: string;
229
229
  children: React.ReactNode;
230
230
  };
231
- declare const ButtonInputGroup: React.FC<Props$5>;
231
+ declare const ButtonInputGroup: React.FC<Props$6>;
232
232
 
233
233
  /**
234
234
  * A very simple, lightweight date chooser
235
235
  * @author Gabe Abrams
236
236
  */
237
237
 
238
- declare type Props$4 = {
238
+ declare type Props$5 = {
239
239
  ariaLabel: string;
240
240
  name: string;
241
241
  month: number;
@@ -250,53 +250,70 @@ declare type Props$4 = {
250
250
  onChange: (month: number, day: number, year: number) => void;
251
251
  numMonthsToShow?: number;
252
252
  };
253
- declare const SimpleDateChooser: React.FC<Props$4>;
253
+ declare const SimpleDateChooser: React.FC<Props$5>;
254
254
 
255
255
  /**
256
256
  * Drawer container
257
257
  * @author Gabe Abrams
258
258
  */
259
259
 
260
- declare type Props$3 = {
260
+ declare type Props$4 = {
261
261
  children: React.ReactNode;
262
262
  };
263
- declare const Drawer: React.FC<Props$3>;
263
+ declare const Drawer: React.FC<Props$4>;
264
264
 
265
265
  /**
266
266
  * Success checkmark that pops into view
267
267
  * @author Gabe Abrams
268
268
  */
269
269
 
270
- declare type Props$2 = {
270
+ declare type Props$3 = {
271
271
  sizeRem?: number;
272
272
  circleVariant?: string;
273
273
  checkVariant?: string;
274
274
  };
275
- declare const PopSuccessMark: React.FC<Props$2>;
275
+ declare const PopSuccessMark: React.FC<Props$3>;
276
276
 
277
277
  /**
278
278
  * Failure x mark that pops into view
279
279
  * @author Gabe Abrams
280
280
  */
281
281
 
282
- declare type Props$1 = {
282
+ declare type Props$2 = {
283
283
  sizeRem?: number;
284
284
  circleVariant?: string;
285
285
  xVariant?: string;
286
286
  };
287
- declare const PopFailureMark: React.FC<Props$1>;
287
+ declare const PopFailureMark: React.FC<Props$2>;
288
288
 
289
289
  /**
290
290
  * Failure pending that pops into view
291
291
  * @author Gabe Abrams
292
292
  */
293
293
 
294
- declare type Props = {
294
+ declare type Props$1 = {
295
295
  sizeRem?: number;
296
296
  circleVariant?: string;
297
297
  hourglassVariant?: string;
298
298
  };
299
- declare const PopPendingMark: React.FC<Props>;
299
+ declare const PopPendingMark: React.FC<Props$1>;
300
+
301
+ /**
302
+ * Copiable text box
303
+ * @author Gabe Abrams
304
+ */
305
+
306
+ declare type Props = {
307
+ name: string;
308
+ text: string;
309
+ label: string;
310
+ labelIcon?: any;
311
+ minLabelWidthRem?: number;
312
+ multiline?: boolean;
313
+ numVisibleLines?: number;
314
+ onClick?: () => void;
315
+ };
316
+ declare const CopiableBox: React.FC<Props>;
300
317
 
301
318
  /**
302
319
  * An error with a code
@@ -594,4 +611,4 @@ declare enum ReactKitErrorCode {
594
611
  NotAdmin = "DRK10"
595
612
  }
596
613
 
597
- export { AppWrapper, ButtonInputGroup, CheckboxButton, DAY_IN_MS, Drawer, ErrorBox, ErrorWithCode, HOUR_IN_MS, LoadingSpinner, MINUTE_IN_MS, Modal, ModalButtonType, ModalSize, ModalType, ParamType, PopFailureMark, PopPendingMark, PopSuccessMark, RadioButton, ReactKitErrorCode, SimpleDateChooser, TabBox, Variant, abbreviate, alert, avg, ceilToNumDecimals, confirm, floorToNumDecimals, forceNumIntoBounds, genRouteHandler, getHumanReadableDate, getOrdinal, getTimeInfoInET, handleError, handleSuccess, initServer, padDecimalZeros, padZerosLeft, roundToNumDecimals, showFatalError, startMinWait, stubServerEndpoint, sum, visitServerEndpoint, waitMs };
614
+ export { AppWrapper, ButtonInputGroup, CheckboxButton, CopiableBox, DAY_IN_MS, Drawer, ErrorBox, ErrorWithCode, HOUR_IN_MS, LoadingSpinner, MINUTE_IN_MS, Modal, ModalButtonType, ModalSize, ModalType, ParamType, PopFailureMark, PopPendingMark, PopSuccessMark, RadioButton, ReactKitErrorCode, SimpleDateChooser, TabBox, Variant, abbreviate, alert, avg, ceilToNumDecimals, confirm, floorToNumDecimals, forceNumIntoBounds, genRouteHandler, getHumanReadableDate, getOrdinal, getTimeInfoInET, handleError, handleSuccess, initServer, padDecimalZeros, padZerosLeft, roundToNumDecimals, showFatalError, startMinWait, stubServerEndpoint, sum, visitServerEndpoint, waitMs };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dce-reactkit",
3
- "version": "3.0.42",
3
+ "version": "3.0.45",
4
4
  "description": "Shared components for Harvard DCE apps",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Copiable text box
3
+ * @author Gabe Abrams
4
+ */
5
+
6
+ // Import React
7
+ import React, { useReducer } from 'react';
8
+
9
+ // Import other reactkit functions
10
+ import { alert, waitMs } from '..';
11
+
12
+ // Import FontAwesome
13
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
14
+ import { faClipboard } from '@fortawesome/free-solid-svg-icons';
15
+
16
+ /*------------------------------------------------------------------------*/
17
+ /* Types */
18
+ /*------------------------------------------------------------------------*/
19
+
20
+ // Props definition
21
+ type Props = {
22
+ // Unique name of the item to copy (no spaces, compatible with css classes)
23
+ name: string,
24
+ // The text to copy
25
+ text: string,
26
+ // Human-readable label of the copy field
27
+ label: string,
28
+ // FontAwesome icon to place before the label
29
+ labelIcon?: any,
30
+ // If defined, the label will have a minimum width
31
+ minLabelWidthRem?: number,
32
+ // If true, the box will be a textarea to support larger, multiline text
33
+ multiline?: boolean,
34
+ // Number of lines to show in multiline view (only relevant if multiline)
35
+ numVisibleLines?: number,
36
+ // If defined, text box becomes clickable and this is the handler
37
+ onClick?: () => void,
38
+ };
39
+
40
+ /*------------------------------------------------------------------------*/
41
+ /* State */
42
+ /*------------------------------------------------------------------------*/
43
+
44
+ /* -------- State Definition -------- */
45
+
46
+ type State = {
47
+ // True if text was recently copied
48
+ recentlyCopied: boolean,
49
+ };
50
+
51
+ /* ------------- Actions ------------ */
52
+
53
+ // Types of actions
54
+ enum ActionType {
55
+ // Indicate that the text was recently copied
56
+ IndicateRecentlyCopied = 'indicate-recently-copied',
57
+ // Clear the status
58
+ ClearRecentlyCopiedStatus = 'clear-recently-copied-status',
59
+ }
60
+
61
+ // Action definitions
62
+ type Action = {
63
+ // Action type
64
+ type: (
65
+ | ActionType.IndicateRecentlyCopied
66
+ | ActionType.ClearRecentlyCopiedStatus
67
+ ),
68
+ };
69
+
70
+ /**
71
+ * Reducer that executes actions
72
+ * @author Gabe Abrams
73
+ * @param state current state
74
+ * @param action action to execute
75
+ */
76
+ const reducer = (state: State, action: Action): State => {
77
+ switch (action.type) {
78
+ case ActionType.IndicateRecentlyCopied: {
79
+ return {
80
+ recentlyCopied: true,
81
+ };
82
+ }
83
+ case ActionType.ClearRecentlyCopiedStatus: {
84
+ return {
85
+ recentlyCopied: false,
86
+ };
87
+ }
88
+ default: {
89
+ return state;
90
+ }
91
+ }
92
+ };
93
+
94
+ /*------------------------------------------------------------------------*/
95
+ /* Component */
96
+ /*------------------------------------------------------------------------*/
97
+
98
+ const CopiableBox: React.FC<Props> = (props) => {
99
+ /*------------------------------------------------------------------------*/
100
+ /* Setup */
101
+ /*------------------------------------------------------------------------*/
102
+
103
+ /* -------------- Props ------------- */
104
+
105
+ // Destructure all props
106
+ const {
107
+ name,
108
+ text,
109
+ label,
110
+ labelIcon,
111
+ minLabelWidthRem,
112
+ multiline,
113
+ numVisibleLines = 10,
114
+ onClick,
115
+ } = props;
116
+
117
+ /* -------------- State ------------- */
118
+
119
+ // Initial state
120
+ const initialState: State = {
121
+ recentlyCopied: false,
122
+ };
123
+
124
+ // Initialize state
125
+ const [state, dispatch] = useReducer(reducer, initialState);
126
+
127
+ // Destructure common state
128
+ const {
129
+ recentlyCopied,
130
+ } = state;
131
+
132
+ /*------------------------------------------------------------------------*/
133
+ /* Component Functions */
134
+ /*------------------------------------------------------------------------*/
135
+
136
+ // Determine the id for the copiable text field
137
+ const copiableFieldClassName = `CopiableBox-text-box-${name}`;
138
+
139
+ /**
140
+ * Perform a copy
141
+ * @author Gabe Abrams
142
+ */
143
+ const performCopy = async () => {
144
+ // Write to clipboard
145
+ let copyFailed = false;
146
+ try {
147
+ await navigator.clipboard.writeText(text);
148
+ } catch (err) {
149
+ copyFailed = true;
150
+ }
151
+
152
+ // Try copy again if it failed
153
+ if (copyFailed) {
154
+ try {
155
+ const input = (
156
+ document.getElementsByClassName(copiableFieldClassName)[0]
157
+ ) as HTMLInputElement;
158
+ input.focus();
159
+ input.select();
160
+ document.execCommand('copy');
161
+ input.blur();
162
+ } catch (err) {
163
+ return alert(
164
+ 'Unable to copy',
165
+ 'Oops! We couldn\'t copy that to the clipboard. Please copy the text manually.',
166
+ );
167
+ }
168
+ }
169
+
170
+ // Show copied notice
171
+ dispatch({
172
+ type: ActionType.IndicateRecentlyCopied,
173
+ });
174
+
175
+ // Wait a moment
176
+ await waitMs(4000);
177
+
178
+ // Hide copied notice
179
+ dispatch({
180
+ type: ActionType.ClearRecentlyCopiedStatus,
181
+ });
182
+ };
183
+
184
+ /*------------------------------------------------------------------------*/
185
+ /* Render */
186
+ /*------------------------------------------------------------------------*/
187
+
188
+ /*----------------------------------------*/
189
+ /* Main UI */
190
+ /*----------------------------------------*/
191
+
192
+ return (
193
+ <div className="input-group mb-2">
194
+ {/* Label */}
195
+ <span
196
+ className="input-group-text"
197
+ style={{
198
+ minWidth: (
199
+ minLabelWidthRem
200
+ ? `${minLabelWidthRem}rem`
201
+ : undefined
202
+ ),
203
+ }}
204
+ >
205
+ {labelIcon && (
206
+ <FontAwesomeIcon
207
+ icon={labelIcon}
208
+ className="me-1"
209
+ />
210
+ )}
211
+ {label}
212
+ </span>
213
+
214
+ {/* Text */}
215
+ {
216
+ multiline
217
+ ? (
218
+ <textarea
219
+ className={`${copiableFieldClassName} CopiableBox-text-multiline form-control bg-white text-dark`}
220
+ value={text}
221
+ aria-label={`${label} text`}
222
+ rows={numVisibleLines}
223
+ onClick={onClick}
224
+ style={{
225
+ cursor: (
226
+ onClick
227
+ ? 'pointer'
228
+ : 'default'
229
+ ),
230
+ textDecoration: (
231
+ onClick
232
+ ? 'underline'
233
+ : undefined
234
+ ),
235
+ }}
236
+ readOnly
237
+ />
238
+ )
239
+ : (
240
+ <input
241
+ type="text"
242
+ className={`${copiableFieldClassName} CopiableBox-text-single-line form-control bg-white text-dark`}
243
+ value={text}
244
+ aria-label={`${label} text`}
245
+ onClick={onClick}
246
+ style={{
247
+ cursor: (
248
+ onClick
249
+ ? 'pointer'
250
+ : 'default'
251
+ ),
252
+ textDecoration: (
253
+ onClick
254
+ ? 'underline'
255
+ : undefined
256
+ ),
257
+ }}
258
+ readOnly
259
+ />
260
+ )
261
+ }
262
+
263
+ <button
264
+ className="btn btn-secondary"
265
+ type="button"
266
+ aria-label={`copy ${label} to the clipboard`}
267
+ disabled={recentlyCopied}
268
+ style={{
269
+ minWidth: '5.2rem',
270
+ }}
271
+ onClick={performCopy}
272
+ >
273
+ {
274
+ recentlyCopied
275
+ ? 'Copied!'
276
+ : (
277
+ <span>
278
+ <FontAwesomeIcon
279
+ icon={faClipboard}
280
+ className="me-1"
281
+ />
282
+ Copy
283
+ </span>
284
+ )
285
+ }
286
+ </button>
287
+ </div>
288
+ );
289
+ };
290
+
291
+ /*------------------------------------------------------------------------*/
292
+ /* Wrap Up */
293
+ /*------------------------------------------------------------------------*/
294
+
295
+ // Export component
296
+ export default CopiableBox;
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ import Drawer from './components/Drawer';
12
12
  import PopSuccessMark from './components/PopSuccessMark';
13
13
  import PopFailureMark from './components/PopFailureMark';
14
14
  import PopPendingMark from './components/PopPendingMark';
15
+ import CopiableBox from './components/CopiableBox';
15
16
 
16
17
  // Import errors
17
18
  import ErrorWithCode from './errors/ErrorWithCode';
@@ -67,6 +68,7 @@ export {
67
68
  PopSuccessMark,
68
69
  PopFailureMark,
69
70
  PopPendingMark,
71
+ CopiableBox,
70
72
  // Global functions
71
73
  alert,
72
74
  confirm,