@utilitywarehouse/hearth-react-native 0.30.4-testid-fix-2 → 0.31.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/.turbo/turbo-build.log +4 -5
- package/.turbo/turbo-lint.log +62 -70
- package/CHANGELOG.md +155 -0
- package/build/components/Badge/Badge.js +2 -2
- package/build/components/Badge/Badge.props.d.ts +1 -0
- package/build/components/Badge/BadgeText.d.ts +1 -1
- package/build/components/Badge/BadgeText.js +2 -2
- package/build/components/Container/Container.props.d.ts +2 -2
- package/build/components/ExpandableCard/ExpandableCard.d.ts +1 -1
- package/build/components/ExpandableCard/ExpandableCard.js +13 -2
- package/build/components/ExpandableCard/ExpandableCard.props.d.ts +43 -23
- package/build/components/ExpandableCard/ExpandableCardText.js +1 -1
- package/build/components/ExpandableCard/ExpandableCardTrigger.d.ts +3 -3
- package/build/components/ExpandableCard/ExpandableCardTrigger.props.d.ts +31 -6
- package/build/components/ExpandableCard/ExpandableCardTriggerRoot.d.ts +1 -1
- package/build/components/ExpandableCard/ExpandableCardTriggerRoot.js +13 -2
- package/build/components/Flex/Flex.props.d.ts +2 -2
- package/build/components/FormField/FormField.d.ts +5 -5
- package/build/components/FormField/FormField.js +3 -2
- package/build/components/Modal/Modal.d.ts +1 -1
- package/build/components/Modal/Modal.js +33 -39
- package/build/components/Modal/Modal.props.d.ts +8 -3
- package/build/components/Modal/Modal.shared.types.d.ts +19 -4
- package/build/components/Modal/Modal.web.d.ts +1 -1
- package/build/components/Modal/Modal.web.js +6 -3
- package/build/components/NavModal/NavModal.d.ts +1 -1
- package/build/components/NavModal/NavModal.js +10 -7
- package/build/components/NavModal/NavModal.props.d.ts +4 -3
- package/build/components/Textarea/Textarea.d.ts +1 -1
- package/build/components/Textarea/Textarea.js +64 -5
- package/build/components/Textarea/Textarea.props.d.ts +10 -0
- package/build/components/Textarea/TextareaRoot.js +4 -1
- package/docs/changelog.mdx +21 -0
- package/package.json +4 -4
- package/src/components/Badge/Badge.props.ts +1 -0
- package/src/components/Badge/Badge.tsx +6 -1
- package/src/components/Badge/BadgeText.tsx +8 -2
- package/src/components/Container/Container.props.ts +10 -1
- package/src/components/ExpandableCard/ExpandableCard.docs.mdx +89 -37
- package/src/components/ExpandableCard/ExpandableCard.props.ts +51 -27
- package/src/components/ExpandableCard/ExpandableCard.stories.tsx +67 -17
- package/src/components/ExpandableCard/ExpandableCard.tsx +15 -7
- package/src/components/ExpandableCard/ExpandableCardText.tsx +1 -1
- package/src/components/ExpandableCard/ExpandableCardTrigger.props.ts +37 -7
- package/src/components/ExpandableCard/ExpandableCardTriggerRoot.tsx +36 -2
- package/src/components/Flex/Flex.props.ts +16 -2
- package/src/components/FormField/FormField.tsx +2 -1
- package/src/components/List/List.stories.tsx +35 -0
- package/src/components/Modal/Modal.docs.mdx +52 -1
- package/src/components/Modal/Modal.props.ts +21 -6
- package/src/components/Modal/Modal.shared.types.ts +23 -4
- package/src/components/Modal/Modal.stories.tsx +165 -1
- package/src/components/Modal/Modal.tsx +101 -81
- package/src/components/Modal/Modal.web.tsx +29 -23
- package/src/components/NavModal/NavModal.docs.mdx +29 -0
- package/src/components/NavModal/NavModal.props.ts +11 -3
- package/src/components/NavModal/NavModal.stories.tsx +29 -0
- package/src/components/NavModal/NavModal.tsx +39 -33
- package/src/components/Textarea/Textarea.docs.mdx +33 -1
- package/src/components/Textarea/Textarea.props.ts +11 -2
- package/src/components/Textarea/Textarea.stories.tsx +21 -1
- package/src/components/Textarea/Textarea.tsx +107 -3
- package/src/components/Textarea/TextareaRoot.tsx +6 -2
- package/build/components/DatePicker/TimePicker.d.ts +0 -3
- package/build/components/DatePicker/TimePicker.js +0 -84
- package/build/components/DatePicker/time-picker/animated-math.d.ts +0 -4
- package/build/components/DatePicker/time-picker/animated-math.js +0 -19
- package/build/components/DatePicker/time-picker/period-native.d.ts +0 -6
- package/build/components/DatePicker/time-picker/period-native.js +0 -17
- package/build/components/DatePicker/time-picker/period-picker.d.ts +0 -6
- package/build/components/DatePicker/time-picker/period-picker.js +0 -10
- package/build/components/DatePicker/time-picker/period-web.d.ts +0 -6
- package/build/components/DatePicker/time-picker/period-web.js +0 -21
- package/build/components/DatePicker/time-picker/wheel-native.d.ts +0 -8
- package/build/components/DatePicker/time-picker/wheel-native.js +0 -19
- package/build/components/DatePicker/time-picker/wheel-picker/index.d.ts +0 -2
- package/build/components/DatePicker/time-picker/wheel-picker/index.js +0 -2
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.d.ts +0 -16
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.js +0 -97
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.d.ts +0 -21
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.js +0 -88
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.d.ts +0 -23
- package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.js +0 -21
- package/build/components/DatePicker/time-picker/wheel-web.d.ts +0 -8
- package/build/components/DatePicker/time-picker/wheel-web.js +0 -146
- package/build/components/DatePicker/time-picker/wheel.d.ts +0 -8
- package/build/components/DatePicker/time-picker/wheel.js +0 -10
- package/build/components/SafeAreaView/SafeAreaView.d.ts +0 -5
- package/build/components/SafeAreaView/SafeAreaView.js +0 -117
- package/build/components/SafeAreaView/SafeAreaView.props.d.ts +0 -17
- package/build/components/SafeAreaView/SafeAreaView.props.js +0 -1
- package/build/components/SafeAreaView/index.d.ts +0 -2
- package/build/components/SafeAreaView/index.js +0 -1
|
@@ -6,9 +6,10 @@ import {
|
|
|
6
6
|
} from '@gorhom/bottom-sheet';
|
|
7
7
|
import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
|
|
8
8
|
import { CloseMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
9
|
-
import { useCallback, useImperativeHandle, useMemo, useRef } from 'react';
|
|
10
|
-
import { AccessibilityInfo, Platform, View, findNodeHandle } from 'react-native';
|
|
9
|
+
import { useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
10
|
+
import { AccessibilityInfo, LayoutChangeEvent, Platform, View, findNodeHandle } from 'react-native';
|
|
11
11
|
import { StyleSheet } from 'react-native-unistyles';
|
|
12
|
+
import { useTheme } from '../../hooks';
|
|
12
13
|
import { BodyText } from '../BodyText';
|
|
13
14
|
import { BottomSheetModal, BottomSheetScrollView } from '../BottomSheet';
|
|
14
15
|
import { useBottomSheetContext } from '../BottomSheet/BottomSheet.context';
|
|
@@ -38,15 +39,19 @@ const Modal = ({
|
|
|
38
39
|
loadingDescription,
|
|
39
40
|
fullscreen = false,
|
|
40
41
|
image,
|
|
42
|
+
footer,
|
|
43
|
+
footerStyle,
|
|
41
44
|
primaryButtonProps,
|
|
42
45
|
secondaryButtonProps,
|
|
43
46
|
closeButtonProps,
|
|
44
47
|
stickyFooter = true,
|
|
45
48
|
...props
|
|
46
49
|
}: ModalProps) => {
|
|
50
|
+
const theme = useTheme();
|
|
47
51
|
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
|
48
52
|
const viewRef = useRef<View>(null);
|
|
49
53
|
const scrollViewRef = useRef<BottomSheetScrollViewMethods>(null);
|
|
54
|
+
const [stickyFooterHeight, setStickyFooterHeight] = useState(0);
|
|
50
55
|
const { useSafeAreaInsets } = useBottomSheetContext();
|
|
51
56
|
|
|
52
57
|
useImperativeHandle(ref, () => ({
|
|
@@ -100,45 +105,57 @@ const Modal = ({
|
|
|
100
105
|
}
|
|
101
106
|
}, [closeOnSecondaryButtonPress, onPressSecondaryButton]);
|
|
102
107
|
|
|
103
|
-
const
|
|
108
|
+
const handleStickyFooterLayout = useCallback((event: LayoutChangeEvent) => {
|
|
109
|
+
const nextHeight = Math.ceil(event.nativeEvent.layout.height);
|
|
110
|
+
|
|
111
|
+
setStickyFooterHeight(currentHeight =>
|
|
112
|
+
currentHeight === nextHeight ? currentHeight : nextHeight
|
|
113
|
+
);
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
const hasPrimaryButton = !!(onPressPrimaryButton && primaryButtonText);
|
|
117
|
+
const hasSecondaryButton = !!(onPressSecondaryButton && secondaryButtonText);
|
|
118
|
+
const hasFooter = !!footer || hasPrimaryButton || hasSecondaryButton;
|
|
119
|
+
const shouldShowFooter = !loading && hasFooter;
|
|
104
120
|
|
|
105
121
|
styles.useVariants({
|
|
106
122
|
loading,
|
|
107
|
-
|
|
108
|
-
noButtons,
|
|
123
|
+
noButtons: !shouldShowFooter,
|
|
109
124
|
stickyFooter,
|
|
110
125
|
showHandle: props.showHandle,
|
|
111
126
|
useSafeAreaInsets,
|
|
112
127
|
});
|
|
113
128
|
|
|
114
|
-
const
|
|
115
|
-
() =>
|
|
116
|
-
|
|
117
|
-
{
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
129
|
+
const footerContent = useMemo(
|
|
130
|
+
() =>
|
|
131
|
+
footer ?? (
|
|
132
|
+
<View style={styles.footer}>
|
|
133
|
+
{hasPrimaryButton ? (
|
|
134
|
+
<Button
|
|
135
|
+
onPress={handlePrimaryButtonPress}
|
|
136
|
+
text={primaryButtonText}
|
|
137
|
+
{...primaryButtonProps}
|
|
138
|
+
variant={(primaryButtonProps?.variant as 'solid') ?? 'solid'}
|
|
139
|
+
colorScheme={(primaryButtonProps?.colorScheme as 'highlight') ?? 'highlight'}
|
|
140
|
+
/>
|
|
141
|
+
) : null}
|
|
142
|
+
{hasSecondaryButton ? (
|
|
143
|
+
<Button
|
|
144
|
+
onPress={handleSecondaryButtonPress}
|
|
145
|
+
text={secondaryButtonText}
|
|
146
|
+
{...secondaryButtonProps}
|
|
147
|
+
variant={(secondaryButtonProps?.variant as 'outline') ?? 'outline'}
|
|
148
|
+
colorScheme={(secondaryButtonProps?.colorScheme as 'functional') ?? 'functional'}
|
|
149
|
+
/>
|
|
150
|
+
) : null}
|
|
151
|
+
</View>
|
|
152
|
+
),
|
|
137
153
|
[
|
|
154
|
+
footer,
|
|
138
155
|
handlePrimaryButtonPress,
|
|
139
156
|
handleSecondaryButtonPress,
|
|
140
|
-
|
|
141
|
-
|
|
157
|
+
hasPrimaryButton,
|
|
158
|
+
hasSecondaryButton,
|
|
142
159
|
primaryButtonProps,
|
|
143
160
|
primaryButtonText,
|
|
144
161
|
secondaryButtonProps,
|
|
@@ -209,38 +226,64 @@ const Modal = ({
|
|
|
209
226
|
</View>
|
|
210
227
|
) : null}
|
|
211
228
|
{children}
|
|
212
|
-
{!stickyFooter &&
|
|
229
|
+
{!stickyFooter && shouldShowFooter ? (
|
|
230
|
+
<View style={footerStyle}>{footerContent}</View>
|
|
231
|
+
) : null}
|
|
213
232
|
</View>
|
|
214
233
|
)}
|
|
215
234
|
</>
|
|
216
235
|
);
|
|
217
236
|
|
|
218
237
|
const renderFooter = useCallback(
|
|
219
|
-
(
|
|
220
|
-
<BottomSheetFooter {...
|
|
221
|
-
<View style={styles.footerWrap}>
|
|
238
|
+
(bottomSheetFooterProps: BottomSheetFooterProps) => (
|
|
239
|
+
<BottomSheetFooter {...bottomSheetFooterProps}>
|
|
240
|
+
<View onLayout={handleStickyFooterLayout} style={[styles.footerWrap, footerStyle]}>
|
|
241
|
+
{footerContent}
|
|
242
|
+
</View>
|
|
222
243
|
</BottomSheetFooter>
|
|
223
244
|
),
|
|
224
|
-
[
|
|
245
|
+
[footerContent, footerStyle, handleStickyFooterLayout]
|
|
225
246
|
);
|
|
226
247
|
|
|
227
248
|
return (
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
249
|
+
<>
|
|
250
|
+
{stickyFooter && shouldShowFooter && stickyFooterHeight === 0 ? (
|
|
251
|
+
<View
|
|
252
|
+
accessible={false}
|
|
253
|
+
importantForAccessibility="no-hide-descendants"
|
|
254
|
+
pointerEvents="none"
|
|
255
|
+
style={styles.footerMeasurementContainer}
|
|
256
|
+
>
|
|
257
|
+
<View onLayout={handleStickyFooterLayout} style={[styles.footerWrap, footerStyle]}>
|
|
258
|
+
{footerContent}
|
|
259
|
+
</View>
|
|
260
|
+
</View>
|
|
261
|
+
) : null}
|
|
262
|
+
<BottomSheetModal
|
|
263
|
+
ref={bottomSheetModalRef}
|
|
264
|
+
enableDynamicSizing={true}
|
|
265
|
+
snapPoints={image || fullscreen ? ['90%'] : props.snapPoints}
|
|
266
|
+
showHandle={typeof loading !== 'undefined' && loading ? false : props.showHandle}
|
|
267
|
+
accessible={false}
|
|
268
|
+
style={styles.modal}
|
|
269
|
+
footerComponent={stickyFooter && shouldShowFooter ? renderFooter : undefined}
|
|
270
|
+
{...props}
|
|
271
|
+
onChange={handleChange}
|
|
272
|
+
>
|
|
273
|
+
{loading ? <View style={styles.loadingTop} /> : null}
|
|
274
|
+
<BottomSheetScrollView
|
|
275
|
+
contentContainerStyle={[
|
|
276
|
+
styles.scrollView,
|
|
277
|
+
stickyFooter && shouldShowFooter && stickyFooterHeight > 0
|
|
278
|
+
? { paddingBottom: stickyFooterHeight + theme.components.modal.gap }
|
|
279
|
+
: null,
|
|
280
|
+
]}
|
|
281
|
+
ref={scrollViewRef}
|
|
282
|
+
>
|
|
283
|
+
{content}
|
|
284
|
+
</BottomSheetScrollView>
|
|
285
|
+
</BottomSheetModal>
|
|
286
|
+
</>
|
|
244
287
|
);
|
|
245
288
|
};
|
|
246
289
|
|
|
@@ -262,14 +305,6 @@ const styles = StyleSheet.create((theme, rt) => ({
|
|
|
262
305
|
scrollView: {
|
|
263
306
|
flex: 1,
|
|
264
307
|
variants: {
|
|
265
|
-
bothButtons: {
|
|
266
|
-
true: {
|
|
267
|
-
paddingBottom: 166,
|
|
268
|
-
},
|
|
269
|
-
false: {
|
|
270
|
-
paddingBottom: 102,
|
|
271
|
-
},
|
|
272
|
-
},
|
|
273
308
|
noButtons: {
|
|
274
309
|
true: {
|
|
275
310
|
paddingBottom: theme.components.modal.padding,
|
|
@@ -287,31 +322,10 @@ const styles = StyleSheet.create((theme, rt) => ({
|
|
|
287
322
|
},
|
|
288
323
|
},
|
|
289
324
|
compoundVariants: [
|
|
290
|
-
{
|
|
291
|
-
bothButtons: true,
|
|
292
|
-
useSafeAreaInsets: true,
|
|
293
|
-
styles: {
|
|
294
|
-
paddingBottom:
|
|
295
|
-
166 +
|
|
296
|
-
rt.insets.bottom -
|
|
297
|
-
theme.components.modal.padding +
|
|
298
|
-
theme.components.bottomSheet.padding,
|
|
299
|
-
},
|
|
300
|
-
},
|
|
301
|
-
{
|
|
302
|
-
bothButtons: false,
|
|
303
|
-
useSafeAreaInsets: true,
|
|
304
|
-
styles: {
|
|
305
|
-
paddingBottom:
|
|
306
|
-
102 +
|
|
307
|
-
rt.insets.bottom -
|
|
308
|
-
theme.components.modal.padding +
|
|
309
|
-
theme.components.bottomSheet.padding,
|
|
310
|
-
},
|
|
311
|
-
},
|
|
312
325
|
{
|
|
313
326
|
noButtons: true,
|
|
314
327
|
useSafeAreaInsets: true,
|
|
328
|
+
stickyFooter: false,
|
|
315
329
|
styles: {
|
|
316
330
|
paddingBottom:
|
|
317
331
|
rt.insets.bottom +
|
|
@@ -372,6 +386,12 @@ const styles = StyleSheet.create((theme, rt) => ({
|
|
|
372
386
|
footer: {
|
|
373
387
|
gap: theme.components.modal.action.gap,
|
|
374
388
|
},
|
|
389
|
+
footerMeasurementContainer: {
|
|
390
|
+
left: 0,
|
|
391
|
+
opacity: 0,
|
|
392
|
+
position: 'absolute',
|
|
393
|
+
right: 0,
|
|
394
|
+
},
|
|
375
395
|
footerWrap: {
|
|
376
396
|
backgroundColor: theme.color.surface.neutral.strong,
|
|
377
397
|
paddingHorizontal: theme.components.bottomSheet.padding,
|
|
@@ -31,6 +31,8 @@ const Modal = ({
|
|
|
31
31
|
loadingHeading = 'Loading...',
|
|
32
32
|
fullscreen = false,
|
|
33
33
|
image,
|
|
34
|
+
footer,
|
|
35
|
+
footerStyle,
|
|
34
36
|
primaryButtonProps,
|
|
35
37
|
secondaryButtonProps,
|
|
36
38
|
closeButtonProps,
|
|
@@ -91,7 +93,32 @@ const Modal = ({
|
|
|
91
93
|
}
|
|
92
94
|
};
|
|
93
95
|
|
|
94
|
-
const
|
|
96
|
+
const hasPrimaryButton = !!(onPressPrimaryButton && primaryButtonText);
|
|
97
|
+
const hasSecondaryButton = !!(onPressSecondaryButton && secondaryButtonText);
|
|
98
|
+
const hasFooter = !!footer || hasPrimaryButton || hasSecondaryButton;
|
|
99
|
+
|
|
100
|
+
const footerContent = footer ?? (
|
|
101
|
+
<View style={styles.footer}>
|
|
102
|
+
{hasPrimaryButton ? (
|
|
103
|
+
<Button
|
|
104
|
+
onPress={handlePrimaryButtonPress}
|
|
105
|
+
text={primaryButtonText}
|
|
106
|
+
{...primaryButtonProps}
|
|
107
|
+
variant={(primaryButtonProps?.variant as 'solid') ?? 'solid'}
|
|
108
|
+
colorScheme={(primaryButtonProps?.colorScheme as 'highlight') ?? 'highlight'}
|
|
109
|
+
/>
|
|
110
|
+
) : null}
|
|
111
|
+
{hasSecondaryButton ? (
|
|
112
|
+
<Button
|
|
113
|
+
onPress={handleSecondaryButtonPress}
|
|
114
|
+
text={secondaryButtonText}
|
|
115
|
+
{...secondaryButtonProps}
|
|
116
|
+
variant={(secondaryButtonProps?.variant as 'outline') ?? 'outline'}
|
|
117
|
+
colorScheme={(secondaryButtonProps?.colorScheme as 'functional') ?? 'functional'}
|
|
118
|
+
/>
|
|
119
|
+
) : null}
|
|
120
|
+
</View>
|
|
121
|
+
);
|
|
95
122
|
|
|
96
123
|
const content = (
|
|
97
124
|
<>
|
|
@@ -152,28 +179,7 @@ const Modal = ({
|
|
|
152
179
|
</View>
|
|
153
180
|
) : null}
|
|
154
181
|
{children}
|
|
155
|
-
{
|
|
156
|
-
<View style={styles.footer}>
|
|
157
|
-
{onPressPrimaryButton && primaryButtonText ? (
|
|
158
|
-
<Button
|
|
159
|
-
onPress={handlePrimaryButtonPress}
|
|
160
|
-
text={primaryButtonText}
|
|
161
|
-
{...primaryButtonProps}
|
|
162
|
-
variant={(primaryButtonProps?.variant as 'solid') ?? 'solid'}
|
|
163
|
-
colorScheme={(primaryButtonProps?.colorScheme as 'highlight') ?? 'highlight'}
|
|
164
|
-
/>
|
|
165
|
-
) : null}
|
|
166
|
-
{onPressSecondaryButton && secondaryButtonText ? (
|
|
167
|
-
<Button
|
|
168
|
-
onPress={handleSecondaryButtonPress}
|
|
169
|
-
text={secondaryButtonText}
|
|
170
|
-
{...secondaryButtonProps}
|
|
171
|
-
variant={(secondaryButtonProps?.variant as 'outline') ?? 'outline'}
|
|
172
|
-
colorScheme={(secondaryButtonProps?.colorScheme as 'functional') ?? 'functional'}
|
|
173
|
-
/>
|
|
174
|
-
) : null}
|
|
175
|
-
</View>
|
|
176
|
-
) : null}
|
|
182
|
+
{hasFooter ? <View style={footerStyle}>{footerContent}</View> : null}
|
|
177
183
|
</View>
|
|
178
184
|
)}
|
|
179
185
|
</>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
+
import { BodyText, Button, Flex } from '../../';
|
|
2
3
|
import StorybookLink from '../../../../../shared/storybook/StorybookLink';
|
|
3
4
|
import modalAndroidVideo from '../../../docs/assets/modal-android.mp4';
|
|
4
5
|
import modaliOSVideo from '../../../docs/assets/modal-ios.mp4';
|
|
@@ -147,6 +148,8 @@ const styles = StyleSheet.create({
|
|
|
147
148
|
| `onPressCloseButton` | `() => void` | Called when the close button is pressed. | - |
|
|
148
149
|
| `primaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Extra props forwarded to the primary button. | - |
|
|
149
150
|
| `secondaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Extra props forwarded to the secondary button. | - |
|
|
151
|
+
| `footer` | `ReactNode` | Custom footer content that replaces the built-in action buttons. | - |
|
|
152
|
+
| `footerStyle` | `StyleProp<ViewStyle>` | Styles applied to the footer container, useful for sticky footer shadows or extra spacing. | - |
|
|
150
153
|
| `closeButtonProps` | `Omit<UnstyledIconButtonProps, 'children'>` | Extra props forwarded to the close button. | - |
|
|
151
154
|
| `loading` | `boolean` | Replaces the content with a loading state and spinner. | `false` |
|
|
152
155
|
| `loadingHeading` | `string` | Heading text shown while `loading` is true. | `'Loading...'` |
|
|
@@ -160,6 +163,8 @@ const styles = StyleSheet.create({
|
|
|
160
163
|
| `useSafeAreaInsets` | `boolean` | Whether to apply safe area insets as padding within the component. This is enabled by default to fix full-screen presentation padding but can be disabled if you want to manage insets yourself. | `true` |
|
|
161
164
|
| `scrollViewProps` | `ScrollViewProps` | Extra props forwarded to the `ScrollView` wrapping the modal content when `scrollable` is true. | - |
|
|
162
165
|
|
|
166
|
+
When `footer` is provided, the primary and secondary button props are not available. Compose the footer actions directly inside the custom footer instead.
|
|
167
|
+
|
|
163
168
|
## Accessibility
|
|
164
169
|
|
|
165
170
|
`NavModal` keeps the same heading, description, button labeling, and loading-state support as `Modal`, but it does not force accessibility focus on mount. Because this component is used on a navigation-presented screen, React Navigation and the platform own the initial screen focus behavior.
|
|
@@ -177,3 +182,27 @@ const styles = StyleSheet.create({
|
|
|
177
182
|
### Full-Screen Presentation
|
|
178
183
|
|
|
179
184
|
<Canvas of={Stories.FullScreenPresentation} />
|
|
185
|
+
|
|
186
|
+
### Sticky Custom Footer
|
|
187
|
+
|
|
188
|
+
<Canvas of={Stories.StickyCustomFooter} />
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
<NavModal
|
|
192
|
+
heading="Confirm changes"
|
|
193
|
+
description="Use a custom sticky footer when your actions need a custom layout."
|
|
194
|
+
footer={
|
|
195
|
+
<Flex direction="row" spacing="md">
|
|
196
|
+
<Button variant="outline" colorScheme="functional" style={{ flex: 1 }}>
|
|
197
|
+
Back
|
|
198
|
+
</Button>
|
|
199
|
+
<Button style={{ flex: 1 }}>Continue</Button>
|
|
200
|
+
</Flex>
|
|
201
|
+
}
|
|
202
|
+
footerStyle={{
|
|
203
|
+
boxShadow: '0px -6px 12px rgba(16, 24, 40, 0.12)',
|
|
204
|
+
}}
|
|
205
|
+
>
|
|
206
|
+
<BodyText>This sticky footer stays pinned while the content scrolls.</BodyText>
|
|
207
|
+
</NavModal>
|
|
208
|
+
```
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { Ref } from 'react';
|
|
2
2
|
import { ScrollViewProps } from 'react-native';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
ModalButtonFooterProps,
|
|
5
|
+
ModalCommonBaseProps,
|
|
6
|
+
ModalCustomFooterProps,
|
|
7
|
+
} from '../Modal/Modal.shared.types';
|
|
4
8
|
|
|
5
9
|
export interface NavModalRef {
|
|
6
10
|
triggerCloseAnimation?: () => void;
|
|
7
11
|
}
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
type NavModalBaseProps = ModalCommonBaseProps & {
|
|
10
14
|
ref?: Ref<NavModalRef>;
|
|
11
15
|
background?: 'default' | 'brand';
|
|
12
16
|
scrollable?: boolean;
|
|
@@ -18,6 +22,10 @@ interface NavModalProps extends ModalCommonProps {
|
|
|
18
22
|
| 'containedTransparentModal';
|
|
19
23
|
useSafeAreaInsets?: boolean;
|
|
20
24
|
scrollViewProps?: Omit<ScrollViewProps, 'children'>;
|
|
21
|
-
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type NavModalProps =
|
|
28
|
+
| (NavModalBaseProps & ModalButtonFooterProps)
|
|
29
|
+
| (NavModalBaseProps & ModalCustomFooterProps);
|
|
22
30
|
|
|
23
31
|
export default NavModalProps;
|
|
@@ -2,6 +2,8 @@ import { Meta, StoryObj } from '@storybook/react-native';
|
|
|
2
2
|
import { Platform, View } from 'react-native';
|
|
3
3
|
import { BodyText } from '../BodyText';
|
|
4
4
|
import { Box } from '../Box';
|
|
5
|
+
import { Button } from '../Button';
|
|
6
|
+
import { Flex } from '../Flex';
|
|
5
7
|
import NavModal from './NavModal';
|
|
6
8
|
|
|
7
9
|
const meta = {
|
|
@@ -129,3 +131,30 @@ export const FullScreenPresentation: Story = {
|
|
|
129
131
|
</View>
|
|
130
132
|
),
|
|
131
133
|
};
|
|
134
|
+
|
|
135
|
+
export const StickyCustomFooter: Story = {
|
|
136
|
+
render: () => (
|
|
137
|
+
<View style={Platform.OS === 'web' ? { width: 400, height: 720 } : { flex: 1 }}>
|
|
138
|
+
<NavModal
|
|
139
|
+
heading="Confirm changes"
|
|
140
|
+
description="This example replaces the default buttons with a custom sticky footer."
|
|
141
|
+
footer={
|
|
142
|
+
<Flex direction="row" spacing="md">
|
|
143
|
+
<Button variant="outline" colorScheme="functional" style={{ flex: 1 }}>
|
|
144
|
+
Back
|
|
145
|
+
</Button>
|
|
146
|
+
<Button style={{ flex: 1 }}>Continue</Button>
|
|
147
|
+
</Flex>
|
|
148
|
+
}
|
|
149
|
+
footerStyle={{
|
|
150
|
+
boxShadow: '0px -6px 12px rgba(16, 24, 40, 0.12)',
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
<Box gap="200">
|
|
154
|
+
<BodyText>This sticky footer stays pinned while the body content scrolls.</BodyText>
|
|
155
|
+
<BodyText>Use the footer prop when you need custom layouts or custom buttons.</BodyText>
|
|
156
|
+
</Box>
|
|
157
|
+
</NavModal>
|
|
158
|
+
</View>
|
|
159
|
+
),
|
|
160
|
+
};
|
|
@@ -33,6 +33,8 @@ const NavModal = ({
|
|
|
33
33
|
loadingHeading = 'Loading...',
|
|
34
34
|
loadingDescription,
|
|
35
35
|
image,
|
|
36
|
+
footer,
|
|
37
|
+
footerStyle,
|
|
36
38
|
primaryButtonProps,
|
|
37
39
|
secondaryButtonProps,
|
|
38
40
|
closeButtonProps,
|
|
@@ -110,7 +112,9 @@ const NavModal = ({
|
|
|
110
112
|
onPressSecondaryButton?.();
|
|
111
113
|
}, [onPressSecondaryButton]);
|
|
112
114
|
|
|
113
|
-
const
|
|
115
|
+
const hasPrimaryButton = !!(onPressPrimaryButton && primaryButtonText);
|
|
116
|
+
const hasSecondaryButton = !!(onPressSecondaryButton && secondaryButtonText);
|
|
117
|
+
const hasFooter = !!footer || hasPrimaryButton || hasSecondaryButton;
|
|
114
118
|
|
|
115
119
|
styles.useVariants({
|
|
116
120
|
loading,
|
|
@@ -121,37 +125,39 @@ const NavModal = ({
|
|
|
121
125
|
stickyFooter,
|
|
122
126
|
});
|
|
123
127
|
|
|
124
|
-
const
|
|
125
|
-
() =>
|
|
126
|
-
|
|
127
|
-
{
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
128
|
+
const footerContent = useMemo(
|
|
129
|
+
() =>
|
|
130
|
+
footer ?? (
|
|
131
|
+
<View style={styles.footer}>
|
|
132
|
+
{hasPrimaryButton ? (
|
|
133
|
+
<Button
|
|
134
|
+
onPress={handlePrimaryButtonPress}
|
|
135
|
+
text={primaryButtonText}
|
|
136
|
+
inverted={isBrandBackground}
|
|
137
|
+
{...primaryButtonProps}
|
|
138
|
+
variant={(primaryButtonProps?.variant as 'solid') ?? 'solid'}
|
|
139
|
+
colorScheme={(primaryButtonProps?.colorScheme as 'highlight') ?? 'highlight'}
|
|
140
|
+
/>
|
|
141
|
+
) : null}
|
|
142
|
+
{hasSecondaryButton ? (
|
|
143
|
+
<Button
|
|
144
|
+
onPress={handleSecondaryButtonPress}
|
|
145
|
+
text={secondaryButtonText}
|
|
146
|
+
inverted={isBrandBackground}
|
|
147
|
+
{...secondaryButtonProps}
|
|
148
|
+
variant={(secondaryButtonProps?.variant as 'outline') ?? 'outline'}
|
|
149
|
+
colorScheme={(secondaryButtonProps?.colorScheme as 'functional') ?? 'functional'}
|
|
150
|
+
/>
|
|
151
|
+
) : null}
|
|
152
|
+
</View>
|
|
153
|
+
),
|
|
149
154
|
[
|
|
155
|
+
footer,
|
|
150
156
|
handlePrimaryButtonPress,
|
|
151
157
|
handleSecondaryButtonPress,
|
|
158
|
+
hasPrimaryButton,
|
|
159
|
+
hasSecondaryButton,
|
|
152
160
|
isBrandBackground,
|
|
153
|
-
onPressPrimaryButton,
|
|
154
|
-
onPressSecondaryButton,
|
|
155
161
|
primaryButtonProps,
|
|
156
162
|
primaryButtonText,
|
|
157
163
|
secondaryButtonProps,
|
|
@@ -238,8 +244,8 @@ const NavModal = ({
|
|
|
238
244
|
{...scrollViewProps}
|
|
239
245
|
>
|
|
240
246
|
{children}
|
|
241
|
-
{!stickyFooter &&
|
|
242
|
-
<View style={styles.inNavModalFooterContainer}>{
|
|
247
|
+
{!stickyFooter && hasFooter ? (
|
|
248
|
+
<View style={[styles.inNavModalFooterContainer, footerStyle]}>{footerContent}</View>
|
|
243
249
|
) : null}
|
|
244
250
|
</ScrollView>
|
|
245
251
|
) : (
|
|
@@ -249,12 +255,12 @@ const NavModal = ({
|
|
|
249
255
|
}}
|
|
250
256
|
>
|
|
251
257
|
{children}
|
|
252
|
-
{!stickyFooter &&
|
|
253
|
-
<View style={styles.inNavModalFooterContainer}>{
|
|
258
|
+
{!stickyFooter && hasFooter ? (
|
|
259
|
+
<View style={[styles.inNavModalFooterContainer, footerStyle]}>{footerContent}</View>
|
|
254
260
|
) : null}
|
|
255
261
|
</View>
|
|
256
262
|
)}
|
|
257
|
-
{stickyFooter &&
|
|
263
|
+
{stickyFooter && hasFooter ? <View style={footerStyle}>{footerContent}</View> : null}
|
|
258
264
|
</View>
|
|
259
265
|
)}
|
|
260
266
|
</>
|
|
@@ -71,7 +71,8 @@ all of the React Native [`View` props](https://reactnative.dev/docs/view).
|
|
|
71
71
|
| helperIcon | `ComponentType` | `-` | Icon to display alongside the helper text. **(Only to be used if the input has no children)** |
|
|
72
72
|
| validText | `string` | `-` | Text to display when validation status is 'valid'. **(Only to be used if the input has no children)** |
|
|
73
73
|
| invalidText | `string` | `-` | Text to display when validation status is 'invalid'. |
|
|
74
|
-
| required | `boolean` | `
|
|
74
|
+
| required | `boolean` | `true` | Whether the input is required. **(Only to be used if the input has no children)** |
|
|
75
|
+
| resizable | `boolean` | `false` | Adds a bottom-right drag handle so the textarea can be resized vertically. |
|
|
75
76
|
| value | `string` | `-` | The value of the input. **(Only to be used if the input has no children)** |
|
|
76
77
|
| onChange | `function` | `-` | Callback function that is triggered when the input value changes. **(Only to be used if the input has no children)** **(Only to be used if the input has no children)** |
|
|
77
78
|
| onBlur | `function` | `-` | Callback function that is triggered when the input loses focus. **(Only to be used if the input has no children)** |
|
|
@@ -118,6 +119,7 @@ const MyComponent = () => {
|
|
|
118
119
|
const handleChange = text => {
|
|
119
120
|
setValue(text);
|
|
120
121
|
};
|
|
122
|
+
|
|
121
123
|
return (
|
|
122
124
|
<Textarea
|
|
123
125
|
label="Description"
|
|
@@ -130,6 +132,36 @@ const MyComponent = () => {
|
|
|
130
132
|
};
|
|
131
133
|
```
|
|
132
134
|
|
|
135
|
+
### Resizable
|
|
136
|
+
|
|
137
|
+
Set `resizable` to `true` to let people drag the bottom-right handle and increase the textarea height.
|
|
138
|
+
|
|
139
|
+
<UsageWrap>
|
|
140
|
+
<Center>
|
|
141
|
+
<Textarea
|
|
142
|
+
label="Additional notes"
|
|
143
|
+
helperText="Drag the corner handle to resize"
|
|
144
|
+
placeholder="Enter your text here..."
|
|
145
|
+
resizable
|
|
146
|
+
/>
|
|
147
|
+
</Center>
|
|
148
|
+
</UsageWrap>
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
import { Textarea } from '@utilitywarehouse/hearth-react-native';
|
|
152
|
+
|
|
153
|
+
const MyComponent = () => {
|
|
154
|
+
return (
|
|
155
|
+
<Textarea
|
|
156
|
+
label="Additional notes"
|
|
157
|
+
helperText="Drag the corner handle to resize"
|
|
158
|
+
placeholder="Enter your text here..."
|
|
159
|
+
resizable
|
|
160
|
+
/>
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
```
|
|
164
|
+
|
|
133
165
|
## Accessibility
|
|
134
166
|
|
|
135
167
|
We have outlined the various features that ensure the Textarea component is accessible to all users, including those with disabilities. These features help ensure that your application is inclusive and meets accessibility standards.Adheres to the [WAI-ARIA design pattern](https://www.w3.org/TR/wai-aria-1.2/#textbox).
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import type { TextInputProps, ViewProps } from 'react-native';
|
|
2
2
|
|
|
3
3
|
export interface TextareaBaseProps {
|
|
4
|
+
/**
|
|
5
|
+
* If true, the textarea can be resized vertically using a drag handle.
|
|
6
|
+
*
|
|
7
|
+
* @type boolean
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* <Textarea resizable />
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
resizable?: boolean;
|
|
4
14
|
/**
|
|
5
15
|
* If true, the textarea will be disabled.
|
|
6
16
|
*
|
|
@@ -37,8 +47,7 @@ export interface TextareaBaseProps {
|
|
|
37
47
|
export interface TextareaWithChildrenProps extends TextareaBaseProps, ViewProps {}
|
|
38
48
|
|
|
39
49
|
export interface TextareaWithoutChildrenProps
|
|
40
|
-
extends TextareaBaseProps,
|
|
41
|
-
Omit<TextInputProps, 'children'> {
|
|
50
|
+
extends TextareaBaseProps, Omit<TextInputProps, 'children'> {
|
|
42
51
|
children?: never;
|
|
43
52
|
}
|
|
44
53
|
|