@capyx/components-library 0.0.10 → 0.0.12
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/dist/addons/AutocompleteInput.d.ts +12 -0
- package/dist/addons/AutocompleteInput.d.ts.map +1 -1
- package/dist/addons/AutocompleteInput.js +106 -62
- package/dist/components/SelectInput.d.ts +5 -1
- package/dist/components/SelectInput.d.ts.map +1 -1
- package/dist/components/SelectInput.js +30 -8
- package/package.json +8 -10
|
@@ -7,6 +7,18 @@ export type AutocompleteInputProps = PropsWithChildren<{
|
|
|
7
7
|
suggestions: string[];
|
|
8
8
|
/** Whether to highlight the first suggestion (default: true) */
|
|
9
9
|
highlightFirstSuggestion?: boolean;
|
|
10
|
+
/** Custom class name for the suggestions dropdown container */
|
|
11
|
+
listClassName?: string;
|
|
12
|
+
/** Custom style for the suggestions dropdown container */
|
|
13
|
+
listStyle?: React.CSSProperties;
|
|
14
|
+
/** Custom class name for individual suggestion items */
|
|
15
|
+
itemClassName?: string;
|
|
16
|
+
/** Custom style for individual suggestion items */
|
|
17
|
+
itemStyle?: React.CSSProperties;
|
|
18
|
+
/** Custom class name for the currently highlighted suggestion item */
|
|
19
|
+
activeItemClassName?: string;
|
|
20
|
+
/** Custom style for the currently highlighted suggestion item */
|
|
21
|
+
activeItemStyle?: React.CSSProperties;
|
|
10
22
|
}>;
|
|
11
23
|
/**
|
|
12
24
|
* AutocompleteInput - A wrapper addon that adds autocomplete/suggestions to text inputs
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AutocompleteInput.d.ts","sourceRoot":"","sources":["../../lib/addons/AutocompleteInput.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,
|
|
1
|
+
{"version":3,"file":"AutocompleteInput.d.ts","sourceRoot":"","sources":["../../lib/addons/AutocompleteInput.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAiB,iBAAiB,EAAgB,MAAM,OAAO,CAAC;AAKhF;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,iBAAiB,CAAC;IACtD,kDAAkD;IAClD,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,gEAAgE;IAChE,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAChC,wDAAwD;IACxD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mDAAmD;IACnD,SAAS,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAChC,sEAAsE;IACtE,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,iEAAiE;IACjE,eAAe,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;CACtC,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,iBAAiB,EAAE,EAAE,CAAC,sBAAsB,CA6LxD,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { cloneElement, useEffect, useRef, useState } from 'react';
|
|
3
|
-
import
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cloneElement, useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { ListGroup } from 'react-bootstrap';
|
|
4
4
|
import { useFormContext } from 'react-hook-form';
|
|
5
5
|
/**
|
|
6
6
|
* AutocompleteInput - A wrapper addon that adds autocomplete/suggestions to text inputs
|
|
@@ -30,72 +30,116 @@ import { useFormContext } from 'react-hook-form';
|
|
|
30
30
|
* </AutocompleteInput>
|
|
31
31
|
* ```
|
|
32
32
|
*/
|
|
33
|
-
export const AutocompleteInput = ({ suggestions: allSuggestions, children, highlightFirstSuggestion = true, }) => {
|
|
33
|
+
export const AutocompleteInput = ({ suggestions: allSuggestions, children, highlightFirstSuggestion = true, listClassName, listStyle, itemClassName, itemStyle, activeItemClassName, activeItemStyle, }) => {
|
|
34
34
|
const formContext = useFormContext();
|
|
35
35
|
const [filteredSuggestions, setFilteredSuggestions] = useState([]);
|
|
36
|
-
const [
|
|
37
|
-
const
|
|
38
|
-
|
|
36
|
+
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
37
|
+
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
|
38
|
+
const containerRef = useRef(null);
|
|
39
|
+
const listRef = useRef(null);
|
|
40
|
+
// Extract child element and its props
|
|
39
41
|
const childElement = children;
|
|
40
42
|
const childProps = childElement?.props;
|
|
41
43
|
const name = childProps?.name || '';
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
// Filter suggestions based on the current input value
|
|
45
|
+
const filterSuggestions = useCallback((value) => {
|
|
46
|
+
const trimmed = value.trim().toLowerCase();
|
|
47
|
+
if (trimmed.length === 0) {
|
|
48
|
+
setFilteredSuggestions([]);
|
|
49
|
+
setShowSuggestions(false);
|
|
50
|
+
return;
|
|
48
51
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (trimmedValue.length === 0)
|
|
57
|
-
return [];
|
|
58
|
-
return allSuggestions.filter((suggestion) => suggestion.toLowerCase().includes(trimmedValue));
|
|
59
|
-
};
|
|
60
|
-
const onSuggestionsFetchRequested = ({ value }) => {
|
|
61
|
-
setFilteredSuggestions(getSuggestions(value));
|
|
62
|
-
};
|
|
63
|
-
const onSuggestionsClearRequested = () => {
|
|
64
|
-
setFilteredSuggestions([]);
|
|
65
|
-
};
|
|
66
|
-
const getSuggestionValue = (suggestion) => suggestion;
|
|
67
|
-
const renderSuggestion = (suggestion) => _jsx("span", { children: suggestion });
|
|
68
|
-
const handleChange = (_event, { newValue }) => {
|
|
69
|
-
setInputValue(newValue);
|
|
70
|
-
// Update form context if available
|
|
52
|
+
const filtered = allSuggestions.filter((s) => s.toLowerCase().includes(trimmed));
|
|
53
|
+
setFilteredSuggestions(filtered);
|
|
54
|
+
setShowSuggestions(filtered.length > 0);
|
|
55
|
+
setHighlightedIndex(highlightFirstSuggestion ? 0 : -1);
|
|
56
|
+
}, [allSuggestions, highlightFirstSuggestion]);
|
|
57
|
+
// Apply a selected suggestion to the input
|
|
58
|
+
const selectSuggestion = useCallback((suggestion) => {
|
|
71
59
|
if (formContext && name) {
|
|
72
|
-
formContext.setValue(name,
|
|
60
|
+
formContext.setValue(name, suggestion, { shouldValidate: true });
|
|
73
61
|
}
|
|
74
|
-
|
|
62
|
+
childProps?.onChange?.(suggestion);
|
|
63
|
+
setShowSuggestions(false);
|
|
64
|
+
setFilteredSuggestions([]);
|
|
65
|
+
}, [formContext, name, childProps?.onChange]);
|
|
66
|
+
// Intercept onChange from the child to filter suggestions
|
|
67
|
+
const handleChange = useCallback((newValue) => {
|
|
75
68
|
childProps?.onChange?.(newValue);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
69
|
+
filterSuggestions(newValue);
|
|
70
|
+
}, [childProps?.onChange, filterSuggestions]);
|
|
71
|
+
// Keyboard navigation for the suggestions dropdown
|
|
72
|
+
const handleKeyDown = useCallback((e) => {
|
|
73
|
+
if (!showSuggestions || filteredSuggestions.length === 0)
|
|
74
|
+
return;
|
|
75
|
+
switch (e.key) {
|
|
76
|
+
case 'ArrowDown':
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
setHighlightedIndex((prev) => prev < filteredSuggestions.length - 1 ? prev + 1 : 0);
|
|
79
|
+
break;
|
|
80
|
+
case 'ArrowUp':
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
setHighlightedIndex((prev) => prev > 0 ? prev - 1 : filteredSuggestions.length - 1);
|
|
83
|
+
break;
|
|
84
|
+
case 'Enter':
|
|
85
|
+
if (highlightedIndex >= 0 &&
|
|
86
|
+
highlightedIndex < filteredSuggestions.length) {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
selectSuggestion(filteredSuggestions[highlightedIndex]);
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
case 'Escape':
|
|
92
|
+
setShowSuggestions(false);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}, [showSuggestions, filteredSuggestions, highlightedIndex, selectSuggestion]);
|
|
96
|
+
// Scroll the highlighted item into view when navigating with keyboard
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (highlightedIndex < 0 || !listRef.current)
|
|
99
|
+
return;
|
|
100
|
+
const items = listRef.current.querySelectorAll('[role="option"]');
|
|
101
|
+
const item = items[highlightedIndex];
|
|
102
|
+
item?.scrollIntoView({ block: 'nearest' });
|
|
103
|
+
}, [highlightedIndex]);
|
|
104
|
+
// Close suggestions when clicking outside
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
const handleClickOutside = (e) => {
|
|
107
|
+
if (containerRef.current &&
|
|
108
|
+
!containerRef.current.contains(e.target)) {
|
|
109
|
+
setShowSuggestions(false);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
113
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
114
|
+
}, []);
|
|
115
|
+
// Clone the child element, overriding onChange and onBlur to hook into the autocomplete logic
|
|
116
|
+
const enhancedChild = cloneElement(childElement, {
|
|
117
|
+
onChange: handleChange,
|
|
118
|
+
onBlur: () => {
|
|
119
|
+
// Delay closing so a suggestion click can register first
|
|
120
|
+
setTimeout(() => setShowSuggestions(false), 150);
|
|
121
|
+
childProps?.onBlur?.();
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
return (_jsxs("div", { ref: containerRef, style: { position: 'relative' }, onKeyDown: handleKeyDown, children: [enhancedChild, showSuggestions && filteredSuggestions.length > 0 && (_jsx(ListGroup, { ref: listRef, role: "listbox", className: listClassName, style: {
|
|
125
|
+
position: 'absolute',
|
|
126
|
+
top: '100%',
|
|
127
|
+
left: 0,
|
|
128
|
+
right: 0,
|
|
129
|
+
zIndex: 1000,
|
|
130
|
+
maxHeight: '200px',
|
|
131
|
+
overflowY: 'auto',
|
|
132
|
+
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
|
133
|
+
...listStyle,
|
|
134
|
+
}, children: filteredSuggestions.map((suggestion, index) => {
|
|
135
|
+
const isActive = index === highlightedIndex;
|
|
136
|
+
return (_jsx(ListGroup.Item, { role: "option", action: true, active: isActive, "aria-selected": isActive, className: isActive ? activeItemClassName : itemClassName, onMouseDown: (e) => {
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
selectSuggestion(suggestion);
|
|
139
|
+
}, onMouseEnter: () => setHighlightedIndex(index), style: {
|
|
140
|
+
cursor: 'pointer',
|
|
141
|
+
...itemStyle,
|
|
142
|
+
...(isActive ? activeItemStyle : undefined),
|
|
143
|
+
}, children: suggestion }, suggestion));
|
|
144
|
+
}) }))] }));
|
|
101
145
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FC } from 'react';
|
|
1
|
+
import type { FC, JSX } from 'react';
|
|
2
2
|
/**
|
|
3
3
|
* Props for the SelectInput component
|
|
4
4
|
*/
|
|
@@ -23,6 +23,10 @@ export type SelectInputProps = {
|
|
|
23
23
|
required?: boolean;
|
|
24
24
|
/** Array of option values to display in bold */
|
|
25
25
|
highlightValues?: string[];
|
|
26
|
+
/** Function to render the option elements */
|
|
27
|
+
renderOption?: (option: string, optionKey: string, isHighlighted: boolean) => JSX.Element;
|
|
28
|
+
/** Sort options in ascending or descending order (default: "desc") */
|
|
29
|
+
sortOptions?: 'asc' | 'desc' | 'highlight-asc' | 'highlight-desc';
|
|
26
30
|
};
|
|
27
31
|
/**
|
|
28
32
|
* SelectInput - A dropdown select component with react-hook-form integration
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelectInput.d.ts","sourceRoot":"","sources":["../../lib/components/SelectInput.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,EAAE,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"SelectInput.d.ts","sourceRoot":"","sources":["../../lib/components/SelectInput.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,EAAE,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAIlD;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC9B,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iCAAiC;IACjC,WAAW,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAC1B,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gDAAgD;IAChD,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,6CAA6C;IAC7C,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,KAAK,GAAG,CAAC,OAAO,CAAC;IAC1F,sEAAsE;IACtE,WAAW,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,eAAe,GAAG,gBAAgB,CAAC;CAClE,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC,gBAAgB,CA+G5C,CAAC"}
|
|
@@ -27,7 +27,7 @@ import { Controller, useFormContext } from 'react-hook-form';
|
|
|
27
27
|
* />
|
|
28
28
|
* ```
|
|
29
29
|
*/
|
|
30
|
-
export const SelectInput = ({ name, label, options = [], helpText = 'Select...', controlSize, value, onChange, disabled = false, required = false, highlightValues = [], }) => {
|
|
30
|
+
export const SelectInput = ({ name, label, options = [], helpText = 'Select...', controlSize, value, onChange, disabled = false, required = false, highlightValues = [], renderOption, sortOptions = 'desc' }) => {
|
|
31
31
|
const formContext = useFormContext();
|
|
32
32
|
const getFieldError = (fieldName) => {
|
|
33
33
|
try {
|
|
@@ -43,13 +43,35 @@ export const SelectInput = ({ name, label, options = [], helpText = 'Select...',
|
|
|
43
43
|
const handleSelectChange = (event) => {
|
|
44
44
|
onChange?.(event.currentTarget.value);
|
|
45
45
|
};
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
const renderOptionInternal = renderOption || ((option, optionKey, isHighlighted) => {
|
|
47
|
+
return (_jsx("option", { style: isHighlighted ? { fontWeight: 'bold' } : {}, value: optionKey, children: option }, optionKey));
|
|
48
|
+
});
|
|
49
|
+
const renderOptions = () => (_jsxs(_Fragment, { children: [_jsx("option", { value: "", children: helpText.trim() }), options
|
|
50
|
+
.sort((a, b) => {
|
|
51
|
+
const aHighlighted = highlightValues.includes(a);
|
|
52
|
+
const bHighlighted = highlightValues.includes(b);
|
|
53
|
+
switch (sortOptions) {
|
|
54
|
+
case 'asc':
|
|
55
|
+
return a.localeCompare(b);
|
|
56
|
+
case 'desc':
|
|
57
|
+
return b.localeCompare(a);
|
|
58
|
+
case 'highlight-asc':
|
|
59
|
+
if (aHighlighted && !bHighlighted)
|
|
60
|
+
return -1;
|
|
61
|
+
if (!aHighlighted && bHighlighted)
|
|
62
|
+
return 1;
|
|
63
|
+
return a.localeCompare(b);
|
|
64
|
+
case 'highlight-desc':
|
|
65
|
+
if (aHighlighted && !bHighlighted)
|
|
66
|
+
return -1;
|
|
67
|
+
if (!aHighlighted && bHighlighted)
|
|
68
|
+
return 1;
|
|
69
|
+
return b.localeCompare(a);
|
|
70
|
+
default:
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
.map((option) => renderOptionInternal(option, option.replace(/ /g, ''), highlightValues.includes(option)))] }));
|
|
53
75
|
// Integrated with react-hook-form
|
|
54
76
|
if (formContext) {
|
|
55
77
|
return (_jsx(Controller, { name: name, control: formContext.control, rules: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capyx/components-library",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "Capyx Components Library for forms across applications",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -52,21 +52,20 @@
|
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@biomejs/biome": "^2.4.4",
|
|
54
54
|
"@chromatic-com/storybook": "^5.0.1",
|
|
55
|
-
"@storybook/addon-a11y": "^10.2.
|
|
56
|
-
"@storybook/addon-docs": "^10.2.
|
|
57
|
-
"@storybook/addon-onboarding": "^10.2.
|
|
58
|
-
"@storybook/addon-vitest": "^10.2.
|
|
59
|
-
"@storybook/react-vite": "^10.2.
|
|
55
|
+
"@storybook/addon-a11y": "^10.2.13",
|
|
56
|
+
"@storybook/addon-docs": "^10.2.13",
|
|
57
|
+
"@storybook/addon-onboarding": "^10.2.13",
|
|
58
|
+
"@storybook/addon-vitest": "^10.2.13",
|
|
59
|
+
"@storybook/react-vite": "^10.2.13",
|
|
60
60
|
"@types/dateformat": "^5.0.3",
|
|
61
61
|
"@types/lodash.debounce": "^4.0.9",
|
|
62
|
-
"@types/node": "^25.3.
|
|
62
|
+
"@types/node": "^25.3.3",
|
|
63
63
|
"@types/react": "^19.2.14",
|
|
64
|
-
"@types/react-autosuggest": "^10.1.11",
|
|
65
64
|
"@types/react-datepicker": "^7.0.0",
|
|
66
65
|
"@vitest/browser-playwright": "^4.0.18",
|
|
67
66
|
"@vitest/coverage-v8": "^4.0.18",
|
|
68
67
|
"playwright": "^1.58.2",
|
|
69
|
-
"storybook": "^10.2.
|
|
68
|
+
"storybook": "^10.2.13",
|
|
70
69
|
"typescript": "^5.9.3",
|
|
71
70
|
"vitest": "^4.0.18"
|
|
72
71
|
},
|
|
@@ -79,7 +78,6 @@
|
|
|
79
78
|
"dayjs": "^1.11.19",
|
|
80
79
|
"lodash.debounce": "^4.0.8",
|
|
81
80
|
"react": "^19.2.4",
|
|
82
|
-
"react-autosuggest": "^10.1.0",
|
|
83
81
|
"react-bootstrap": "^2.10.10",
|
|
84
82
|
"react-hook-form": "^7.71.2",
|
|
85
83
|
"react-quill-new": "^3.8.3"
|