@papernote/ui 1.7.5 → 1.7.7
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/components/Autocomplete.d.ts +4 -0
- package/dist/components/Autocomplete.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.esm.js +77 -10
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +77 -10
- package/dist/index.js.map +1 -1
- package/dist/styles.css +13 -0
- package/package.json +1 -1
- package/src/components/Autocomplete.tsx +104 -28
package/dist/index.js
CHANGED
|
@@ -5027,7 +5027,7 @@ const MaskedInput = React.forwardRef(({ value, onChange, maskType = 'phone', cus
|
|
|
5027
5027
|
});
|
|
5028
5028
|
MaskedInput.displayName = 'MaskedInput';
|
|
5029
5029
|
|
|
5030
|
-
const Autocomplete = React.forwardRef(({ value, onChange, options = [], onSearch, label, placeholder = 'Search...', required = false, disabled = false, error, helperText, minChars = 1, debounceMs = 300, maxResults = 10, clearable = true, className = '', }, ref) => {
|
|
5030
|
+
const Autocomplete = React.forwardRef(({ value, onChange, options = [], onSearch, label, placeholder = 'Search...', required = false, disabled = false, error, helperText, minChars = 1, debounceMs = 300, maxResults = 10, clearable = true, className = '', showOptionsOnFocus = true, }, ref) => {
|
|
5031
5031
|
const [isOpen, setIsOpen] = React.useState(false);
|
|
5032
5032
|
const [filteredOptions, setFilteredOptions] = React.useState([]);
|
|
5033
5033
|
const [loading, setLoading] = React.useState(false);
|
|
@@ -5039,6 +5039,30 @@ const Autocomplete = React.forwardRef(({ value, onChange, options = [], onSearch
|
|
|
5039
5039
|
const labelId = React.useId();
|
|
5040
5040
|
const listboxId = React.useId();
|
|
5041
5041
|
const errorId = React.useId();
|
|
5042
|
+
// Helper to find next selectable (non-header) index
|
|
5043
|
+
const findNextSelectableIndex = (currentIndex, optionsList) => {
|
|
5044
|
+
for (let i = currentIndex + 1; i < optionsList.length; i++) {
|
|
5045
|
+
if (!optionsList[i].isHeader)
|
|
5046
|
+
return i;
|
|
5047
|
+
}
|
|
5048
|
+
return currentIndex; // Stay at current if no next selectable
|
|
5049
|
+
};
|
|
5050
|
+
// Helper to find previous selectable (non-header) index
|
|
5051
|
+
const findPrevSelectableIndex = (currentIndex, optionsList) => {
|
|
5052
|
+
for (let i = currentIndex - 1; i >= 0; i--) {
|
|
5053
|
+
if (!optionsList[i].isHeader)
|
|
5054
|
+
return i;
|
|
5055
|
+
}
|
|
5056
|
+
return -1; // Go to -1 if no previous selectable
|
|
5057
|
+
};
|
|
5058
|
+
// Helper to find first selectable (non-header) index
|
|
5059
|
+
const findFirstSelectableIndex = (optionsList) => {
|
|
5060
|
+
for (let i = 0; i < optionsList.length; i++) {
|
|
5061
|
+
if (!optionsList[i].isHeader)
|
|
5062
|
+
return i;
|
|
5063
|
+
}
|
|
5064
|
+
return -1;
|
|
5065
|
+
};
|
|
5042
5066
|
// Expose methods via ref
|
|
5043
5067
|
React.useImperativeHandle(ref, () => ({
|
|
5044
5068
|
focus: () => inputRef.current?.focus(),
|
|
@@ -5068,11 +5092,14 @@ const Autocomplete = React.forwardRef(({ value, onChange, options = [], onSearch
|
|
|
5068
5092
|
const results = await onSearch(query);
|
|
5069
5093
|
setFilteredOptions(results.slice(0, maxResults));
|
|
5070
5094
|
setIsOpen(results.length > 0);
|
|
5095
|
+
// Auto-highlight first selectable (non-header) result
|
|
5096
|
+
setHighlightedIndex(findFirstSelectableIndex(results.slice(0, maxResults)));
|
|
5071
5097
|
}
|
|
5072
5098
|
catch (err) {
|
|
5073
5099
|
console.error('Autocomplete search error:', err);
|
|
5074
5100
|
setFilteredOptions([]);
|
|
5075
5101
|
setIsOpen(false);
|
|
5102
|
+
setHighlightedIndex(-1);
|
|
5076
5103
|
}
|
|
5077
5104
|
finally {
|
|
5078
5105
|
setLoading(false);
|
|
@@ -5083,6 +5110,16 @@ const Autocomplete = React.forwardRef(({ value, onChange, options = [], onSearch
|
|
|
5083
5110
|
const filtered = filterOptions(query);
|
|
5084
5111
|
setFilteredOptions(filtered);
|
|
5085
5112
|
setIsOpen(filtered.length > 0);
|
|
5113
|
+
// Auto-highlight first selectable (non-header) result
|
|
5114
|
+
setHighlightedIndex(findFirstSelectableIndex(filtered));
|
|
5115
|
+
}
|
|
5116
|
+
};
|
|
5117
|
+
// Show static options (for focus/arrow down when input is empty)
|
|
5118
|
+
const showStaticOptions = () => {
|
|
5119
|
+
if (options.length > 0) {
|
|
5120
|
+
setFilteredOptions(options.slice(0, maxResults));
|
|
5121
|
+
setIsOpen(true);
|
|
5122
|
+
setHighlightedIndex(findFirstSelectableIndex(options.slice(0, maxResults)));
|
|
5086
5123
|
}
|
|
5087
5124
|
};
|
|
5088
5125
|
// Debounced search
|
|
@@ -5119,23 +5156,40 @@ const Autocomplete = React.forwardRef(({ value, onChange, options = [], onSearch
|
|
|
5119
5156
|
const handleKeyDown = (e) => {
|
|
5120
5157
|
if (!isOpen) {
|
|
5121
5158
|
if (e.key === 'ArrowDown') {
|
|
5122
|
-
|
|
5159
|
+
e.preventDefault();
|
|
5160
|
+
// If we have cached results from a previous search, show them
|
|
5161
|
+
if (filteredOptions.length > 0) {
|
|
5162
|
+
setIsOpen(true);
|
|
5163
|
+
setHighlightedIndex(findFirstSelectableIndex(filteredOptions));
|
|
5164
|
+
}
|
|
5165
|
+
else if (value.length < minChars && options.length > 0) {
|
|
5166
|
+
// Show static options when input is empty/below minChars
|
|
5167
|
+
showStaticOptions();
|
|
5168
|
+
}
|
|
5169
|
+
else if (value.length >= minChars) {
|
|
5170
|
+
// Otherwise trigger a new search
|
|
5171
|
+
handleSearch(value);
|
|
5172
|
+
}
|
|
5123
5173
|
}
|
|
5124
5174
|
return;
|
|
5125
5175
|
}
|
|
5126
5176
|
switch (e.key) {
|
|
5127
5177
|
case 'ArrowDown':
|
|
5128
5178
|
e.preventDefault();
|
|
5129
|
-
setHighlightedIndex((prev) => prev
|
|
5179
|
+
setHighlightedIndex((prev) => findNextSelectableIndex(prev, filteredOptions));
|
|
5130
5180
|
break;
|
|
5131
5181
|
case 'ArrowUp':
|
|
5132
5182
|
e.preventDefault();
|
|
5133
|
-
setHighlightedIndex((prev) => (prev
|
|
5183
|
+
setHighlightedIndex((prev) => findPrevSelectableIndex(prev, filteredOptions));
|
|
5134
5184
|
break;
|
|
5135
5185
|
case 'Enter':
|
|
5136
5186
|
e.preventDefault();
|
|
5137
5187
|
if (highlightedIndex >= 0 && highlightedIndex < filteredOptions.length) {
|
|
5138
|
-
|
|
5188
|
+
const option = filteredOptions[highlightedIndex];
|
|
5189
|
+
// Don't select headers
|
|
5190
|
+
if (!option.isHeader) {
|
|
5191
|
+
handleSelect(option);
|
|
5192
|
+
}
|
|
5139
5193
|
}
|
|
5140
5194
|
break;
|
|
5141
5195
|
case 'Escape':
|
|
@@ -5165,7 +5219,16 @@ const Autocomplete = React.forwardRef(({ value, onChange, options = [], onSearch
|
|
|
5165
5219
|
}
|
|
5166
5220
|
};
|
|
5167
5221
|
}, []);
|
|
5168
|
-
return (jsxRuntime.jsxs("div", { className: `relative ${className}`, children: [label && (jsxRuntime.jsxs("label", { id: labelId, className: "block text-sm font-medium text-ink-900 mb-1.5", children: [label, required && jsxRuntime.jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsx("div", { className: "absolute left-3 top-1/2 -translate-y-1/2", children: loading ? (jsxRuntime.jsx(lucideReact.Loader2, { className: "h-4 w-4 text-ink-400 animate-spin" })) : (jsxRuntime.jsx(lucideReact.Search, { className: "h-4 w-4 text-ink-400" })) }), jsxRuntime.jsx("input", { ref: inputRef, type: "text", value: value, onChange: handleInputChange, onKeyDown: handleKeyDown, onFocus: () =>
|
|
5222
|
+
return (jsxRuntime.jsxs("div", { className: `relative ${className}`, children: [label && (jsxRuntime.jsxs("label", { id: labelId, className: "block text-sm font-medium text-ink-900 mb-1.5", children: [label, required && jsxRuntime.jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsx("div", { className: "absolute left-3 top-1/2 -translate-y-1/2", children: loading ? (jsxRuntime.jsx(lucideReact.Loader2, { className: "h-4 w-4 text-ink-400 animate-spin" })) : (jsxRuntime.jsx(lucideReact.Search, { className: "h-4 w-4 text-ink-400" })) }), jsxRuntime.jsx("input", { ref: inputRef, type: "text", value: value, onChange: handleInputChange, onKeyDown: handleKeyDown, onFocus: () => {
|
|
5223
|
+
if (showOptionsOnFocus && value.length < minChars && options.length > 0) {
|
|
5224
|
+
// Show static options when input is empty/below minChars
|
|
5225
|
+
showStaticOptions();
|
|
5226
|
+
}
|
|
5227
|
+
else if (value.length >= minChars) {
|
|
5228
|
+
// Trigger search if we have enough chars
|
|
5229
|
+
handleSearch(value);
|
|
5230
|
+
}
|
|
5231
|
+
}, placeholder: placeholder, disabled: disabled, className: `
|
|
5169
5232
|
w-full pl-9 pr-9 py-2
|
|
5170
5233
|
text-sm text-ink-900 placeholder-ink-400
|
|
5171
5234
|
bg-white border rounded-lg
|
|
@@ -5175,12 +5238,16 @@ const Autocomplete = React.forwardRef(({ value, onChange, options = [], onSearch
|
|
|
5175
5238
|
${error
|
|
5176
5239
|
? 'border-error-500 focus:ring-error-400 focus:border-error-400'
|
|
5177
5240
|
: 'border-paper-300'}
|
|
5178
|
-
`, role: "combobox", "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? 'Search' : undefined, "aria-autocomplete": "list", "aria-expanded": isOpen, "aria-controls": listboxId, "aria-activedescendant": highlightedIndex >= 0 ? `autocomplete-option-${highlightedIndex}` : undefined, "aria-invalid": error ? 'true' : undefined, "aria-describedby": error ? errorId : undefined, "aria-busy": loading }), clearable && value && !disabled && (jsxRuntime.jsx("button", { type: "button", onClick: handleClear, className: "absolute right-3 top-1/2 -translate-y-1/2 text-ink-400 hover:text-ink-600 transition-colors", "aria-label": "Clear", children: jsxRuntime.jsx(lucideReact.X, { className: "h-4 w-4" }) }))] }), isOpen && filteredOptions.length > 0 && (jsxRuntime.jsx("div", { ref: dropdownRef, id: listboxId, className: "absolute z-50 w-full mt-1 bg-white border border-paper-200 rounded-lg shadow-lg max-h-60 overflow-y-auto", role: "listbox", "aria-label": "Search results", children: filteredOptions.map((option, index) => (
|
|
5179
|
-
|
|
5180
|
-
|
|
5241
|
+
`, role: "combobox", "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? 'Search' : undefined, "aria-autocomplete": "list", "aria-expanded": isOpen, "aria-controls": listboxId, "aria-activedescendant": highlightedIndex >= 0 ? `autocomplete-option-${highlightedIndex}` : undefined, "aria-invalid": error ? 'true' : undefined, "aria-describedby": error ? errorId : undefined, "aria-busy": loading }), clearable && value && !disabled && (jsxRuntime.jsx("button", { type: "button", onClick: handleClear, className: "absolute right-3 top-1/2 -translate-y-1/2 text-ink-400 hover:text-ink-600 transition-colors", "aria-label": "Clear", children: jsxRuntime.jsx(lucideReact.X, { className: "h-4 w-4" }) }))] }), isOpen && filteredOptions.length > 0 && (jsxRuntime.jsx("div", { ref: dropdownRef, id: listboxId, className: "absolute z-50 w-full mt-1 bg-white border border-paper-200 rounded-lg shadow-lg max-h-60 overflow-y-auto", role: "listbox", "aria-label": "Search results", children: filteredOptions.map((option, index) => (option.isHeader ? (
|
|
5242
|
+
// Render section header (non-selectable)
|
|
5243
|
+
jsxRuntime.jsx("div", { className: "px-3 py-2 text-xs font-semibold text-ink-500 uppercase tracking-wide bg-paper-50 border-t border-paper-200 first:border-t-0 first:rounded-t-lg cursor-default", role: "presentation", children: option.label }, `header-${option.value}`)) : (
|
|
5244
|
+
// Render selectable option
|
|
5245
|
+
jsxRuntime.jsxs("button", { id: `autocomplete-option-${index}`, type: "button", onClick: () => handleSelect(option), onMouseEnter: () => setHighlightedIndex(index), role: "option", "aria-selected": highlightedIndex === index, className: `
|
|
5246
|
+
w-full text-left px-3 py-2 transition-colors
|
|
5247
|
+
${highlightedIndex === index
|
|
5181
5248
|
? 'bg-accent-50'
|
|
5182
5249
|
: 'hover:bg-paper-50'}
|
|
5183
|
-
|
|
5250
|
+
`, children: [jsxRuntime.jsx("div", { className: "text-sm font-medium text-ink-900", children: option.label }), option.description && (jsxRuntime.jsx("div", { className: "text-xs text-ink-600 mt-0.5", children: option.description }))] }, option.value)))) })), isOpen && !loading && filteredOptions.length === 0 && value.length >= minChars && (jsxRuntime.jsx("div", { className: "absolute z-50 w-full mt-1 bg-white border border-paper-200 rounded-lg shadow-lg p-3", role: "status", "aria-live": "polite", children: jsxRuntime.jsx("p", { className: "text-sm text-ink-500 text-center", children: "No results found" }) })), error && (jsxRuntime.jsx("p", { id: errorId, className: "mt-1.5 text-xs text-error-600", role: "alert", "aria-live": "assertive", children: error })), helperText && !error && (jsxRuntime.jsx("p", { className: "mt-1.5 text-xs text-ink-600", children: helperText }))] }));
|
|
5184
5251
|
});
|
|
5185
5252
|
Autocomplete.displayName = 'Autocomplete';
|
|
5186
5253
|
|