@tamagui/select 1.0.1-beta.103 → 1.0.1-beta.106

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/src/Select.tsx CHANGED
@@ -20,11 +20,11 @@ import {
20
20
  } from '@floating-ui/react-dom-interactions'
21
21
  import { usePrevious } from '@radix-ui/react-use-previous'
22
22
  import { useComposedRefs } from '@tamagui/compose-refs'
23
+ import { GetProps, MediaPropKeys, SizeTokens, TamaguiElement, isWeb, useMedia } from '@tamagui/core'
23
24
  import {
24
- GetProps,
25
- SizeTokens,
26
25
  Theme,
27
26
  styled,
27
+ useGet,
28
28
  useIsomorphicLayoutEffect,
29
29
  useThemeName,
30
30
  withStaticProperties,
@@ -33,29 +33,14 @@ import { useId } from '@tamagui/core'
33
33
  import { createContextScope } from '@tamagui/create-context'
34
34
  import type { Scope } from '@tamagui/create-context'
35
35
  import { ListItem, ListItemProps } from '@tamagui/list-item'
36
+ import { PortalHost, PortalItem, PortalItemProps } from '@tamagui/portal'
36
37
  import { Separator } from '@tamagui/separator'
38
+ import { Sheet, SheetController } from '@tamagui/sheet'
37
39
  import { ThemeableStack, XStack, YStack, YStackProps } from '@tamagui/stacks'
38
40
  import { Paragraph } from '@tamagui/text'
39
41
  import { useControllableState } from '@tamagui/use-controllable-state'
40
42
  import * as React from 'react'
41
43
  import * as ReactDOM from 'react-dom'
42
- import { View } from 'react-native'
43
-
44
- type TamaguiElement = HTMLElement | View
45
-
46
- // Cross browser fixes for pinch-zooming/backdrop-filter 🙄
47
- const userAgent = (typeof navigator !== 'undefined' && navigator.userAgent) || ''
48
- const isFirefox = userAgent.toLowerCase().includes('firefox')
49
- if (isFirefox) {
50
- document.body.classList.add('firefox')
51
- }
52
- function getVisualOffsetTop() {
53
- return !/^((?!chrome|android).)*safari/i.test(userAgent) ? visualViewport?.offsetTop ?? 0 : 0
54
- }
55
-
56
- /* -------------------------------------------------------------------------------------------------
57
- * SelectContext
58
- * -----------------------------------------------------------------------------------------------*/
59
44
 
60
45
  const SELECT_NAME = 'Select'
61
46
  const WINDOW_PADDING = 8
@@ -64,7 +49,28 @@ const SCROLL_ARROW_THRESHOLD = 8
64
49
  const MIN_HEIGHT = 80
65
50
  const FALLBACK_THRESHOLD = 16
66
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
+
67
70
  interface SelectContextValue {
71
+ dir?: Direction
72
+ scopeKey: string
73
+ sheetBreakpoint: NonNull<SelectProps['sheetBreakpoint']>
68
74
  size?: SizeTokens
69
75
  value: any
70
76
  selectedIndex: number
@@ -72,29 +78,41 @@ interface SelectContextValue {
72
78
  activeIndex: number | null
73
79
  setActiveIndex: (index: number | null) => void
74
80
  setValueAtIndex: (index: number, value: string) => void
75
- listRef: React.MutableRefObject<Array<HTMLElement | null>>
76
- floatingRef: React.MutableRefObject<HTMLElement | null>
77
81
  open: boolean
78
82
  setOpen: (open: boolean) => void
79
83
  onChange: (value: string) => void
80
- dataRef: React.MutableRefObject<ContextData>
81
- controlledScrolling: boolean
82
84
  valueNode: Element | null
83
85
  onValueNodeChange(node: HTMLElement): void
84
86
  valueNodeHasChildren: boolean
85
87
  onValueNodeHasChildrenChange(hasChildren: boolean): void
86
- canScrollUp: boolean
87
- canScrollDown: boolean
88
- floatingContext: FloatingContext<ReferenceType>
89
- increaseHeight: (floating: HTMLElement, amount?: any) => number | undefined
90
88
  forceUpdate: React.DispatchWithoutAction
91
- interactions: {
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?: {
92
100
  getReferenceProps: (userProps?: React.HTMLProps<Element> | undefined) => any
93
101
  getFloatingProps: (userProps?: React.HTMLProps<HTMLElement> | undefined) => any
94
102
  getItemProps: (userProps?: React.HTMLProps<HTMLElement> | undefined) => any
95
103
  }
96
104
  }
97
105
 
106
+ // Cross browser fixes for pinch-zooming/backdrop-filter 🙄
107
+ 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() {
113
+ return !/^((?!chrome|android).)*safari/i.test(userAgent) ? visualViewport?.offsetTop ?? 0 : 0
114
+ }
115
+
98
116
  type ScopedProps<P> = P & { __scopeSelect?: Scope }
99
117
 
100
118
  const [createSelectContext, createSelectScope] = createContextScope(SELECT_NAME)
@@ -103,8 +121,6 @@ const [SelectProvider, useSelectContext] = createSelectContext<SelectContextValu
103
121
  // const [SelectContentContextProvider, useSelectContentContext] =
104
122
  // createSelectContext<SelectContentContextValue>(CONTENT_NAME);
105
123
 
106
- type GenericElement = HTMLElement | View
107
-
108
124
  type Direction = 'ltr' | 'rtl'
109
125
 
110
126
  /* -------------------------------------------------------------------------------------------------
@@ -115,7 +131,7 @@ const TRIGGER_NAME = 'SelectTrigger'
115
131
 
116
132
  export type SelectTriggerProps = ListItemProps
117
133
 
118
- export const SelectTrigger = React.forwardRef<GenericElement, SelectTriggerProps>(
134
+ export const SelectTrigger = React.forwardRef<TamaguiElement, SelectTriggerProps>(
119
135
  (props: ScopedProps<SelectTriggerProps>, forwardedRef) => {
120
136
  const {
121
137
  __scopeSelect,
@@ -128,7 +144,7 @@ export const SelectTrigger = React.forwardRef<GenericElement, SelectTriggerProps
128
144
  // const composedRefs = useComposedRefs(forwardedRef, context.onTriggerChange)
129
145
  // const getItems = useCollection(__scopeSelect)
130
146
  // const labelId = useLabelContext(context.trigger)
131
- // const labelledBy = ariaLabelledby || labelId
147
+ const labelledBy = ariaLabelledby // || labelId
132
148
 
133
149
  return (
134
150
  <ListItem
@@ -144,13 +160,19 @@ export const SelectTrigger = React.forwardRef<GenericElement, SelectTriggerProps
144
160
  // aria-controls={context.contentId}
145
161
  aria-expanded={context.open}
146
162
  aria-autocomplete="none"
147
- // aria-labelledby={labelledBy}
148
- // dir={context.dir}
163
+ aria-labelledby={labelledBy}
164
+ dir={context.dir}
149
165
  disabled={disabled}
150
166
  data-disabled={disabled ? '' : undefined}
151
167
  {...triggerProps}
152
168
  ref={forwardedRef}
153
- {...context.interactions.getReferenceProps()}
169
+ {...(context.interactions
170
+ ? context.interactions.getReferenceProps()
171
+ : {
172
+ onPress() {
173
+ context.setOpen(!context.open)
174
+ },
175
+ })}
154
176
  />
155
177
  )
156
178
  }
@@ -225,9 +247,13 @@ export type SelectContentProps = { children?: React.ReactNode }
225
247
  const SelectContent = ({ children, __scopeSelect }: ScopedProps<SelectContentProps>) => {
226
248
  const context = useSelectContext(CONTENT_NAME, __scopeSelect)
227
249
  const themeName = useThemeName()
228
-
250
+ const showSheet = useShowSelectSheet(context)
229
251
  const contents = <Theme name={themeName}>{children}</Theme>
230
252
 
253
+ if (showSheet && context.open) {
254
+ return contents
255
+ }
256
+
231
257
  return (
232
258
  <FloatingPortal>
233
259
  {context.open ? (
@@ -250,6 +276,7 @@ export const SelectViewportFrame = styled(ThemeableStack, {
250
276
  backgroundColor: '$background',
251
277
  elevate: true,
252
278
  bordered: true,
279
+ // width: '100%',
253
280
  overflow: 'scroll',
254
281
  userSelect: 'none',
255
282
 
@@ -274,9 +301,21 @@ export const SelectViewport = React.forwardRef<TamaguiElement, SelectViewportPro
274
301
  (props: ScopedProps<SelectViewportProps>, forwardedRef) => {
275
302
  const { __scopeSelect, children, ...viewportProps } = props
276
303
  const context = useSelectContext(VIEWPORT_NAME, __scopeSelect)
277
- const { style, ...floatingProps } = context.interactions.getFloatingProps()
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
278
316
  delete style['scrollbarWidth']
279
317
  delete style['listStyleType']
318
+
280
319
  return (
281
320
  <>
282
321
  {/* Hide scrollbars cross-browser and enable momentum scroll for touch devices */}
@@ -364,7 +403,9 @@ export const SelectItem = React.forwardRef<TamaguiElement, SelectItemProps>(
364
403
  forwardedRef,
365
404
  (node) => {
366
405
  if (node instanceof HTMLElement) {
367
- listRef.current[index] = node
406
+ if (listRef) {
407
+ listRef.current[index] = node
408
+ }
368
409
  }
369
410
  }
370
411
  // isSelected ? context.onSelectedItemChange : undefined
@@ -372,7 +413,7 @@ export const SelectItem = React.forwardRef<TamaguiElement, SelectItemProps>(
372
413
 
373
414
  React.useEffect(() => {
374
415
  setValueAtIndex(index, value)
375
- }, [index])
416
+ }, [index, setValueAtIndex, value])
376
417
 
377
418
  const timeoutRef = React.useRef<any>()
378
419
  const [allowMouseUp, setAllowMouseUp] = React.useState(false)
@@ -399,17 +440,21 @@ export const SelectItem = React.forwardRef<TamaguiElement, SelectItemProps>(
399
440
  }, [open, index, setActiveIndex, selectedIndex])
400
441
 
401
442
  function handleKeyDown(event: React.KeyboardEvent) {
402
- if (event.key === 'Enter' || (event.key === ' ' && !dataRef.current.typing)) {
443
+ if (event.key === 'Enter' || (event.key === ' ' && !dataRef?.current.typing)) {
403
444
  event.preventDefault()
404
445
  handleSelect()
405
446
  }
406
447
  }
407
448
 
408
- const selectItemProps = context.interactions.getItemProps({
409
- onClick: allowMouseUp ? handleSelect : undefined,
410
- onMouseUp: allowMouseUp ? handleSelect : undefined,
411
- onKeyDown: handleKeyDown,
412
- })
449
+ const selectItemProps = context.interactions
450
+ ? context.interactions.getItemProps({
451
+ onClick: allowMouseUp ? handleSelect : undefined,
452
+ onMouseUp: allowMouseUp ? handleSelect : undefined,
453
+ onKeyDown: handleKeyDown,
454
+ })
455
+ : {
456
+ onPress: handleSelect,
457
+ }
413
458
 
414
459
  return (
415
460
  <SelectItemContextProvider
@@ -461,8 +506,7 @@ type SelectItemTextProps = GetProps<typeof SelectItemTextFrame>
461
506
 
462
507
  const SelectItemText = React.forwardRef<TamaguiElement, SelectItemTextProps>(
463
508
  (props: ScopedProps<SelectItemTextProps>, forwardedRef) => {
464
- // We ignore `className` and `style` as this part shouldn't be styled.
465
- const { __scopeSelect, className, style, ...itemTextProps } = props
509
+ const { __scopeSelect, className, ...itemTextProps } = props
466
510
  const context = useSelectContext(ITEM_TEXT_NAME, __scopeSelect)
467
511
  const itemContext = useSelectItemContext(ITEM_TEXT_NAME, __scopeSelect)
468
512
  const ref = React.useRef<TamaguiElement | null>(null)
@@ -471,6 +515,7 @@ const SelectItemText = React.forwardRef<TamaguiElement, SelectItemTextProps>(
471
515
  return (
472
516
  <>
473
517
  <SelectItemTextFrame
518
+ className={className}
474
519
  size={context.size}
475
520
  id={itemContext.textId}
476
521
  {...itemTextProps}
@@ -639,99 +684,102 @@ interface SelectScrollButtonImplProps extends YStackProps {
639
684
  componentName: string
640
685
  }
641
686
 
642
- const SelectScrollButtonImpl = React.forwardRef<
643
- SelectScrollButtonImplElement,
644
- SelectScrollButtonImplProps
645
- >((props: ScopedProps<SelectScrollButtonImplProps>, forwardedRef) => {
646
- const { __scopeSelect, dir, componentName, ...scrollIndicatorProps } = props
647
- const { floatingRef, increaseHeight, forceUpdate, ...context } = useSelectContext(
648
- componentName,
649
- __scopeSelect
650
- )
651
- const intervalRef = React.useRef<any>()
652
- const loopingRef = React.useRef(false)
653
- const isVisible = context[dir === 'down' ? 'canScrollDown' : 'canScrollUp']
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
+ })
654
704
 
655
- const { x, y, reference, floating, strategy, update, refs } = useFloating({
656
- strategy: 'fixed',
657
- placement: dir === 'up' ? 'top' : 'bottom',
658
- middleware: [offset(({ rects }) => -rects.floating.height)],
659
- })
705
+ const composedRefs = useComposedRefs(forwardedRef, floating)
660
706
 
661
- const composedRefs = useComposedRefs(forwardedRef, floating)
707
+ React.useLayoutEffect(() => {
708
+ if (!floatingRef) return
709
+ reference(floatingRef.current)
710
+ // eslint-disable-next-line react-hooks/exhaustive-deps
711
+ }, [reference, floatingRef?.current])
662
712
 
663
- React.useLayoutEffect(() => {
664
- reference(floatingRef.current)
665
- // eslint-disable-next-line react-hooks/exhaustive-deps
666
- }, [reference, floatingRef.current])
713
+ React.useEffect(() => {
714
+ if (!refs.reference.current || !refs.floating.current || !isVisible) {
715
+ return
716
+ }
667
717
 
668
- React.useEffect(() => {
669
- if (!refs.reference.current || !refs.floating.current || !isVisible) {
670
- return
671
- }
718
+ const cleanup = autoUpdate(refs.reference.current, refs.floating.current, update, {
719
+ animationFrame: true,
720
+ })
672
721
 
673
- const cleanup = autoUpdate(refs.reference.current, refs.floating.current, update, {
674
- animationFrame: true,
675
- })
722
+ return () => {
723
+ clearInterval(intervalRef.current)
724
+ loopingRef.current = false
725
+ cleanup()
726
+ }
727
+ }, [isVisible, update, refs.floating, refs.reference])
676
728
 
677
- return () => {
678
- clearInterval(intervalRef.current)
679
- loopingRef.current = false
680
- cleanup()
681
- }
682
- }, [isVisible, update, refs.floating, refs.reference])
729
+ if (!isVisible) {
730
+ return null
731
+ }
683
732
 
684
- if (!isVisible) {
685
- return null
686
- }
733
+ const handleScrollArrowChange = () => {
734
+ if (!floatingRef) return
735
+ const floating = floatingRef.current
736
+ const isUp = dir === 'up'
687
737
 
688
- const handleScrollArrowChange = () => {
689
- const floating = floatingRef.current
690
- const isUp = dir === 'up'
691
-
692
- if (floating) {
693
- const value = isUp ? -SCROLL_ARROW_VELOCITY : SCROLL_ARROW_VELOCITY
694
- const multi =
695
- (isUp && floating.scrollTop <= SCROLL_ARROW_THRESHOLD * 2) ||
696
- (!isUp &&
697
- floating.scrollTop >=
698
- floating.scrollHeight - floating.clientHeight - SCROLL_ARROW_THRESHOLD * 2)
699
- ? 2
700
- : 1
701
-
702
- floating.scrollTop += multi * (isUp ? -SCROLL_ARROW_VELOCITY : SCROLL_ARROW_VELOCITY)
703
-
704
- increaseHeight(floating, multi === 2 ? value * 2 : value)
705
- // Ensure derived data (scroll arrows) is fresh
706
- forceUpdate()
707
- }
708
- }
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
709
747
 
710
- return (
711
- <YStack
712
- ref={composedRefs}
713
- componentName={componentName}
714
- aria-hidden
715
- {...scrollIndicatorProps}
716
- zIndex={1000}
717
- // @ts-expect-error
718
- position={strategy}
719
- left={x || 0}
720
- top={y || 0}
721
- width={`calc(${(floatingRef.current?.offsetWidth ?? 0) - 2}px)`}
722
- onPointerMove={() => {
723
- if (!loopingRef.current) {
724
- intervalRef.current = setInterval(handleScrollArrowChange, 1000 / 60)
725
- loopingRef.current = true
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()
726
753
  }
727
- }}
728
- onPointerLeave={() => {
729
- loopingRef.current = false
730
- clearInterval(intervalRef.current)
731
- }}
732
- />
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
+ }
733
781
  )
734
- })
782
+ )
735
783
 
736
784
  /* -------------------------------------------------------------------------------------------------
737
785
  * SelectSeparator
@@ -741,539 +789,622 @@ export const SelectSeparator = styled(Separator, {
741
789
  name: 'SelectSeparator',
742
790
  })
743
791
 
744
- /* -------------------------------------------------------------------------------------------------
745
- * Select
746
- * -----------------------------------------------------------------------------------------------*/
792
+ const useSelectBreakpointActive = (sheetBreakpoint: SelectContextValue['sheetBreakpoint']) => {
793
+ const media = useMedia()
794
+ return sheetBreakpoint ? media[sheetBreakpoint] : false
795
+ }
747
796
 
748
- export interface SelectProps {
749
- id?: string
750
- children?: React.ReactNode
751
- value?: string
752
- defaultValue?: string
753
- onValueChange?(value: string): void
754
- open?: boolean
755
- defaultOpen?: boolean
756
- onOpenChange?(open: boolean): void
757
- dir?: Direction
758
- name?: string
759
- autoComplete?: string
760
- size?: SizeTokens
797
+ const useShowSelectSheet = (context: SelectContextValue) => {
798
+ const breakpointActive = useSelectBreakpointActive(context.sheetBreakpoint)
799
+ return context.open === false ? false : breakpointActive
761
800
  }
762
801
 
763
- export const Select = withStaticProperties(
764
- (props: ScopedProps<SelectProps>) => {
765
- const {
766
- // TODO use id for focusing from label
767
- id,
768
- __scopeSelect,
769
- children,
770
- open: openProp,
771
- defaultOpen,
772
- onOpenChange,
773
- value: valueProp,
774
- defaultValue,
775
- onValueChange,
776
- size: sizeProp,
777
- dir,
778
- name,
779
- autoComplete,
780
- } = props
802
+ const SelectSheetController = (
803
+ props: ScopedProps<{}> & {
804
+ children: React.ReactNode
805
+ onChangeOpen: React.Dispatch<React.SetStateAction<boolean>>
806
+ }
807
+ ) => {
808
+ const context = useSelectContext('SelectSheetController', props.__scopeSelect)
809
+ const showSheet = useShowSelectSheet(context)
810
+ const breakpointActive = useSelectBreakpointActive(context.sheetBreakpoint)
811
+ const getShowSheet = useGet(showSheet)
781
812
 
782
- // const [trigger, setTrigger] = React.useState<SelectTriggerElement | null>(null)
813
+ return (
814
+ <SheetController
815
+ onChangeOpen={(val) => {
816
+ if (getShowSheet()) {
817
+ props.onChangeOpen(val)
818
+ }
819
+ }}
820
+ open={context.open}
821
+ hidden={breakpointActive === false}
822
+ >
823
+ {props.children}
824
+ </SheetController>
825
+ )
826
+ }
783
827
 
784
- const [open, setOpen] = useControllableState({
785
- prop: openProp,
786
- defaultProp: defaultOpen || false,
787
- onChange: onOpenChange,
788
- })
828
+ /* -------------------------------------------------------------------------------------------------
829
+ * SelectSheetContents
830
+ * -----------------------------------------------------------------------------------------------*/
789
831
 
790
- const [value, setValue] = useControllableState({
791
- prop: valueProp,
792
- defaultProp: defaultValue || '',
793
- onChange: onValueChange,
794
- })
832
+ const SHEET_CONTENTS_NAME = 'SelectSheetContents'
795
833
 
796
- const [activeIndex, setActiveIndex] = React.useState<number | null>(null)
797
- const selectedIndexRef = React.useRef<number | null>(null)
798
- const activeIndexRef = React.useRef<number | null>(null)
799
- const prevActiveIndex = usePrevious<number | null>(activeIndex)
834
+ export const SelectSheetContents = ({ __scopeSelect }: ScopedProps<{}>) => {
835
+ const context = useSelectContext(SHEET_CONTENTS_NAME, __scopeSelect)
836
+ return <PortalHost name={`${context.scopeKey}SheetContents`} />
837
+ }
800
838
 
801
- const [showArrows, setShowArrows] = React.useState(false)
802
- const [scrollTop, setScrollTop] = React.useState(0)
803
- const listItemsRef = React.useRef<Array<HTMLElement | null>>([])
804
- const listContentRef = React.useRef<string[]>([])
839
+ SelectSheetContents.displayName = SHEET_CONTENTS_NAME
805
840
 
806
- const [selectedIndex, setSelectedIndex] = React.useState(
807
- Math.max(0, listContentRef.current.indexOf(value))
808
- )
841
+ /* -------------------------------------------------------------------------------------------------
842
+ * Select
843
+ * -----------------------------------------------------------------------------------------------*/
809
844
 
810
- const [controlledScrolling, setControlledScrolling] = React.useState(false)
811
- const [middlewareType, setMiddlewareType] = React.useState<'align' | 'fallback'>('align')
845
+ type SelectImplProps = ScopedProps<SelectProps> & {
846
+ activeIndexRef: any
847
+ selectedIndexRef: any
848
+ listContentRef: any
849
+ }
812
850
 
813
- useIsomorphicLayoutEffect(() => {
814
- selectedIndexRef.current = selectedIndex
815
- activeIndexRef.current = activeIndex
816
- })
851
+ // TODO use id for focusing from label
817
852
 
818
- // Wait for scroll position to settle before showing arrows to prevent
819
- // interference with pointer events.
820
- React.useEffect(() => {
821
- const frame = requestAnimationFrame(() => {
822
- setShowArrows(open)
823
-
824
- if (!open) {
825
- setScrollTop(0)
826
- setMiddlewareType('align')
827
- setActiveIndex(null)
828
- setControlledScrolling(false)
829
- }
830
- })
831
- return () => {
832
- cancelAnimationFrame(frame)
833
- }
834
- }, [open])
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>>([])
835
871
 
836
- function getFloatingPadding(floating: HTMLElement | null) {
837
- if (!floating) {
838
- return 0
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)
839
886
  }
840
- return Number(getComputedStyle(floating).paddingLeft?.replace('px', ''))
887
+ })
888
+ return () => {
889
+ cancelAnimationFrame(frame)
841
890
  }
891
+ }, [open, setActiveIndex])
842
892
 
843
- const { x, y, reference, floating, strategy, context, refs, middlewareData, update } =
844
- useFloating({
845
- open,
846
- onOpenChange: setOpen,
847
- placement: 'bottom',
848
- middleware:
849
- middlewareType === 'align'
850
- ? [
851
- offset(({ rects }) => {
852
- const index = activeIndexRef.current ?? selectedIndexRef.current
853
-
854
- if (index == null) {
855
- return 0
856
- }
857
-
858
- const item = listItemsRef.current[index]
893
+ function getFloatingPadding(floating: HTMLElement | null) {
894
+ if (!floating) {
895
+ return 0
896
+ }
897
+ return Number(getComputedStyle(floating).paddingLeft?.replace('px', ''))
898
+ }
859
899
 
860
- if (item == null) {
861
- return 0
862
- }
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
+ }
863
914
 
864
- const offsetTop = item.offsetTop
865
- const itemHeight = item.offsetHeight
866
- const height = rects.reference.height
867
-
868
- return -offsetTop - height - (itemHeight - height) / 2
869
- }),
870
- // Custom `size` that can handle the opposite direction of the placement
871
- {
872
- name: 'size',
873
- async fn(args) {
874
- const {
875
- elements: { floating },
876
- rects: { reference },
877
- middlewareData,
878
- } = args
879
-
880
- const overflow = await detectOverflow(args, {
881
- padding: WINDOW_PADDING,
882
- })
883
-
884
- const top = Math.max(0, overflow.top)
885
- const bottom = Math.max(0, overflow.bottom)
886
- const nextY = args.y + top
887
-
888
- if (middlewareData.size?.skip) {
889
- return {
890
- y: nextY,
891
- data: {
892
- y: middlewareData.size.y,
893
- },
894
- }
895
- }
915
+ const item = listItemsRef.current[index]
896
916
 
897
- Object.assign(floating.style, {
898
- maxHeight: `${floating.scrollHeight - Math.abs(top + bottom)}px`,
899
- minWidth: `${reference.width + getFloatingPadding(floating) * 2}px`,
900
- })
917
+ if (item == null) {
918
+ return 0
919
+ }
901
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) {
902
946
  return {
903
947
  y: nextY,
904
948
  data: {
905
- y: top,
906
- skip: true,
907
- },
908
- reset: {
909
- rects: true,
949
+ y: middlewareData.size.y,
910
950
  },
911
951
  }
912
- },
913
- },
914
- ]
915
- : [
916
- offset(5),
917
- flip(),
918
- size({
919
- apply({ rects, availableHeight, elements }) {
920
- Object.assign(elements.floating.style, {
921
- width: `${rects.reference.width}px`,
922
- maxHeight: `${availableHeight}px`,
923
- })
924
- },
925
- padding: WINDOW_PADDING,
926
- }),
927
- ],
928
- })
952
+ }
929
953
 
930
- const floatingRef = refs.floating
931
- const forceUpdate = React.useReducer(() => ({}), {})[1]
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
+ })
932
986
 
933
- const showUpArrow = showArrows && scrollTop > SCROLL_ARROW_THRESHOLD
934
- const showDownArrow =
935
- showArrows &&
936
- floatingRef.current &&
937
- scrollTop <
938
- floatingRef.current.scrollHeight - floatingRef.current.clientHeight - SCROLL_ARROW_THRESHOLD
939
-
940
- const interactions = useInteractions([
941
- useClick(context, { pointerDown: true }),
942
- useRole(context, { role: 'listbox' }),
943
- useDismiss(context),
944
- useListNavigation(context, {
945
- listRef: listItemsRef,
946
- activeIndex,
947
- selectedIndex,
948
- onNavigate: setActiveIndex,
949
- }),
950
- useTypeahead(context, {
951
- listRef: listContentRef,
952
- onMatch: open ? setActiveIndex : setSelectedIndex,
953
- selectedIndex,
954
- activeIndex,
955
- }),
956
- ])
957
-
958
- const increaseHeight = React.useCallback(
959
- (floating: HTMLElement, amount = 0) => {
960
- if (middlewareType === 'fallback') {
961
- return
962
- }
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
+ ])
963
1013
 
964
- const currentMaxHeight = Number(floating.style.maxHeight.replace('px', ''))
965
- const currentTop = Number(floating.style.top.replace('px', ''))
966
- const rect = floating.getBoundingClientRect()
967
- const rectTop = rect.top
968
- const rectBottom = rect.bottom
969
- const viewportHeight = visualViewport?.height ?? 0
970
- const visualMaxHeight = viewportHeight - WINDOW_PADDING * 2
971
-
972
- if (
973
- amount < 0 &&
974
- selectedIndexRef.current != null &&
975
- Math.round(rectBottom) <
976
- Math.round(viewportHeight + getVisualOffsetTop() - WINDOW_PADDING)
977
- ) {
978
- floating.style.maxHeight = `${Math.min(visualMaxHeight, currentMaxHeight - amount)}px`
979
- }
1014
+ const increaseHeight = React.useCallback(
1015
+ (floating: HTMLElement, amount = 0) => {
1016
+ if (middlewareType === 'fallback') {
1017
+ return
1018
+ }
980
1019
 
981
- if (
982
- amount > 0 &&
983
- Math.round(rectTop) > Math.round(WINDOW_PADDING - getVisualOffsetTop()) &&
984
- floating.scrollHeight > floating.offsetHeight
985
- ) {
986
- const nextTop = Math.max(WINDOW_PADDING + getVisualOffsetTop(), currentTop - amount)
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
+ }
987
1035
 
988
- const nextMaxHeight = Math.min(visualMaxHeight, currentMaxHeight + amount)
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)
989
1042
 
990
- Object.assign(floating.style, {
991
- maxHeight: `${nextMaxHeight}px`,
992
- top: `${nextTop}px`,
993
- })
1043
+ const nextMaxHeight = Math.min(visualMaxHeight, currentMaxHeight + amount)
994
1044
 
995
- if (nextTop - WINDOW_PADDING > getVisualOffsetTop()) {
996
- floating.scrollTop -= nextMaxHeight - currentMaxHeight + getFloatingPadding(floating)
997
- }
1045
+ Object.assign(floating.style, {
1046
+ maxHeight: `${nextMaxHeight}px`,
1047
+ top: `${nextTop}px`,
1048
+ })
998
1049
 
999
- return currentTop - nextTop
1050
+ if (nextTop - WINDOW_PADDING > getVisualOffsetTop()) {
1051
+ floating.scrollTop -= nextMaxHeight - currentMaxHeight + getFloatingPadding(floating)
1000
1052
  }
1001
- },
1002
- [middlewareType]
1003
- )
1004
1053
 
1005
- const touchPageYRef = React.useRef<number | null>(null)
1054
+ return currentTop - nextTop
1055
+ }
1056
+ },
1057
+ [middlewareType, selectedIndexRef]
1058
+ )
1006
1059
 
1007
- const handleWheel = React.useCallback(
1008
- (event: WheelEvent | TouchEvent) => {
1009
- const pinching = event.ctrlKey
1060
+ const touchPageYRef = React.useRef<number | null>(null)
1010
1061
 
1011
- const currentTarget = event.currentTarget as HTMLElement
1062
+ const handleWheel = React.useCallback(
1063
+ (event: WheelEvent | TouchEvent) => {
1064
+ const pinching = event.ctrlKey
1012
1065
 
1013
- function isWheelEvent(event: any): event is WheelEvent {
1014
- return typeof event.deltaY === 'number'
1015
- }
1066
+ const currentTarget = event.currentTarget as HTMLElement
1016
1067
 
1017
- function isTouchEvent(event: any): event is TouchEvent {
1018
- return event.touches != null
1019
- }
1068
+ function isWheelEvent(event: any): event is WheelEvent {
1069
+ return typeof event.deltaY === 'number'
1070
+ }
1020
1071
 
1021
- if (
1022
- Math.abs(
1023
- (currentTarget?.offsetHeight ?? 0) -
1024
- ((visualViewport?.height ?? 0) - WINDOW_PADDING * 2)
1025
- ) > 1 &&
1026
- !pinching
1027
- ) {
1028
- event.preventDefault()
1029
- } else if (isWheelEvent(event) && isFirefox) {
1030
- // Firefox needs this to propagate scrolling
1031
- // during momentum scrolling phase if the
1032
- // height reached its maximum (at boundaries)
1033
- currentTarget.scrollTop += event.deltaY
1034
- }
1072
+ function isTouchEvent(event: any): event is TouchEvent {
1073
+ return event.touches != null
1074
+ }
1035
1075
 
1036
- if (!pinching) {
1037
- let delta = 5
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
+ }
1038
1089
 
1039
- if (isTouchEvent(event)) {
1040
- const currentPageY = touchPageYRef.current
1041
- const pageY = event.touches[0]?.pageY
1090
+ if (!pinching) {
1091
+ let delta = 5
1042
1092
 
1043
- if (pageY != null) {
1044
- touchPageYRef.current = pageY
1093
+ if (isTouchEvent(event)) {
1094
+ const currentPageY = touchPageYRef.current
1095
+ const pageY = event.touches[0]?.pageY
1045
1096
 
1046
- if (currentPageY != null) {
1047
- delta = currentPageY - pageY
1048
- }
1097
+ if (pageY != null) {
1098
+ touchPageYRef.current = pageY
1099
+
1100
+ if (currentPageY != null) {
1101
+ delta = currentPageY - pageY
1049
1102
  }
1050
1103
  }
1051
-
1052
- increaseHeight(currentTarget, isWheelEvent(event) ? event.deltaY : delta)
1053
- setScrollTop(currentTarget.scrollTop)
1054
- // Ensure derived data (scroll arrows) is fresh
1055
- forceUpdate()
1056
1104
  }
1057
- },
1058
- [increaseHeight, forceUpdate]
1059
- )
1060
1105
 
1061
- // Handle `onWheel` event in an effect to remove the `passive` option so we
1062
- // can .preventDefault() it
1063
- React.useEffect(() => {
1064
- function onTouchEnd() {
1065
- touchPageYRef.current = null
1106
+ increaseHeight(currentTarget, isWheelEvent(event) ? event.deltaY : delta)
1107
+ setScrollTop(currentTarget.scrollTop)
1108
+ // Ensure derived data (scroll arrows) is fresh
1109
+ forceUpdate()
1066
1110
  }
1111
+ },
1112
+ [forceUpdate, increaseHeight]
1113
+ )
1067
1114
 
1068
- const floating = floatingRef.current
1069
- if (open && floating && middlewareType === 'align') {
1070
- floating.addEventListener('wheel', handleWheel)
1071
- floating.addEventListener('touchmove', handleWheel)
1072
- floating.addEventListener('touchend', onTouchEnd, { passive: true })
1073
- return () => {
1074
- floating.removeEventListener('wheel', handleWheel)
1075
- floating.removeEventListener('touchmove', handleWheel)
1076
- floating.removeEventListener('touchend', onTouchEnd)
1077
- }
1078
- }
1079
- }, [open, floatingRef, handleWheel, middlewareType])
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
+ }
1080
1121
 
1081
- // Ensure the menu remains attached to the reference element when resizing.
1082
- React.useEffect(() => {
1083
- window.addEventListener('resize', update)
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 })
1084
1127
  return () => {
1085
- window.removeEventListener('resize', update)
1128
+ floating.removeEventListener('wheel', handleWheel)
1129
+ floating.removeEventListener('touchmove', handleWheel)
1130
+ floating.removeEventListener('touchend', onTouchEnd)
1086
1131
  }
1087
- }, [update])
1132
+ }
1133
+ }, [open, floatingRef, handleWheel, middlewareType])
1088
1134
 
1089
- // Scroll the active or selected item into view when in `controlledScrolling`
1090
- // mode (i.e. arrow key nav).
1091
- React.useLayoutEffect(() => {
1092
- const floating = floatingRef.current
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])
1093
1142
 
1094
- if (open && controlledScrolling && floating) {
1095
- const item =
1096
- activeIndex != null
1097
- ? listItemsRef.current[activeIndex]
1098
- : selectedIndex != null
1099
- ? listItemsRef.current[selectedIndex]
1100
- : null
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
1101
1147
 
1102
- if (item && prevActiveIndex != null) {
1103
- const itemHeight = listItemsRef.current[prevActiveIndex]?.offsetHeight ?? 0
1148
+ if (open && controlledScrolling && floating) {
1149
+ const item =
1150
+ activeIndex != null
1151
+ ? listItemsRef.current[activeIndex]
1152
+ : selectedIndex != null
1153
+ ? listItemsRef.current[selectedIndex]
1154
+ : null
1104
1155
 
1105
- const floatingHeight = floating.offsetHeight
1106
- const top = item.offsetTop
1107
- const bottom = top + itemHeight
1156
+ if (item && prevActiveIndex != null) {
1157
+ const itemHeight = listItemsRef.current[prevActiveIndex]?.offsetHeight ?? 0
1108
1158
 
1109
- if (top < floating.scrollTop + 20) {
1110
- const diff = floating.scrollTop - top + 20
1111
- floating.scrollTop -= diff
1159
+ const floatingHeight = floating.offsetHeight
1160
+ const top = item.offsetTop
1161
+ const bottom = top + itemHeight
1112
1162
 
1113
- if (activeIndex != selectedIndex && activeIndex != null) {
1114
- increaseHeight(floating, -diff)
1115
- }
1116
- } else if (bottom > floatingHeight + floating.scrollTop - 20) {
1117
- const diff = bottom - floatingHeight - floating.scrollTop + 20
1163
+ if (top < floating.scrollTop + 20) {
1164
+ const diff = floating.scrollTop - top + 20
1165
+ floating.scrollTop -= diff
1118
1166
 
1119
- floating.scrollTop += diff
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
1120
1172
 
1121
- if (activeIndex != selectedIndex && activeIndex != null) {
1122
- floating.scrollTop -= increaseHeight(floating, diff) ?? 0
1123
- }
1173
+ floating.scrollTop += diff
1174
+
1175
+ if (activeIndex != selectedIndex && activeIndex != null) {
1176
+ floating.scrollTop -= increaseHeight(floating, diff) ?? 0
1124
1177
  }
1125
1178
  }
1126
1179
  }
1127
- }, [
1128
- open,
1129
- controlledScrolling,
1130
- prevActiveIndex,
1131
- activeIndex,
1132
- selectedIndex,
1133
- floatingRef,
1134
- increaseHeight,
1135
- ])
1136
-
1137
- // Sync the height and the scrollTop values and device whether to use fallback
1138
- // positioning.
1139
- React.useLayoutEffect(() => {
1140
- const floating = refs.floating.current
1141
- const reference = refs.reference.current
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
1142
1196
 
1143
- if (open && floating && reference && floating.offsetHeight < floating.scrollHeight) {
1144
- const referenceRect = reference.getBoundingClientRect()
1197
+ if (open && floating && reference && floating.offsetHeight < floating.scrollHeight) {
1198
+ const referenceRect = reference.getBoundingClientRect()
1145
1199
 
1146
- if (middlewareType === 'fallback') {
1147
- const item = listItemsRef.current[selectedIndex]
1148
- if (item) {
1149
- floating.scrollTop = item.offsetTop - floating.clientHeight + referenceRect.height
1150
- }
1151
- return
1200
+ if (middlewareType === 'fallback') {
1201
+ const item = listItemsRef.current[selectedIndex]
1202
+ if (item) {
1203
+ floating.scrollTop = item.offsetTop - floating.clientHeight + referenceRect.height
1152
1204
  }
1205
+ return
1206
+ }
1153
1207
 
1154
- floating.scrollTop = middlewareData.size?.y
1208
+ floating.scrollTop = middlewareData.size?.y
1155
1209
 
1156
- const closeToBottom =
1157
- (visualViewport?.height ?? 0) + getVisualOffsetTop() - referenceRect.bottom <
1158
- FALLBACK_THRESHOLD
1159
- const closeToTop = referenceRect.top < FALLBACK_THRESHOLD
1210
+ const closeToBottom =
1211
+ (visualViewport?.height ?? 0) + getVisualOffsetTop() - referenceRect.bottom <
1212
+ FALLBACK_THRESHOLD
1213
+ const closeToTop = referenceRect.top < FALLBACK_THRESHOLD
1160
1214
 
1161
- if (floating.offsetHeight < MIN_HEIGHT || closeToTop || closeToBottom) {
1162
- setMiddlewareType('fallback')
1163
- }
1215
+ if (floating.offsetHeight < MIN_HEIGHT || closeToTop || closeToBottom) {
1216
+ setMiddlewareType('fallback')
1164
1217
  }
1165
- }, [
1166
- open,
1167
- increaseHeight,
1168
- selectedIndex,
1169
- middlewareType,
1170
- refs.floating,
1171
- refs.reference,
1172
- // Always re-run this effect when the position has been computed so the
1173
- // .scrollTop change works with fresh sizing.
1174
- middlewareData,
1175
- ])
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
+ ])
1176
1230
 
1177
- React.useLayoutEffect(() => {
1178
- if (open && selectedIndex != null) {
1179
- requestAnimationFrame(() => {
1180
- listItemsRef.current[selectedIndex]?.focus({ preventScroll: true })
1181
- })
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)
1182
1250
  }
1183
- }, [listItemsRef, selectedIndex, open])
1251
+ })
1252
+ return () => cancelAnimationFrame(frame)
1253
+ }, [open])
1184
1254
 
1185
- // Wait for scroll position to settle before showing arrows to prevent
1186
- // interference with pointer events.
1187
- React.useEffect(() => {
1188
- const frame = requestAnimationFrame(() => {
1189
- setShowArrows(open)
1190
-
1191
- if (!open) {
1192
- setScrollTop(0)
1193
- setMiddlewareType('align')
1194
- setActiveIndex(null)
1195
- setControlledScrolling(false)
1196
- }
1197
- })
1198
- return () => cancelAnimationFrame(frame)
1199
- }, [open])
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)
1200
1259
 
1201
- // We set this to true by default so that events bubble to forms without JS (SSR)
1202
- // const isFormControl = trigger ? Boolean(trigger.closest('form')) : true
1203
- // const [bubbleSelect, setBubbleSelect] = React.useState<HTMLSelectElement | null>(null)
1204
- // const triggerPointerDownPosRef = React.useRef<{ x: number; y: number } | null>(null)
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
+ const SelectSheetImpl = (props: SelectImplProps) => {
1339
+ return <>{props.children}</>
1340
+ }
1341
+
1342
+ export const Select = withStaticProperties(
1343
+ (props: ScopedProps<SelectProps>) => {
1344
+ const {
1345
+ __scopeSelect,
1346
+ children,
1347
+ open: openProp,
1348
+ defaultOpen,
1349
+ onOpenChange,
1350
+ value: valueProp,
1351
+ defaultValue,
1352
+ onValueChange,
1353
+ size: sizeProp,
1354
+ sheetBreakpoint = false,
1355
+ dir,
1356
+ } = props
1357
+
1358
+ const isSheet = useSelectBreakpointActive(sheetBreakpoint)
1359
+ const SelectImpl = isSheet ? SelectSheetImpl : SelectInlineImpl
1360
+ const forceUpdate = React.useReducer(() => ({}), {})[1]
1361
+
1362
+ const [open, setOpen] = useControllableState({
1363
+ prop: openProp,
1364
+ defaultProp: defaultOpen || false,
1365
+ onChange: onOpenChange,
1366
+ })
1367
+
1368
+ const [value, setValue] = useControllableState({
1369
+ prop: valueProp,
1370
+ defaultProp: defaultValue || '',
1371
+ onChange: onValueChange,
1372
+ })
1373
+
1374
+ const [activeIndex, setActiveIndex] = React.useState<number | null>(null)
1375
+ const selectedIndexRef = React.useRef<number | null>(null)
1376
+ const activeIndexRef = React.useRef<number | null>(null)
1377
+
1378
+ const listContentRef = React.useRef<string[]>([])
1379
+
1380
+ const [selectedIndex, setSelectedIndex] = React.useState(
1381
+ Math.max(0, listContentRef.current.indexOf(value))
1382
+ )
1205
1383
 
1206
1384
  const [valueNode, setValueNode] = React.useState<HTMLElement | null>(null)
1207
1385
  const [valueNodeHasChildren, setValueNodeHasChildren] = React.useState(false)
1208
1386
 
1387
+ useIsomorphicLayoutEffect(() => {
1388
+ selectedIndexRef.current = selectedIndex
1389
+ activeIndexRef.current = activeIndex
1390
+ })
1391
+
1209
1392
  return (
1210
1393
  <SelectProvider
1394
+ dir={dir}
1211
1395
  size={sizeProp}
1212
- scope={__scopeSelect}
1213
- increaseHeight={increaseHeight}
1214
1396
  forceUpdate={forceUpdate}
1215
- floatingRef={floatingRef}
1216
1397
  valueNode={valueNode}
1217
1398
  onValueNodeChange={setValueNode}
1218
- valueNodeHasChildren={valueNodeHasChildren}
1219
1399
  onValueNodeHasChildrenChange={setValueNodeHasChildren}
1400
+ valueNodeHasChildren={valueNodeHasChildren}
1401
+ scopeKey={__scopeSelect ? Object.keys(__scopeSelect)[0] : ''}
1402
+ sheetBreakpoint={sheetBreakpoint}
1403
+ scope={__scopeSelect}
1220
1404
  setValueAtIndex={(index, value) => {
1221
1405
  listContentRef.current[index] = value
1222
1406
  }}
1223
- interactions={{
1224
- ...interactions,
1225
- getReferenceProps() {
1226
- return interactions.getReferenceProps({
1227
- ref: reference,
1228
- className: 'SelectTrigger',
1229
- onKeyDown(event) {
1230
- if (
1231
- event.key === 'Enter' ||
1232
- (event.key === ' ' && !context.dataRef.current.typing)
1233
- ) {
1234
- event.preventDefault()
1235
- setOpen(true)
1236
- }
1237
- },
1238
- })
1239
- },
1240
- getFloatingProps(props) {
1241
- return interactions.getFloatingProps({
1242
- ref: floating,
1243
- className: 'Select',
1244
- ...props,
1245
- style: {
1246
- position: strategy,
1247
- top: y ?? '',
1248
- left: x ?? '',
1249
- outline: 0,
1250
- listStyleType: 'none',
1251
- scrollbarWidth: 'none',
1252
- userSelect: 'none',
1253
- ...props?.style,
1254
- },
1255
- onPointerEnter() {
1256
- setControlledScrolling(false)
1257
- },
1258
- onPointerMove() {
1259
- setControlledScrolling(false)
1260
- },
1261
- onKeyDown() {
1262
- setControlledScrolling(true)
1263
- },
1264
- onScroll(event) {
1265
- setScrollTop(event.currentTarget.scrollTop)
1266
- },
1267
- })
1268
- },
1269
- }}
1270
- floatingContext={context}
1271
1407
  activeIndex={activeIndex}
1272
- canScrollDown={!!showDownArrow}
1273
- canScrollUp={!!showUpArrow}
1274
- controlledScrolling
1275
- dataRef={context.dataRef}
1276
- listRef={listItemsRef}
1277
1408
  onChange={setValue}
1278
1409
  selectedIndex={selectedIndex}
1279
1410
  setActiveIndex={setActiveIndex}
@@ -1282,19 +1413,18 @@ export const Select = withStaticProperties(
1282
1413
  value={value}
1283
1414
  open={open}
1284
1415
  >
1285
- {children}
1286
- {/* {isFormControl ? (
1287
- <BubbleSelect
1288
- ref={setBubbleSelect}
1289
- aria-hidden
1290
- tabIndex={-1}
1291
- name={name}
1292
- autoComplete={autoComplete}
1293
- value={value}
1294
- // enable form autofill
1295
- onChange={(event) => setValue(event.target.value)}
1296
- />
1297
- ) : null} */}
1416
+ <SelectSheetController onChangeOpen={setOpen} __scopeSelect={__scopeSelect}>
1417
+ <SelectImpl
1418
+ activeIndexRef={activeIndexRef}
1419
+ listContentRef={listContentRef}
1420
+ selectedIndexRef={selectedIndexRef}
1421
+ {...props}
1422
+ open={open}
1423
+ value={value}
1424
+ >
1425
+ {children}
1426
+ </SelectImpl>
1427
+ </SelectSheetController>
1298
1428
  </SelectProvider>
1299
1429
  )
1300
1430
  },
@@ -1311,6 +1441,8 @@ export const Select = withStaticProperties(
1311
1441
  Trigger: SelectTrigger,
1312
1442
  Value: SelectValue,
1313
1443
  Viewport: SelectViewport,
1444
+ SheetContents: SelectSheetContents,
1445
+ Sheet,
1314
1446
  }
1315
1447
  )
1316
1448