@papernote/ui 1.7.6 → 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 +67 -14
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +67 -14
- package/dist/index.js.map +1 -1
- package/dist/styles.css +13 -0
- package/package.json +1 -1
- package/src/components/Autocomplete.tsx +95 -32
|
@@ -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,8 +5072,8 @@ 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);
|
|
5051
|
-
// Auto-highlight first
|
|
5052
|
-
setHighlightedIndex(results.
|
|
5075
|
+
// Auto-highlight first selectable (non-header) result
|
|
5076
|
+
setHighlightedIndex(findFirstSelectableIndex(results.slice(0, maxResults)));
|
|
5053
5077
|
}
|
|
5054
5078
|
catch (err) {
|
|
5055
5079
|
console.error('Autocomplete search error:', err);
|
|
@@ -5066,8 +5090,16 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
|
|
|
5066
5090
|
const filtered = filterOptions(query);
|
|
5067
5091
|
setFilteredOptions(filtered);
|
|
5068
5092
|
setIsOpen(filtered.length > 0);
|
|
5069
|
-
// Auto-highlight first
|
|
5070
|
-
setHighlightedIndex(filtered
|
|
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)));
|
|
5071
5103
|
}
|
|
5072
5104
|
};
|
|
5073
5105
|
// Debounced search
|
|
@@ -5108,7 +5140,11 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
|
|
|
5108
5140
|
// If we have cached results from a previous search, show them
|
|
5109
5141
|
if (filteredOptions.length > 0) {
|
|
5110
5142
|
setIsOpen(true);
|
|
5111
|
-
setHighlightedIndex(
|
|
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();
|
|
5112
5148
|
}
|
|
5113
5149
|
else if (value.length >= minChars) {
|
|
5114
5150
|
// Otherwise trigger a new search
|
|
@@ -5120,16 +5156,20 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
|
|
|
5120
5156
|
switch (e.key) {
|
|
5121
5157
|
case 'ArrowDown':
|
|
5122
5158
|
e.preventDefault();
|
|
5123
|
-
setHighlightedIndex((prev) => prev
|
|
5159
|
+
setHighlightedIndex((prev) => findNextSelectableIndex(prev, filteredOptions));
|
|
5124
5160
|
break;
|
|
5125
5161
|
case 'ArrowUp':
|
|
5126
5162
|
e.preventDefault();
|
|
5127
|
-
setHighlightedIndex((prev) => (prev
|
|
5163
|
+
setHighlightedIndex((prev) => findPrevSelectableIndex(prev, filteredOptions));
|
|
5128
5164
|
break;
|
|
5129
5165
|
case 'Enter':
|
|
5130
5166
|
e.preventDefault();
|
|
5131
5167
|
if (highlightedIndex >= 0 && highlightedIndex < filteredOptions.length) {
|
|
5132
|
-
|
|
5168
|
+
const option = filteredOptions[highlightedIndex];
|
|
5169
|
+
// Don't select headers
|
|
5170
|
+
if (!option.isHeader) {
|
|
5171
|
+
handleSelect(option);
|
|
5172
|
+
}
|
|
5133
5173
|
}
|
|
5134
5174
|
break;
|
|
5135
5175
|
case 'Escape':
|
|
@@ -5159,7 +5199,16 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
|
|
|
5159
5199
|
}
|
|
5160
5200
|
};
|
|
5161
5201
|
}, []);
|
|
5162
|
-
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: `
|
|
5163
5212
|
w-full pl-9 pr-9 py-2
|
|
5164
5213
|
text-sm text-ink-900 placeholder-ink-400
|
|
5165
5214
|
bg-white border rounded-lg
|
|
@@ -5169,12 +5218,16 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
|
|
|
5169
5218
|
${error
|
|
5170
5219
|
? 'border-error-500 focus:ring-error-400 focus:border-error-400'
|
|
5171
5220
|
: 'border-paper-300'}
|
|
5172
|
-
`, 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) => (
|
|
5173
|
-
|
|
5174
|
-
|
|
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
|
|
5175
5228
|
? 'bg-accent-50'
|
|
5176
5229
|
: 'hover:bg-paper-50'}
|
|
5177
|
-
|
|
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 }))] }));
|
|
5178
5231
|
});
|
|
5179
5232
|
Autocomplete.displayName = 'Autocomplete';
|
|
5180
5233
|
|