@hero-design/rn 8.103.7 → 8.104.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 (29) hide show
  1. package/.turbo/turbo-build.log +3 -3
  2. package/CHANGELOG.md +12 -0
  3. package/es/index.js +215 -18
  4. package/lib/index.js +215 -17
  5. package/package.json +1 -1
  6. package/src/components/FilterTrigger/StyledFilterTrigger.tsx +104 -0
  7. package/src/components/FilterTrigger/__tests__/__snapshots__/index.spec.tsx.snap +637 -0
  8. package/src/components/FilterTrigger/__tests__/index.spec.tsx +161 -0
  9. package/src/components/FilterTrigger/index.tsx +106 -0
  10. package/src/components/Icon/HeroIcon/index.tsx +3 -1
  11. package/src/components/Icon/__tests__/__snapshots__/index.spec.tsx.snap +45 -0
  12. package/src/components/Icon/__tests__/index.spec.tsx +1 -0
  13. package/src/components/Icon/index.tsx +2 -1
  14. package/src/components/Toolbar/StyledToolbar.tsx +0 -1
  15. package/src/components/Toolbar/__tests__/__snapshots__/ToolbarMessage.spec.tsx.snap +0 -4
  16. package/src/index.ts +2 -0
  17. package/src/theme/__tests__/__snapshots__/index.spec.ts.snap +71 -0
  18. package/src/theme/components/filterTrigger.ts +88 -0
  19. package/src/theme/components/icon.ts +1 -0
  20. package/src/theme/getTheme.ts +3 -0
  21. package/stats/8.104.0/rn-stats.html +4844 -0
  22. package/types/components/FilterTrigger/StyledFilterTrigger.d.ts +20 -0
  23. package/types/components/FilterTrigger/index.d.ts +39 -0
  24. package/types/components/Icon/HeroIcon/index.d.ts +1 -1
  25. package/types/components/Icon/index.d.ts +1 -1
  26. package/types/index.d.ts +2 -1
  27. package/types/theme/components/filterTrigger.d.ts +72 -0
  28. package/types/theme/components/icon.d.ts +1 -0
  29. package/types/theme/getTheme.d.ts +2 -0
@@ -0,0 +1,161 @@
1
+ import React from 'react';
2
+ import { fireEvent } from '@testing-library/react-native';
3
+ import FilterTrigger, { FilterTriggerProps } from '..';
4
+ import renderWithTheme from '../../../testHelpers/renderWithTheme';
5
+
6
+ describe('FilterTrigger', () => {
7
+ const defaultProps = {
8
+ label: 'Filter',
9
+ onPress: jest.fn(),
10
+ variant: 'filled' as const,
11
+ };
12
+
13
+ describe('Rendering', () => {
14
+ it('should render correctly with basic props', () => {
15
+ const { toJSON, getByText } = renderWithTheme(
16
+ <FilterTrigger {...defaultProps} />
17
+ );
18
+
19
+ expect(getByText('Filter')).toBeVisible();
20
+ expect(toJSON()).toMatchSnapshot();
21
+ });
22
+
23
+ it('should render suffix icon when provided', () => {
24
+ const { getByText, getByTestId } = renderWithTheme(
25
+ <FilterTrigger
26
+ {...defaultProps}
27
+ suffix="filter"
28
+ testID="filter-trigger"
29
+ />
30
+ );
31
+
32
+ expect(getByText('Filter')).toBeVisible();
33
+ expect(getByTestId('filter-trigger-suffix')).toBeVisible();
34
+ });
35
+
36
+ it.each`
37
+ filterCount | active | shouldShowBadge
38
+ ${3} | ${true} | ${true}
39
+ ${3} | ${false} | ${false}
40
+ ${0} | ${true} | ${false}
41
+ ${undefined} | ${true} | ${false}
42
+ `(
43
+ `should $shouldShowBadge ? "show" : "not show" badge when filterCount is $filterCount and active is $active`,
44
+ ({ filterCount, active, shouldShowBadge }) => {
45
+ const props: FilterTriggerProps = { ...defaultProps, active };
46
+ if (filterCount !== undefined) {
47
+ props.filterCount = filterCount;
48
+ }
49
+
50
+ const { getByText, queryByText } = renderWithTheme(
51
+ <FilterTrigger {...props} />
52
+ );
53
+
54
+ if (shouldShowBadge) {
55
+ const badge = getByText(filterCount!.toString());
56
+ expect(badge).toBeVisible();
57
+ } else {
58
+ expect(queryByText(filterCount?.toString() || '0')).toBeNull();
59
+ }
60
+ }
61
+ );
62
+
63
+ it.each`
64
+ variant
65
+ ${'filled'}
66
+ ${'outlined'}
67
+ ${'ghost'}
68
+ `(`should render $variant variant correctly`, (variant) => {
69
+ const { toJSON, getByText } = renderWithTheme(
70
+ <FilterTrigger {...defaultProps} variant={variant} />
71
+ );
72
+
73
+ expect(getByText('Filter')).toBeVisible();
74
+ expect(toJSON()).toMatchSnapshot();
75
+ });
76
+
77
+ it.each`
78
+ active
79
+ ${true}
80
+ ${false}
81
+ `(`should render correctly when active is $active`, (active) => {
82
+ const { toJSON, getByText } = renderWithTheme(
83
+ <FilterTrigger {...defaultProps} active={active} />
84
+ );
85
+
86
+ expect(getByText('Filter')).toBeVisible();
87
+ expect(toJSON()).toMatchSnapshot();
88
+ });
89
+
90
+ it.each`
91
+ label | active | filterCount | suffix | variant | expectedTexts
92
+ ${'Advanced Filter'} | ${true} | ${7} | ${'filter'} | ${'outlined'} | ${['Advanced Filter', '7']}
93
+ ${'Simple Filter'} | ${true} | ${2} | ${undefined} | ${undefined} | ${['Simple Filter', '2']}
94
+ ${undefined} | ${true} | ${4} | ${'filter'} | ${undefined} | ${['4']}
95
+ `(
96
+ `should render correctly with complex props: $label || no label, $filterCount count, $suffix || no suffix`,
97
+ ({ label, active, filterCount, suffix, variant, expectedTexts }) => {
98
+ const props: FilterTriggerProps = {
99
+ ...defaultProps,
100
+ active,
101
+ filterCount,
102
+ };
103
+ if (label) props.label = label;
104
+ if (suffix) props.suffix = suffix;
105
+ if (variant) props.variant = variant;
106
+
107
+ const { getByText } = renderWithTheme(<FilterTrigger {...props} />);
108
+
109
+ expectedTexts.forEach((text: string) => {
110
+ const textElement = getByText(text);
111
+ expect(textElement).toBeVisible();
112
+ });
113
+ }
114
+ );
115
+
116
+ it('should render correctly without label', () => {
117
+ const { queryByText, getByText } = renderWithTheme(
118
+ <FilterTrigger
119
+ {...defaultProps}
120
+ active
121
+ filterCount={3}
122
+ testID="labeless-filter"
123
+ label={undefined}
124
+ />
125
+ );
126
+
127
+ // Should not render the label text
128
+ expect(queryByText('Filter')).toBeNull();
129
+ // Should still show the badge
130
+ expect(getByText('3')).toBeVisible();
131
+ });
132
+
133
+ it('should render suffix icon when no label', () => {
134
+ const { getByTestId } = renderWithTheme(
135
+ <FilterTrigger
136
+ {...defaultProps}
137
+ suffix="filter"
138
+ testID="labeless-suffix"
139
+ label={undefined}
140
+ />
141
+ );
142
+
143
+ const suffixIcon = getByTestId('labeless-suffix-suffix');
144
+ expect(suffixIcon).toBeVisible();
145
+ });
146
+ });
147
+
148
+ describe('Interaction', () => {
149
+ it('should call onPress when pressed', () => {
150
+ const onPress = jest.fn();
151
+ const { getByText } = renderWithTheme(
152
+ <FilterTrigger {...defaultProps} onPress={onPress} />
153
+ );
154
+
155
+ const filterTrigger = getByText('Filter');
156
+ fireEvent.press(filterTrigger);
157
+
158
+ expect(onPress).toHaveBeenCalledTimes(1);
159
+ });
160
+ });
161
+ });
@@ -0,0 +1,106 @@
1
+ import React from 'react';
2
+ import { StyleProp, ViewStyle } from 'react-native';
3
+ import Badge from '../Badge';
4
+ import type { IconName } from '../Icon';
5
+ import Icon from '../Icon';
6
+ import {
7
+ StyledBadge,
8
+ StyledFilterWrapper,
9
+ StyledText,
10
+ } from './StyledFilterTrigger';
11
+
12
+ export interface FilterTriggerProps {
13
+ /**
14
+ * The label of the FilterTrigger.
15
+ */
16
+ label?: string;
17
+ /**
18
+ * Whether the filter is currently active
19
+ */
20
+ active?: boolean;
21
+ /**
22
+ * The number of active filters
23
+ */
24
+ filterCount?: number;
25
+ /**
26
+ * The testID of the FilterTrigger.
27
+ */
28
+ testID?: string;
29
+ /**
30
+ * The style of the FilterTrigger.
31
+ */
32
+ style?: StyleProp<ViewStyle>;
33
+ /**
34
+ * The onPress callback
35
+ */
36
+ onPress?: () => void;
37
+ /**
38
+ * The variant of the FilterTrigger.
39
+ */
40
+ variant?: 'filled' | 'outlined' | 'ghost';
41
+ /**
42
+ * The suffix of the FilterTrigger.
43
+ */
44
+ suffix?: IconName;
45
+ }
46
+
47
+ const FilterTrigger = ({
48
+ label,
49
+ active = false,
50
+ filterCount = 0,
51
+ variant = 'filled',
52
+ suffix,
53
+ onPress,
54
+ testID,
55
+ style,
56
+ }: FilterTriggerProps) => {
57
+ const shouldShowBadge = filterCount > 0 && active;
58
+ const iconIntent = active ? 'text' : 'inactive';
59
+ const typographyVariant = active ? 'small-bold' : 'small';
60
+
61
+ return (
62
+ <StyledFilterWrapper
63
+ testID={testID}
64
+ style={style}
65
+ themeActive={active}
66
+ themeVariant={variant}
67
+ onPress={onPress}
68
+ themeHasLabel={!!label}
69
+ >
70
+ {label ? (
71
+ <>
72
+ <StyledText variant={typographyVariant}>{label}</StyledText>
73
+ {shouldShowBadge && <Badge content={filterCount} size="small" />}
74
+ {suffix && (
75
+ <Icon
76
+ icon={suffix}
77
+ size="xxxsmall"
78
+ intent={iconIntent}
79
+ testID={`${testID}-suffix`}
80
+ />
81
+ )}
82
+ </>
83
+ ) : (
84
+ <>
85
+ {suffix && (
86
+ <Icon
87
+ icon={suffix}
88
+ size="xsmall"
89
+ intent={iconIntent}
90
+ testID={`${testID}-suffix`}
91
+ />
92
+ )}
93
+ {shouldShowBadge && (
94
+ <StyledBadge
95
+ content={filterCount}
96
+ size="small"
97
+ themeHasLabel={!!label}
98
+ />
99
+ )}
100
+ </>
101
+ )}
102
+ </StyledFilterWrapper>
103
+ );
104
+ };
105
+
106
+ export default FilterTrigger;
@@ -19,7 +19,8 @@ type ThemeIntent =
19
19
  | 'warning'
20
20
  | 'disabled-text'
21
21
  | 'text-inverted'
22
- | 'muted';
22
+ | 'muted'
23
+ | 'inactive';
23
24
 
24
25
  const HeroIcon = createIconSet(
25
26
  glyphMap,
@@ -38,6 +39,7 @@ const COLOR_INTENTS = {
38
39
  'disabled-text': 'disabledText',
39
40
  'text-inverted': 'invertedText',
40
41
  muted: 'muted',
42
+ inactive: 'inactive',
41
43
  } as const;
42
44
 
43
45
  const StyledHeroIcon = styled(HeroIcon)<{
@@ -597,3 +597,48 @@ exports[`Icon renders correctly with intent 10`] = `
597
597
  />
598
598
  </View>
599
599
  `;
600
+
601
+ exports[`Icon renders correctly with intent 11`] = `
602
+ <View
603
+ style={
604
+ {
605
+ "flex": 1,
606
+ }
607
+ }
608
+ >
609
+ <HeroIcon
610
+ name="home"
611
+ style={
612
+ [
613
+ {
614
+ "color": "#808f91",
615
+ "fontSize": 24,
616
+ },
617
+ undefined,
618
+ ]
619
+ }
620
+ themeIntent="inactive"
621
+ themeSize="medium"
622
+ />
623
+ <View
624
+ pointerEvents="box-none"
625
+ position="bottom"
626
+ style={
627
+ [
628
+ {
629
+ "bottom": 0,
630
+ "elevation": 9999,
631
+ "flexDirection": "column-reverse",
632
+ "left": 0,
633
+ "paddingHorizontal": 24,
634
+ "paddingVertical": 16,
635
+ "position": "absolute",
636
+ "right": 0,
637
+ "top": 0,
638
+ },
639
+ undefined,
640
+ ]
641
+ }
642
+ />
643
+ </View>
644
+ `;
@@ -21,6 +21,7 @@ describe('Icon', () => {
21
21
  ${'disabled-text'}
22
22
  ${'text-inverted'}
23
23
  ${'muted'}
24
+ ${'inactive'}
24
25
  `('renders correctly with intent', ({ intent }) => {
25
26
  const { toJSON } = renderWithTheme(<Icon icon="home" intent={intent} />);
26
27
 
@@ -25,7 +25,8 @@ export interface IconProps extends AccessibilityProps {
25
25
  | 'warning'
26
26
  | 'disabled-text'
27
27
  | 'text-inverted'
28
- | 'muted';
28
+ | 'muted'
29
+ | 'inactive';
29
30
  /**
30
31
  * Size of the Icon.
31
32
  */
@@ -90,7 +90,6 @@ export const StyledInputContainer = styled(View)(({ theme }) => {
90
90
  borderRadius: theme.__hd__.toolbar.radii.messageContainer,
91
91
  height: theme.__hd__.toolbar.sizes.messageInputHeight,
92
92
  paddingHorizontal: theme.__hd__.toolbar.space.messageInputPaddingHorizontal,
93
- paddingVertical: theme.__hd__.toolbar.space.messageInputPaddingVertical,
94
93
  };
95
94
  });
96
95
 
@@ -37,7 +37,6 @@ exports[`ToolbarMessage disabled renders correctly 1`] = `
37
37
  "flexDirection": "row",
38
38
  "height": 40,
39
39
  "paddingHorizontal": 12,
40
- "paddingVertical": 8,
41
40
  },
42
41
  undefined,
43
42
  ]
@@ -128,7 +127,6 @@ exports[`ToolbarMessage filled renders correctly 1`] = `
128
127
  "flexDirection": "row",
129
128
  "height": 40,
130
129
  "paddingHorizontal": 12,
131
- "paddingVertical": 8,
132
130
  },
133
131
  undefined,
134
132
  ]
@@ -232,7 +230,6 @@ exports[`ToolbarMessage idle renders correctly 1`] = `
232
230
  "flexDirection": "row",
233
231
  "height": 40,
234
232
  "paddingHorizontal": 12,
235
- "paddingVertical": 8,
236
233
  },
237
234
  undefined,
238
235
  ]
@@ -335,7 +332,6 @@ exports[`ToolbarMessage readonly renders correctly 1`] = `
335
332
  "flexDirection": "row",
336
333
  "height": 40,
337
334
  "paddingHorizontal": 12,
338
- "paddingVertical": 8,
339
335
  },
340
336
  undefined,
341
337
  ]
package/src/index.ts CHANGED
@@ -79,6 +79,7 @@ import {
79
79
  import Search from './components/Search';
80
80
  import FloatingIsland from './components/FloatingIsland';
81
81
  import LocaleProvider from './components/LocaleProvider';
82
+ import FilterTrigger from './components/FilterTrigger';
82
83
 
83
84
  export {
84
85
  theme,
@@ -158,6 +159,7 @@ export {
158
159
  RichTextEditor,
159
160
  FloatingIsland,
160
161
  LocaleProvider,
162
+ FilterTrigger,
161
163
  styled,
162
164
  };
163
165
 
@@ -702,6 +702,76 @@ exports[`theme returns correct theme object 1`] = `
702
702
  "titleMarginHorizontal": 8,
703
703
  },
704
704
  },
705
+ "filterTrigger": {
706
+ "borderWidths": {
707
+ "wrapper": {
708
+ "filled": 2,
709
+ "ghost": 0,
710
+ "outlined": 2,
711
+ },
712
+ },
713
+ "colors": {
714
+ "wrapper": {
715
+ "activeBackground": "#ece8ef",
716
+ "background": {
717
+ "active": {
718
+ "filled": "#ece8ef",
719
+ "filledLabeless": "#f6f6f7",
720
+ "ghost": "transparent",
721
+ "outlined": "transparent",
722
+ },
723
+ "inactive": {
724
+ "filled": "#f6f6f7",
725
+ "ghost": "transparent",
726
+ "outlined": "transparent",
727
+ },
728
+ },
729
+ "border": {
730
+ "active": {
731
+ "filled": "#ece8ef",
732
+ "filledLabeless": "#f6f6f7",
733
+ "ghost": "transparent",
734
+ "outlined": "#001f23",
735
+ },
736
+ "inactive": {
737
+ "filled": "#f6f6f7",
738
+ "ghost": "transparent",
739
+ "outlined": "#e8e9ea",
740
+ },
741
+ },
742
+ "inactiveBackground": "#f6f6f7",
743
+ },
744
+ },
745
+ "lineHeights": {
746
+ "text": 16,
747
+ },
748
+ "radii": {
749
+ "wrapper": {
750
+ "default": 24,
751
+ "labeless": 16,
752
+ },
753
+ },
754
+ "sizes": {
755
+ "wrapperHeight": 36,
756
+ },
757
+ "space": {
758
+ "badge": {
759
+ "labelessBottom": 2,
760
+ "labelessRight": 2,
761
+ },
762
+ "wrapper": {
763
+ "default": {
764
+ "paddingHorizontal": 12,
765
+ "paddingVertical": 4,
766
+ },
767
+ "itemGap": 4,
768
+ "labeless": {
769
+ "paddingHorizontal": 8,
770
+ "paddingVertical": 4,
771
+ },
772
+ },
773
+ },
774
+ },
705
775
  "floatingIsland": {
706
776
  "colors": {
707
777
  "wrapperBackground": "#ffffff",
@@ -736,6 +806,7 @@ exports[`theme returns correct theme object 1`] = `
736
806
  "colors": {
737
807
  "danger": "#cb300a",
738
808
  "disabledText": "#bfc1c5",
809
+ "inactive": "#808f91",
739
810
  "info": "#b5c3fd",
740
811
  "invertedText": "#ffffff",
741
812
  "muted": "#4d6265",
@@ -0,0 +1,88 @@
1
+ import { scale } from '../../utils/scale';
2
+ import type { GlobalTheme } from '../global';
3
+
4
+ const getFilterTriggerTheme = (theme: GlobalTheme) => {
5
+ const borderWidths = {
6
+ wrapper: {
7
+ filled: theme.borderWidths.medium,
8
+ outlined: theme.borderWidths.medium,
9
+ ghost: 0,
10
+ },
11
+ };
12
+ const colors = {
13
+ wrapper: {
14
+ activeBackground: theme.colors.highlightedSurface,
15
+ inactiveBackground: theme.colors.neutralGlobalSurface,
16
+ background: {
17
+ active: {
18
+ filled: theme.colors.highlightedSurface,
19
+ outlined: 'transparent',
20
+ ghost: 'transparent',
21
+ filledLabeless: theme.colors.neutralGlobalSurface,
22
+ },
23
+ inactive: {
24
+ filled: theme.colors.neutralGlobalSurface,
25
+ outlined: 'transparent',
26
+ ghost: 'transparent',
27
+ },
28
+ },
29
+ border: {
30
+ active: {
31
+ filled: theme.colors.highlightedSurface,
32
+ outlined: theme.colors.primaryOutline,
33
+ ghost: 'transparent',
34
+ filledLabeless: theme.colors.neutralGlobalSurface,
35
+ },
36
+ inactive: {
37
+ filled: theme.colors.neutralGlobalSurface,
38
+ outlined: theme.colors.secondaryOutline,
39
+ ghost: 'transparent',
40
+ },
41
+ },
42
+ },
43
+ };
44
+
45
+ const space = {
46
+ wrapper: {
47
+ default: {
48
+ paddingHorizontal: theme.space.smallMedium,
49
+ paddingVertical: theme.space.xsmall,
50
+ },
51
+ labeless: {
52
+ paddingHorizontal: theme.space.small,
53
+ paddingVertical: theme.space.xsmall,
54
+ },
55
+ itemGap: theme.space.xsmall,
56
+ },
57
+ badge: {
58
+ labelessRight: theme.space.xxsmall,
59
+ labelessBottom: theme.space.xxsmall,
60
+ },
61
+ };
62
+
63
+ const radii = {
64
+ wrapper: {
65
+ default: theme.radii.xxxlarge,
66
+ labeless: theme.radii.xlarge,
67
+ },
68
+ };
69
+
70
+ const sizes = {
71
+ wrapperHeight: scale(36),
72
+ };
73
+
74
+ const lineHeights = {
75
+ text: theme.lineHeights.small,
76
+ };
77
+
78
+ return {
79
+ colors,
80
+ space,
81
+ radii,
82
+ borderWidths,
83
+ sizes,
84
+ lineHeights,
85
+ };
86
+ };
87
+
88
+ export default getFilterTriggerTheme;
@@ -12,6 +12,7 @@ const getIconTheme = (theme: GlobalTheme) => {
12
12
  disabledText: theme.colors.disabledOnDefaultGlobalSurface,
13
13
  invertedText: theme.colors.onDarkGlobalSurface,
14
14
  muted: theme.colors.mutedOnDefaultGlobalSurface,
15
+ inactive: theme.colors.inactiveOnDefaultGlobalSurface,
15
16
  };
16
17
 
17
18
  const sizes = {
@@ -55,6 +55,7 @@ import getMapPinTheme from './components/mapPin';
55
55
  import getFloatingIslandTheme from './components/floatingIsland';
56
56
  import getAppCueTheme from './components/appCue';
57
57
  import { ThemeMode } from './global/colors/types';
58
+ import getFilterTriggerTheme from './components/filterTrigger';
58
59
 
59
60
  type Theme = GlobalTheme & {
60
61
  themeMode?: ThemeMode;
@@ -84,6 +85,7 @@ type Theme = GlobalTheme & {
84
85
  empty: EmptyThemeType;
85
86
  error: ErrorThemeType;
86
87
  fab: FABThemeType;
88
+ filterTrigger: ReturnType<typeof getFilterTriggerTheme>;
87
89
  icon: ReturnType<typeof getIconTheme>;
88
90
  image: ReturnType<typeof getImageTheme>;
89
91
  list: ReturnType<typeof getListTheme>;
@@ -148,6 +150,7 @@ const getTheme = (
148
150
  empty: getEmptyTheme(globalTheme),
149
151
  error: getErrorTheme(globalTheme),
150
152
  fab: getFABTheme(globalTheme),
153
+ filterTrigger: getFilterTriggerTheme(globalTheme),
151
154
  icon: getIconTheme(globalTheme),
152
155
  image: getImageTheme(globalTheme),
153
156
  list: getListTheme(globalTheme),