@capyx/components-library 0.0.6 → 0.0.8

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.
@@ -1,18 +1,26 @@
1
- import type { FC } from 'react';
1
+ import { type FC } from 'react';
2
2
  /**
3
3
  * Props for the TagsInput component
4
4
  */
5
5
  export type TagsInputProps = {
6
+ /** Field name — required for form integration and the hidden native input */
7
+ name: string;
6
8
  /** Array of current tag values */
7
- value: string[];
9
+ value?: string[];
8
10
  /** Callback function called when tags change */
9
- onChange: (value: string[]) => void;
11
+ onChange?: (value: string[]) => void;
10
12
  /** Placeholder text shown when no tags are entered (default: "Add tags...") */
11
13
  placeholder?: string;
12
14
  /** Whether the input is disabled */
13
15
  disabled?: boolean;
14
- /** Name attribute for the input field */
15
- name?: string;
16
+ /** Whether at least one tag is required */
17
+ required?: boolean;
18
+ /** Label used in validation error messages (e.g. "Skills is required") */
19
+ label?: string;
20
+ /** Minimum number of tags required */
21
+ min?: number;
22
+ /** Maximum number of tags allowed */
23
+ max?: number;
16
24
  };
17
25
  /**
18
26
  * TagsInput - A multi-tag input component using Material-UI Autocomplete
@@ -21,6 +29,14 @@ export type TagsInputProps = {
21
29
  * Automatically trims whitespace and filters empty values. Tags are displayed
22
30
  * as chips with delete functionality.
23
31
  *
32
+ * Supports validation via:
33
+ * - **react-hook-form**: When wrapped in a `FormProvider`, the component
34
+ * registers itself with `Controller` and validates `required`, `min`,
35
+ * and `max` rules automatically.
36
+ * - **Native HTML5**: A hidden `<input>` element participates in native
37
+ * constraint validation (`required` attribute), making it compatible with
38
+ * any form library that relies on `checkValidity()` / `reportValidity()`.
39
+ *
24
40
  * @example
25
41
  * ```tsx
26
42
  * <TagsInput
@@ -28,6 +44,9 @@ export type TagsInputProps = {
28
44
  * value={tags}
29
45
  * onChange={setTags}
30
46
  * placeholder="Add skills..."
47
+ * required
48
+ * min={1}
49
+ * max={5}
31
50
  * />
32
51
  * ```
33
52
  */
@@ -1 +1 @@
1
- {"version":3,"file":"TagsInput.d.ts","sourceRoot":"","sources":["../../lib/components/TagsInput.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAEhC;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC5B,kCAAkC;IAClC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,gDAAgD;IAChD,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACpC,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,SAAS,EAAE,EAAE,CAAC,cAAc,CAiFxC,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,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,CAgPxC,CAAC"}
@@ -1,6 +1,8 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createElement as _createElement } from "react";
3
- import { Autocomplete, Chip, TextField } from '@mui/material';
3
+ import { Autocomplete, Chip, FormHelperText, TextField } from '@mui/material';
4
+ import { useEffect, useRef } from 'react';
5
+ import { Controller, useFormContext } from 'react-hook-form';
4
6
  /**
5
7
  * TagsInput - A multi-tag input component using Material-UI Autocomplete
6
8
  *
@@ -8,6 +10,14 @@ import { Autocomplete, Chip, TextField } from '@mui/material';
8
10
  * Automatically trims whitespace and filters empty values. Tags are displayed
9
11
  * as chips with delete functionality.
10
12
  *
13
+ * Supports validation via:
14
+ * - **react-hook-form**: When wrapped in a `FormProvider`, the component
15
+ * registers itself with `Controller` and validates `required`, `min`,
16
+ * and `max` rules automatically.
17
+ * - **Native HTML5**: A hidden `<input>` element participates in native
18
+ * constraint validation (`required` attribute), making it compatible with
19
+ * any form library that relies on `checkValidity()` / `reportValidity()`.
20
+ *
11
21
  * @example
12
22
  * ```tsx
13
23
  * <TagsInput
@@ -15,53 +25,138 @@ import { Autocomplete, Chip, TextField } from '@mui/material';
15
25
  * value={tags}
16
26
  * onChange={setTags}
17
27
  * placeholder="Add skills..."
28
+ * required
29
+ * min={1}
30
+ * max={5}
18
31
  * />
19
32
  * ```
20
33
  */
21
- export const TagsInput = ({ value = [], onChange, placeholder = 'Add tags...', disabled = false, name, }) => {
22
- return (_jsx(Autocomplete, { multiple: true, freeSolo: true, options: [], value: value, onChange: (_event, newValue) => {
23
- // Filter out empty strings and trim whitespace
24
- const cleanedValues = newValue
25
- .map((v) => (typeof v === 'string' ? v.trim() : v))
26
- .filter((v) => v.length > 0);
27
- onChange(cleanedValues);
28
- }, disabled: disabled, renderValue: (tagValue, getTagProps) => tagValue.map((option, index) => (_createElement(Chip, { ...getTagProps({ index }), key: option, label: option, sx: {
29
- backgroundColor: '#212529',
30
- color: '#ffffff',
31
- '& .MuiChip-deleteIcon': {
32
- color: 'rgba(255, 255, 255, 0.7)',
34
+ export const TagsInput = ({ name, value: valueProp = [], onChange, placeholder = 'Add tags...', disabled = false, required = false, label, min, max, }) => {
35
+ const formContext = useFormContext();
36
+ const nativeInputRef = useRef(null);
37
+ const fieldLabel = label || 'This field';
38
+ // Keep the hidden native input's custom validity in sync with the tag count
39
+ // so that form.checkValidity() / form.reportValidity() report the right error.
40
+ useEffect(() => {
41
+ const input = nativeInputRef.current;
42
+ if (!input)
43
+ return;
44
+ const count = valueProp.length;
45
+ if (required && count === 0) {
46
+ input.setCustomValidity(`${fieldLabel} is required`);
47
+ }
48
+ else if (min != null && count < min) {
49
+ input.setCustomValidity(`At least ${min} tag${min === 1 ? '' : 's'} required`);
50
+ }
51
+ else if (max != null && count > max) {
52
+ input.setCustomValidity(`No more than ${max} tag${max === 1 ? '' : 's'} allowed`);
53
+ }
54
+ else {
55
+ input.setCustomValidity('');
56
+ }
57
+ }, [valueProp, required, min, max, fieldLabel]);
58
+ const getFieldError = (fieldName) => {
59
+ try {
60
+ const error = formContext?.formState?.errors?.[fieldName];
61
+ return error?.message;
62
+ }
63
+ catch {
64
+ return undefined;
65
+ }
66
+ };
67
+ const buildRules = () => ({
68
+ required: required ? `${fieldLabel} is required` : false,
69
+ validate: {
70
+ ...(min != null
71
+ ? {
72
+ min: (v) => v.length >= min ||
73
+ `At least ${min} tag${min === 1 ? '' : 's'} required`,
74
+ }
75
+ : {}),
76
+ ...(max != null
77
+ ? {
78
+ max: (v) => v.length <= max ||
79
+ `No more than ${max} tag${max === 1 ? '' : 's'} allowed`,
80
+ }
81
+ : {}),
82
+ },
83
+ });
84
+ const cleanValues = (newValue) => {
85
+ const cleaned = newValue
86
+ .map((v) => (typeof v === 'string' ? v.trim() : v))
87
+ .filter((v) => v.length > 0);
88
+ // Enforce max cap
89
+ if (max != null && cleaned.length > max) {
90
+ return cleaned.slice(0, max);
91
+ }
92
+ return cleaned;
93
+ };
94
+ const renderChips = (tagValue, getTagProps) => tagValue.map((option, index) => (_createElement(Chip, { ...getTagProps({ index }), key: option, label: option, sx: {
95
+ backgroundColor: '#212529',
96
+ color: '#ffffff',
97
+ '& .MuiChip-deleteIcon': {
98
+ color: 'rgba(255, 255, 255, 0.7)',
99
+ userSelect: 'none',
100
+ WebkitUserSelect: 'none',
101
+ MozUserSelect: 'none',
102
+ msUserSelect: 'none',
103
+ pointerEvents: 'auto',
104
+ '&::selection': {
105
+ backgroundColor: 'transparent',
106
+ color: 'transparent',
107
+ },
108
+ '& svg': {
33
109
  userSelect: 'none',
34
110
  WebkitUserSelect: 'none',
35
- MozUserSelect: 'none',
36
- msUserSelect: 'none',
37
- pointerEvents: 'auto',
38
- '&::selection': {
39
- backgroundColor: 'transparent',
40
- color: 'transparent',
41
- },
42
- '& svg': {
43
- userSelect: 'none',
44
- WebkitUserSelect: 'none',
45
- pointerEvents: 'none',
46
- },
47
- '&:hover': {
48
- color: '#dc3545',
49
- },
111
+ pointerEvents: 'none',
50
112
  },
51
- } }))), renderInput: (params) => (_jsx(TextField, { ...params, name: name, placeholder: value.length === 0 ? placeholder : undefined, variant: "outlined", size: "small", sx: {
52
- '& .MuiOutlinedInput-root': {
53
- padding: '4px',
54
- minHeight: '38px',
55
- '& fieldset': {
56
- borderColor: '#ced4da',
57
- },
58
- '&:hover fieldset': {
59
- borderColor: '#86b7fe',
60
- },
61
- '&.Mui-focused fieldset': {
62
- borderColor: '#86b7fe',
63
- borderWidth: '1px',
64
- },
113
+ '&:hover': {
114
+ color: '#dc3545',
65
115
  },
66
- } })) }));
116
+ },
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
+ },
133
+ });
134
+ // ── react-hook-form mode ──────────────────────────────────────────────
135
+ if (formContext) {
136
+ const errorMessage = getFieldError(name);
137
+ const hasError = !!errorMessage;
138
+ return (_jsx(Controller, { name: name, control: formContext.control, defaultValue: valueProp, rules: buildRules(), render: ({ field }) => {
139
+ const tags = field.value ?? [];
140
+ return (_jsxs("div", { children: [_jsx(Autocomplete, { multiple: true, freeSolo: true, options: [], value: tags, onChange: (_event, newValue) => {
141
+ const cleaned = cleanValues(newValue);
142
+ field.onChange(cleaned);
143
+ onChange?.(cleaned);
144
+ }, disabled: disabled, renderValue: (tagValue, getTagProps) => renderChips(tagValue, getTagProps), renderInput: (params) => (_jsx(TextField, { ...params, name: name, placeholder: tags.length === 0 ? placeholder : undefined, variant: "outlined", size: "small", error: hasError, sx: inputSx(hasError), onBlur: field.onBlur })) }), hasError && (_jsx(FormHelperText, { error: true, sx: { mx: '14px', mt: '3px' }, children: errorMessage }))] }));
145
+ } }));
146
+ }
147
+ // ── standalone mode ───────────────────────────────────────────────────
148
+ return (_jsxs("div", { children: [_jsx(Autocomplete, { multiple: true, freeSolo: true, options: [], value: valueProp, onChange: (_event, newValue) => {
149
+ const cleaned = cleanValues(newValue);
150
+ onChange?.(cleaned);
151
+ }, disabled: disabled, renderValue: (tagValue, getTagProps) => renderChips(tagValue, getTagProps), renderInput: (params) => (_jsx(TextField, { ...params, name: name, placeholder: valueProp.length === 0 ? placeholder : undefined, variant: "outlined", size: "small", sx: inputSx(false) })) }), _jsx("input", { ref: nativeInputRef, type: "text", name: `${name}__native`, value: valueProp.join(','), "aria-hidden": "true", tabIndex: -1, onChange: () => { }, style: {
152
+ position: 'absolute',
153
+ width: 0,
154
+ height: 0,
155
+ opacity: 0,
156
+ padding: 0,
157
+ margin: 0,
158
+ border: 'none',
159
+ overflow: 'hidden',
160
+ pointerEvents: 'none',
161
+ } })] }));
67
162
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capyx/components-library",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Capyx Components Library for forms across applications",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -50,38 +50,38 @@
50
50
  "build-storybook": "storybook build"
51
51
  },
52
52
  "devDependencies": {
53
- "@biomejs/biome": "^2.3.11",
54
- "@chromatic-com/storybook": "^5.0.0",
55
- "@storybook/addon-a11y": "^10.1.11",
56
- "@storybook/addon-docs": "^10.1.11",
57
- "@storybook/addon-onboarding": "^10.1.11",
58
- "@storybook/addon-vitest": "^10.1.11",
59
- "@storybook/react-vite": "^10.1.11",
53
+ "@biomejs/biome": "^2.4.3",
54
+ "@chromatic-com/storybook": "^5.0.1",
55
+ "@storybook/addon-a11y": "^10.2.10",
56
+ "@storybook/addon-docs": "^10.2.10",
57
+ "@storybook/addon-onboarding": "^10.2.10",
58
+ "@storybook/addon-vitest": "^10.2.10",
59
+ "@storybook/react-vite": "^10.2.10",
60
60
  "@types/dateformat": "^5.0.3",
61
61
  "@types/lodash.debounce": "^4.0.9",
62
- "@types/node": "^25.0.9",
63
- "@types/react": "^19.2.8",
62
+ "@types/node": "^25.3.0",
63
+ "@types/react": "^19.2.14",
64
64
  "@types/react-autosuggest": "^10.1.11",
65
65
  "@types/react-datepicker": "^7.0.0",
66
- "@vitest/browser-playwright": "^4.0.17",
67
- "@vitest/coverage-v8": "^4.0.17",
68
- "playwright": "^1.57.0",
69
- "storybook": "^10.1.11",
66
+ "@vitest/browser-playwright": "^4.0.18",
67
+ "@vitest/coverage-v8": "^4.0.18",
68
+ "playwright": "^1.58.2",
69
+ "storybook": "^10.2.10",
70
70
  "typescript": "^5.9.3",
71
- "vitest": "^4.0.17"
71
+ "vitest": "^4.0.18"
72
72
  },
73
73
  "dependencies": {
74
74
  "@emotion/styled": "^11.14.1",
75
- "@mui/material": "^7.3.7",
76
- "@mui/x-date-pickers": "^8.25.0",
75
+ "@mui/material": "^7.3.8",
76
+ "@mui/x-date-pickers": "^8.27.0",
77
77
  "bootstrap": "^5.3.8",
78
78
  "dateformat": "^5.0.3",
79
79
  "dayjs": "^1.11.19",
80
80
  "lodash.debounce": "^4.0.8",
81
- "react": "^19.2.3",
81
+ "react": "^19.2.4",
82
82
  "react-autosuggest": "^10.1.0",
83
83
  "react-bootstrap": "^2.10.10",
84
84
  "react-hook-form": "^7.71.1",
85
- "react-quill-new": "^3.7.0"
85
+ "react-quill-new": "^3.8.3"
86
86
  }
87
87
  }