@tamagui/select 1.0.1-beta.113 → 1.0.1-beta.114

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.
Files changed (109) hide show
  1. package/dist/cjs/Select.js +50 -558
  2. package/dist/cjs/Select.js.map +2 -2
  3. package/dist/cjs/SelectContent.js +53 -0
  4. package/dist/cjs/SelectContent.js.map +7 -0
  5. package/dist/cjs/SelectContent.native.js +31 -0
  6. package/dist/cjs/SelectContent.native.js.map +7 -0
  7. package/dist/cjs/SelectImpl.js +393 -0
  8. package/dist/cjs/SelectImpl.js.map +7 -0
  9. package/dist/cjs/SelectScrollButton.js +128 -0
  10. package/dist/cjs/SelectScrollButton.js.map +7 -0
  11. package/dist/cjs/SelectScrollButton.native.js +32 -0
  12. package/dist/cjs/SelectScrollButton.native.js.map +7 -0
  13. package/dist/cjs/SelectViewport.js +96 -0
  14. package/dist/cjs/SelectViewport.js.map +7 -0
  15. package/dist/cjs/SelectViewport.native.js +43 -0
  16. package/dist/cjs/SelectViewport.native.js.map +7 -0
  17. package/dist/cjs/constants.js +47 -0
  18. package/dist/cjs/constants.js.map +7 -0
  19. package/dist/cjs/context.js +38 -0
  20. package/dist/cjs/context.js.map +7 -0
  21. package/dist/cjs/index.js +19 -0
  22. package/dist/cjs/index.js.map +2 -2
  23. package/dist/cjs/types.js +17 -0
  24. package/dist/cjs/types.js.map +7 -0
  25. package/dist/esm/Select.js +33 -565
  26. package/dist/esm/Select.js.map +2 -2
  27. package/dist/esm/SelectContent.js +26 -0
  28. package/dist/esm/SelectContent.js.map +7 -0
  29. package/dist/esm/SelectContent.native.js +7 -0
  30. package/dist/esm/SelectContent.native.js.map +7 -0
  31. package/dist/esm/SelectImpl.js +378 -0
  32. package/dist/esm/SelectImpl.js.map +7 -0
  33. package/dist/esm/SelectScrollButton.js +100 -0
  34. package/dist/esm/SelectScrollButton.js.map +7 -0
  35. package/dist/esm/SelectScrollButton.native.js +7 -0
  36. package/dist/esm/SelectScrollButton.native.js.map +7 -0
  37. package/dist/esm/SelectViewport.js +68 -0
  38. package/dist/esm/SelectViewport.js.map +7 -0
  39. package/dist/esm/SelectViewport.native.js +16 -0
  40. package/dist/esm/SelectViewport.native.js.map +7 -0
  41. package/dist/esm/constants.js +17 -0
  42. package/dist/esm/constants.js.map +7 -0
  43. package/dist/esm/context.js +11 -0
  44. package/dist/esm/context.js.map +7 -0
  45. package/dist/esm/index.js +13 -0
  46. package/dist/esm/index.js.map +2 -2
  47. package/dist/esm/types.js +1 -0
  48. package/dist/esm/types.js.map +7 -0
  49. package/dist/jsx/Select.js +32 -515
  50. package/dist/jsx/Select.js.map +2 -2
  51. package/dist/jsx/SelectContent.js +19 -0
  52. package/dist/jsx/SelectContent.js.map +7 -0
  53. package/dist/jsx/SelectContent.native.js +7 -0
  54. package/dist/jsx/SelectContent.native.js.map +7 -0
  55. package/dist/jsx/SelectImpl.js +366 -0
  56. package/dist/jsx/SelectImpl.js.map +7 -0
  57. package/dist/jsx/SelectScrollButton.js +78 -0
  58. package/dist/jsx/SelectScrollButton.js.map +7 -0
  59. package/dist/jsx/SelectScrollButton.native.js +7 -0
  60. package/dist/jsx/SelectScrollButton.native.js.map +7 -0
  61. package/dist/jsx/SelectViewport.js +57 -0
  62. package/dist/jsx/SelectViewport.js.map +7 -0
  63. package/dist/jsx/SelectViewport.native.js +14 -0
  64. package/dist/jsx/SelectViewport.native.js.map +7 -0
  65. package/dist/jsx/constants.js +17 -0
  66. package/dist/jsx/constants.js.map +7 -0
  67. package/dist/jsx/context.js +11 -0
  68. package/dist/jsx/context.js.map +7 -0
  69. package/dist/jsx/index.js +13 -0
  70. package/dist/jsx/index.js.map +2 -2
  71. package/dist/jsx/types.js +1 -0
  72. package/dist/jsx/types.js.map +7 -0
  73. package/package.json +12 -12
  74. package/src/Select.tsx +57 -881
  75. package/src/SelectContent.native.tsx +5 -0
  76. package/src/SelectContent.tsx +34 -0
  77. package/src/SelectImpl.tsx +511 -0
  78. package/src/SelectScrollButton.native.tsx +7 -0
  79. package/src/SelectScrollButton.tsx +150 -0
  80. package/src/SelectViewport.native.tsx +17 -0
  81. package/src/SelectViewport.tsx +86 -0
  82. package/src/constants.tsx +7 -0
  83. package/src/context.tsx +9 -0
  84. package/src/index.tsx +7 -0
  85. package/src/types.tsx +83 -0
  86. package/types/Select.d.ts +140 -298
  87. package/types/Select.d.ts.map +1 -1
  88. package/types/SelectContent.d.ts +4 -0
  89. package/types/SelectContent.d.ts.map +1 -0
  90. package/types/SelectContent.native.d.ts +4 -0
  91. package/types/SelectContent.native.d.ts.map +1 -0
  92. package/types/SelectImpl.d.ts +9 -0
  93. package/types/SelectImpl.d.ts.map +1 -0
  94. package/types/SelectScrollButton.d.ts +6 -0
  95. package/types/SelectScrollButton.d.ts.map +1 -0
  96. package/types/SelectScrollButton.native.d.ts +4 -0
  97. package/types/SelectScrollButton.native.d.ts.map +1 -0
  98. package/types/SelectViewport.d.ts +127 -0
  99. package/types/SelectViewport.d.ts.map +1 -0
  100. package/types/SelectViewport.native.d.ts +54 -0
  101. package/types/SelectViewport.native.d.ts.map +1 -0
  102. package/types/constants.d.ts +8 -0
  103. package/types/constants.d.ts.map +1 -0
  104. package/types/context.d.ts +17 -0
  105. package/types/context.d.ts.map +1 -0
  106. package/types/index.d.ts +2 -0
  107. package/types/index.d.ts.map +1 -1
  108. package/types/types.d.ts +74 -0
  109. package/types/types.d.ts.map +1 -0
package/src/Select.tsx CHANGED
@@ -1,128 +1,32 @@
1
- import {
2
- ContextData,
3
- FloatingContext,
4
- FloatingFocusManager,
5
- FloatingOverlay,
6
- FloatingPortal,
7
- ReferenceType,
8
- autoUpdate,
9
- detectOverflow,
10
- flip,
11
- offset,
12
- size,
13
- useClick,
14
- useDismiss,
15
- useFloating,
16
- useInteractions,
17
- useListNavigation,
18
- useRole,
19
- useTypeahead,
20
- } from '@floating-ui/react-dom-interactions'
21
- import { usePrevious } from '@radix-ui/react-use-previous'
22
1
  import { useComposedRefs } from '@tamagui/compose-refs'
23
- import { GetProps, MediaPropKeys, SizeTokens, TamaguiElement, isWeb, useMedia } from '@tamagui/core'
24
- import {
25
- Theme,
26
- styled,
27
- useGet,
28
- useIsomorphicLayoutEffect,
29
- useThemeName,
30
- withStaticProperties,
31
- } from '@tamagui/core'
2
+ import { GetProps, TamaguiElement, isWeb, useMedia } from '@tamagui/core'
3
+ import { styled, useGet, useIsomorphicLayoutEffect, withStaticProperties } from '@tamagui/core'
32
4
  import { useId } from '@tamagui/core'
33
- import { createContextScope } from '@tamagui/create-context'
34
- import type { Scope } from '@tamagui/create-context'
35
5
  import { ListItem, ListItemProps } from '@tamagui/list-item'
36
- import { PortalHost, PortalItem, PortalItemProps } from '@tamagui/portal'
6
+ import { PortalHost } from '@tamagui/portal'
37
7
  import { Separator } from '@tamagui/separator'
38
8
  import { Sheet, SheetController } from '@tamagui/sheet'
39
- import { ThemeableStack, XStack, YStack, YStackProps } from '@tamagui/stacks'
9
+ import { XStack, YStack, YStackProps } from '@tamagui/stacks'
40
10
  import { Paragraph } from '@tamagui/text'
41
11
  import { useControllableState } from '@tamagui/use-controllable-state'
42
12
  import * as React from 'react'
43
13
  import * as ReactDOM from 'react-dom'
44
14
 
45
- const SELECT_NAME = 'Select'
46
- const WINDOW_PADDING = 8
47
- const SCROLL_ARROW_VELOCITY = 8
48
- const SCROLL_ARROW_THRESHOLD = 8
49
- const MIN_HEIGHT = 80
50
- const FALLBACK_THRESHOLD = 16
51
-
52
- export interface SelectProps {
53
- id?: string
54
- children?: React.ReactNode
55
- value?: string
56
- defaultValue?: string
57
- onValueChange?(value: string): void
58
- open?: boolean
59
- defaultOpen?: boolean
60
- onOpenChange?(open: boolean): void
61
- dir?: Direction
62
- name?: string
63
- autoComplete?: string
64
- size?: SizeTokens
65
- sheetBreakpoint?: MediaPropKeys | false
66
- }
67
-
68
- type NonNull<A> = Exclude<A, void | null>
69
-
70
- interface SelectContextValue {
71
- dir?: Direction
72
- scopeKey: string
73
- sheetBreakpoint: NonNull<SelectProps['sheetBreakpoint']>
74
- size?: SizeTokens
75
- value: any
76
- selectedIndex: number
77
- setSelectedIndex: (index: number) => void
78
- activeIndex: number | null
79
- setActiveIndex: (index: number | null) => void
80
- setValueAtIndex: (index: number, value: string) => void
81
- open: boolean
82
- setOpen: (open: boolean) => void
83
- onChange: (value: string) => void
84
- valueNode: Element | null
85
- onValueNodeChange(node: HTMLElement): void
86
- valueNodeHasChildren: boolean
87
- onValueNodeHasChildrenChange(hasChildren: boolean): void
88
- forceUpdate: React.DispatchWithoutAction
89
-
90
- // InlineImpl only:
91
- dataRef?: React.MutableRefObject<ContextData>
92
- controlledScrolling?: boolean
93
- listRef?: React.MutableRefObject<Array<HTMLElement | null>>
94
- floatingRef?: React.MutableRefObject<HTMLElement | null>
95
- canScrollUp?: boolean
96
- canScrollDown?: boolean
97
- floatingContext?: FloatingContext<ReferenceType>
98
- increaseHeight?: (floating: HTMLElement, amount?: any) => number | undefined
99
- interactions?: {
100
- getReferenceProps: (userProps?: React.HTMLProps<Element> | undefined) => any
101
- getFloatingProps: (userProps?: React.HTMLProps<HTMLElement> | undefined) => any
102
- getItemProps: (userProps?: React.HTMLProps<HTMLElement> | undefined) => any
103
- }
104
- }
15
+ import { SELECT_NAME } from './constants'
16
+ import { SelectProvider, createSelectContext, useSelectContext } from './context'
17
+ import { SelectContent } from './SelectContent'
18
+ import { SelectImplProps, SelectInlineImpl } from './SelectImpl'
19
+ import { SelectScrollDownButton, SelectScrollUpButton } from './SelectScrollButton'
20
+ import { SelectViewport } from './SelectViewport'
21
+ import { ScopedProps, SelectContextValue, SelectProps } from './types'
105
22
 
106
23
  // Cross browser fixes for pinch-zooming/backdrop-filter 🙄
107
24
  const userAgent = (typeof navigator !== 'undefined' && navigator.userAgent) || ''
108
- const isFirefox = userAgent.toLowerCase().includes('firefox')
109
- if (isFirefox) {
110
- document.body.classList.add('firefox')
111
- }
112
- function getVisualOffsetTop() {
25
+ export const isFirefox = userAgent.toLowerCase().includes('firefox')
26
+ export function getVisualOffsetTop() {
113
27
  return !/^((?!chrome|android).)*safari/i.test(userAgent) ? visualViewport?.offsetTop ?? 0 : 0
114
28
  }
115
29
 
116
- type ScopedProps<P> = P & { __scopeSelect?: Scope }
117
-
118
- const [createSelectContext, createSelectScope] = createContextScope(SELECT_NAME)
119
- const [SelectProvider, useSelectContext] = createSelectContext<SelectContextValue>(SELECT_NAME)
120
-
121
- // const [SelectContentContextProvider, useSelectContentContext] =
122
- // createSelectContext<SelectContentContextValue>(CONTENT_NAME);
123
-
124
- type Direction = 'ltr' | 'rtl'
125
-
126
30
  /* -------------------------------------------------------------------------------------------------
127
31
  * SelectTrigger
128
32
  * -----------------------------------------------------------------------------------------------*/
@@ -197,17 +101,22 @@ type SelectValueProps = GetProps<typeof SelectValueFrame> & {
197
101
 
198
102
  const SelectValue = SelectValueFrame.extractable(
199
103
  React.forwardRef<TamaguiElement, SelectValueProps>(
200
- ({ __scopeSelect, children, placeholder }: ScopedProps<SelectValueProps>, forwardedRef) => {
104
+ (
105
+ { __scopeSelect, children: childrenProp, placeholder }: ScopedProps<SelectValueProps>,
106
+ forwardedRef
107
+ ) => {
201
108
  // We ignore `className` and `style` as this part shouldn't be styled.
202
109
  const context = useSelectContext(VALUE_NAME, __scopeSelect)
203
110
  const { onValueNodeHasChildrenChange } = context
204
- const hasChildren = children !== undefined
111
+ const hasChildren = childrenProp !== undefined
205
112
  const composedRefs = useComposedRefs(forwardedRef, context.onValueNodeChange)
206
113
 
207
114
  React.useLayoutEffect(() => {
208
115
  onValueNodeHasChildrenChange(hasChildren)
209
116
  }, [onValueNodeHasChildrenChange, hasChildren])
210
117
 
118
+ const children = childrenProp ?? context.selectedItem
119
+
211
120
  return (
212
121
  <SelectValueFrame
213
122
  size={context.size}
@@ -236,115 +145,6 @@ export const SelectIcon = styled(XStack, {
236
145
  children: <Paragraph>▼</Paragraph>,
237
146
  })
238
147
 
239
- /* -------------------------------------------------------------------------------------------------
240
- * SelectContent
241
- * -----------------------------------------------------------------------------------------------*/
242
-
243
- const CONTENT_NAME = 'SelectContent'
244
-
245
- export type SelectContentProps = { children?: React.ReactNode }
246
-
247
- const SelectContent = ({ children, __scopeSelect }: ScopedProps<SelectContentProps>) => {
248
- const context = useSelectContext(CONTENT_NAME, __scopeSelect)
249
- const themeName = useThemeName()
250
- const showSheet = useShowSelectSheet(context)
251
- const contents = <Theme name={themeName}>{children}</Theme>
252
-
253
- if (showSheet && context.open) {
254
- return contents
255
- }
256
-
257
- return (
258
- <FloatingPortal>
259
- {context.open ? (
260
- <FloatingOverlay lockScroll>{contents}</FloatingOverlay>
261
- ) : (
262
- <div style={{ display: 'none' }}>{contents}</div>
263
- )}
264
- </FloatingPortal>
265
- )
266
- }
267
-
268
- /* -------------------------------------------------------------------------------------------------
269
- * SelectViewport
270
- * -----------------------------------------------------------------------------------------------*/
271
-
272
- const VIEWPORT_NAME = 'SelectViewport'
273
-
274
- export const SelectViewportFrame = styled(ThemeableStack, {
275
- name: VIEWPORT_NAME,
276
- backgroundColor: '$background',
277
- elevate: true,
278
- bordered: true,
279
- // width: '100%',
280
- overflow: 'scroll',
281
- userSelect: 'none',
282
-
283
- variants: {
284
- size: {
285
- '...size': (val, { tokens }) => {
286
- return {
287
- borderRadius: tokens.radius[val] ?? val,
288
- }
289
- },
290
- },
291
- },
292
-
293
- defaultVariants: {
294
- size: '$2',
295
- },
296
- })
297
-
298
- export type SelectViewportProps = GetProps<typeof SelectViewportFrame>
299
-
300
- export const SelectViewport = React.forwardRef<TamaguiElement, SelectViewportProps>(
301
- (props: ScopedProps<SelectViewportProps>, forwardedRef) => {
302
- const { __scopeSelect, children, ...viewportProps } = props
303
- const context = useSelectContext(VIEWPORT_NAME, __scopeSelect)
304
- const { style, ...floatingProps } = context.interactions?.getFloatingProps() ?? {}
305
- const breakpointActive = useSelectBreakpointActive(context.sheetBreakpoint)
306
-
307
- if (breakpointActive || !isWeb) {
308
- return <PortalItem hostName={`${context.scopeKey}SheetContents`}>{children}</PortalItem>
309
- }
310
-
311
- if (!context.floatingContext) {
312
- return null
313
- }
314
-
315
- // TODO
316
- delete style['scrollbarWidth']
317
- delete style['listStyleType']
318
-
319
- return (
320
- <>
321
- {/* Hide scrollbars cross-browser and enable momentum scroll for touch devices */}
322
- <style
323
- dangerouslySetInnerHTML={{
324
- __html: `[data-tamagui-select-viewport]{scrollbar-width:none;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;}[data-tamagui-select-viewport]::-webkit-scrollbar{display:none}`,
325
- }}
326
- />
327
- <FloatingFocusManager context={context.floatingContext} preventTabbing>
328
- <SelectViewportFrame
329
- size={context.size}
330
- data-tamagui-select-viewport=""
331
- // @ts-ignore
332
- role="presentation"
333
- {...viewportProps}
334
- ref={forwardedRef}
335
- {...floatingProps}
336
- {...style}
337
- >
338
- {children}
339
- </SelectViewportFrame>
340
- </FloatingFocusManager>
341
- </>
342
- )
343
- }
344
- )
345
-
346
- SelectViewport.displayName = VIEWPORT_NAME
347
-
348
148
  /* -------------------------------------------------------------------------------------------------
349
149
  * SelectItem
350
150
  * -----------------------------------------------------------------------------------------------*/
@@ -368,8 +168,6 @@ export interface SelectItemProps extends YStackProps {
368
168
  textValue?: string
369
169
  }
370
170
 
371
- // TODO styled(ListItem, { name: 'SelectItem' })
372
-
373
171
  export const SelectItem = React.forwardRef<TamaguiElement, SelectItemProps>(
374
172
  (props: ScopedProps<SelectItemProps>, forwardedRef) => {
375
173
  const {
@@ -402,6 +200,7 @@ export const SelectItem = React.forwardRef<TamaguiElement, SelectItemProps>(
402
200
  const composedRefs = useComposedRefs(
403
201
  forwardedRef,
404
202
  (node) => {
203
+ if (!isWeb) return
405
204
  if (node instanceof HTMLElement) {
406
205
  if (listRef) {
407
206
  listRef.current[index] = node
@@ -511,20 +310,35 @@ const SelectItemText = React.forwardRef<TamaguiElement, SelectItemTextProps>(
511
310
  const itemContext = useSelectItemContext(ITEM_TEXT_NAME, __scopeSelect)
512
311
  const ref = React.useRef<TamaguiElement | null>(null)
513
312
  const composedRefs = useComposedRefs(forwardedRef, ref, itemContext.onItemTextChange)
313
+ const isSelected = itemContext.isSelected && context.valueNode && !context.valueNodeHasChildren
314
+
315
+ const contents = (
316
+ <SelectItemTextFrame
317
+ className={className}
318
+ size={context.size}
319
+ id={itemContext.textId}
320
+ {...itemTextProps}
321
+ ref={composedRefs}
322
+ />
323
+ )
324
+
325
+ // until portals work in sub-trees on RN, use this just for native:
326
+ if (!isWeb) {
327
+ React.useEffect(() => {
328
+ if (isSelected) {
329
+ context.setSelectedItem(contents)
330
+ }
331
+ // eslint-disable-next-line react-hooks/exhaustive-deps
332
+ }, [isSelected])
333
+ }
514
334
 
515
335
  return (
516
336
  <>
517
- <SelectItemTextFrame
518
- className={className}
519
- size={context.size}
520
- id={itemContext.textId}
521
- {...itemTextProps}
522
- ref={composedRefs}
523
- />
337
+ {contents}
524
338
 
525
339
  {/* Portal the select item text into the trigger value node */}
526
- {itemContext.isSelected && context.valueNode && !context.valueNodeHasChildren
527
- ? ReactDOM.createPortal(itemTextProps.children, context.valueNode)
340
+ {isWeb && isSelected
341
+ ? ReactDOM.createPortal(itemTextProps.children, context.valueNode!)
528
342
  : null}
529
343
 
530
344
  {/* Portal an option in the bubble select */}
@@ -633,154 +447,6 @@ const SelectLabel = React.forwardRef<TamaguiElement, SelectLabelProps>(
633
447
 
634
448
  SelectLabel.displayName = LABEL_NAME
635
449
 
636
- /* -------------------------------------------------------------------------------------------------
637
- * SelectScrollUpButton
638
- * -----------------------------------------------------------------------------------------------*/
639
-
640
- const SCROLL_UP_BUTTON_NAME = 'SelectScrollUpButton'
641
-
642
- interface SelectScrollButtonProps
643
- extends Omit<SelectScrollButtonImplProps, 'dir' | 'componentName'> {}
644
-
645
- const SelectScrollUpButton = React.forwardRef<TamaguiElement, SelectScrollButtonProps>(
646
- (props: ScopedProps<SelectScrollButtonProps>, forwardedRef) => {
647
- return (
648
- <SelectScrollButtonImpl
649
- componentName={SCROLL_UP_BUTTON_NAME}
650
- {...props}
651
- dir="up"
652
- ref={forwardedRef}
653
- />
654
- )
655
- }
656
- )
657
-
658
- SelectScrollUpButton.displayName = SCROLL_UP_BUTTON_NAME
659
-
660
- /* -------------------------------------------------------------------------------------------------
661
- * SelectScrollDownButton
662
- * -----------------------------------------------------------------------------------------------*/
663
-
664
- const SCROLL_DOWN_BUTTON_NAME = 'SelectScrollDownButton'
665
-
666
- const SelectScrollDownButton = React.forwardRef<TamaguiElement, SelectScrollButtonProps>(
667
- (props: ScopedProps<SelectScrollButtonProps>, forwardedRef) => {
668
- return (
669
- <SelectScrollButtonImpl
670
- componentName={SCROLL_DOWN_BUTTON_NAME}
671
- {...props}
672
- dir="down"
673
- ref={forwardedRef}
674
- />
675
- )
676
- }
677
- )
678
-
679
- SelectScrollDownButton.displayName = SCROLL_DOWN_BUTTON_NAME
680
-
681
- type SelectScrollButtonImplElement = TamaguiElement
682
- interface SelectScrollButtonImplProps extends YStackProps {
683
- dir: 'up' | 'down'
684
- componentName: string
685
- }
686
-
687
- const SelectScrollButtonImpl = React.memo(
688
- React.forwardRef<SelectScrollButtonImplElement, SelectScrollButtonImplProps>(
689
- (props: ScopedProps<SelectScrollButtonImplProps>, forwardedRef) => {
690
- const { __scopeSelect, dir, componentName, ...scrollIndicatorProps } = props
691
- const { floatingRef, increaseHeight, forceUpdate, ...context } = useSelectContext(
692
- componentName,
693
- __scopeSelect
694
- )
695
- const intervalRef = React.useRef<any>()
696
- const loopingRef = React.useRef(false)
697
- const isVisible = context[dir === 'down' ? 'canScrollDown' : 'canScrollUp']
698
-
699
- const { x, y, reference, floating, strategy, update, refs } = useFloating({
700
- strategy: 'fixed',
701
- placement: dir === 'up' ? 'top' : 'bottom',
702
- middleware: [offset(({ rects }) => -rects.floating.height)],
703
- })
704
-
705
- const composedRefs = useComposedRefs(forwardedRef, floating)
706
-
707
- React.useLayoutEffect(() => {
708
- if (!floatingRef) return
709
- reference(floatingRef.current)
710
- // eslint-disable-next-line react-hooks/exhaustive-deps
711
- }, [reference, floatingRef?.current])
712
-
713
- React.useEffect(() => {
714
- if (!refs.reference.current || !refs.floating.current || !isVisible) {
715
- return
716
- }
717
-
718
- const cleanup = autoUpdate(refs.reference.current, refs.floating.current, update, {
719
- animationFrame: true,
720
- })
721
-
722
- return () => {
723
- clearInterval(intervalRef.current)
724
- loopingRef.current = false
725
- cleanup()
726
- }
727
- }, [isVisible, update, refs.floating, refs.reference])
728
-
729
- if (!isVisible) {
730
- return null
731
- }
732
-
733
- const handleScrollArrowChange = () => {
734
- if (!floatingRef) return
735
- const floating = floatingRef.current
736
- const isUp = dir === 'up'
737
-
738
- if (floating) {
739
- const value = isUp ? -SCROLL_ARROW_VELOCITY : SCROLL_ARROW_VELOCITY
740
- const multi =
741
- (isUp && floating.scrollTop <= SCROLL_ARROW_THRESHOLD * 2) ||
742
- (!isUp &&
743
- floating.scrollTop >=
744
- floating.scrollHeight - floating.clientHeight - SCROLL_ARROW_THRESHOLD * 2)
745
- ? 2
746
- : 1
747
-
748
- floating.scrollTop += multi * (isUp ? -SCROLL_ARROW_VELOCITY : SCROLL_ARROW_VELOCITY)
749
-
750
- increaseHeight?.(floating, multi === 2 ? value * 2 : value)
751
- // Ensure derived data (scroll arrows) is fresh
752
- forceUpdate()
753
- }
754
- }
755
-
756
- return (
757
- <YStack
758
- ref={composedRefs}
759
- componentName={componentName}
760
- aria-hidden
761
- {...scrollIndicatorProps}
762
- zIndex={1000}
763
- // @ts-expect-error
764
- position={strategy}
765
- left={x || 0}
766
- top={y || 0}
767
- width={`calc(${(floatingRef?.current?.offsetWidth ?? 0) - 2}px)`}
768
- onPointerMove={() => {
769
- if (!loopingRef.current) {
770
- intervalRef.current = setInterval(handleScrollArrowChange, 1000 / 60)
771
- loopingRef.current = true
772
- }
773
- }}
774
- onPointerLeave={() => {
775
- loopingRef.current = false
776
- clearInterval(intervalRef.current)
777
- }}
778
- />
779
- )
780
- }
781
- )
782
- )
783
-
784
450
  /* -------------------------------------------------------------------------------------------------
785
451
  * SelectSeparator
786
452
  * -----------------------------------------------------------------------------------------------*/
@@ -789,12 +455,14 @@ export const SelectSeparator = styled(Separator, {
789
455
  name: 'SelectSeparator',
790
456
  })
791
457
 
792
- const useSelectBreakpointActive = (sheetBreakpoint: SelectContextValue['sheetBreakpoint']) => {
458
+ export const useSelectBreakpointActive = (
459
+ sheetBreakpoint: SelectContextValue['sheetBreakpoint']
460
+ ) => {
793
461
  const media = useMedia()
794
462
  return sheetBreakpoint ? media[sheetBreakpoint] : false
795
463
  }
796
464
 
797
- const useShowSelectSheet = (context: SelectContextValue) => {
465
+ export const useShowSelectSheet = (context: SelectContextValue) => {
798
466
  const breakpointActive = useSelectBreakpointActive(context.sheetBreakpoint)
799
467
  return context.open === false ? false : breakpointActive
800
468
  }
@@ -838,507 +506,14 @@ export const SelectSheetContents = ({ __scopeSelect }: ScopedProps<{}>) => {
838
506
 
839
507
  SelectSheetContents.displayName = SHEET_CONTENTS_NAME
840
508
 
841
- /* -------------------------------------------------------------------------------------------------
842
- * Select
843
- * -----------------------------------------------------------------------------------------------*/
844
-
845
- type SelectImplProps = ScopedProps<SelectProps> & {
846
- activeIndexRef: any
847
- selectedIndexRef: any
848
- listContentRef: any
849
- }
850
-
851
- // TODO use id for focusing from label
852
-
853
- const SelectInlineImpl = (props: SelectImplProps) => {
854
- const {
855
- __scopeSelect,
856
- children,
857
- open = false,
858
- activeIndexRef,
859
- selectedIndexRef,
860
- listContentRef,
861
- } = props
862
-
863
- const selectContext = useSelectContext('SelectSheetImpl', __scopeSelect)
864
- const { setActiveIndex, setOpen, setSelectedIndex, selectedIndex, activeIndex, forceUpdate } =
865
- selectContext
866
- const [showArrows, setShowArrows] = React.useState(false)
867
- const [scrollTop, setScrollTop] = React.useState(0)
868
- const prevActiveIndex = usePrevious<number | null>(activeIndex)
869
-
870
- const listItemsRef = React.useRef<Array<HTMLElement | null>>([])
871
-
872
- const [controlledScrolling, setControlledScrolling] = React.useState(false)
873
- const [middlewareType, setMiddlewareType] = React.useState<'align' | 'fallback'>('align')
874
-
875
- // Wait for scroll position to settle before showing arrows to prevent
876
- // interference with pointer events.
877
- React.useEffect(() => {
878
- const frame = requestAnimationFrame(() => {
879
- setShowArrows(open)
880
-
881
- if (!open) {
882
- setScrollTop(0)
883
- setMiddlewareType('align')
884
- setActiveIndex(null)
885
- setControlledScrolling(false)
886
- }
887
- })
888
- return () => {
889
- cancelAnimationFrame(frame)
890
- }
891
- }, [open, setActiveIndex])
892
-
893
- function getFloatingPadding(floating: HTMLElement | null) {
894
- if (!floating) {
895
- return 0
896
- }
897
- return Number(getComputedStyle(floating).paddingLeft?.replace('px', ''))
898
- }
899
-
900
- const { x, y, reference, floating, strategy, context, refs, middlewareData, update } =
901
- useFloating({
902
- open,
903
- onOpenChange: setOpen,
904
- placement: 'bottom',
905
- middleware:
906
- middlewareType === 'align'
907
- ? [
908
- offset(({ rects }) => {
909
- const index = activeIndexRef.current ?? selectedIndexRef.current
910
-
911
- if (index == null) {
912
- return 0
913
- }
914
-
915
- const item = listItemsRef.current[index]
916
-
917
- if (item == null) {
918
- return 0
919
- }
920
-
921
- const offsetTop = item.offsetTop
922
- const itemHeight = item.offsetHeight
923
- const height = rects.reference.height
924
-
925
- return -offsetTop - height - (itemHeight - height) / 2
926
- }),
927
- // Custom `size` that can handle the opposite direction of the placement
928
- {
929
- name: 'size',
930
- async fn(args) {
931
- const {
932
- elements: { floating },
933
- rects: { reference },
934
- middlewareData,
935
- } = args
936
-
937
- const overflow = await detectOverflow(args, {
938
- padding: WINDOW_PADDING,
939
- })
940
-
941
- const top = Math.max(0, overflow.top)
942
- const bottom = Math.max(0, overflow.bottom)
943
- const nextY = args.y + top
944
-
945
- if (middlewareData.size?.skip) {
946
- return {
947
- y: nextY,
948
- data: {
949
- y: middlewareData.size.y,
950
- },
951
- }
952
- }
953
-
954
- Object.assign(floating.style, {
955
- maxHeight: `${floating.scrollHeight - Math.abs(top + bottom)}px`,
956
- minWidth: `${reference.width + getFloatingPadding(floating) * 2}px`,
957
- })
958
-
959
- return {
960
- y: nextY,
961
- data: {
962
- y: top,
963
- skip: true,
964
- },
965
- reset: {
966
- rects: true,
967
- },
968
- }
969
- },
970
- },
971
- ]
972
- : [
973
- offset(5),
974
- flip(),
975
- size({
976
- apply({ rects, availableHeight, elements }) {
977
- Object.assign(elements.floating.style, {
978
- width: `${rects.reference.width}px`,
979
- maxHeight: `${availableHeight}px`,
980
- })
981
- },
982
- padding: WINDOW_PADDING,
983
- }),
984
- ],
985
- })
986
-
987
- const floatingRef = refs.floating
988
-
989
- const showUpArrow = showArrows && scrollTop > SCROLL_ARROW_THRESHOLD
990
- const showDownArrow =
991
- showArrows &&
992
- floatingRef.current &&
993
- scrollTop <
994
- floatingRef.current.scrollHeight - floatingRef.current.clientHeight - SCROLL_ARROW_THRESHOLD
995
-
996
- const interactions = useInteractions([
997
- useClick(context, { pointerDown: true }),
998
- useRole(context, { role: 'listbox' }),
999
- useDismiss(context),
1000
- useListNavigation(context, {
1001
- listRef: listItemsRef,
1002
- activeIndex,
1003
- selectedIndex,
1004
- onNavigate: setActiveIndex,
1005
- }),
1006
- useTypeahead(context, {
1007
- listRef: listContentRef,
1008
- onMatch: open ? setActiveIndex : setSelectedIndex,
1009
- selectedIndex,
1010
- activeIndex,
1011
- }),
1012
- ])
1013
-
1014
- const increaseHeight = React.useCallback(
1015
- (floating: HTMLElement, amount = 0) => {
1016
- if (middlewareType === 'fallback') {
1017
- return
1018
- }
1019
-
1020
- const currentMaxHeight = Number(floating.style.maxHeight.replace('px', ''))
1021
- const currentTop = Number(floating.style.top.replace('px', ''))
1022
- const rect = floating.getBoundingClientRect()
1023
- const rectTop = rect.top
1024
- const rectBottom = rect.bottom
1025
- const viewportHeight = visualViewport?.height ?? 0
1026
- const visualMaxHeight = viewportHeight - WINDOW_PADDING * 2
1027
-
1028
- if (
1029
- amount < 0 &&
1030
- selectedIndexRef.current != null &&
1031
- Math.round(rectBottom) < Math.round(viewportHeight + getVisualOffsetTop() - WINDOW_PADDING)
1032
- ) {
1033
- floating.style.maxHeight = `${Math.min(visualMaxHeight, currentMaxHeight - amount)}px`
1034
- }
1035
-
1036
- if (
1037
- amount > 0 &&
1038
- Math.round(rectTop) > Math.round(WINDOW_PADDING - getVisualOffsetTop()) &&
1039
- floating.scrollHeight > floating.offsetHeight
1040
- ) {
1041
- const nextTop = Math.max(WINDOW_PADDING + getVisualOffsetTop(), currentTop - amount)
1042
-
1043
- const nextMaxHeight = Math.min(visualMaxHeight, currentMaxHeight + amount)
1044
-
1045
- Object.assign(floating.style, {
1046
- maxHeight: `${nextMaxHeight}px`,
1047
- top: `${nextTop}px`,
1048
- })
1049
-
1050
- if (nextTop - WINDOW_PADDING > getVisualOffsetTop()) {
1051
- floating.scrollTop -= nextMaxHeight - currentMaxHeight + getFloatingPadding(floating)
1052
- }
1053
-
1054
- return currentTop - nextTop
1055
- }
1056
- },
1057
- [middlewareType, selectedIndexRef]
1058
- )
1059
-
1060
- const touchPageYRef = React.useRef<number | null>(null)
1061
-
1062
- const handleWheel = React.useCallback(
1063
- (event: WheelEvent | TouchEvent) => {
1064
- const pinching = event.ctrlKey
1065
-
1066
- const currentTarget = event.currentTarget as HTMLElement
1067
-
1068
- function isWheelEvent(event: any): event is WheelEvent {
1069
- return typeof event.deltaY === 'number'
1070
- }
1071
-
1072
- function isTouchEvent(event: any): event is TouchEvent {
1073
- return event.touches != null
1074
- }
1075
-
1076
- if (
1077
- Math.abs(
1078
- (currentTarget?.offsetHeight ?? 0) - ((visualViewport?.height ?? 0) - WINDOW_PADDING * 2)
1079
- ) > 1 &&
1080
- !pinching
1081
- ) {
1082
- event.preventDefault()
1083
- } else if (isWheelEvent(event) && isFirefox) {
1084
- // Firefox needs this to propagate scrolling
1085
- // during momentum scrolling phase if the
1086
- // height reached its maximum (at boundaries)
1087
- currentTarget.scrollTop += event.deltaY
1088
- }
1089
-
1090
- if (!pinching) {
1091
- let delta = 5
1092
-
1093
- if (isTouchEvent(event)) {
1094
- const currentPageY = touchPageYRef.current
1095
- const pageY = event.touches[0]?.pageY
1096
-
1097
- if (pageY != null) {
1098
- touchPageYRef.current = pageY
1099
-
1100
- if (currentPageY != null) {
1101
- delta = currentPageY - pageY
1102
- }
1103
- }
1104
- }
1105
-
1106
- increaseHeight(currentTarget, isWheelEvent(event) ? event.deltaY : delta)
1107
- setScrollTop(currentTarget.scrollTop)
1108
- // Ensure derived data (scroll arrows) is fresh
1109
- forceUpdate()
1110
- }
1111
- },
1112
- [forceUpdate, increaseHeight]
1113
- )
1114
-
1115
- // Handle `onWheel` event in an effect to remove the `passive` option so we
1116
- // can .preventDefault() it
1117
- React.useEffect(() => {
1118
- function onTouchEnd() {
1119
- touchPageYRef.current = null
1120
- }
1121
-
1122
- const floating = floatingRef.current
1123
- if (open && floating && middlewareType === 'align') {
1124
- floating.addEventListener('wheel', handleWheel)
1125
- floating.addEventListener('touchmove', handleWheel)
1126
- floating.addEventListener('touchend', onTouchEnd, { passive: true })
1127
- return () => {
1128
- floating.removeEventListener('wheel', handleWheel)
1129
- floating.removeEventListener('touchmove', handleWheel)
1130
- floating.removeEventListener('touchend', onTouchEnd)
1131
- }
1132
- }
1133
- }, [open, floatingRef, handleWheel, middlewareType])
1134
-
1135
- // Ensure the menu remains attached to the reference element when resizing.
1136
- React.useEffect(() => {
1137
- window.addEventListener('resize', update)
1138
- return () => {
1139
- window.removeEventListener('resize', update)
1140
- }
1141
- }, [update])
1142
-
1143
- // Scroll the active or selected item into view when in `controlledScrolling`
1144
- // mode (i.e. arrow key nav).
1145
- React.useLayoutEffect(() => {
1146
- const floating = floatingRef.current
1147
-
1148
- if (open && controlledScrolling && floating) {
1149
- const item =
1150
- activeIndex != null
1151
- ? listItemsRef.current[activeIndex]
1152
- : selectedIndex != null
1153
- ? listItemsRef.current[selectedIndex]
1154
- : null
1155
-
1156
- if (item && prevActiveIndex != null) {
1157
- const itemHeight = listItemsRef.current[prevActiveIndex]?.offsetHeight ?? 0
1158
-
1159
- const floatingHeight = floating.offsetHeight
1160
- const top = item.offsetTop
1161
- const bottom = top + itemHeight
1162
-
1163
- if (top < floating.scrollTop + 20) {
1164
- const diff = floating.scrollTop - top + 20
1165
- floating.scrollTop -= diff
1166
-
1167
- if (activeIndex != selectedIndex && activeIndex != null) {
1168
- increaseHeight(floating, -diff)
1169
- }
1170
- } else if (bottom > floatingHeight + floating.scrollTop - 20) {
1171
- const diff = bottom - floatingHeight - floating.scrollTop + 20
1172
-
1173
- floating.scrollTop += diff
1174
-
1175
- if (activeIndex != selectedIndex && activeIndex != null) {
1176
- floating.scrollTop -= increaseHeight(floating, diff) ?? 0
1177
- }
1178
- }
1179
- }
1180
- }
1181
- }, [
1182
- open,
1183
- controlledScrolling,
1184
- prevActiveIndex,
1185
- activeIndex,
1186
- selectedIndex,
1187
- floatingRef,
1188
- increaseHeight,
1189
- ])
1190
-
1191
- // Sync the height and the scrollTop values and device whether to use fallback
1192
- // positioning.
1193
- React.useLayoutEffect(() => {
1194
- const floating = refs.floating.current
1195
- const reference = refs.reference.current
1196
-
1197
- if (open && floating && reference && floating.offsetHeight < floating.scrollHeight) {
1198
- const referenceRect = reference.getBoundingClientRect()
1199
-
1200
- if (middlewareType === 'fallback') {
1201
- const item = listItemsRef.current[selectedIndex]
1202
- if (item) {
1203
- floating.scrollTop = item.offsetTop - floating.clientHeight + referenceRect.height
1204
- }
1205
- return
1206
- }
1207
-
1208
- floating.scrollTop = middlewareData.size?.y
1209
-
1210
- const closeToBottom =
1211
- (visualViewport?.height ?? 0) + getVisualOffsetTop() - referenceRect.bottom <
1212
- FALLBACK_THRESHOLD
1213
- const closeToTop = referenceRect.top < FALLBACK_THRESHOLD
1214
-
1215
- if (floating.offsetHeight < MIN_HEIGHT || closeToTop || closeToBottom) {
1216
- setMiddlewareType('fallback')
1217
- }
1218
- }
1219
- }, [
1220
- open,
1221
- increaseHeight,
1222
- selectedIndex,
1223
- middlewareType,
1224
- refs.floating,
1225
- refs.reference,
1226
- // Always re-run this effect when the position has been computed so the
1227
- // .scrollTop change works with fresh sizing.
1228
- middlewareData,
1229
- ])
1230
-
1231
- React.useLayoutEffect(() => {
1232
- if (open && selectedIndex != null) {
1233
- requestAnimationFrame(() => {
1234
- listItemsRef.current[selectedIndex]?.focus({ preventScroll: true })
1235
- })
1236
- }
1237
- }, [listItemsRef, selectedIndex, open])
1238
-
1239
- // Wait for scroll position to settle before showing arrows to prevent
1240
- // interference with pointer events.
1241
- React.useEffect(() => {
1242
- const frame = requestAnimationFrame(() => {
1243
- setShowArrows(open)
1244
-
1245
- if (!open) {
1246
- setScrollTop(0)
1247
- setMiddlewareType('align')
1248
- setActiveIndex(null)
1249
- setControlledScrolling(false)
1250
- }
1251
- })
1252
- return () => cancelAnimationFrame(frame)
1253
- }, [open])
1254
-
1255
- // We set this to true by default so that events bubble to forms without JS (SSR)
1256
- // const isFormControl = trigger ? Boolean(trigger.closest('form')) : true
1257
- // const [bubbleSelect, setBubbleSelect] = React.useState<HTMLSelectElement | null>(null)
1258
- // const triggerPointerDownPosRef = React.useRef<{ x: number; y: number } | null>(null)
1259
-
1260
- return (
1261
- <SelectProvider
1262
- scope={__scopeSelect}
1263
- {...(selectContext as Required<typeof selectContext>)}
1264
- increaseHeight={increaseHeight}
1265
- floatingRef={floatingRef}
1266
- setValueAtIndex={(index, value) => {
1267
- listContentRef.current[index] = value
1268
- }}
1269
- interactions={{
1270
- ...interactions,
1271
- getReferenceProps() {
1272
- return interactions.getReferenceProps({
1273
- ref: reference,
1274
- className: 'SelectTrigger',
1275
- onKeyDown(event) {
1276
- if (event.key === 'Enter' || (event.key === ' ' && !context.dataRef.current.typing)) {
1277
- event.preventDefault()
1278
- setOpen(true)
1279
- }
1280
- },
1281
- })
1282
- },
1283
- getFloatingProps(props) {
1284
- return interactions.getFloatingProps({
1285
- ref: floating,
1286
- className: 'Select',
1287
- ...props,
1288
- style: {
1289
- position: strategy,
1290
- top: y ?? '',
1291
- left: x ?? '',
1292
- outline: 0,
1293
- listStyleType: 'none',
1294
- scrollbarWidth: 'none',
1295
- userSelect: 'none',
1296
- ...props?.style,
1297
- },
1298
- onPointerEnter() {
1299
- setControlledScrolling(false)
1300
- },
1301
- onPointerMove() {
1302
- setControlledScrolling(false)
1303
- },
1304
- onKeyDown() {
1305
- setControlledScrolling(true)
1306
- },
1307
- onScroll(event) {
1308
- setScrollTop(event.currentTarget.scrollTop)
1309
- },
1310
- })
1311
- },
1312
- }}
1313
- floatingContext={context}
1314
- activeIndex={activeIndex}
1315
- canScrollDown={!!showDownArrow}
1316
- canScrollUp={!!showUpArrow}
1317
- controlledScrolling
1318
- dataRef={context.dataRef}
1319
- listRef={listItemsRef}
1320
- >
1321
- {children}
1322
- {/* {isFormControl ? (
1323
- <BubbleSelect
1324
- ref={setBubbleSelect}
1325
- aria-hidden
1326
- tabIndex={-1}
1327
- name={name}
1328
- autoComplete={autoComplete}
1329
- value={value}
1330
- // enable form autofill
1331
- onChange={(event) => setValue(event.target.value)}
1332
- />
1333
- ) : null} */}
1334
- </SelectProvider>
1335
- )
1336
- }
1337
-
1338
509
  const SelectSheetImpl = (props: SelectImplProps) => {
1339
510
  return <>{props.children}</>
1340
511
  }
1341
512
 
513
+ /* -------------------------------------------------------------------------------------------------
514
+ * Select
515
+ * -----------------------------------------------------------------------------------------------*/
516
+
1342
517
  export const Select = withStaticProperties(
1343
518
  (props: ScopedProps<SelectProps>) => {
1344
519
  const {
@@ -1350,7 +525,7 @@ export const Select = withStaticProperties(
1350
525
  value: valueProp,
1351
526
  defaultValue,
1352
527
  onValueChange,
1353
- size: sizeProp,
528
+ size: sizeProp = '$4',
1354
529
  sheetBreakpoint = false,
1355
530
  dir,
1356
531
  } = props
@@ -1358,6 +533,7 @@ export const Select = withStaticProperties(
1358
533
  const isSheet = useSelectBreakpointActive(sheetBreakpoint)
1359
534
  const SelectImpl = isSheet ? SelectSheetImpl : SelectInlineImpl
1360
535
  const forceUpdate = React.useReducer(() => ({}), {})[1]
536
+ const [selectedItem, setSelectedItem] = React.useState<React.ReactNode>(null)
1361
537
 
1362
538
  const [open, setOpen] = useControllableState({
1363
539
  prop: openProp,
@@ -1393,6 +569,8 @@ export const Select = withStaticProperties(
1393
569
  <SelectProvider
1394
570
  dir={dir}
1395
571
  size={sizeProp}
572
+ selectedItem={selectedItem}
573
+ setSelectedItem={setSelectedItem}
1396
574
  forceUpdate={forceUpdate}
1397
575
  valueNode={valueNode}
1398
576
  onValueNodeChange={setValueNode}
@@ -1448,5 +626,3 @@ export const Select = withStaticProperties(
1448
626
 
1449
627
  // @ts-ignore
1450
628
  Select.displayName = SELECT_NAME
1451
-
1452
- export { createSelectScope }