@utilitywarehouse/hearth-react-native 0.27.2 → 0.28.0-testid-fix-1

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 (155) hide show
  1. package/.turbo/turbo-build.log +5 -4
  2. package/.turbo/turbo-lint.log +70 -69
  3. package/CHANGELOG.md +149 -0
  4. package/build/components/Button/ButtonRoot.js +8 -0
  5. package/build/components/Combobox/Combobox.context.d.ts +13 -0
  6. package/build/components/Combobox/Combobox.context.js +9 -0
  7. package/build/components/Combobox/Combobox.d.ts +6 -0
  8. package/build/components/Combobox/Combobox.js +246 -0
  9. package/build/components/Combobox/Combobox.props.d.ts +180 -0
  10. package/build/components/Combobox/Combobox.props.js +1 -0
  11. package/build/components/Combobox/ComboboxOption.d.ts +6 -0
  12. package/build/components/Combobox/ComboboxOption.js +56 -0
  13. package/build/components/Combobox/index.d.ts +4 -0
  14. package/build/components/Combobox/index.js +3 -0
  15. package/build/components/DatePicker/TimePicker.d.ts +3 -0
  16. package/build/components/DatePicker/TimePicker.js +84 -0
  17. package/build/components/DatePicker/time-picker/animated-math.d.ts +4 -0
  18. package/build/components/DatePicker/time-picker/animated-math.js +19 -0
  19. package/build/components/DatePicker/time-picker/period-native.d.ts +6 -0
  20. package/build/components/DatePicker/time-picker/period-native.js +17 -0
  21. package/build/components/DatePicker/time-picker/period-picker.d.ts +6 -0
  22. package/build/components/DatePicker/time-picker/period-picker.js +10 -0
  23. package/build/components/DatePicker/time-picker/period-web.d.ts +6 -0
  24. package/build/components/DatePicker/time-picker/period-web.js +21 -0
  25. package/build/components/DatePicker/time-picker/wheel-native.d.ts +8 -0
  26. package/build/components/DatePicker/time-picker/wheel-native.js +19 -0
  27. package/build/components/DatePicker/time-picker/wheel-picker/index.d.ts +2 -0
  28. package/build/components/DatePicker/time-picker/wheel-picker/index.js +2 -0
  29. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.d.ts +16 -0
  30. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.js +97 -0
  31. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.d.ts +21 -0
  32. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.js +88 -0
  33. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.d.ts +23 -0
  34. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.js +21 -0
  35. package/build/components/DatePicker/time-picker/wheel-web.d.ts +8 -0
  36. package/build/components/DatePicker/time-picker/wheel-web.js +146 -0
  37. package/build/components/DatePicker/time-picker/wheel.d.ts +8 -0
  38. package/build/components/DatePicker/time-picker/wheel.js +10 -0
  39. package/build/components/List/List.js +2 -2
  40. package/build/components/Modal/Modal.js +31 -42
  41. package/build/components/Modal/Modal.web.js +3 -3
  42. package/build/components/Pagination/Pagination.d.ts +6 -0
  43. package/build/components/Pagination/Pagination.js +125 -0
  44. package/build/components/Pagination/Pagination.props.d.ts +26 -0
  45. package/build/components/Pagination/Pagination.props.js +1 -0
  46. package/build/components/Pagination/Pagination.utils.d.ts +2 -0
  47. package/build/components/Pagination/Pagination.utils.js +20 -0
  48. package/build/components/Pagination/Pagination.utils.test.d.ts +1 -0
  49. package/build/components/Pagination/Pagination.utils.test.js +16 -0
  50. package/build/components/Pagination/index.d.ts +2 -0
  51. package/build/components/Pagination/index.js +1 -0
  52. package/build/components/SafeAreaView/SafeAreaView.d.ts +5 -0
  53. package/build/components/SafeAreaView/SafeAreaView.js +117 -0
  54. package/build/components/SafeAreaView/SafeAreaView.props.d.ts +17 -0
  55. package/build/components/SafeAreaView/SafeAreaView.props.js +1 -0
  56. package/build/components/SafeAreaView/index.d.ts +2 -0
  57. package/build/components/SafeAreaView/index.js +1 -0
  58. package/build/components/Select/Select.d.ts +1 -1
  59. package/build/components/Select/Select.js +6 -5
  60. package/build/components/Select/Select.props.d.ts +4 -0
  61. package/build/components/Select/SelectOption.d.ts +1 -1
  62. package/build/components/Select/SelectOption.js +2 -2
  63. package/build/components/Table/Table.context.d.ts +12 -0
  64. package/build/components/Table/Table.context.js +9 -0
  65. package/build/components/Table/Table.d.ts +6 -0
  66. package/build/components/Table/Table.js +71 -0
  67. package/build/components/Table/Table.props.d.ts +56 -0
  68. package/build/components/Table/Table.props.js +1 -0
  69. package/build/components/Table/Table.utils.d.ts +5 -0
  70. package/build/components/Table/Table.utils.js +48 -0
  71. package/build/components/Table/Table.utils.test.d.ts +1 -0
  72. package/build/components/Table/Table.utils.test.js +71 -0
  73. package/build/components/Table/TableBody.d.ts +6 -0
  74. package/build/components/Table/TableBody.js +16 -0
  75. package/build/components/Table/TableCell.d.ts +10 -0
  76. package/build/components/Table/TableCell.js +44 -0
  77. package/build/components/Table/TableHeader.d.ts +6 -0
  78. package/build/components/Table/TableHeader.js +24 -0
  79. package/build/components/Table/TableHeaderCell.d.ts +10 -0
  80. package/build/components/Table/TableHeaderCell.js +97 -0
  81. package/build/components/Table/TablePagination.d.ts +6 -0
  82. package/build/components/Table/TablePagination.js +7 -0
  83. package/build/components/Table/TableRow.d.ts +8 -0
  84. package/build/components/Table/TableRow.js +25 -0
  85. package/build/components/Table/index.d.ts +8 -0
  86. package/build/components/Table/index.js +7 -0
  87. package/build/components/Timeline/Timeline.d.ts +6 -0
  88. package/build/components/Timeline/Timeline.js +34 -0
  89. package/build/components/Timeline/Timeline.props.d.ts +47 -0
  90. package/build/components/Timeline/Timeline.props.js +1 -0
  91. package/build/components/Timeline/TimelineItem.d.ts +6 -0
  92. package/build/components/Timeline/TimelineItem.js +235 -0
  93. package/build/components/Timeline/index.d.ts +3 -0
  94. package/build/components/Timeline/index.js +2 -0
  95. package/build/components/VerificationInput/VerificationInput.js +3 -3
  96. package/build/components/index.d.ts +5 -0
  97. package/build/components/index.js +5 -0
  98. package/build/tokens/components/dark/timeline.d.ts +2 -2
  99. package/build/tokens/components/dark/timeline.js +2 -2
  100. package/docs/components/AllComponents.web.tsx +106 -23
  101. package/docs/llm-docs/unistyles-llms-full.txt +1132 -534
  102. package/docs/llm-docs/unistyles-llms-small.txt +37 -37
  103. package/package.json +4 -4
  104. package/src/components/Button/Button.stories.tsx +43 -7
  105. package/src/components/Button/ButtonRoot.tsx +8 -0
  106. package/src/components/Combobox/Combobox.context.ts +26 -0
  107. package/src/components/Combobox/Combobox.docs.mdx +277 -0
  108. package/src/components/Combobox/Combobox.figma.tsx +60 -0
  109. package/src/components/Combobox/Combobox.props.ts +187 -0
  110. package/src/components/Combobox/Combobox.stories.tsx +233 -0
  111. package/src/components/Combobox/Combobox.tsx +446 -0
  112. package/src/components/Combobox/ComboboxOption.tsx +100 -0
  113. package/src/components/Combobox/index.ts +9 -0
  114. package/src/components/List/List.tsx +5 -4
  115. package/src/components/Modal/Modal.tsx +67 -74
  116. package/src/components/Modal/Modal.web.tsx +3 -3
  117. package/src/components/Pagination/Pagination.docs.mdx +99 -0
  118. package/src/components/Pagination/Pagination.figma.tsx +20 -0
  119. package/src/components/Pagination/Pagination.props.ts +28 -0
  120. package/src/components/Pagination/Pagination.stories.tsx +88 -0
  121. package/src/components/Pagination/Pagination.tsx +248 -0
  122. package/src/components/Pagination/Pagination.utils.test.ts +20 -0
  123. package/src/components/Pagination/Pagination.utils.ts +37 -0
  124. package/src/components/Pagination/index.ts +2 -0
  125. package/src/components/SafeAreaView/SafeAreaView.props.ts +20 -0
  126. package/src/components/SafeAreaView/SafeAreaView.tsx +173 -0
  127. package/src/components/SafeAreaView/index.ts +2 -0
  128. package/src/components/Select/Select.props.ts +4 -0
  129. package/src/components/Select/Select.tsx +35 -28
  130. package/src/components/Select/SelectOption.tsx +2 -0
  131. package/src/components/Table/Table.context.tsx +23 -0
  132. package/src/components/Table/Table.docs.mdx +239 -0
  133. package/src/components/Table/Table.figma.tsx +65 -0
  134. package/src/components/Table/Table.props.ts +65 -0
  135. package/src/components/Table/Table.stories.tsx +399 -0
  136. package/src/components/Table/Table.tsx +127 -0
  137. package/src/components/Table/Table.utils.test.ts +82 -0
  138. package/src/components/Table/Table.utils.ts +72 -0
  139. package/src/components/Table/TableBody.tsx +25 -0
  140. package/src/components/Table/TableCell.tsx +67 -0
  141. package/src/components/Table/TableHeader.tsx +41 -0
  142. package/src/components/Table/TableHeaderCell.tsx +136 -0
  143. package/src/components/Table/TablePagination.tsx +10 -0
  144. package/src/components/Table/TableRow.tsx +42 -0
  145. package/src/components/Table/index.ts +16 -0
  146. package/src/components/Timeline/Timeline.docs.mdx +177 -0
  147. package/src/components/Timeline/Timeline.figma.tsx +89 -0
  148. package/src/components/Timeline/Timeline.props.ts +51 -0
  149. package/src/components/Timeline/Timeline.stories.tsx +102 -0
  150. package/src/components/Timeline/Timeline.tsx +48 -0
  151. package/src/components/Timeline/TimelineItem.tsx +293 -0
  152. package/src/components/Timeline/index.ts +9 -0
  153. package/src/components/VerificationInput/VerificationInput.tsx +3 -0
  154. package/src/components/index.ts +5 -0
  155. package/src/tokens/components/dark/timeline.ts +2 -2
@@ -0,0 +1,248 @@
1
+ import {
2
+ ChevronLeftSmallIcon,
3
+ ChevronRightSmallIcon,
4
+ SkipFirstSmallIcon,
5
+ SkipLastSmallIcon,
6
+ } from '@utilitywarehouse/hearth-react-native-icons';
7
+ import { ComponentType, useState } from 'react';
8
+ import { Pressable, View } from 'react-native';
9
+ import { StyleSheet } from 'react-native-unistyles';
10
+ import { BodyText } from '../BodyText';
11
+ import { UnstyledIconButton } from '../UnstyledIconButton';
12
+ import type PaginationProps from './Pagination.props';
13
+ import { ELLIPSIS, generatePageNumbers } from './Pagination.utils';
14
+
15
+ interface PaginationItemProps {
16
+ label: number;
17
+ selected?: boolean;
18
+ onPress: () => void;
19
+ }
20
+
21
+ const PaginationItem = ({ label, selected = false, onPress }: PaginationItemProps) => {
22
+ const [isFocused, setIsFocused] = useState(false);
23
+
24
+ styles.useVariants({ selected });
25
+
26
+ return (
27
+ <Pressable
28
+ accessibilityRole="button"
29
+ accessibilityLabel={`Go to page ${label}`}
30
+ accessibilityState={{ selected }}
31
+ onBlur={() => setIsFocused(false)}
32
+ onFocus={() => setIsFocused(true)}
33
+ onPress={onPress}
34
+ style={({ pressed }) => [
35
+ styles.pageItem,
36
+ pressed && !selected && styles.pageItemPressed,
37
+ isFocused && styles.pageItemFocused,
38
+ ]}
39
+ >
40
+ <BodyText size="md" style={styles.pageItemText}>
41
+ {label}
42
+ </BodyText>
43
+ </Pressable>
44
+ );
45
+ };
46
+
47
+ const PaginationArrowButton = ({
48
+ accessibilityLabel,
49
+ disabled,
50
+ icon,
51
+ onPress,
52
+ }: {
53
+ accessibilityLabel: string;
54
+ disabled: boolean;
55
+ icon: ComponentType;
56
+ onPress: () => void;
57
+ }) => {
58
+ return (
59
+ <UnstyledIconButton
60
+ accessibilityLabel={accessibilityLabel}
61
+ disabled={disabled}
62
+ icon={icon}
63
+ onPress={onPress}
64
+ size="sm"
65
+ style={styles.arrowButton}
66
+ />
67
+ );
68
+ };
69
+
70
+ const Pagination = ({
71
+ currentPage,
72
+ totalPages,
73
+ onPageChange,
74
+ condensed = false,
75
+ hideSkipButtons = false,
76
+ style,
77
+ ...props
78
+ }: PaginationProps) => {
79
+ const pages = generatePageNumbers(currentPage, totalPages);
80
+
81
+ const handleFirst = () => {
82
+ if (currentPage !== 1) {
83
+ onPageChange(1);
84
+ }
85
+ };
86
+
87
+ const handlePrevious = () => {
88
+ if (currentPage > 1) {
89
+ onPageChange(currentPage - 1);
90
+ }
91
+ };
92
+
93
+ const handleNext = () => {
94
+ if (currentPage < totalPages) {
95
+ onPageChange(currentPage + 1);
96
+ }
97
+ };
98
+
99
+ const handleLast = () => {
100
+ if (currentPage !== totalPages) {
101
+ onPageChange(totalPages);
102
+ }
103
+ };
104
+
105
+ return (
106
+ <View accessibilityLabel="Pagination" style={[styles.container, style]} {...props}>
107
+ <View style={styles.controllers}>
108
+ {!hideSkipButtons ? (
109
+ <PaginationArrowButton
110
+ accessibilityLabel="Go to first page"
111
+ disabled={currentPage === 1}
112
+ icon={SkipFirstSmallIcon}
113
+ onPress={handleFirst}
114
+ />
115
+ ) : null}
116
+ <PaginationArrowButton
117
+ accessibilityLabel="Go to previous page"
118
+ disabled={currentPage === 1}
119
+ icon={ChevronLeftSmallIcon}
120
+ onPress={handlePrevious}
121
+ />
122
+ </View>
123
+
124
+ {condensed ? (
125
+ <BodyText size="md">
126
+ Page {currentPage} of {totalPages}
127
+ </BodyText>
128
+ ) : (
129
+ <View style={styles.pages}>
130
+ {pages.map((page, index) => {
131
+ if (page === ELLIPSIS) {
132
+ return (
133
+ <BodyText key={`ellipsis-${index}`} size="md" style={styles.ellipsis}>
134
+ {ELLIPSIS}
135
+ </BodyText>
136
+ );
137
+ }
138
+
139
+ return (
140
+ <PaginationItem
141
+ key={page}
142
+ label={page}
143
+ onPress={() => onPageChange(page)}
144
+ selected={page === currentPage}
145
+ />
146
+ );
147
+ })}
148
+ </View>
149
+ )}
150
+
151
+ <View style={styles.controllers}>
152
+ <PaginationArrowButton
153
+ accessibilityLabel="Go to next page"
154
+ disabled={currentPage === totalPages}
155
+ icon={ChevronRightSmallIcon}
156
+ onPress={handleNext}
157
+ />
158
+ {!hideSkipButtons ? (
159
+ <PaginationArrowButton
160
+ accessibilityLabel="Go to last page"
161
+ disabled={currentPage === totalPages}
162
+ icon={SkipLastSmallIcon}
163
+ onPress={handleLast}
164
+ />
165
+ ) : null}
166
+ </View>
167
+ </View>
168
+ );
169
+ };
170
+
171
+ PaginationItem.displayName = 'PaginationItem';
172
+ Pagination.displayName = 'Pagination';
173
+
174
+ const styles = StyleSheet.create(theme => ({
175
+ container: {
176
+ width: '100%',
177
+ minHeight: theme.components.pagination.item.height,
178
+ flexDirection: 'row',
179
+ alignItems: 'center',
180
+ justifyContent: 'space-between',
181
+ gap: theme.components.pagination.gap,
182
+ },
183
+ controllers: {
184
+ flexDirection: 'row',
185
+ alignItems: 'center',
186
+ gap: theme.components.pagination.gap,
187
+ },
188
+ arrowButton: {
189
+ width: theme.components.pagination.item.width,
190
+ height: theme.components.pagination.item.height,
191
+ },
192
+ pages: {
193
+ flex: 1,
194
+ flexDirection: 'row',
195
+ alignItems: 'center',
196
+ justifyContent: 'center',
197
+ flexWrap: 'wrap',
198
+ gap: theme.components.pagination.gap,
199
+ },
200
+ pageItem: {
201
+ width: theme.components.pagination.item.width,
202
+ height: theme.components.pagination.item.height,
203
+ borderRadius: theme.components.pagination.item.radius,
204
+ alignItems: 'center',
205
+ justifyContent: 'center',
206
+ backgroundColor: 'transparent',
207
+ _web: {
208
+ _hover: {
209
+ backgroundColor: theme.color.interactive.functional.surface.subtle.hover,
210
+ },
211
+ },
212
+ variants: {
213
+ selected: {
214
+ true: {
215
+ backgroundColor: theme.color.interactive.brand.surface.strong.default,
216
+ },
217
+ },
218
+ },
219
+ },
220
+ pageItemPressed: {
221
+ backgroundColor: theme.color.interactive.functional.surface.subtle.active,
222
+ },
223
+ pageItemFocused: {
224
+ outlineWidth: 2,
225
+ outlineOffset: -2,
226
+ outlineColor: theme.color.border.strong,
227
+ outlineStyle: 'solid',
228
+ },
229
+ pageItemText: {
230
+ variants: {
231
+ selected: {
232
+ true: {
233
+ color: theme.color.text.inverted,
234
+ },
235
+ false: {
236
+ color: theme.color.text.primary,
237
+ },
238
+ },
239
+ },
240
+ },
241
+ ellipsis: {
242
+ minWidth: theme.components.pagination.item.width,
243
+ textAlign: 'center',
244
+ color: theme.color.text.primary,
245
+ },
246
+ }));
247
+
248
+ export default Pagination;
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ELLIPSIS, generatePageNumbers } from './Pagination.utils';
3
+
4
+ describe('generatePageNumbers', () => {
5
+ it('returns all pages when the total fits within the visible limit', () => {
6
+ expect(generatePageNumbers(1, 7)).toEqual([1, 2, 3, 4, 5, 6, 7]);
7
+ });
8
+
9
+ it('returns the leading window when the current page is near the start', () => {
10
+ expect(generatePageNumbers(2, 10)).toEqual([1, 2, 3, 4, 5, ELLIPSIS, 10]);
11
+ });
12
+
13
+ it('returns the centered window when the current page is in the middle', () => {
14
+ expect(generatePageNumbers(5, 10)).toEqual([1, ELLIPSIS, 4, 5, 6, ELLIPSIS, 10]);
15
+ });
16
+
17
+ it('returns the trailing window when the current page is near the end', () => {
18
+ expect(generatePageNumbers(9, 10)).toEqual([1, ELLIPSIS, 6, 7, 8, 9, 10]);
19
+ });
20
+ });
@@ -0,0 +1,37 @@
1
+ export const ELLIPSIS = '...';
2
+
3
+ const MAX_VISIBLE_ITEMS = 7;
4
+
5
+ export const generatePageNumbers = (
6
+ currentPage: number,
7
+ totalPages: number
8
+ ): Array<number | typeof ELLIPSIS> => {
9
+ if (totalPages <= MAX_VISIBLE_ITEMS) {
10
+ return Array.from({ length: totalPages }, (_, index) => index + 1);
11
+ }
12
+
13
+ const pages: Array<number | typeof ELLIPSIS> = [];
14
+ const isNearStart = currentPage <= 4;
15
+ const isNearEnd = currentPage > totalPages - 4;
16
+
17
+ if (isNearStart) {
18
+ pages.push(1, 2, 3, 4, 5, ELLIPSIS, totalPages);
19
+ return pages;
20
+ }
21
+
22
+ if (isNearEnd) {
23
+ pages.push(
24
+ 1,
25
+ ELLIPSIS,
26
+ totalPages - 4,
27
+ totalPages - 3,
28
+ totalPages - 2,
29
+ totalPages - 1,
30
+ totalPages
31
+ );
32
+ return pages;
33
+ }
34
+
35
+ pages.push(1, ELLIPSIS, currentPage - 1, currentPage, currentPage + 1, ELLIPSIS, totalPages);
36
+ return pages;
37
+ };
@@ -0,0 +1,2 @@
1
+ export { default as Pagination } from './Pagination';
2
+ export type { default as PaginationProps } from './Pagination.props';
@@ -0,0 +1,20 @@
1
+ import { ViewProps } from 'react-native';
2
+
3
+ export type SafeAreaEdge = 'top' | 'right' | 'bottom' | 'left';
4
+
5
+ interface SafeAreaViewProps extends ViewProps {
6
+ /**
7
+ * Which edges should receive safe area compensation.
8
+ *
9
+ * @default ['top', 'right', 'bottom', 'left']
10
+ */
11
+ edges?: SafeAreaEdge[];
12
+ /**
13
+ * Whether safe area compensation should be applied as padding or margin.
14
+ *
15
+ * @default 'padding'
16
+ */
17
+ mode?: 'padding' | 'margin';
18
+ }
19
+
20
+ export default SafeAreaViewProps;
@@ -0,0 +1,173 @@
1
+ import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ LayoutChangeEvent,
4
+ StyleSheet as RNStyleSheet,
5
+ useWindowDimensions,
6
+ View,
7
+ ViewStyle,
8
+ } from 'react-native';
9
+ import { UnistylesRuntime } from '../../core';
10
+ import SafeAreaViewProps, { SafeAreaEdge } from './SafeAreaView.props';
11
+
12
+ type EdgeInsets = Record<SafeAreaEdge, number>;
13
+
14
+ const DEFAULT_EDGES: SafeAreaEdge[] = ['top', 'right', 'bottom', 'left'];
15
+ const EMPTY_INSETS: EdgeInsets = { top: 0, right: 0, bottom: 0, left: 0 };
16
+ const EDGE_EPSILON = 1;
17
+
18
+ const getNumericStyleValue = (value: unknown): number => {
19
+ return typeof value === 'number' ? value : 0;
20
+ };
21
+
22
+ const getStyleInsetValue = (
23
+ style: ViewStyle | undefined,
24
+ mode: SafeAreaViewProps['mode'],
25
+ edge: SafeAreaEdge
26
+ ): number => {
27
+ const prefix = mode === 'margin' ? 'margin' : 'padding';
28
+
29
+ if (!style) {
30
+ // No style specified at all; treat as zero inset for safe-area calculations.
31
+ return 0;
32
+ }
33
+
34
+ if (edge === 'top') {
35
+ const raw = style[`${prefix}Top`] ?? style[`${prefix}Vertical`] ?? style[prefix];
36
+ if (raw == null) {
37
+ return 0;
38
+ }
39
+ return getNumericStyleValue(raw);
40
+ }
41
+
42
+ if (edge === 'bottom') {
43
+ const raw = style[`${prefix}Bottom`] ?? style[`${prefix}Vertical`] ?? style[prefix];
44
+ if (raw == null) {
45
+ return 0;
46
+ }
47
+ return getNumericStyleValue(raw);
48
+ }
49
+
50
+ if (edge === 'left') {
51
+ const raw = style[`${prefix}Left`] ?? style[`${prefix}Horizontal`] ?? style[prefix];
52
+ if (raw == null) {
53
+ return 0;
54
+ }
55
+ return getNumericStyleValue(raw);
56
+ }
57
+
58
+ const raw = style[`${prefix}Right`] ?? style[`${prefix}Horizontal`] ?? style[prefix];
59
+ if (raw == null) {
60
+ return 0;
61
+ }
62
+ return getNumericStyleValue(raw);
63
+ };
64
+
65
+ const SafeAreaView = forwardRef<View, SafeAreaViewProps>(
66
+ ({ children, edges = DEFAULT_EDGES, mode = 'padding', onLayout, style, ...props }, ref) => {
67
+ const viewRef = useRef<View>(null);
68
+ const { width: windowWidth, height: windowHeight } = useWindowDimensions();
69
+ const [edgeInsets, setEdgeInsets] = useState<EdgeInsets>(EMPTY_INSETS);
70
+
71
+ const flattenedStyle = useMemo(
72
+ () => RNStyleSheet.flatten(style) as ViewStyle | undefined,
73
+ [style]
74
+ );
75
+
76
+ const updateEdgeInsets = useCallback(() => {
77
+ const currentView = viewRef.current;
78
+
79
+ if (!currentView) {
80
+ return;
81
+ }
82
+
83
+ currentView.measureInWindow((x, y, width, height) => {
84
+ const runtimeInsets = UnistylesRuntime.insets ?? EMPTY_INSETS;
85
+ const nextEdgeInsets: EdgeInsets = {
86
+ top: edges.includes('top') ? Math.max(runtimeInsets.top - Math.max(y, 0), 0) : 0,
87
+ right: edges.includes('right')
88
+ ? Math.max(runtimeInsets.right - Math.max(windowWidth - (x + width), 0), 0)
89
+ : 0,
90
+ bottom: edges.includes('bottom')
91
+ ? Math.max(runtimeInsets.bottom - Math.max(windowHeight - (y + height), 0), 0)
92
+ : 0,
93
+ left: edges.includes('left') ? Math.max(runtimeInsets.left - Math.max(x, 0), 0) : 0,
94
+ };
95
+
96
+ setEdgeInsets(previousInsets => {
97
+ const hasChanged = (Object.keys(nextEdgeInsets) as SafeAreaEdge[]).some(
98
+ edge => Math.abs(previousInsets[edge] - nextEdgeInsets[edge]) > EDGE_EPSILON
99
+ );
100
+
101
+ return hasChanged ? nextEdgeInsets : previousInsets;
102
+ });
103
+ });
104
+ }, [edges, windowHeight, windowWidth]);
105
+
106
+ useEffect(() => {
107
+ const frame = requestAnimationFrame(updateEdgeInsets);
108
+
109
+ return () => cancelAnimationFrame(frame);
110
+ }, [updateEdgeInsets]);
111
+
112
+ const handleRef = useCallback(
113
+ (node: View | null) => {
114
+ viewRef.current = node;
115
+
116
+ if (typeof ref === 'function') {
117
+ ref(node);
118
+ } else if (ref) {
119
+ ref.current = node;
120
+ }
121
+ },
122
+ [ref]
123
+ );
124
+
125
+ const handleLayout = useCallback(
126
+ (event: LayoutChangeEvent) => {
127
+ onLayout?.(event);
128
+ requestAnimationFrame(updateEdgeInsets);
129
+ },
130
+ [onLayout, updateEdgeInsets]
131
+ );
132
+
133
+ const safeAreaStyle = useMemo(() => {
134
+ const nextStyle: ViewStyle = {};
135
+
136
+ if (mode === 'padding') {
137
+ nextStyle.paddingTop = getStyleInsetValue(flattenedStyle, mode, 'top') + edgeInsets.top;
138
+ nextStyle.paddingRight =
139
+ getStyleInsetValue(flattenedStyle, mode, 'right') + edgeInsets.right;
140
+ nextStyle.paddingBottom =
141
+ getStyleInsetValue(flattenedStyle, mode, 'bottom') + edgeInsets.bottom;
142
+ nextStyle.paddingLeft = getStyleInsetValue(flattenedStyle, mode, 'left') + edgeInsets.left;
143
+
144
+ return nextStyle;
145
+ }
146
+
147
+ nextStyle.marginTop = getStyleInsetValue(flattenedStyle, mode, 'top') + edgeInsets.top;
148
+ nextStyle.marginRight = getStyleInsetValue(flattenedStyle, mode, 'right') + edgeInsets.right;
149
+ nextStyle.marginBottom =
150
+ getStyleInsetValue(flattenedStyle, mode, 'bottom') + edgeInsets.bottom;
151
+ nextStyle.marginLeft = getStyleInsetValue(flattenedStyle, mode, 'left') + edgeInsets.left;
152
+
153
+ return nextStyle;
154
+ }, [
155
+ edgeInsets.bottom,
156
+ edgeInsets.left,
157
+ edgeInsets.right,
158
+ edgeInsets.top,
159
+ flattenedStyle,
160
+ mode,
161
+ ]);
162
+
163
+ return (
164
+ <View ref={handleRef} onLayout={handleLayout} style={[style, safeAreaStyle]} {...props}>
165
+ {children}
166
+ </View>
167
+ );
168
+ }
169
+ );
170
+
171
+ SafeAreaView.displayName = 'SafeAreaView';
172
+
173
+ export default SafeAreaView;
@@ -0,0 +1,2 @@
1
+ export { default as SafeAreaView } from './SafeAreaView';
2
+ export type { default as SafeAreaViewProps, SafeAreaEdge } from './SafeAreaView.props';
@@ -152,6 +152,10 @@ export interface SelectOptionProps {
152
152
  * Callback when this option is selected
153
153
  */
154
154
  onPress?: (value: string) => void;
155
+ /**
156
+ * Test ID for testing
157
+ */
158
+ testID?: string;
155
159
  }
156
160
 
157
161
  export default SelectProps;
@@ -16,6 +16,7 @@ import { Input } from '../Input';
16
16
  import { SelectContext } from './Select.context';
17
17
  import SelectProps, { SelectOptionItemProps } from './Select.props';
18
18
  import SelectOption from './SelectOption';
19
+ import { SafeAreaView } from '../SafeAreaView';
19
20
 
20
21
  const Select = ({
21
22
  options = [],
@@ -40,6 +41,7 @@ const Select = ({
40
41
  listProps,
41
42
  searchable = false,
42
43
  searchPlaceholder = 'Search',
44
+ testID,
43
45
  ...rest
44
46
  }: SelectProps) => {
45
47
  const formFieldContext = useFormFieldContext();
@@ -109,9 +111,10 @@ const Select = ({
109
111
  disabled={item.disabled}
110
112
  leadingIcon={item.leadingIcon}
111
113
  trailingIcon={item.trailingIcon}
114
+ testID={testID ? `${testID}-option-${item.label}` : undefined}
112
115
  />
113
116
  ),
114
- []
117
+ [testID]
115
118
  );
116
119
 
117
120
  const renderEmptyComponent = useCallback(
@@ -140,6 +143,7 @@ const Select = ({
140
143
  <Pressable
141
144
  onPress={openBottomSheet}
142
145
  disabled={isDisabled || isReadonly}
146
+ testID={testID}
143
147
  style={({ pressed }) => [
144
148
  styles.selectContainer,
145
149
  styles.pressedContainer(pressed || isOpen),
@@ -179,34 +183,37 @@ const Select = ({
179
183
  close: closeBottomSheet,
180
184
  }}
181
185
  >
182
- {menuHeading && (
183
- <View style={styles.headingContainer}>
184
- <DetailText size="lg">{menuHeading}</DetailText>
185
- </View>
186
- )}
187
- {searchable && (
188
- <View style={styles.searchContainer}>
189
- <Input
190
- placeholder={searchPlaceholder}
191
- value={search}
192
- inBottomSheet
193
- onChangeText={setSearch}
194
- type="search"
195
- />
196
- </View>
197
- )}
186
+ <SafeAreaView edges={['top']} style={{ flex: 1 }}>
187
+ {menuHeading && (
188
+ <View style={styles.headingContainer}>
189
+ <DetailText size="lg">{menuHeading}</DetailText>
190
+ </View>
191
+ )}
192
+ {searchable && (
193
+ <View style={styles.searchContainer}>
194
+ <Input
195
+ placeholder={searchPlaceholder}
196
+ value={search}
197
+ inBottomSheet
198
+ onChangeText={setSearch}
199
+ type="search"
200
+ testID={testID ? `${testID}-search` : undefined}
201
+ />
202
+ </View>
203
+ )}
198
204
 
199
- {children ? (
200
- <BottomSheetScrollView>{children}</BottomSheetScrollView>
201
- ) : (
202
- <BottomSheetFlatList
203
- data={filteredOptions}
204
- keyExtractor={(option: any) => option.value}
205
- renderItem={renderSelectOption}
206
- ListEmptyComponent={renderEmptyComponent}
207
- {...listProps}
208
- />
209
- )}
205
+ {children ? (
206
+ <BottomSheetScrollView>{children}</BottomSheetScrollView>
207
+ ) : (
208
+ <BottomSheetFlatList
209
+ data={filteredOptions}
210
+ keyExtractor={(option: any) => option.value}
211
+ renderItem={renderSelectOption}
212
+ ListEmptyComponent={renderEmptyComponent}
213
+ {...listProps}
214
+ />
215
+ )}
216
+ </SafeAreaView>
210
217
  </SelectContext.Provider>
211
218
  </BottomSheetModal>
212
219
  </View>
@@ -14,6 +14,7 @@ const SelectOption = ({
14
14
  selected,
15
15
  disabled,
16
16
  onPress,
17
+ testID,
17
18
  }: SelectOptionProps) => {
18
19
  const { selectedValue, onValueChange, close } = useSelectContext();
19
20
  const isSelected = selected !== undefined ? selected : selectedValue === value;
@@ -38,6 +39,7 @@ const SelectOption = ({
38
39
  <Pressable
39
40
  onPress={handlePress}
40
41
  disabled={disabled}
42
+ testID={testID}
41
43
  style={({ pressed }) => [styles.container, pressed && styles.pressed]}
42
44
  >
43
45
  {!!LeftIcon && (
@@ -0,0 +1,23 @@
1
+ import { createContext, useContext } from 'react';
2
+ import type { TableProps } from './Table.props';
3
+
4
+ type TableContainer = TableProps['container'];
5
+ type TableColumnWidths = NonNullable<TableProps['columnWidths']>;
6
+
7
+ interface TableContextValue {
8
+ columnWidths: TableColumnWidths;
9
+ container: TableContainer;
10
+ columnCount: number;
11
+ hasPagination: boolean;
12
+ }
13
+
14
+ const TableContext = createContext<TableContextValue>({
15
+ columnWidths: [],
16
+ container: 'none',
17
+ columnCount: 1,
18
+ hasPagination: false,
19
+ });
20
+
21
+ export const TableContextProvider = TableContext.Provider;
22
+
23
+ export const useTableContext = () => useContext(TableContext);