@hero-design/rn 8.59.0 → 8.60.0

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/lib/index.js CHANGED
@@ -14416,7 +14416,8 @@ var Error$1 = function Error(_ref2) {
14416
14416
  }, nativeProps));
14417
14417
  };
14418
14418
 
14419
- var StyledFAB$1 = index$9(reactNative.TouchableHighlight)(function (_ref) {
14419
+ var AnimatedTouchableHighlight = reactNative.Animated.createAnimatedComponent(reactNative.TouchableHighlight);
14420
+ var StyledFAB$1 = index$9(AnimatedTouchableHighlight)(function (_ref) {
14420
14421
  var theme = _ref.theme,
14421
14422
  themeActive = _ref.themeActive;
14422
14423
  return {
@@ -14431,7 +14432,8 @@ var StyledFAB$1 = index$9(reactNative.TouchableHighlight)(function (_ref) {
14431
14432
  shadowColor: theme.__hd__.fab.shadows.color,
14432
14433
  shadowOffset: theme.__hd__.fab.shadows.offset,
14433
14434
  shadowRadius: theme.__hd__.fab.shadows.radius,
14434
- shadowOpacity: theme.__hd__.fab.shadows.opacity
14435
+ shadowOpacity: theme.__hd__.fab.shadows.opacity,
14436
+ height: theme.__hd__.fab.sizes.height
14435
14437
  };
14436
14438
  });
14437
14439
  var StyledFABIcon = index$9(Icon)(function (_ref2) {
@@ -14523,16 +14525,14 @@ var IconWithTextContent = function IconWithTextContent(_ref2) {
14523
14525
  testID: "styled-fab-icon"
14524
14526
  })), /*#__PURE__*/React__default["default"].createElement(StyledFABText, null, title));
14525
14527
  };
14526
- var defaultAnimation = {
14527
- create: {
14528
- type: 'easeInEaseOut',
14529
- property: 'opacity'
14530
- },
14531
- update: {
14532
- type: 'spring',
14533
- springDamping: reactNative.Platform.OS === 'ios' ? 0.7 : 1.2
14534
- },
14535
- duration: reactNative.Platform.OS === 'ios' ? 300 : 400
14528
+ var animateWidth = function animateWidth() {
14529
+ reactNative.LayoutAnimation.configureNext({
14530
+ duration: reactNative.Platform.OS === 'ios' ? 200 : 400,
14531
+ update: {
14532
+ type: 'spring',
14533
+ springDamping: reactNative.Platform.OS === 'ios' ? 1 : 1.5
14534
+ }
14535
+ });
14536
14536
  };
14537
14537
  var FAB$1 = /*#__PURE__*/React.forwardRef(function (_ref3, ref) {
14538
14538
  var _StyleSheet$flatten, _StyleSheet$flatten2;
@@ -14544,61 +14544,99 @@ var FAB$1 = /*#__PURE__*/React.forwardRef(function (_ref3, ref) {
14544
14544
  active = _ref3.active,
14545
14545
  style = _ref3.style;
14546
14546
  var theme = useTheme();
14547
- var _React$useState = React__default["default"].useState(false),
14548
- _React$useState2 = _slicedToArray(_React$useState, 2),
14549
- canAnimate = _React$useState2[0],
14550
- setCanAnimate = _React$useState2[1];
14551
- var _React$useState3 = React__default["default"].useState({
14547
+ var _React$useState = React__default["default"].useState({
14552
14548
  hideTitle: false,
14553
14549
  hideButton: false
14554
14550
  }),
14555
- _React$useState4 = _slicedToArray(_React$useState3, 2),
14556
- displayState = _React$useState4[0],
14557
- setDisplayState = _React$useState4[1];
14551
+ _React$useState2 = _slicedToArray(_React$useState, 2),
14552
+ displayState = _React$useState2[0],
14553
+ setDisplayState = _React$useState2[1];
14558
14554
  var isIconOnly = displayState.hideTitle || active || !title;
14555
+ var animatedValues = {
14556
+ opacity: React__default["default"].useRef(new reactNative.Animated.Value(1)).current,
14557
+ width: React__default["default"].useRef(new reactNative.Animated.Value(1)).current,
14558
+ translateY: React__default["default"].useRef(new reactNative.Animated.Value(0)).current
14559
+ };
14560
+ var marginBottom = Number((_StyleSheet$flatten = reactNative.StyleSheet.flatten(style)) === null || _StyleSheet$flatten === void 0 ? void 0 : _StyleSheet$flatten.marginBottom) || 0;
14561
+ var _React$useState3 = React__default["default"].useState(0),
14562
+ _React$useState4 = _slicedToArray(_React$useState3, 2),
14563
+ buttonWidth = _React$useState4[0],
14564
+ setButtonWidth = _React$useState4[1];
14565
+ var hasSetButtonWidth = buttonWidth > 0;
14559
14566
  React__default["default"].useImperativeHandle(ref, function () {
14560
14567
  return {
14561
14568
  show: function show() {
14569
+ reactNative.Animated.spring(animatedValues.translateY, {
14570
+ toValue: 0,
14571
+ useNativeDriver: true
14572
+ }).start();
14562
14573
  setDisplayState({
14563
14574
  hideButton: false,
14564
14575
  hideTitle: false
14565
14576
  });
14577
+ animateWidth();
14578
+ reactNative.Animated.spring(animatedValues.opacity, {
14579
+ toValue: 1,
14580
+ useNativeDriver: true
14581
+ }).start();
14566
14582
  },
14567
14583
  collapse: function collapse() {
14584
+ reactNative.Animated.parallel([reactNative.Animated.spring(animatedValues.opacity, {
14585
+ toValue: 1,
14586
+ useNativeDriver: true
14587
+ }), reactNative.Animated.spring(animatedValues.translateY, {
14588
+ toValue: 0,
14589
+ useNativeDriver: true
14590
+ })]).start();
14591
+ animateWidth();
14568
14592
  setDisplayState({
14569
14593
  hideButton: false,
14570
14594
  hideTitle: true
14571
14595
  });
14572
14596
  },
14573
14597
  hide: function hide() {
14574
- setDisplayState(function (previousState) {
14575
- return _objectSpread2(_objectSpread2({}, previousState), {}, {
14576
- hideButton: true
14598
+ reactNative.Animated.stagger(20, [reactNative.Animated.spring(animatedValues.opacity, {
14599
+ toValue: 0,
14600
+ useNativeDriver: true
14601
+ }), reactNative.Animated.spring(animatedValues.translateY, {
14602
+ toValue: 1,
14603
+ useNativeDriver: true
14604
+ })]).start(function () {
14605
+ animateWidth();
14606
+ setDisplayState(function (previousState) {
14607
+ return _objectSpread2(_objectSpread2({}, previousState), {}, {
14608
+ hideButton: true
14609
+ });
14577
14610
  });
14578
14611
  });
14579
14612
  }
14580
14613
  };
14581
14614
  }, []);
14582
- React__default["default"].useEffect(function () {
14583
- if (canAnimate) {
14584
- reactNative.LayoutAnimation.configureNext(defaultAnimation);
14585
- }
14586
- }, [isIconOnly, displayState.hideButton, canAnimate]);
14587
- var marginBottom = Number((_StyleSheet$flatten = reactNative.StyleSheet.flatten(style)) === null || _StyleSheet$flatten === void 0 ? void 0 : _StyleSheet$flatten.marginBottom) || 0;
14588
- return /*#__PURE__*/React__default["default"].createElement(StyledFAB$1
14589
- /** Add a small timeout before executing animation to prevent flakiness */, {
14590
- onLayout: function onLayout() {
14591
- return setTimeout(function () {
14592
- return setCanAnimate(true);
14593
- }, 500);
14615
+ return /*#__PURE__*/React__default["default"].createElement(StyledFAB$1, {
14616
+ onLayout: function onLayout(event) {
14617
+ return !hasSetButtonWidth && !active && setButtonWidth(event.nativeEvent.layout.width);
14594
14618
  },
14595
14619
  underlayColor: theme.__hd__.fab.colors.buttonPressedBackground,
14596
14620
  onPress: onPress,
14597
14621
  style: [style, {
14598
- bottom: displayState.hideButton ? -(marginBottom + theme.__hd__.fab.sizes.height * 2) : (_StyleSheet$flatten2 = reactNative.StyleSheet.flatten(style)) === null || _StyleSheet$flatten2 === void 0 ? void 0 : _StyleSheet$flatten2.bottom
14622
+ bottom: displayState.hideButton ? -(marginBottom + theme.__hd__.fab.sizes.height * 2) : (_StyleSheet$flatten2 = reactNative.StyleSheet.flatten(style)) === null || _StyleSheet$flatten2 === void 0 ? void 0 : _StyleSheet$flatten2.bottom,
14623
+ transform: [{
14624
+ translateY: animatedValues.translateY.interpolate({
14625
+ inputRange: [0, 1],
14626
+ outputRange: [0, marginBottom + theme.__hd__.fab.sizes.height * 2]
14627
+ })
14628
+ }]
14599
14629
  }],
14600
14630
  testID: testID,
14601
14631
  themeActive: active
14632
+ }, /*#__PURE__*/React__default["default"].createElement(reactNative.Animated.View, {
14633
+ style: {
14634
+ flexDirection: 'row',
14635
+ opacity: animatedValues.opacity.interpolate({
14636
+ inputRange: [0, 1],
14637
+ outputRange: [0, 1]
14638
+ })
14639
+ }
14602
14640
  }, isIconOnly ? /*#__PURE__*/React__default["default"].createElement(IconOnlyContent, {
14603
14641
  animated: animated,
14604
14642
  active: active,
@@ -14606,7 +14644,7 @@ var FAB$1 = /*#__PURE__*/React.forwardRef(function (_ref3, ref) {
14606
14644
  }) : /*#__PURE__*/React__default["default"].createElement(IconWithTextContent, {
14607
14645
  icon: icon,
14608
14646
  title: title
14609
- }));
14647
+ })));
14610
14648
  });
14611
14649
  FAB$1.displayName = 'FAB';
14612
14650
 
@@ -14656,7 +14694,9 @@ var ActionItem = function ActionItem(_ref) {
14656
14694
  reactNative.Animated.spring(animatedValue.current, {
14657
14695
  toValue: active ? 1 : 0,
14658
14696
  useNativeDriver: reactNative.Platform.OS !== 'web',
14659
- delay: index * 20
14697
+ delay: index * 30,
14698
+ speed: 10,
14699
+ bounciness: 10
14660
14700
  }).start();
14661
14701
  }, [active, index]);
14662
14702
  return /*#__PURE__*/React__default["default"].createElement(reactNative.Animated.View, {
@@ -14733,8 +14773,9 @@ var ActionGroup = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
14733
14773
  _ref$fabIcon = _ref.fabIcon,
14734
14774
  fabIcon = _ref$fabIcon === void 0 ? 'add' : _ref$fabIcon;
14735
14775
  useDeprecation("FAB.ActionGroup's headerTitle prop will be removed in the next major release. Please remove it.", headerTitle !== undefined);
14776
+ var theme = useTheme();
14736
14777
  var fabRef = React.useRef(null);
14737
- var tranlateXAnimation = React.useRef(new reactNative.Animated.Value(active ? 1 : 0));
14778
+ var animatedValue = React.useRef(new reactNative.Animated.Value(active ? 1 : 0));
14738
14779
  React__default["default"].useImperativeHandle(ref, function () {
14739
14780
  return {
14740
14781
  showFAB: function showFAB() {
@@ -14752,28 +14793,28 @@ var ActionGroup = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
14752
14793
  };
14753
14794
  }, [fabRef]);
14754
14795
  React__default["default"].useEffect(function () {
14755
- reactNative.Animated.spring(tranlateXAnimation.current, {
14796
+ reactNative.Animated.spring(animatedValue.current, {
14756
14797
  toValue: active ? 1 : 0,
14798
+ delay: 100,
14757
14799
  useNativeDriver: reactNative.Platform.OS !== 'web'
14758
14800
  }).start();
14801
+ if (active) {
14802
+ var _fabRef$current4;
14803
+ (_fabRef$current4 = fabRef.current) === null || _fabRef$current4 === void 0 || _fabRef$current4.collapse();
14804
+ } else {
14805
+ var _fabRef$current5;
14806
+ (_fabRef$current5 = fabRef.current) === null || _fabRef$current5 === void 0 || _fabRef$current5.show();
14807
+ }
14759
14808
  }, [active]);
14760
- var interpolatedActionGroupOpacityAnimation = tranlateXAnimation.current.interpolate({
14809
+ var actionGroupOpacity = animatedValue.current.interpolate({
14761
14810
  inputRange: [0, 1],
14762
14811
  outputRange: [0, 1]
14763
14812
  });
14764
- var interpolatedFABOpacityAnimation = tranlateXAnimation.current.interpolate({
14765
- inputRange: [0, 1],
14766
- outputRange: [1, 0]
14767
- });
14768
14813
  return /*#__PURE__*/React__default["default"].createElement(StyledContainer$2, {
14769
14814
  testID: testID,
14770
14815
  pointerEvents: "box-none",
14771
14816
  style: style
14772
- }, /*#__PURE__*/React__default["default"].createElement(reactNative.Animated.View, {
14773
- style: {
14774
- opacity: interpolatedFABOpacityAnimation
14775
- }
14776
- }, /*#__PURE__*/React__default["default"].createElement(StyledFAB, {
14817
+ }, /*#__PURE__*/React__default["default"].createElement(reactNative.Animated.View, null, /*#__PURE__*/React__default["default"].createElement(StyledFAB, {
14777
14818
  key: "fab",
14778
14819
  testID: "fab",
14779
14820
  icon: fabIcon,
@@ -14797,7 +14838,7 @@ var ActionGroup = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
14797
14838
  testID: "action-group",
14798
14839
  pointerEvents: "box-none",
14799
14840
  style: {
14800
- opacity: interpolatedActionGroupOpacityAnimation
14841
+ opacity: actionGroupOpacity
14801
14842
  }
14802
14843
  }, /*#__PURE__*/React__default["default"].createElement(Box, {
14803
14844
  style: [style, {
@@ -14810,7 +14851,13 @@ var ActionGroup = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
14810
14851
  index: active ? index : items.length - index,
14811
14852
  active: active
14812
14853
  }));
14813
- }))), /*#__PURE__*/React__default["default"].createElement(StyledFAB, {
14854
+ }))), active && /*#__PURE__*/React__default["default"].createElement(StyledFAB
14855
+ // This FAB is moved up a bit compared to the original FAB,
14856
+ // set marginBottom to negative value to compensate for it
14857
+ , {
14858
+ style: {
14859
+ marginBottom: -theme.space.xxsmall
14860
+ },
14814
14861
  key: "fab-in-portal",
14815
14862
  testID: "fab-in-portal",
14816
14863
  icon: fabIcon,
@@ -35584,8 +35631,9 @@ var index = Object.assign(RichTextEditorWithRef, {
35584
35631
  Toolbar: EditorToolbar
35585
35632
  });
35586
35633
 
35587
- var COLLAPSE_BREAKPOINT = 10;
35588
- var SHOW_AND_HIDE_BREAKPOINT = 200;
35634
+ var LAST_BREAKPOINT = 100;
35635
+ var MIDDLE_BREAKPOINT = 250;
35636
+ var MAX_ANIMATABLE_SCROLL_DISTANCE = 400;
35589
35637
  var REF_ACTIONS_BY_COMPONENT = {
35590
35638
  FAB: {
35591
35639
  show: 'show',
@@ -35600,27 +35648,23 @@ var REF_ACTIONS_BY_COMPONENT = {
35600
35648
  };
35601
35649
  var AnimatedFAB = function AnimatedFAB(_ref) {
35602
35650
  var fabProps = _ref.fabProps,
35603
- contentOffsetY = _ref.contentOffsetY;
35651
+ contentOffsetY = _ref.contentOffsetY,
35652
+ contentHeight = _ref.contentHeight,
35653
+ layoutHeight = _ref.layoutHeight;
35604
35654
  var component = 'items' in fabProps ? 'ActionGroup' : 'FAB';
35605
- var _React$useState = React__default["default"].useState('down'),
35606
- _React$useState2 = _slicedToArray(_React$useState, 2),
35607
- currentScrollDirection = _React$useState2[0],
35608
- setCurrentScrollDirection = _React$useState2[1];
35609
- var _React$useState3 = React__default["default"].useState(0),
35610
- _React$useState4 = _slicedToArray(_React$useState3, 2),
35611
- lastScrollY = _React$useState4[0],
35612
- setLastScrollY = _React$useState4[1];
35613
- var _React$useState5 = React__default["default"].useState('show'),
35614
- _React$useState6 = _slicedToArray(_React$useState5, 2),
35615
- fabState = _React$useState6[0],
35616
- setFabState = _React$useState6[1];
35617
- var _React$useState7 = React__default["default"].useState(SHOW_AND_HIDE_BREAKPOINT),
35618
- _React$useState8 = _slicedToArray(_React$useState7, 2),
35619
- remainingScrollOffset = _React$useState8[0],
35620
- setRemainingScrollOffset = _React$useState8[1];
35621
35655
  var ref = React__default["default"].useRef(null);
35656
+ var currentContentHeight = React__default["default"].useRef(0);
35657
+ var currentLayoutHeight = React__default["default"].useRef(0);
35658
+ /** fabState is used to avoid calling duplicated animations. */
35659
+ var fabState = React__default["default"].useRef('show');
35660
+ /** remainingScrollOffset determines whether to animate the FAB. */
35661
+ var remainingScrollOffset = React__default["default"].useRef(MAX_ANIMATABLE_SCROLL_DISTANCE);
35662
+ /** currentScrollDirection is used to determine the scroll direction. */
35663
+ var currentScrollDirection = React__default["default"].useRef('down');
35664
+ /** lastScrollY is the scrollY from the preview scroll event. */
35665
+ var lastScrollY = React__default["default"].useRef(0);
35622
35666
  var animateFab = React__default["default"].useCallback(function (newState) {
35623
- if (fabState !== newState) {
35667
+ if (fabState.current !== newState) {
35624
35668
  if (newState === 'show') {
35625
35669
  var _ref$current;
35626
35670
  (_ref$current = ref.current) === null || _ref$current === void 0 || _ref$current[REF_ACTIONS_BY_COMPONENT[component].show]();
@@ -35631,37 +35675,64 @@ var AnimatedFAB = function AnimatedFAB(_ref) {
35631
35675
  var _ref$current3;
35632
35676
  (_ref$current3 = ref.current) === null || _ref$current3 === void 0 || _ref$current3[REF_ACTIONS_BY_COMPONENT[component].collapse]();
35633
35677
  }
35634
- setFabState(newState);
35635
- }
35636
- }, [fabState, component]);
35637
- // Listen to ScrollView's contentOffsetY value
35638
- contentOffsetY.addListener(function (_ref2) {
35639
- var value = _ref2.value;
35640
- if (value < 0) {
35641
- return;
35642
- }
35643
- var newScrollDirection = value > lastScrollY ? 'down' : 'up';
35644
- var isScrollingDown = newScrollDirection === 'down';
35645
- if (newScrollDirection !== currentScrollDirection || lastScrollY === 0) {
35646
- setLastScrollY(value);
35647
- setCurrentScrollDirection(newScrollDirection);
35648
- }
35649
- var offsetFromLastDirection = Math.abs(value - lastScrollY);
35650
- var offsetDiff = Math.round(Math.max(remainingScrollOffset - offsetFromLastDirection, 0));
35651
- if (remainingScrollOffset > 0) {
35652
- if (offsetDiff === SHOW_AND_HIDE_BREAKPOINT) {
35653
- animateFab(isScrollingDown ? 'show' : 'hide');
35654
- } else if (offsetDiff <= SHOW_AND_HIDE_BREAKPOINT && offsetDiff > COLLAPSE_BREAKPOINT) {
35655
- animateFab('collapse');
35656
- } else if (offsetDiff <= COLLAPSE_BREAKPOINT) {
35657
- animateFab(isScrollingDown ? 'hide' : 'show');
35658
- }
35659
- setRemainingScrollOffset(offsetDiff);
35678
+ fabState.current = newState;
35660
35679
  }
35661
- });
35680
+ }, [component]);
35662
35681
  React__default["default"].useEffect(function () {
35663
- setRemainingScrollOffset(SHOW_AND_HIDE_BREAKPOINT);
35664
- }, [currentScrollDirection]);
35682
+ contentHeight.addListener(function (_ref2) {
35683
+ var value = _ref2.value;
35684
+ if (value > 0 && value !== currentContentHeight.current) {
35685
+ currentContentHeight.current = value;
35686
+ }
35687
+ });
35688
+ layoutHeight.addListener(function (_ref3) {
35689
+ var value = _ref3.value;
35690
+ if (value > 0 && value !== currentLayoutHeight.current) {
35691
+ currentLayoutHeight.current = value;
35692
+ }
35693
+ });
35694
+ // Listen to ScrollView's contentOffsetY value
35695
+ contentOffsetY.addListener(function (_ref4) {
35696
+ var value = _ref4.value;
35697
+ if (value < 0 ||
35698
+ // Prevent calling the function if the scroll is not significant
35699
+ value > 0 && Math.abs(value - lastScrollY.current) < 5) {
35700
+ return;
35701
+ }
35702
+ // Scroll up to top, bouncing included.
35703
+ if (value === 0 && lastScrollY.current !== 0) {
35704
+ animateFab('show');
35705
+ }
35706
+ var newScrollDirection = value >= lastScrollY.current ? 'down' : 'up';
35707
+ if (newScrollDirection !== currentScrollDirection.current) {
35708
+ // If scroll direction changes, reset all values
35709
+ currentScrollDirection.current = newScrollDirection;
35710
+ remainingScrollOffset.current = MAX_ANIMATABLE_SCROLL_DISTANCE;
35711
+ }
35712
+ var hasReachedBottom = value + currentLayoutHeight.current >= currentContentHeight.current;
35713
+ // Scroll down to bottom, bouncing included.
35714
+ if (hasReachedBottom) {
35715
+ animateFab('hide');
35716
+ return;
35717
+ }
35718
+ if (remainingScrollOffset.current) {
35719
+ var offsetDiff = Math.round(Math.max(Math.abs(value - lastScrollY.current), 0));
35720
+ var newRemainingScrollOffset = Math.max(remainingScrollOffset.current - offsetDiff, 0);
35721
+ if (newRemainingScrollOffset <= LAST_BREAKPOINT) {
35722
+ animateFab(currentScrollDirection.current === 'down' ? 'hide' : 'show');
35723
+ } else if (newRemainingScrollOffset <= MIDDLE_BREAKPOINT) {
35724
+ animateFab('collapse');
35725
+ }
35726
+ remainingScrollOffset.current = newRemainingScrollOffset;
35727
+ }
35728
+ lastScrollY.current = value;
35729
+ });
35730
+ return function () {
35731
+ contentOffsetY.removeAllListeners();
35732
+ contentHeight.removeAllListeners();
35733
+ layoutHeight.removeAllListeners();
35734
+ };
35735
+ }, [contentHeight, contentOffsetY, layoutHeight]);
35665
35736
  return component === 'FAB' ? /*#__PURE__*/React__default["default"].createElement(FAB, _extends$1({
35666
35737
  ref: ref
35667
35738
  }, fabProps)) : /*#__PURE__*/React__default["default"].createElement(ActionGroup, _extends$1({
@@ -35673,6 +35744,8 @@ function AnimatedScroller(_ref) {
35673
35744
  var ScrollComponent = _ref.ScrollComponent,
35674
35745
  fabProps = _ref.fabProps;
35675
35746
  var contentOffsetY = React__default["default"].useRef(new reactNative.Animated.Value(0)).current;
35747
+ var contentHeight = React__default["default"].useRef(new reactNative.Animated.Value(0)).current;
35748
+ var layoutHeight = React__default["default"].useRef(new reactNative.Animated.Value(0)).current;
35676
35749
  // Common props for all ScrollView, FlatList and SectionList.
35677
35750
  var _ScrollComponent$prop = ScrollComponent.props,
35678
35751
  onScroll = _ScrollComponent$prop.onScroll,
@@ -35683,6 +35756,12 @@ function AnimatedScroller(_ref) {
35683
35756
  nativeEvent: {
35684
35757
  contentOffset: {
35685
35758
  y: contentOffsetY
35759
+ },
35760
+ contentSize: {
35761
+ height: contentHeight
35762
+ },
35763
+ layoutMeasurement: {
35764
+ height: layoutHeight
35686
35765
  }
35687
35766
  }
35688
35767
  }], {
@@ -35691,7 +35770,9 @@ function AnimatedScroller(_ref) {
35691
35770
  })
35692
35771
  })), !!fabProps && /*#__PURE__*/React__default["default"].createElement(AnimatedFAB, {
35693
35772
  fabProps: fabProps,
35694
- contentOffsetY: contentOffsetY
35773
+ contentOffsetY: contentOffsetY,
35774
+ contentHeight: contentHeight,
35775
+ layoutHeight: layoutHeight
35695
35776
  }));
35696
35777
  }
35697
35778
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hero-design/rn",
3
- "version": "8.59.0",
3
+ "version": "8.60.0",
4
4
  "license": "MIT",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.js",
@@ -7,8 +7,9 @@ import ActionGroup, {
7
7
  } from '../FAB/ActionGroup';
8
8
  import { FABHandles, FABProps } from '../FAB/FAB';
9
9
 
10
- const COLLAPSE_BREAKPOINT = 10;
11
- const SHOW_AND_HIDE_BREAKPOINT = 200;
10
+ const LAST_BREAKPOINT = 100;
11
+ const MIDDLE_BREAKPOINT = 250;
12
+ const MAX_ANIMATABLE_SCROLL_DISTANCE = 400;
12
13
  const REF_ACTIONS_BY_COMPONENT = {
13
14
  FAB: {
14
15
  show: 'show',
@@ -25,25 +26,36 @@ const REF_ACTIONS_BY_COMPONENT = {
25
26
  interface AnimatedFABProps {
26
27
  fabProps: FABProps | ActionGroupProps;
27
28
  contentOffsetY: Animated.Value;
29
+ contentHeight: Animated.Value;
30
+ layoutHeight: Animated.Value;
28
31
  }
29
32
 
30
- const AnimatedFAB = ({ fabProps, contentOffsetY }: AnimatedFABProps) => {
33
+ const AnimatedFAB = ({
34
+ fabProps,
35
+ contentOffsetY,
36
+ contentHeight,
37
+ layoutHeight,
38
+ }: AnimatedFABProps) => {
31
39
  const component = 'items' in fabProps ? 'ActionGroup' : 'FAB';
32
- const [currentScrollDirection, setCurrentScrollDirection] = React.useState<
33
- 'up' | 'down'
34
- >('down');
35
- const [lastScrollY, setLastScrollY] = React.useState(0);
36
- const [fabState, setFabState] = React.useState<'show' | 'hide' | 'collapse'>(
37
- 'show'
38
- );
39
- const [remainingScrollOffset, setRemainingScrollOffset] = React.useState(
40
- SHOW_AND_HIDE_BREAKPOINT
41
- );
42
40
  const ref = React.useRef<FABHandles & ActionGroupHandles>(null);
41
+ const currentContentHeight = React.useRef(0);
42
+ const currentLayoutHeight = React.useRef(0);
43
+
44
+ /** fabState is used to avoid calling duplicated animations. */
45
+ const fabState = React.useRef<'show' | 'hide' | 'collapse'>('show');
46
+
47
+ /** remainingScrollOffset determines whether to animate the FAB. */
48
+ const remainingScrollOffset = React.useRef(MAX_ANIMATABLE_SCROLL_DISTANCE);
49
+
50
+ /** currentScrollDirection is used to determine the scroll direction. */
51
+ const currentScrollDirection = React.useRef<'up' | 'down'>('down');
52
+
53
+ /** lastScrollY is the scrollY from the preview scroll event. */
54
+ const lastScrollY = React.useRef(0);
43
55
 
44
56
  const animateFab = React.useCallback(
45
57
  (newState: 'show' | 'hide' | 'collapse') => {
46
- if (fabState !== newState) {
58
+ if (fabState.current !== newState) {
47
59
  if (newState === 'show') {
48
60
  ref.current?.[REF_ACTIONS_BY_COMPONENT[component].show]();
49
61
  } else if (newState === 'hide') {
@@ -51,49 +63,87 @@ const AnimatedFAB = ({ fabProps, contentOffsetY }: AnimatedFABProps) => {
51
63
  } else {
52
64
  ref.current?.[REF_ACTIONS_BY_COMPONENT[component].collapse]();
53
65
  }
54
- setFabState(newState);
66
+ fabState.current = newState;
55
67
  }
56
68
  },
57
- [fabState, component]
69
+ [component]
58
70
  );
59
71
 
60
- // Listen to ScrollView's contentOffsetY value
61
- contentOffsetY.addListener(({ value }) => {
62
- if (value < 0) {
63
- return;
64
- }
65
-
66
- const newScrollDirection = value > lastScrollY ? 'down' : 'up';
67
- const isScrollingDown = newScrollDirection === 'down';
68
-
69
- if (newScrollDirection !== currentScrollDirection || lastScrollY === 0) {
70
- setLastScrollY(value);
71
- setCurrentScrollDirection(newScrollDirection);
72
- }
73
- const offsetFromLastDirection = Math.abs(value - lastScrollY);
74
- const offsetDiff = Math.round(
75
- Math.max(remainingScrollOffset - offsetFromLastDirection, 0)
76
- );
77
-
78
- if (remainingScrollOffset > 0) {
79
- if (offsetDiff === SHOW_AND_HIDE_BREAKPOINT) {
80
- animateFab(isScrollingDown ? 'show' : 'hide');
81
- } else if (
82
- offsetDiff <= SHOW_AND_HIDE_BREAKPOINT &&
83
- offsetDiff > COLLAPSE_BREAKPOINT
72
+ React.useEffect(() => {
73
+ contentHeight.addListener(({ value }) => {
74
+ if (value > 0 && value !== currentContentHeight.current) {
75
+ currentContentHeight.current = value;
76
+ }
77
+ });
78
+
79
+ layoutHeight.addListener(({ value }) => {
80
+ if (value > 0 && value !== currentLayoutHeight.current) {
81
+ currentLayoutHeight.current = value;
82
+ }
83
+ });
84
+
85
+ // Listen to ScrollView's contentOffsetY value
86
+ contentOffsetY.addListener(({ value }) => {
87
+ if (
88
+ value < 0 ||
89
+ // Prevent calling the function if the scroll is not significant
90
+ (value > 0 && Math.abs(value - lastScrollY.current) < 5)
84
91
  ) {
85
- animateFab('collapse');
86
- } else if (offsetDiff <= COLLAPSE_BREAKPOINT) {
87
- animateFab(isScrollingDown ? 'hide' : 'show');
92
+ return;
88
93
  }
89
94
 
90
- setRemainingScrollOffset(offsetDiff);
91
- }
92
- });
95
+ // Scroll up to top, bouncing included.
96
+ if (value === 0 && lastScrollY.current !== 0) {
97
+ animateFab('show');
98
+ }
93
99
 
94
- React.useEffect(() => {
95
- setRemainingScrollOffset(SHOW_AND_HIDE_BREAKPOINT);
96
- }, [currentScrollDirection]);
100
+ const newScrollDirection = value >= lastScrollY.current ? 'down' : 'up';
101
+
102
+ if (newScrollDirection !== currentScrollDirection.current) {
103
+ // If scroll direction changes, reset all values
104
+ currentScrollDirection.current = newScrollDirection;
105
+ remainingScrollOffset.current = MAX_ANIMATABLE_SCROLL_DISTANCE;
106
+ }
107
+
108
+ const hasReachedBottom =
109
+ value + currentLayoutHeight.current >= currentContentHeight.current;
110
+
111
+ // Scroll down to bottom, bouncing included.
112
+ if (hasReachedBottom) {
113
+ animateFab('hide');
114
+ return;
115
+ }
116
+
117
+ if (remainingScrollOffset.current) {
118
+ const offsetDiff = Math.round(
119
+ Math.max(Math.abs(value - lastScrollY.current), 0)
120
+ );
121
+
122
+ const newRemainingScrollOffset = Math.max(
123
+ remainingScrollOffset.current - offsetDiff,
124
+ 0
125
+ );
126
+
127
+ if (newRemainingScrollOffset <= LAST_BREAKPOINT) {
128
+ animateFab(
129
+ currentScrollDirection.current === 'down' ? 'hide' : 'show'
130
+ );
131
+ } else if (newRemainingScrollOffset <= MIDDLE_BREAKPOINT) {
132
+ animateFab('collapse');
133
+ }
134
+
135
+ remainingScrollOffset.current = newRemainingScrollOffset;
136
+ }
137
+
138
+ lastScrollY.current = value;
139
+ });
140
+
141
+ return () => {
142
+ contentOffsetY.removeAllListeners();
143
+ contentHeight.removeAllListeners();
144
+ layoutHeight.removeAllListeners();
145
+ };
146
+ }, [contentHeight, contentOffsetY, layoutHeight]);
97
147
 
98
148
  return component === 'FAB' ? (
99
149
  <FAB ref={ref} {...(fabProps as FABProps)} />