@hero-design/rn 8.44.1 → 8.45.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.
@@ -1,6 +1,9 @@
1
1
  import React from 'react';
2
2
  import renderWithTheme from '../../../../testHelpers/renderWithTheme';
3
3
  import Avatar from '../..';
4
+ import Typography from '../../../Typography';
5
+ import HeroDesignProvider from '../../../HeroDesignProvider';
6
+ import { theme } from '../../../..';
4
7
 
5
8
  beforeEach(() => {
6
9
  jest.spyOn(Math, 'random').mockReturnValue(0.123);
@@ -11,9 +14,13 @@ afterEach(() => {
11
14
  });
12
15
 
13
16
  describe('AvatarStack', () => {
14
- it('renders correctly by default', () => {
17
+ it.each`
18
+ variant
19
+ ${'horizontal'}
20
+ ${'vertical'}
21
+ `('renders correctly when variant is $variant', ({ variant }) => {
15
22
  const wrapper = renderWithTheme(
16
- <Avatar.Stack testID="group-avatar">
23
+ <Avatar.Stack variant={variant} testID="group-avatar">
17
24
  <Avatar title="TT" />
18
25
  <Avatar title="SS" />
19
26
  <Avatar title="AA" />
@@ -27,10 +34,14 @@ describe('AvatarStack', () => {
27
34
  expect(wrapper.queryAllByText('AA')).toHaveLength(1);
28
35
  expect(wrapper.queryAllByText('OO')).toHaveLength(1);
29
36
  expect(wrapper.queryAllByText('NA')).toHaveLength(1);
30
- expect(wrapper.getByTestId('group-avatar')).toHaveStyle({
31
- width: 121.6,
32
- height: 32,
33
- });
37
+ expect(wrapper.getByTestId('group-avatar')).toHaveStyle(
38
+ variant === 'horizontal'
39
+ ? {
40
+ width: 121.6,
41
+ height: 32,
42
+ }
43
+ : { width: 32, height: 121.6 }
44
+ );
34
45
 
35
46
  expect(wrapper.toJSON()).toMatchSnapshot();
36
47
  });
@@ -59,4 +70,127 @@ describe('AvatarStack', () => {
59
70
 
60
71
  expect(wrapper.toJSON()).toMatchSnapshot();
61
72
  });
73
+
74
+ it('does not render surplus when max is equal or larger than the number of avatars', () => {
75
+ const { toJSON, getByText, queryByTestId, rerender } = renderWithTheme(
76
+ <Avatar.Stack
77
+ max={4}
78
+ renderSurplus={(surplus) => (
79
+ <Typography.Body>Custom surplus {surplus}</Typography.Body>
80
+ )}
81
+ >
82
+ <Avatar title="TT" />
83
+ <Avatar title="SS" />
84
+ <Avatar title="AA" />
85
+ <Avatar title="OO" />
86
+ </Avatar.Stack>
87
+ );
88
+
89
+ expect(toJSON()).toMatchSnapshot();
90
+ expect(getByText('TT')).toBeDefined();
91
+ expect(getByText('OO')).toBeDefined();
92
+ expect(queryByTestId('surplus-container')).toBeNull();
93
+
94
+ rerender(
95
+ <HeroDesignProvider theme={theme}>
96
+ <Avatar.Stack
97
+ max={10}
98
+ renderSurplus={(surplus) => (
99
+ <Typography.Body>Custom surplus {surplus}</Typography.Body>
100
+ )}
101
+ >
102
+ <Avatar title="TT" />
103
+ <Avatar title="SS" />
104
+ <Avatar title="AA" />
105
+ <Avatar title="OO" />
106
+ </Avatar.Stack>
107
+ </HeroDesignProvider>
108
+ );
109
+
110
+ expect(queryByTestId('surplus-container')).toBeNull();
111
+ });
112
+
113
+ it('does not render surplus when total is equal to the number of avatars', () => {
114
+ const { toJSON, getByText, queryByTestId, rerender } = renderWithTheme(
115
+ <Avatar.Stack
116
+ total={4}
117
+ renderSurplus={(surplus) => (
118
+ <Typography.Body>Custom surplus {surplus}</Typography.Body>
119
+ )}
120
+ >
121
+ <Avatar title="TT" />
122
+ <Avatar title="SS" />
123
+ <Avatar title="AA" />
124
+ <Avatar title="OO" />
125
+ </Avatar.Stack>
126
+ );
127
+
128
+ expect(toJSON()).toMatchSnapshot();
129
+ expect(getByText('TT')).toBeDefined();
130
+ expect(getByText('OO')).toBeDefined();
131
+ expect(queryByTestId('surplus-container')).toBeNull();
132
+
133
+ rerender(
134
+ <HeroDesignProvider theme={theme}>
135
+ <Avatar.Stack
136
+ total={10}
137
+ renderSurplus={(surplus) => (
138
+ <Typography.Body>Custom surplus {surplus}</Typography.Body>
139
+ )}
140
+ >
141
+ <Avatar title="TT" />
142
+ <Avatar title="SS" />
143
+ <Avatar title="AA" />
144
+ <Avatar title="OO" />
145
+ </Avatar.Stack>
146
+ </HeroDesignProvider>
147
+ );
148
+
149
+ expect(getByText('Custom surplus 6')).toBeDefined();
150
+ });
151
+
152
+ it('render correctly surplus when both max and total are set', () => {
153
+ const { toJSON, getByText, queryByText } = renderWithTheme(
154
+ <Avatar.Stack
155
+ total={25}
156
+ max={2}
157
+ renderSurplus={(surplus) => (
158
+ <Typography.Body>Custom surplus {surplus}</Typography.Body>
159
+ )}
160
+ >
161
+ <Avatar title="TT" />
162
+ <Avatar title="SS" />
163
+ <Avatar title="AA" />
164
+ <Avatar title="OO" />
165
+ <Avatar title="NA" />
166
+ </Avatar.Stack>
167
+ );
168
+
169
+ expect(toJSON()).toMatchSnapshot();
170
+ expect(getByText('TT')).toBeDefined();
171
+ expect(queryByText('AA')).toBeNull();
172
+ expect(getByText('Custom surplus 23')).toBeDefined();
173
+ });
174
+
175
+ it('allow rendering custom surplus', () => {
176
+ const { toJSON, getByText } = renderWithTheme(
177
+ <Avatar.Stack
178
+ total={25}
179
+ renderSurplus={(surplus) => (
180
+ <Typography.Body>Custom surplus {surplus}</Typography.Body>
181
+ )}
182
+ >
183
+ <Avatar title="TT" />
184
+ <Avatar title="SS" />
185
+ <Avatar title="AA" />
186
+ <Avatar title="OO" />
187
+ <Avatar title="NA" />
188
+ </Avatar.Stack>
189
+ );
190
+
191
+ expect(toJSON()).toMatchSnapshot();
192
+ expect(getByText('TT')).toBeDefined();
193
+ expect(getByText('NA')).toBeDefined();
194
+ expect(getByText('Custom surplus 20')).toBeDefined();
195
+ });
62
196
  });
@@ -1,7 +1,11 @@
1
- import React, { ReactElement } from 'react';
1
+ import React, { ReactElement, ReactNode } from 'react';
2
2
  import { StyleProp, ViewStyle } from 'react-native';
3
- import Avatar, { AvatarProps } from '../Avatar';
4
- import { StyledAvatar, StyledWrapper } from './StyledAvatarStack';
3
+ import { AvatarProps } from '../Avatar';
4
+ import {
5
+ StyledAvatar,
6
+ StyledSurplusContainer,
7
+ StyledWrapper,
8
+ } from './StyledAvatarStack';
5
9
  import { useAvatarColors } from './utils';
6
10
 
7
11
  export interface AvatarStackProps extends Pick<AvatarProps, 'size'> {
@@ -13,6 +17,14 @@ export interface AvatarStackProps extends Pick<AvatarProps, 'size'> {
13
17
  * Max avatars to show.
14
18
  */
15
19
  max?: number;
20
+ /**
21
+ * The total number of avatars. Used for calculating the number of extra avatars.
22
+ */
23
+ total?: number;
24
+ /**
25
+ * Custom renderer of extraAvatars.
26
+ */
27
+ renderSurplus?: (value: number) => ReactNode;
16
28
  /**
17
29
  * Additional style.
18
30
  */
@@ -21,39 +33,112 @@ export interface AvatarStackProps extends Pick<AvatarProps, 'size'> {
21
33
  * Testing id of the component.
22
34
  */
23
35
  testID?: string;
36
+ /**
37
+ * Variant of the avatar stack.
38
+ */
39
+ variant?: 'horizontal' | 'vertical';
24
40
  }
25
41
 
42
+ type SurplusProps = {
43
+ value: number;
44
+ renderSurplus?: AvatarStackProps['renderSurplus'];
45
+ size?: AvatarStackProps['size'];
46
+ variant?: AvatarStackProps['variant'];
47
+ index: number;
48
+ backgroundColor: string;
49
+ };
50
+
51
+ const Surplus = ({
52
+ value,
53
+ renderSurplus,
54
+ size,
55
+ variant = 'horizontal',
56
+ index,
57
+ backgroundColor,
58
+ }: SurplusProps) => {
59
+ if (value > 0) {
60
+ if (renderSurplus) {
61
+ return (
62
+ <StyledSurplusContainer
63
+ testID="surplus-container"
64
+ themeSize={size}
65
+ themeVariant={variant}
66
+ themeIndex={index}
67
+ style={{ backgroundColor }}
68
+ >
69
+ {renderSurplus(value)}
70
+ </StyledSurplusContainer>
71
+ );
72
+ }
73
+
74
+ return (
75
+ <StyledAvatar
76
+ testID="surplus-container"
77
+ themeVariant={variant}
78
+ title={`+${value}`}
79
+ size={size}
80
+ themeIndex={index}
81
+ style={{ backgroundColor }}
82
+ />
83
+ );
84
+ }
85
+
86
+ return null;
87
+ };
88
+
26
89
  const AvatarStack = ({
27
90
  children,
28
91
  max,
29
92
  size = 'small',
30
93
  style,
31
94
  testID,
95
+ variant = 'horizontal',
96
+ total,
97
+ renderSurplus,
32
98
  }: AvatarStackProps) => {
33
99
  const colors = useAvatarColors();
34
100
  const avatars = children.slice(0, max);
35
- if (max !== undefined && children.length - max > 0) {
36
- const remainingAvatar = (
37
- <Avatar title={`+${children.length - max}`} size={size} />
38
- );
39
- avatars.push(remainingAvatar);
40
- }
101
+
102
+ const remainingAvatar = (() => {
103
+ let remain = 0;
104
+
105
+ // Remaining value will prioritize total prop if it exists
106
+ if (total && total > children.length) {
107
+ remain = total - avatars.length;
108
+ } else if (max && children.length > max) {
109
+ remain = children.length - max;
110
+ }
111
+
112
+ return remain;
113
+ })();
41
114
 
42
115
  return (
43
116
  <StyledWrapper
44
117
  themeSize={size}
45
118
  themeAvatarCount={avatars.length}
119
+ themeHasSurplus={remainingAvatar > 0}
46
120
  style={style}
47
121
  testID={testID}
122
+ themeVariant={variant}
48
123
  >
49
124
  {avatars.map((avt, index) => (
50
125
  <StyledAvatar
126
+ themeVariant={variant}
51
127
  {...avt.props}
52
128
  size={size}
53
129
  themeIndex={index}
54
130
  style={{ backgroundColor: colors[index % colors.length] }}
55
131
  />
56
132
  ))}
133
+
134
+ <Surplus
135
+ value={remainingAvatar}
136
+ index={avatars.length}
137
+ size={size}
138
+ variant={variant}
139
+ renderSurplus={renderSurplus}
140
+ backgroundColor={colors[avatars.length % colors.length]}
141
+ />
57
142
  </StyledWrapper>
58
143
  );
59
144
  };
@@ -2,22 +2,25 @@ import React, {
2
2
  Dispatch,
3
3
  SetStateAction,
4
4
  useCallback,
5
- useRef,
6
5
  useEffect,
6
+ useRef,
7
7
  useState,
8
8
  } from 'react';
9
9
  import {
10
10
  Animated,
11
11
  FlatList,
12
12
  StyleProp,
13
- useWindowDimensions,
14
13
  View,
15
14
  ViewProps,
16
15
  ViewStyle,
17
16
  ViewToken,
17
+ useWindowDimensions,
18
18
  } from 'react-native';
19
19
 
20
- import { CarouselData } from './types';
20
+ import { useTheme } from '../../theme';
21
+ import { useDeprecation } from '../../utils/hooks';
22
+ import { CardCarousel } from './CardCarousel';
23
+ import CarouselItem from './CarouselItem';
21
24
  import {
22
25
  StyledBackDrop,
23
26
  StyledCarouselFooterWrapper,
@@ -25,9 +28,7 @@ import {
25
28
  StyledPageControl,
26
29
  StyledPageControlWrapper,
27
30
  } from './StyledCarousel';
28
- import CarouselItem from './CarouselItem';
29
- import { CardCarousel } from './CardCarousel';
30
- import { useDeprecation } from '../../utils/hooks';
31
+ import { CarouselData } from './types';
31
32
 
32
33
  interface CarouselProps extends ViewProps {
33
34
  /**
@@ -98,6 +99,8 @@ const Carousel = ({
98
99
  `The use of 'pageControlPosition == bottom' has been deprecated`,
99
100
  pageControlPosition === 'bottom'
100
101
  );
102
+
103
+ const theme = useTheme();
101
104
  const carouselRef = useRef<FlatList>(null);
102
105
 
103
106
  const [currentSlideIndex, setCurrentSlideIndex] =
@@ -130,7 +133,8 @@ const Carousel = ({
130
133
  }, [currentSlideIndex, carouselRef]);
131
134
 
132
135
  const viewConfig = useRef({ viewAreaCoveragePercentThreshold: 50 }).current;
133
- const onViewCallBack = React.useCallback(
136
+
137
+ const onViewCallBack = useRef(
134
138
  (info: { viewableItems: Array<ViewToken>; changed: Array<ViewToken> }) => {
135
139
  const firstVisibleItem = info.viewableItems.find(
136
140
  (view) => view.index != null && view.isViewable
@@ -138,13 +142,16 @@ const Carousel = ({
138
142
  if (firstVisibleItem) {
139
143
  internalOnItemIndexChange(firstVisibleItem.index || 0);
140
144
  }
141
- },
142
- [internalOnItemIndexChange]
145
+ }
143
146
  );
147
+
144
148
  return (
145
149
  <View style={style} testID={testID} {...nativeProps}>
146
150
  <StyledBackDrop
147
- themeSlideBackground={items[currentSlideIndex].background}
151
+ themeSlideBackground={
152
+ items[currentSlideIndex]?.background ||
153
+ theme.colors.defaultGlobalSurface
154
+ }
148
155
  />
149
156
 
150
157
  <StyledPageControlWrapper>
@@ -165,7 +172,7 @@ const Carousel = ({
165
172
  pagingEnabled
166
173
  bounces={false}
167
174
  data={items}
168
- onViewableItemsChanged={onViewCallBack}
175
+ onViewableItemsChanged={onViewCallBack.current}
169
176
  viewabilityConfig={viewConfig}
170
177
  scrollEventThrottle={32}
171
178
  ref={carouselRef}
@@ -1,12 +1,15 @@
1
1
  /// <reference types="react" />
2
2
  import { View } from 'react-native';
3
3
  import { AvatarProps } from '../Avatar';
4
+ type ThemeVariant = 'horizontal' | 'vertical';
4
5
  export declare const StyledWrapper: import("@emotion/native").StyledComponent<import("react-native").ViewProps & {
5
6
  theme?: import("@emotion/react").Theme | undefined;
6
7
  as?: import("react").ElementType<any> | undefined;
7
8
  } & {
8
9
  themeSize: Required<AvatarProps>['size'];
9
10
  themeAvatarCount: number;
11
+ themeVariant: ThemeVariant;
12
+ themeHasSurplus: boolean;
10
13
  }, {}, {
11
14
  ref?: import("react").Ref<View> | undefined;
12
15
  }>;
@@ -15,4 +18,14 @@ export declare const StyledAvatar: import("@emotion/native").StyledComponent<Ava
15
18
  as?: import("react").ElementType<any> | undefined;
16
19
  } & {
17
20
  themeIndex: number;
21
+ themeVariant: ThemeVariant;
18
22
  }, {}, {}>;
23
+ export declare const StyledSurplusContainer: import("@emotion/native").StyledComponent<import("../../Box").BoxProps & {
24
+ theme?: import("@emotion/react").Theme | undefined;
25
+ as?: import("react").ElementType<any> | undefined;
26
+ } & {
27
+ themeIndex: number;
28
+ themeSize: AvatarProps['size'];
29
+ themeVariant: ThemeVariant;
30
+ }, {}, {}>;
31
+ export {};
@@ -1,4 +1,4 @@
1
- import React, { ReactElement } from 'react';
1
+ import React, { ReactElement, ReactNode } from 'react';
2
2
  import { StyleProp, ViewStyle } from 'react-native';
3
3
  import { AvatarProps } from '../Avatar';
4
4
  export interface AvatarStackProps extends Pick<AvatarProps, 'size'> {
@@ -10,6 +10,14 @@ export interface AvatarStackProps extends Pick<AvatarProps, 'size'> {
10
10
  * Max avatars to show.
11
11
  */
12
12
  max?: number;
13
+ /**
14
+ * The total number of avatars. Used for calculating the number of extra avatars.
15
+ */
16
+ total?: number;
17
+ /**
18
+ * Custom renderer of extraAvatars.
19
+ */
20
+ renderSurplus?: (value: number) => ReactNode;
13
21
  /**
14
22
  * Additional style.
15
23
  */
@@ -18,6 +26,10 @@ export interface AvatarStackProps extends Pick<AvatarProps, 'size'> {
18
26
  * Testing id of the component.
19
27
  */
20
28
  testID?: string;
29
+ /**
30
+ * Variant of the avatar stack.
31
+ */
32
+ variant?: 'horizontal' | 'vertical';
21
33
  }
22
- declare const AvatarStack: ({ children, max, size, style, testID, }: AvatarStackProps) => React.JSX.Element;
34
+ declare const AvatarStack: ({ children, max, size, style, testID, variant, total, renderSurplus, }: AvatarStackProps) => React.JSX.Element;
23
35
  export default AvatarStack;
@@ -1,7 +1,7 @@
1
1
  /// <reference types="react" />
2
2
  import { useAvatarColors } from './AvatarStack/utils';
3
3
  declare const _default: (({ onPress, source, testID, style, title, size, intent, }: import("./Avatar").AvatarProps) => import("react").JSX.Element | null) & {
4
- Stack: ({ children, max, size, style, testID, }: import("./AvatarStack").AvatarStackProps) => import("react").JSX.Element;
4
+ Stack: ({ children, max, size, style, testID, variant, total, renderSurplus, }: import("./AvatarStack").AvatarStackProps) => import("react").JSX.Element;
5
5
  };
6
6
  export default _default;
7
7
  export { useAvatarColors };