@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
|
@@ -8,6 +8,8 @@ export interface AutocompleteOption {
|
|
|
8
8
|
label: string;
|
|
9
9
|
description?: string;
|
|
10
10
|
metadata?: Record<string, unknown>;
|
|
11
|
+
/** If true, renders as a non-selectable section header */
|
|
12
|
+
isHeader?: boolean;
|
|
11
13
|
}
|
|
12
14
|
export interface AutocompleteProps {
|
|
13
15
|
value: string;
|
|
@@ -25,6 +27,8 @@ export interface AutocompleteProps {
|
|
|
25
27
|
maxResults?: number;
|
|
26
28
|
clearable?: boolean;
|
|
27
29
|
className?: string;
|
|
30
|
+
/** Show static options dropdown on focus when input is empty. Default: true */
|
|
31
|
+
showOptionsOnFocus?: boolean;
|
|
28
32
|
}
|
|
29
33
|
declare const Autocomplete: React.ForwardRefExoticComponent<AutocompleteProps & React.RefAttributes<AutocompleteHandle>>;
|
|
30
34
|
export default Autocomplete;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Autocomplete.d.ts","sourceRoot":"","sources":["../../src/components/Autocomplete.tsx"],"names":[],"mappings":"AACA,OAAO,KAA8E,MAAM,OAAO,CAAC;AAGnG,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"Autocomplete.d.ts","sourceRoot":"","sources":["../../src/components/Autocomplete.tsx"],"names":[],"mappings":"AACA,OAAO,KAA8E,MAAM,OAAO,CAAC;AAGnG,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC/D,OAAO,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC/B,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAC;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,QAAA,MAAM,YAAY,8FA6WhB,CAAC;AAGH,eAAe,YAAY,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1966,6 +1966,8 @@ interface AutocompleteOption {
|
|
|
1966
1966
|
label: string;
|
|
1967
1967
|
description?: string;
|
|
1968
1968
|
metadata?: Record<string, unknown>;
|
|
1969
|
+
/** If true, renders as a non-selectable section header */
|
|
1970
|
+
isHeader?: boolean;
|
|
1969
1971
|
}
|
|
1970
1972
|
interface AutocompleteProps {
|
|
1971
1973
|
value: string;
|
|
@@ -1983,6 +1985,8 @@ interface AutocompleteProps {
|
|
|
1983
1985
|
maxResults?: number;
|
|
1984
1986
|
clearable?: boolean;
|
|
1985
1987
|
className?: string;
|
|
1988
|
+
/** Show static options dropdown on focus when input is empty. Default: true */
|
|
1989
|
+
showOptionsOnFocus?: boolean;
|
|
1986
1990
|
}
|
|
1987
1991
|
declare const Autocomplete: React__default.ForwardRefExoticComponent<AutocompleteProps & React__default.RefAttributes<AutocompleteHandle>>;
|
|
1988
1992
|
|
package/dist/index.esm.js
CHANGED
|
@@ -5007,7 +5007,7 @@ const MaskedInput = forwardRef(({ value, onChange, maskType = 'phone', customMas
|
|
|
5007
5007
|
});
|
|
5008
5008
|
MaskedInput.displayName = 'MaskedInput';
|
|
5009
5009
|
|
|
5010
|
-
const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, label, placeholder = 'Search...', required = false, disabled = false, error, helperText, minChars = 1, debounceMs = 300, maxResults = 10, clearable = true, className = '', }, ref) => {
|
|
5010
|
+
const Autocomplete = 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) => {
|
|
5011
5011
|
const [isOpen, setIsOpen] = useState(false);
|
|
5012
5012
|
const [filteredOptions, setFilteredOptions] = useState([]);
|
|
5013
5013
|
const [loading, setLoading] = useState(false);
|
|
@@ -5019,6 +5019,30 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
|
|
|
5019
5019
|
const labelId = useId();
|
|
5020
5020
|
const listboxId = useId();
|
|
5021
5021
|
const errorId = useId();
|
|
5022
|
+
// Helper to find next selectable (non-header) index
|
|
5023
|
+
const findNextSelectableIndex = (currentIndex, optionsList) => {
|
|
5024
|
+
for (let i = currentIndex + 1; i < optionsList.length; i++) {
|
|
5025
|
+
if (!optionsList[i].isHeader)
|
|
5026
|
+
return i;
|
|
5027
|
+
}
|
|
5028
|
+
return currentIndex; // Stay at current if no next selectable
|
|
5029
|
+
};
|
|
5030
|
+
// Helper to find previous selectable (non-header) index
|
|
5031
|
+
const findPrevSelectableIndex = (currentIndex, optionsList) => {
|
|
5032
|
+
for (let i = currentIndex - 1; i >= 0; i--) {
|
|
5033
|
+
if (!optionsList[i].isHeader)
|
|
5034
|
+
return i;
|
|
5035
|
+
}
|
|
5036
|
+
return -1; // Go to -1 if no previous selectable
|
|
5037
|
+
};
|
|
5038
|
+
// Helper to find first selectable (non-header) index
|
|
5039
|
+
const findFirstSelectableIndex = (optionsList) => {
|
|
5040
|
+
for (let i = 0; i < optionsList.length; i++) {
|
|
5041
|
+
if (!optionsList[i].isHeader)
|
|
5042
|
+
return i;
|
|
5043
|
+
}
|
|
5044
|
+
return -1;
|
|
5045
|
+
};
|
|
5022
5046
|
// Expose methods via ref
|
|
5023
5047
|
useImperativeHandle(ref, () => ({
|
|
5024
5048
|
focus: () => inputRef.current?.focus(),
|
|
@@ -5048,11 +5072,14 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
|
|
|
5048
5072
|
const results = await onSearch(query);
|
|
5049
5073
|
setFilteredOptions(results.slice(0, maxResults));
|
|
5050
5074
|
setIsOpen(results.length > 0);
|
|
5075
|
+
// Auto-highlight first selectable (non-header) result
|
|
5076
|
+
setHighlightedIndex(findFirstSelectableIndex(results.slice(0, maxResults)));
|
|
5051
5077
|
}
|
|
5052
5078
|
catch (err) {
|
|
5053
5079
|
console.error('Autocomplete search error:', err);
|
|
5054
5080
|
setFilteredOptions([]);
|
|
5055
5081
|
setIsOpen(false);
|
|
5082
|
+
setHighlightedIndex(-1);
|
|
5056
5083
|
}
|
|
5057
5084
|
finally {
|
|
5058
5085
|
setLoading(false);
|
|
@@ -5063,6 +5090,16 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
|
|
|
5063
5090
|
const filtered = filterOptions(query);
|
|
5064
5091
|
setFilteredOptions(filtered);
|
|
5065
5092
|
setIsOpen(filtered.length > 0);
|
|
5093
|
+
// Auto-highlight first selectable (non-header) result
|
|
5094
|
+
setHighlightedIndex(findFirstSelectableIndex(filtered));
|
|
5095
|
+
}
|
|
5096
|
+
};
|
|
5097
|
+
// Show static options (for focus/arrow down when input is empty)
|
|
5098
|
+
const showStaticOptions = () => {
|
|
5099
|
+
if (options.length > 0) {
|
|
5100
|
+
setFilteredOptions(options.slice(0, maxResults));
|
|
5101
|
+
setIsOpen(true);
|
|
5102
|
+
setHighlightedIndex(findFirstSelectableIndex(options.slice(0, maxResults)));
|
|
5066
5103
|
}
|
|
5067
5104
|
};
|
|
5068
5105
|
// Debounced search
|
|
@@ -5099,23 +5136,40 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
|
|
|
5099
5136
|
const handleKeyDown = (e) => {
|
|
5100
5137
|
if (!isOpen) {
|
|
5101
5138
|
if (e.key === 'ArrowDown') {
|
|
5102
|
-
|
|
5139
|
+
e.preventDefault();
|
|
5140
|
+
// If we have cached results from a previous search, show them
|
|
5141
|
+
if (filteredOptions.length > 0) {
|
|
5142
|
+
setIsOpen(true);
|
|
5143
|
+
setHighlightedIndex(findFirstSelectableIndex(filteredOptions));
|
|
5144
|
+
}
|
|
5145
|
+
else if (value.length < minChars && options.length > 0) {
|
|
5146
|
+
// Show static options when input is empty/below minChars
|
|
5147
|
+
showStaticOptions();
|
|
5148
|
+
}
|
|
5149
|
+
else if (value.length >= minChars) {
|
|
5150
|
+
// Otherwise trigger a new search
|
|
5151
|
+
handleSearch(value);
|
|
5152
|
+
}
|
|
5103
5153
|
}
|
|
5104
5154
|
return;
|
|
5105
5155
|
}
|
|
5106
5156
|
switch (e.key) {
|
|
5107
5157
|
case 'ArrowDown':
|
|
5108
5158
|
e.preventDefault();
|
|
5109
|
-
setHighlightedIndex((prev) => prev
|
|
5159
|
+
setHighlightedIndex((prev) => findNextSelectableIndex(prev, filteredOptions));
|
|
5110
5160
|
break;
|
|
5111
5161
|
case 'ArrowUp':
|
|
5112
5162
|
e.preventDefault();
|
|
5113
|
-
setHighlightedIndex((prev) => (prev
|
|
5163
|
+
setHighlightedIndex((prev) => findPrevSelectableIndex(prev, filteredOptions));
|
|
5114
5164
|
break;
|
|
5115
5165
|
case 'Enter':
|
|
5116
5166
|
e.preventDefault();
|
|
5117
5167
|
if (highlightedIndex >= 0 && highlightedIndex < filteredOptions.length) {
|
|
5118
|
-
|
|
5168
|
+
const option = filteredOptions[highlightedIndex];
|
|
5169
|
+
// Don't select headers
|
|
5170
|
+
if (!option.isHeader) {
|
|
5171
|
+
handleSelect(option);
|
|
5172
|
+
}
|
|
5119
5173
|
}
|
|
5120
5174
|
break;
|
|
5121
5175
|
case 'Escape':
|
|
@@ -5145,7 +5199,16 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
|
|
|
5145
5199
|
}
|
|
5146
5200
|
};
|
|
5147
5201
|
}, []);
|
|
5148
|
-
return (jsxs("div", { className: `relative ${className}`, children: [label && (jsxs("label", { id: labelId, className: "block text-sm font-medium text-ink-900 mb-1.5", children: [label, required && jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsxs("div", { className: "relative", children: [jsx("div", { className: "absolute left-3 top-1/2 -translate-y-1/2", children: loading ? (jsx(Loader2, { className: "h-4 w-4 text-ink-400 animate-spin" })) : (jsx(Search, { className: "h-4 w-4 text-ink-400" })) }), jsx("input", { ref: inputRef, type: "text", value: value, onChange: handleInputChange, onKeyDown: handleKeyDown, onFocus: () =>
|
|
5202
|
+
return (jsxs("div", { className: `relative ${className}`, children: [label && (jsxs("label", { id: labelId, className: "block text-sm font-medium text-ink-900 mb-1.5", children: [label, required && jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsxs("div", { className: "relative", children: [jsx("div", { className: "absolute left-3 top-1/2 -translate-y-1/2", children: loading ? (jsx(Loader2, { className: "h-4 w-4 text-ink-400 animate-spin" })) : (jsx(Search, { className: "h-4 w-4 text-ink-400" })) }), jsx("input", { ref: inputRef, type: "text", value: value, onChange: handleInputChange, onKeyDown: handleKeyDown, onFocus: () => {
|
|
5203
|
+
if (showOptionsOnFocus && value.length < minChars && options.length > 0) {
|
|
5204
|
+
// Show static options when input is empty/below minChars
|
|
5205
|
+
showStaticOptions();
|
|
5206
|
+
}
|
|
5207
|
+
else if (value.length >= minChars) {
|
|
5208
|
+
// Trigger search if we have enough chars
|
|
5209
|
+
handleSearch(value);
|
|
5210
|
+
}
|
|
5211
|
+
}, placeholder: placeholder, disabled: disabled, className: `
|
|
5149
5212
|
w-full pl-9 pr-9 py-2
|
|
5150
5213
|
text-sm text-ink-900 placeholder-ink-400
|
|
5151
5214
|
bg-white border rounded-lg
|
|
@@ -5155,12 +5218,16 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
|
|
|
5155
5218
|
${error
|
|
5156
5219
|
? 'border-error-500 focus:ring-error-400 focus:border-error-400'
|
|
5157
5220
|
: 'border-paper-300'}
|
|
5158
|
-
`, 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 && (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: jsx(X, { className: "h-4 w-4" }) }))] }), isOpen && filteredOptions.length > 0 && (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) => (
|
|
5159
|
-
|
|
5160
|
-
|
|
5221
|
+
`, 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 && (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: jsx(X, { className: "h-4 w-4" }) }))] }), isOpen && filteredOptions.length > 0 && (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 ? (
|
|
5222
|
+
// Render section header (non-selectable)
|
|
5223
|
+
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}`)) : (
|
|
5224
|
+
// Render selectable option
|
|
5225
|
+
jsxs("button", { id: `autocomplete-option-${index}`, type: "button", onClick: () => handleSelect(option), onMouseEnter: () => setHighlightedIndex(index), role: "option", "aria-selected": highlightedIndex === index, className: `
|
|
5226
|
+
w-full text-left px-3 py-2 transition-colors
|
|
5227
|
+
${highlightedIndex === index
|
|
5161
5228
|
? 'bg-accent-50'
|
|
5162
5229
|
: 'hover:bg-paper-50'}
|
|
5163
|
-
|
|
5230
|
+
`, children: [jsx("div", { className: "text-sm font-medium text-ink-900", children: option.label }), option.description && (jsx("div", { className: "text-xs text-ink-600 mt-0.5", children: option.description }))] }, option.value)))) })), isOpen && !loading && filteredOptions.length === 0 && value.length >= minChars && (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: jsx("p", { className: "text-sm text-ink-500 text-center", children: "No results found" }) })), error && (jsx("p", { id: errorId, className: "mt-1.5 text-xs text-error-600", role: "alert", "aria-live": "assertive", children: error })), helperText && !error && (jsx("p", { className: "mt-1.5 text-xs text-ink-600", children: helperText }))] }));
|
|
5164
5231
|
});
|
|
5165
5232
|
Autocomplete.displayName = 'Autocomplete';
|
|
5166
5233
|
|