@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.
- package/dist/Autocomplete.svelte +163 -0
- package/dist/index.js +3 -0
- package/package.json +35 -0
|
@@ -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
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
|
+
}
|