@choice-ui/command 0.0.3 → 0.0.5
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.d.ts +67 -6
- package/dist/index.js +293 -269
- package/package.json +13 -15
- package/dist/index.cjs +0 -1309
- package/dist/index.d.cts +0 -130
- package/src/command-score.ts +0 -171
- package/src/command.tsx +0 -482
- package/src/components/command-divider.tsx +0 -30
- package/src/components/command-empty.tsx +0 -30
- package/src/components/command-footer.tsx +0 -22
- package/src/components/command-group.tsx +0 -76
- package/src/components/command-input.tsx +0 -66
- package/src/components/command-item.tsx +0 -165
- package/src/components/command-list.tsx +0 -77
- package/src/components/command-loading.tsx +0 -30
- package/src/components/command-tabs.tsx +0 -20
- package/src/components/command-value.tsx +0 -23
- package/src/components/index.ts +0 -10
- package/src/context/command-context.ts +0 -5
- package/src/context/create-command-context.ts +0 -140
- package/src/context/index.ts +0 -2
- package/src/hooks/index.ts +0 -10
- package/src/hooks/use-as-ref.ts +0 -12
- package/src/hooks/use-command-state.ts +0 -18
- package/src/hooks/use-command.ts +0 -10
- package/src/hooks/use-schedule-layout-effect.ts +0 -19
- package/src/hooks/use-value.ts +0 -39
- package/src/index.ts +0 -31
- package/src/store/index.ts +0 -1
- package/src/tv.ts +0 -248
- package/src/types.ts +0 -84
- package/src/utils/constants.ts +0 -7
- package/src/utils/dom.ts +0 -19
- package/src/utils/helpers.ts +0 -45
- package/src/utils/index.ts +0 -3
package/src/command.tsx
DELETED
|
@@ -1,482 +0,0 @@
|
|
|
1
|
-
import { tcx } from "@choice-ui/shared"
|
|
2
|
-
import React, { forwardRef, useCallback, useId, useMemo, useRef } from "react"
|
|
3
|
-
import { useEventCallback, useIsomorphicLayoutEffect } from "usehooks-ts"
|
|
4
|
-
import { useLazyRef } from "@choice-ui/shared"
|
|
5
|
-
import { commandScore } from "./command-score"
|
|
6
|
-
import { CommandContext, createCommandContext, StoreContext } from "./context"
|
|
7
|
-
import { useAsRef, useScheduleLayoutEffect } from "./hooks"
|
|
8
|
-
import { commandTv } from "./tv"
|
|
9
|
-
import { CommandFilter, CommandProps, State, Store } from "./types"
|
|
10
|
-
import {
|
|
11
|
-
findNextSibling,
|
|
12
|
-
findPreviousSibling,
|
|
13
|
-
GROUP_HEADING_SELECTOR,
|
|
14
|
-
GROUP_ITEMS_SELECTOR,
|
|
15
|
-
GROUP_SELECTOR,
|
|
16
|
-
ITEM_SELECTOR,
|
|
17
|
-
SELECT_EVENT,
|
|
18
|
-
SlottableWithNestedChildren,
|
|
19
|
-
VALID_ITEM_SELECTOR,
|
|
20
|
-
VALUE_ATTR,
|
|
21
|
-
} from "./utils"
|
|
22
|
-
|
|
23
|
-
export const defaultFilter: CommandFilter = (value, search, keywords) =>
|
|
24
|
-
commandScore(value, search, keywords)
|
|
25
|
-
|
|
26
|
-
export const Command = forwardRef<HTMLDivElement, CommandProps>((props, forwardedRef) => {
|
|
27
|
-
const state = useLazyRef<State>(() => ({
|
|
28
|
-
/** Value of the search query. */
|
|
29
|
-
search: "",
|
|
30
|
-
/** Currently selected item value. */
|
|
31
|
-
value: props.value ?? props.defaultValue ?? "",
|
|
32
|
-
/** Currently selected item id. */
|
|
33
|
-
selectedItemId: undefined,
|
|
34
|
-
filtered: {
|
|
35
|
-
/** The count of all visible items. */
|
|
36
|
-
count: 0,
|
|
37
|
-
/** Map from visible item id to its search score. */
|
|
38
|
-
items: new Map(),
|
|
39
|
-
/** Set of groups with at least one visible item. */
|
|
40
|
-
groups: new Set(),
|
|
41
|
-
},
|
|
42
|
-
}))
|
|
43
|
-
const allItems = useLazyRef<Set<string>>(() => new Set()) // [...itemIds]
|
|
44
|
-
const allGroups = useLazyRef<Map<string, Set<string>>>(() => new Map()) // groupId → [...itemIds]
|
|
45
|
-
const ids = useLazyRef<Map<string, { keywords?: string[]; value: string }>>(() => new Map()) // id → { value, keywords }
|
|
46
|
-
const listeners = useLazyRef<Set<() => void>>(() => new Set()) // [...rerenders]
|
|
47
|
-
const propsRef = useAsRef(props)
|
|
48
|
-
const {
|
|
49
|
-
label,
|
|
50
|
-
children,
|
|
51
|
-
value,
|
|
52
|
-
onChange: onValueChange,
|
|
53
|
-
filter,
|
|
54
|
-
shouldFilter,
|
|
55
|
-
loop,
|
|
56
|
-
size = "default",
|
|
57
|
-
variant = "default",
|
|
58
|
-
disablePointerSelection = false,
|
|
59
|
-
vimBindings = true,
|
|
60
|
-
className,
|
|
61
|
-
...etc
|
|
62
|
-
} = props
|
|
63
|
-
|
|
64
|
-
const listId = useId()
|
|
65
|
-
const labelId = useId()
|
|
66
|
-
const inputId = useId()
|
|
67
|
-
|
|
68
|
-
const listInnerRef = useRef<HTMLDivElement | null>(null)
|
|
69
|
-
|
|
70
|
-
const schedule = useScheduleLayoutEffect()
|
|
71
|
-
|
|
72
|
-
const tv = commandTv({ variant })
|
|
73
|
-
|
|
74
|
-
/** Controlled mode `value` handling. */
|
|
75
|
-
const store: Store = useMemo(() => {
|
|
76
|
-
return {
|
|
77
|
-
subscribe: (cb) => {
|
|
78
|
-
listeners.current.add(cb)
|
|
79
|
-
return () => listeners.current.delete(cb)
|
|
80
|
-
},
|
|
81
|
-
snapshot: () => {
|
|
82
|
-
return state.current
|
|
83
|
-
},
|
|
84
|
-
setState: (key, value, opts) => {
|
|
85
|
-
if (Object.is(state.current[key], value)) return
|
|
86
|
-
state.current[key] = value
|
|
87
|
-
|
|
88
|
-
if (key === "search") {
|
|
89
|
-
// Filter synchronously before emitting back to children
|
|
90
|
-
filterItems()
|
|
91
|
-
sort()
|
|
92
|
-
schedule(1, selectFirstItem)
|
|
93
|
-
} else if (key === "value") {
|
|
94
|
-
// Force focus input or root so accessibility works
|
|
95
|
-
const activeElement = document.activeElement as HTMLElement
|
|
96
|
-
if (
|
|
97
|
-
activeElement?.hasAttribute("data-command-input") ||
|
|
98
|
-
activeElement?.hasAttribute("data-command-root")
|
|
99
|
-
) {
|
|
100
|
-
const input = document.getElementById(inputId)
|
|
101
|
-
if (input) input.focus()
|
|
102
|
-
else document.getElementById(listId)?.focus()
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
schedule(7, () => {
|
|
106
|
-
state.current.selectedItemId = getSelectedItem()?.id
|
|
107
|
-
store.emit()
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
// opts is a boolean referring to whether it should NOT be scrolled into view
|
|
111
|
-
if (!opts) {
|
|
112
|
-
// Scroll the selected item into view
|
|
113
|
-
schedule(5, scrollSelectedIntoView)
|
|
114
|
-
}
|
|
115
|
-
if (propsRef.current?.value !== undefined) {
|
|
116
|
-
// If controlled, just call the callback instead of updating state internally
|
|
117
|
-
const newValue = (value ?? "") as string
|
|
118
|
-
propsRef.current.onChange?.(newValue)
|
|
119
|
-
return
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Notify subscribers that state has changed
|
|
124
|
-
store.emit()
|
|
125
|
-
},
|
|
126
|
-
emit: () => {
|
|
127
|
-
listeners.current.forEach((l) => l())
|
|
128
|
-
},
|
|
129
|
-
}
|
|
130
|
-
}, [])
|
|
131
|
-
|
|
132
|
-
useIsomorphicLayoutEffect(() => {
|
|
133
|
-
if (value !== undefined) {
|
|
134
|
-
const v = value.trim()
|
|
135
|
-
state.current.value = v
|
|
136
|
-
store.emit()
|
|
137
|
-
}
|
|
138
|
-
}, [value])
|
|
139
|
-
|
|
140
|
-
useIsomorphicLayoutEffect(() => {
|
|
141
|
-
schedule(6, scrollSelectedIntoView)
|
|
142
|
-
}, [])
|
|
143
|
-
|
|
144
|
-
const score = useEventCallback((value: string, keywords?: string[]) => {
|
|
145
|
-
const filter = propsRef.current?.filter ?? defaultFilter
|
|
146
|
-
return value ? filter(value, state.current.search, keywords) : 0
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
/** Sorts items by score, and groups by highest item score. */
|
|
150
|
-
const sort = useCallback(() => {
|
|
151
|
-
if (
|
|
152
|
-
!state.current.search ||
|
|
153
|
-
// Explicitly false, because true | undefined is the default
|
|
154
|
-
propsRef.current.shouldFilter === false
|
|
155
|
-
) {
|
|
156
|
-
return
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const scores = state.current.filtered.items
|
|
160
|
-
|
|
161
|
-
// Sort the groups
|
|
162
|
-
const groups: [string, number][] = []
|
|
163
|
-
state.current.filtered.groups.forEach((value) => {
|
|
164
|
-
const items = allGroups.current.get(value)
|
|
165
|
-
|
|
166
|
-
// Get the maximum score of the group's items
|
|
167
|
-
let max = 0
|
|
168
|
-
items?.forEach((item) => {
|
|
169
|
-
const score = scores.get(item) ?? 0
|
|
170
|
-
max = Math.max(score, max)
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
groups.push([value, max])
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
// Sort items within groups to bottom
|
|
177
|
-
// Sort items outside of groups
|
|
178
|
-
// Sort groups to bottom (pushes all non-grouped items to the top)
|
|
179
|
-
const listInsertionElement = listInnerRef.current
|
|
180
|
-
|
|
181
|
-
// Sort the items
|
|
182
|
-
getValidItems()
|
|
183
|
-
.sort((a, b) => {
|
|
184
|
-
const valueA = a.getAttribute("id")
|
|
185
|
-
const valueB = b.getAttribute("id")
|
|
186
|
-
return (scores.get(valueB ?? "") ?? 0) - (scores.get(valueA ?? "") ?? 0)
|
|
187
|
-
})
|
|
188
|
-
.forEach((item) => {
|
|
189
|
-
const group = item.closest(GROUP_ITEMS_SELECTOR)
|
|
190
|
-
|
|
191
|
-
if (group) {
|
|
192
|
-
const elementToMove =
|
|
193
|
-
item.parentElement === group ? item : item.closest(`${GROUP_ITEMS_SELECTOR} > *`)
|
|
194
|
-
if (elementToMove) group.appendChild(elementToMove)
|
|
195
|
-
} else {
|
|
196
|
-
const elementToMove =
|
|
197
|
-
item.parentElement === listInsertionElement
|
|
198
|
-
? item
|
|
199
|
-
: item.closest(`${GROUP_ITEMS_SELECTOR} > *`)
|
|
200
|
-
if (elementToMove) listInsertionElement?.appendChild(elementToMove)
|
|
201
|
-
}
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
groups
|
|
205
|
-
.sort((a, b) => b[1] - a[1])
|
|
206
|
-
.forEach((group) => {
|
|
207
|
-
const element = listInnerRef.current?.querySelector(
|
|
208
|
-
`${GROUP_SELECTOR}[${VALUE_ATTR}="${encodeURIComponent(group[0])}"]`,
|
|
209
|
-
)
|
|
210
|
-
if (element && element.parentElement) {
|
|
211
|
-
element.parentElement.appendChild(element)
|
|
212
|
-
}
|
|
213
|
-
})
|
|
214
|
-
}, [])
|
|
215
|
-
|
|
216
|
-
const selectFirstItem = useCallback(() => {
|
|
217
|
-
const item = getValidItems().find((item) => item.getAttribute("aria-disabled") !== "true")
|
|
218
|
-
const value = item?.getAttribute(VALUE_ATTR)
|
|
219
|
-
store.setState("value", value || "")
|
|
220
|
-
}, [])
|
|
221
|
-
|
|
222
|
-
/** Filters the current items. */
|
|
223
|
-
const filterItems = useCallback(() => {
|
|
224
|
-
if (
|
|
225
|
-
!state.current.search ||
|
|
226
|
-
// Explicitly false, because true | undefined is the default
|
|
227
|
-
propsRef.current.shouldFilter === false
|
|
228
|
-
) {
|
|
229
|
-
state.current.filtered.count = allItems.current.size
|
|
230
|
-
// Do nothing, each item will know to show itself because search is empty
|
|
231
|
-
return
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Reset the groups
|
|
235
|
-
state.current.filtered.groups = new Set()
|
|
236
|
-
let itemCount = 0
|
|
237
|
-
|
|
238
|
-
// Check which items should be included
|
|
239
|
-
for (const id of allItems.current) {
|
|
240
|
-
const value = ids.current.get(id)?.value ?? ""
|
|
241
|
-
const keywords = ids.current.get(id)?.keywords ?? []
|
|
242
|
-
const rank = score(value, keywords)
|
|
243
|
-
state.current.filtered.items.set(id, rank)
|
|
244
|
-
if (rank > 0) itemCount++
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Check which groups have at least 1 item shown
|
|
248
|
-
for (const [groupId, group] of allGroups.current) {
|
|
249
|
-
for (const itemId of group) {
|
|
250
|
-
if ((state.current.filtered.items.get(itemId) ?? 0) > 0) {
|
|
251
|
-
state.current.filtered.groups.add(groupId)
|
|
252
|
-
break
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
state.current.filtered.count = itemCount
|
|
258
|
-
}, [])
|
|
259
|
-
|
|
260
|
-
const scrollSelectedIntoView = useCallback(() => {
|
|
261
|
-
const item = getSelectedItem()
|
|
262
|
-
|
|
263
|
-
if (item) {
|
|
264
|
-
if (item.parentElement?.firstChild === item) {
|
|
265
|
-
// First item in Group, ensure heading is in view
|
|
266
|
-
item
|
|
267
|
-
.closest(GROUP_SELECTOR)
|
|
268
|
-
?.querySelector(GROUP_HEADING_SELECTOR)
|
|
269
|
-
?.scrollIntoView({ block: "nearest" })
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Ensure the item is always in view
|
|
273
|
-
item.scrollIntoView({ block: "nearest" })
|
|
274
|
-
}
|
|
275
|
-
}, [])
|
|
276
|
-
|
|
277
|
-
/** Getters */
|
|
278
|
-
|
|
279
|
-
const getSelectedItem = useCallback(() => {
|
|
280
|
-
return listInnerRef.current?.querySelector(`${ITEM_SELECTOR}[aria-selected="true"]`)
|
|
281
|
-
}, [])
|
|
282
|
-
|
|
283
|
-
function getValidItems(): HTMLElement[] {
|
|
284
|
-
return Array.from(
|
|
285
|
-
listInnerRef.current?.querySelectorAll(VALID_ITEM_SELECTOR) || [],
|
|
286
|
-
) as HTMLElement[]
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/** Setters */
|
|
290
|
-
|
|
291
|
-
const updateSelectedToIndex = useEventCallback((index: number) => {
|
|
292
|
-
const items = getValidItems()
|
|
293
|
-
const item = items[index]
|
|
294
|
-
if (item) store.setState("value", item.getAttribute(VALUE_ATTR) || "")
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
const updateSelectedByItem = useEventCallback((change: 1 | -1) => {
|
|
298
|
-
const selected = getSelectedItem()
|
|
299
|
-
const items = getValidItems()
|
|
300
|
-
const index = items.findIndex((item) => item === selected)
|
|
301
|
-
|
|
302
|
-
// Get item at this index
|
|
303
|
-
let newSelected = items[index + change]
|
|
304
|
-
|
|
305
|
-
if (propsRef.current?.loop) {
|
|
306
|
-
newSelected =
|
|
307
|
-
index + change < 0
|
|
308
|
-
? items[items.length - 1]
|
|
309
|
-
: index + change === items.length
|
|
310
|
-
? items[0]
|
|
311
|
-
: items[index + change]
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (newSelected) store.setState("value", newSelected.getAttribute(VALUE_ATTR) || "")
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
const updateSelectedByGroup = useEventCallback((change: 1 | -1) => {
|
|
318
|
-
const selected = getSelectedItem()
|
|
319
|
-
let group = selected?.closest(GROUP_SELECTOR)
|
|
320
|
-
let item: HTMLElement | null = null
|
|
321
|
-
|
|
322
|
-
while (group && !item) {
|
|
323
|
-
group =
|
|
324
|
-
change > 0
|
|
325
|
-
? (findNextSibling(group, GROUP_SELECTOR) as HTMLElement)
|
|
326
|
-
: (findPreviousSibling(group, GROUP_SELECTOR) as HTMLElement)
|
|
327
|
-
item = group?.querySelector(VALID_ITEM_SELECTOR) as HTMLElement
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (item) {
|
|
331
|
-
store.setState("value", item.getAttribute(VALUE_ATTR) || "")
|
|
332
|
-
} else {
|
|
333
|
-
updateSelectedByItem(change)
|
|
334
|
-
}
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
const context = useMemo(
|
|
338
|
-
() =>
|
|
339
|
-
createCommandContext({
|
|
340
|
-
allGroups,
|
|
341
|
-
allItems,
|
|
342
|
-
filterItems,
|
|
343
|
-
ids,
|
|
344
|
-
inputId,
|
|
345
|
-
label,
|
|
346
|
-
labelId,
|
|
347
|
-
listId,
|
|
348
|
-
listInnerRef,
|
|
349
|
-
propsRef,
|
|
350
|
-
schedule,
|
|
351
|
-
selectFirstItem,
|
|
352
|
-
size,
|
|
353
|
-
sort,
|
|
354
|
-
state,
|
|
355
|
-
store,
|
|
356
|
-
variant,
|
|
357
|
-
}),
|
|
358
|
-
[], // ❌ 空依赖数组,和原始实现一致
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
// Store now directly uses the functions in closure, no need for updateHandlers
|
|
362
|
-
|
|
363
|
-
const last = () => updateSelectedToIndex(getValidItems().length - 1)
|
|
364
|
-
|
|
365
|
-
const next = (e: React.KeyboardEvent) => {
|
|
366
|
-
e.preventDefault()
|
|
367
|
-
|
|
368
|
-
if (e.metaKey) {
|
|
369
|
-
// Last item
|
|
370
|
-
last()
|
|
371
|
-
} else if (e.altKey) {
|
|
372
|
-
// Next group
|
|
373
|
-
updateSelectedByGroup(1)
|
|
374
|
-
} else {
|
|
375
|
-
// Next item
|
|
376
|
-
updateSelectedByItem(1)
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const prev = (e: React.KeyboardEvent) => {
|
|
381
|
-
e.preventDefault()
|
|
382
|
-
|
|
383
|
-
if (e.metaKey) {
|
|
384
|
-
// First item
|
|
385
|
-
updateSelectedToIndex(0)
|
|
386
|
-
} else if (e.altKey) {
|
|
387
|
-
// Previous group
|
|
388
|
-
updateSelectedByGroup(-1)
|
|
389
|
-
} else {
|
|
390
|
-
// Previous item
|
|
391
|
-
updateSelectedByItem(-1)
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const handleKeyDown = useEventCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
396
|
-
etc.onKeyDown?.(e)
|
|
397
|
-
|
|
398
|
-
// Check if IME composition is finished before triggering key binds
|
|
399
|
-
// This prevents unwanted triggering while user is still inputting text with IME
|
|
400
|
-
// e.keyCode === 229 is for the CJK IME with Legacy Browser [https://w3c.github.io/uievents/#determine-keydown-keyup-keyCode]
|
|
401
|
-
// isComposing is for the CJK IME with Modern Browser [https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent/isComposing]
|
|
402
|
-
const isComposing = e.nativeEvent.isComposing || e.keyCode === 229
|
|
403
|
-
|
|
404
|
-
if (e.defaultPrevented || isComposing) {
|
|
405
|
-
return
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
switch (e.key) {
|
|
409
|
-
case "n":
|
|
410
|
-
case "j": {
|
|
411
|
-
// vim keybind down
|
|
412
|
-
if (vimBindings && e.ctrlKey) {
|
|
413
|
-
next(e)
|
|
414
|
-
}
|
|
415
|
-
break
|
|
416
|
-
}
|
|
417
|
-
case "ArrowDown": {
|
|
418
|
-
next(e)
|
|
419
|
-
break
|
|
420
|
-
}
|
|
421
|
-
case "p":
|
|
422
|
-
case "k": {
|
|
423
|
-
// vim keybind up
|
|
424
|
-
if (vimBindings && e.ctrlKey) {
|
|
425
|
-
prev(e)
|
|
426
|
-
}
|
|
427
|
-
break
|
|
428
|
-
}
|
|
429
|
-
case "ArrowUp": {
|
|
430
|
-
prev(e)
|
|
431
|
-
break
|
|
432
|
-
}
|
|
433
|
-
case "Home": {
|
|
434
|
-
// First item
|
|
435
|
-
e.preventDefault()
|
|
436
|
-
updateSelectedToIndex(0)
|
|
437
|
-
break
|
|
438
|
-
}
|
|
439
|
-
case "End": {
|
|
440
|
-
// Last item
|
|
441
|
-
e.preventDefault()
|
|
442
|
-
last()
|
|
443
|
-
break
|
|
444
|
-
}
|
|
445
|
-
case "Enter": {
|
|
446
|
-
// Trigger item onSelect
|
|
447
|
-
e.preventDefault()
|
|
448
|
-
const item = getSelectedItem()
|
|
449
|
-
if (item) {
|
|
450
|
-
const event = new Event(SELECT_EVENT)
|
|
451
|
-
item.dispatchEvent(event)
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
})
|
|
456
|
-
|
|
457
|
-
return (
|
|
458
|
-
<div
|
|
459
|
-
ref={forwardedRef}
|
|
460
|
-
tabIndex={-1}
|
|
461
|
-
{...etc}
|
|
462
|
-
className={tcx(tv.root({ className }))}
|
|
463
|
-
data-command-root=""
|
|
464
|
-
onKeyDown={handleKeyDown}
|
|
465
|
-
>
|
|
466
|
-
<label
|
|
467
|
-
htmlFor={context.inputId}
|
|
468
|
-
id={context.labelId}
|
|
469
|
-
className="sr-only"
|
|
470
|
-
>
|
|
471
|
-
{label}
|
|
472
|
-
</label>
|
|
473
|
-
{SlottableWithNestedChildren(props, (child) => (
|
|
474
|
-
<StoreContext.Provider value={store}>
|
|
475
|
-
<CommandContext.Provider value={context}>{child}</CommandContext.Provider>
|
|
476
|
-
</StoreContext.Provider>
|
|
477
|
-
))}
|
|
478
|
-
</div>
|
|
479
|
-
)
|
|
480
|
-
})
|
|
481
|
-
|
|
482
|
-
Command.displayName = "Command"
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { tcx } from "@choice-ui/shared"
|
|
2
|
-
import { forwardRef, HTMLProps } from "react"
|
|
3
|
-
import { useCommand, useCommandState } from "../hooks"
|
|
4
|
-
import { commandTv } from "../tv"
|
|
5
|
-
|
|
6
|
-
export interface CommandDividerProps extends HTMLProps<HTMLDivElement> {
|
|
7
|
-
/** 是否始终渲染此分隔符。当禁用自动过滤功能时特别有用。 */
|
|
8
|
-
alwaysRender?: boolean
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const CommandDivider = forwardRef<HTMLDivElement, CommandDividerProps>(
|
|
12
|
-
({ className, alwaysRender, ...props }, forwardedRef) => {
|
|
13
|
-
const context = useCommand()
|
|
14
|
-
const render = useCommandState((state) => !state.search)
|
|
15
|
-
const tv = commandTv({ variant: context.variant })
|
|
16
|
-
|
|
17
|
-
if (!alwaysRender && !render) return null
|
|
18
|
-
|
|
19
|
-
return (
|
|
20
|
-
<div
|
|
21
|
-
ref={forwardedRef}
|
|
22
|
-
{...props}
|
|
23
|
-
className={tcx(tv.divider({ className }))}
|
|
24
|
-
role="separator"
|
|
25
|
-
/>
|
|
26
|
-
)
|
|
27
|
-
},
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
CommandDivider.displayName = "CommandDivider"
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { tcx } from "@choice-ui/shared"
|
|
2
|
-
import { forwardRef, HTMLProps } from "react"
|
|
3
|
-
import { useCommand, useCommandState } from "../hooks"
|
|
4
|
-
import { commandEmptyTv } from "../tv"
|
|
5
|
-
|
|
6
|
-
export interface CommandEmptyProps extends HTMLProps<HTMLDivElement> {
|
|
7
|
-
className?: string
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const CommandEmpty = forwardRef<HTMLDivElement, CommandEmptyProps>(
|
|
11
|
-
({ className, ...props }, forwardedRef) => {
|
|
12
|
-
const context = useCommand()
|
|
13
|
-
const render = useCommandState((state) => state.filtered.count === 0)
|
|
14
|
-
|
|
15
|
-
const tv = commandEmptyTv({ variant: context.variant })
|
|
16
|
-
|
|
17
|
-
if (!render) return null
|
|
18
|
-
|
|
19
|
-
return (
|
|
20
|
-
<div
|
|
21
|
-
ref={forwardedRef}
|
|
22
|
-
{...props}
|
|
23
|
-
className={tcx(tv.root({ className }))}
|
|
24
|
-
role="presentation"
|
|
25
|
-
/>
|
|
26
|
-
)
|
|
27
|
-
},
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
CommandEmpty.displayName = "CommandEmpty"
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { tcx } from "@choice-ui/shared"
|
|
2
|
-
import { forwardRef, HTMLProps } from "react"
|
|
3
|
-
import { useCommand } from "../hooks/use-command"
|
|
4
|
-
import { commandFooterTv } from "../tv"
|
|
5
|
-
|
|
6
|
-
export const CommandFooter = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(
|
|
7
|
-
({ className, ...props }, ref) => {
|
|
8
|
-
const context = useCommand()
|
|
9
|
-
|
|
10
|
-
const tv = commandFooterTv({ variant: context.variant })
|
|
11
|
-
|
|
12
|
-
return (
|
|
13
|
-
<div
|
|
14
|
-
ref={ref}
|
|
15
|
-
className={tcx(tv.root({ className }))}
|
|
16
|
-
{...props}
|
|
17
|
-
/>
|
|
18
|
-
)
|
|
19
|
-
},
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
CommandFooter.displayName = "CommandFooter"
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { tcx } from "@choice-ui/shared"
|
|
2
|
-
import React, { forwardRef, HTMLProps, useEffect, useId, useMemo, useRef } from "react"
|
|
3
|
-
import { GroupContext, useCommand, useCommandState, useValue } from "../hooks"
|
|
4
|
-
import { commandGroupTv } from "../tv"
|
|
5
|
-
|
|
6
|
-
export interface CommandGroupProps extends HTMLProps<HTMLDivElement> {
|
|
7
|
-
forceMount?: boolean
|
|
8
|
-
heading?: React.ReactNode
|
|
9
|
-
value?: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const CommandGroup = forwardRef<HTMLDivElement, CommandGroupProps>((props, forwardedRef) => {
|
|
13
|
-
const { className, heading, children, forceMount, value, ...rest } = props
|
|
14
|
-
const id = useId()
|
|
15
|
-
const ref = useRef<HTMLDivElement | null>(null)
|
|
16
|
-
const headingRef = useRef<HTMLDivElement | null>(null)
|
|
17
|
-
const headingId = React.useId()
|
|
18
|
-
const context = useCommand()
|
|
19
|
-
|
|
20
|
-
const render = useCommandState((state) =>
|
|
21
|
-
forceMount
|
|
22
|
-
? true
|
|
23
|
-
: context.filter() === false
|
|
24
|
-
? true
|
|
25
|
-
: !state.search
|
|
26
|
-
? true
|
|
27
|
-
: state.filtered.groups.has(id),
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
// 注册group
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
return context.group(id)
|
|
33
|
-
}, [context, id])
|
|
34
|
-
|
|
35
|
-
const valueDeps = useMemo(() => [value, heading, headingRef], [value, heading])
|
|
36
|
-
useValue(id, ref, valueDeps)
|
|
37
|
-
|
|
38
|
-
const contextValue = useMemo(() => ({ id, forceMount }), [id, forceMount])
|
|
39
|
-
|
|
40
|
-
const tv = commandGroupTv({ variant: context.variant })
|
|
41
|
-
|
|
42
|
-
if (!render) return null
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<div
|
|
46
|
-
ref={(el) => {
|
|
47
|
-
ref.current = el
|
|
48
|
-
if (typeof forwardedRef === "function") forwardedRef(el)
|
|
49
|
-
else if (forwardedRef) forwardedRef.current = el
|
|
50
|
-
}}
|
|
51
|
-
{...rest}
|
|
52
|
-
className={tcx(tv.root({ className }))}
|
|
53
|
-
role="presentation"
|
|
54
|
-
data-value={value}
|
|
55
|
-
>
|
|
56
|
-
{heading && (
|
|
57
|
-
<div
|
|
58
|
-
ref={headingRef}
|
|
59
|
-
className={tcx(tv.heading())}
|
|
60
|
-
aria-hidden
|
|
61
|
-
id={headingId}
|
|
62
|
-
>
|
|
63
|
-
{heading}
|
|
64
|
-
</div>
|
|
65
|
-
)}
|
|
66
|
-
<div
|
|
67
|
-
role="group"
|
|
68
|
-
aria-labelledby={heading ? headingId : undefined}
|
|
69
|
-
>
|
|
70
|
-
<GroupContext.Provider value={contextValue}>{children}</GroupContext.Provider>
|
|
71
|
-
</div>
|
|
72
|
-
</div>
|
|
73
|
-
)
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
CommandGroup.displayName = "CommandGroup"
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { tcx } from "@choice-ui/shared"
|
|
2
|
-
import { Input, type InputProps } from "@choice-ui/input"
|
|
3
|
-
import { forwardRef, ReactNode, useEffect } from "react"
|
|
4
|
-
import { useEventCallback } from "usehooks-ts"
|
|
5
|
-
import { useCommand, useCommandState } from "../hooks"
|
|
6
|
-
import { commandInputTv } from "../tv"
|
|
7
|
-
|
|
8
|
-
export interface CommandInputProps extends Omit<InputProps, "value" | "onChange" | "type"> {
|
|
9
|
-
onChange?: (search: string) => void
|
|
10
|
-
prefixElement?: ReactNode
|
|
11
|
-
suffixElement?: ReactNode
|
|
12
|
-
value?: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const CommandInput = forwardRef<HTMLInputElement, CommandInputProps>((props, ref) => {
|
|
16
|
-
const { className, onChange, value, prefixElement, suffixElement, ...rest } = props
|
|
17
|
-
const isControlled = value != null
|
|
18
|
-
const store = useCommand().store
|
|
19
|
-
const search = useCommandState((state) => state.search)
|
|
20
|
-
const selectedItemId = useCommandState((state) => state.selectedItemId)
|
|
21
|
-
const context = useCommand()
|
|
22
|
-
|
|
23
|
-
useEffect(() => {
|
|
24
|
-
if (value != null) {
|
|
25
|
-
store.setState("search", value)
|
|
26
|
-
}
|
|
27
|
-
}, [value, store])
|
|
28
|
-
|
|
29
|
-
const handleChange = useEventCallback((value: string) => {
|
|
30
|
-
if (!isControlled) {
|
|
31
|
-
store.setState("search", value)
|
|
32
|
-
}
|
|
33
|
-
onChange?.(value)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
const tv = commandInputTv({ size: context.size })
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<div className={tcx(tv.root({ className }))}>
|
|
40
|
-
{prefixElement}
|
|
41
|
-
<Input
|
|
42
|
-
ref={ref}
|
|
43
|
-
{...rest}
|
|
44
|
-
className={tcx(tv.input({ className }))}
|
|
45
|
-
variant={props.variant || context.variant}
|
|
46
|
-
data-command-input=""
|
|
47
|
-
autoComplete="off"
|
|
48
|
-
autoCorrect="off"
|
|
49
|
-
spellCheck={false}
|
|
50
|
-
aria-autocomplete="list"
|
|
51
|
-
role="combobox"
|
|
52
|
-
aria-expanded={true}
|
|
53
|
-
aria-controls={context.listId}
|
|
54
|
-
aria-labelledby={context.labelId}
|
|
55
|
-
aria-activedescendant={selectedItemId}
|
|
56
|
-
id={context.inputId}
|
|
57
|
-
type="text"
|
|
58
|
-
value={isControlled ? value : search}
|
|
59
|
-
onChange={handleChange}
|
|
60
|
-
/>
|
|
61
|
-
{suffixElement}
|
|
62
|
-
</div>
|
|
63
|
-
)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
CommandInput.displayName = "CommandInput"
|