@capyx/components-library 0.0.16 → 0.0.18

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
 
@@ -16,3 +16,21 @@
16
16
  overflow-y: auto;
17
17
  min-height: 150px;
18
18
  }
19
+
20
+ /* --constrained modifier: bridges the flex chain through ReactQuill's .quill node.
21
+ * Only activates when the constrainHeight prop is true.
22
+ * Requires the host to set an explicit height on the wrapper (via style prop):
23
+ * - flex fill: style={{ flex: 1, minHeight: 0 }}
24
+ * - fixed height: style={{ height: '40vh' }}
25
+ */
26
+ .ql-rich-text-wrapper--constrained .quill {
27
+ flex: 1;
28
+ min-height: 0;
29
+ display: flex;
30
+ flex-direction: column;
31
+ }
32
+
33
+ .ql-rich-text-wrapper--constrained .ql-container {
34
+ flex: 1;
35
+ min-height: 0;
36
+ }
@@ -53,6 +53,15 @@ export type RichTextInputProps = {
53
53
  style?: CSSProperties;
54
54
  /** Extra CSS class names added to the outer wrapper `div`. */
55
55
  wrapperClassName?: string;
56
+ /**
57
+ * When true, activates internal height-constraining / scrollable mode by adding the
58
+ * `ql-rich-text-wrapper--constrained` CSS modifier class.
59
+ *
60
+ * Pass alongside a `style` that sets the height:
61
+ * - Inside a flex column (e.g. a modal body): `style={{ flex: 1, minHeight: 0 }}`
62
+ * - Fixed-height inline placement: `style={{ height: '40vh' }}`
63
+ */
64
+ constrainHeight?: boolean;
56
65
  };
57
66
  /**
58
67
  * RichTextInput - A rich text editor component using ReactQuill
@@ -1 +1 @@
1
- {"version":3,"file":"RichTextInput.d.ts","sourceRoot":"","sources":["../../lib/components/RichTextInput.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,EAAE,EAAwC,MAAM,OAAO,CAAC;AAG1F,OAAO,qCAAqC,CAAC;AAC7C,OAAO,qBAAqB,CAAC;AAE7B,QAAA,MAAM,OAAO,qIAAsI,CAAC;AACpJ,KAAK,UAAU,GAAG,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC;AAEzC;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAChC,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,mFAAmF;IACnF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;IACvB;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,kBAAkB,CAiFhD,CAAC"}
1
+ {"version":3,"file":"RichTextInput.d.ts","sourceRoot":"","sources":["../../lib/components/RichTextInput.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,EAAE,EAAwC,MAAM,OAAO,CAAC;AAG1F,OAAO,qCAAqC,CAAC;AAC7C,OAAO,qBAAqB,CAAC;AAE7B,QAAA,MAAM,OAAO,qIAAsI,CAAC;AACpJ,KAAK,UAAU,GAAG,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC;AAEzC;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAChC,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,mFAAmF;IACnF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;IACvB;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,kBAAkB,CAkFhD,CAAC"}
@@ -37,7 +37,7 @@ const formats = ['header', 'bold', 'color', 'italic', 'link', 'strike', 'script'
37
37
  * </Modal>
38
38
  * ```
39
39
  */
40
- export const RichTextInput = ({ readonly = false, maxLength, value = '', onChange, isInvalid = false, formats = ['header', 'bold', 'italic', 'underline', 'list'], style, wrapperClassName, }) => {
40
+ export const RichTextInput = ({ readonly = false, maxLength, value = '', onChange, isInvalid = false, formats = ['header', 'bold', 'italic', 'underline', 'list'], style, wrapperClassName, constrainHeight = false, }) => {
41
41
  const reactQuillRef = useRef(null);
42
42
  const [count, setCount] = useState(value.length);
43
43
  const checkCharacterCount = (event) => {
@@ -72,5 +72,5 @@ export const RichTextInput = ({ readonly = false, maxLength, value = '', onChang
72
72
  ],
73
73
  };
74
74
  // const formats = ['header', 'bold', 'color', 'italic', 'link', 'strike', 'script', 'underline', 'list', 'code', 'blockquote', 'code-block'];
75
- return (_jsxs("div", { className: `ql-rich-text-wrapper${wrapperClassName ? ` ${wrapperClassName}` : ''}`, style: style, children: [_jsx(ReactQuill, { ref: reactQuillRef, theme: "snow", onKeyDown: checkCharacterCount, onKeyUp: setContentLength, formats: formats, modules: modules, value: value, onChange: onChange, readOnly: readonly, className: isInvalid ? 'is-invalid' : '' }), maxLength && (_jsxs(Form.Text, { className: count > maxLength ? 'text-danger' : 'text-muted', children: [count, "/", maxLength, " characters"] }))] }));
75
+ return (_jsxs("div", { className: `ql-rich-text-wrapper${constrainHeight ? ' ql-rich-text-wrapper--constrained' : ''}${wrapperClassName ? ` ${wrapperClassName}` : ''}`, style: style, children: [_jsx(ReactQuill, { ref: reactQuillRef, theme: "snow", onKeyDown: checkCharacterCount, onKeyUp: setContentLength, formats: formats, modules: modules, value: value, onChange: onChange, readOnly: readonly, className: isInvalid ? 'is-invalid' : '' }), maxLength && (_jsxs(Form.Text, { className: count > maxLength ? 'text-danger' : 'text-muted', children: [count, "/", maxLength, " characters"] }))] }));
76
76
  };
@@ -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.16",
3
+ "version": "0.0.18",
4
4
  "description": "Shared React component library for Capyx applications",
5
5
  "publishConfig": {
6
6
  "access": "public"