@lumen-design/autocomplete 0.0.2

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.
@@ -0,0 +1,163 @@
1
+ <script lang="ts">
2
+ import Icon from '@lumen-design/icon'
3
+ import { X, Loader2, ChevronDown } from 'lucide'
4
+
5
+ interface SuggestionItem {
6
+ value: string
7
+ [key: string]: any
8
+ }
9
+
10
+ interface Props {
11
+ modelValue?: string
12
+ placeholder?: string
13
+ disabled?: boolean
14
+ clearable?: boolean
15
+ loading?: boolean
16
+ debounce?: number
17
+ valueKey?: string
18
+ fetchSuggestions?: (query: string) => Promise<SuggestionItem[]> | SuggestionItem[]
19
+ size?: 'small' | 'default' | 'large'
20
+ showMenuArrow?: boolean
21
+ class?: string
22
+ }
23
+
24
+ let {
25
+ modelValue = $bindable(''),
26
+ placeholder = '',
27
+ disabled = false,
28
+ clearable = false,
29
+ loading = $bindable(false),
30
+ debounce = 300,
31
+ valueKey = 'value',
32
+ fetchSuggestions,
33
+ size = 'default',
34
+ showMenuArrow = true,
35
+ class: cls = '',
36
+ ...attrs
37
+ }: Props = $props()
38
+
39
+ let visible = $state(false)
40
+ let suggestions = $state<SuggestionItem[]>([])
41
+ let highlightIndex = $state(-1)
42
+ let inputRef: HTMLElement | null = $state(null)
43
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null
44
+
45
+ const handleInput = (e: Event): void => {
46
+ modelValue = (e.target as HTMLInputElement).value
47
+ highlightIndex = -1
48
+ if (debounceTimer) clearTimeout(debounceTimer)
49
+ debounceTimer = setTimeout(() => fetchData(modelValue), debounce)
50
+ }
51
+
52
+ const fetchData = async (query: string): Promise<void> => {
53
+ if (!fetchSuggestions) {
54
+ suggestions = []
55
+ visible = false
56
+ return
57
+ }
58
+ loading = true
59
+ try {
60
+ suggestions = await fetchSuggestions(query)
61
+ visible = suggestions.length > 0
62
+ } catch {
63
+ suggestions = []
64
+ visible = false
65
+ } finally {
66
+ loading = false
67
+ }
68
+ }
69
+
70
+ const handleSelect = (item: SuggestionItem): void => {
71
+ modelValue = item[valueKey]
72
+ visible = false
73
+ suggestions = []
74
+ }
75
+
76
+ const handleClear = (e: MouseEvent): void => {
77
+ e.stopPropagation()
78
+ modelValue = ''
79
+ suggestions = []
80
+ visible = false
81
+ }
82
+
83
+ const handleKeydown = (e: KeyboardEvent): void => {
84
+ if (!visible) return
85
+ if (e.key === 'ArrowDown') {
86
+ e.preventDefault()
87
+ highlightIndex = Math.min(highlightIndex + 1, suggestions.length - 1)
88
+ } else if (e.key === 'ArrowUp') {
89
+ e.preventDefault()
90
+ highlightIndex = Math.max(highlightIndex - 1, 0)
91
+ } else if (e.key === 'Enter' && highlightIndex >= 0 && suggestions[highlightIndex]) {
92
+ e.preventDefault()
93
+ handleSelect(suggestions[highlightIndex])
94
+ } else if (e.key === 'Escape') {
95
+ visible = false
96
+ }
97
+ }
98
+
99
+ const handleClickOutside = (e: MouseEvent): void => {
100
+ if (inputRef && !inputRef.contains(e.target as Node)) visible = false
101
+ }
102
+
103
+ $effect(() => {
104
+ if (visible) document.addEventListener('click', handleClickOutside)
105
+ return () => document.removeEventListener('click', handleClickOutside)
106
+ })
107
+
108
+ const classes = $derived(['lm-autocomplete', disabled && 'is-disabled', size !== 'default' && `lm-autocomplete--${size}`, cls].filter(Boolean).join(' '))
109
+ </script>
110
+
111
+ <div bind:this={inputRef} class={classes} {...attrs}>
112
+ <div class="lm-autocomplete__input" class:is-focus={visible}>
113
+ <input
114
+ type="text"
115
+ value={modelValue}
116
+ {placeholder}
117
+ {disabled}
118
+ role="combobox"
119
+ aria-expanded={visible}
120
+ aria-controls="autocomplete-listbox"
121
+ aria-autocomplete="list"
122
+ oninput={handleInput}
123
+ onfocus={() => suggestions.length > 0 && (visible = true)}
124
+ onkeydown={handleKeydown}
125
+ />
126
+ {#if loading}
127
+ <span class="lm-autocomplete__loading" aria-label="加载中">
128
+ <Icon icon={Loader2} size={14} />
129
+ </span>
130
+ {:else if clearable && modelValue && !disabled}
131
+ <button type="button" class="lm-autocomplete__clear" onclick={handleClear} aria-label="清除">
132
+ <Icon icon={X} size={14} />
133
+ </button>
134
+ {:else}
135
+ <span class="lm-autocomplete__arrow" class:is-open={visible} aria-hidden="true">
136
+ <Icon icon={ChevronDown} size={14} />
137
+ </span>
138
+ {/if}
139
+ </div>
140
+
141
+ {#if visible && suggestions.length > 0}
142
+ <div class="lm-autocomplete__dropdown" role="listbox" id="autocomplete-listbox">
143
+ {#if showMenuArrow}
144
+ <div class="lm-autocomplete__menu-arrow" aria-hidden="true"></div>
145
+ {/if}
146
+
147
+ <ul class="lm-autocomplete__menu" role="group">
148
+ {#each suggestions as item, index (item[valueKey])}
149
+ <li
150
+ class="lm-autocomplete__item"
151
+ class:is-highlighted={index === highlightIndex}
152
+ role="option"
153
+ aria-selected={index === highlightIndex}
154
+ onmousedown={() => handleSelect(item)}
155
+ onmouseenter={() => (highlightIndex = index)}
156
+ >
157
+ {item[valueKey]}
158
+ </li>
159
+ {/each}
160
+ </ul>
161
+ </div>
162
+ {/if}
163
+ </div>
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import Autocomplete from './Autocomplete.svelte';
2
+ export default Autocomplete;
3
+ export { Autocomplete };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@lumen-design/autocomplete",
3
+ "version": "0.0.2",
4
+ "description": "Autocomplete component for Lumen UI",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "svelte": "./dist/index.js",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "svelte-package -i src -o dist --types",
21
+ "build:watch": "svelte-package -i src -o dist --types -w"
22
+ },
23
+ "dependencies": {
24
+ "@lumen-design/input": "0.0.2",
25
+ "@lumen-design/icon": "0.0.2",
26
+ "lucide": "^0.563.0"
27
+ },
28
+ "devDependencies": {
29
+ "@sveltejs/package": "^2.5.7",
30
+ "svelte": "5.48.2"
31
+ },
32
+ "peerDependencies": {
33
+ "svelte": "^5.0.0"
34
+ }
35
+ }