@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/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
- handleSearch(value);
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 < filteredOptions.length - 1 ? prev + 1 : prev);
5179
+ setHighlightedIndex((prev) => findNextSelectableIndex(prev, filteredOptions));
5130
5180
  break;
5131
5181
  case 'ArrowUp':
5132
5182
  e.preventDefault();
5133
- setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : -1));
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
- handleSelect(filteredOptions[highlightedIndex]);
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: () => value.length >= minChars && handleSearch(value), placeholder: placeholder, disabled: disabled, className: `
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) => (jsxRuntime.jsxs("button", { id: `autocomplete-option-${index}`, type: "button", onClick: () => handleSelect(option), onMouseEnter: () => setHighlightedIndex(index), role: "option", "aria-selected": highlightedIndex === index, className: `
5179
- w-full text-left px-3 py-2 transition-colors
5180
- ${highlightedIndex === index
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
- `, 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 }))] }));
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