@capyx/components-library 0.0.17 → 0.0.19

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/README.md CHANGED
@@ -176,11 +176,11 @@ function MyForm() {
176
176
 
177
177
  By default the editor has no maximum height. In a constrained context — such as a Bootstrap `Modal` — you want the editor to grow until the modal fills the viewport, then scroll inside the editor instead of overflowing the page.
178
178
 
179
- Three things are required:
179
+ Use the `constrainHeight` prop together with a `style` that sets the height. Three things are required:
180
180
 
181
181
  1. **`<Modal scrollable>`** — Bootstrap caps the modal body at the available viewport height.
182
182
  2. **`d-flex flex-column overflow-hidden` on `<Modal.Body>`** — turns the body into a flex column that constrains its children to the available height.
183
- 3. **`style={{ flex: 1, minHeight: 0 }}` on `<RichTextInput>`** — `flex: 1` fills the remaining space; `minHeight: 0` is required in a flex context so the wrapper can shrink below its natural size and pass the overflow constraint down to Quill.
183
+ 3. **`constrainHeight` + `style={{ flex: 1, minHeight: 0 }}` on `<RichTextInput>`** — `constrainHeight` activates the CSS flex chain through Quill's internal DOM nodes; `flex: 1` fills the remaining space; `minHeight: 0` is required in a flex context so the wrapper can shrink below its natural size.
184
184
 
185
185
  ```tsx
186
186
  import { RichTextInput } from '@your-package/components-library';
@@ -200,6 +200,7 @@ function MyModal({ show, onHide }) {
200
200
  <RichTextInput
201
201
  value={content}
202
202
  onChange={setContent}
203
+ constrainHeight
203
204
  {/* flex:1 fills remaining space; minHeight:0 enables overflow */}
204
205
  style={{ flex: 1, minHeight: 0 }}
205
206
  />
@@ -226,6 +227,59 @@ function MyModal({ show, onHide }) {
226
227
  | `formats` | `FormatType[]` | `['header','bold','italic','underline','list']` | Allowed Quill formats |
227
228
  | `style` | `CSSProperties` | — | Inline styles on the outer wrapper `div`. Use to constrain height in a flex context |
228
229
  | `wrapperClassName` | `string` | — | Extra CSS class names on the outer wrapper `div` |
230
+ | `constrainHeight` | `boolean` | `false` | Activates internal flex chain so the editor scrolls inside itself. Requires a height to be set via `style` (e.g. `style={{ flex: 1, minHeight: 0 }}` or `style={{ height: '40vh' }}`) |
231
+
232
+ ### TextAreaInput
233
+
234
+ `TextAreaInput` is a multiline text input that **auto-grows with its content** using a row-counting approach. There are no inline `style` height overrides, so consumers can freely customise appearance with plain CSS.
235
+
236
+ #### Basic usage
237
+
238
+ ```tsx
239
+ import { TextAreaInput } from '@capyx/components-library';
240
+ import { FormProvider, useForm } from 'react-hook-form';
241
+
242
+ function MyForm() {
243
+ const methods = useForm();
244
+ return (
245
+ <FormProvider {...methods}>
246
+ <TextAreaInput name="notes" label="Notes" placeholder="Enter notes…" />
247
+ </FormProvider>
248
+ );
249
+ }
250
+ ```
251
+
252
+ #### Controlling row height
253
+
254
+ ```tsx
255
+ {/* Starts at 5 rows, grows without limit */}
256
+ <TextAreaInput name="bio" label="Biography" minRows={5} />
257
+
258
+ {/* Starts at 2 rows, scrolls internally after 8 rows */}
259
+ <TextAreaInput name="summary" label="Summary" maxRows={8} />
260
+
261
+ {/* Fixed band: always between 3 and 6 rows */}
262
+ <TextAreaInput name="notes" label="Notes" minRows={3} maxRows={6} />
263
+ ```
264
+
265
+ #### Props
266
+
267
+ | Prop | Type | Default | Description |
268
+ |---|---|---|---|
269
+ | `name` | `string` | — | Field name for form registration |
270
+ | `label` | `string` | — | Label text |
271
+ | `required` | `boolean` | `false` | Marks the field as required |
272
+ | `maxLength` | `number` | — | Maximum number of characters allowed |
273
+ | `placeholder` | `string` | — | Placeholder text |
274
+ | `value` | `string` | — | Controlled value (standalone mode) |
275
+ | `onChange` | `(value: string) => void` | — | Change callback |
276
+ | `disabled` | `boolean` | `false` | Disables the field |
277
+ | `isReadOnly` | `boolean` | `false` | Makes the field read-only |
278
+ | `isPlainText` | `boolean` | `false` | Renders as plain text (no border) |
279
+ | `controlSize` | `'sm' \| 'lg'` | — | Bootstrap size variant |
280
+ | `debounceMs` | `number` | — | Debounce delay in milliseconds for `onChange` |
281
+ | `minRows` | `number` | `2` | Minimum number of visible rows |
282
+ | `maxRows` | `number` | — | Maximum rows before the textarea scrolls internally. Unset = grows without limit |
229
283
 
230
284
  ### Using Addons
231
285
 
@@ -3,6 +3,22 @@ import { type FC } from 'react';
3
3
  * Props for the TagsInput component
4
4
  */
5
5
  export type TagsInputProps = {
6
+ chipStyle?: {
7
+ backgroundColor?: string;
8
+ borderColor?: string;
9
+ color?: string;
10
+ hoverBackgroundColor?: string;
11
+ hoverBorderColor?: string;
12
+ hoverColor?: string;
13
+ deleteIconColor?: string;
14
+ deleteIconHoverColor?: string;
15
+ };
16
+ inputStyle?: {
17
+ borderColor?: string;
18
+ errorBorderColor?: string;
19
+ hoverBorderColor?: string;
20
+ focusBorderColor?: string;
21
+ };
6
22
  /** Field name — required for form integration and the hidden native input */
7
23
  name: string;
8
24
  /** Array of current tag values */
@@ -37,16 +53,44 @@ export type TagsInputProps = {
37
53
  * constraint validation (`required` attribute), making it compatible with
38
54
  * any form library that relies on `checkValidity()` / `reportValidity()`.
39
55
  *
56
+ * ## Customization
57
+ *
58
+ * - `chipStyle`: Minimal style customization for all chips. Only the following keys are supported:
59
+ * - backgroundColor
60
+ * - borderColor
61
+ * - color
62
+ * - hoverBackgroundColor
63
+ * - hoverBorderColor
64
+ * - hoverColor
65
+ * - deleteIconColor
66
+ * - deleteIconHoverColor
67
+ * - `inputStyle`: Minimal style customization for the input border. Only the following keys are supported:
68
+ * - borderColor
69
+ * - errorBorderColor
70
+ * - hoverBorderColor
71
+ * - focusBorderColor
72
+ *
40
73
  * @example
41
74
  * ```tsx
42
75
  * <TagsInput
43
76
  * name="skills"
44
77
  * value={tags}
45
78
  * onChange={setTags}
46
- * placeholder="Add skills..."
47
79
  * required
48
- * min={1}
49
- * max={5}
80
+ * chipStyle={{
81
+ * backgroundColor: tagsError ? '#ffeaea' : '#212529',
82
+ * borderColor: tagsError ? '#ff4d4f' : '#212529',
83
+ * color: tagsError ? '#ff4d4f' : '#fff',
84
+ * hoverBackgroundColor: tagsError ? '#ffd6d6' : '#343a40',
85
+ * hoverBorderColor: tagsError ? '#ff4d4f' : '#343a40',
86
+ * hoverColor: tagsError ? '#ff4d4f' : '#fff',
87
+ * }}
88
+ * inputStyle={{
89
+ * borderColor: '#ced4da',
90
+ * errorBorderColor: '#ff4d4f',
91
+ * hoverBorderColor: '#86b7fe',
92
+ * focusBorderColor: '#1976d2',
93
+ * }}
50
94
  * />
51
95
  * ```
52
96
  */
@@ -1 +1 @@
1
- {"version":3,"file":"TagsInput.d.ts","sourceRoot":"","sources":["../../lib/components/TagsInput.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,EAAqB,MAAM,OAAO,CAAC;AAGnD;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC5B,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACrC,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,GAAG,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,SAAS,EAAE,EAAE,CAAC,cAAc,CA2OxC,CAAC"}
1
+ {"version":3,"file":"TagsInput.d.ts","sourceRoot":"","sources":["../../lib/components/TagsInput.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,EAAqB,MAAM,OAAO,CAAC;AAGnD;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC5B,SAAS,CAAC,EAAE;QACX,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,oBAAoB,CAAC,EAAE,MAAM,CAAC;QAC9B,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,oBAAoB,CAAC,EAAE,MAAM,CAAC;KAC9B,CAAC;IACF,UAAU,CAAC,EAAE;QACZ,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC;IACF,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACrC,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,GAAG,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,eAAO,MAAM,SAAS,EAAE,EAAE,CAAC,cAAc,CA4QxC,CAAC"}
@@ -18,20 +18,48 @@ import { Controller, useFormContext } from 'react-hook-form';
18
18
  * constraint validation (`required` attribute), making it compatible with
19
19
  * any form library that relies on `checkValidity()` / `reportValidity()`.
20
20
  *
21
+ * ## Customization
22
+ *
23
+ * - `chipStyle`: Minimal style customization for all chips. Only the following keys are supported:
24
+ * - backgroundColor
25
+ * - borderColor
26
+ * - color
27
+ * - hoverBackgroundColor
28
+ * - hoverBorderColor
29
+ * - hoverColor
30
+ * - deleteIconColor
31
+ * - deleteIconHoverColor
32
+ * - `inputStyle`: Minimal style customization for the input border. Only the following keys are supported:
33
+ * - borderColor
34
+ * - errorBorderColor
35
+ * - hoverBorderColor
36
+ * - focusBorderColor
37
+ *
21
38
  * @example
22
39
  * ```tsx
23
40
  * <TagsInput
24
41
  * name="skills"
25
42
  * value={tags}
26
43
  * onChange={setTags}
27
- * placeholder="Add skills..."
28
44
  * required
29
- * min={1}
30
- * max={5}
45
+ * chipStyle={{
46
+ * backgroundColor: tagsError ? '#ffeaea' : '#212529',
47
+ * borderColor: tagsError ? '#ff4d4f' : '#212529',
48
+ * color: tagsError ? '#ff4d4f' : '#fff',
49
+ * hoverBackgroundColor: tagsError ? '#ffd6d6' : '#343a40',
50
+ * hoverBorderColor: tagsError ? '#ff4d4f' : '#343a40',
51
+ * hoverColor: tagsError ? '#ff4d4f' : '#fff',
52
+ * }}
53
+ * inputStyle={{
54
+ * borderColor: '#ced4da',
55
+ * errorBorderColor: '#ff4d4f',
56
+ * hoverBorderColor: '#86b7fe',
57
+ * focusBorderColor: '#1976d2',
58
+ * }}
31
59
  * />
32
60
  * ```
33
61
  */
34
- export const TagsInput = ({ name, value: valueProp = [], onChange, placeholder = 'Add tags...', disabled = false, required = false, label, min, max, }) => {
62
+ export const TagsInput = ({ name, value: valueProp = [], onChange, placeholder = 'Add tags...', disabled = false, required = false, label, min, max, chipStyle, inputStyle, }) => {
35
63
  const formContext = useFormContext();
36
64
  const nativeInputRef = useRef(null);
37
65
  const fieldLabel = label || 'This field';
@@ -91,11 +119,31 @@ export const TagsInput = ({ name, value: valueProp = [], onChange, placeholder =
91
119
  }
92
120
  return cleaned;
93
121
  };
94
- const renderChips = (tagValue, getTagProps) => tagValue.map((option, index) => (_createElement(Chip, { ...getTagProps({ index }), key: option, label: option, sx: {
122
+ const renderChips = (tagValue, getTagProps) => tagValue.map((option, index) => {
123
+ const baseChipSx = {
95
124
  backgroundColor: '#212529',
96
125
  color: '#ffffff',
126
+ border: '1px solid transparent',
127
+ ...(chipStyle?.backgroundColor && {
128
+ backgroundColor: chipStyle.backgroundColor,
129
+ }),
130
+ ...(chipStyle?.borderColor && {
131
+ borderColor: chipStyle.borderColor,
132
+ border: `1px solid ${chipStyle.borderColor}`,
133
+ }),
134
+ ...(chipStyle?.color && { color: chipStyle.color }),
135
+ '&:hover': {
136
+ ...(chipStyle?.hoverBackgroundColor && {
137
+ backgroundColor: chipStyle.hoverBackgroundColor,
138
+ }),
139
+ ...(chipStyle?.hoverBorderColor && {
140
+ borderColor: chipStyle.hoverBorderColor,
141
+ border: `1px solid ${chipStyle.hoverBorderColor}`,
142
+ }),
143
+ ...(chipStyle?.hoverColor && { color: chipStyle.hoverColor }),
144
+ },
97
145
  '& .MuiChip-deleteIcon': {
98
- color: 'rgba(255, 255, 255, 0.7)',
146
+ color: chipStyle?.deleteIconColor || 'rgba(255, 255, 255, 0.7)',
99
147
  userSelect: 'none',
100
148
  WebkitUserSelect: 'none',
101
149
  MozUserSelect: 'none',
@@ -111,26 +159,34 @@ export const TagsInput = ({ name, value: valueProp = [], onChange, placeholder =
111
159
  pointerEvents: 'none',
112
160
  },
113
161
  '&:hover': {
114
- color: '#dc3545',
162
+ color: chipStyle?.deleteIconHoverColor || '#dc3545',
115
163
  },
116
164
  },
117
- } })));
118
- const inputSx = (hasError) => ({
119
- '& .MuiOutlinedInput-root': {
120
- padding: '4px',
121
- minHeight: '38px',
122
- '& fieldset': {
123
- borderColor: hasError ? '#dc3545' : '#ced4da',
124
- },
125
- '&:hover fieldset': {
126
- borderColor: hasError ? '#dc3545' : '#86b7fe',
127
- },
128
- '&.Mui-focused fieldset': {
129
- borderColor: hasError ? '#dc3545' : '#86b7fe',
130
- borderWidth: '1px',
131
- },
132
- },
165
+ };
166
+ return (_createElement(Chip, { ...getTagProps({ index }), key: option, label: option, sx: baseChipSx }));
133
167
  });
168
+ const inputSx = (hasError) => {
169
+ const borderColor = inputStyle?.borderColor || '#ced4da';
170
+ const errorBorderColor = inputStyle?.errorBorderColor || '#dc3545';
171
+ const hoverBorderColor = inputStyle?.hoverBorderColor || '#86b7fe';
172
+ const focusBorderColor = inputStyle?.focusBorderColor || '#86b7fe';
173
+ return {
174
+ '& .MuiOutlinedInput-root': {
175
+ padding: '4px',
176
+ minHeight: '38px',
177
+ '& fieldset': {
178
+ borderColor: hasError ? errorBorderColor : borderColor,
179
+ },
180
+ '&:hover fieldset': {
181
+ borderColor: hasError ? errorBorderColor : hoverBorderColor,
182
+ },
183
+ '&.Mui-focused fieldset': {
184
+ borderColor: hasError ? errorBorderColor : focusBorderColor,
185
+ borderWidth: '1px',
186
+ },
187
+ },
188
+ };
189
+ };
134
190
  // ── react-hook-form mode ──────────────────────────────────────────────
135
191
  if (formContext) {
136
192
  const errorMessage = getFieldError(name);
@@ -0,0 +1,6 @@
1
+ /* Applied via className on every TextAreaInput <Form.Control>.
2
+ Using a stylesheet rule (not an inline style) so consumers can override
3
+ with normal CSS specificity — no !important required. */
4
+ .rhi-textarea-input {
5
+ resize: none;
6
+ }
@@ -1,4 +1,5 @@
1
1
  import { type FC } from 'react';
2
+ import './TextAreaInput.css';
2
3
  /**
3
4
  * Props for the TextAreaInput component
4
5
  */
@@ -27,6 +28,16 @@ export type TextAreaInputProps = {
27
28
  isPlainText?: boolean;
28
29
  /** Debounce delay in milliseconds for value changes */
29
30
  debounceMs?: number;
31
+ /**
32
+ * Minimum number of visible rows.
33
+ * @default 2
34
+ */
35
+ minRows?: number;
36
+ /**
37
+ * Maximum number of visible rows before the textarea scrolls internally.
38
+ * When unset the textarea grows without any row cap.
39
+ */
40
+ maxRows?: number;
30
41
  };
31
42
  /**
32
43
  * A flexible textarea input component with automatic height adjustment,
@@ -1 +1 @@
1
- {"version":3,"file":"TextAreaInput.d.ts","sourceRoot":"","sources":["../../lib/components/TextAreaInput.tsx"],"names":[],"mappings":"AACA,OAAO,EAEN,KAAK,EAAE,EAKP,MAAM,OAAO,CAAC;AAIf;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAChC,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oCAAoC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,WAAW,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAC1B,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uCAAuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,wCAAwC;IACxC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,sCAAsC;IACtC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAIF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,kBAAkB,CA+HhD,CAAC"}
1
+ {"version":3,"file":"TextAreaInput.d.ts","sourceRoot":"","sources":["../../lib/components/TextAreaInput.tsx"],"names":[],"mappings":"AACA,OAAO,EAEN,KAAK,EAAE,EAKP,MAAM,OAAO,CAAC;AAGf,OAAO,qBAAqB,CAAC;AAE7B;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAChC,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oCAAoC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,WAAW,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAC1B,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uCAAuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,wCAAwC;IACxC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,sCAAsC;IACtC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAIF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,kBAAkB,CAyIhD,CAAC"}
@@ -3,7 +3,8 @@ import debounce from 'lodash.debounce';
3
3
  import { useEffect, useLayoutEffect, useRef, useState, } from 'react';
4
4
  import { Form } from 'react-bootstrap';
5
5
  import { Controller, useFormContext } from 'react-hook-form';
6
- const MIN_TEXTAREA_HEIGHT = 32;
6
+ import './TextAreaInput.css';
7
+ const MIN_ROWS = 2;
7
8
  /**
8
9
  * A flexible textarea input component with automatic height adjustment,
9
10
  * react-hook-form integration, and optional debouncing.
@@ -43,7 +44,8 @@ const MIN_TEXTAREA_HEIGHT = 32;
43
44
  * placeholder="Add your comment..."
44
45
  * />
45
46
  */
46
- export const TextAreaInput = ({ name, label, required = false, maxLength, controlSize, placeholder, value, onChange, disabled = false, isReadOnly = false, isPlainText = false, debounceMs, }) => {
47
+ export const TextAreaInput = ({ name, label, required = false, maxLength, controlSize, placeholder, value, onChange, disabled = false, isReadOnly = false, isPlainText = false, debounceMs, minRows, maxRows, }) => {
48
+ const resolvedMinRows = minRows ?? MIN_ROWS;
47
49
  const formContext = useFormContext();
48
50
  // Create ref for debounced onChange to clean up on unmount
49
51
  const debouncedOnChangeRef = useRef(null);
@@ -78,17 +80,27 @@ export const TextAreaInput = ({ name, label, required = false, maxLength, contro
78
80
  const errorMessage = getFieldError(name);
79
81
  const isInvalid = !!errorMessage;
80
82
  const textareaRef = useRef(null);
81
- const [_textAreaValue, setTextAreaValue] = useState('');
83
+ const [rowCount, setRowCount] = useState(resolvedMinRows);
82
84
  const _handleTextAreaChange = (event) => {
83
- setTextAreaValue(event.target.value);
84
85
  handleChange(event.target.value);
85
86
  };
87
+ // Runs after every render (no dep-array) so it reacts to any value change,
88
+ // including programmatic resets (e.g. react-hook-form reset()).
89
+ // Guards against infinite loops by only scheduling a state update when the
90
+ // row count actually changes — React will bail out of the re-render when the
91
+ // new state value is identical to the current one.
86
92
  useLayoutEffect(() => {
87
- if (textareaRef.current) {
88
- textareaRef.current.style.height = 'inherit';
89
- textareaRef.current.style.height = `${Math.max(textareaRef.current.scrollHeight, MIN_TEXTAREA_HEIGHT)}px`;
90
- }
91
- }, []);
93
+ const el = textareaRef.current;
94
+ if (!el)
95
+ return;
96
+ const savedRows = el.rows;
97
+ el.rows = 1; // collapse to measure natural scrollHeight
98
+ const lineHeight = Number.parseInt(getComputedStyle(el).lineHeight, 10) || 20;
99
+ const rawRows = Math.ceil(el.scrollHeight / lineHeight);
100
+ el.rows = savedRows; // restore before React paints
101
+ const next = Math.max(resolvedMinRows, maxRows !== undefined ? Math.min(rawRows, maxRows) : rawRows);
102
+ setRowCount((prev) => (prev === next ? prev : next));
103
+ });
92
104
  // Integrated with react-hook-form
93
105
  if (formContext) {
94
106
  return (_jsx(Controller, { name: name, control: formContext.control, rules: {
@@ -101,13 +113,9 @@ export const TextAreaInput = ({ name, label, required = false, maxLength, contro
101
113
  : undefined,
102
114
  }, render: ({ field }) => (_jsx(Form.Control, { ...field, onChange: (e) => {
103
115
  field.onChange(e);
104
- setTextAreaValue(e.target.value);
105
- handleChange(e.target.value);
106
- }, ref: textareaRef, style: {
107
- minHeight: MIN_TEXTAREA_HEIGHT,
108
- resize: 'none',
109
- }, as: "textarea", required: required, maxLength: maxLength, size: controlSize, placeholder: placeholder, disabled: disabled, readOnly: isReadOnly, plaintext: isPlainText, isInvalid: isInvalid })) }));
116
+ _handleTextAreaChange(e);
117
+ }, ref: textareaRef, className: "rhi-textarea-input", as: "textarea", rows: rowCount, required: required, maxLength: maxLength, size: controlSize, placeholder: placeholder, disabled: disabled, readOnly: isReadOnly, plaintext: isPlainText, isInvalid: isInvalid })) }));
110
118
  }
111
119
  // Standalone mode
112
- return (_jsx(Form.Control, { as: "textarea", required: required, maxLength: maxLength, size: controlSize, placeholder: placeholder, value: value || '', disabled: disabled, readOnly: isReadOnly, plaintext: isPlainText }));
120
+ return (_jsx(Form.Control, { as: "textarea", ref: textareaRef, className: "rhi-textarea-input", rows: rowCount, required: required, maxLength: maxLength, size: controlSize, placeholder: placeholder, value: value || '', onChange: _handleTextAreaChange, disabled: disabled, readOnly: isReadOnly, plaintext: isPlainText }));
113
121
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capyx/components-library",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "description": "Shared React component library for Capyx applications",
5
5
  "publishConfig": {
6
6
  "access": "public"