@utilitywarehouse/hearth-react-native 0.30.4 → 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.
Files changed (63) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +12 -15
  3. package/CHANGELOG.md +149 -0
  4. package/build/components/Badge/Badge.js +2 -2
  5. package/build/components/Badge/Badge.props.d.ts +1 -0
  6. package/build/components/Badge/BadgeText.d.ts +1 -1
  7. package/build/components/Badge/BadgeText.js +2 -2
  8. package/build/components/Container/Container.props.d.ts +2 -2
  9. package/build/components/ExpandableCard/ExpandableCard.d.ts +1 -1
  10. package/build/components/ExpandableCard/ExpandableCard.js +13 -2
  11. package/build/components/ExpandableCard/ExpandableCard.props.d.ts +43 -23
  12. package/build/components/ExpandableCard/ExpandableCardText.js +1 -1
  13. package/build/components/ExpandableCard/ExpandableCardTrigger.d.ts +3 -3
  14. package/build/components/ExpandableCard/ExpandableCardTrigger.props.d.ts +31 -6
  15. package/build/components/ExpandableCard/ExpandableCardTriggerRoot.d.ts +1 -1
  16. package/build/components/ExpandableCard/ExpandableCardTriggerRoot.js +13 -2
  17. package/build/components/Flex/Flex.props.d.ts +2 -2
  18. package/build/components/FormField/FormField.d.ts +5 -5
  19. package/build/components/FormField/FormField.js +3 -2
  20. package/build/components/Modal/Modal.d.ts +1 -1
  21. package/build/components/Modal/Modal.js +33 -39
  22. package/build/components/Modal/Modal.props.d.ts +8 -3
  23. package/build/components/Modal/Modal.shared.types.d.ts +19 -4
  24. package/build/components/Modal/Modal.web.d.ts +1 -1
  25. package/build/components/Modal/Modal.web.js +6 -3
  26. package/build/components/NavModal/NavModal.d.ts +1 -1
  27. package/build/components/NavModal/NavModal.js +10 -7
  28. package/build/components/NavModal/NavModal.props.d.ts +4 -3
  29. package/build/components/Textarea/Textarea.d.ts +1 -1
  30. package/build/components/Textarea/Textarea.js +64 -5
  31. package/build/components/Textarea/Textarea.props.d.ts +10 -0
  32. package/build/components/Textarea/TextareaRoot.js +4 -1
  33. package/docs/changelog.mdx +21 -0
  34. package/package.json +1 -1
  35. package/src/components/Badge/Badge.props.ts +1 -0
  36. package/src/components/Badge/Badge.tsx +6 -1
  37. package/src/components/Badge/BadgeText.tsx +8 -2
  38. package/src/components/Container/Container.props.ts +10 -1
  39. package/src/components/ExpandableCard/ExpandableCard.docs.mdx +89 -37
  40. package/src/components/ExpandableCard/ExpandableCard.props.ts +51 -27
  41. package/src/components/ExpandableCard/ExpandableCard.stories.tsx +67 -17
  42. package/src/components/ExpandableCard/ExpandableCard.tsx +15 -7
  43. package/src/components/ExpandableCard/ExpandableCardText.tsx +1 -1
  44. package/src/components/ExpandableCard/ExpandableCardTrigger.props.ts +37 -7
  45. package/src/components/ExpandableCard/ExpandableCardTriggerRoot.tsx +36 -2
  46. package/src/components/Flex/Flex.props.ts +16 -2
  47. package/src/components/FormField/FormField.tsx +2 -1
  48. package/src/components/List/List.stories.tsx +35 -0
  49. package/src/components/Modal/Modal.docs.mdx +52 -1
  50. package/src/components/Modal/Modal.props.ts +21 -6
  51. package/src/components/Modal/Modal.shared.types.ts +23 -4
  52. package/src/components/Modal/Modal.stories.tsx +165 -1
  53. package/src/components/Modal/Modal.tsx +101 -81
  54. package/src/components/Modal/Modal.web.tsx +29 -23
  55. package/src/components/NavModal/NavModal.docs.mdx +29 -0
  56. package/src/components/NavModal/NavModal.props.ts +11 -3
  57. package/src/components/NavModal/NavModal.stories.tsx +29 -0
  58. package/src/components/NavModal/NavModal.tsx +39 -33
  59. package/src/components/Textarea/Textarea.docs.mdx +33 -1
  60. package/src/components/Textarea/Textarea.props.ts +11 -2
  61. package/src/components/Textarea/Textarea.stories.tsx +21 -1
  62. package/src/components/Textarea/Textarea.tsx +107 -3
  63. package/src/components/Textarea/TextareaRoot.tsx +6 -2
@@ -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 { ModalCommonProps } from '../Modal/Modal.shared.types';
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
- interface NavModalProps extends ModalCommonProps {
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 noButtons = !onPressPrimaryButton && !onPressSecondaryButton;
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 footer = useMemo(
125
- () => (
126
- <View style={styles.footer}>
127
- {onPressPrimaryButton && primaryButtonText ? (
128
- <Button
129
- onPress={handlePrimaryButtonPress}
130
- text={primaryButtonText}
131
- inverted={isBrandBackground}
132
- {...primaryButtonProps}
133
- variant={(primaryButtonProps?.variant as 'solid') ?? 'solid'}
134
- colorScheme={(primaryButtonProps?.colorScheme as 'highlight') ?? 'highlight'}
135
- />
136
- ) : null}
137
- {onPressSecondaryButton && secondaryButtonText ? (
138
- <Button
139
- onPress={handleSecondaryButtonPress}
140
- text={secondaryButtonText}
141
- inverted={isBrandBackground}
142
- {...secondaryButtonProps}
143
- variant={(secondaryButtonProps?.variant as 'outline') ?? 'outline'}
144
- colorScheme={(secondaryButtonProps?.colorScheme as 'functional') ?? 'functional'}
145
- />
146
- ) : null}
147
- </View>
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 && !noButtons ? (
242
- <View style={styles.inNavModalFooterContainer}>{footer}</View>
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 && !noButtons ? (
253
- <View style={styles.inNavModalFooterContainer}>{footer}</View>
258
+ {!stickyFooter && hasFooter ? (
259
+ <View style={[styles.inNavModalFooterContainer, footerStyle]}>{footerContent}</View>
254
260
  ) : null}
255
261
  </View>
256
262
  )}
257
- {stickyFooter && !noButtons ? footer : null}
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` | `false` | Whether the input is required. **(Only to be used if the input has no children)** |
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
 
@@ -1,4 +1,4 @@
1
- import { Meta, StoryObj } from '@storybook/react-vite';
1
+ import { Meta, StoryObj } from '@storybook/react-native';
2
2
  import { Textarea } from '.';
3
3
 
4
4
  const meta = {
@@ -56,6 +56,16 @@ const meta = {
56
56
  description: 'Focus the Textarea component',
57
57
  defaultValue: false,
58
58
  },
59
+ required: {
60
+ control: 'boolean',
61
+ description: 'Whether the Textarea component is required',
62
+ defaultValue: true,
63
+ },
64
+ resizable: {
65
+ control: 'boolean',
66
+ description: 'Enables a drag handle to resize the Textarea vertically',
67
+ defaultValue: false,
68
+ },
59
69
  },
60
70
  args: {
61
71
  placeholder: 'Textarea placeholder',
@@ -63,6 +73,7 @@ const meta = {
63
73
  disabled: false,
64
74
  readonly: false,
65
75
  focused: false,
76
+ resizable: false,
66
77
  },
67
78
  } satisfies Meta<typeof Textarea>;
68
79
 
@@ -70,3 +81,12 @@ export default meta;
70
81
  type Story = StoryObj<typeof meta>;
71
82
 
72
83
  export const Playground: Story = {};
84
+
85
+ export const Resizable: Story = {
86
+ args: {
87
+ label: 'Notes',
88
+ helperText: 'Drag the bottom-right handle to resize',
89
+ placeholder: 'Add more detail here...',
90
+ resizable: true,
91
+ },
92
+ };
@@ -1,7 +1,19 @@
1
1
  import { createTextarea } from '@gluestack-ui/textarea';
2
2
  import type TextareaProps from './Textarea.props';
3
3
 
4
- import { useEffect } from 'react';
4
+ import { useEffect, useMemo, useRef } from 'react';
5
+ import {
6
+ View,
7
+ type LayoutChangeEvent,
8
+ type StyleProp,
9
+ type TextStyle,
10
+ type ViewStyle,
11
+ } from 'react-native';
12
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
13
+ import { useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
14
+ import { Path, Svg } from 'react-native-svg';
15
+ import { StyleSheet } from 'react-native-unistyles';
16
+ import { useTheme } from '../../hooks';
5
17
  import { FormField, useFormFieldContext } from '../FormField';
6
18
  import TextareaFieldComponent from './TextareaField';
7
19
  import TextareaRoot from './TextareaRoot';
@@ -13,9 +25,14 @@ export const TextareaComponent = createTextarea({
13
25
 
14
26
  export const TextareaField = TextareaComponent.Input;
15
27
 
28
+ const DEFAULT_TEXTAREA_HEIGHT = 96;
29
+ const RESIZE_HANDLE_TOUCH_SIZE = 28;
30
+ const RESIZE_HANDLE_ICON_SIZE = 9;
31
+
16
32
  const Textarea = ({
17
33
  validationStatus = 'initial',
18
34
  children,
35
+ resizable = false,
19
36
  disabled,
20
37
  focused,
21
38
  readonly,
@@ -26,9 +43,11 @@ const Textarea = ({
26
43
  invalidText,
27
44
  required,
28
45
  helperIcon,
46
+ onLayout,
29
47
  ...props
30
48
  }: TextareaProps) => {
31
49
  const formFieldContext = useFormFieldContext();
50
+ const hasMeasuredHeight = useRef(false);
32
51
  const textareaLabel = label ?? formFieldContext?.label;
33
52
  const textareaHelperText = helperText ?? formFieldContext?.helperText;
34
53
  const textareaValidText = validText ?? formFieldContext?.validText;
@@ -37,12 +56,15 @@ const Textarea = ({
37
56
  const textareaDisabled = disabled ?? formFieldContext?.disabled;
38
57
  const textareaReadonly = readonly ?? formFieldContext?.readonly;
39
58
  const textareaValidationStatus = formFieldContext?.validationStatus ?? validationStatus;
59
+ const textareaHeight = useSharedValue(DEFAULT_TEXTAREA_HEIGHT);
60
+ const resizeStartHeight = useSharedValue(DEFAULT_TEXTAREA_HEIGHT);
61
+ const theme = useTheme();
40
62
 
41
63
  useEffect(() => {
42
64
  if (formFieldContext?.setShouldHandleAccessibility) {
43
65
  formFieldContext.setShouldHandleAccessibility(true);
44
66
  }
45
- }, []);
67
+ }, [formFieldContext]);
46
68
 
47
69
  const getAccessibilityLabel = () => {
48
70
  let accessibilityLabel = '';
@@ -75,6 +97,47 @@ const Textarea = ({
75
97
  return accessibilityHint || props.accessibilityHint;
76
98
  };
77
99
 
100
+ const handleTextareaLayout = (event: LayoutChangeEvent) => {
101
+ if (!hasMeasuredHeight.current) {
102
+ textareaHeight.value = event.nativeEvent.layout.height;
103
+ resizeStartHeight.value = event.nativeEvent.layout.height;
104
+ hasMeasuredHeight.current = true;
105
+ }
106
+
107
+ if (children) {
108
+ onLayout?.(event);
109
+ }
110
+ };
111
+
112
+ const resizeGesture = useMemo(
113
+ () =>
114
+ Gesture.Pan()
115
+ .enabled(resizable && !textareaDisabled)
116
+ .onBegin(() => {
117
+ resizeStartHeight.value = textareaHeight.value;
118
+ })
119
+ .onUpdate(event => {
120
+ const nextHeight =
121
+ resizeStartHeight.value + event.translationY + event.translationX * 0.35;
122
+
123
+ textareaHeight.value = Math.max(DEFAULT_TEXTAREA_HEIGHT, nextHeight);
124
+ }),
125
+ [resizable, resizeStartHeight, textareaDisabled, textareaHeight]
126
+ );
127
+
128
+ const animatedHeightStyle = useAnimatedStyle(
129
+ () => ({
130
+ height: textareaHeight.value,
131
+ }),
132
+ [textareaHeight]
133
+ );
134
+
135
+ const rootStyle = (children ? props.style : undefined) as StyleProp<ViewStyle>;
136
+ const inputStyle = (!children ? props.style : undefined) as StyleProp<TextStyle>;
137
+ const textareaStyle = (
138
+ resizable ? [rootStyle, animatedHeightStyle] : rootStyle
139
+ ) as StyleProp<ViewStyle>;
140
+
78
141
  return (
79
142
  <FormField
80
143
  label={label}
@@ -91,6 +154,8 @@ const Textarea = ({
91
154
  >
92
155
  <TextareaComponent
93
156
  {...(children ? props : {})}
157
+ onLayout={handleTextareaLayout}
158
+ style={textareaStyle}
94
159
  validationStatus={textareaValidationStatus}
95
160
  isInvalid={textareaValidationStatus === 'invalid'}
96
161
  isReadOnly={textareaReadonly}
@@ -104,12 +169,51 @@ const Textarea = ({
104
169
  <>{children}</>
105
170
  ) : (
106
171
  <>
107
- <TextareaField {...props} />
172
+ <TextareaField {...props} onLayout={onLayout} style={[styles.textarea, inputStyle]} />
108
173
  </>
109
174
  )}
175
+ {resizable && !textareaDisabled ? (
176
+ <GestureDetector gesture={resizeGesture}>
177
+ <View style={styles.resizeHandle}>
178
+ <Svg
179
+ width={RESIZE_HANDLE_ICON_SIZE}
180
+ height={RESIZE_HANDLE_ICON_SIZE}
181
+ viewBox="0 0 9 9"
182
+ fill="none"
183
+ >
184
+ <Path
185
+ d="M0.353516 8.35355L8.35352 0.353546M4.35352 8.35355L8.35352 4.35355"
186
+ stroke={theme.color.icon.primary}
187
+ />
188
+ </Svg>
189
+ </View>
190
+ </GestureDetector>
191
+ ) : null}
110
192
  </TextareaComponent>
111
193
  </FormField>
112
194
  );
113
195
  };
114
196
 
197
+ const styles = StyleSheet.create({
198
+ textarea: {
199
+ padding: 0,
200
+ _web: {
201
+ outlineStyle: 'none',
202
+ _focusVisible: {
203
+ outlineStyle: 'none',
204
+ },
205
+ },
206
+ },
207
+ resizeHandle: {
208
+ position: 'absolute',
209
+ right: 0,
210
+ bottom: 0,
211
+ width: RESIZE_HANDLE_TOUCH_SIZE,
212
+ height: RESIZE_HANDLE_TOUCH_SIZE,
213
+ alignItems: 'center',
214
+ justifyContent: 'center',
215
+ zIndex: 1,
216
+ },
217
+ });
218
+
115
219
  export default Textarea;
@@ -1,9 +1,12 @@
1
1
  import { useMemo } from 'react';
2
2
  import { View } from 'react-native';
3
+ import Animated from 'react-native-reanimated';
3
4
  import { StyleSheet } from 'react-native-unistyles';
4
5
  import { TextareaContext } from './Textarea.context';
5
6
  import InputProps from './Textarea.props';
6
7
 
8
+ const AnimatedView = Animated.createAnimatedComponent(View);
9
+
7
10
  const TextareaRoot = ({
8
11
  children,
9
12
  style,
@@ -21,9 +24,9 @@ const TextareaRoot = ({
21
24
 
22
25
  return (
23
26
  <TextareaContext.Provider value={value}>
24
- <View {...props} style={[styles.container, style]}>
27
+ <AnimatedView {...props} style={[styles.container, style]}>
25
28
  {children}
26
- </View>
29
+ </AnimatedView>
27
30
  </TextareaContext.Provider>
28
31
  );
29
32
  };
@@ -35,6 +38,7 @@ const styles = StyleSheet.create(theme => ({
35
38
  borderColor: theme.color.border.strong,
36
39
  height: theme.components.input.textArea.height,
37
40
  borderRadius: theme.components.input.borderRadius,
41
+ position: 'relative',
38
42
  flexDirection: 'row',
39
43
  overflow: 'hidden',
40
44
  alignContent: 'center',