@hero-design/rn 8.59.0 → 8.60.1-alpha.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.
Files changed (35) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +15 -0
  3. package/es/index.js +240 -116
  4. package/lib/index.js +240 -116
  5. package/package.json +2 -2
  6. package/src/components/AnimatedScroller/AnimatedFAB.tsx +99 -49
  7. package/src/components/AnimatedScroller/AnimatedScrollable.tsx +18 -3
  8. package/src/components/AnimatedScroller/__tests__/ScrollablesWithFAB.spec.tsx +30 -9
  9. package/src/components/AnimatedScroller/__tests__/__snapshots__/ScrollablesWithFAB.spec.tsx.snap +474 -447
  10. package/src/components/FAB/ActionGroup/ActionItem.tsx +3 -1
  11. package/src/components/FAB/ActionGroup/__tests__/__snapshots__/index.spec.tsx.snap +216 -211
  12. package/src/components/FAB/ActionGroup/index.tsx +34 -28
  13. package/src/components/FAB/FAB.tsx +102 -41
  14. package/src/components/FAB/StyledFAB.tsx +10 -8
  15. package/src/components/FAB/__tests__/__snapshots__/StyledFAB.spec.tsx.snap +34 -38
  16. package/src/components/FAB/__tests__/__snapshots__/index.spec.tsx.snap +191 -170
  17. package/src/components/Radio/Radio.tsx +16 -4
  18. package/src/components/Radio/RadioGroup.tsx +10 -3
  19. package/src/components/Radio/StyledRadio.tsx +20 -3
  20. package/src/components/Radio/__tests__/Radio.spec.tsx +46 -13
  21. package/src/components/Radio/__tests__/RadioGroup.spec.tsx +40 -7
  22. package/src/components/Radio/__tests__/__snapshots__/Radio.spec.tsx.snap +446 -77
  23. package/src/components/Radio/__tests__/__snapshots__/RadioGroup.spec.tsx.snap +946 -112
  24. package/src/components/Radio/types.ts +6 -1
  25. package/src/theme/__tests__/__snapshots__/index.spec.ts.snap +8 -2
  26. package/src/theme/components/radio.ts +8 -2
  27. package/types/components/AnimatedScroller/AnimatedFAB.d.ts +3 -1
  28. package/types/components/AnimatedScroller/AnimatedScrollable.d.ts +1 -1
  29. package/types/components/FAB/StyledFAB.d.ts +4 -6
  30. package/types/components/Radio/Radio.d.ts +9 -1
  31. package/types/components/Radio/RadioGroup.d.ts +5 -1
  32. package/types/components/Radio/StyledRadio.d.ts +11 -1
  33. package/types/components/Radio/index.d.ts +1 -1
  34. package/types/components/Radio/types.d.ts +1 -0
  35. package/types/theme/components/radio.d.ts +7 -1
@@ -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)} />
@@ -5,9 +5,9 @@ import {
5
5
  ScrollViewProps as RnScrollViewProps,
6
6
  SectionListProps,
7
7
  } from 'react-native';
8
+ import { ActionGroupProps } from '../FAB/ActionGroup';
8
9
  import { FABProps } from '../FAB/FAB';
9
10
  import AnimatedFAB from './AnimatedFAB';
10
- import { ActionGroupProps } from '../FAB/ActionGroup';
11
11
 
12
12
  export interface AnimatedScrollerProps<T> {
13
13
  /**
@@ -27,6 +27,8 @@ function AnimatedScroller<T>({
27
27
  fabProps,
28
28
  }: AnimatedScrollerProps<T>) {
29
29
  const contentOffsetY = React.useRef(new Animated.Value(0)).current;
30
+ const contentHeight = React.useRef(new Animated.Value(0)).current;
31
+ const layoutHeight = React.useRef(new Animated.Value(0)).current;
30
32
 
31
33
  // Common props for all ScrollView, FlatList and SectionList.
32
34
  const { onScroll, scrollEventThrottle } = ScrollComponent.props;
@@ -37,7 +39,15 @@ function AnimatedScroller<T>({
37
39
  ...ScrollComponent.props,
38
40
  scrollEventThrottle: scrollEventThrottle || 100,
39
41
  onScroll: Animated.event(
40
- [{ nativeEvent: { contentOffset: { y: contentOffsetY } } }],
42
+ [
43
+ {
44
+ nativeEvent: {
45
+ contentOffset: { y: contentOffsetY },
46
+ contentSize: { height: contentHeight },
47
+ layoutMeasurement: { height: layoutHeight },
48
+ },
49
+ },
50
+ ],
41
51
  {
42
52
  useNativeDriver: false,
43
53
  listener: onScroll,
@@ -46,7 +56,12 @@ function AnimatedScroller<T>({
46
56
  })}
47
57
 
48
58
  {!!fabProps && (
49
- <AnimatedFAB fabProps={fabProps} contentOffsetY={contentOffsetY} />
59
+ <AnimatedFAB
60
+ fabProps={fabProps}
61
+ contentOffsetY={contentOffsetY}
62
+ contentHeight={contentHeight}
63
+ layoutHeight={layoutHeight}
64
+ />
50
65
  )}
51
66
  </>
52
67
  );
@@ -125,17 +125,28 @@ describe('Scrollables With FAB', () => {
125
125
  const fabProps =
126
126
  fabComponent === 'FAB' ? defaultFabProps : defaultActionGroupProps;
127
127
 
128
+ const scrollConfig = {
129
+ contentSize: { height: 2000 },
130
+ layoutMeasurement: { height: 700, width: 400 },
131
+ };
132
+
128
133
  const { getByText, queryByText, getByTestId, queryByTestId } =
129
134
  renderWithTheme(<ScrollComponent fabProps={fabProps} />);
130
135
 
136
+ fireEvent.scroll(getByTestId('scrollable-with-fab'), {
137
+ nativeEvent: {
138
+ ...scrollConfig,
139
+ contentOffset: { y: 50 },
140
+ },
141
+ });
142
+
131
143
  // Scrolling down
132
144
  expect(getByText('Shout out')).toBeDefined();
133
145
 
134
146
  fireEvent.scroll(getByTestId('scrollable-with-fab'), {
135
147
  nativeEvent: {
136
- contentSize: { height: 1000 },
137
- contentOffset: { y: 10 },
138
- layoutMeasurement: { height: 2000, width: 400 },
148
+ ...scrollConfig,
149
+ contentOffset: { y: 150 },
139
150
  },
140
151
  });
141
152
 
@@ -145,9 +156,8 @@ describe('Scrollables With FAB', () => {
145
156
 
146
157
  fireEvent.scroll(getByTestId('scrollable-with-fab'), {
147
158
  nativeEvent: {
148
- contentSize: { height: 1000 },
149
- contentOffset: { y: 400 },
150
- layoutMeasurement: { height: 2000, width: 400 },
159
+ ...scrollConfig,
160
+ contentOffset: { y: 500 },
151
161
  },
152
162
  });
153
163
 
@@ -158,15 +168,26 @@ describe('Scrollables With FAB', () => {
158
168
  // Scrolling up
159
169
  fireEvent.scroll(getByTestId('scrollable-with-fab'), {
160
170
  nativeEvent: {
161
- contentSize: { height: 1000 },
162
- contentOffset: { y: -10 },
163
- layoutMeasurement: { height: 2000, width: 400 },
171
+ ...scrollConfig,
172
+ contentOffset: { y: -150 },
164
173
  },
165
174
  });
166
175
 
167
176
  // Collapsed
168
177
  expect(queryByText('Shout out')).toBeNull();
169
178
  expect(getByTestId('animated-fab-icon')).toBeDefined();
179
+
180
+ // Scrolling up to top
181
+ fireEvent.scroll(getByTestId('scrollable-with-fab'), {
182
+ nativeEvent: {
183
+ ...scrollConfig,
184
+ contentOffset: { y: 0 },
185
+ },
186
+ });
187
+
188
+ // Expanded
189
+ expect(getByText('Shout out')).toBeDefined();
190
+ expect(getByTestId('styled-fab-icon')).toBeDefined();
170
191
  }
171
192
  );
172
193
  });