rnnovaui 0.8.3 → 0.8.4

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": "rnnovaui",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "react-native": "src/index.js",
@@ -1,4 +1,12 @@
1
- import React, { memo, useCallback, useMemo, useRef, useState } from "react";
1
+ import React, {
2
+ forwardRef,
3
+ memo,
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from "react";
2
10
 
3
11
  import {
4
12
  Animated,
@@ -11,9 +19,9 @@ import {
11
19
 
12
20
  import { useUITheme } from "../../theme";
13
21
 
14
- /* -------------------------------------------------------------------------- */
15
- /* CONSTANTS */
16
- /* -------------------------------------------------------------------------- */
22
+ /* ==========================================================================
23
+ CONSTANTS
24
+ ========================================================================== */
17
25
 
18
26
  export const UIFloatingButtonSizes = {
19
27
  sm: "sm",
@@ -53,9 +61,50 @@ export const UIFloatingButtonDirections = {
53
61
  right: "right",
54
62
  };
55
63
 
56
- /* -------------------------------------------------------------------------- */
57
- /* HELPERS */
58
- /* -------------------------------------------------------------------------- */
64
+ /* ==========================================================================
65
+ FALLBACK COLORS
66
+ ========================================================================== */
67
+
68
+ const FALLBACK_COLORS = {
69
+ transparent: "transparent",
70
+
71
+ primary: "#FF5A1F",
72
+ primaryPressed: "#E94D17",
73
+
74
+ secondary: "#7CFF32",
75
+
76
+ success: "#16A34A",
77
+ danger: "#DC2626",
78
+
79
+ background: "#FFFFFF",
80
+ surface: "#F8F8F8",
81
+ card: "#FFFFFF",
82
+
83
+ text: "#111111",
84
+ textSecondary: "#525252",
85
+
86
+ border: "#E5E5E5",
87
+
88
+ onPrimary: "#FFFFFF",
89
+ onSecondary: "#111111",
90
+
91
+ backdrop: "rgba(0,0,0,0.45)",
92
+ };
93
+
94
+ /* ==========================================================================
95
+ SAFE HELPERS
96
+ ========================================================================== */
97
+
98
+ function getColors(context) {
99
+ if (context && context.theme && context.theme.colors) {
100
+ return {
101
+ ...FALLBACK_COLORS,
102
+ ...context.theme.colors,
103
+ };
104
+ }
105
+
106
+ return FALLBACK_COLORS;
107
+ }
59
108
 
60
109
  function getSafeActions(actions) {
61
110
  if (!Array.isArray(actions)) {
@@ -63,35 +112,26 @@ function getSafeActions(actions) {
63
112
  }
64
113
 
65
114
  return actions.filter(
66
- (item) => item !== null && item !== undefined && typeof item === "object",
115
+ (action) =>
116
+ action !== null && action !== undefined && typeof action === "object",
67
117
  );
68
118
  }
69
119
 
70
- function getSafeColors(context) {
71
- if (context && context.theme && context.theme.colors) {
72
- return context.theme.colors;
73
- }
120
+ /* ==========================================================================
121
+ VARIANT
122
+ ========================================================================== */
74
123
 
75
- return {
76
- primary: "#FF5A1F",
77
- secondary: "#7CFF32",
78
- success: "#16A34A",
79
- danger: "#DC2626",
80
- card: "#FFFFFF",
81
- text: "#111111",
82
- border: "#E5E5E5",
83
- onPrimary: "#FFFFFF",
84
- onSecondary: "#111111",
85
- };
86
- }
87
-
88
- function getVariantStyle(variant, colors) {
124
+ function getVariantColors(variant, colors) {
89
125
  switch (variant) {
90
126
  case "secondary":
91
127
  return {
92
128
  backgroundColor: colors.secondary || "#7CFF32",
93
129
 
94
130
  color: colors.onSecondary || "#111111",
131
+
132
+ borderColor: colors.secondary || "#7CFF32",
133
+
134
+ borderWidth: 0,
95
135
  };
96
136
 
97
137
  case "success":
@@ -99,6 +139,10 @@ function getVariantStyle(variant, colors) {
99
139
  backgroundColor: colors.success || "#16A34A",
100
140
 
101
141
  color: colors.onPrimary || "#FFFFFF",
142
+
143
+ borderColor: colors.success || "#16A34A",
144
+
145
+ borderWidth: 0,
102
146
  };
103
147
 
104
148
  case "danger":
@@ -106,6 +150,10 @@ function getVariantStyle(variant, colors) {
106
150
  backgroundColor: colors.danger || "#DC2626",
107
151
 
108
152
  color: colors.onPrimary || "#FFFFFF",
153
+
154
+ borderColor: colors.danger || "#DC2626",
155
+
156
+ borderWidth: 0,
109
157
  };
110
158
 
111
159
  case "neutral":
@@ -125,10 +173,18 @@ function getVariantStyle(variant, colors) {
125
173
  backgroundColor: colors.primary || "#FF5A1F",
126
174
 
127
175
  color: colors.onPrimary || "#FFFFFF",
176
+
177
+ borderColor: colors.primary || "#FF5A1F",
178
+
179
+ borderWidth: 0,
128
180
  };
129
181
  }
130
182
  }
131
183
 
184
+ /* ==========================================================================
185
+ SIZE
186
+ ========================================================================== */
187
+
132
188
  function getMainSize(size) {
133
189
  switch (size) {
134
190
  case "sm":
@@ -158,30 +214,25 @@ function getMainSize(size) {
158
214
  }
159
215
  }
160
216
 
161
- function getActionSize(size) {
162
- switch (size) {
163
- case "sm":
164
- return {
165
- width: 42,
166
- height: 42,
167
- };
217
+ function getActionSize(size, action) {
218
+ const defaultSize = size === "sm" ? 42 : size === "lg" ? 58 : 50;
168
219
 
169
- case "lg":
170
- return {
171
- width: 58,
172
- height: 58,
173
- };
220
+ const width = action.width ?? defaultSize;
174
221
 
175
- case "md":
176
- default:
177
- return {
178
- width: 50,
179
- height: 50,
180
- };
181
- }
222
+ const height = action.height ?? defaultSize;
223
+
224
+ return {
225
+ width,
226
+ height,
227
+ radius: action.borderRadius ?? Math.min(width, height) / 2,
228
+ };
182
229
  }
183
230
 
184
- function getPosition(position, offset) {
231
+ /* ==========================================================================
232
+ POSITION
233
+ ========================================================================== */
234
+
235
+ function getPositionStyle(position, offset) {
185
236
  switch (position) {
186
237
  case "bottomLeft":
187
238
  return {
@@ -210,45 +261,46 @@ function getPosition(position, offset) {
210
261
  }
211
262
  }
212
263
 
213
- /* -------------------------------------------------------------------------- */
214
- /* STRAIGHT POSITION */
215
- /* -------------------------------------------------------------------------- */
264
+ /* ==========================================================================
265
+ STRAIGHT POSITION
266
+ ========================================================================== */
216
267
 
217
268
  function getStraightPosition(direction, index, itemSpacing) {
218
269
  const distance = (index + 1) * (50 + itemSpacing);
219
270
 
220
- if (direction === "down") {
221
- return {
222
- x: 0,
223
- y: distance,
224
- };
225
- }
271
+ switch (direction) {
272
+ case "down":
273
+ return {
274
+ x: 0,
275
+ y: distance,
276
+ };
226
277
 
227
- if (direction === "left") {
228
- return {
229
- x: -distance,
230
- y: 0,
231
- };
232
- }
278
+ case "left":
279
+ return {
280
+ x: -distance,
281
+ y: 0,
282
+ };
233
283
 
234
- if (direction === "right") {
235
- return {
236
- x: distance,
237
- y: 0,
238
- };
239
- }
284
+ case "right":
285
+ return {
286
+ x: distance,
287
+ y: 0,
288
+ };
240
289
 
241
- return {
242
- x: 0,
243
- y: -distance,
244
- };
290
+ case "up":
291
+ default:
292
+ return {
293
+ x: 0,
294
+ y: -distance,
295
+ };
296
+ }
245
297
  }
246
298
 
247
- /* -------------------------------------------------------------------------- */
248
- /* CIRCULAR POSITION */
249
- /* -------------------------------------------------------------------------- */
299
+ /* ==========================================================================
300
+ CIRCULAR POSITION
301
+ ========================================================================== */
250
302
 
251
- function getCircularPosition(direction, index, count, radius) {
303
+ function getCircularPosition(direction, index, count, radius, spread) {
252
304
  if (count <= 0) {
253
305
  return {
254
306
  x: 0,
@@ -284,22 +336,31 @@ function getCircularPosition(direction, index, count, radius) {
284
336
  };
285
337
  }
286
338
 
287
- const spread = 90;
339
+ const safeSpread = Math.max(10, Math.min(180, spread || 90));
288
340
 
289
- const step = spread / (count - 1);
341
+ const step = safeSpread / (count - 1);
290
342
 
291
- const start = -spread / 2;
343
+ const start = -safeSpread / 2;
292
344
 
293
345
  let angle = start + step * index;
294
346
 
295
- if (direction === "down") {
296
- angle += 90;
297
- } else if (direction === "left") {
298
- angle += 180;
299
- } else if (direction === "right") {
300
- angle += 0;
301
- } else {
302
- angle += 270;
347
+ switch (direction) {
348
+ case "down":
349
+ angle += 90;
350
+ break;
351
+
352
+ case "left":
353
+ angle += 180;
354
+ break;
355
+
356
+ case "right":
357
+ angle += 0;
358
+ break;
359
+
360
+ case "up":
361
+ default:
362
+ angle += 270;
363
+ break;
303
364
  }
304
365
 
305
366
  const radians = (angle * Math.PI) / 180;
@@ -311,17 +372,21 @@ function getCircularPosition(direction, index, count, radius) {
311
372
  };
312
373
  }
313
374
 
314
- /* -------------------------------------------------------------------------- */
315
- /* LOADING */
316
- /* -------------------------------------------------------------------------- */
375
+ /* ==========================================================================
376
+ LOADING
377
+ ========================================================================== */
317
378
 
318
- function LoadingIndicator({ color }) {
379
+ function LoadingIndicator({ color, size = 20 }) {
319
380
  return (
320
381
  <View
321
382
  style={[
322
383
  styles.loading,
323
384
 
324
385
  {
386
+ width: size,
387
+ height: size,
388
+ borderRadius: size / 2,
389
+
325
390
  borderTopColor: color,
326
391
 
327
392
  borderRightColor: color,
@@ -335,12 +400,13 @@ function LoadingIndicator({ color }) {
335
400
  );
336
401
  }
337
402
 
338
- /* -------------------------------------------------------------------------- */
339
- /* ACTION ITEM */
340
- /* -------------------------------------------------------------------------- */
403
+ /* ==========================================================================
404
+ ACTION ITEM
405
+ ========================================================================== */
341
406
 
342
407
  const FloatingActionItem = memo(function FloatingActionItem({
343
408
  action,
409
+
344
410
  index,
345
411
  count,
346
412
 
@@ -348,34 +414,27 @@ const FloatingActionItem = memo(function FloatingActionItem({
348
414
  direction,
349
415
 
350
416
  radius,
417
+ circularSpread,
418
+
351
419
  itemSpacing,
352
420
 
353
421
  size,
354
422
 
355
423
  colors,
356
424
 
357
- onPress,
358
-
359
425
  duration,
426
+ staggerDelay,
360
427
 
361
- stagger,
428
+ onPress,
362
429
 
363
430
  actionTextStyle,
364
431
  actionIconStyle,
365
432
  }) {
366
433
  const animation = useRef(new Animated.Value(0)).current;
367
434
 
368
- const [pressed, setPressed] = useState(false);
435
+ const pressed = useRef(new Animated.Value(1)).current;
369
436
 
370
- const position = useMemo(() => {
371
- if (menuType === UIFloatingButtonMenuTypes.circular) {
372
- return getCircularPosition(direction, index, count, radius);
373
- }
374
-
375
- return getStraightPosition(direction, index, itemSpacing);
376
- }, [menuType, direction, index, count, radius, itemSpacing]);
377
-
378
- React.useEffect(() => {
437
+ useEffect(() => {
379
438
  const timer = setTimeout(() => {
380
439
  Animated.timing(animation, {
381
440
  toValue: 1,
@@ -386,14 +445,30 @@ const FloatingActionItem = memo(function FloatingActionItem({
386
445
 
387
446
  useNativeDriver: true,
388
447
  }).start();
389
- }, index * stagger);
448
+ }, index * staggerDelay);
390
449
 
391
450
  return () => {
392
451
  clearTimeout(timer);
452
+
453
+ animation.stopAnimation();
393
454
  };
394
- }, [animation, duration, index, stagger]);
455
+ }, [animation, duration, index, staggerDelay]);
395
456
 
396
- const actionSize = getActionSize(size);
457
+ const position = useMemo(() => {
458
+ if (menuType === UIFloatingButtonMenuTypes.circular) {
459
+ return getCircularPosition(
460
+ direction,
461
+ index,
462
+ count,
463
+ radius,
464
+ circularSpread,
465
+ );
466
+ }
467
+
468
+ return getStraightPosition(direction, index, itemSpacing);
469
+ }, [menuType, direction, index, count, radius, circularSpread, itemSpacing]);
470
+
471
+ const actionSize = getActionSize(size, action);
397
472
 
398
473
  const backgroundColor = action.backgroundColor || colors.card || "#FFFFFF";
399
474
 
@@ -409,9 +484,9 @@ const FloatingActionItem = memo(function FloatingActionItem({
409
484
  outputRange: [0, position.y],
410
485
  });
411
486
 
412
- const scale = animation.interpolate({
487
+ const actionScale = animation.interpolate({
413
488
  inputRange: [0, 1],
414
- outputRange: [0.7, 1],
489
+ outputRange: [0.65, 1],
415
490
  });
416
491
 
417
492
  const opacity = animation.interpolate({
@@ -419,6 +494,32 @@ const FloatingActionItem = memo(function FloatingActionItem({
419
494
  outputRange: [0, 1],
420
495
  });
421
496
 
497
+ const pressScale = pressed.current;
498
+
499
+ const handlePressIn = () => {
500
+ Animated.spring(pressScale, {
501
+ toValue: 0.9,
502
+
503
+ friction: 7,
504
+
505
+ tension: 140,
506
+
507
+ useNativeDriver: true,
508
+ }).start();
509
+ };
510
+
511
+ const handlePressOut = () => {
512
+ Animated.spring(pressScale, {
513
+ toValue: 1,
514
+
515
+ friction: 7,
516
+
517
+ tension: 140,
518
+
519
+ useNativeDriver: true,
520
+ }).start();
521
+ };
522
+
422
523
  const handlePress = () => {
423
524
  if (action.disabled || action.loading) {
424
525
  return;
@@ -431,6 +532,48 @@ const FloatingActionItem = memo(function FloatingActionItem({
431
532
  onPress();
432
533
  };
433
534
 
535
+ const actionStyle = {
536
+ width: actionSize.width,
537
+
538
+ height: actionSize.height,
539
+
540
+ borderRadius: actionSize.radius,
541
+
542
+ backgroundColor,
543
+
544
+ borderColor: action.borderColor || "transparent",
545
+
546
+ borderWidth: action.borderWidth || 0,
547
+
548
+ padding: action.padding,
549
+
550
+ paddingTop: action.paddingTop,
551
+
552
+ paddingRight: action.paddingRight,
553
+
554
+ paddingBottom: action.paddingBottom,
555
+
556
+ paddingLeft: action.paddingLeft,
557
+
558
+ paddingHorizontal: action.paddingHorizontal,
559
+
560
+ paddingVertical: action.paddingVertical,
561
+
562
+ margin: action.margin,
563
+
564
+ marginTop: action.marginTop,
565
+
566
+ marginRight: action.marginRight,
567
+
568
+ marginBottom: action.marginBottom,
569
+
570
+ marginLeft: action.marginLeft,
571
+
572
+ elevation: action.elevation ?? 4,
573
+
574
+ opacity: action.disabled ? 0.5 : 1,
575
+ };
576
+
434
577
  return (
435
578
  <Animated.View
436
579
  style={[
@@ -443,11 +586,13 @@ const FloatingActionItem = memo(function FloatingActionItem({
443
586
  {
444
587
  translateX,
445
588
  },
589
+
446
590
  {
447
591
  translateY,
448
592
  },
593
+
449
594
  {
450
- scale,
595
+ scale: actionScale,
451
596
  },
452
597
  ],
453
598
  },
@@ -466,16 +611,20 @@ const FloatingActionItem = memo(function FloatingActionItem({
466
611
  styles.actionLabel,
467
612
 
468
613
  {
469
- backgroundColor: colors.card || "#FFFFFF",
614
+ backgroundColor:
615
+ action.labelBackgroundColor || colors.card || "#FFFFFF",
470
616
  },
617
+
618
+ action.labelStyle,
471
619
  ]}
472
620
  >
473
621
  <Text
622
+ numberOfLines={1}
474
623
  style={[
475
624
  styles.actionLabelText,
476
625
 
477
626
  {
478
- color: colors.text || "#111111",
627
+ color: action.labelColor || colors.text || "#111111",
479
628
  },
480
629
 
481
630
  actionTextStyle,
@@ -486,61 +635,55 @@ const FloatingActionItem = memo(function FloatingActionItem({
486
635
  </View>
487
636
  ) : null}
488
637
 
489
- <Pressable
490
- disabled={Boolean(action.disabled) || Boolean(action.loading)}
491
- onPress={handlePress}
492
- onPressIn={() => setPressed(true)}
493
- onPressOut={() => setPressed(false)}
494
- style={[
495
- styles.actionButton,
496
-
497
- {
498
- width: actionSize.width,
499
-
500
- height: actionSize.height,
501
-
502
- borderRadius: actionSize.width / 2,
503
-
504
- backgroundColor,
505
-
506
- opacity: action.disabled ? 0.5 : 1,
507
-
508
- transform: [
509
- {
510
- scale: pressed ? 0.92 : 1,
511
- },
512
- ],
513
- },
514
-
515
- action.style,
516
- ]}
638
+ <Animated.View
639
+ style={{
640
+ transform: [
641
+ {
642
+ scale: pressScale,
643
+ },
644
+ ],
645
+ }}
517
646
  >
518
- {action.loading ? (
519
- <LoadingIndicator color={iconColor} />
520
- ) : (
521
- <View
522
- style={[styles.actionIcon, actionIconStyle, action.iconStyle]}
523
- >
524
- {action.icon || null}
525
- </View>
526
- )}
527
-
528
- {action.badge !== undefined && action.badge !== null ? (
529
- <View style={styles.actionBadge}>
530
- <Text style={styles.actionBadgeText}>{String(action.badge)}</Text>
531
- </View>
532
- ) : null}
533
- </Pressable>
647
+ <Pressable
648
+ disabled={Boolean(action.disabled) || Boolean(action.loading)}
649
+ onPress={handlePress}
650
+ onPressIn={handlePressIn}
651
+ onPressOut={handlePressOut}
652
+ accessibilityRole="button"
653
+ accessibilityLabel={
654
+ action.accessibilityLabel || action.label || "Floating action"
655
+ }
656
+ style={[styles.actionButton, actionStyle, action.style]}
657
+ >
658
+ {action.loading ? (
659
+ <LoadingIndicator color={iconColor} />
660
+ ) : (
661
+ <View
662
+ style={[styles.actionIcon, actionIconStyle, action.iconStyle]}
663
+ >
664
+ {action.icon || null}
665
+ </View>
666
+ )}
667
+
668
+ {action.badge !== undefined && action.badge !== null ? (
669
+ <View style={[styles.actionBadge, action.badgeStyle]}>
670
+ <Text style={[styles.actionBadgeText, action.badgeTextStyle]}>
671
+ {String(action.badge)}
672
+ </Text>
673
+ </View>
674
+ ) : null}
675
+ </Pressable>
676
+ </Animated.View>
534
677
  </View>
535
678
  </Animated.View>
536
679
  );
537
680
  });
538
681
 
539
- /* -------------------------------------------------------------------------- */
540
- /* MAIN COMPONENT */
541
- /* -------------------------------------------------------------------------- */
682
+ /* ==========================================================================
683
+ MAIN COMPONENT
684
+ ========================================================================== */
542
685
 
543
- const UIFloatingButtonComponent = function UIFloatingButton(
686
+ const UIFloatingButtonComponent = forwardRef(function UIFloatingButton(
544
687
  {
545
688
  title,
546
689
  label,
@@ -568,6 +711,8 @@ const UIFloatingButtonComponent = function UIFloatingButton(
568
711
  direction = "up",
569
712
 
570
713
  radius = 100,
714
+ circularSpread = 90,
715
+
571
716
  itemSpacing = 12,
572
717
 
573
718
  animationDuration = 220,
@@ -580,10 +725,18 @@ const UIFloatingButtonComponent = function UIFloatingButton(
580
725
 
581
726
  iconOnly = false,
582
727
 
728
+ containerStyle,
583
729
  textStyle,
584
730
  iconStyle,
585
- containerStyle,
586
731
 
732
+ /*
733
+ * WHOLE ACTION GROUP
734
+ */
735
+ actionGroupStyle,
736
+
737
+ /*
738
+ * Shared action styles
739
+ */
587
740
  actionTextStyle,
588
741
  actionIconStyle,
589
742
 
@@ -591,10 +744,11 @@ const UIFloatingButtonComponent = function UIFloatingButton(
591
744
  overlayColor,
592
745
 
593
746
  badge,
594
-
595
- testID,
747
+ badgeStyle,
748
+ badgeTextStyle,
596
749
 
597
750
  accessibilityLabel,
751
+ testID,
598
752
 
599
753
  ...rest
600
754
  },
@@ -602,7 +756,7 @@ const UIFloatingButtonComponent = function UIFloatingButton(
602
756
  ) {
603
757
  const context = useUITheme();
604
758
 
605
- const colors = getSafeColors(context);
759
+ const colors = getColors(context);
606
760
 
607
761
  const safeActions = useMemo(() => getSafeActions(actions), [actions]);
608
762
 
@@ -610,20 +764,22 @@ const UIFloatingButtonComponent = function UIFloatingButton(
610
764
 
611
765
  const [open, setOpen] = useState(false);
612
766
 
613
- const scale = useRef(new Animated.Value(1)).current;
767
+ const mainScale = useRef(new Animated.Value(1)).current;
614
768
 
615
- const dimensions = getMainSize(size);
769
+ const actionGroupAnimation = useRef(new Animated.Value(0)).current;
616
770
 
617
- const variantStyle = getVariantStyle(variant, colors);
771
+ const mainSize = getMainSize(size);
618
772
 
619
- const animateButton = useCallback(
620
- (value) => {
621
- if (disabled || loading) {
622
- return;
623
- }
773
+ const variantColors = getVariantColors(variant, colors);
774
+
775
+ /* -------------------------------------------------------------------- */
776
+ /* MAIN PRESS ANIMATION */
777
+ /* -------------------------------------------------------------------- */
624
778
 
625
- Animated.spring(scale, {
626
- toValue: value,
779
+ const animateMain = useCallback(
780
+ (toValue) => {
781
+ Animated.spring(mainScale, {
782
+ toValue,
627
783
 
628
784
  friction: 7,
629
785
 
@@ -632,9 +788,13 @@ const UIFloatingButtonComponent = function UIFloatingButton(
632
788
  useNativeDriver: true,
633
789
  }).start();
634
790
  },
635
- [disabled, loading, scale],
791
+ [mainScale],
636
792
  );
637
793
 
794
+ /* -------------------------------------------------------------------- */
795
+ /* OPEN */
796
+ /* -------------------------------------------------------------------- */
797
+
638
798
  const openMenu = useCallback(() => {
639
799
  if (disabled || loading) {
640
800
  return;
@@ -650,14 +810,50 @@ const UIFloatingButtonComponent = function UIFloatingButton(
650
810
 
651
811
  setOpen(true);
652
812
 
653
- animateButton(0.94);
654
- }, [disabled, loading, actionCount, onPress, animateButton]);
813
+ Animated.timing(actionGroupAnimation, {
814
+ toValue: 1,
815
+
816
+ duration: animationDuration,
817
+
818
+ easing: Easing.out(Easing.cubic),
819
+
820
+ useNativeDriver: true,
821
+ }).start();
822
+
823
+ animateMain(0.94);
824
+ }, [
825
+ disabled,
826
+ loading,
827
+ actionCount,
828
+ onPress,
829
+ actionGroupAnimation,
830
+ animationDuration,
831
+ animateMain,
832
+ ]);
833
+
834
+ /* -------------------------------------------------------------------- */
835
+ /* CLOSE */
836
+ /* -------------------------------------------------------------------- */
655
837
 
656
838
  const closeMenu = useCallback(() => {
657
- setOpen(false);
839
+ Animated.timing(actionGroupAnimation, {
840
+ toValue: 0,
658
841
 
659
- animateButton(1);
660
- }, [animateButton]);
842
+ duration: animationDuration,
843
+
844
+ easing: Easing.in(Easing.cubic),
845
+
846
+ useNativeDriver: true,
847
+ }).start(() => {
848
+ setOpen(false);
849
+ });
850
+
851
+ animateMain(1);
852
+ }, [actionGroupAnimation, animationDuration, animateMain]);
853
+
854
+ /* -------------------------------------------------------------------- */
855
+ /* PRESS */
856
+ /* -------------------------------------------------------------------- */
661
857
 
662
858
  const handlePress = useCallback(() => {
663
859
  if (disabled || loading) {
@@ -688,26 +884,32 @@ const UIFloatingButtonComponent = function UIFloatingButton(
688
884
  onPress,
689
885
  ]);
690
886
 
887
+ /* -------------------------------------------------------------------- */
888
+ /* ACTION PRESS */
889
+ /* -------------------------------------------------------------------- */
890
+
691
891
  const handleActionPress = useCallback(() => {
692
892
  if (closeOnAction) {
693
893
  closeMenu();
694
894
  }
695
895
  }, [closeOnAction, closeMenu]);
696
896
 
897
+ /* -------------------------------------------------------------------- */
898
+ /* MAIN ICON */
899
+ /* -------------------------------------------------------------------- */
900
+
697
901
  let mainIcon = icon || null;
698
902
 
699
903
  if (loading) {
700
- mainIcon = <LoadingIndicator color={variantStyle.color} />;
701
- }
702
-
703
- if (expandable && actionCount > 0 && open && !loading) {
904
+ mainIcon = <LoadingIndicator color={variantColors.color} />;
905
+ } else if (expandable && actionCount > 0 && open) {
704
906
  mainIcon = (
705
907
  <Text
706
908
  style={[
707
909
  styles.closeIcon,
708
910
 
709
911
  {
710
- color: variantStyle.color,
912
+ color: variantColors.color,
711
913
  },
712
914
  ]}
713
915
  >
@@ -716,10 +918,32 @@ const UIFloatingButtonComponent = function UIFloatingButton(
716
918
  );
717
919
  }
718
920
 
921
+ /* -------------------------------------------------------------------- */
922
+ /* TITLE */
923
+ /* -------------------------------------------------------------------- */
924
+
719
925
  const resolvedTitle = title ?? label;
720
926
 
721
927
  const showTitle = !iconOnly && Boolean(resolvedTitle);
722
928
 
929
+ /* -------------------------------------------------------------------- */
930
+ /* ACTION GROUP */
931
+ /* -------------------------------------------------------------------- */
932
+
933
+ const groupOpacity = actionGroupAnimation.interpolate({
934
+ inputRange: [0, 1],
935
+ outputRange: [0, 1],
936
+ });
937
+
938
+ const groupScale = actionGroupAnimation.interpolate({
939
+ inputRange: [0, 1],
940
+ outputRange: [0.9, 1],
941
+ });
942
+
943
+ /* -------------------------------------------------------------------- */
944
+ /* RENDER */
945
+ /* -------------------------------------------------------------------- */
946
+
723
947
  return (
724
948
  <>
725
949
  {overlay && open && actionCount > 0 ? (
@@ -729,7 +953,7 @@ const UIFloatingButtonComponent = function UIFloatingButton(
729
953
 
730
954
  {
731
955
  backgroundColor:
732
- overlayColor || colors.backdrop || "rgba(0,0,0,0.35)",
956
+ overlayColor || colors.backdrop || FALLBACK_COLORS.backdrop,
733
957
  },
734
958
  ]}
735
959
  onPress={closeMenu}
@@ -738,40 +962,61 @@ const UIFloatingButtonComponent = function UIFloatingButton(
738
962
 
739
963
  <View
740
964
  pointerEvents="box-none"
741
- style={[styles.positionWrapper, getPosition(position, offset)]}
965
+ style={[styles.positionWrapper, getPositionStyle(position, offset)]}
742
966
  >
743
967
  {expandable && open && actionCount > 0 ? (
744
- <View pointerEvents="box-none" style={styles.actionContainer}>
968
+ <Animated.View
969
+ pointerEvents="box-none"
970
+ style={[
971
+ styles.actionGroup,
972
+
973
+ {
974
+ opacity: groupOpacity,
975
+
976
+ transform: [
977
+ {
978
+ scale: groupScale,
979
+ },
980
+ ],
981
+ },
982
+
983
+ /*
984
+ * USER CUSTOM GROUP STYLE
985
+ */
986
+ actionGroupStyle,
987
+ ]}
988
+ >
745
989
  {safeActions.map((action, index) => (
746
990
  <FloatingActionItem
747
- key={action.id || `action-${index}`}
991
+ key={action.id || `floating-action-${index}`}
748
992
  action={action}
749
993
  index={index}
750
994
  count={actionCount}
751
995
  menuType={menuType}
752
996
  direction={direction}
753
997
  radius={radius}
998
+ circularSpread={circularSpread}
754
999
  itemSpacing={itemSpacing}
755
1000
  size={size}
756
1001
  colors={colors}
757
1002
  duration={animationDuration}
758
- stagger={staggerDelay}
1003
+ staggerDelay={staggerDelay}
759
1004
  onPress={handleActionPress}
760
1005
  actionTextStyle={actionTextStyle}
761
1006
  actionIconStyle={actionIconStyle}
762
1007
  />
763
1008
  ))}
764
- </View>
1009
+ </Animated.View>
765
1010
  ) : null}
766
1011
 
767
1012
  <Animated.View
768
1013
  style={[
769
- styles.animatedMain,
1014
+ styles.mainAnimated,
770
1015
 
771
1016
  {
772
1017
  transform: [
773
1018
  {
774
- scale,
1019
+ scale: mainScale,
775
1020
  },
776
1021
  ],
777
1022
  },
@@ -784,31 +1029,46 @@ const UIFloatingButtonComponent = function UIFloatingButton(
784
1029
  disabled={disabled || loading}
785
1030
  onPress={handlePress}
786
1031
  onLongPress={onLongPress}
787
- onPressIn={() => animateButton(0.92)}
788
- onPressOut={() => animateButton(open ? 0.94 : 1)}
1032
+ onPressIn={() => {
1033
+ if (!disabled && !loading) {
1034
+ animateMain(0.92);
1035
+ }
1036
+ }}
1037
+ onPressOut={() => {
1038
+ if (!disabled && !loading) {
1039
+ animateMain(open ? 0.94 : 1);
1040
+ }
1041
+ }}
789
1042
  accessibilityRole="button"
790
1043
  accessibilityLabel={
791
1044
  accessibilityLabel || resolvedTitle || "Floating button"
792
1045
  }
1046
+ accessibilityState={{
1047
+ disabled: disabled || loading,
1048
+
1049
+ busy: loading,
1050
+
1051
+ expanded: expandable && actionCount > 0 ? open : undefined,
1052
+ }}
793
1053
  style={[
794
1054
  styles.mainButton,
795
1055
 
796
1056
  {
797
- width: showTitle ? undefined : dimensions.width,
1057
+ width: showTitle ? undefined : mainSize.width,
798
1058
 
799
- minWidth: showTitle ? dimensions.width : dimensions.width,
1059
+ minWidth: mainSize.width,
800
1060
 
801
- height: dimensions.height,
1061
+ height: mainSize.height,
802
1062
 
803
1063
  paddingHorizontal: showTitle ? 16 : 0,
804
1064
 
805
- borderRadius: shape === "circle" ? dimensions.radius : 14,
1065
+ borderRadius: shape === "circle" ? mainSize.radius : 14,
806
1066
 
807
- backgroundColor: variantStyle.backgroundColor,
1067
+ backgroundColor: variantColors.backgroundColor,
808
1068
 
809
- borderColor: variantStyle.borderColor || "transparent",
1069
+ borderColor: variantColors.borderColor,
810
1070
 
811
- borderWidth: variantStyle.borderWidth || 0,
1071
+ borderWidth: variantColors.borderWidth,
812
1072
 
813
1073
  elevation,
814
1074
 
@@ -843,9 +1103,9 @@ const UIFloatingButtonComponent = function UIFloatingButton(
843
1103
  styles.mainText,
844
1104
 
845
1105
  {
846
- color: variantStyle.color,
1106
+ color: variantColors.color,
847
1107
 
848
- fontSize: dimensions.fontSize,
1108
+ fontSize: mainSize.fontSize,
849
1109
  },
850
1110
 
851
1111
  textStyle,
@@ -856,8 +1116,10 @@ const UIFloatingButtonComponent = function UIFloatingButton(
856
1116
  ) : null}
857
1117
 
858
1118
  {badge !== undefined && badge !== null ? (
859
- <View style={styles.badge}>
860
- <Text style={styles.badgeText}>{String(badge)}</Text>
1119
+ <View style={[styles.badge, badgeStyle]}>
1120
+ <Text style={[styles.badgeText, badgeTextStyle]}>
1121
+ {String(badge)}
1122
+ </Text>
861
1123
  </View>
862
1124
  ) : null}
863
1125
  </Pressable>
@@ -865,21 +1127,21 @@ const UIFloatingButtonComponent = function UIFloatingButton(
865
1127
  </View>
866
1128
  </>
867
1129
  );
868
- };
1130
+ });
869
1131
 
870
- /* -------------------------------------------------------------------------- */
871
- /* EXPORT */
872
- /* -------------------------------------------------------------------------- */
1132
+ /* ==========================================================================
1133
+ EXPORT
1134
+ ========================================================================== */
873
1135
 
874
- export const UIFloatingButton = memo(
875
- React.forwardRef(UIFloatingButtonComponent),
876
- );
1136
+ export const UIFloatingButton = memo(UIFloatingButtonComponent);
877
1137
 
878
1138
  UIFloatingButton.displayName = "UIFloatingButton";
879
1139
 
880
- /* -------------------------------------------------------------------------- */
881
- /* STYLES */
882
- /* -------------------------------------------------------------------------- */
1140
+ export default UIFloatingButton;
1141
+
1142
+ /* ==========================================================================
1143
+ STYLES
1144
+ ========================================================================== */
883
1145
 
884
1146
  const styles = StyleSheet.create({
885
1147
  overlay: {
@@ -898,7 +1160,7 @@ const styles = StyleSheet.create({
898
1160
  justifyContent: "center",
899
1161
  },
900
1162
 
901
- animatedMain: {
1163
+ mainAnimated: {
902
1164
  zIndex: 1002,
903
1165
  },
904
1166
 
@@ -912,19 +1174,6 @@ const styles = StyleSheet.create({
912
1174
  overflow: "visible",
913
1175
  },
914
1176
 
915
- shadow: {
916
- shadowColor: "#000000",
917
-
918
- shadowOffset: {
919
- width: 0,
920
- height: 4,
921
- },
922
-
923
- shadowOpacity: 0.22,
924
-
925
- shadowRadius: 7,
926
- },
927
-
928
1177
  mainIcon: {
929
1178
  alignItems: "center",
930
1179
 
@@ -948,24 +1197,29 @@ const styles = StyleSheet.create({
948
1197
  },
949
1198
 
950
1199
  loading: {
951
- width: 20,
1200
+ borderWidth: 2,
1201
+ },
952
1202
 
953
- height: 20,
1203
+ shadow: {
1204
+ shadowColor: "#000000",
954
1205
 
955
- borderWidth: 2,
1206
+ shadowOffset: {
1207
+ width: 0,
1208
+ height: 4,
1209
+ },
956
1210
 
957
- borderRadius: 10,
1211
+ shadowOpacity: 0.22,
1212
+
1213
+ shadowRadius: 7,
958
1214
  },
959
1215
 
960
1216
  badge: {
961
1217
  position: "absolute",
962
1218
 
963
1219
  top: -4,
964
-
965
1220
  right: -4,
966
1221
 
967
1222
  minWidth: 19,
968
-
969
1223
  height: 19,
970
1224
 
971
1225
  paddingHorizontal: 4,
@@ -973,13 +1227,11 @@ const styles = StyleSheet.create({
973
1227
  borderRadius: 10,
974
1228
 
975
1229
  alignItems: "center",
976
-
977
1230
  justifyContent: "center",
978
1231
 
979
1232
  backgroundColor: "#DC2626",
980
1233
 
981
1234
  borderWidth: 2,
982
-
983
1235
  borderColor: "#FFFFFF",
984
1236
  },
985
1237
 
@@ -993,15 +1245,19 @@ const styles = StyleSheet.create({
993
1245
  includeFontPadding: false,
994
1246
  },
995
1247
 
996
- actionContainer: {
1248
+ /*
1249
+ * WHOLE ACTION GROUP
1250
+ *
1251
+ * This is the wrapper around all floating
1252
+ * action items.
1253
+ */
1254
+ actionGroup: {
997
1255
  position: "absolute",
998
1256
 
999
1257
  width: 1,
1000
-
1001
1258
  height: 1,
1002
1259
 
1003
1260
  alignItems: "center",
1004
-
1005
1261
  justifyContent: "center",
1006
1262
 
1007
1263
  zIndex: 1001,
@@ -1011,7 +1267,6 @@ const styles = StyleSheet.create({
1011
1267
  position: "absolute",
1012
1268
 
1013
1269
  alignItems: "center",
1014
-
1015
1270
  justifyContent: "center",
1016
1271
  },
1017
1272
 
@@ -1029,12 +1284,22 @@ const styles = StyleSheet.create({
1029
1284
  marginRight: 8,
1030
1285
 
1031
1286
  paddingHorizontal: 10,
1032
-
1033
1287
  paddingVertical: 6,
1034
1288
 
1035
1289
  borderRadius: 8,
1036
1290
 
1037
1291
  elevation: 3,
1292
+
1293
+ shadowColor: "#000000",
1294
+
1295
+ shadowOffset: {
1296
+ width: 0,
1297
+ height: 2,
1298
+ },
1299
+
1300
+ shadowOpacity: 0.15,
1301
+
1302
+ shadowRadius: 5,
1038
1303
  },
1039
1304
 
1040
1305
  actionLabelText: {
@@ -1050,7 +1315,7 @@ const styles = StyleSheet.create({
1050
1315
 
1051
1316
  justifyContent: "center",
1052
1317
 
1053
- elevation: 4,
1318
+ overflow: "visible",
1054
1319
  },
1055
1320
 
1056
1321
  actionIcon: {
@@ -1063,11 +1328,9 @@ const styles = StyleSheet.create({
1063
1328
  position: "absolute",
1064
1329
 
1065
1330
  top: -5,
1066
-
1067
1331
  right: -5,
1068
1332
 
1069
1333
  minWidth: 19,
1070
-
1071
1334
  height: 19,
1072
1335
 
1073
1336
  paddingHorizontal: 4,
@@ -1075,13 +1338,11 @@ const styles = StyleSheet.create({
1075
1338
  borderRadius: 10,
1076
1339
 
1077
1340
  alignItems: "center",
1078
-
1079
1341
  justifyContent: "center",
1080
1342
 
1081
1343
  backgroundColor: "#DC2626",
1082
1344
 
1083
1345
  borderWidth: 2,
1084
-
1085
1346
  borderColor: "#FFFFFF",
1086
1347
  },
1087
1348
 
@@ -1091,7 +1352,7 @@ const styles = StyleSheet.create({
1091
1352
  fontSize: 9,
1092
1353
 
1093
1354
  fontWeight: "700",
1355
+
1356
+ includeFontPadding: false,
1094
1357
  },
1095
1358
  });
1096
-
1097
- export default UIFloatingButton;