@tamagui/select 2.7.7 → 3.0.0-beta.1093.1
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/cjs/Select.cjs +406 -467
- package/dist/cjs/Select.native.cjs +495 -0
- package/dist/cjs/Select.native.js +445 -492
- package/dist/cjs/Select.native.js.map +1 -1
- package/dist/cjs/SelectContent.cjs +79 -79
- package/dist/cjs/SelectContent.native.cjs +29 -0
- package/dist/cjs/SelectContent.native.js +17 -22
- package/dist/cjs/SelectContent.native.js.map +1 -1
- package/dist/cjs/SelectImpl.cjs +305 -320
- package/dist/cjs/SelectImpl.native.cjs +29 -0
- package/dist/cjs/SelectImpl.native.js +16 -24
- package/dist/cjs/SelectImpl.native.js.map +1 -1
- package/dist/cjs/SelectItem.cjs +256 -205
- package/dist/cjs/SelectItem.native.cjs +288 -0
- package/dist/cjs/SelectItem.native.js +264 -213
- package/dist/cjs/SelectItem.native.js.map +1 -1
- package/dist/cjs/SelectItemText.cjs +48 -79
- package/dist/cjs/SelectItemText.native.cjs +73 -0
- package/dist/cjs/SelectItemText.native.js +49 -79
- package/dist/cjs/SelectItemText.native.js.map +1 -1
- package/dist/cjs/SelectScrollButton.cjs +110 -130
- package/dist/cjs/SelectScrollButton.native.cjs +37 -0
- package/dist/cjs/SelectScrollButton.native.js +24 -22
- package/dist/cjs/SelectScrollButton.native.js.map +1 -1
- package/dist/cjs/SelectTrigger.cjs +68 -84
- package/dist/cjs/SelectTrigger.native.cjs +90 -0
- package/dist/cjs/SelectTrigger.native.js +70 -66
- package/dist/cjs/SelectTrigger.native.js.map +1 -1
- package/dist/cjs/SelectViewport.cjs +121 -142
- package/dist/cjs/SelectViewport.native.cjs +72 -0
- package/dist/cjs/SelectViewport.native.js +53 -50
- package/dist/cjs/SelectViewport.native.js.map +1 -1
- package/dist/cjs/constants.cjs +20 -22
- package/dist/cjs/constants.native.cjs +40 -0
- package/dist/cjs/constants.native.js +20 -21
- package/dist/cjs/constants.native.js.map +1 -1
- package/dist/cjs/context.cjs +37 -48
- package/dist/cjs/context.native.cjs +57 -0
- package/dist/cjs/context.native.js +39 -49
- package/dist/cjs/context.native.js.map +1 -1
- package/dist/cjs/index.cjs +14 -11
- package/dist/cjs/index.native.cjs +26 -0
- package/dist/cjs/index.native.js +14 -10
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/selectionController.cjs +343 -0
- package/dist/cjs/selectionController.native.cjs +407 -0
- package/dist/cjs/selectionController.native.js +409 -0
- package/dist/cjs/selectionController.native.js.map +1 -0
- package/dist/cjs/types.cjs +9 -11
- package/dist/cjs/types.native.cjs +18 -0
- package/dist/cjs/types.native.js +9 -10
- package/dist/cjs/types.native.js.map +1 -1
- package/dist/esm/Select.mjs +381 -443
- package/dist/esm/Select.mjs.map +1 -1
- package/dist/esm/Select.native.js +420 -469
- package/dist/esm/Select.native.js.map +1 -1
- package/dist/esm/SelectContent.mjs +67 -61
- package/dist/esm/SelectContent.mjs.map +1 -1
- package/dist/esm/SelectContent.native.js +4 -5
- package/dist/esm/SelectContent.native.js.map +1 -1
- package/dist/esm/SelectImpl.mjs +291 -295
- package/dist/esm/SelectImpl.mjs.map +1 -1
- package/dist/esm/SelectImpl.native.js +5 -8
- package/dist/esm/SelectImpl.native.js.map +1 -1
- package/dist/esm/SelectItem.mjs +240 -181
- package/dist/esm/SelectItem.mjs.map +1 -1
- package/dist/esm/SelectItem.native.js +249 -191
- package/dist/esm/SelectItem.native.js.map +1 -1
- package/dist/esm/SelectItemText.mjs +33 -55
- package/dist/esm/SelectItemText.mjs.map +1 -1
- package/dist/esm/SelectItemText.native.js +34 -56
- package/dist/esm/SelectItemText.native.js.map +1 -1
- package/dist/esm/SelectScrollButton.mjs +95 -107
- package/dist/esm/SelectScrollButton.mjs.map +1 -1
- package/dist/esm/SelectScrollButton.native.js +11 -7
- package/dist/esm/SelectScrollButton.native.js.map +1 -1
- package/dist/esm/SelectTrigger.mjs +56 -58
- package/dist/esm/SelectTrigger.mjs.map +1 -1
- package/dist/esm/SelectTrigger.native.js +54 -44
- package/dist/esm/SelectTrigger.native.js.map +1 -1
- package/dist/esm/SelectViewport.mjs +107 -119
- package/dist/esm/SelectViewport.mjs.map +1 -1
- package/dist/esm/SelectViewport.native.js +44 -39
- package/dist/esm/SelectViewport.native.js.map +1 -1
- package/dist/esm/constants.mjs +1 -0
- package/dist/esm/constants.mjs.map +1 -1
- package/dist/esm/constants.native.js +1 -0
- package/dist/esm/constants.native.js.map +1 -1
- package/dist/esm/context.mjs +20 -28
- package/dist/esm/context.mjs.map +1 -1
- package/dist/esm/context.native.js +23 -31
- package/dist/esm/context.native.js.map +1 -1
- package/dist/esm/index.js +13 -3
- package/dist/esm/index.mjs +13 -3
- package/dist/esm/index.native.js +13 -3
- package/dist/esm/selectionController.mjs +315 -0
- package/dist/esm/selectionController.mjs.map +1 -0
- package/dist/esm/selectionController.native.js +377 -0
- package/dist/esm/selectionController.native.js.map +1 -0
- package/dist/esm/types.mjs +0 -2
- package/dist/esm/types.native.js +0 -2
- package/dist/jsx/Select.mjs +381 -443
- package/dist/jsx/Select.mjs.map +1 -1
- package/dist/jsx/Select.native.cjs +495 -0
- package/dist/jsx/Select.native.js +445 -492
- package/dist/jsx/Select.native.js.map +1 -1
- package/dist/jsx/SelectContent.mjs +67 -61
- package/dist/jsx/SelectContent.mjs.map +1 -1
- package/dist/jsx/SelectContent.native.cjs +29 -0
- package/dist/jsx/SelectContent.native.js +17 -22
- package/dist/jsx/SelectContent.native.js.map +1 -1
- package/dist/jsx/SelectImpl.mjs +291 -295
- package/dist/jsx/SelectImpl.mjs.map +1 -1
- package/dist/jsx/SelectImpl.native.cjs +29 -0
- package/dist/jsx/SelectImpl.native.js +16 -24
- package/dist/jsx/SelectImpl.native.js.map +1 -1
- package/dist/jsx/SelectItem.mjs +240 -181
- package/dist/jsx/SelectItem.mjs.map +1 -1
- package/dist/jsx/SelectItem.native.cjs +288 -0
- package/dist/jsx/SelectItem.native.js +264 -213
- package/dist/jsx/SelectItem.native.js.map +1 -1
- package/dist/jsx/SelectItemText.mjs +33 -55
- package/dist/jsx/SelectItemText.mjs.map +1 -1
- package/dist/jsx/SelectItemText.native.cjs +73 -0
- package/dist/jsx/SelectItemText.native.js +49 -79
- package/dist/jsx/SelectItemText.native.js.map +1 -1
- package/dist/jsx/SelectScrollButton.mjs +95 -107
- package/dist/jsx/SelectScrollButton.mjs.map +1 -1
- package/dist/jsx/SelectScrollButton.native.cjs +37 -0
- package/dist/jsx/SelectScrollButton.native.js +24 -22
- package/dist/jsx/SelectScrollButton.native.js.map +1 -1
- package/dist/jsx/SelectTrigger.mjs +56 -58
- package/dist/jsx/SelectTrigger.mjs.map +1 -1
- package/dist/jsx/SelectTrigger.native.cjs +90 -0
- package/dist/jsx/SelectTrigger.native.js +70 -66
- package/dist/jsx/SelectTrigger.native.js.map +1 -1
- package/dist/jsx/SelectViewport.mjs +107 -119
- package/dist/jsx/SelectViewport.mjs.map +1 -1
- package/dist/jsx/SelectViewport.native.cjs +72 -0
- package/dist/jsx/SelectViewport.native.js +53 -50
- package/dist/jsx/SelectViewport.native.js.map +1 -1
- package/dist/jsx/constants.mjs +1 -0
- package/dist/jsx/constants.mjs.map +1 -1
- package/dist/jsx/constants.native.cjs +40 -0
- package/dist/jsx/constants.native.js +20 -21
- package/dist/jsx/constants.native.js.map +1 -1
- package/dist/jsx/context.mjs +20 -28
- package/dist/jsx/context.mjs.map +1 -1
- package/dist/jsx/context.native.cjs +57 -0
- package/dist/jsx/context.native.js +39 -49
- package/dist/jsx/context.native.js.map +1 -1
- package/dist/jsx/index.js +13 -3
- package/dist/jsx/index.mjs +13 -3
- package/dist/jsx/index.native.cjs +26 -0
- package/dist/jsx/index.native.js +14 -10
- package/dist/jsx/index.native.js.map +1 -1
- package/dist/jsx/selectionController.mjs +315 -0
- package/dist/jsx/selectionController.mjs.map +1 -0
- package/dist/jsx/selectionController.native.cjs +407 -0
- package/dist/jsx/selectionController.native.js +409 -0
- package/dist/jsx/selectionController.native.js.map +1 -0
- package/dist/jsx/types.mjs +0 -2
- package/dist/jsx/types.native.cjs +18 -0
- package/dist/jsx/types.native.js +9 -10
- package/dist/jsx/types.native.js.map +1 -1
- package/package.json +31 -30
- package/src/Select.tsx +478 -378
- package/src/SelectContent.tsx +46 -16
- package/src/SelectImpl.native.tsx +0 -8
- package/src/SelectImpl.tsx +149 -160
- package/src/SelectItem.tsx +280 -159
- package/src/SelectItemText.tsx +16 -33
- package/src/SelectScrollButton.native.tsx +13 -4
- package/src/SelectScrollButton.tsx +57 -42
- package/src/SelectTrigger.tsx +77 -53
- package/src/SelectViewport.native.tsx +42 -25
- package/src/SelectViewport.tsx +60 -60
- package/src/context.tsx +6 -2
- package/src/index.tsx +5 -2
- package/src/selectionController.ts +404 -0
- package/src/types.tsx +105 -59
- package/types/Select.d.ts +139 -201
- package/types/Select.d.ts.map +1 -1
- package/types/SelectContent.d.ts +1 -1
- package/types/SelectContent.d.ts.map +1 -1
- package/types/SelectContent.native.d.ts.map +1 -1
- package/types/SelectImpl.d.ts +2 -1
- package/types/SelectImpl.d.ts.map +1 -1
- package/types/SelectImpl.native.d.ts +1 -5
- package/types/SelectImpl.native.d.ts.map +1 -1
- package/types/SelectItem.d.ts +13 -16
- package/types/SelectItem.d.ts.map +1 -1
- package/types/SelectItemText.d.ts +9 -13
- package/types/SelectItemText.d.ts.map +1 -1
- package/types/SelectScrollButton.d.ts +15 -2
- package/types/SelectScrollButton.d.ts.map +1 -1
- package/types/SelectScrollButton.native.d.ts +16 -3
- package/types/SelectScrollButton.native.d.ts.map +1 -1
- package/types/SelectTrigger.d.ts +11 -23
- package/types/SelectTrigger.d.ts.map +1 -1
- package/types/SelectViewport.d.ts +11 -38
- package/types/SelectViewport.d.ts.map +1 -1
- package/types/SelectViewport.native.d.ts +15 -4
- package/types/SelectViewport.native.d.ts.map +1 -1
- package/types/context.d.ts +2 -0
- package/types/context.d.ts.map +1 -1
- package/types/index.d.ts +5 -2
- package/types/index.d.ts.map +1 -1
- package/types/selectionController.d.ts +90 -0
- package/types/selectionController.d.ts.map +1 -0
- package/types/types.d.ts +66 -50
- package/types/types.d.ts.map +1 -1
- package/dist/cjs/BubbleSelect.cjs +0 -50
- package/dist/cjs/BubbleSelect.native.js +0 -30
- package/dist/cjs/BubbleSelect.native.js.map +0 -1
- package/dist/cjs/useSelectBreakpointActive.cjs +0 -32
- package/dist/cjs/useSelectBreakpointActive.native.js +0 -35
- package/dist/cjs/useSelectBreakpointActive.native.js.map +0 -1
- package/dist/esm/BubbleSelect.mjs +0 -27
- package/dist/esm/BubbleSelect.mjs.map +0 -1
- package/dist/esm/BubbleSelect.native.js +0 -4
- package/dist/esm/BubbleSelect.native.js.map +0 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/index.mjs.map +0 -1
- package/dist/esm/index.native.js.map +0 -1
- package/dist/esm/types.mjs.map +0 -1
- package/dist/esm/types.native.js.map +0 -1
- package/dist/esm/useSelectBreakpointActive.mjs +0 -7
- package/dist/esm/useSelectBreakpointActive.mjs.map +0 -1
- package/dist/esm/useSelectBreakpointActive.native.js +0 -7
- package/dist/esm/useSelectBreakpointActive.native.js.map +0 -1
- package/dist/jsx/BubbleSelect.mjs +0 -27
- package/dist/jsx/BubbleSelect.mjs.map +0 -1
- package/dist/jsx/BubbleSelect.native.js +0 -30
- package/dist/jsx/BubbleSelect.native.js.map +0 -1
- package/dist/jsx/index.js.map +0 -1
- package/dist/jsx/index.mjs.map +0 -1
- package/dist/jsx/types.mjs.map +0 -1
- package/dist/jsx/useSelectBreakpointActive.mjs +0 -7
- package/dist/jsx/useSelectBreakpointActive.mjs.map +0 -1
- package/dist/jsx/useSelectBreakpointActive.native.js +0 -35
- package/dist/jsx/useSelectBreakpointActive.native.js.map +0 -1
- package/src/BubbleSelect.tsx +0 -50
- package/src/useSelectBreakpointActive.tsx +0 -7
- package/types/BubbleSelect.d.ts +0 -2
- package/types/BubbleSelect.d.ts.map +0 -1
- package/types/useSelectBreakpointActive.d.ts +0 -3
- package/types/useSelectBreakpointActive.d.ts.map +0 -1
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export type SelectMode = 'single' | 'multiple'
|
|
4
|
+
export type SelectSelection = string | string[]
|
|
5
|
+
|
|
6
|
+
export type SelectRegisteredItem = {
|
|
7
|
+
id: symbol
|
|
8
|
+
value: string
|
|
9
|
+
disabled: boolean
|
|
10
|
+
textValue?: string
|
|
11
|
+
label?: ReactNode
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type SelectItemRegistration = {
|
|
15
|
+
id: symbol
|
|
16
|
+
update(item: Partial<Omit<SelectRegisteredItem, 'id'>>): void
|
|
17
|
+
setNode(node: unknown): void
|
|
18
|
+
unregister(): void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type SelectItemRegistry = ReturnType<typeof createSelectItemRegistry>
|
|
22
|
+
|
|
23
|
+
export function getSelectLabelText(label: ReactNode): string {
|
|
24
|
+
if (typeof label === 'string' || typeof label === 'number') {
|
|
25
|
+
return String(label)
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(label)) {
|
|
28
|
+
return label.map(getSelectLabelText).join('')
|
|
29
|
+
}
|
|
30
|
+
if (label && typeof label === 'object' && 'props' in label) {
|
|
31
|
+
return getSelectLabelText((label as any).props?.children)
|
|
32
|
+
}
|
|
33
|
+
return ''
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createSelectItemRegistry(onChange?: () => void) {
|
|
37
|
+
const items: SelectRegisteredItem[] = []
|
|
38
|
+
const labelRegistrations = new Map<
|
|
39
|
+
string,
|
|
40
|
+
Map<symbol, { label: ReactNode; textValue?: string }>
|
|
41
|
+
>()
|
|
42
|
+
const explicitTextValues = new Map<symbol, string | undefined>()
|
|
43
|
+
const itemNodes = new Map<symbol, any>()
|
|
44
|
+
const listeners = new Set<() => void>()
|
|
45
|
+
|
|
46
|
+
// value -> first index, rebuilt after any change: items look themselves up
|
|
47
|
+
// on every active-index emit, so the lookup must not walk the list
|
|
48
|
+
let indexByValue: Map<string, number> | null = null
|
|
49
|
+
|
|
50
|
+
const notify = () => {
|
|
51
|
+
indexByValue = null
|
|
52
|
+
onChange?.()
|
|
53
|
+
listeners.forEach((listener) => listener())
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const getItems = () => items
|
|
57
|
+
|
|
58
|
+
const getItem = (value: string) => items.find((item) => item.value === value)
|
|
59
|
+
|
|
60
|
+
const getIndex = (value: string) => {
|
|
61
|
+
if (!indexByValue) {
|
|
62
|
+
indexByValue = new Map()
|
|
63
|
+
items.forEach((item, index) => {
|
|
64
|
+
if (!indexByValue!.has(item.value)) indexByValue!.set(item.value, index)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
return indexByValue.get(value) ?? -1
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** the mounted nodes in item order; the owner mirrors this into its list ref */
|
|
71
|
+
const getNodes = () => items.map((item) => itemNodes.get(item.id) ?? null)
|
|
72
|
+
|
|
73
|
+
const getRegisteredLabel = (value: string) => {
|
|
74
|
+
const registrations = labelRegistrations.get(value)
|
|
75
|
+
if (!registrations?.size) return undefined
|
|
76
|
+
const values = Array.from(registrations.values())
|
|
77
|
+
return values[values.length - 1]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const applyRegisteredLabel = (value: string) => {
|
|
81
|
+
const registeredLabel = getRegisteredLabel(value)
|
|
82
|
+
let changed = false
|
|
83
|
+
for (const item of items) {
|
|
84
|
+
if (item.value !== value) continue
|
|
85
|
+
const nextLabel = registeredLabel?.label
|
|
86
|
+
const nextTextValue = explicitTextValues.get(item.id) || registeredLabel?.textValue
|
|
87
|
+
if (item.label !== nextLabel || item.textValue !== nextTextValue) {
|
|
88
|
+
item.label = nextLabel
|
|
89
|
+
item.textValue = nextTextValue
|
|
90
|
+
changed = true
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (changed) notify()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const syncNodeOrder = () => {
|
|
97
|
+
items.sort((a, b) => {
|
|
98
|
+
const aNode = itemNodes.get(a.id)
|
|
99
|
+
const bNode = itemNodes.get(b.id)
|
|
100
|
+
if (!aNode || !bNode || typeof aNode.compareDocumentPosition !== 'function') {
|
|
101
|
+
return 0
|
|
102
|
+
}
|
|
103
|
+
const position = aNode.compareDocumentPosition(bNode)
|
|
104
|
+
if (position & 4) return -1
|
|
105
|
+
if (position & 2) return 1
|
|
106
|
+
return 0
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const registerItem = (
|
|
111
|
+
item: Omit<SelectRegisteredItem, 'id'>
|
|
112
|
+
): SelectItemRegistration => {
|
|
113
|
+
const id = Symbol(item.value)
|
|
114
|
+
const registeredLabel = getRegisteredLabel(item.value)
|
|
115
|
+
const registeredItem: SelectRegisteredItem = {
|
|
116
|
+
...item,
|
|
117
|
+
...registeredLabel,
|
|
118
|
+
textValue: item.textValue ?? registeredLabel?.textValue,
|
|
119
|
+
id,
|
|
120
|
+
}
|
|
121
|
+
explicitTextValues.set(id, item.textValue)
|
|
122
|
+
items.push(registeredItem)
|
|
123
|
+
notify()
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
id,
|
|
127
|
+
update(next) {
|
|
128
|
+
const index = items.findIndex((current) => current.id === id)
|
|
129
|
+
if (index < 0) return
|
|
130
|
+
const current = items[index]
|
|
131
|
+
if ('textValue' in next) {
|
|
132
|
+
explicitTextValues.set(id, next.textValue)
|
|
133
|
+
}
|
|
134
|
+
const nextItem = { ...current, ...next }
|
|
135
|
+
if (next.value !== undefined && next.value !== current.value) {
|
|
136
|
+
const nextLabel = getRegisteredLabel(next.value)
|
|
137
|
+
nextItem.label = nextLabel?.label
|
|
138
|
+
}
|
|
139
|
+
const explicitTextValue = explicitTextValues.get(id)
|
|
140
|
+
nextItem.textValue =
|
|
141
|
+
explicitTextValue ||
|
|
142
|
+
getSelectLabelText(nextItem.label) ||
|
|
143
|
+
(nextItem.label ? nextItem.value : undefined)
|
|
144
|
+
const changed = Object.entries(next).some(
|
|
145
|
+
([key, value]) => current[key as keyof SelectRegisteredItem] !== value
|
|
146
|
+
)
|
|
147
|
+
const textValueChanged = current.textValue !== nextItem.textValue
|
|
148
|
+
if (!changed && !textValueChanged) return
|
|
149
|
+
items[index] = nextItem
|
|
150
|
+
notify()
|
|
151
|
+
},
|
|
152
|
+
setNode(node) {
|
|
153
|
+
if ((itemNodes.get(id) ?? null) === (node ?? null)) return
|
|
154
|
+
if (node) {
|
|
155
|
+
itemNodes.set(id, node)
|
|
156
|
+
syncNodeOrder()
|
|
157
|
+
} else {
|
|
158
|
+
itemNodes.delete(id)
|
|
159
|
+
}
|
|
160
|
+
notify()
|
|
161
|
+
},
|
|
162
|
+
unregister() {
|
|
163
|
+
const index = items.findIndex((current) => current.id === id)
|
|
164
|
+
if (index < 0) return
|
|
165
|
+
items.splice(index, 1)
|
|
166
|
+
explicitTextValues.delete(id)
|
|
167
|
+
itemNodes.delete(id)
|
|
168
|
+
notify()
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const registerLabel = (value: string, label: ReactNode, textValue?: string) => {
|
|
174
|
+
const id = Symbol(value)
|
|
175
|
+
const normalizedTextValue = textValue || getSelectLabelText(label) || value
|
|
176
|
+
const registrations =
|
|
177
|
+
labelRegistrations.get(value) ??
|
|
178
|
+
(() => {
|
|
179
|
+
const next = new Map<symbol, { label: ReactNode; textValue?: string }>()
|
|
180
|
+
labelRegistrations.set(value, next)
|
|
181
|
+
return next
|
|
182
|
+
})()
|
|
183
|
+
registrations.set(id, { label, textValue: normalizedTextValue })
|
|
184
|
+
applyRegisteredLabel(value)
|
|
185
|
+
|
|
186
|
+
return () => {
|
|
187
|
+
registrations.delete(id)
|
|
188
|
+
if (!registrations.size) {
|
|
189
|
+
labelRegistrations.delete(value)
|
|
190
|
+
}
|
|
191
|
+
applyRegisteredLabel(value)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const firstEnabledIndex = () => items.findIndex((item) => !item.disabled)
|
|
196
|
+
|
|
197
|
+
const nextEnabledIndex = (fromIndex: number | null, direction: 1 | -1) => {
|
|
198
|
+
if (!items.length) return -1
|
|
199
|
+
if (fromIndex == null || fromIndex < 0 || fromIndex >= items.length) {
|
|
200
|
+
if (direction === 1) return firstEnabledIndex()
|
|
201
|
+
for (let index = items.length - 1; index >= 0; index--) {
|
|
202
|
+
if (!items[index].disabled) return index
|
|
203
|
+
}
|
|
204
|
+
return -1
|
|
205
|
+
}
|
|
206
|
+
for (let offset = 1; offset <= items.length; offset++) {
|
|
207
|
+
const index = (fromIndex + direction * offset + items.length) % items.length
|
|
208
|
+
if (!items[index]?.disabled) return index
|
|
209
|
+
}
|
|
210
|
+
return -1
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const findTypeaheadIndex = (search: string, fromIndex: number | null) => {
|
|
214
|
+
const normalizedSearch = search.trim().toLocaleLowerCase()
|
|
215
|
+
if (!normalizedSearch) return -1
|
|
216
|
+
const start = fromIndex == null || fromIndex < 0 ? -1 : fromIndex
|
|
217
|
+
for (let offset = 1; offset <= items.length; offset++) {
|
|
218
|
+
const index = (start + offset) % items.length
|
|
219
|
+
const item = items[index]
|
|
220
|
+
const label = item.textValue || getSelectLabelText(item.label) || item.value
|
|
221
|
+
if (!item.disabled && label.toLocaleLowerCase().startsWith(normalizedSearch)) {
|
|
222
|
+
return index
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return -1
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
subscribe(listener: () => void) {
|
|
230
|
+
listeners.add(listener)
|
|
231
|
+
return () => {
|
|
232
|
+
listeners.delete(listener)
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
registerItem,
|
|
236
|
+
registerLabel,
|
|
237
|
+
getItems,
|
|
238
|
+
getItem,
|
|
239
|
+
getIndex,
|
|
240
|
+
getNodes,
|
|
241
|
+
firstEnabledIndex,
|
|
242
|
+
nextEnabledIndex,
|
|
243
|
+
findTypeaheadIndex,
|
|
244
|
+
getDisabledIndices: () =>
|
|
245
|
+
items.flatMap((item, index) => (item.disabled ? [index] : [])),
|
|
246
|
+
getTypeaheadLabels: () =>
|
|
247
|
+
items.map((item) => item.textValue || getSelectLabelText(item.label) || item.value),
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function normalizeSelectSelection(
|
|
252
|
+
mode: SelectMode,
|
|
253
|
+
value: SelectSelection | undefined
|
|
254
|
+
): SelectSelection {
|
|
255
|
+
if (mode === 'single') {
|
|
256
|
+
return typeof value === 'string' ? value : ''
|
|
257
|
+
}
|
|
258
|
+
const values = Array.isArray(value) ? value : []
|
|
259
|
+
return [...new Set(values)]
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function selectedValuesFromSelection(
|
|
263
|
+
mode: SelectMode,
|
|
264
|
+
value: SelectSelection | undefined
|
|
265
|
+
): string[] {
|
|
266
|
+
const normalized = normalizeSelectSelection(mode, value)
|
|
267
|
+
return mode === 'multiple'
|
|
268
|
+
? (normalized as string[])
|
|
269
|
+
: normalized
|
|
270
|
+
? [normalized as string]
|
|
271
|
+
: []
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function createSelectSelectionController({
|
|
275
|
+
mode: initialMode,
|
|
276
|
+
value: initialValue,
|
|
277
|
+
registry,
|
|
278
|
+
}: {
|
|
279
|
+
mode: SelectMode
|
|
280
|
+
value: SelectSelection
|
|
281
|
+
registry: SelectItemRegistry
|
|
282
|
+
}) {
|
|
283
|
+
let mode = initialMode
|
|
284
|
+
let value = normalizeSelectSelection(mode, initialValue)
|
|
285
|
+
let activeIndex: number | null = null
|
|
286
|
+
|
|
287
|
+
const selectedValues = () => selectedValuesFromSelection(mode, value)
|
|
288
|
+
|
|
289
|
+
const setMode = (nextMode: SelectMode) => {
|
|
290
|
+
if (mode === nextMode) return value
|
|
291
|
+
mode = nextMode
|
|
292
|
+
value = normalizeSelectSelection(mode, value)
|
|
293
|
+
return value
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const setValue = (nextValue: SelectSelection) => {
|
|
297
|
+
value = normalizeSelectSelection(mode, nextValue)
|
|
298
|
+
return value
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const isSelected = (itemValue: string) => selectedValues().includes(itemValue)
|
|
302
|
+
|
|
303
|
+
const toggle = (itemValue: string) => {
|
|
304
|
+
const item = registry.getItem(itemValue)
|
|
305
|
+
if (!item || item.disabled) return value
|
|
306
|
+
if (mode === 'single') {
|
|
307
|
+
value = itemValue
|
|
308
|
+
return value
|
|
309
|
+
}
|
|
310
|
+
const current = selectedValues()
|
|
311
|
+
value = current.includes(itemValue)
|
|
312
|
+
? current.filter((currentValue) => currentValue !== itemValue)
|
|
313
|
+
: [...current, itemValue]
|
|
314
|
+
return value
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const selectionAnchor = () => {
|
|
318
|
+
const values = selectedValues()
|
|
319
|
+
for (let index = values.length - 1; index >= 0; index--) {
|
|
320
|
+
const item = registry.getItem(values[index])
|
|
321
|
+
if (item) return item
|
|
322
|
+
}
|
|
323
|
+
const firstEnabledIndex = registry.firstEnabledIndex()
|
|
324
|
+
return firstEnabledIndex < 0 ? undefined : registry.getItems()[firstEnabledIndex]
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const selectionAnchorIndex = () => {
|
|
328
|
+
const anchor = selectionAnchor()
|
|
329
|
+
return anchor ? registry.getIndex(anchor.value) : -1
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const initialActiveIndex = () => {
|
|
333
|
+
const anchorIndex = selectionAnchorIndex()
|
|
334
|
+
const anchor = anchorIndex < 0 ? undefined : registry.getItems()[anchorIndex]
|
|
335
|
+
return anchor && !anchor.disabled ? anchorIndex : registry.firstEnabledIndex()
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const moveActive = (direction: 1 | -1) => {
|
|
339
|
+
const nextIndex = registry.nextEnabledIndex(activeIndex, direction)
|
|
340
|
+
activeIndex = nextIndex < 0 ? null : nextIndex
|
|
341
|
+
return activeIndex == null ? undefined : registry.getItems()[activeIndex]
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
registry,
|
|
346
|
+
get mode() {
|
|
347
|
+
return mode
|
|
348
|
+
},
|
|
349
|
+
get value() {
|
|
350
|
+
return value
|
|
351
|
+
},
|
|
352
|
+
get activeIndex() {
|
|
353
|
+
return activeIndex
|
|
354
|
+
},
|
|
355
|
+
get activeItem() {
|
|
356
|
+
return activeIndex == null ? undefined : registry.getItems()[activeIndex]
|
|
357
|
+
},
|
|
358
|
+
get shouldCloseOnSelect() {
|
|
359
|
+
return mode === 'single'
|
|
360
|
+
},
|
|
361
|
+
setMode,
|
|
362
|
+
setValue,
|
|
363
|
+
isSelected,
|
|
364
|
+
toggle,
|
|
365
|
+
selectionAnchor,
|
|
366
|
+
selectionAnchorIndex,
|
|
367
|
+
initialActiveIndex,
|
|
368
|
+
setActiveIndex(index: number | null) {
|
|
369
|
+
activeIndex = index
|
|
370
|
+
},
|
|
371
|
+
moveActive,
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function getSelectListboxProps(mode: SelectMode) {
|
|
376
|
+
return {
|
|
377
|
+
role: 'listbox' as const,
|
|
378
|
+
'aria-multiselectable': mode === 'multiple' ? true : undefined,
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function getSelectOptionProps(
|
|
383
|
+
mode: SelectMode,
|
|
384
|
+
selected: boolean,
|
|
385
|
+
disabled: boolean,
|
|
386
|
+
platform: 'web' | 'native'
|
|
387
|
+
) {
|
|
388
|
+
if (platform === 'native') {
|
|
389
|
+
return {
|
|
390
|
+
accessibilityRole:
|
|
391
|
+
mode === 'multiple' ? ('checkbox' as const) : ('button' as const),
|
|
392
|
+
accessibilityState: {
|
|
393
|
+
selected: mode === 'single' ? selected : undefined,
|
|
394
|
+
checked: mode === 'multiple' ? selected : undefined,
|
|
395
|
+
disabled,
|
|
396
|
+
},
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
role: 'option' as const,
|
|
401
|
+
'aria-selected': selected,
|
|
402
|
+
'aria-disabled': disabled || undefined,
|
|
403
|
+
}
|
|
404
|
+
}
|
package/src/types.tsx
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import type { NativeValue, SizeTokens } from '@tamagui/core'
|
|
2
|
-
import type { YStackProps } from '@tamagui/stacks'
|
|
3
1
|
import type {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from '
|
|
2
|
+
NativeValue,
|
|
3
|
+
SizeTokens,
|
|
4
|
+
TamaguiChangeEventDetails,
|
|
5
|
+
TamaguiEventDetails,
|
|
6
|
+
ViewProps,
|
|
7
|
+
} from '@tamagui/core'
|
|
8
|
+
import type { DismissableProps } from '@tamagui/dismissable'
|
|
9
|
+
import type { HTMLProps, MutableRefObject, ReactNode, RefObject } from 'react'
|
|
10
|
+
import type {
|
|
11
|
+
SelectItemRegistry,
|
|
12
|
+
SelectMode,
|
|
13
|
+
SelectSelection,
|
|
14
|
+
} from './selectionController'
|
|
10
15
|
|
|
11
16
|
// minimal types replacing @floating-ui/react imports
|
|
12
17
|
type ContextData = Record<string, any>
|
|
@@ -30,27 +35,53 @@ export type SelectScopes = string
|
|
|
30
35
|
|
|
31
36
|
export type SelectScopedProps<P> = P & { scope?: SelectScopes }
|
|
32
37
|
|
|
33
|
-
export type
|
|
38
|
+
export type SelectValueForMode<
|
|
39
|
+
Value extends string = string,
|
|
40
|
+
Multiple extends boolean | undefined = false,
|
|
41
|
+
> = Multiple extends true
|
|
42
|
+
? Value[]
|
|
43
|
+
: Multiple extends false | undefined
|
|
44
|
+
? Value
|
|
45
|
+
: Value | Value[]
|
|
46
|
+
|
|
47
|
+
export type SelectValueChangeDetails = TamaguiChangeEventDetails<
|
|
48
|
+
'item-press' | 'keyboard' | 'native-change'
|
|
49
|
+
>
|
|
50
|
+
|
|
51
|
+
export type SelectOpenChangeDetails = TamaguiChangeEventDetails<
|
|
52
|
+
'trigger-press' | 'keyboard' | 'outside-press' | 'escape-key' | 'item-press'
|
|
53
|
+
>
|
|
54
|
+
|
|
55
|
+
export type SelectActiveChangeDetails = TamaguiEventDetails<
|
|
56
|
+
'item-hover' | 'list-navigation' | 'keyboard',
|
|
57
|
+
unknown,
|
|
58
|
+
{ index: number }
|
|
59
|
+
>
|
|
60
|
+
|
|
61
|
+
export type SelectImplProps = SelectScopedProps<SelectProps<string, boolean>> & {
|
|
34
62
|
activeIndexRef: any
|
|
35
63
|
selectedIndexRef: any
|
|
36
64
|
listContentRef: any
|
|
37
|
-
/** fast setter: updates ref + emits to subscribers (no re-render) - use for hover/navigation */
|
|
38
|
-
setActiveIndexFast: (index: number | null) => void
|
|
39
65
|
}
|
|
40
66
|
|
|
41
|
-
export interface SelectProps<
|
|
67
|
+
export interface SelectProps<
|
|
68
|
+
Value extends string = string,
|
|
69
|
+
Multiple extends boolean | undefined = false,
|
|
70
|
+
> {
|
|
42
71
|
id?: string
|
|
43
72
|
children?: ReactNode
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
73
|
+
multiple?: Multiple
|
|
74
|
+
value?: SelectValueForMode<Value, Multiple>
|
|
75
|
+
defaultValue?: SelectValueForMode<Value, Multiple>
|
|
76
|
+
onValueChange?(
|
|
77
|
+
value: SelectValueForMode<Value, Multiple>,
|
|
78
|
+
details: SelectValueChangeDetails
|
|
79
|
+
): void
|
|
47
80
|
open?: boolean
|
|
48
81
|
defaultOpen?: boolean
|
|
49
|
-
onOpenChange?(open: boolean): void
|
|
82
|
+
onOpenChange?(open: boolean, details: SelectOpenChangeDetails): void
|
|
50
83
|
dir?: SelectDirection
|
|
51
|
-
|
|
52
|
-
autoComplete?: string
|
|
53
|
-
size?: SizeTokens
|
|
84
|
+
size?: SizeTokens | true
|
|
54
85
|
/**
|
|
55
86
|
* If passed, will render a native component instead of the custom one. Currently only `web` is supported.
|
|
56
87
|
*/
|
|
@@ -64,7 +95,7 @@ export interface SelectProps<Value extends string = string> {
|
|
|
64
95
|
/**
|
|
65
96
|
* Called when an item is hovered by mouse or navigated to by keyboard.
|
|
66
97
|
*/
|
|
67
|
-
onActiveChange?(value: string,
|
|
98
|
+
onActiveChange?(value: string, details: SelectActiveChangeDetails): void
|
|
68
99
|
|
|
69
100
|
/**
|
|
70
101
|
* Render function for the selected value. Use this for SSR support.
|
|
@@ -79,7 +110,13 @@ export interface SelectProps<Value extends string = string> {
|
|
|
79
110
|
* >
|
|
80
111
|
* ```
|
|
81
112
|
*/
|
|
82
|
-
renderValue?(value: Value): ReactNode
|
|
113
|
+
renderValue?(value: SelectValueForMode<Value, Multiple>): ReactNode
|
|
114
|
+
|
|
115
|
+
/** web form field name. inert on react native. */
|
|
116
|
+
name?: string
|
|
117
|
+
|
|
118
|
+
/** associates the web form control with an external form. inert on react native. */
|
|
119
|
+
form?: string
|
|
83
120
|
|
|
84
121
|
/**
|
|
85
122
|
* When true, defers mounting Select items until opened using startTransition.
|
|
@@ -106,64 +143,69 @@ export interface SelectItemParentContextValue {
|
|
|
106
143
|
adaptScope: string
|
|
107
144
|
scopeName: string
|
|
108
145
|
id?: string
|
|
109
|
-
|
|
110
|
-
|
|
146
|
+
name?: string
|
|
147
|
+
form?: string
|
|
148
|
+
mode: SelectMode
|
|
149
|
+
selectedValues: string[]
|
|
150
|
+
registry: SelectItemRegistry
|
|
111
151
|
listRef?: MutableRefObject<Array<HTMLElement | null>>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
activeIndexSubscribe: EmitterSubscriber<number>
|
|
152
|
+
requestOpenChange: (open: boolean, details: SelectOpenChangeDetails) => void
|
|
153
|
+
selectValue: (value: string, details: SelectValueChangeDetails) => void
|
|
154
|
+
changeNativeValue: (value: SelectSelection, event: unknown) => void
|
|
155
|
+
activeIndexSubscribe: EmitterSubscriber<number | null>
|
|
116
156
|
activeIndexRef?: MutableRefObject<number | null>
|
|
117
|
-
valueSubscribe: EmitterSubscriber<any>
|
|
118
157
|
allowSelectRef?: MutableRefObject<boolean>
|
|
119
158
|
allowMouseUpRef?: MutableRefObject<boolean>
|
|
120
|
-
setValueAtIndex: (index: number, value: string) => void
|
|
121
159
|
selectTimeoutRef?: MutableRefObject<any>
|
|
122
160
|
dataRef?: MutableRefObject<ContextData>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
getFloatingProps: (userProps?: HTMLProps<HTMLElement> | undefined) => any
|
|
126
|
-
getItemProps: (userProps?: HTMLProps<HTMLElement> | undefined) => any
|
|
127
|
-
}
|
|
161
|
+
/** web only: stable once the list mounts, so it can live beside the items */
|
|
162
|
+
getItemProps?: (userProps?: HTMLProps<HTMLElement> | undefined) => any
|
|
128
163
|
shouldRenderWebNative?: boolean
|
|
129
|
-
size?: SizeTokens
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
setSelectedItem: (item: ReactNode) => void
|
|
164
|
+
size?: SizeTokens | true
|
|
165
|
+
setActiveIndex: (index: number | null, details?: SelectActiveChangeDetails) => void
|
|
166
|
+
selectedIndex: number
|
|
167
|
+
lastPointerRef: MutableRefObject<{ x: number; y: number }>
|
|
168
|
+
moveActive: (direction: 1 | -1, event?: unknown) => void
|
|
169
|
+
search: (text: string, event?: unknown) => void
|
|
136
170
|
}
|
|
137
171
|
|
|
138
172
|
export interface SelectContextValue {
|
|
173
|
+
/** web only: the trigger and viewport prop getters; change with the floating element */
|
|
174
|
+
interactions?: {
|
|
175
|
+
getReferenceProps: (userProps?: HTMLProps<Element> | undefined) => any
|
|
176
|
+
getFloatingProps: (userProps?: HTMLProps<HTMLElement> | undefined) => any
|
|
177
|
+
}
|
|
139
178
|
dir?: SelectDirection
|
|
140
179
|
scopeName: string
|
|
141
180
|
adaptScope: string
|
|
142
181
|
value: any
|
|
182
|
+
mode: SelectMode
|
|
183
|
+
selectedValues: string[]
|
|
143
184
|
selectedIndex: number
|
|
144
|
-
/**
|
|
145
|
-
activeIndex: number | null
|
|
146
|
-
/** ref to current active index - always up to date, use for reads */
|
|
185
|
+
/** the active index is never state: read the ref, or subscribe through the item parent context */
|
|
147
186
|
activeIndexRef: MutableRefObject<number | null>
|
|
148
|
-
|
|
149
|
-
setActiveIndex: (index: number | null) => void
|
|
187
|
+
setActiveIndex: (index: number | null, details?: SelectActiveChangeDetails) => void
|
|
150
188
|
open: boolean
|
|
151
189
|
valueNode: Element | null
|
|
152
190
|
onValueNodeChange(node: HTMLElement): void
|
|
153
|
-
forceUpdate: DispatchWithoutAction
|
|
154
|
-
// SheetImpl only:
|
|
155
|
-
isInSheet?: boolean
|
|
156
191
|
// InlineImpl only:
|
|
157
192
|
fallback: boolean
|
|
158
193
|
blockSelection: boolean
|
|
159
194
|
upArrowRef?: MutableRefObject<HTMLDivElement | null>
|
|
160
195
|
downArrowRef?: MutableRefObject<HTMLDivElement | null>
|
|
161
|
-
|
|
196
|
+
/** re-reads the viewport and flips canScrollUp/Down when they change */
|
|
197
|
+
updateScrollArrows?: () => void
|
|
162
198
|
setInnerOffset?: Function
|
|
163
199
|
controlledScrolling?: boolean
|
|
164
200
|
canScrollUp?: boolean
|
|
165
201
|
canScrollDown?: boolean
|
|
166
202
|
floatingContext?: FloatingContext<ReferenceType>
|
|
203
|
+
/** InlineImpl only: where floating-ui put the viewport */
|
|
204
|
+
floatingPosition?: {
|
|
205
|
+
position: 'absolute' | 'fixed'
|
|
206
|
+
top: number | ''
|
|
207
|
+
left: number | ''
|
|
208
|
+
}
|
|
167
209
|
native?: NativeValue
|
|
168
210
|
disablePreventBodyScroll?: boolean
|
|
169
211
|
/** update floating-ui to recalculate */
|
|
@@ -175,24 +217,28 @@ export interface SelectContextValue {
|
|
|
175
217
|
}
|
|
176
218
|
|
|
177
219
|
export type SelectViewportExtraProps = SelectScopedProps<{
|
|
178
|
-
size?: SizeTokens
|
|
220
|
+
size?: SizeTokens | true
|
|
179
221
|
disableScroll?: boolean
|
|
180
|
-
unstyled?: boolean
|
|
181
222
|
}>
|
|
182
223
|
|
|
183
|
-
export type SelectViewportProps =
|
|
224
|
+
export type SelectViewportProps = ViewProps & SelectViewportExtraProps
|
|
184
225
|
|
|
185
|
-
export type SelectContentProps = SelectScopedProps<
|
|
186
|
-
|
|
187
|
-
|
|
226
|
+
export type SelectContentProps = SelectScopedProps<
|
|
227
|
+
{
|
|
228
|
+
children?: React.ReactNode
|
|
229
|
+
} & Pick<
|
|
230
|
+
DismissableProps,
|
|
231
|
+
'onEscapeKeyDown' | 'onPointerDownOutside' | 'onFocusOutside' | 'onInteractOutside'
|
|
232
|
+
>
|
|
233
|
+
>
|
|
188
234
|
|
|
189
|
-
export type SelectScrollButtonImplProps =
|
|
235
|
+
export type SelectScrollButtonImplProps = ViewProps &
|
|
190
236
|
SelectScopedProps<{
|
|
191
237
|
dir: 'up' | 'down'
|
|
192
|
-
|
|
238
|
+
partClassName: string
|
|
193
239
|
}>
|
|
194
240
|
|
|
195
241
|
export interface SelectScrollButtonProps extends Omit<
|
|
196
242
|
SelectScrollButtonImplProps,
|
|
197
|
-
'dir' | '
|
|
243
|
+
'dir' | 'partClassName'
|
|
198
244
|
> {}
|