@capyx/components-library 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/main.ts +33 -0
- package/.storybook/preview.ts +36 -0
- package/.storybook/vitest.setup.ts +7 -0
- package/README.md +210 -0
- package/biome.json +37 -0
- package/lib/addons/CharacterCountInput.tsx +204 -0
- package/lib/addons/index.ts +2 -0
- package/lib/components/CheckInput.tsx +126 -0
- package/lib/components/DateInput.tsx +179 -0
- package/lib/components/FileInput.tsx +353 -0
- package/lib/components/RichTextInput.tsx +112 -0
- package/lib/components/SelectInput.tsx +144 -0
- package/lib/components/SwitchInput.tsx +116 -0
- package/lib/components/TagsInput.tsx +118 -0
- package/lib/components/TextAreaInput.tsx +211 -0
- package/lib/components/TextInput.tsx +381 -0
- package/lib/components/index.ts +9 -0
- package/lib/index.ts +2 -0
- package/package.json +72 -0
- package/stories/CharacterCountInput.stories.tsx +104 -0
- package/stories/CheckInput.stories.tsx +80 -0
- package/stories/DateInput.stories.tsx +137 -0
- package/stories/FileInput.stories.tsx +125 -0
- package/stories/RichTextInput.stories.tsx +77 -0
- package/stories/SelectInput.stories.tsx +131 -0
- package/stories/SwitchInput.stories.tsx +80 -0
- package/stories/TagsInput.stories.tsx +69 -0
- package/stories/TextAreaInput.stories.tsx +117 -0
- package/stories/TextInput.stories.tsx +167 -0
- package/vitest.config.ts +37 -0
- package/vitest.shims.d.ts +1 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { StorybookConfig } from '@storybook/react-vite';
|
|
2
|
+
|
|
3
|
+
const config: StorybookConfig = {
|
|
4
|
+
"stories": [
|
|
5
|
+
"../stories/**/*.mdx",
|
|
6
|
+
"../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)"
|
|
7
|
+
],
|
|
8
|
+
"addons": [
|
|
9
|
+
"@chromatic-com/storybook",
|
|
10
|
+
"@storybook/addon-vitest",
|
|
11
|
+
"@storybook/addon-a11y",
|
|
12
|
+
"@storybook/addon-docs",
|
|
13
|
+
"@storybook/addon-onboarding"
|
|
14
|
+
],
|
|
15
|
+
"framework": "@storybook/react-vite",
|
|
16
|
+
async viteFinal(config) {
|
|
17
|
+
// Suppress "use client" directive warnings from MUI components
|
|
18
|
+
if (config.build) {
|
|
19
|
+
config.build.rollupOptions = {
|
|
20
|
+
...config.build.rollupOptions,
|
|
21
|
+
onwarn(warning, warn) {
|
|
22
|
+
// Ignore "use client" directive warnings
|
|
23
|
+
if (warning.code === 'MODULE_LEVEL_DIRECTIVE' && warning.message.includes('"use client"')) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
warn(warning);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return config;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
export default config;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Preview } from '@storybook/react-vite'
|
|
2
|
+
import 'bootstrap/dist/css/bootstrap.min.css';
|
|
3
|
+
import 'react-quill-new/dist/quill.snow.css';
|
|
4
|
+
|
|
5
|
+
const preview: Preview = {
|
|
6
|
+
parameters: {
|
|
7
|
+
backgrounds: {
|
|
8
|
+
default: 'dark',
|
|
9
|
+
values: [
|
|
10
|
+
{
|
|
11
|
+
name: 'dark',
|
|
12
|
+
value: '#1a1a1a',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'light',
|
|
16
|
+
value: '#ffffff',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
controls: {
|
|
21
|
+
matchers: {
|
|
22
|
+
color: /(background|color)$/i,
|
|
23
|
+
date: /Date$/i,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
a11y: {
|
|
28
|
+
// 'todo' - show a11y violations in the test UI only
|
|
29
|
+
// 'error' - fail CI on a11y violations
|
|
30
|
+
// 'off' - skip a11y checks entirely
|
|
31
|
+
test: 'todo'
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default preview;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
|
|
2
|
+
import { setProjectAnnotations } from '@storybook/react-vite';
|
|
3
|
+
import * as projectAnnotations from './preview';
|
|
4
|
+
|
|
5
|
+
// This is an important step to apply the right configuration when testing your stories.
|
|
6
|
+
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
|
|
7
|
+
setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);
|
package/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Components Library
|
|
2
|
+
|
|
3
|
+
A comprehensive React component library built with TypeScript, React 19, react-hook-form, react-bootstrap, and Material-UI (MUI).
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
This library follows a clear separation between **Components** and **Addons**:
|
|
8
|
+
|
|
9
|
+
### Components (Base Input Types)
|
|
10
|
+
|
|
11
|
+
Components are specific input elements based on HTML input types and specialized use cases:
|
|
12
|
+
|
|
13
|
+
- **CheckInput** - Checkbox input for boolean values
|
|
14
|
+
- **DateInput** - Date picker with formatted string output (using MUI DatePicker)
|
|
15
|
+
- **FileInput** - File upload with validation and preview
|
|
16
|
+
- **SelectInput** - Dropdown selection for single/multiple options
|
|
17
|
+
- **SwitchInput** - Toggle switch for on/off values
|
|
18
|
+
- **TagsInput** - Tag management with MUI Autocomplete
|
|
19
|
+
- **TextAreaInput** - Multiline text input with auto-growing height
|
|
20
|
+
- **TextInput** - Single-line text input with optional autocomplete
|
|
21
|
+
|
|
22
|
+
### Addons (Enhancement Wrappers)
|
|
23
|
+
|
|
24
|
+
Addons wrap existing components to add functionality:
|
|
25
|
+
|
|
26
|
+
- **CharacterCountInput** - Adds character counting to text, textarea, or editor inputs
|
|
27
|
+
- **EditorAddon** - Wraps TextAreaInput to add rich text editing (ReactQuill)
|
|
28
|
+
- **Editor** (Legacy) - Standalone rich text editor (deprecated, use EditorAddon instead)
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- ✅ **React 19** compatible
|
|
33
|
+
- ✅ **TypeScript** with full type safety
|
|
34
|
+
- ✅ **react-hook-form** integration for form state management and validation
|
|
35
|
+
- ✅ **react-bootstrap** for consistent styling
|
|
36
|
+
- ✅ **Material-UI (MUI)** for advanced components (DatePicker, Autocomplete)
|
|
37
|
+
- ✅ Standalone or form-integrated modes
|
|
38
|
+
- ✅ Consistent error handling
|
|
39
|
+
- ✅ Customizable styling
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install react react-dom react-hook-form react-bootstrap @mui/material @mui/x-date-pickers dayjs react-quill-new react-autosuggest
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
### Basic Component Usage
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { TextInput, CheckInput, DateInput } from '@your-package/components-library';
|
|
53
|
+
import { FormProvider, useForm } from 'react-hook-form';
|
|
54
|
+
|
|
55
|
+
function MyForm() {
|
|
56
|
+
const methods = useForm();
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<FormProvider {...methods}>
|
|
60
|
+
<form>
|
|
61
|
+
<TextInput
|
|
62
|
+
name='username'
|
|
63
|
+
label='Username'
|
|
64
|
+
required
|
|
65
|
+
maxLength={50}
|
|
66
|
+
/>
|
|
67
|
+
|
|
68
|
+
<CheckInput
|
|
69
|
+
name='terms'
|
|
70
|
+
label='I agree to terms'
|
|
71
|
+
required
|
|
72
|
+
/>
|
|
73
|
+
|
|
74
|
+
<DateInput
|
|
75
|
+
name='birthdate'
|
|
76
|
+
label='Date of Birth'
|
|
77
|
+
required
|
|
78
|
+
/>
|
|
79
|
+
</form>
|
|
80
|
+
</FormProvider>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Using Addons
|
|
86
|
+
|
|
87
|
+
#### Character Count Addon
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
import { TextInput, CharacterCountInput } from '@your-package/components-library';
|
|
91
|
+
|
|
92
|
+
function MyForm() {
|
|
93
|
+
return (
|
|
94
|
+
<CharacterCountInput>
|
|
95
|
+
<TextInput
|
|
96
|
+
name='bio'
|
|
97
|
+
label='Biography'
|
|
98
|
+
maxLength={200}
|
|
99
|
+
/>
|
|
100
|
+
</CharacterCountInput>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### Editor Addon (Rich Text)
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
import { EditorAddon } from '@your-package/components-library';
|
|
109
|
+
import { Controller, useFormContext } from 'react-hook-form';
|
|
110
|
+
|
|
111
|
+
function MyForm() {
|
|
112
|
+
const { control } = useFormContext();
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<Controller
|
|
116
|
+
name='content'
|
|
117
|
+
control={control}
|
|
118
|
+
render={({ field }) => (
|
|
119
|
+
<EditorAddon
|
|
120
|
+
value={field.value}
|
|
121
|
+
onChange={field.onChange}
|
|
122
|
+
maxLength={5000}
|
|
123
|
+
/>
|
|
124
|
+
)}
|
|
125
|
+
/>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### Combining Addons
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
import { CharacterCountInput, EditorAddon } from '@your-package/components-library';
|
|
134
|
+
|
|
135
|
+
function MyForm() {
|
|
136
|
+
const [value, setValue] = useState('');
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<CharacterCountInput>
|
|
140
|
+
<EditorAddon
|
|
141
|
+
value={value}
|
|
142
|
+
onChange={setValue}
|
|
143
|
+
maxLength={1000}
|
|
144
|
+
/>
|
|
145
|
+
</CharacterCountInput>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Component API
|
|
151
|
+
|
|
152
|
+
### Common Props
|
|
153
|
+
|
|
154
|
+
All components support these common props when used with react-hook-form:
|
|
155
|
+
|
|
156
|
+
- `name: string` (required) - Field name for form registration
|
|
157
|
+
- `label?: string` - Label text
|
|
158
|
+
- `required?: boolean` - Whether the field is required
|
|
159
|
+
- `disabled?: boolean` - Whether the field is disabled
|
|
160
|
+
|
|
161
|
+
### Standalone Mode
|
|
162
|
+
|
|
163
|
+
All components can be used without react-hook-form by providing `value` and `onChange` props:
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
<TextInput
|
|
167
|
+
name='standalone'
|
|
168
|
+
value={value}
|
|
169
|
+
onChange={(newValue) => setValue(newValue)}
|
|
170
|
+
/>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Styling
|
|
174
|
+
|
|
175
|
+
Components use react-bootstrap and MUI components, making them easy to customize:
|
|
176
|
+
|
|
177
|
+
- Override Bootstrap variables for global theming
|
|
178
|
+
- Use MUI theme provider for MUI components
|
|
179
|
+
- Add custom className props (where supported)
|
|
180
|
+
|
|
181
|
+
## TypeScript Support
|
|
182
|
+
|
|
183
|
+
All components are fully typed with TypeScript. Import types as needed:
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
import type { TextAreaInputProps, CheckInputProps } from '@your-package/components-library';
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## React 19 Compatibility
|
|
190
|
+
|
|
191
|
+
This library is built and tested with React 19. All components use modern React patterns:
|
|
192
|
+
|
|
193
|
+
- Functional components with hooks
|
|
194
|
+
- No deprecated lifecycle methods
|
|
195
|
+
- No unsafe React APIs
|
|
196
|
+
- Full concurrent mode support
|
|
197
|
+
|
|
198
|
+
## Contributing
|
|
199
|
+
|
|
200
|
+
When adding new components:
|
|
201
|
+
|
|
202
|
+
1. **Components** go in `lib/components/` - base input types
|
|
203
|
+
2. **Addons** go in `lib/addons/` - wrappers that enhance components
|
|
204
|
+
3. All components must support both react-hook-form and standalone modes
|
|
205
|
+
4. Export types alongside components
|
|
206
|
+
5. Update index files for proper exports
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
MIT
|
package/biome.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": false,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": false
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"ignoreUnknown": false
|
|
10
|
+
},
|
|
11
|
+
"formatter": {
|
|
12
|
+
"enabled": true,
|
|
13
|
+
"indentStyle": "tab"
|
|
14
|
+
},
|
|
15
|
+
"linter": {
|
|
16
|
+
"enabled": true,
|
|
17
|
+
"rules": {
|
|
18
|
+
"recommended": true,
|
|
19
|
+
"suspicious": {
|
|
20
|
+
"noExplicitAny": "warn"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"javascript": {
|
|
25
|
+
"formatter": {
|
|
26
|
+
"quoteStyle": "single"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"assist": {
|
|
30
|
+
"enabled": true,
|
|
31
|
+
"actions": {
|
|
32
|
+
"source": {
|
|
33
|
+
"organizeImports": "on"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
type FC,
|
|
3
|
+
type PropsWithChildren,
|
|
4
|
+
type ReactElement,
|
|
5
|
+
type ReactNode,
|
|
6
|
+
useEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import { Form } from 'react-bootstrap';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_VALID_HTML_ELEMENTS = [
|
|
13
|
+
'text',
|
|
14
|
+
'email',
|
|
15
|
+
'password',
|
|
16
|
+
'search',
|
|
17
|
+
'tel',
|
|
18
|
+
'url',
|
|
19
|
+
'textarea',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Props for the CharacterCountInput component
|
|
24
|
+
*/
|
|
25
|
+
export type CharacterCountInputProps = PropsWithChildren<{
|
|
26
|
+
/** Array of valid HTML element types to monitor (e.g., ["text", "email", "textarea"]) */
|
|
27
|
+
validHTMLElements?: string[];
|
|
28
|
+
/** CSS class for the container wrapper */
|
|
29
|
+
containerClassName?: string;
|
|
30
|
+
/** CSS class for the character counter element */
|
|
31
|
+
counterClassName?: string;
|
|
32
|
+
/** CSS class applied when character count is normal */
|
|
33
|
+
normalClassName?: string;
|
|
34
|
+
/** CSS class applied when character count is near the limit */
|
|
35
|
+
warningClassName?: string;
|
|
36
|
+
/** CSS class applied when character count reaches the limit */
|
|
37
|
+
dangerClassName?: string;
|
|
38
|
+
/** Number of characters remaining before warning state (default: 10) */
|
|
39
|
+
warningThreshold?: number;
|
|
40
|
+
/** Custom function to format the counter display */
|
|
41
|
+
formatCounter?: (current: number, max: number) => ReactNode;
|
|
42
|
+
/** Whether to show the character counter (default: true) */
|
|
43
|
+
showCounter?: boolean;
|
|
44
|
+
/** Whether to show counter even when input is empty (default: false) */
|
|
45
|
+
showWhenEmpty?: boolean;
|
|
46
|
+
}>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A wrapper component that adds a character counter to text inputs and textareas.
|
|
50
|
+
* Automatically detects the maxLength property from the child input and displays
|
|
51
|
+
* the current character count with visual feedback.
|
|
52
|
+
*
|
|
53
|
+
* Features:
|
|
54
|
+
* - Automatic character counting from input/textarea children
|
|
55
|
+
* - Visual feedback with customizable warning and danger states
|
|
56
|
+
* - Configurable warning threshold
|
|
57
|
+
* - Custom counter formatting
|
|
58
|
+
* - Support for multiple input types (text, email, password, textarea, etc.)
|
|
59
|
+
* - Show/hide counter toggle
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* // Basic usage with TextInput
|
|
63
|
+
* <CharacterCountInput>
|
|
64
|
+
* <TextInput
|
|
65
|
+
* name="bio"
|
|
66
|
+
* type="text"
|
|
67
|
+
* maxLength={100}
|
|
68
|
+
* placeholder="Enter your bio"
|
|
69
|
+
* />
|
|
70
|
+
* </CharacterCountInput>
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* // With custom styling and warning threshold
|
|
74
|
+
* <CharacterCountInput
|
|
75
|
+
* warningThreshold={20}
|
|
76
|
+
* normalClassName="text-muted"
|
|
77
|
+
* warningClassName="text-warning"
|
|
78
|
+
* dangerClassName="text-danger"
|
|
79
|
+
* >
|
|
80
|
+
* <TextAreaInput name="message" maxLength={200} />
|
|
81
|
+
* </CharacterCountInput>
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* // With custom counter format
|
|
85
|
+
* <CharacterCountInput
|
|
86
|
+
* formatCounter={(current, max) => (
|
|
87
|
+
* <span>{max - current} characters remaining</span>
|
|
88
|
+
* )}
|
|
89
|
+
* >
|
|
90
|
+
* <TextInput name="title" maxLength={50} />
|
|
91
|
+
* </CharacterCountInput>
|
|
92
|
+
*/
|
|
93
|
+
export const CharacterCountInput: FC<CharacterCountInputProps> = ({
|
|
94
|
+
validHTMLElements,
|
|
95
|
+
children,
|
|
96
|
+
containerClassName = 'text-end',
|
|
97
|
+
counterClassName = '',
|
|
98
|
+
normalClassName = 'text-muted',
|
|
99
|
+
warningClassName = 'text-warning',
|
|
100
|
+
dangerClassName = 'text-danger',
|
|
101
|
+
warningThreshold = 10,
|
|
102
|
+
formatCounter,
|
|
103
|
+
showCounter = true,
|
|
104
|
+
showWhenEmpty = false,
|
|
105
|
+
}) => {
|
|
106
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
107
|
+
const [characterCount, setCharacterCount] = useState<number>(0);
|
|
108
|
+
const [maxLength, setMaxLength] = useState<number | null>(null);
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!containerRef.current) return;
|
|
112
|
+
|
|
113
|
+
// Extract maxLength from child props
|
|
114
|
+
const childProps = (
|
|
115
|
+
children as ReactElement<{
|
|
116
|
+
type?: string;
|
|
117
|
+
as?: string;
|
|
118
|
+
maxLength?: number;
|
|
119
|
+
}>
|
|
120
|
+
)?.props;
|
|
121
|
+
|
|
122
|
+
if (childProps?.maxLength) {
|
|
123
|
+
setMaxLength(childProps.maxLength);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Use MutationObserver to wait for input element to be rendered
|
|
127
|
+
const findAndAttachInput = () => {
|
|
128
|
+
const input = containerRef.current?.querySelector('input, textarea') as
|
|
129
|
+
| HTMLInputElement
|
|
130
|
+
| HTMLTextAreaElement
|
|
131
|
+
| null;
|
|
132
|
+
|
|
133
|
+
if (!input) return null;
|
|
134
|
+
|
|
135
|
+
const inputReader = () => {
|
|
136
|
+
setCharacterCount(input.value?.length ?? 0);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
input.addEventListener('input', inputReader);
|
|
140
|
+
input.addEventListener('keyup', inputReader);
|
|
141
|
+
inputReader(); // Initial read
|
|
142
|
+
|
|
143
|
+
return () => {
|
|
144
|
+
input.removeEventListener('input', inputReader);
|
|
145
|
+
input.removeEventListener('keyup', inputReader);
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Try to find input immediately
|
|
150
|
+
let cleanup = findAndAttachInput();
|
|
151
|
+
|
|
152
|
+
// If not found, wait for it to be rendered
|
|
153
|
+
if (!cleanup && containerRef.current) {
|
|
154
|
+
const observer = new MutationObserver(() => {
|
|
155
|
+
if (!cleanup) {
|
|
156
|
+
cleanup = findAndAttachInput();
|
|
157
|
+
if (cleanup) {
|
|
158
|
+
observer.disconnect();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
observer.observe(containerRef.current, {
|
|
164
|
+
childList: true,
|
|
165
|
+
subtree: true,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return () => {
|
|
169
|
+
observer.disconnect();
|
|
170
|
+
cleanup?.();
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return cleanup || undefined;
|
|
175
|
+
}, [children, validHTMLElements]);
|
|
176
|
+
|
|
177
|
+
const getCounterClassName = (): string => {
|
|
178
|
+
if (maxLength === null) return normalClassName;
|
|
179
|
+
|
|
180
|
+
if (characterCount === maxLength) return dangerClassName;
|
|
181
|
+
if (characterCount >= maxLength - warningThreshold) return warningClassName;
|
|
182
|
+
|
|
183
|
+
return normalClassName;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const renderCounter = (): ReactNode => {
|
|
187
|
+
if (!showCounter || maxLength === null) return null;
|
|
188
|
+
if (!showWhenEmpty && characterCount === 0) return null;
|
|
189
|
+
|
|
190
|
+
const className = `${counterClassName} ${getCounterClassName()}`.trim();
|
|
191
|
+
const content = formatCounter
|
|
192
|
+
? formatCounter(characterCount, maxLength)
|
|
193
|
+
: `${characterCount} / ${maxLength}`;
|
|
194
|
+
|
|
195
|
+
return <Form.Text className={className}>{content}</Form.Text>;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<div ref={containerRef} className={containerClassName}>
|
|
200
|
+
{children}
|
|
201
|
+
{renderCounter()}
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import React, { type FC } from 'react';
|
|
2
|
+
import { Form } from 'react-bootstrap';
|
|
3
|
+
import { Controller, useFormContext } from 'react-hook-form';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Props for the CheckInput component
|
|
7
|
+
*/
|
|
8
|
+
export type CheckInputProps = {
|
|
9
|
+
/** The name of the checkbox input field */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Label text displayed next to the checkbox */
|
|
12
|
+
label?: string;
|
|
13
|
+
/** Whether the checkbox is required */
|
|
14
|
+
required?: boolean;
|
|
15
|
+
/** Current checked state (standalone mode) */
|
|
16
|
+
value?: boolean;
|
|
17
|
+
/** Callback function called when the checked state changes */
|
|
18
|
+
onChange?: (checked: boolean) => void;
|
|
19
|
+
/** Callback function called when the checkbox loses focus */
|
|
20
|
+
onBlur?: () => void;
|
|
21
|
+
/** Whether the checkbox is disabled */
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
/** Custom HTML id for the checkbox element */
|
|
24
|
+
id?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* CheckInput - A checkbox input component with react-hook-form integration
|
|
29
|
+
*
|
|
30
|
+
* Provides a checkbox input that works both standalone and with react-hook-form.
|
|
31
|
+
* Automatically integrates with FormProvider when available, providing validation
|
|
32
|
+
* and error handling.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```tsx
|
|
36
|
+
* // With react-hook-form
|
|
37
|
+
* <CheckInput
|
|
38
|
+
* name="agreeToTerms"
|
|
39
|
+
* label="I agree to the terms"
|
|
40
|
+
* required
|
|
41
|
+
* />
|
|
42
|
+
*
|
|
43
|
+
* // Standalone mode
|
|
44
|
+
* <CheckInput
|
|
45
|
+
* name="newsletter"
|
|
46
|
+
* label="Subscribe to newsletter"
|
|
47
|
+
* value={subscribed}
|
|
48
|
+
* onChange={setSubscribed}
|
|
49
|
+
* />
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export const CheckInput: FC<CheckInputProps> = ({
|
|
53
|
+
name,
|
|
54
|
+
label,
|
|
55
|
+
required = false,
|
|
56
|
+
value,
|
|
57
|
+
onChange,
|
|
58
|
+
onBlur,
|
|
59
|
+
disabled = false,
|
|
60
|
+
id,
|
|
61
|
+
}) => {
|
|
62
|
+
const formContext = useFormContext();
|
|
63
|
+
|
|
64
|
+
// Helper to safely get nested error
|
|
65
|
+
const getFieldError = (fieldName: string) => {
|
|
66
|
+
try {
|
|
67
|
+
const error = formContext?.formState?.errors?.[fieldName];
|
|
68
|
+
return error?.message as string | undefined;
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const errorMessage = getFieldError(name);
|
|
75
|
+
const inputId = id || `check-input-${name}`;
|
|
76
|
+
|
|
77
|
+
// Integrated with react-hook-form
|
|
78
|
+
if (formContext) {
|
|
79
|
+
return (
|
|
80
|
+
<Controller
|
|
81
|
+
name={name}
|
|
82
|
+
control={formContext.control}
|
|
83
|
+
rules={{
|
|
84
|
+
required: required ? `${label || 'This field'} is required` : false,
|
|
85
|
+
}}
|
|
86
|
+
render={({ field }) => (
|
|
87
|
+
<Form.Check
|
|
88
|
+
{...field}
|
|
89
|
+
id={inputId}
|
|
90
|
+
type="checkbox"
|
|
91
|
+
label={label}
|
|
92
|
+
checked={field.value ?? false}
|
|
93
|
+
onChange={(e) => {
|
|
94
|
+
const checked = e.target.checked;
|
|
95
|
+
field.onChange(checked);
|
|
96
|
+
onChange?.(checked);
|
|
97
|
+
}}
|
|
98
|
+
onBlur={() => {
|
|
99
|
+
field.onBlur();
|
|
100
|
+
onBlur?.();
|
|
101
|
+
}}
|
|
102
|
+
disabled={disabled}
|
|
103
|
+
required={required}
|
|
104
|
+
isInvalid={!!errorMessage}
|
|
105
|
+
feedback={errorMessage}
|
|
106
|
+
feedbackType="invalid"
|
|
107
|
+
/>
|
|
108
|
+
)}
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Standalone mode (no form context)
|
|
114
|
+
return (
|
|
115
|
+
<Form.Check
|
|
116
|
+
id={inputId}
|
|
117
|
+
type="checkbox"
|
|
118
|
+
label={label}
|
|
119
|
+
checked={value ?? false}
|
|
120
|
+
onChange={(e) => onChange?.(e.target.checked)}
|
|
121
|
+
onBlur={onBlur}
|
|
122
|
+
disabled={disabled}
|
|
123
|
+
required={required}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
};
|