@onehat/ui 0.4.60 → 0.4.61

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onehat/ui",
3
- "version": "0.4.60",
3
+ "version": "0.4.61",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -0,0 +1,98 @@
1
+ import { useCallback } from 'react';
2
+ import {
3
+ Fab, FabIcon, FabLabel,
4
+ ScrollView,
5
+ VStack,
6
+ } from '@project-components/Gluestack';
7
+ import Animated, {
8
+ useSharedValue,
9
+ useAnimatedStyle,
10
+ withTiming,
11
+ interpolate,
12
+ } from 'react-native-reanimated';
13
+ import EllipsisVertical from '../Icons/EllipsisVertical.js';
14
+ import Xmark from '../Icons/Xmark.js';
15
+
16
+ // This component creates a floating action button (FAB)
17
+ // that can expand and collapse to show multiple FABs beneath it.
18
+
19
+ export default function DynamicFab(props) {
20
+ const {
21
+ fabs,
22
+ icon = null,
23
+ label = null,
24
+ collapseOnPress = true,
25
+ } = props,
26
+ isExpanded = useSharedValue(0),
27
+ toggleFab = useCallback(() => {
28
+ isExpanded.value = isExpanded.value ? 0 : 1;
29
+ }, []),
30
+ fabSpacing = 50,
31
+ verticalOffset = 15; // to shift the entire expanded group up
32
+
33
+ let className = `
34
+ DynamicFab
35
+ fixed
36
+ pb-[20px]
37
+ bottom-4
38
+ right-4
39
+ `;
40
+ if (props.className) {
41
+ className += ` ${props.className}`;
42
+ }
43
+
44
+ return <VStack className={className}>
45
+ {fabs
46
+ .slice() // clone, so we don't mutate the original array
47
+ .reverse() // so fabs appear in the correct order
48
+ .map((fab, index) => {
49
+ const {
50
+ icon,
51
+ label,
52
+ onPress,
53
+ } = fab,
54
+ animatedStyle = useAnimatedStyle(() => {
55
+ const translateY = interpolate(
56
+ isExpanded.value,
57
+ [0, 1],
58
+ [0, -(fabSpacing * (index + 1)) - verticalOffset]
59
+ );
60
+ return {
61
+ transform: [{ translateY }],
62
+ opacity: withTiming(isExpanded.value, { duration: 200 }),
63
+ };
64
+ });
65
+
66
+ return <Animated.View
67
+ key={index}
68
+ style={animatedStyle}
69
+ className="absolute bottom-0 right-0"
70
+ >
71
+ <Fab
72
+ size="md"
73
+ className="bg-primary-600"
74
+ onPress={() => {
75
+ onPress();
76
+ if (collapseOnPress) {
77
+ isExpanded.value = 0;
78
+ }
79
+ }}
80
+ style={{
81
+ shadowColor: 'transparent', // otherwise, on collapse a bunch of shadows build up for a moment!
82
+ }}
83
+ >
84
+ <FabIcon as={icon} />
85
+ {label && <FabLabel>{label}</FabLabel>}
86
+ </Fab>
87
+ </Animated.View>;
88
+ })}
89
+ <Fab
90
+ size="lg"
91
+ onPress={toggleFab}
92
+ className="z-100 bg-primary-600"
93
+ >
94
+ <FabIcon as={isExpanded.value ? Xmark : icon || EllipsisVertical} />
95
+ {label && <FabLabel>{label}</FabLabel>}
96
+ </Fab>
97
+ </VStack>;
98
+ };
@@ -40,6 +40,7 @@ import testProps from '../../Functions/testProps.js';
40
40
  import Toolbar from '../Toolbar/Toolbar.js';
41
41
  import Button from '../Buttons/Button.js';
42
42
  import IconButton from '../Buttons/IconButton.js';
43
+ import DynamicFab from '../Buttons/DynamicFab.js';
43
44
  import AngleLeft from '../Icons/AngleLeft.js';
44
45
  import Eye from '../Icons/Eye.js';
45
46
  import Rotate from '../Icons/Rotate.js';
@@ -47,9 +48,9 @@ import Pencil from '../Icons/Pencil.js';
47
48
  import Plus from '../Icons/Plus.js';
48
49
  import FloppyDiskRegular from '../Icons/FloppyDiskRegular.js';
49
50
  import Trash from '../Icons/Trash.js';
51
+ import ArrowUp from '../Icons/ArrowUp.js';
50
52
  import Xmark from '../Icons/Xmark.js';
51
53
  import Check from '../Icons/Check.js';
52
-
53
54
  import Footer from '../Layout/Footer.js';
54
55
  import Label from '../Form/Label.js';
55
56
  import _ from 'lodash';
@@ -138,6 +139,8 @@ function Form(props) {
138
139
 
139
140
  } = props,
140
141
  formRef = useRef(),
142
+ ancillaryItemsRef = useRef({}),
143
+ ancillaryFabs = useRef([]),
141
144
  styles = UiGlobals.styles,
142
145
  record = props.record?.length === 1 ? props.record[0] : props.record;
143
146
 
@@ -899,12 +902,21 @@ function Form(props) {
899
902
  },
900
903
  buildAncillary = () => {
901
904
  const components = [];
905
+ ancillaryFabs.current = [];
902
906
  if (ancillaryItems.length) {
907
+
908
+ // add the "scroll to top" button
909
+ ancillaryFabs.current.push({
910
+ icon: ArrowUp,
911
+ onPress: () => scrollToAncillaryItem(0),
912
+ });
913
+
903
914
  _.each(ancillaryItems, (item, ix) => {
904
915
  let {
905
916
  type,
906
917
  title = null,
907
918
  description = null,
919
+ icon,
908
920
  selectorId,
909
921
  selectorSelectedField,
910
922
  ...itemPropsToPass
@@ -912,6 +924,14 @@ function Form(props) {
912
924
  if (isMultiple && type !== 'Attachments') {
913
925
  return;
914
926
  }
927
+ if (icon) {
928
+ // TODO: this assumes that if one Ancillary item has an icon, they all do.
929
+ // If they don't, the ix will be wrong.
930
+ ancillaryFabs.current.push({
931
+ icon,
932
+ onPress: () => scrollToAncillaryItem(ix +1), // offset for the "scroll to top" button
933
+ });
934
+ }
915
935
  if (type.match(/Grid/) && !itemPropsToPass.h) {
916
936
  itemPropsToPass.h = 400;
917
937
  }
@@ -938,6 +958,9 @@ function Form(props) {
938
958
  ${styles.FORM_ANCILLARY_TITLE_CLASSNAME}
939
959
  `}
940
960
  >{title}</Text>;
961
+ if (icon) {
962
+ title = <HStack className="items-center"><Icon as={icon} size="lg" className="mr-2" />{title}</HStack>
963
+ }
941
964
  }
942
965
  if (description) {
943
966
  description = <Text
@@ -949,6 +972,7 @@ function Form(props) {
949
972
  >{description}</Text>;
950
973
  }
951
974
  components.push(<VStack
975
+ ref={(el) => (ancillaryItemsRef.current[ix +1 /* offset for "scroll to top" */] = el)}
952
976
  key={'ancillary-' + ix}
953
977
  className={`
954
978
  Form-VStack12
@@ -964,6 +988,12 @@ function Form(props) {
964
988
  }
965
989
  return components;
966
990
  },
991
+ scrollToAncillaryItem = (ix) => {
992
+ ancillaryItemsRef.current[ix]?.scrollIntoView({
993
+ behavior: 'smooth',
994
+ block: 'start',
995
+ });
996
+ },
967
997
  onSubmitError = (errors, e) => {
968
998
  if (editorType === EDITOR_TYPE__INLINE) {
969
999
  alert(errors.message);
@@ -1088,6 +1118,7 @@ function Form(props) {
1088
1118
  formComponents,
1089
1119
  editor,
1090
1120
  additionalButtons,
1121
+ fab = null,
1091
1122
  isSaveDisabled = false,
1092
1123
  isSubmitDisabled = false,
1093
1124
  showDeleteBtn = false,
@@ -1101,13 +1132,13 @@ function Form(props) {
1101
1132
  // create editor
1102
1133
  if (editorType === EDITOR_TYPE__INLINE) {
1103
1134
  editor = buildFromColumnsConfig();
1104
- // } else if (editorType === EDITOR_TYPE__PLAIN) {
1105
- // formComponents = buildFromItems();
1106
- // const formAncillaryComponents = buildAncillary();
1107
- // editor = <>
1108
- // <VStack className="p-4">{formComponents}</VStack>
1109
- // <VStack className="pt-4">{formAncillaryComponents}</VStack>
1110
- // </>;
1135
+ // } else if (editorType === EDITOR_TYPE__PLAIN) {
1136
+ // formComponents = buildFromItems();
1137
+ // const formAncillaryComponents = buildAncillary();
1138
+ // editor = <>
1139
+ // <VStack className="p-4">{formComponents}</VStack>
1140
+ // <VStack className="pt-4">{formAncillaryComponents}</VStack>
1141
+ // </>;
1111
1142
  } else {
1112
1143
  formComponents = buildFromItems();
1113
1144
  const formAncillaryComponents = buildAncillary();
@@ -1161,10 +1192,15 @@ function Form(props) {
1161
1192
  {additionalButtons}
1162
1193
  </Toolbar>)
1163
1194
  }
1195
+ if (!_.isEmpty(ancillaryFabs.current)) {
1196
+ fab = <DynamicFab
1197
+ fabs={ancillaryFabs.current}
1198
+ collapseOnPress={false}
1199
+ className="bottom-[55px]"
1200
+ />;
1201
+ }
1164
1202
  }
1165
1203
 
1166
-
1167
-
1168
1204
  // create footer
1169
1205
  if (!formState.isValid) {
1170
1206
  isSaveDisabled = true;
@@ -1356,6 +1392,7 @@ function Form(props) {
1356
1392
 
1357
1393
  let className = props.className || '';
1358
1394
  className += ' Form-VStackNative';
1395
+ const scrollToTopAnchor = <Box ref={(el) => (ancillaryItemsRef.current[0] = el)} className="h-0" />;
1359
1396
  return <VStackNative
1360
1397
  ref={formRef}
1361
1398
  {...testProps(self)}
@@ -1379,6 +1416,7 @@ function Form(props) {
1379
1416
  // height: '100%',
1380
1417
  }}
1381
1418
  >
1419
+ {scrollToTopAnchor}
1382
1420
  {modeHeader}
1383
1421
  {formHeader}
1384
1422
  {formButtons}
@@ -1386,6 +1424,7 @@ function Form(props) {
1386
1424
  </ScrollView>}
1387
1425
 
1388
1426
  {footer}
1427
+ {fab}
1389
1428
 
1390
1429
  </>}
1391
1430
  </VStackNative>;
@@ -0,0 +1,11 @@
1
+ import { createIcon } from "../Gluestack/icon";
2
+ // Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.
3
+ import { Path, Svg } from 'react-native-svg';
4
+
5
+ const SvgComponent = createIcon({
6
+ Root: Svg,
7
+ viewBox: '0 0 384 512',
8
+ path: <Path d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.3l105.4 105.3c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z" />,
9
+ });
10
+
11
+ export default SvgComponent
@@ -1,6 +1,8 @@
1
1
  import { useEffect, useRef, useState, isValidElement, } from 'react';
2
2
  import {
3
+ Box,
3
4
  HStack,
5
+ Icon,
4
6
  ScrollView,
5
7
  Text,
6
8
  VStack,
@@ -24,7 +26,9 @@ import inArray from '../../Functions/inArray.js';
24
26
  import getComponentFromType from '../../Functions/getComponentFromType.js';
25
27
  import buildAdditionalButtons from '../../Functions/buildAdditionalButtons.js';
26
28
  import testProps from '../../Functions/testProps.js';
29
+ import DynamicFab from '../Buttons/DynamicFab.js';
27
30
  import Toolbar from '../Toolbar/Toolbar.js';
31
+ import ArrowUp from '../Icons/ArrowUp.js';
28
32
  import Button from '../Buttons/Button.js';
29
33
  import Label from '../Form/Label.js';
30
34
  import Pencil from '../Icons/Pencil.js';
@@ -66,6 +70,8 @@ function Viewer(props) {
66
70
 
67
71
  } = props,
68
72
  scrollViewRef = useRef(),
73
+ ancillaryItemsRef = useRef({}),
74
+ ancillaryFabs = useRef([]),
69
75
  isMultiple = _.isArray(record),
70
76
  [containerWidth, setContainerWidth] = useState(),
71
77
  isSideEditor = editorType === EDITOR_TYPE__SIDE,
@@ -295,17 +301,34 @@ function Viewer(props) {
295
301
  },
296
302
  buildAncillary = () => {
297
303
  const components = [];
304
+ ancillaryFabs.current = [];
298
305
  if (ancillaryItems.length) {
306
+
307
+ // add the "scroll to top" button
308
+ ancillaryFabs.current.push({
309
+ icon: ArrowUp,
310
+ onPress: () => scrollToAncillaryItem(0),
311
+ });
312
+
299
313
  _.each(ancillaryItems, (item, ix) => {
300
314
  let {
301
315
  type,
302
316
  title = null,
317
+ icon,
303
318
  selectorId = null,
304
319
  ...itemPropsToPass
305
320
  } = item;
306
321
  if (isMultiple && type !== 'Attachments') {
307
322
  return;
308
323
  }
324
+ if (icon) {
325
+ // NOTE: this assumes that if one Ancillary item has an icon, they all do.
326
+ // If they don't, the ix will be wrong.
327
+ ancillaryFabs.current.push({
328
+ icon,
329
+ onPress: () => scrollToAncillaryItem(ix +1), // offset for the "scroll to top" button
330
+ });
331
+ }
309
332
  if (type.match(/Grid/) && !itemPropsToPass.h) {
310
333
  itemPropsToPass.h = 400;
311
334
  }
@@ -334,12 +357,28 @@ function Viewer(props) {
334
357
  title += ' for ' + record.displayValue;
335
358
  }
336
359
  title = <Text className={`${styles.VIEWER_ANCILLARY_FONTSIZE} font-bold`}>{title}</Text>;
360
+ if (icon) {
361
+ title = <HStack className="items-center"><Icon as={icon} size="lg" className="mr-2" />{title}</HStack>
362
+ }
337
363
  }
338
- components.push(<VStack key={'ancillary-' + ix} className="my-3">{title}{element}</VStack>);
364
+ components.push(<VStack
365
+ ref={(el) => (ancillaryItemsRef.current[ix +1 /* offset for "scroll to top" */] = el)}
366
+ key={'ancillary-' + ix}
367
+ className="my-3"
368
+ >
369
+ {title}
370
+ {element}
371
+ </VStack>);
339
372
  });
340
373
  }
341
374
  return components;
342
375
  },
376
+ scrollToAncillaryItem = (ix) => {
377
+ ancillaryItemsRef.current[ix]?.scrollIntoView({
378
+ behavior: 'smooth',
379
+ block: 'start',
380
+ });
381
+ },
343
382
  onLayout = (e) => {
344
383
  setContainerWidth(e.nativeEvent.layout.width);
345
384
  };
@@ -356,15 +395,25 @@ function Viewer(props) {
356
395
 
357
396
  const
358
397
  showDeleteBtn = onDelete && viewerCanDelete,
359
- showCloseBtn = !isSideEditor;
398
+ showCloseBtn = !isSideEditor,
399
+ showFooter = (showDeleteBtn || showCloseBtn);
360
400
  let additionalButtons = null,
361
401
  viewerComponents = null,
362
- ancillaryComponents = null;
402
+ ancillaryComponents = null,
403
+ fab = null;
363
404
 
364
405
  if (containerWidth) { // we need to render this component twice in order to get the container width. Skip this on first render
365
406
  additionalButtons = buildAdditionalButtons(additionalViewButtons);
366
407
  viewerComponents = buildFromItems();
367
408
  ancillaryComponents = buildAncillary();
409
+
410
+ if (!_.isEmpty(ancillaryFabs.current)) {
411
+ fab = <DynamicFab
412
+ fabs={ancillaryFabs.current}
413
+ collapseOnPress={false}
414
+ verticalOffset={showFooter ? 15 : 0}
415
+ />;
416
+ }
368
417
  }
369
418
 
370
419
  let canEdit = true;
@@ -385,6 +434,33 @@ function Viewer(props) {
385
434
  className += ' ' + props.className;
386
435
  }
387
436
 
437
+ const footer = showFooter ?
438
+ <Footer className="justify-end">
439
+ {showDeleteBtn &&
440
+ <HStack className="flex-1 justify-start">
441
+ <Button
442
+ {...testProps('deleteBtn')}
443
+ key="deleteBtn"
444
+ onPress={onDelete}
445
+ className={`
446
+ text-white
447
+ bg-warning-500
448
+ hover:bg-warning-600
449
+ `}
450
+ text="Delete"
451
+ />
452
+ </HStack>}
453
+ {onClose && showCloseBtn &&
454
+ <Button
455
+ {...testProps('closeBtn')}
456
+ key="closeBtn"
457
+ onPress={onClose}
458
+ className="text-white"
459
+ text="Close"
460
+ />}
461
+ </Footer> : null;
462
+
463
+ const scrollToTopAnchor = <Box ref={(el) => (ancillaryItemsRef.current[0] = el)} className="h-0" />;
388
464
  return <VStackNative
389
465
  {...testProps(self)}
390
466
  style={style}
@@ -403,6 +479,7 @@ function Viewer(props) {
403
479
  flex-1
404
480
  `}
405
481
  >
482
+ {scrollToTopAnchor}
406
483
  {canEdit && onEditMode &&
407
484
  <Toolbar className="justify-end">
408
485
  <HStack className="flex-1 items-center">
@@ -434,31 +511,8 @@ function Viewer(props) {
434
511
  <VStack className="Viewer-AncillaryComponents m-2 pt-4 px-2">{ancillaryComponents}</VStack>
435
512
  </ScrollView>
436
513
 
437
- {(showDeleteBtn || showCloseBtn) &&
438
- <Footer className="justify-end">
439
- {showDeleteBtn &&
440
- <HStack className="flex-1 justify-start">
441
- <Button
442
- {...testProps('deleteBtn')}
443
- key="deleteBtn"
444
- onPress={onDelete}
445
- className={`
446
- text-white
447
- bg-warning-500
448
- hover:bg-warning-600
449
- `}
450
- text="Delete"
451
- />
452
- </HStack>}
453
- {onClose && showCloseBtn &&
454
- <Button
455
- {...testProps('closeBtn')}
456
- key="closeBtn"
457
- onPress={onClose}
458
- className="text-white"
459
- text="Close"
460
- />}
461
- </Footer>}
514
+ {footer}
515
+ {fab}
462
516
 
463
517
  </>}
464
518
  </VStackNative>;
@@ -9,6 +9,7 @@ import AngleRight from './Icons/AngleRight.js';
9
9
  import AnglesLeft from './Icons/AnglesLeft.js';
10
10
  import AnglesRight from './Icons/AnglesRight.js';
11
11
  import Asterisk from './Icons/Asterisk.js';
12
+ import ArrowUp from './Icons/ArrowUp.js';
12
13
  import Ban from './Icons/Ban.js';
13
14
  import Bars from './Icons/Bars.js';
14
15
  import BarsStaggered from './Icons/BarsStaggered.js';
@@ -253,6 +254,7 @@ const components = {
253
254
  AnglesLeft,
254
255
  AnglesRight,
255
256
  Asterisk,
257
+ ArrowUp,
256
258
  Ban,
257
259
  Bars,
258
260
  BarsStaggered,