dce-reactkit 3.5.4 → 3.5.6
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/cjs/index.js +515 -160
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/types/components/AppWrapper.d.ts +5 -0
- package/dist/cjs/types/components/ScrollLockToBottomContainer.d.ts +19 -0
- package/dist/cjs/types/components/ToggleSwitch.d.ts +20 -0
- package/dist/cjs/types/helpers/idify.d.ts +9 -0
- package/dist/cjs/types/index.d.ts +5 -2
- package/dist/esm/index.js +512 -161
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/types/components/AppWrapper.d.ts +5 -0
- package/dist/esm/types/components/ScrollLockToBottomContainer.d.ts +19 -0
- package/dist/esm/types/components/ToggleSwitch.d.ts +20 -0
- package/dist/esm/types/helpers/idify.d.ts +9 -0
- package/dist/esm/types/index.d.ts +5 -2
- package/dist/index.d.ts +90 -39
- package/package.json +1 -1
- package/src/components/AppWrapper.tsx +52 -34
- package/src/components/ScrollLockToBottomContainer.tsx +453 -0
- package/src/components/ToggleSwitch.tsx +102 -0
- package/src/helpers/idify.ts +24 -0
- package/src/index.ts +13 -1
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Container that automatically scrolls when new items are added,
|
|
3
|
+
* lets the user scroll up to see old items, but resumes
|
|
4
|
+
* autoscroll when the user scrolls back to the bottom
|
|
5
|
+
* @author Gabe Abrams
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Import React
|
|
9
|
+
import React, { useReducer, useEffect, useRef } from 'react';
|
|
10
|
+
|
|
11
|
+
// Import FontAwesome
|
|
12
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
13
|
+
import {
|
|
14
|
+
faChevronDown,
|
|
15
|
+
} from '@fortawesome/free-solid-svg-icons';
|
|
16
|
+
|
|
17
|
+
// Import shared types
|
|
18
|
+
import Variant from '../types/Variant';
|
|
19
|
+
|
|
20
|
+
// Import shared helper
|
|
21
|
+
import idify from '../helpers/idify';
|
|
22
|
+
|
|
23
|
+
/*------------------------------------------------------------------------*/
|
|
24
|
+
/* -------------------------------- Types ------------------------------- */
|
|
25
|
+
/*------------------------------------------------------------------------*/
|
|
26
|
+
|
|
27
|
+
// Props definition
|
|
28
|
+
type Props = {
|
|
29
|
+
// Name items in the container, "Messages" or "Comments" for example
|
|
30
|
+
// If excluded, a generic name will be used
|
|
31
|
+
itemsName?: string,
|
|
32
|
+
// Items in the scroll container
|
|
33
|
+
items: AutoScrollItem[],
|
|
34
|
+
// Custom variant for the "Jump to Bottom" button
|
|
35
|
+
jumpToBottomButtonVariant?: Variant,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// AutoScrollItem definition
|
|
39
|
+
type AutoScrollItem = {
|
|
40
|
+
// Unique item id
|
|
41
|
+
id: string | number,
|
|
42
|
+
// The item to render
|
|
43
|
+
item: React.ReactNode,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/*------------------------------------------------------------------------*/
|
|
47
|
+
/* -------------------------------- Style ------------------------------- */
|
|
48
|
+
/*------------------------------------------------------------------------*/
|
|
49
|
+
|
|
50
|
+
const style = `
|
|
51
|
+
.ScrollLockToBottom-outer-container {
|
|
52
|
+
/* Take up all space */
|
|
53
|
+
height: 100%;
|
|
54
|
+
position: relative;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.ScrollLockToBottom-scrollable-container {
|
|
58
|
+
/* Take up max 100% height, don't take it up if not needed */
|
|
59
|
+
/* (so column-reverse layout doesn't start at the bottom) */
|
|
60
|
+
max-height: 100%;
|
|
61
|
+
overflow-y: auto;
|
|
62
|
+
|
|
63
|
+
/* Allow children to position */
|
|
64
|
+
position: relative;
|
|
65
|
+
|
|
66
|
+
/* Reverse order of children so scroll starts at bottom */
|
|
67
|
+
/* (but children will have to be rendered in reverse order) */
|
|
68
|
+
display: flex;
|
|
69
|
+
flex-direction: column-reverse;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.ScrollLockToBottom-jump-to-bottom-container {
|
|
73
|
+
/* Don't take any space in parent */
|
|
74
|
+
height: 0;
|
|
75
|
+
overflow: visible;
|
|
76
|
+
|
|
77
|
+
/* On top of items */
|
|
78
|
+
z-index: 2;
|
|
79
|
+
|
|
80
|
+
/* Position in center bottom */
|
|
81
|
+
position: absolute;
|
|
82
|
+
bottom: 2rem;
|
|
83
|
+
|
|
84
|
+
/* Center horizontally */
|
|
85
|
+
width: 100%;
|
|
86
|
+
text-align: center;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.ScrollLockToBottom-item-container {
|
|
90
|
+
/* Normal Height */
|
|
91
|
+
position: relative;
|
|
92
|
+
z-index: 1;
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
/*------------------------------------------------------------------------*/
|
|
97
|
+
/* ------------------------------ Constants ----------------------------- */
|
|
98
|
+
/*------------------------------------------------------------------------*/
|
|
99
|
+
|
|
100
|
+
// Scrolled to bottom threshold
|
|
101
|
+
// (how close you have to be to the bottom for it to count as the bottom)
|
|
102
|
+
const SCROLLED_TO_BOTTOM_THRESHOLD_REMS = 2;
|
|
103
|
+
|
|
104
|
+
/*------------------------------------------------------------------------*/
|
|
105
|
+
/* -------------------------- Static Functions -------------------------- */
|
|
106
|
+
/*------------------------------------------------------------------------*/
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get ids of items
|
|
110
|
+
* @author Gabe Abrams
|
|
111
|
+
* @param items the items in the container
|
|
112
|
+
* @returns ids of items
|
|
113
|
+
*/
|
|
114
|
+
const getItemIds = (items: AutoScrollItem[]): (string | number)[] => {
|
|
115
|
+
return Array.from(items).map((child) => {
|
|
116
|
+
return child.id;
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/*------------------------------------------------------------------------*/
|
|
121
|
+
/* -------------------------------- State ------------------------------- */
|
|
122
|
+
/*------------------------------------------------------------------------*/
|
|
123
|
+
|
|
124
|
+
/* -------- State Definition -------- */
|
|
125
|
+
|
|
126
|
+
type State = {
|
|
127
|
+
// If true, the user was scrolled to the bottom after last update
|
|
128
|
+
wasScrolledToBottom: boolean,
|
|
129
|
+
// If true, the "jump to bottom" button is visible
|
|
130
|
+
jumpToBottomButtonVisible: boolean,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/* ------------- Actions ------------ */
|
|
134
|
+
|
|
135
|
+
// Types of actions
|
|
136
|
+
enum ActionType {
|
|
137
|
+
// Identify that the user is now scrolled to the bottom
|
|
138
|
+
NowScrolledToBottom = 'NowScrolledToBottom',
|
|
139
|
+
// Identify that the user scrolled away from the bottom
|
|
140
|
+
NowScrolledAwayFromBottom = 'NowScrolledAwayFromBottom',
|
|
141
|
+
// Identify that new content appeared at the bottom but is not visible
|
|
142
|
+
NewContentAtBottom = 'NewContentAtBottom',
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Action definitions
|
|
146
|
+
type Action = (
|
|
147
|
+
| {
|
|
148
|
+
// Action type
|
|
149
|
+
type: (
|
|
150
|
+
| ActionType.NowScrolledToBottom
|
|
151
|
+
| ActionType.NowScrolledAwayFromBottom
|
|
152
|
+
| ActionType.NewContentAtBottom
|
|
153
|
+
),
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Reducer that executes actions
|
|
159
|
+
* @author Gabe Abrams
|
|
160
|
+
* @param state current state
|
|
161
|
+
* @param action action to execute
|
|
162
|
+
*/
|
|
163
|
+
const reducer = (state: State, action: Action): State => {
|
|
164
|
+
switch (action.type) {
|
|
165
|
+
case ActionType.NowScrolledToBottom: {
|
|
166
|
+
return {
|
|
167
|
+
...state,
|
|
168
|
+
// Store the event
|
|
169
|
+
wasScrolledToBottom: true,
|
|
170
|
+
// Hide the "jump to bottom" button
|
|
171
|
+
jumpToBottomButtonVisible: false,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
case ActionType.NowScrolledAwayFromBottom: {
|
|
175
|
+
return {
|
|
176
|
+
...state,
|
|
177
|
+
// Store the event
|
|
178
|
+
wasScrolledToBottom: false,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
case ActionType.NewContentAtBottom: {
|
|
182
|
+
return {
|
|
183
|
+
...state,
|
|
184
|
+
// Store the event
|
|
185
|
+
wasScrolledToBottom: false,
|
|
186
|
+
// Show the "jump to bottom" button
|
|
187
|
+
jumpToBottomButtonVisible: true,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
default: {
|
|
191
|
+
return state;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/*------------------------------------------------------------------------*/
|
|
197
|
+
/* ------------------------------ Component ----------------------------- */
|
|
198
|
+
/*------------------------------------------------------------------------*/
|
|
199
|
+
|
|
200
|
+
const ScrollLockToBottom: React.FC<Props> = (props) => {
|
|
201
|
+
/*------------------------------------------------------------------------*/
|
|
202
|
+
/* -------------------------------- Setup ------------------------------- */
|
|
203
|
+
/*------------------------------------------------------------------------*/
|
|
204
|
+
|
|
205
|
+
/* -------------- Props ------------- */
|
|
206
|
+
|
|
207
|
+
// Destructure all props
|
|
208
|
+
const {
|
|
209
|
+
itemsName,
|
|
210
|
+
items,
|
|
211
|
+
jumpToBottomButtonVariant = Variant.Danger,
|
|
212
|
+
} = props;
|
|
213
|
+
|
|
214
|
+
/* -------------- State ------------- */
|
|
215
|
+
|
|
216
|
+
// Initial state
|
|
217
|
+
const initialState: State = {
|
|
218
|
+
wasScrolledToBottom: true,
|
|
219
|
+
jumpToBottomButtonVisible: false,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Initialize state
|
|
223
|
+
const [state, dispatch] = useReducer(reducer, initialState);
|
|
224
|
+
|
|
225
|
+
// Destructure common state
|
|
226
|
+
const {
|
|
227
|
+
wasScrolledToBottom,
|
|
228
|
+
jumpToBottomButtonVisible,
|
|
229
|
+
} = state;
|
|
230
|
+
|
|
231
|
+
/* -------------- Refs -------------- */
|
|
232
|
+
|
|
233
|
+
// Initialize refs
|
|
234
|
+
const lastItemIds = useRef<(string | number)[]>([]);
|
|
235
|
+
const container = useRef<HTMLDivElement | null>(null);
|
|
236
|
+
|
|
237
|
+
/*------------------------------------------------------------------------*/
|
|
238
|
+
/* ------------------------- Component Functions ------------------------ */
|
|
239
|
+
/*------------------------------------------------------------------------*/
|
|
240
|
+
|
|
241
|
+
/*----------------------------------------*/
|
|
242
|
+
/* --------------- Scroll --------------- */
|
|
243
|
+
/*----------------------------------------*/
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check if the user is scrolled to the bottom
|
|
247
|
+
* @author Gabe Abrams
|
|
248
|
+
* @returns true if scrolled to the bottom
|
|
249
|
+
*/
|
|
250
|
+
const isScrolledToBottom = (): boolean => {
|
|
251
|
+
// Skip if no container
|
|
252
|
+
if (!container.current) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Get info about container
|
|
257
|
+
const {
|
|
258
|
+
scrollTop,
|
|
259
|
+
} = container.current;
|
|
260
|
+
|
|
261
|
+
// Distance to bottom
|
|
262
|
+
const rootFontSizePx = Number.parseInt(
|
|
263
|
+
getComputedStyle(document.documentElement).fontSize,
|
|
264
|
+
10,
|
|
265
|
+
);
|
|
266
|
+
const distanceToBottomRems = Math.abs(scrollTop / rootFontSizePx);
|
|
267
|
+
|
|
268
|
+
// Figure out if we're scrolled to the bottom
|
|
269
|
+
return (distanceToBottomRems < SCROLLED_TO_BOTTOM_THRESHOLD_REMS);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Scroll the user to the bottom of the container
|
|
274
|
+
* @author Gabe Abrams
|
|
275
|
+
*/
|
|
276
|
+
const scrollToBottom = () => {
|
|
277
|
+
// Skip if no container
|
|
278
|
+
if (!container.current) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Update state
|
|
283
|
+
dispatch({
|
|
284
|
+
type: ActionType.NowScrolledToBottom,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Scroll to bottom
|
|
288
|
+
container.current.scrollTop = 0;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
/*----------------------------------------*/
|
|
292
|
+
/* -------------- Handlers -------------- */
|
|
293
|
+
/*----------------------------------------*/
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Handle scroll events on the container
|
|
297
|
+
* @author Gabe Abrams
|
|
298
|
+
*/
|
|
299
|
+
const handleScroll = () => {
|
|
300
|
+
// Skip if no container
|
|
301
|
+
if (!container.current) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Check if now scrolled to bottom
|
|
306
|
+
const nowScrolledToBottom = isScrolledToBottom();
|
|
307
|
+
|
|
308
|
+
// Remember if now no longer scrolled to bottom
|
|
309
|
+
if (nowScrolledToBottom) {
|
|
310
|
+
dispatch({
|
|
311
|
+
type: ActionType.NowScrolledToBottom,
|
|
312
|
+
});
|
|
313
|
+
} else {
|
|
314
|
+
dispatch({
|
|
315
|
+
type: ActionType.NowScrolledAwayFromBottom,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/*------------------------------------------------------------------------*/
|
|
321
|
+
/* ------------------------- Lifecycle Functions ------------------------ */
|
|
322
|
+
/*------------------------------------------------------------------------*/
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Mount
|
|
326
|
+
* @author Gabe Abrams
|
|
327
|
+
*/
|
|
328
|
+
useEffect(
|
|
329
|
+
() => {
|
|
330
|
+
// Scroll to the bottom
|
|
331
|
+
scrollToBottom();
|
|
332
|
+
},
|
|
333
|
+
[],
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Update (also called on mount)
|
|
338
|
+
* @author Gabe Abrams
|
|
339
|
+
*/
|
|
340
|
+
useEffect(
|
|
341
|
+
() => {
|
|
342
|
+
// Check if new content appeared
|
|
343
|
+
const currentItemIds = getItemIds(items);
|
|
344
|
+
|
|
345
|
+
// Check if new content appeared at bottom
|
|
346
|
+
const newContentAtBottom = (
|
|
347
|
+
currentItemIds.length > 0
|
|
348
|
+
&& (
|
|
349
|
+
currentItemIds[currentItemIds.length - 1]
|
|
350
|
+
!== lastItemIds.current[lastItemIds.current.length - 1]
|
|
351
|
+
)
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
// Do nothing if no new content
|
|
355
|
+
if (!newContentAtBottom) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Check if used to be scrolled to the bottom
|
|
360
|
+
if (wasScrolledToBottom) {
|
|
361
|
+
// Was scrolled to the bottom! Autoscroll.
|
|
362
|
+
scrollToBottom();
|
|
363
|
+
} else {
|
|
364
|
+
// Not scrolled to bottom. Show "jump to bottom" button.
|
|
365
|
+
dispatch({
|
|
366
|
+
type: ActionType.NewContentAtBottom,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Update last item ids
|
|
371
|
+
lastItemIds.current = currentItemIds;
|
|
372
|
+
},
|
|
373
|
+
[items],
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
/*------------------------------------------------------------------------*/
|
|
377
|
+
/* ------------------------------- Render ------------------------------- */
|
|
378
|
+
/*------------------------------------------------------------------------*/
|
|
379
|
+
|
|
380
|
+
/*----------------------------------------*/
|
|
381
|
+
/* --------------- Main UI -------------- */
|
|
382
|
+
/*----------------------------------------*/
|
|
383
|
+
|
|
384
|
+
// Jump to Bottom button
|
|
385
|
+
let jumpToBottomButton: React.ReactNode;
|
|
386
|
+
if (jumpToBottomButtonVisible) {
|
|
387
|
+
jumpToBottomButton = (
|
|
388
|
+
<div className={`ScrollLockToBottom-jump-to-bottom-container ScrollLockToBottom-for-${idify(itemsName ?? 'items')}`}>
|
|
389
|
+
<button
|
|
390
|
+
type="button"
|
|
391
|
+
className={`ScrollLockToBottom-jump-to-bottom-button ScrollLockToBottom-jump-to-bottom-button-for-${idify(itemsName ?? 'items')} btn btn-sm btn-${jumpToBottomButtonVariant} pt-0 pb-0`}
|
|
392
|
+
onClick={scrollToBottom}
|
|
393
|
+
aria-label="scroll back to bottom and show new content"
|
|
394
|
+
>
|
|
395
|
+
New
|
|
396
|
+
{' '}
|
|
397
|
+
{itemsName ?? 'Content'}
|
|
398
|
+
<FontAwesomeIcon
|
|
399
|
+
icon={faChevronDown}
|
|
400
|
+
className="ms-1"
|
|
401
|
+
/>
|
|
402
|
+
</button>
|
|
403
|
+
</div>
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Main UI
|
|
408
|
+
return (
|
|
409
|
+
<div className="ScrollLockToBottom-outer-container bg-warning">
|
|
410
|
+
{/* Style */}
|
|
411
|
+
<style>
|
|
412
|
+
{style}
|
|
413
|
+
</style>
|
|
414
|
+
|
|
415
|
+
{/* Jump to bottom button */}
|
|
416
|
+
{jumpToBottomButton}
|
|
417
|
+
|
|
418
|
+
{/* Scrollable Item Container */}
|
|
419
|
+
<div
|
|
420
|
+
className="ScrollLockToBottom-scrollable-container"
|
|
421
|
+
onScroll={() => {
|
|
422
|
+
handleScroll();
|
|
423
|
+
}}
|
|
424
|
+
ref={container}
|
|
425
|
+
>
|
|
426
|
+
{/* Items */}
|
|
427
|
+
{
|
|
428
|
+
items
|
|
429
|
+
// Render each item with a key
|
|
430
|
+
.map((item) => {
|
|
431
|
+
return (
|
|
432
|
+
<div
|
|
433
|
+
className="ScrollLockToBottom-item-container"
|
|
434
|
+
key={item.id}
|
|
435
|
+
>
|
|
436
|
+
{item.item}
|
|
437
|
+
</div>
|
|
438
|
+
);
|
|
439
|
+
})
|
|
440
|
+
// Reverse order because flex column is reverse
|
|
441
|
+
.reverse()
|
|
442
|
+
}
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
);
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
/*------------------------------------------------------------------------*/
|
|
449
|
+
/* ------------------------------- Wrap Up ------------------------------ */
|
|
450
|
+
/*------------------------------------------------------------------------*/
|
|
451
|
+
|
|
452
|
+
// Export component
|
|
453
|
+
export default ScrollLockToBottom;
|
|
@@ -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,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a string to hyphenated lowercase format with no space or
|
|
3
|
+
* non-alphanumeric characters
|
|
4
|
+
* @author Gabe Abrams
|
|
5
|
+
* @param str the string to convert
|
|
6
|
+
* @returns the idified string
|
|
7
|
+
*/
|
|
8
|
+
const idify = (str: string): string => {
|
|
9
|
+
return (
|
|
10
|
+
str
|
|
11
|
+
// Trim whitespace
|
|
12
|
+
.trim()
|
|
13
|
+
// Convert to lowercase
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
// Replace non-alphanumeric characters with hyphens
|
|
16
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
17
|
+
// Change multiple hyphens in a row for a single hyphen
|
|
18
|
+
.replace(/-+/g, '-')
|
|
19
|
+
// Remove hyphens at the beginning and end of the string
|
|
20
|
+
.replace(/^-+|-+$/g, '')
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default idify;
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// Import components
|
|
2
|
-
import 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';
|
|
@@ -19,6 +24,8 @@ import IntelliTable from './components/IntelliTable';
|
|
|
19
24
|
import CSVDownloadButton from './components/CSVDownloadButton';
|
|
20
25
|
import DBEntryManagerPanel from './components/DBEntryManagerPanel';
|
|
21
26
|
import Tooltip from './components/Tooltip';
|
|
27
|
+
import ToggleSwitch from './components/ToggleSwitch';
|
|
28
|
+
import ScrollLockToBottom from './components/ScrollLockToBottomContainer';
|
|
22
29
|
|
|
23
30
|
// Import errors
|
|
24
31
|
import ErrorWithCode from './errors/ErrorWithCode';
|
|
@@ -71,6 +78,7 @@ import genCommaList from './helpers/genCommaList';
|
|
|
71
78
|
import validateEmail from './helpers/validators/validateEmail';
|
|
72
79
|
import validatePhoneNumber from './helpers/validators/validatePhoneNumber';
|
|
73
80
|
import validateString from './helpers/validators/validateString';
|
|
81
|
+
import idify from './helpers/idify';
|
|
74
82
|
|
|
75
83
|
// Import types
|
|
76
84
|
import ModalButtonType from './types/ModalButtonType';
|
|
@@ -117,6 +125,8 @@ export {
|
|
|
117
125
|
CSVDownloadButton,
|
|
118
126
|
DBEntryManagerPanel,
|
|
119
127
|
Tooltip,
|
|
128
|
+
ToggleSwitch,
|
|
129
|
+
ScrollLockToBottom,
|
|
120
130
|
// Global functions
|
|
121
131
|
alert,
|
|
122
132
|
confirm,
|
|
@@ -160,10 +170,12 @@ export {
|
|
|
160
170
|
validatePhoneNumber,
|
|
161
171
|
validateString,
|
|
162
172
|
getLocalTimeInfo,
|
|
173
|
+
idify,
|
|
163
174
|
// Client helpers
|
|
164
175
|
initClient,
|
|
165
176
|
visitServerEndpoint,
|
|
166
177
|
logClientEvent,
|
|
178
|
+
addFatalErrorHandler,
|
|
167
179
|
// Server helpers
|
|
168
180
|
initServer,
|
|
169
181
|
genRouteHandler,
|