@tamagui/select 3.0.0-beta.924.1 → 3.0.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 +23 -25
- package/dist/cjs/Select.native.cjs +27 -26
- package/dist/cjs/Select.native.js +27 -26
- package/dist/cjs/Select.native.js.map +1 -1
- package/dist/cjs/SelectImpl.cjs +67 -63
- package/dist/cjs/SelectItem.cjs +49 -60
- package/dist/cjs/SelectItem.native.cjs +51 -79
- package/dist/cjs/SelectItem.native.js +51 -79
- package/dist/cjs/SelectItem.native.js.map +1 -1
- package/dist/cjs/SelectScrollButton.cjs +9 -25
- package/dist/cjs/SelectTrigger.cjs +22 -5
- package/dist/cjs/SelectTrigger.native.cjs +15 -5
- package/dist/cjs/SelectTrigger.native.js +15 -5
- package/dist/cjs/SelectTrigger.native.js.map +1 -1
- package/dist/cjs/SelectViewport.cjs +29 -32
- package/dist/cjs/SelectViewport.native.cjs +2 -2
- package/dist/cjs/SelectViewport.native.js +2 -2
- package/dist/cjs/SelectViewport.native.js.map +1 -1
- package/dist/cjs/selectionController.cjs +15 -5
- package/dist/cjs/selectionController.native.cjs +20 -15
- package/dist/cjs/selectionController.native.js +20 -15
- package/dist/cjs/selectionController.native.js.map +1 -1
- package/dist/esm/Select.mjs +27 -28
- package/dist/esm/Select.mjs.map +1 -1
- package/dist/esm/Select.native.js +31 -29
- package/dist/esm/Select.native.js.map +1 -1
- package/dist/esm/SelectImpl.mjs +68 -64
- package/dist/esm/SelectImpl.mjs.map +1 -1
- package/dist/esm/SelectItem.mjs +50 -61
- package/dist/esm/SelectItem.mjs.map +1 -1
- package/dist/esm/SelectItem.native.js +52 -80
- package/dist/esm/SelectItem.native.js.map +1 -1
- package/dist/esm/SelectScrollButton.mjs +10 -26
- package/dist/esm/SelectScrollButton.mjs.map +1 -1
- package/dist/esm/SelectTrigger.mjs +16 -5
- package/dist/esm/SelectTrigger.mjs.map +1 -1
- package/dist/esm/SelectTrigger.native.js +15 -4
- package/dist/esm/SelectTrigger.native.js.map +1 -1
- package/dist/esm/SelectViewport.mjs +30 -33
- package/dist/esm/SelectViewport.mjs.map +1 -1
- package/dist/esm/SelectViewport.native.js +3 -3
- package/dist/esm/SelectViewport.native.js.map +1 -1
- package/dist/esm/selectionController.mjs +15 -5
- package/dist/esm/selectionController.mjs.map +1 -1
- package/dist/esm/selectionController.native.js +20 -15
- package/dist/esm/selectionController.native.js.map +1 -1
- package/dist/jsx/Select.mjs +27 -28
- package/dist/jsx/Select.mjs.map +1 -1
- package/dist/jsx/Select.native.cjs +27 -26
- package/dist/jsx/Select.native.js +27 -26
- package/dist/jsx/Select.native.js.map +1 -1
- package/dist/jsx/SelectImpl.mjs +68 -64
- package/dist/jsx/SelectImpl.mjs.map +1 -1
- package/dist/jsx/SelectItem.mjs +50 -61
- package/dist/jsx/SelectItem.mjs.map +1 -1
- package/dist/jsx/SelectItem.native.cjs +51 -79
- package/dist/jsx/SelectItem.native.js +51 -79
- package/dist/jsx/SelectItem.native.js.map +1 -1
- package/dist/jsx/SelectScrollButton.mjs +10 -26
- package/dist/jsx/SelectScrollButton.mjs.map +1 -1
- package/dist/jsx/SelectTrigger.mjs +16 -5
- package/dist/jsx/SelectTrigger.mjs.map +1 -1
- package/dist/jsx/SelectTrigger.native.cjs +15 -5
- package/dist/jsx/SelectTrigger.native.js +15 -5
- package/dist/jsx/SelectTrigger.native.js.map +1 -1
- package/dist/jsx/SelectViewport.mjs +30 -33
- package/dist/jsx/SelectViewport.mjs.map +1 -1
- package/dist/jsx/SelectViewport.native.cjs +2 -2
- package/dist/jsx/SelectViewport.native.js +2 -2
- package/dist/jsx/SelectViewport.native.js.map +1 -1
- package/dist/jsx/selectionController.mjs +15 -5
- package/dist/jsx/selectionController.mjs.map +1 -1
- package/dist/jsx/selectionController.native.cjs +20 -15
- package/dist/jsx/selectionController.native.js +20 -15
- package/dist/jsx/selectionController.native.js.map +1 -1
- package/package.json +30 -29
- package/src/Select.tsx +41 -41
- package/src/SelectImpl.tsx +107 -95
- package/src/SelectItem.tsx +66 -91
- package/src/SelectScrollButton.tsx +14 -32
- package/src/SelectTrigger.tsx +19 -2
- package/src/SelectViewport.native.tsx +4 -4
- package/src/SelectViewport.tsx +34 -30
- package/src/selectionController.ts +20 -5
- package/src/types.tsx +26 -22
- package/types/Select.d.ts +42 -81
- package/types/Select.d.ts.map +1 -1
- package/types/SelectImpl.d.ts.map +1 -1
- package/types/SelectItem.d.ts +3 -5
- package/types/SelectItem.d.ts.map +1 -1
- package/types/SelectItemText.d.ts +3 -5
- package/types/SelectItemText.d.ts.map +1 -1
- package/types/SelectScrollButton.d.ts +5 -9
- package/types/SelectScrollButton.d.ts.map +1 -1
- package/types/SelectScrollButton.native.d.ts +5 -9
- package/types/SelectScrollButton.native.d.ts.map +1 -1
- package/types/SelectTrigger.d.ts +3 -5
- package/types/SelectTrigger.d.ts.map +1 -1
- package/types/SelectViewport.d.ts +4 -9
- package/types/SelectViewport.d.ts.map +1 -1
- package/types/SelectViewport.native.d.ts +4 -9
- package/types/SelectViewport.native.d.ts.map +1 -1
- package/types/selectionController.d.ts +2 -0
- package/types/selectionController.d.ts.map +1 -1
- package/types/types.d.ts +30 -22
- package/types/types.d.ts.map +1 -1
package/src/SelectImpl.tsx
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
autoUpdate,
|
|
3
2
|
inner,
|
|
4
3
|
offset,
|
|
5
4
|
size,
|
|
6
5
|
useClick,
|
|
7
6
|
useFloatingRaw as useFloatingDom,
|
|
8
7
|
useInteractions,
|
|
9
|
-
useInnerOffset,
|
|
10
8
|
useListNavigation,
|
|
11
9
|
useRole,
|
|
12
10
|
useTypeahead,
|
|
@@ -17,7 +15,6 @@ import { useIsomorphicLayoutEffect } from '@tamagui/constants'
|
|
|
17
15
|
import { createChangeEventDetails, useEvent, useIsTouchDevice } from '@tamagui/core'
|
|
18
16
|
import { composeEventHandlers } from '@tamagui/helpers'
|
|
19
17
|
import * as React from 'react'
|
|
20
|
-
import { flushSync } from 'react-dom'
|
|
21
18
|
import { SCROLL_ARROW_THRESHOLD } from './constants'
|
|
22
19
|
import {
|
|
23
20
|
SelectItemParentProvider,
|
|
@@ -38,9 +35,8 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
|
|
|
38
35
|
const selectItemParentContext = useSelectItemParentContext(scope)
|
|
39
36
|
const { setActiveIndex, selectedIndex, activeIndexRef } = selectContext
|
|
40
37
|
|
|
41
|
-
const { requestOpenChange, registry } = selectItemParentContext
|
|
38
|
+
const { requestOpenChange, registry, lastPointerRef } = selectItemParentContext
|
|
42
39
|
|
|
43
|
-
const [scrollTop, setScrollTop] = React.useState(0)
|
|
44
40
|
const touch = useIsTouchDevice()
|
|
45
41
|
|
|
46
42
|
const listItemsRef = selectItemParentContext.listRef!
|
|
@@ -58,14 +54,12 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
|
|
|
58
54
|
|
|
59
55
|
const [controlledScrolling, setControlledScrolling] = React.useState(false)
|
|
60
56
|
const [fallback, setFallback] = React.useState(false)
|
|
61
|
-
const [innerOffset, setInnerOffset] = React.useState(0)
|
|
62
57
|
const [blockSelection, setBlockSelection] = React.useState(false)
|
|
63
|
-
const floatingStyle = React.useRef({})
|
|
64
58
|
|
|
65
|
-
// sync activeIndex on open/close
|
|
66
59
|
React.useEffect(() => {
|
|
67
60
|
if (!open) {
|
|
68
|
-
|
|
61
|
+
setCanScrollUp(false)
|
|
62
|
+
setCanScrollDown(false)
|
|
69
63
|
setFallback(false)
|
|
70
64
|
setControlledScrolling(false)
|
|
71
65
|
}
|
|
@@ -100,58 +94,62 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
|
|
|
100
94
|
} = useFloatingDom({
|
|
101
95
|
open,
|
|
102
96
|
placement: 'bottom-start',
|
|
103
|
-
whileElementsMounted: autoUpdate,
|
|
104
97
|
// The removed alternate stack was the logic from floating-ui,
|
|
105
98
|
// but it causes issues (open, drag select, close, then re-open its not positioned "over")
|
|
106
99
|
// https://github.com/floating-ui/floating-ui/blob/master/packages/react/test/visual/components/MacSelect.tsx
|
|
107
100
|
middleware: [
|
|
108
101
|
size({
|
|
109
|
-
apply({
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
},
|
|
113
|
-
}) {
|
|
114
|
-
Object.assign(floatingStyle.current, {
|
|
115
|
-
minWidth: width + 8,
|
|
116
|
-
})
|
|
117
|
-
if (refs.floating.current) {
|
|
118
|
-
Object.assign(refs.floating.current.style, floatingStyle.current)
|
|
119
|
-
}
|
|
102
|
+
apply({ rects, elements }) {
|
|
103
|
+
// the list overhangs the trigger by 4px a side (with the -4 cross offset)
|
|
104
|
+
elements.floating.style.minWidth = `${rects.reference.width + 8}px`
|
|
120
105
|
},
|
|
121
106
|
}),
|
|
122
107
|
inner({
|
|
123
108
|
listRef: listItemsRef,
|
|
124
109
|
overflowRef,
|
|
125
110
|
index: selectedIndex,
|
|
126
|
-
offset: innerOffset,
|
|
127
111
|
onFallbackChange: setFallback,
|
|
128
|
-
|
|
112
|
+
// arrows sit outside the scrollport, with room reserved at both screen edges
|
|
113
|
+
get padding() {
|
|
114
|
+
return (
|
|
115
|
+
10 +
|
|
116
|
+
Math.max(
|
|
117
|
+
upArrowRef.current?.offsetHeight ?? 0,
|
|
118
|
+
downArrowRef.current?.offsetHeight ?? 0
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
},
|
|
129
122
|
minItemsVisible: touch ? 10 : 4,
|
|
130
123
|
referenceOverflowThreshold: 20,
|
|
131
124
|
}),
|
|
132
|
-
offset({ crossAxis: -
|
|
125
|
+
offset({ crossAxis: -4 }),
|
|
133
126
|
],
|
|
134
127
|
} as any)
|
|
135
128
|
|
|
136
|
-
|
|
129
|
+
// the arrows are booleans that flip at a threshold, never the scroll offset
|
|
130
|
+
// itself, so a scroll event costs no render unless an arrow appears or goes
|
|
131
|
+
const [canScrollUp, setCanScrollUp] = React.useState(false)
|
|
132
|
+
const [canScrollDown, setCanScrollDown] = React.useState(false)
|
|
133
|
+
const updateScrollArrows = React.useCallback(() => {
|
|
134
|
+
const el = refs.floating.current
|
|
135
|
+
if (!el) return
|
|
136
|
+
setCanScrollUp(el.scrollTop > SCROLL_ARROW_THRESHOLD)
|
|
137
|
+
setCanScrollDown(
|
|
138
|
+
el.scrollTop < el.scrollHeight - el.clientHeight - SCROLL_ARROW_THRESHOLD
|
|
139
|
+
)
|
|
140
|
+
}, [refs])
|
|
137
141
|
|
|
138
|
-
const showUpArrow = open &&
|
|
139
|
-
const showDownArrow =
|
|
140
|
-
open &&
|
|
141
|
-
floatingRef.current &&
|
|
142
|
-
scrollTop <
|
|
143
|
-
floatingRef.current.scrollHeight -
|
|
144
|
-
floatingRef.current.clientHeight -
|
|
145
|
-
SCROLL_ARROW_THRESHOLD
|
|
146
|
-
|
|
147
|
-
const isScrollable = showDownArrow || showUpArrow
|
|
142
|
+
const showUpArrow = open && canScrollUp
|
|
143
|
+
const showDownArrow = open && canScrollDown
|
|
148
144
|
|
|
145
|
+
// the list is placed once per open, not kept in sync: the inner middleware
|
|
146
|
+
// scrolls the list to align the selected item, so recomputing on a page
|
|
147
|
+
// scroll would yank the list out from under the user's own scroll. the list
|
|
148
|
+
// is absolutely positioned, so it rides along with page scroll on its own.
|
|
149
149
|
useIsomorphicLayoutEffect(() => {
|
|
150
|
-
if (
|
|
150
|
+
if (!open) return
|
|
151
|
+
update()
|
|
151
152
|
window.addEventListener('resize', update)
|
|
152
|
-
if (open) {
|
|
153
|
-
update()
|
|
154
|
-
}
|
|
155
153
|
return () => window.removeEventListener('resize', update)
|
|
156
154
|
}, [update, open])
|
|
157
155
|
|
|
@@ -165,47 +163,51 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
|
|
|
165
163
|
} as SelectActiveChangeDetails)
|
|
166
164
|
})
|
|
167
165
|
|
|
168
|
-
//
|
|
166
|
+
// the interaction context for the floating hooks. every hook memoizes its
|
|
167
|
+
// props on this object, and those props sit in the item context, so a new
|
|
168
|
+
// identity here re-renders every item
|
|
169
169
|
const dataRef = React.useRef<{ openEvent?: Event; placement?: string }>({})
|
|
170
170
|
dataRef.current.placement = computedPlacement
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
171
|
+
const onOpenChange = useEvent((nextOpen: boolean, event?: Event, reason?: string) => {
|
|
172
|
+
requestOpenChange(
|
|
173
|
+
nextOpen,
|
|
174
|
+
createChangeEventDetails(
|
|
175
|
+
reason === 'list-navigation' ? 'keyboard' : 'trigger-press',
|
|
176
|
+
event,
|
|
177
|
+
refs.reference.current
|
|
178
|
+
) as SelectOpenChangeDetails
|
|
179
|
+
)
|
|
180
|
+
})
|
|
181
|
+
const referenceElement = (refs.reference.current as Element | null) || null
|
|
182
|
+
const floatingElement = refs.floating.current || null
|
|
183
|
+
const interactionContext: FloatingInteractionContext = React.useMemo(
|
|
184
|
+
() => ({
|
|
185
|
+
open,
|
|
186
|
+
onOpenChange,
|
|
187
|
+
refs: {
|
|
188
|
+
reference: refs.reference as any,
|
|
189
|
+
floating: refs.floating,
|
|
190
|
+
domReference: refs.reference as any,
|
|
191
|
+
},
|
|
192
|
+
elements: {
|
|
193
|
+
reference: referenceElement,
|
|
194
|
+
floating: floatingElement,
|
|
195
|
+
domReference: referenceElement,
|
|
196
|
+
},
|
|
197
|
+
dataRef,
|
|
198
|
+
}),
|
|
199
|
+
[open, onOpenChange, refs, referenceElement, floatingElement]
|
|
200
|
+
)
|
|
195
201
|
|
|
196
202
|
const interactionsProps = [
|
|
197
203
|
useClick(interactionContext, { event: 'mousedown', keyboardHandlers: false }),
|
|
198
204
|
// useDismiss removed - already handled by Dismissable in SelectContent
|
|
199
205
|
useRole(interactionContext, { role: 'listbox' }),
|
|
200
|
-
useInnerOffset(interactionContext, {
|
|
201
|
-
enabled: !fallback && isScrollable,
|
|
202
|
-
onChange: setInnerOffset,
|
|
203
|
-
overflowRef,
|
|
204
|
-
scrollRef: refs.floating,
|
|
205
|
-
}),
|
|
206
206
|
useListNavigation(interactionContext, {
|
|
207
207
|
listRef: listItemsRef,
|
|
208
|
-
|
|
208
|
+
// items focus themselves from the active-index emitter, and the hook's
|
|
209
|
+
// own index syncs from item focus, so it never needs the value
|
|
210
|
+
activeIndex: null,
|
|
209
211
|
selectedIndex,
|
|
210
212
|
onNavigate: (index) => {
|
|
211
213
|
if (index !== null) {
|
|
@@ -219,12 +221,15 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
|
|
|
219
221
|
},
|
|
220
222
|
disabledIndices: registry.getDisabledIndices(),
|
|
221
223
|
scrollItemIntoView: false,
|
|
224
|
+
// items own hover: the hook's leave handler would focus the viewport when
|
|
225
|
+
// a scroll moves the list under a still pointer, losing the keyboard position
|
|
226
|
+
focusItemOnHover: false,
|
|
222
227
|
}),
|
|
223
228
|
useTypeahead(interactionContext, {
|
|
224
229
|
listRef: listContentRef,
|
|
225
230
|
onMatch,
|
|
226
231
|
selectedIndex,
|
|
227
|
-
activeIndex:
|
|
232
|
+
activeIndex: null,
|
|
228
233
|
onTypingChange: (e) => {
|
|
229
234
|
state.current.isTyping = e
|
|
230
235
|
},
|
|
@@ -237,12 +242,23 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
|
|
|
237
242
|
}, interactionsProps)
|
|
238
243
|
)
|
|
239
244
|
|
|
245
|
+
// the trigger and viewport getters. items get getItemProps alone, which is
|
|
246
|
+
// stable once the list mounts, so the floating element mounting (a new
|
|
247
|
+
// identity here) never re-renders the list
|
|
240
248
|
const interactionsContext = React.useMemo(() => {
|
|
241
249
|
return {
|
|
242
|
-
...interactions,
|
|
243
250
|
getReferenceProps(props: Record<string, any> = {}) {
|
|
244
251
|
return interactions.getReferenceProps({
|
|
245
252
|
...props,
|
|
253
|
+
// the press that opens is the pointer's last real position: WebKit
|
|
254
|
+
// replays a mousemove there whenever the list scrolls under it (say a
|
|
255
|
+
// keyboard move scrolling the next item into view), and that replay
|
|
256
|
+
// must not read as a hover
|
|
257
|
+
onMouseDown: composeEventHandlers(props.onMouseDown, (event: any) => {
|
|
258
|
+
const last = lastPointerRef.current
|
|
259
|
+
last.x = event.clientX
|
|
260
|
+
last.y = event.clientY
|
|
261
|
+
}),
|
|
246
262
|
onKeyDown: composeEventHandlers(props.onKeyDown, (event: any) => {
|
|
247
263
|
latestKeyboardEventRef.current = event.nativeEvent || event
|
|
248
264
|
if (
|
|
@@ -266,17 +282,7 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
|
|
|
266
282
|
getFloatingProps(props) {
|
|
267
283
|
return interactions.getFloatingProps({
|
|
268
284
|
ref: refs.floating,
|
|
269
|
-
className: 'Select',
|
|
270
285
|
...props,
|
|
271
|
-
style: {
|
|
272
|
-
position: strategy,
|
|
273
|
-
top: y ?? '',
|
|
274
|
-
left: x ?? '',
|
|
275
|
-
outline: 0,
|
|
276
|
-
scrollbarWidth: 'none',
|
|
277
|
-
...floatingStyle.current,
|
|
278
|
-
...props?.style,
|
|
279
|
-
},
|
|
280
286
|
onPointerEnter() {
|
|
281
287
|
setControlledScrolling(false)
|
|
282
288
|
state.current.isMouseOutside = false
|
|
@@ -295,15 +301,16 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
|
|
|
295
301
|
onContextMenu(e) {
|
|
296
302
|
e.preventDefault()
|
|
297
303
|
},
|
|
298
|
-
onScroll
|
|
299
|
-
flushSync(() => {
|
|
300
|
-
setScrollTop(event.currentTarget.scrollTop)
|
|
301
|
-
})
|
|
302
|
-
},
|
|
304
|
+
onScroll: updateScrollArrows,
|
|
303
305
|
})
|
|
304
306
|
},
|
|
305
307
|
}
|
|
306
|
-
}, [refs
|
|
308
|
+
}, [refs, interactions, requestOpenChange, updateScrollArrows])
|
|
309
|
+
|
|
310
|
+
const floatingPosition = React.useMemo(
|
|
311
|
+
() => ({ position: strategy, top: y ?? '', left: x ?? '' }),
|
|
312
|
+
[strategy, x, y]
|
|
313
|
+
)
|
|
307
314
|
|
|
308
315
|
// effects
|
|
309
316
|
|
|
@@ -322,7 +329,6 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
|
|
|
322
329
|
}
|
|
323
330
|
allowSelectRef.current = false
|
|
324
331
|
allowMouseUpRef.current = true
|
|
325
|
-
setInnerOffset(0)
|
|
326
332
|
setFallback(false)
|
|
327
333
|
setBlockSelection(false)
|
|
328
334
|
}, [open])
|
|
@@ -341,13 +347,18 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
|
|
|
341
347
|
if (controlledScrolling && index != null) {
|
|
342
348
|
listItemsRef.current[index]?.scrollIntoView({ block: 'nearest' })
|
|
343
349
|
}
|
|
344
|
-
|
|
350
|
+
updateScrollArrows()
|
|
345
351
|
}
|
|
346
352
|
|
|
347
353
|
scrollActiveIntoView(activeIndexRef.current)
|
|
348
354
|
|
|
349
355
|
return selectItemParentContext.activeIndexSubscribe(scrollActiveIntoView)
|
|
350
|
-
}, [
|
|
356
|
+
}, [
|
|
357
|
+
open,
|
|
358
|
+
controlledScrolling,
|
|
359
|
+
selectItemParentContext.activeIndexSubscribe,
|
|
360
|
+
updateScrollArrows,
|
|
361
|
+
])
|
|
351
362
|
|
|
352
363
|
React.useEffect(() => {
|
|
353
364
|
if (open && fallback) {
|
|
@@ -376,10 +387,11 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
|
|
|
376
387
|
<SelectProvider
|
|
377
388
|
scope={scope}
|
|
378
389
|
{...(selectContext as Required<typeof selectContext>)}
|
|
379
|
-
|
|
380
|
-
setInnerOffset={setInnerOffset}
|
|
390
|
+
updateScrollArrows={updateScrollArrows}
|
|
381
391
|
fallback={fallback}
|
|
382
392
|
floatingContext={floatingContext as any}
|
|
393
|
+
interactions={interactionsContext}
|
|
394
|
+
floatingPosition={floatingPosition}
|
|
383
395
|
canScrollDown={!!showDownArrow}
|
|
384
396
|
canScrollUp={!!showUpArrow}
|
|
385
397
|
controlledScrolling={controlledScrolling}
|
|
@@ -394,7 +406,7 @@ export const SelectInlineImpl = (props: SelectImplProps) => {
|
|
|
394
406
|
allowMouseUpRef={allowMouseUpRef}
|
|
395
407
|
allowSelectRef={allowSelectRef}
|
|
396
408
|
dataRef={dataRef as any}
|
|
397
|
-
|
|
409
|
+
getItemProps={interactions.getItemProps}
|
|
398
410
|
listRef={listItemsRef}
|
|
399
411
|
selectTimeoutRef={selectTimeoutRef}
|
|
400
412
|
>
|
package/src/SelectItem.tsx
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import type { GetProps } from '@tamagui/core'
|
|
12
12
|
import { composeEventHandlers } from '@tamagui/helpers'
|
|
13
13
|
import * as React from 'react'
|
|
14
|
-
import {
|
|
14
|
+
import { useSelectItemParentContext } from './context'
|
|
15
15
|
import { getSelectOptionProps } from './selectionController'
|
|
16
16
|
import type {
|
|
17
17
|
SelectActiveChangeDetails,
|
|
@@ -67,12 +67,12 @@ export const SelectItem = createStyledHOC(
|
|
|
67
67
|
} = props
|
|
68
68
|
const disabled = disabledProp ?? ariaDisabled === true
|
|
69
69
|
|
|
70
|
+
// items read only the item parent context: the select context carries
|
|
71
|
+
// viewport state (position, scroll arrows) that must not re-render the list
|
|
70
72
|
const context = useSelectItemParentContext(scope)
|
|
71
|
-
const
|
|
72
|
-
const isAdapted = useAdaptIsActive(selectContext.adaptScope)
|
|
73
|
+
const isAdapted = useAdaptIsActive(context.adaptScope)
|
|
73
74
|
|
|
74
75
|
const {
|
|
75
|
-
listRef,
|
|
76
76
|
registry,
|
|
77
77
|
mode,
|
|
78
78
|
selectedValues,
|
|
@@ -82,9 +82,10 @@ export const SelectItem = createStyledHOC(
|
|
|
82
82
|
allowMouseUpRef,
|
|
83
83
|
allowSelectRef,
|
|
84
84
|
selectTimeoutRef,
|
|
85
|
-
|
|
85
|
+
getItemProps,
|
|
86
86
|
shouldRenderWebNative,
|
|
87
|
-
|
|
87
|
+
setActiveIndex,
|
|
88
|
+
lastPointerRef,
|
|
88
89
|
moveActive,
|
|
89
90
|
search,
|
|
90
91
|
} = context
|
|
@@ -92,7 +93,6 @@ export const SelectItem = createStyledHOC(
|
|
|
92
93
|
const isSelected = selectedValues.includes(value)
|
|
93
94
|
const pendingMouseUpSelectionRef = React.useRef(false)
|
|
94
95
|
const itemNodeRef = React.useRef<any>(null)
|
|
95
|
-
const [, rerenderRegistry] = React.useReducer((version) => version + 1, 0)
|
|
96
96
|
const registrationRef = React.useRef<ReturnType<typeof registry.registerItem> | null>(
|
|
97
97
|
null
|
|
98
98
|
)
|
|
@@ -102,38 +102,13 @@ export const SelectItem = createStyledHOC(
|
|
|
102
102
|
textValue: textValueProp,
|
|
103
103
|
})
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
[registry]
|
|
108
|
-
)
|
|
109
|
-
|
|
105
|
+
// the item never subscribes to the registry: its index is read at event
|
|
106
|
+
// time, and the owner mirrors the registry's nodes into the list ref
|
|
110
107
|
useIsomorphicLayoutEffect(() => {
|
|
111
108
|
const registration = registry.registerItem(initialRegistration.current)
|
|
112
109
|
registrationRef.current = registration
|
|
113
|
-
|
|
114
|
-
registration.setNode(registeredNode)
|
|
115
|
-
const registeredIndex = registry
|
|
116
|
-
.getItems()
|
|
117
|
-
.findIndex((item) => item.id === registration.id)
|
|
118
|
-
if (
|
|
119
|
-
isWeb &&
|
|
120
|
-
listRef &&
|
|
121
|
-
registeredIndex >= 0 &&
|
|
122
|
-
registeredNode instanceof HTMLElement
|
|
123
|
-
) {
|
|
124
|
-
listRef.current[registeredIndex] = registeredNode
|
|
125
|
-
}
|
|
110
|
+
registration.setNode(itemNodeRef.current)
|
|
126
111
|
return () => {
|
|
127
|
-
const currentIndex = registry
|
|
128
|
-
.getItems()
|
|
129
|
-
.findIndex((item) => item.id === registration.id)
|
|
130
|
-
if (
|
|
131
|
-
listRef &&
|
|
132
|
-
currentIndex >= 0 &&
|
|
133
|
-
listRef.current[currentIndex] === registeredNode
|
|
134
|
-
) {
|
|
135
|
-
listRef.current[currentIndex] = null
|
|
136
|
-
}
|
|
137
112
|
registrationRef.current = null
|
|
138
113
|
registration.unregister()
|
|
139
114
|
}
|
|
@@ -147,43 +122,41 @@ export const SelectItem = createStyledHOC(
|
|
|
147
122
|
registrationRef.current?.setNode(itemNodeRef.current)
|
|
148
123
|
})
|
|
149
124
|
|
|
150
|
-
|
|
125
|
+
// the only per-item state: this item is the active one. the active index
|
|
126
|
+
// itself is a ref plus an emitter, so a hover re-renders two items, not the list
|
|
127
|
+
const [isActive, setIsActive] = React.useState(false)
|
|
128
|
+
// every item hears every change; only the two whose state flips may set
|
|
129
|
+
// it, since a same-value set still renders once when updates are queued
|
|
130
|
+
const isActiveRef = React.useRef(false)
|
|
151
131
|
|
|
152
132
|
React.useEffect(() => {
|
|
153
|
-
const handleActiveIndex = (i: number) => {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
133
|
+
const handleActiveIndex = (i: number | null) => {
|
|
134
|
+
const next = i != null && registry.getIndex(value) === i
|
|
135
|
+
if (next !== isActiveRef.current) {
|
|
136
|
+
isActiveRef.current = next
|
|
137
|
+
setIsActive(next)
|
|
138
|
+
}
|
|
139
|
+
if (next && isWeb) {
|
|
140
|
+
// use rAF to focus after browser's click handling completes
|
|
141
|
+
// this prevents the trigger from stealing focus after we set it
|
|
142
|
+
requestAnimationFrame(() => {
|
|
143
|
+
itemNodeRef.current?.focus?.()
|
|
144
|
+
})
|
|
162
145
|
}
|
|
163
146
|
}
|
|
164
147
|
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
if (currentActiveIndex !== null && currentActiveIndex !== undefined) {
|
|
168
|
-
handleActiveIndex(currentActiveIndex)
|
|
169
|
-
}
|
|
148
|
+
// the parent effect may have set it before we subscribed
|
|
149
|
+
handleActiveIndex(activeIndexRef?.current ?? null)
|
|
170
150
|
|
|
171
151
|
return activeIndexSubscribe(handleActiveIndex)
|
|
172
|
-
}, [activeIndexRef, activeIndexSubscribe,
|
|
152
|
+
}, [activeIndexRef, activeIndexSubscribe, registry, value])
|
|
173
153
|
|
|
174
154
|
const textId = React.useId()
|
|
175
155
|
|
|
176
|
-
const refCallback = React.useCallback(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (!isWeb) return
|
|
181
|
-
if (listRef && index >= 0) {
|
|
182
|
-
listRef.current[index] = node instanceof HTMLElement ? node : null
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
[index, listRef]
|
|
186
|
-
)
|
|
156
|
+
const refCallback = React.useCallback((node) => {
|
|
157
|
+
itemNodeRef.current = node
|
|
158
|
+
registrationRef.current?.setNode(node)
|
|
159
|
+
}, [])
|
|
187
160
|
|
|
188
161
|
const composedRefs = useComposedRefs(forwardedRef, refCallback)
|
|
189
162
|
|
|
@@ -203,6 +176,25 @@ export const SelectItem = createStyledHOC(
|
|
|
203
176
|
[disabled, selectValue, value]
|
|
204
177
|
)
|
|
205
178
|
|
|
179
|
+
const handleMouseMove = React.useCallback(
|
|
180
|
+
(event: any) => {
|
|
181
|
+
if (disabled) return
|
|
182
|
+
const index = registry.getIndex(value)
|
|
183
|
+
if (index < 0) return
|
|
184
|
+
const last = lastPointerRef.current
|
|
185
|
+
if (event.clientX === last.x && event.clientY === last.y) return
|
|
186
|
+
last.x = event.clientX
|
|
187
|
+
last.y = event.clientY
|
|
188
|
+
setActiveIndex(index, {
|
|
189
|
+
reason: 'item-hover',
|
|
190
|
+
event: event.nativeEvent || event,
|
|
191
|
+
trigger: event.currentTarget,
|
|
192
|
+
index,
|
|
193
|
+
} as SelectActiveChangeDetails)
|
|
194
|
+
},
|
|
195
|
+
[disabled, lastPointerRef, registry, setActiveIndex, value]
|
|
196
|
+
)
|
|
197
|
+
|
|
206
198
|
const handleKeyDown = React.useCallback(
|
|
207
199
|
(event: any) => {
|
|
208
200
|
if (disabled) return
|
|
@@ -218,7 +210,7 @@ export const SelectItem = createStyledHOC(
|
|
|
218
210
|
return
|
|
219
211
|
}
|
|
220
212
|
if (
|
|
221
|
-
!
|
|
213
|
+
!getItemProps &&
|
|
222
214
|
event.key?.length === 1 &&
|
|
223
215
|
!event.metaKey &&
|
|
224
216
|
!event.ctrlKey
|
|
@@ -229,11 +221,11 @@ export const SelectItem = createStyledHOC(
|
|
|
229
221
|
allowSelectRef.current = true
|
|
230
222
|
}
|
|
231
223
|
},
|
|
232
|
-
[allowSelectRef, disabled, handleSelect,
|
|
224
|
+
[allowSelectRef, disabled, handleSelect, getItemProps, moveActive, search]
|
|
233
225
|
)
|
|
234
226
|
|
|
235
227
|
const selectItemProps = React.useMemo(() => {
|
|
236
|
-
if (
|
|
228
|
+
if (getItemProps) {
|
|
237
229
|
const {
|
|
238
230
|
onTouchMove,
|
|
239
231
|
onTouchEnd,
|
|
@@ -244,7 +236,7 @@ export const SelectItem = createStyledHOC(
|
|
|
244
236
|
onPress,
|
|
245
237
|
...itemProps
|
|
246
238
|
} = restProps
|
|
247
|
-
const interactionProps =
|
|
239
|
+
const interactionProps = getItemProps({
|
|
248
240
|
...itemProps,
|
|
249
241
|
onTouchMove() {
|
|
250
242
|
allowSelectRef!.current = true
|
|
@@ -254,15 +246,7 @@ export const SelectItem = createStyledHOC(
|
|
|
254
246
|
allowSelectRef!.current = false
|
|
255
247
|
allowMouseUpRef!.current = true
|
|
256
248
|
},
|
|
257
|
-
onMouseMove
|
|
258
|
-
if (disabled || index < 0) return
|
|
259
|
-
setActiveIndexFast?.(index, {
|
|
260
|
-
reason: 'item-hover',
|
|
261
|
-
event: event.nativeEvent || event,
|
|
262
|
-
trigger: event.currentTarget,
|
|
263
|
-
index,
|
|
264
|
-
} as SelectActiveChangeDetails)
|
|
265
|
-
},
|
|
249
|
+
onMouseMove: handleMouseMove,
|
|
266
250
|
onKeyDown: handleKeyDown,
|
|
267
251
|
onClick(event) {
|
|
268
252
|
if (disabled) return
|
|
@@ -332,15 +316,7 @@ export const SelectItem = createStyledHOC(
|
|
|
332
316
|
return {
|
|
333
317
|
...restProps,
|
|
334
318
|
onKeyDown: composeEventHandlers(restProps.onKeyDown as any, handleKeyDown),
|
|
335
|
-
onMouseMove: composeEventHandlers(restProps.onMouseMove as any,
|
|
336
|
-
if (disabled || index < 0) return
|
|
337
|
-
setActiveIndexFast?.(index, {
|
|
338
|
-
reason: 'item-hover',
|
|
339
|
-
event: event.nativeEvent || event,
|
|
340
|
-
trigger: event.currentTarget,
|
|
341
|
-
index,
|
|
342
|
-
} as SelectActiveChangeDetails)
|
|
343
|
-
}),
|
|
319
|
+
onMouseMove: composeEventHandlers(restProps.onMouseMove as any, handleMouseMove),
|
|
344
320
|
onPress: composeEventHandlers(restProps.onPress as any, handleSelect),
|
|
345
321
|
}
|
|
346
322
|
}, [
|
|
@@ -348,12 +324,11 @@ export const SelectItem = createStyledHOC(
|
|
|
348
324
|
allowSelectRef,
|
|
349
325
|
disabled,
|
|
350
326
|
handleKeyDown,
|
|
327
|
+
handleMouseMove,
|
|
351
328
|
handleSelect,
|
|
352
|
-
|
|
353
|
-
interactions,
|
|
329
|
+
getItemProps,
|
|
354
330
|
restProps,
|
|
355
331
|
selectTimeoutRef,
|
|
356
|
-
setActiveIndexFast,
|
|
357
332
|
])
|
|
358
333
|
|
|
359
334
|
const accessibilityProps = getSelectOptionProps(
|
|
@@ -385,10 +360,10 @@ export const SelectItem = createStyledHOC(
|
|
|
385
360
|
tabIndex={
|
|
386
361
|
disabled
|
|
387
362
|
? undefined
|
|
388
|
-
: isWeb &&
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
363
|
+
: isWeb &&
|
|
364
|
+
isAdapted &&
|
|
365
|
+
(isActive || (activeIndexRef?.current == null && isSelected))
|
|
366
|
+
? 0
|
|
392
367
|
: -1
|
|
393
368
|
}
|
|
394
369
|
zIndex={100}
|