@tamagui/popover 2.7.7 → 3.0.0-beta.1091.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/cjs/Popover.cjs +522 -665
  2. package/dist/cjs/Popover.native.cjs +615 -0
  3. package/dist/cjs/Popover.native.js +549 -692
  4. package/dist/cjs/Popover.native.js.map +1 -1
  5. package/dist/cjs/index.cjs +9 -11
  6. package/dist/cjs/index.native.cjs +21 -0
  7. package/dist/cjs/index.native.js +9 -10
  8. package/dist/cjs/index.native.js.map +1 -1
  9. package/dist/cjs/useFloatingContext.cjs +213 -234
  10. package/dist/cjs/useFloatingContext.native.cjs +35 -0
  11. package/dist/cjs/useFloatingContext.native.js +19 -27
  12. package/dist/cjs/useFloatingContext.native.js.map +1 -1
  13. package/dist/esm/Popover.mjs +487 -618
  14. package/dist/esm/Popover.mjs.map +1 -1
  15. package/dist/esm/Popover.native.js +513 -645
  16. package/dist/esm/Popover.native.js.map +1 -1
  17. package/dist/esm/index.js +3 -3
  18. package/dist/esm/index.mjs +3 -3
  19. package/dist/esm/index.native.js +3 -3
  20. package/dist/esm/useFloatingContext.mjs +198 -208
  21. package/dist/esm/useFloatingContext.mjs.map +1 -1
  22. package/dist/esm/useFloatingContext.native.js +4 -2
  23. package/dist/esm/useFloatingContext.native.js.map +1 -1
  24. package/dist/jsx/Popover.mjs +487 -618
  25. package/dist/jsx/Popover.mjs.map +1 -1
  26. package/dist/jsx/Popover.native.cjs +615 -0
  27. package/dist/jsx/Popover.native.js +549 -692
  28. package/dist/jsx/Popover.native.js.map +1 -1
  29. package/dist/jsx/index.js +3 -3
  30. package/dist/jsx/index.mjs +3 -3
  31. package/dist/jsx/index.native.cjs +21 -0
  32. package/dist/jsx/index.native.js +9 -10
  33. package/dist/jsx/index.native.js.map +1 -1
  34. package/dist/jsx/useFloatingContext.mjs +198 -208
  35. package/dist/jsx/useFloatingContext.mjs.map +1 -1
  36. package/dist/jsx/useFloatingContext.native.cjs +35 -0
  37. package/dist/jsx/useFloatingContext.native.js +19 -27
  38. package/dist/jsx/useFloatingContext.native.js.map +1 -1
  39. package/package.json +24 -23
  40. package/src/Popover.tsx +157 -154
  41. package/src/useFloatingContext.tsx +15 -7
  42. package/types/Popover.d.ts +40 -112
  43. package/types/Popover.d.ts.map +1 -1
  44. package/types/useFloatingContext.d.ts +5 -2
  45. package/types/useFloatingContext.d.ts.map +1 -1
  46. package/dist/esm/index.js.map +0 -1
  47. package/dist/esm/index.mjs.map +0 -1
  48. package/dist/esm/index.native.js.map +0 -1
  49. package/dist/jsx/index.js.map +0 -1
  50. package/dist/jsx/index.mjs.map +0 -1
package/src/Popover.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import '@tamagui/polyfill-dev'
2
2
 
3
- import type { UseHoverProps } from '@tamagui/floating'
3
+ import type { OpenChangeReason, UseHoverProps } from '@tamagui/floating'
4
4
  import {
5
5
  Adapt,
6
6
  AdaptParent,
@@ -13,12 +13,13 @@ import { Animate } from '@tamagui/animate'
13
13
  import { ResetPresence } from '@tamagui/animate-presence'
14
14
  import { useComposedRefs } from '@tamagui/compose-refs'
15
15
  import { isWeb, useIsomorphicLayoutEffect } from '@tamagui/constants'
16
- import type { SizeTokens, TamaguiElement, ViewProps } from '@tamagui/core'
16
+ import type { SizeTokens, GetRef, TamaguiElement, ViewProps } from '@tamagui/core'
17
17
  import {
18
+ createStyledHOC,
19
+ createRefComponent,
18
20
  createStyledContext,
19
21
  useCreateShallowSetState,
20
22
  useEvent,
21
- useGet,
22
23
  View,
23
24
  } from '@tamagui/core'
24
25
  import {
@@ -47,7 +48,6 @@ import {
47
48
  import { needsPortalRepropagation, Portal } from '@tamagui/portal'
48
49
  import { RemoveScroll } from '@tamagui/remove-scroll'
49
50
  import { ScrollView, type ScrollViewProps } from '@tamagui/scroll-view'
50
- import { SheetController } from '@tamagui/sheet/controller'
51
51
  import type { YStackProps } from '@tamagui/stacks'
52
52
  import { YStack } from '@tamagui/stacks'
53
53
  import { useControllableState } from '@tamagui/use-controllable-state'
@@ -105,7 +105,8 @@ export type PopoverProps = ScopedPopoverProps<PopperProps> & {
105
105
  hoverable?: boolean | UseHoverProps
106
106
 
107
107
  /**
108
- * Disable focusing behavior on open
108
+ * disable opening when the trigger receives keyboard focus.
109
+ * defaults to true unless hoverable is enabled.
109
110
  */
110
111
  disableFocus?: boolean
111
112
 
@@ -151,6 +152,8 @@ type PopoverContextValue = {
151
152
  anchorTo?: Rect
152
153
  // scoped branches Set for DismissableBranch/Dismissable to share
153
154
  branches: Set<HTMLElement>
155
+ // carry dismissal intent through focus restoration.
156
+ closeReasonRef?: React.RefObject<OpenChangeReason | undefined>
154
157
  }
155
158
 
156
159
  type PopoverTriggerStateSetter = React.Dispatch<React.SetStateAction<boolean>>
@@ -168,23 +171,15 @@ type PopoverTriggerContextValue = {
168
171
 
169
172
  export const PopoverContext = createStyledContext<PopoverContextValue>(
170
173
  // since we always provide this we can avoid setting here
171
- {} as PopoverContextValue,
174
+ {},
172
175
  'Popover__'
173
176
  )
174
177
 
175
178
  // zIndex flows from root Popover prop to PopoverContent portal
176
179
  export const PopoverZIndexContext = React.createContext<number | undefined>(undefined)
177
180
 
178
- // when adapted to a Sheet, tracks whether the sheet has finished sliding out.
179
- // PopoverSheetController flips this true via SheetController.onAnimationComplete
180
- // so PopoverContent can hold its adapted children mounted until the slide-out
181
- // is done, instead of unmounting them on the popup's (passThrough) exit.
182
- // defaults true (= safe to unmount) for any PopoverContent rendered outside a
183
- // PopoverSheetController.
184
- const PopoverAdaptHiddenContext = React.createContext(true)
185
-
186
181
  export const PopoverTriggerContext = createStyledContext<PopoverTriggerContextValue>(
187
- {} as PopoverTriggerContextValue,
182
+ {},
188
183
  'PopoverTrigger__'
189
184
  )
190
185
 
@@ -268,6 +263,7 @@ export type PopoverContextProviderProps = {
268
263
  keepChildrenMounted?: boolean | 'lazy'
269
264
  disableDismissable?: boolean
270
265
  hoverable?: boolean | object
266
+ closeReasonRef?: React.RefObject<OpenChangeReason | undefined>
271
267
  }
272
268
 
273
269
  /**
@@ -293,6 +289,7 @@ export const PopoverContextProvider = React.memo(
293
289
  keepChildrenMounted,
294
290
  disableDismissable,
295
291
  hoverable,
292
+ closeReasonRef,
296
293
  }: PopoverContextProviderProps) => {
297
294
  const [branches] = React.useState(() => new Set<HTMLElement>())
298
295
  const { setActiveTrigger, registerTrigger, unregisterTrigger } =
@@ -318,6 +315,7 @@ export const PopoverContextProvider = React.memo(
318
315
  keepChildrenMounted={keepChildrenMounted}
319
316
  disableDismissable={disableDismissable}
320
317
  hoverable={hoverable}
318
+ closeReasonRef={closeReasonRef}
321
319
  >
322
320
  <PopoverTriggerContext.Provider
323
321
  scope={scope}
@@ -346,7 +344,7 @@ const voidFn = () => {}
346
344
  export type PopoverAnchorProps = ScopedPopoverProps<YStackProps>
347
345
 
348
346
  export const PopoverAnchor = React.memo(
349
- React.forwardRef<TamaguiElement, PopoverAnchorProps>(
347
+ createRefComponent<TamaguiElement, PopoverAnchorProps>(
350
348
  function PopoverAnchor(props, forwardedRef) {
351
349
  const { scope, ...rest } = props
352
350
  const context = usePopoverContext(scope)
@@ -378,7 +376,7 @@ export type PopoverTriggerProps = ScopedPopoverProps<
378
376
  >
379
377
 
380
378
  export const PopoverTrigger = React.memo(
381
- React.forwardRef<TamaguiElement, PopoverTriggerProps>(
379
+ createRefComponent<TamaguiElement, PopoverTriggerProps>(
382
380
  function PopoverTrigger(props, forwardedRef) {
383
381
  const { scope, disablePressTrigger, ...rest } = props
384
382
  const triggerContext = usePopoverTriggerContext(scope)
@@ -411,8 +409,10 @@ export const PopoverTrigger = React.memo(
411
409
  const trigger = (
412
410
  <View
413
411
  aria-expanded={open}
414
- // TODO not matching
415
- // aria-controls={context.contentId}
412
+ // aria-controls is supplied at runtime by floating-ui's useRole
413
+ // (see useFloatingContext), pointing at the role="dialog" content
414
+ // element while open - no manual wiring is needed or wanted here
415
+ // (a manual id would point at the inner frame, not the dialog element)
416
416
  data-state={getState(open)}
417
417
  {...rest}
418
418
  // @ts-ignore
@@ -484,23 +484,24 @@ export interface PopoverContentTypeProps extends Omit<
484
484
  * Enable smooth animation when the content position changes (e.g., when flipping sides)
485
485
  */
486
486
  animatePosition?: boolean | 'even-when-repositioning'
487
- /** @deprecated Use `animatePosition` instead */
488
- enableAnimationForPositionChange?: boolean
489
487
  }
490
488
 
491
489
  export type PopoverContentProps = PopoverContentTypeProps
492
490
 
493
- export const PopoverContent = PopperContentFrame.styleable<PopoverContentProps>(
494
- function PopoverContent(props, forwardedRef) {
491
+ export const PopoverContent = createStyledHOC(
492
+ PopperContentFrame,
493
+ function PopoverContent(props: PopoverContentProps, forwardedRef) {
495
494
  const {
496
495
  trapFocus,
497
496
  enableRemoveScroll = false,
498
497
  zIndex: zIndexProp,
499
498
  scope,
499
+ forceMount,
500
500
  ...contentImplProps
501
501
  } = props
502
502
 
503
503
  const context = usePopoverContext(scope)
504
+ const adaptContext = useAdaptContext(context.adaptScope)
504
505
  const zIndexFromContext = React.useContext(PopoverZIndexContext)
505
506
  // prop on Content takes precedence for backwards compatibility, then context from root
506
507
  const zIndex = zIndexProp ?? zIndexFromContext
@@ -522,14 +523,13 @@ export const PopoverContent = PopperContentFrame.styleable<PopoverContentProps>(
522
523
 
523
524
  // when adapted to a Sheet, the content is portaled into the sheet via
524
525
  // Adapt.Contents. its mount lifecycle must follow the sheet's slide-out
525
- // (PopoverAdaptHiddenContext, flipped by SheetController.onAnimationComplete),
526
- // NOT the popup's own exit animation (isFullyHidden); the popup runs in
527
- // passThrough mode while adapted, so isFullyHidden either fires immediately
528
- // (content vanishes mid-slide) or never (content leaks), depending on driver.
529
- const isAdaptFullyHidden = React.useContext(PopoverAdaptHiddenContext)
530
- if (!context.keepChildrenMounted) {
526
+ // (adaptContext.targetFullyHidden, driven by the Adapt handoff), NOT the
527
+ // popup's own exit animation (isFullyHidden); the popup runs in passThrough
528
+ // mode while adapted, so isFullyHidden either fires immediately (content
529
+ // vanishes mid-slide) or never (content leaks), depending on driver.
530
+ if (!forceMount && !context.keepChildrenMounted) {
531
531
  if (context.breakpointActive) {
532
- if (!open && isAdaptFullyHidden) {
532
+ if (!open && adaptContext.targetFullyHidden) {
533
533
  return null
534
534
  }
535
535
  } else if (isFullyHidden && !open) {
@@ -542,6 +542,7 @@ export const PopoverContent = PopperContentFrame.styleable<PopoverContentProps>(
542
542
  passThrough={context.breakpointActive}
543
543
  context={context}
544
544
  open={open}
545
+ visuallyActive={open || !isFullyHidden}
545
546
  zIndex={zIndex}
546
547
  >
547
548
  <View
@@ -552,6 +553,7 @@ export const PopoverContent = PopperContentFrame.styleable<PopoverContentProps>(
552
553
  {...contentImplProps}
553
554
  context={context}
554
555
  open={open}
556
+ forceMount={forceMount}
555
557
  enableRemoveScroll={enableRemoveScroll}
556
558
  ref={composedRefs}
557
559
  setIsFullyHidden={setIsFullyHidden}
@@ -564,8 +566,8 @@ export const PopoverContent = PopperContentFrame.styleable<PopoverContentProps>(
564
566
  props.onCloseAutoFocus === false
565
567
  ? undefined
566
568
  : composeEventHandlers(props.onCloseAutoFocus, (event) => {
567
- if (event.defaultPrevented) return
568
- event.preventDefault()
569
+ if (event.isCanceled) return
570
+ event.cancel()
569
571
  if (!isRightClickOutsideRef.current)
570
572
  context.triggerRef.current?.focus()
571
573
  })
@@ -573,7 +575,8 @@ export const PopoverContent = PopperContentFrame.styleable<PopoverContentProps>(
573
575
  onPointerDownOutside={composeEventHandlers(
574
576
  props.onPointerDownOutside,
575
577
  (event) => {
576
- const originalEvent = event.detail.originalEvent
578
+ const originalEvent = event.event
579
+ if (!originalEvent) return
577
580
  const ctrlLeftClick =
578
581
  originalEvent.button === 0 && originalEvent.ctrlKey === true
579
582
  const isRightClick = originalEvent.button === 2 || ctrlLeftClick
@@ -585,13 +588,25 @@ export const PopoverContent = PopperContentFrame.styleable<PopoverContentProps>(
585
588
  // We make sure we don't trigger our `onDismiss` in such case.
586
589
  onFocusOutside={composeEventHandlers(
587
590
  props.onFocusOutside,
588
- (event) => event.preventDefault(),
591
+ (event) => {
592
+ if (trapFocus ?? open) event.cancel()
593
+ },
589
594
  { checkDefaultPrevented: false }
590
595
  )}
591
596
  />
592
597
  </View>
593
598
  </PopoverPortal>
594
599
  )
600
+ },
601
+ {
602
+ staticConfig: {
603
+ inlineProps: new Set([
604
+ ...(PopperContentFrame.staticConfig.inlineProps || []),
605
+ 'zIndex',
606
+ 'opacity',
607
+ 'y',
608
+ ]),
609
+ },
595
610
  }
596
611
  )
597
612
 
@@ -656,6 +671,7 @@ const PortalAdaptSafe = ({
656
671
  function PopoverPortal({
657
672
  context,
658
673
  open,
674
+ visuallyActive,
659
675
  zIndex,
660
676
  passThrough,
661
677
  children,
@@ -663,6 +679,9 @@ function PopoverPortal({
663
679
  }: Pick<PopoverContentProps, 'zIndex' | 'passThrough' | 'children' | 'onPress'> & {
664
680
  context: PopoverContextValue
665
681
  open: boolean
682
+ // true while open or still animating out - hides the portal host when false
683
+ // (matters for keepChildrenMounted/forceMount content, see Portal.tsx)
684
+ visuallyActive: boolean
666
685
  }) {
667
686
  'use no memo'
668
687
 
@@ -677,12 +696,18 @@ function PopoverPortal({
677
696
  }
678
697
 
679
698
  return (
680
- <Portal passThrough={passThrough} stackZIndex zIndex={zIndex}>
699
+ <Portal
700
+ passThrough={passThrough}
701
+ stackZIndex
702
+ zIndex={zIndex}
703
+ hidden={!visuallyActive}
704
+ >
681
705
  {/* forceClassName avoids forced re-mount renders for some reason... see the HeadMenu as you change tints a few times */}
682
706
  {/* without this you'll see the site menu re-rendering. It must be something in wrapping children in Theme */}
683
707
  {!!open && !context.breakpointActive && !context.hoverable && (
684
708
  <YStack
685
- fullscreen
709
+ position="absolute"
710
+ inset={0}
686
711
  onPress={composeEventHandlers(onPress as any, context.onOpenToggle)}
687
712
  />
688
713
  )}
@@ -695,7 +720,7 @@ function PopoverPortal({
695
720
 
696
721
  /* -----------------------------------------------------------------------------------------------*/
697
722
 
698
- type PopoverContentImplElement = React.ElementRef<typeof PopperContent>
723
+ type PopoverContentImplElement = GetRef<typeof PopperContent>
699
724
 
700
725
  export type PopoverContentImplProps = PopperContentProps &
701
726
  Omit<DismissableProps, 'onDismiss' | 'children' | 'onPointerDownCapture'> & {
@@ -705,6 +730,13 @@ export type PopoverContentImplProps = PopperContentProps &
705
730
  */
706
731
  lazyMount?: boolean
707
732
 
733
+ /**
734
+ * Used to force mounting when more control is needed. Useful when
735
+ * controlling animation with React animation libraries. Matches Dialog:
736
+ * disables part presence gating so the content is always mounted.
737
+ */
738
+ forceMount?: boolean
739
+
708
740
  /**
709
741
  * Whether focus should be trapped within the `Popover`
710
742
  * @default false
@@ -718,12 +750,12 @@ export type PopoverContentImplProps = PopperContentProps &
718
750
  disableFocusScope?: boolean
719
751
 
720
752
  /**
721
- * Event handler called when auto-focusing on open. Can be prevented.
753
+ * Event handler called when auto-focusing on open. Can be canceled.
722
754
  */
723
755
  onOpenAutoFocus?: FocusScopeProps['onMountAutoFocus']
724
756
 
725
757
  /**
726
- * Event handler called when auto-focusing on close. Can be prevented.
758
+ * Event handler called when auto-focusing on close. Can be canceled.
727
759
  */
728
760
  onCloseAutoFocus?: FocusScopeProps['onUnmountAutoFocus'] | false
729
761
 
@@ -747,7 +779,7 @@ type PopoverContentImplInteralProps = PopoverContentImplProps & {
747
779
  setIsFullyHidden: React.Dispatch<React.SetStateAction<boolean>>
748
780
  }
749
781
 
750
- const PopoverContentImpl = React.forwardRef<
782
+ const PopoverContentImpl = createRefComponent<
751
783
  PopoverContentImplElement,
752
784
  PopoverContentImplInteralProps
753
785
  >(function PopoverContentImpl(props, forwardedRef) {
@@ -768,13 +800,15 @@ const PopoverContentImpl = React.forwardRef<
768
800
  setIsFullyHidden,
769
801
  lazyMount,
770
802
  forceUnmount,
803
+ forceMount,
771
804
  context,
772
805
  open,
773
806
  alwaysDisable,
774
807
  ...contentProps
775
808
  } = props
776
809
 
777
- const { keepChildrenMounted, disableDismissable } = context
810
+ const { disableDismissable } = context
811
+ const keepChildrenMounted = context.keepChildrenMounted || forceMount
778
812
 
779
813
  const handleExitComplete = React.useCallback(() => {
780
814
  setIsFullyHidden?.(true)
@@ -784,9 +818,16 @@ const PopoverContentImpl = React.forwardRef<
784
818
  <ResetPresence disable={context.breakpointActive}>{children}</ResetPresence>
785
819
  )
786
820
 
787
- const handleDismiss = React.useCallback(() => {
788
- context.onOpenChange(false, 'press')
789
- }, [context])
821
+ const handleDismiss = React.useCallback<NonNullable<DismissableProps['onDismiss']>>(
822
+ (details) => {
823
+ if (context.closeReasonRef) {
824
+ context.closeReasonRef.current =
825
+ details.reason === 'escape-key' ? 'escape-key' : 'dismiss'
826
+ }
827
+ context.onOpenChange(false, 'press')
828
+ },
829
+ [context]
830
+ )
790
831
 
791
832
  // i want to avoid reparenting but react-remove-scroll makes it hard
792
833
  // TODO its removed now so we can probable do it now
@@ -801,7 +842,7 @@ const PopoverContentImpl = React.forwardRef<
801
842
  onMountAutoFocus={onOpenAutoFocus}
802
843
  onUnmountAutoFocus={onCloseAutoFocus === false ? undefined : onCloseAutoFocus}
803
844
  >
804
- <div style={dspContentsStyle}>{contents}</div>
845
+ {contents}
805
846
  </FocusScope>
806
847
  )
807
848
  }
@@ -850,11 +891,6 @@ const PopoverContentImpl = React.forwardRef<
850
891
  id={context.contentId}
851
892
  ref={forwardedRef}
852
893
  passThrough={context.breakpointActive}
853
- {...(!contentProps.unstyled && {
854
- size: '$true',
855
- backgroundColor: '$background',
856
- alignItems: 'center',
857
- })}
858
894
  {...contentProps}
859
895
  >
860
896
  <PortalAdaptSafe context={context}>{contents}</PortalAdaptSafe>
@@ -863,25 +899,21 @@ const PopoverContentImpl = React.forwardRef<
863
899
  )
864
900
  })
865
901
 
866
- const dspContentsStyle = {
867
- display: 'contents',
868
- }
869
-
870
902
  /* -------------------------------------------------------------------------------------------------
871
903
  * PopoverClose
872
904
  * -----------------------------------------------------------------------------------------------*/
873
905
 
874
906
  export type PopoverCloseProps = ScopedPopoverProps<YStackProps>
875
907
 
876
- export const PopoverClose = React.forwardRef<TamaguiElement, PopoverCloseProps>(
908
+ export const PopoverClose = createRefComponent<TamaguiElement, PopoverCloseProps>(
877
909
  function PopoverClose(props: ScopedPopoverProps<PopoverCloseProps>, forwardedRef) {
878
- const { scope, ...rest } = props
910
+ const { scope, className, ...rest } = props
879
911
  const context = usePopoverContext(scope)
880
912
  return (
881
913
  <YStack
882
914
  {...rest}
883
915
  ref={forwardedRef}
884
- componentName="PopoverClose"
916
+ className={`is_PopoverClose ${className || ''}`.trim()}
885
917
  onPress={composeEventHandlers(props.onPress as any, () =>
886
918
  context?.onOpenChange?.(false, 'press')
887
919
  )}
@@ -896,9 +928,10 @@ export const PopoverClose = React.forwardRef<TamaguiElement, PopoverCloseProps>(
896
928
 
897
929
  export type PopoverArrowProps = PopperArrowProps
898
930
 
899
- export const PopoverArrow = PopperArrowFrame.styleable<PopperArrowExtraProps>(
900
- function PopoverArrow(props, forwardedRef) {
901
- const { scope, ...rest } = props
931
+ export const PopoverArrow = createStyledHOC(
932
+ PopperArrowFrame,
933
+ function PopoverArrow(props: PopperArrowExtraProps, forwardedRef) {
934
+ const { scope, className, ...rest } = props as PopperArrowProps
902
935
  const context = usePopoverContext(scope)
903
936
  const isAdapted = useAdaptIsActive(context.adaptScope)
904
937
 
@@ -909,7 +942,7 @@ export const PopoverArrow = PopperArrowFrame.styleable<PopperArrowExtraProps>(
909
942
  return (
910
943
  <PopperArrow
911
944
  scope={scope}
912
- componentName="PopoverArrow"
945
+ className={`is_PopoverArrow ${className || ''}`.trim()}
913
946
  {...rest}
914
947
  ref={forwardedRef}
915
948
  />
@@ -940,7 +973,7 @@ export type PopoverScrollViewProps = ScrollViewProps & {
940
973
  scope?: string
941
974
  }
942
975
 
943
- const PopoverScrollView = React.forwardRef<ScrollView, PopoverScrollViewProps>(
976
+ const PopoverScrollView = createRefComponent<ScrollView, PopoverScrollViewProps>(
944
977
  ({ scope, ...props }, ref) => {
945
978
  const context = usePopoverContext(scope)
946
979
 
@@ -961,20 +994,56 @@ const PopoverScrollView = React.forwardRef<ScrollView, PopoverScrollViewProps>(
961
994
  const DEFAULT_SCOPE = ''
962
995
 
963
996
  export const Popover = withStaticProperties(
964
- React.forwardRef<Popover, PopoverProps>(function Popover(
997
+ createRefComponent<Popover, PopoverProps>(function Popover(
965
998
  { scope = DEFAULT_SCOPE, ...props },
966
999
  ref
967
1000
  ) {
968
1001
  const id = React.useId()
969
1002
  const adaptScope = `PopoverAdapt${scope}`
1003
+ const { open: openProp, defaultOpen, onOpenChange } = props
1004
+ const viaRef = React.useRef<PopoverVia>(undefined)
1005
+ const closeReasonRef = React.useRef<OpenChangeReason | undefined>(undefined)
1006
+
1007
+ // open state lives here (above AdaptParent) so the Adapt handoff can drive
1008
+ // the adapted Sheet's open/close and unmount timing directly, mirroring Dialog
1009
+ const [open, setOpen] = useControllableState({
1010
+ prop: openProp,
1011
+ defaultProp: defaultOpen || false,
1012
+ onChange: (val) => {
1013
+ onOpenChange?.(val, viaRef.current)
1014
+ },
1015
+ })
1016
+
1017
+ const handleOpenChange = useEvent((val: boolean, via?: PopoverVia): void => {
1018
+ viaRef.current = via
1019
+ setOpen(val)
1020
+ })
1021
+
1022
+ // track open popovers for closeOpenPopovers()
1023
+ React.useEffect(() => {
1024
+ if (!open) return
1025
+ openPopovers.add(setOpen)
1026
+ return () => {
1027
+ openPopovers.delete(setOpen)
1028
+ }
1029
+ }, [open, setOpen])
970
1030
 
971
1031
  return (
972
- <AdaptParent scope={adaptScope} portal>
1032
+ <AdaptParent
1033
+ scope={adaptScope}
1034
+ adaptChildren={props.children}
1035
+ open={open}
1036
+ onOpenChange={setOpen}
1037
+ >
973
1038
  <PopoverInner
974
1039
  adaptScope={adaptScope}
975
1040
  ref={ref}
976
1041
  id={id}
977
1042
  scope={scope}
1043
+ open={open}
1044
+ setOpen={setOpen}
1045
+ handleOpenChange={handleOpenChange}
1046
+ closeReasonRef={closeReasonRef}
978
1047
  {...props}
979
1048
  />
980
1049
  </AdaptParent>
@@ -992,29 +1061,39 @@ export const Popover = withStaticProperties(
992
1061
  }
993
1062
  )
994
1063
 
995
- const PopoverInner = React.forwardRef<
1064
+ const PopoverInner = createRefComponent<
996
1065
  Popover,
997
- PopoverProps & { id: string; adaptScope: string }
1066
+ PopoverProps & {
1067
+ id: string
1068
+ adaptScope: string
1069
+ open: boolean
1070
+ setOpen: React.Dispatch<React.SetStateAction<boolean>>
1071
+ handleOpenChange: (open: boolean, via?: PopoverVia) => void
1072
+ closeReasonRef: React.RefObject<OpenChangeReason | undefined>
1073
+ }
998
1074
  >(function PopoverInner(props, forwardedRef) {
999
1075
  const {
1000
1076
  children,
1001
- open: openProp,
1002
- defaultOpen,
1003
- onOpenChange,
1077
+ open: _openProp,
1078
+ defaultOpen: _defaultOpen,
1079
+ onOpenChange: _onOpenChange,
1004
1080
  scope = DEFAULT_SCOPE,
1005
1081
  keepChildrenMounted: keepChildrenMountedProp,
1006
1082
  hoverable,
1007
- disableFocus,
1083
+ disableFocus = !hoverable,
1008
1084
  disableDismissable,
1009
1085
  zIndex,
1010
1086
  id,
1011
1087
  adaptScope,
1088
+ open,
1089
+ setOpen,
1090
+ handleOpenChange,
1091
+ closeReasonRef,
1012
1092
  ...restProps
1013
1093
  } = props
1014
1094
 
1015
1095
  const triggerRef = React.useRef<TamaguiElement>(null)
1016
1096
  const [hasCustomAnchor, setHasCustomAnchor] = React.useState(false)
1017
- const viaRef = React.useRef<PopoverVia>(undefined)
1018
1097
 
1019
1098
  const [keepChildrenMounted] = useControllableState({
1020
1099
  prop: keepChildrenMountedProp,
@@ -1022,36 +1101,16 @@ const PopoverInner = React.forwardRef<
1022
1101
  transition: keepChildrenMountedProp === 'lazy',
1023
1102
  })
1024
1103
 
1025
- const [open, setOpen] = useControllableState({
1026
- prop: openProp,
1027
- defaultProp: defaultOpen || false,
1028
- onChange: (val) => {
1029
- onOpenChange?.(val, viaRef.current)
1030
- },
1031
- })
1032
-
1033
- // track open popovers for closeOpenPopovers()
1034
- React.useEffect(() => {
1035
- if (!open) return
1036
- openPopovers.add(setOpen)
1037
- return () => {
1038
- openPopovers.delete(setOpen)
1039
- }
1040
- }, [open, setOpen])
1041
-
1042
- const handleOpenChange = useEvent((val, via) => {
1043
- viaRef.current = via
1044
- setOpen(val)
1045
- })
1046
-
1047
1104
  const isAdapted = useAdaptIsActive(adaptScope)
1048
1105
 
1049
1106
  const floatingContext = useFloatingContext({
1050
1107
  open,
1051
- setOpen: handleOpenChange,
1108
+ // floating passes a string via ('hover'), widen at this boundary
1109
+ setOpen: handleOpenChange as (val: boolean, type?: string) => void,
1110
+ closeReasonRef,
1052
1111
  disable: isAdapted,
1053
1112
  hoverable,
1054
- disableFocus: disableFocus,
1113
+ disableFocus,
1055
1114
  })
1056
1115
 
1057
1116
  const [anchorTo, setAnchorToRaw] = React.useState<Rect>()
@@ -1074,6 +1133,7 @@ const PopoverInner = React.forwardRef<
1074
1133
  if (open && isAdapted) {
1075
1134
  return
1076
1135
  }
1136
+ closeReasonRef.current = open ? 'reference-press' : undefined
1077
1137
  setOpen(!open)
1078
1138
  })
1079
1139
 
@@ -1099,10 +1159,9 @@ const PopoverInner = React.forwardRef<
1099
1159
  keepChildrenMounted={keepChildrenMounted}
1100
1160
  disableDismissable={disableDismissable}
1101
1161
  hoverable={hoverable}
1162
+ closeReasonRef={closeReasonRef}
1102
1163
  >
1103
- <PopoverSheetController onOpenChange={setOpen} open={open} scope={scope}>
1104
- {children}
1105
- </PopoverSheetController>
1164
+ {children}
1106
1165
  </PopoverContextProvider>
1107
1166
  </Popper>
1108
1167
  )
@@ -1135,59 +1194,3 @@ const PopoverInner = React.forwardRef<
1135
1194
  function getState(open: boolean) {
1136
1195
  return open ? 'open' : 'closed'
1137
1196
  }
1138
-
1139
- const PopoverSheetController = ({
1140
- open,
1141
- scope,
1142
- ...props
1143
- }: {
1144
- open: boolean
1145
- scope?: string
1146
- children: React.ReactNode
1147
- onOpenChange: React.Dispatch<React.SetStateAction<boolean>>
1148
- }) => {
1149
- const context = usePopoverContext(scope)
1150
- const showSheet = useShowPopoverSheet(context, open)
1151
- const breakpointActive = context?.breakpointActive
1152
- const getShowSheet = useGet(showSheet)
1153
-
1154
- // tracks whether the adapted Sheet has finished its slide-out animation.
1155
- // starts true (= safe to unmount) when closed; flips false the moment the
1156
- // popover opens; flips back to true when the sheet signals onAnimationComplete
1157
- // with open=false (slide-out finished). mirrors DialogSheetController.
1158
- const [isAdaptFullyHidden, setIsAdaptFullyHidden] = React.useState(!open)
1159
- if (open && isAdaptFullyHidden) {
1160
- setIsAdaptFullyHidden(false)
1161
- }
1162
-
1163
- const handleSheetAnimationComplete = React.useCallback(
1164
- ({ open: isOpen }: { open: boolean }) => {
1165
- if (!isOpen) {
1166
- setIsAdaptFullyHidden(true)
1167
- }
1168
- },
1169
- []
1170
- )
1171
-
1172
- return (
1173
- <SheetController
1174
- onOpenChange={(val: boolean) => {
1175
- if (getShowSheet()) {
1176
- props.onOpenChange?.(val)
1177
- }
1178
- }}
1179
- onAnimationComplete={handleSheetAnimationComplete}
1180
- open={open}
1181
- hidden={!breakpointActive}
1182
- >
1183
- <PopoverAdaptHiddenContext.Provider value={isAdaptFullyHidden}>
1184
- {props.children}
1185
- </PopoverAdaptHiddenContext.Provider>
1186
- </SheetController>
1187
- )
1188
- }
1189
-
1190
- const useShowPopoverSheet = (context: PopoverContextValue, open: boolean) => {
1191
- const isAdapted = useAdaptIsActive(context.adaptScope)
1192
- return open === false ? false : isAdapted
1193
- }