@tamagui/popover 2.0.0-rc.8 → 2.0.0

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 (49) hide show
  1. package/dist/cjs/Popover.cjs +624 -408
  2. package/dist/cjs/Popover.native.js +637 -438
  3. package/dist/cjs/Popover.native.js.map +1 -1
  4. package/dist/cjs/index.cjs +7 -5
  5. package/dist/cjs/index.native.js +7 -5
  6. package/dist/cjs/index.native.js.map +1 -1
  7. package/dist/cjs/useFloatingContext.cjs +226 -58
  8. package/dist/cjs/useFloatingContext.native.js +28 -26
  9. package/dist/cjs/useFloatingContext.native.js.map +1 -1
  10. package/dist/esm/Popover.mjs +576 -377
  11. package/dist/esm/Popover.mjs.map +1 -1
  12. package/dist/esm/Popover.native.js +591 -409
  13. package/dist/esm/Popover.native.js.map +1 -1
  14. package/dist/esm/index.js +2 -2
  15. package/dist/esm/index.js.map +1 -6
  16. package/dist/esm/useFloatingContext.mjs +200 -34
  17. package/dist/esm/useFloatingContext.mjs.map +1 -1
  18. package/dist/jsx/Popover.mjs +576 -377
  19. package/dist/jsx/Popover.mjs.map +1 -1
  20. package/dist/jsx/Popover.native.js +637 -438
  21. package/dist/jsx/Popover.native.js.map +1 -1
  22. package/dist/jsx/index.js +2 -2
  23. package/dist/jsx/index.js.map +1 -6
  24. package/dist/jsx/index.native.js +7 -5
  25. package/dist/jsx/useFloatingContext.mjs +200 -34
  26. package/dist/jsx/useFloatingContext.mjs.map +1 -1
  27. package/dist/jsx/useFloatingContext.native.js +28 -26
  28. package/dist/jsx/useFloatingContext.native.js.map +1 -1
  29. package/package.json +26 -31
  30. package/src/Popover.tsx +494 -175
  31. package/src/useFloatingContext.tsx +321 -43
  32. package/types/Popover.d.ts +126 -8
  33. package/types/Popover.d.ts.map +1 -1
  34. package/types/useFloatingContext.d.ts +14 -8
  35. package/types/useFloatingContext.d.ts.map +1 -1
  36. package/dist/cjs/Popover.js +0 -394
  37. package/dist/cjs/Popover.js.map +0 -6
  38. package/dist/cjs/index.js +0 -16
  39. package/dist/cjs/index.js.map +0 -6
  40. package/dist/cjs/useFloatingContext.js +0 -74
  41. package/dist/cjs/useFloatingContext.js.map +0 -6
  42. package/dist/esm/Popover.js +0 -415
  43. package/dist/esm/Popover.js.map +0 -6
  44. package/dist/esm/useFloatingContext.js +0 -59
  45. package/dist/esm/useFloatingContext.js.map +0 -6
  46. package/dist/jsx/Popover.js +0 -415
  47. package/dist/jsx/Popover.js.map +0 -6
  48. package/dist/jsx/useFloatingContext.js +0 -59
  49. package/dist/jsx/useFloatingContext.js.map +0 -6
package/src/Popover.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import '@tamagui/polyfill-dev'
2
2
 
3
- import type { UseHoverProps } from '@floating-ui/react'
3
+ import type { UseHoverProps } from '@tamagui/floating'
4
4
  import {
5
5
  Adapt,
6
6
  AdaptParent,
@@ -12,19 +12,20 @@ import {
12
12
  import { Animate } from '@tamagui/animate'
13
13
  import { ResetPresence } from '@tamagui/animate-presence'
14
14
  import { useComposedRefs } from '@tamagui/compose-refs'
15
- import { isWeb } from '@tamagui/constants'
16
- import type { SizeTokens, ViewProps, TamaguiElement } from '@tamagui/core'
15
+ import { isWeb, useIsomorphicLayoutEffect } from '@tamagui/constants'
16
+ import type { SizeTokens, TamaguiElement, ViewProps } from '@tamagui/core'
17
17
  import {
18
18
  createStyledContext,
19
- styled,
20
- Theme,
21
19
  useCreateShallowSetState,
22
20
  useEvent,
23
21
  useGet,
24
- useThemeName,
25
22
  View,
26
23
  } from '@tamagui/core'
27
- import type { DismissableProps } from '@tamagui/dismissable'
24
+ import {
25
+ Dismissable,
26
+ DismissableBranch,
27
+ type DismissableProps,
28
+ } from '@tamagui/dismissable'
28
29
  import { FloatingOverrideContext } from '@tamagui/floating'
29
30
  import type { FocusScopeProps } from '@tamagui/focus-scope'
30
31
  import { FocusScope, FocusScopeController } from '@tamagui/focus-scope'
@@ -43,14 +44,13 @@ import {
43
44
  PopperProvider,
44
45
  usePopperContext,
45
46
  } from '@tamagui/popper'
46
- import { needsPortalRepropagation, Portal, resolveViewZIndex } from '@tamagui/portal'
47
+ import { needsPortalRepropagation, Portal } from '@tamagui/portal'
47
48
  import { RemoveScroll } from '@tamagui/remove-scroll'
48
49
  import { ScrollView, type ScrollViewProps } from '@tamagui/scroll-view'
49
50
  import { SheetController } from '@tamagui/sheet/controller'
50
51
  import type { YStackProps } from '@tamagui/stacks'
51
52
  import { YStack } from '@tamagui/stacks'
52
53
  import { useControllableState } from '@tamagui/use-controllable-state'
53
- import { StackZIndexContext } from '@tamagui/z-index-stack'
54
54
  import * as React from 'react'
55
55
  import { useFloatingContext } from './useFloatingContext'
56
56
 
@@ -62,6 +62,28 @@ type ScopedPopoverProps<P> = Omit<P, 'scope'> & {
62
62
 
63
63
  const needsRepropagation = needsPortalRepropagation()
64
64
 
65
+ const openPopovers = new Set<React.Dispatch<React.SetStateAction<boolean>>>()
66
+
67
+ export const hasOpenPopovers = () => {
68
+ return openPopovers.size > 0
69
+ }
70
+
71
+ export const closeOpenPopovers = () => {
72
+ if (openPopovers.size === 0) return false
73
+ openPopovers.forEach((setOpen) => setOpen(false))
74
+ return true
75
+ }
76
+
77
+ export const closeLastOpenedPopover = () => {
78
+ if (openPopovers.size === 0) return false
79
+ const last = Array.from(openPopovers).pop()
80
+ if (last) {
81
+ last(false)
82
+ return true
83
+ }
84
+ return false
85
+ }
86
+
65
87
  type PopoverVia = 'hover' | 'press'
66
88
 
67
89
  export type PopoverProps = ScopedPopoverProps<PopperProps> & {
@@ -86,6 +108,24 @@ export type PopoverProps = ScopedPopoverProps<PopperProps> & {
86
108
  * Disable focusing behavior on open
87
109
  */
88
110
  disableFocus?: boolean
111
+
112
+ /**
113
+ * Disable the dismissable layer (escape key, outside click handling).
114
+ * Useful for popovers that stay mounted but are visually hidden.
115
+ */
116
+ disableDismissable?: boolean
117
+
118
+ /**
119
+ * z-index for the popover portal. Use this when popovers need to appear
120
+ * above other portaled content like dialogs or fixed headers.
121
+ *
122
+ * By default, Tamagui automatically stacks overlays - later-opened content
123
+ * appears above earlier content, and nested content appears above its parent.
124
+ * Only set this if you need to override the automatic stacking behavior.
125
+ *
126
+ * @see https://tamagui.dev/ui/z-index
127
+ */
128
+ zIndex?: number
89
129
  }
90
130
 
91
131
  // let users override for type safety
@@ -106,7 +146,24 @@ type PopoverContextValue = {
106
146
  size?: SizeTokens
107
147
  breakpointActive?: boolean
108
148
  keepChildrenMounted?: boolean | 'lazy'
149
+ disableDismissable?: boolean
150
+ hoverable?: boolean | object
109
151
  anchorTo?: Rect
152
+ // scoped branches Set for DismissableBranch/Dismissable to share
153
+ branches: Set<HTMLElement>
154
+ }
155
+
156
+ type PopoverTriggerStateSetter = React.Dispatch<React.SetStateAction<boolean>>
157
+
158
+ type PopoverTriggerContextValue = {
159
+ triggerRef: React.RefObject<any>
160
+ hasCustomAnchor: boolean
161
+ anchorTo?: Rect
162
+ branches: Set<HTMLElement>
163
+ onOpenToggle(): void
164
+ setActiveTrigger(id: string | null): void
165
+ registerTrigger(id: string, setOpen: PopoverTriggerStateSetter): void
166
+ unregisterTrigger(id: string): void
110
167
  }
111
168
 
112
169
  export const PopoverContext = createStyledContext<PopoverContextValue>(
@@ -115,93 +172,302 @@ export const PopoverContext = createStyledContext<PopoverContextValue>(
115
172
  'Popover__'
116
173
  )
117
174
 
175
+ // zIndex flows from root Popover prop to PopoverContent portal
176
+ export const PopoverZIndexContext = React.createContext<number | undefined>(undefined)
177
+
178
+ export const PopoverTriggerContext = createStyledContext<PopoverTriggerContextValue>(
179
+ {} as PopoverTriggerContextValue,
180
+ 'PopoverTrigger__'
181
+ )
182
+
118
183
  export const usePopoverContext = PopoverContext.useStyledContext
184
+ export const usePopoverTriggerContext = PopoverTriggerContext.useStyledContext
119
185
 
120
- /* -------------------------------------------------------------------------------------------------
121
- * PopoverAnchor
122
- * -----------------------------------------------------------------------------------------------*/
186
+ /**
187
+ * Read reactive popover open state from the popover context.
188
+ */
189
+ export function usePopoverOpen(scope?: string): boolean {
190
+ return usePopoverContext(scope).open
191
+ }
123
192
 
124
- export type PopoverAnchorProps = ScopedPopoverProps<YStackProps>
193
+ /**
194
+ * Hook to set up trigger registration/isolation logic.
195
+ * Used internally by Popover and can be used by Tooltip.
196
+ */
197
+ export function usePopoverTriggerSetup(open: boolean) {
198
+ const triggerStateSettersRef = React.useRef(
199
+ new Map<string, PopoverTriggerStateSetter>()
200
+ )
201
+ const activeTriggerIdRef = React.useRef<string | null>(null)
125
202
 
126
- export const PopoverAnchor = React.forwardRef<TamaguiElement, PopoverAnchorProps>(
127
- function PopoverAnchor(props, forwardedRef) {
128
- const { scope, ...rest } = props
129
- const context = usePopoverContext(scope)
130
- const { onCustomAnchorAdd, onCustomAnchorRemove } = context || {}
203
+ const setActiveTrigger = useEvent((id: string | null) => {
204
+ const prevId = activeTriggerIdRef.current
205
+ if (prevId === id) return
206
+ if (prevId) {
207
+ triggerStateSettersRef.current.get(prevId)?.(false)
208
+ }
209
+ activeTriggerIdRef.current = id
210
+ if (id && open) {
211
+ triggerStateSettersRef.current.get(id)?.(true)
212
+ }
213
+ })
131
214
 
132
- React.useEffect(() => {
133
- onCustomAnchorAdd()
134
- return () => onCustomAnchorRemove()
135
- }, [onCustomAnchorAdd, onCustomAnchorRemove])
215
+ const registerTrigger = useEvent(
216
+ (id: string, setOpenState: PopoverTriggerStateSetter) => {
217
+ triggerStateSettersRef.current.set(id, setOpenState)
218
+ setOpenState(activeTriggerIdRef.current === id && open)
219
+ }
220
+ )
136
221
 
137
- return <PopperAnchor scope={scope} {...rest} ref={forwardedRef} />
222
+ const unregisterTrigger = useEvent((id: string) => {
223
+ triggerStateSettersRef.current.delete(id)
224
+ if (activeTriggerIdRef.current === id) {
225
+ activeTriggerIdRef.current = null
226
+ }
227
+ })
228
+
229
+ React.useEffect(() => {
230
+ if (!open) {
231
+ setActiveTrigger(null)
232
+ return
233
+ }
234
+ const activeId = activeTriggerIdRef.current
235
+ if (activeId) {
236
+ triggerStateSettersRef.current.get(activeId)?.(true)
237
+ }
238
+ }, [open, setActiveTrigger])
239
+
240
+ return { setActiveTrigger, registerTrigger, unregisterTrigger }
241
+ }
242
+
243
+ export type PopoverContextProviderProps = {
244
+ scope: string
245
+ children: React.ReactNode
246
+ // PopoverContext values
247
+ open: boolean
248
+ onOpenChange(open: boolean, via?: 'hover' | 'press'): void
249
+ onOpenToggle(): void
250
+ triggerRef: React.RefObject<any>
251
+ id?: string
252
+ contentId?: string
253
+ hasCustomAnchor?: boolean
254
+ onCustomAnchorAdd?: () => void
255
+ onCustomAnchorRemove?: () => void
256
+ anchorTo?: Rect
257
+ // extra props for Popover (optional for Tooltip)
258
+ adaptScope?: string
259
+ breakpointActive?: boolean
260
+ keepChildrenMounted?: boolean | 'lazy'
261
+ disableDismissable?: boolean
262
+ hoverable?: boolean | object
263
+ }
264
+
265
+ /**
266
+ * Provider that sets up both PopoverContext and PopoverTriggerContext.
267
+ * Use this in Tooltip or other components that need popover trigger behavior.
268
+ */
269
+ export const PopoverContextProvider = React.memo(
270
+ ({
271
+ scope,
272
+ children,
273
+ open,
274
+ onOpenChange,
275
+ onOpenToggle,
276
+ triggerRef,
277
+ id = '',
278
+ contentId,
279
+ hasCustomAnchor = false,
280
+ onCustomAnchorAdd = voidFn,
281
+ onCustomAnchorRemove = voidFn,
282
+ anchorTo,
283
+ adaptScope,
284
+ breakpointActive,
285
+ keepChildrenMounted,
286
+ disableDismissable,
287
+ hoverable,
288
+ }: PopoverContextProviderProps) => {
289
+ const [branches] = React.useState(() => new Set<HTMLElement>())
290
+ const { setActiveTrigger, registerTrigger, unregisterTrigger } =
291
+ usePopoverTriggerSetup(open)
292
+
293
+ return (
294
+ <PopoverContext.Provider
295
+ scope={scope}
296
+ popoverScope={scope}
297
+ adaptScope={adaptScope}
298
+ id={id}
299
+ contentId={contentId}
300
+ triggerRef={triggerRef}
301
+ open={open}
302
+ onOpenChange={onOpenChange}
303
+ onOpenToggle={onOpenToggle}
304
+ hasCustomAnchor={hasCustomAnchor}
305
+ onCustomAnchorAdd={onCustomAnchorAdd}
306
+ onCustomAnchorRemove={onCustomAnchorRemove}
307
+ anchorTo={anchorTo}
308
+ branches={branches}
309
+ breakpointActive={breakpointActive}
310
+ keepChildrenMounted={keepChildrenMounted}
311
+ disableDismissable={disableDismissable}
312
+ hoverable={hoverable}
313
+ >
314
+ <PopoverTriggerContext.Provider
315
+ scope={scope}
316
+ triggerRef={triggerRef}
317
+ hasCustomAnchor={hasCustomAnchor}
318
+ anchorTo={anchorTo}
319
+ branches={branches}
320
+ onOpenToggle={onOpenToggle}
321
+ setActiveTrigger={setActiveTrigger}
322
+ registerTrigger={registerTrigger}
323
+ unregisterTrigger={unregisterTrigger}
324
+ >
325
+ {children}
326
+ </PopoverTriggerContext.Provider>
327
+ </PopoverContext.Provider>
328
+ )
138
329
  }
139
330
  )
140
331
 
332
+ const voidFn = () => {}
333
+
141
334
  /* -------------------------------------------------------------------------------------------------
142
- * PopoverTrigger
335
+ * PopoverAnchor
143
336
  * -----------------------------------------------------------------------------------------------*/
144
337
 
145
- export type PopoverTriggerProps = ScopedPopoverProps<ViewProps>
338
+ export type PopoverAnchorProps = ScopedPopoverProps<YStackProps>
146
339
 
147
- export const PopoverTrigger = React.forwardRef<TamaguiElement, PopoverTriggerProps>(
148
- function PopoverTrigger(props, forwardedRef) {
149
- const { scope, ...rest } = props
150
- const context = usePopoverContext(scope)
340
+ export const PopoverAnchor = React.memo(
341
+ React.forwardRef<TamaguiElement, PopoverAnchorProps>(
342
+ function PopoverAnchor(props, forwardedRef) {
343
+ const { scope, ...rest } = props
344
+ const context = usePopoverContext(scope)
345
+ const { onCustomAnchorAdd, onCustomAnchorRemove } = context || {}
151
346
 
152
- const anchorTo = context.anchorTo
153
- const composedTriggerRef = useComposedRefs(forwardedRef, context.triggerRef)
347
+ React.useEffect(() => {
348
+ onCustomAnchorAdd()
349
+ return () => onCustomAnchorRemove()
350
+ }, [onCustomAnchorAdd, onCustomAnchorRemove])
154
351
 
155
- if (!props.children) {
156
- return null
352
+ return <PopperAnchor scope={scope} {...rest} ref={forwardedRef} />
157
353
  }
354
+ )
355
+ )
158
356
 
159
- const trigger = (
160
- <View
161
- aria-expanded={context.open}
162
- // TODO not matching
163
- // aria-controls={context.contentId}
164
- data-state={getState(context.open)}
165
- {...rest}
166
- // @ts-ignore
167
- ref={composedTriggerRef}
168
- onPress={composeEventHandlers(props.onPress as any, context.onOpenToggle)}
169
- />
170
- )
357
+ /* -------------------------------------------------------------------------------------------------
358
+ * PopoverTrigger
359
+ * -----------------------------------------------------------------------------------------------*/
360
+
361
+ export type PopoverTriggerProps = ScopedPopoverProps<
362
+ ViewProps & {
363
+ /**
364
+ * When true, disables the built-in click-to-toggle behavior on the trigger.
365
+ * Useful for hoverable popovers where you want to control open/close
366
+ * entirely through hover or your own handlers.
367
+ */
368
+ disablePressTrigger?: boolean
369
+ }
370
+ >
371
+
372
+ export const PopoverTrigger = React.memo(
373
+ React.forwardRef<TamaguiElement, PopoverTriggerProps>(
374
+ function PopoverTrigger(props, forwardedRef) {
375
+ const { scope, disablePressTrigger, ...rest } = props
376
+ const triggerContext = usePopoverTriggerContext(scope)
377
+ const triggerId = React.useId()
378
+ const [open, setOpen] = React.useState(false)
379
+ const anchorTo = triggerContext.anchorTo
380
+ const triggerElRef = React.useRef<TamaguiElement>(null)
381
+ const composedTriggerRef = useComposedRefs(forwardedRef, triggerElRef)
382
+
383
+ const { registerTrigger, unregisterTrigger } = triggerContext
384
+ React.useEffect(() => {
385
+ registerTrigger(triggerId, setOpen)
386
+ return () => {
387
+ unregisterTrigger(triggerId)
388
+ }
389
+ }, [registerTrigger, unregisterTrigger, triggerId])
171
390
 
172
- const virtualRef = React.useMemo(() => {
173
- if (!anchorTo) {
391
+ if (!rest.children) {
174
392
  return null
175
393
  }
176
- return {
177
- current: {
178
- getBoundingClientRect: () => (isWeb ? DOMRect.fromRect(anchorTo) : anchorTo),
179
- ...(!isWeb && {
180
- measure: (c) =>
181
- c(anchorTo?.x, anchorTo?.y, anchorTo?.width, anchorTo?.height),
182
- measureInWindow: (c) =>
183
- c(anchorTo?.x, anchorTo?.y, anchorTo?.width, anchorTo?.height),
184
- }),
185
- },
394
+
395
+ const activateSelf = () => {
396
+ triggerContext.setActiveTrigger(triggerId)
397
+ const el = triggerElRef.current
398
+ if (el) {
399
+ triggerContext.triggerRef.current = el
400
+ }
186
401
  }
187
- }, [context.anchorTo, anchorTo?.x, anchorTo?.y, anchorTo?.height, anchorTo?.width])
188
-
189
- return context.hasCustomAnchor ? (
190
- trigger
191
- ) : (
192
- <PopperAnchor {...(virtualRef && { virtualRef })} scope={scope} asChild>
193
- {trigger}
194
- </PopperAnchor>
195
- )
196
- }
402
+
403
+ const trigger = (
404
+ <View
405
+ aria-expanded={open}
406
+ // TODO not matching
407
+ // aria-controls={context.contentId}
408
+ data-state={getState(open)}
409
+ {...rest}
410
+ // @ts-ignore
411
+ ref={composedTriggerRef}
412
+ onPress={composeEventHandlers(rest.onPress as any, () => {
413
+ if (disablePressTrigger) return
414
+ triggerContext.setActiveTrigger(open ? null : triggerId)
415
+ triggerContext.onOpenToggle()
416
+ })}
417
+ onMouseEnter={composeEventHandlers(rest.onMouseEnter as any, activateSelf)}
418
+ onPressIn={composeEventHandlers(rest.onPressIn as any, activateSelf)}
419
+ onFocus={composeEventHandlers(rest.onFocus as any, activateSelf)}
420
+ />
421
+ )
422
+
423
+ const virtualRef = React.useMemo(() => {
424
+ if (!anchorTo) {
425
+ return null
426
+ }
427
+ return {
428
+ current: {
429
+ getBoundingClientRect: () => (isWeb ? DOMRect.fromRect(anchorTo) : anchorTo),
430
+ ...(!isWeb && {
431
+ measure: (c) =>
432
+ c(anchorTo?.x, anchorTo?.y, anchorTo?.width, anchorTo?.height),
433
+ measureInWindow: (c) =>
434
+ c(anchorTo?.x, anchorTo?.y, anchorTo?.width, anchorTo?.height),
435
+ }),
436
+ },
437
+ }
438
+ }, [
439
+ triggerContext.anchorTo,
440
+ anchorTo?.x,
441
+ anchorTo?.y,
442
+ anchorTo?.height,
443
+ anchorTo?.width,
444
+ ])
445
+
446
+ // wrap trigger in DismissableBranch so clicking it doesn't fire pointerDownOutside
447
+ // which would close the popover before onPress can toggle it
448
+ const wrappedTrigger = isWeb ? (
449
+ <DismissableBranch branches={triggerContext.branches}>
450
+ {trigger}
451
+ </DismissableBranch>
452
+ ) : (
453
+ trigger
454
+ )
455
+
456
+ return triggerContext.hasCustomAnchor ? (
457
+ wrappedTrigger
458
+ ) : (
459
+ <PopperAnchor {...(virtualRef && { virtualRef })} scope={scope} asChild>
460
+ {wrappedTrigger}
461
+ </PopperAnchor>
462
+ )
463
+ }
464
+ )
197
465
  )
198
466
 
199
467
  /* -------------------------------------------------------------------------------------------------
200
468
  * PopoverContent
201
469
  * -----------------------------------------------------------------------------------------------*/
202
470
 
203
- type PopoverContentTypeElement = PopoverContentImplElement
204
-
205
471
  export interface PopoverContentTypeProps extends Omit<
206
472
  PopoverContentImplProps,
207
473
  'disableOutsidePointerEvents'
@@ -216,38 +482,38 @@ export interface PopoverContentTypeProps extends Omit<
216
482
 
217
483
  export type PopoverContentProps = PopoverContentTypeProps
218
484
 
219
- const PopoverContentFrame = styled(PopperContentFrame, {
220
- name: 'Popover',
221
- })
222
-
223
- export const PopoverContent = PopoverContentFrame.styleable<PopoverContentProps>(
485
+ export const PopoverContent = PopperContentFrame.styleable<PopoverContentProps>(
224
486
  function PopoverContent(props, forwardedRef) {
225
487
  const {
226
488
  trapFocus,
227
489
  enableRemoveScroll = false,
228
- zIndex,
490
+ zIndex: zIndexProp,
229
491
  scope,
230
492
  ...contentImplProps
231
493
  } = props
232
494
 
233
495
  const context = usePopoverContext(scope)
496
+ const zIndexFromContext = React.useContext(PopoverZIndexContext)
497
+ // prop on Content takes precedence for backwards compatibility, then context from root
498
+ const zIndex = zIndexProp ?? zIndexFromContext
499
+ const open = usePopoverOpen(scope)
234
500
  const contentRef = React.useRef<any>(null)
235
501
  const composedRefs = useComposedRefs(forwardedRef, contentRef)
236
502
  const isRightClickOutsideRef = React.useRef(false)
237
- const [isFullyHidden, setIsFullyHidden] = React.useState(!context.open)
503
+ const [isFullyHidden, setIsFullyHidden] = React.useState(!open)
238
504
 
239
505
  // Reset isFullyHidden when popover opens (useEffect avoids render-phase timing issues)
240
506
  // there was a hard to isolate bug in tamagui.dev where moving between /ui docs pages quickly
241
507
  // caused it to infinite loop, the setState in render (and useLayoutEffect) made it too prone
242
508
  // to bug, useEffect maybe fine here because its hidden, ok to be slightly delayed while hidden
243
- React.useEffect(() => {
244
- if (context.open && isFullyHidden) {
509
+ useIsomorphicLayoutEffect(() => {
510
+ if (open && isFullyHidden) {
245
511
  setIsFullyHidden(false)
246
512
  }
247
- }, [context.open, isFullyHidden])
513
+ }, [open, isFullyHidden])
248
514
 
249
515
  if (!context.keepChildrenMounted) {
250
- if (isFullyHidden && !context.open) {
516
+ if (isFullyHidden && !open) {
251
517
  return null
252
518
  }
253
519
  }
@@ -256,24 +522,24 @@ export const PopoverContent = PopoverContentFrame.styleable<PopoverContentProps>
256
522
  <PopoverPortal
257
523
  passThrough={context.breakpointActive}
258
524
  context={context}
525
+ open={open}
259
526
  zIndex={zIndex}
260
527
  >
261
528
  <View
262
529
  passThrough={context.breakpointActive}
263
- pointerEvents={
264
- context.open ? (contentImplProps.pointerEvents ?? 'auto') : 'none'
265
- }
530
+ pointerEvents={open ? (contentImplProps.pointerEvents ?? 'auto') : 'none'}
266
531
  >
267
532
  <PopoverContentImpl
268
533
  {...contentImplProps}
269
534
  context={context}
535
+ open={open}
270
536
  enableRemoveScroll={enableRemoveScroll}
271
537
  ref={composedRefs}
272
538
  setIsFullyHidden={setIsFullyHidden}
273
539
  scope={scope}
274
540
  // we make sure we're not trapping once it's been closed
275
541
  // (closed !== unmounted when animating out)
276
- trapFocus={trapFocus ?? context.open}
542
+ trapFocus={trapFocus ?? open}
277
543
  disableOutsidePointerEvents
278
544
  onCloseAutoFocus={
279
545
  props.onCloseAutoFocus === false
@@ -312,12 +578,14 @@ export const PopoverContent = PopoverContentFrame.styleable<PopoverContentProps>
312
578
 
313
579
  const useParentContexts = (scope: string) => {
314
580
  const context = usePopoverContext(scope)
581
+ const triggerContext = usePopoverTriggerContext(scope)
315
582
  const popperContext = usePopperContext(scope)
316
583
  const adaptContext = useAdaptContext(context.adaptScope)
317
584
  return {
318
585
  popperContext,
319
586
  adaptContext,
320
587
  context,
588
+ triggerContext,
321
589
  }
322
590
  }
323
591
 
@@ -327,14 +595,17 @@ function RepropagateParentContexts({
327
595
  adaptContext,
328
596
  children,
329
597
  context,
598
+ triggerContext,
330
599
  popperContext,
331
600
  }: ParentContexts & {
332
601
  children: React.ReactNode
333
602
  }) {
334
603
  return (
335
604
  <PopperProvider scope={context.popoverScope} {...popperContext}>
336
- <PopoverContext.Provider {...context}>
337
- <ProvideAdaptContext {...adaptContext}>{children}</ProvideAdaptContext>
605
+ <PopoverContext.Provider scope={context.popoverScope} {...context}>
606
+ <PopoverTriggerContext.Provider scope={context.popoverScope} {...triggerContext}>
607
+ <ProvideAdaptContext {...adaptContext}>{children}</ProvideAdaptContext>
608
+ </PopoverTriggerContext.Provider>
338
609
  </PopoverContext.Provider>
339
610
  </PopperProvider>
340
611
  )
@@ -347,6 +618,8 @@ const PortalAdaptSafe = ({
347
618
  children?: React.ReactNode
348
619
  context: PopoverContextValue
349
620
  }) => {
621
+ 'use no memo'
622
+
350
623
  if (needsRepropagation) {
351
624
  const parentContexts = useParentContexts(context.popoverScope)
352
625
  return (
@@ -363,42 +636,40 @@ const PortalAdaptSafe = ({
363
636
 
364
637
  function PopoverPortal({
365
638
  context,
639
+ open,
366
640
  zIndex,
367
641
  passThrough,
368
642
  children,
369
643
  onPress,
370
644
  }: Pick<PopoverContentProps, 'zIndex' | 'passThrough' | 'children' | 'onPress'> & {
371
645
  context: PopoverContextValue
646
+ open: boolean
372
647
  }) {
373
- const themeName = useThemeName()
648
+ 'use no memo'
374
649
 
375
650
  let content = children
376
651
 
377
- // native doesnt support portals
652
+ // native without teleport
378
653
  if (needsRepropagation) {
379
654
  const parentContexts = useParentContexts(context.popoverScope)
380
-
381
655
  content = (
382
656
  <RepropagateParentContexts {...parentContexts}>{content}</RepropagateParentContexts>
383
657
  )
384
658
  }
385
659
 
386
660
  return (
387
- <Portal passThrough={passThrough} stackZIndex zIndex={zIndex as any}>
661
+ <Portal passThrough={passThrough} stackZIndex zIndex={zIndex}>
388
662
  {/* forceClassName avoids forced re-mount renders for some reason... see the HeadMenu as you change tints a few times */}
389
663
  {/* without this you'll see the site menu re-rendering. It must be something in wrapping children in Theme */}
390
- <Theme passThrough={passThrough} contain forceClassName name={themeName}>
391
- {!!context.open && !context.breakpointActive && (
392
- <YStack
393
- fullscreen
394
- onPress={composeEventHandlers(onPress as any, context.onOpenToggle)}
395
- />
396
- )}
664
+ {!!open && !context.breakpointActive && !context.hoverable && (
665
+ <YStack
666
+ fullscreen
667
+ onPress={composeEventHandlers(onPress as any, context.onOpenToggle)}
668
+ />
669
+ )}
397
670
 
398
- <StackZIndexContext zIndex={resolveViewZIndex(zIndex)}>
399
- {content}
400
- </StackZIndexContext>
401
- </Theme>
671
+ {/* i removed a hardcoded StackZIndex because Portal has it internally now with useStackedZIndex + ZIndexHardcoded */}
672
+ {content}
402
673
  </Portal>
403
674
  )
404
675
  }
@@ -440,10 +711,20 @@ export type PopoverContentImplProps = PopperContentProps &
440
711
  enableRemoveScroll?: boolean
441
712
 
442
713
  freezeContentsWhenHidden?: boolean
714
+
715
+ /**
716
+ * Performance - if never going to use feature can permanently disable
717
+ */
718
+ alwaysDisable?: {
719
+ focus?: boolean
720
+ 'remove-scroll'?: boolean
721
+ dismiss?: boolean
722
+ }
443
723
  }
444
724
 
445
725
  type PopoverContentImplInteralProps = PopoverContentImplProps & {
446
726
  context: PopoverContextValue
727
+ open: boolean
447
728
  setIsFullyHidden: React.Dispatch<React.SetStateAction<boolean>>
448
729
  }
449
730
 
@@ -467,11 +748,14 @@ const PopoverContentImpl = React.forwardRef<
467
748
  freezeContentsWhenHidden,
468
749
  setIsFullyHidden,
469
750
  lazyMount,
751
+ forceUnmount,
470
752
  context,
753
+ open,
754
+ alwaysDisable,
471
755
  ...contentProps
472
756
  } = props
473
757
 
474
- const { open, keepChildrenMounted } = context
758
+ const { keepChildrenMounted, disableDismissable } = context
475
759
 
476
760
  const handleExitComplete = React.useCallback(() => {
477
761
  setIsFullyHidden?.(true)
@@ -481,14 +765,16 @@ const PopoverContentImpl = React.forwardRef<
481
765
  <ResetPresence disable={context.breakpointActive}>{children}</ResetPresence>
482
766
  )
483
767
 
768
+ const handleDismiss = React.useCallback(() => {
769
+ context.onOpenChange(false, 'press')
770
+ }, [context])
771
+
484
772
  // i want to avoid reparenting but react-remove-scroll makes it hard
485
773
  // TODO its removed now so we can probable do it now
486
774
  if (!context.breakpointActive) {
487
775
  if (process.env.TAMAGUI_TARGET !== 'native') {
488
- contents = (
489
- <RemoveScroll
490
- enabled={context.breakpointActive ? false : enableRemoveScroll ? open : false}
491
- >
776
+ if (!alwaysDisable || !alwaysDisable.focus) {
777
+ contents = (
492
778
  <FocusScope
493
779
  loop={trapFocus !== false}
494
780
  enabled={context.breakpointActive ? false : disableFocusScope ? false : open}
@@ -498,24 +784,36 @@ const PopoverContentImpl = React.forwardRef<
498
784
  >
499
785
  <div style={dspContentsStyle}>{contents}</div>
500
786
  </FocusScope>
501
- </RemoveScroll>
502
- )
503
- }
504
- }
787
+ )
788
+ }
505
789
 
506
- // const handleDismiss = React.useCallback((event: GestureResponderEvent) =>{
507
- // context.onOpenChange(false);
508
- // }, [])
509
- // <Dismissable
510
- // disableOutsidePointerEvents={disableOutsidePointerEvents}
511
- // // onInteractOutside={onInteractOutside}
512
- // onEscapeKeyDown={onEscapeKeyDown}
513
- // // onPointerDownOutside={onPointerDownOutside}
514
- // // onFocusOutside={onFocusOutside}
515
- // onDismiss={handleDismiss}
516
- // >
790
+ if (!alwaysDisable || !alwaysDisable['remove-scroll']) {
791
+ contents = (
792
+ <RemoveScroll
793
+ enabled={context.breakpointActive ? false : enableRemoveScroll ? open : false}
794
+ >
795
+ {contents}
796
+ </RemoveScroll>
797
+ )
798
+ }
517
799
 
518
- // const freeze = Boolean(isFullyHidden && freezeContentsWhenHidden)
800
+ if (!alwaysDisable || !alwaysDisable.dismiss) {
801
+ contents = (
802
+ <Dismissable
803
+ branches={context.branches}
804
+ forceUnmount={disableDismissable || (forceUnmount ?? !open)}
805
+ onEscapeKeyDown={onEscapeKeyDown}
806
+ onPointerDownOutside={onPointerDownOutside}
807
+ onFocusOutside={onFocusOutside}
808
+ onInteractOutside={onInteractOutside}
809
+ onDismiss={handleDismiss}
810
+ >
811
+ {contents}
812
+ </Dismissable>
813
+ )
814
+ }
815
+ }
816
+ }
519
817
 
520
818
  return (
521
819
  <Animate
@@ -533,6 +831,11 @@ const PopoverContentImpl = React.forwardRef<
533
831
  id={context.contentId}
534
832
  ref={forwardedRef}
535
833
  passThrough={context.breakpointActive}
834
+ {...(!contentProps.unstyled && {
835
+ size: '$true',
836
+ backgroundColor: '$background',
837
+ alignItems: 'center',
838
+ })}
536
839
  {...contentProps}
537
840
  >
538
841
  <PortalAdaptSafe context={context}>{contents}</PortalAdaptSafe>
@@ -683,6 +986,8 @@ const PopoverInner = React.forwardRef<
683
986
  keepChildrenMounted: keepChildrenMountedProp,
684
987
  hoverable,
685
988
  disableFocus,
989
+ disableDismissable,
990
+ zIndex,
686
991
  id,
687
992
  adaptScope,
688
993
  ...restProps
@@ -691,11 +996,13 @@ const PopoverInner = React.forwardRef<
691
996
  const triggerRef = React.useRef<TamaguiElement>(null)
692
997
  const [hasCustomAnchor, setHasCustomAnchor] = React.useState(false)
693
998
  const viaRef = React.useRef<PopoverVia>(undefined)
999
+
694
1000
  const [keepChildrenMounted] = useControllableState({
695
1001
  prop: keepChildrenMountedProp,
696
1002
  defaultProp: false,
697
1003
  transition: keepChildrenMountedProp === 'lazy',
698
1004
  })
1005
+
699
1006
  const [open, setOpen] = useControllableState({
700
1007
  prop: openProp,
701
1008
  defaultProp: defaultOpen || false,
@@ -704,6 +1011,15 @@ const PopoverInner = React.forwardRef<
704
1011
  },
705
1012
  })
706
1013
 
1014
+ // track open popovers for closeOpenPopovers()
1015
+ React.useEffect(() => {
1016
+ if (!open) return
1017
+ openPopovers.add(setOpen)
1018
+ return () => {
1019
+ openPopovers.delete(setOpen)
1020
+ }
1021
+ }, [open, setOpen])
1022
+
707
1023
  const handleOpenChange = useEvent((val, via) => {
708
1024
  viaRef.current = via
709
1025
  setOpen(val)
@@ -717,7 +1033,7 @@ const PopoverInner = React.forwardRef<
717
1033
  disable: isAdapted,
718
1034
  hoverable,
719
1035
  disableFocus: disableFocus,
720
- }) as any
1036
+ })
721
1037
 
722
1038
  const [anchorTo, setAnchorToRaw] = React.useState<Rect>()
723
1039
 
@@ -733,56 +1049,46 @@ const PopoverInner = React.forwardRef<
733
1049
  setOpen,
734
1050
  }))
735
1051
 
736
- // needs to be entirely memoized!
737
- const popoverContext = {
738
- popoverScope: scope,
739
- adaptScope,
740
- id,
741
- contentId: React.useId(),
742
- triggerRef,
743
- open,
744
- breakpointActive: isAdapted,
745
- onOpenChange: handleOpenChange,
746
- onOpenToggle: useEvent(() => {
747
- if (open && isAdapted) {
748
- return
749
- }
750
- setOpen(!open)
751
- }),
752
- hasCustomAnchor,
753
- anchorTo,
754
- onCustomAnchorAdd: React.useCallback(() => setHasCustomAnchor(true), []),
755
- onCustomAnchorRemove: React.useCallback(() => setHasCustomAnchor(false), []),
756
- keepChildrenMounted,
757
- } satisfies PopoverContextValue
758
-
759
- // // debug if changing too often
760
- // if (process.env.NODE_ENV === 'development') {
761
- // Object.keys(popoverContext).forEach((key) => {
762
- // React.useEffect(
763
- // () => console.log(`changed`, key, popoverContext[key]),
764
- // [popoverContext[key]]
765
- // )
766
- // })
767
- // }
768
-
769
- const memoizedChildren = React.useMemo(() => {
770
- return (
771
- <PopoverContext.Provider scope={scope} {...popoverContext}>
772
- <PopoverSheetController context={popoverContext} onOpenChange={setOpen}>
773
- {children}
774
- </PopoverSheetController>
775
- </PopoverContext.Provider>
776
- )
777
- }, [scope, setOpen, children, ...Object.values(popoverContext)])
1052
+ const contentId = React.useId()
1053
+
1054
+ const onOpenToggle = useEvent(() => {
1055
+ if (open && isAdapted) {
1056
+ return
1057
+ }
1058
+ setOpen(!open)
1059
+ })
1060
+
1061
+ const onCustomAnchorAdd = React.useCallback(() => setHasCustomAnchor(true), [])
1062
+ const onCustomAnchorRemove = React.useCallback(() => setHasCustomAnchor(false), [])
778
1063
 
779
1064
  const contents = (
780
1065
  <Popper open={open} passThrough={isAdapted} scope={scope} stayInFrame {...restProps}>
781
- {memoizedChildren}
1066
+ <PopoverContextProvider
1067
+ scope={scope}
1068
+ open={open}
1069
+ onOpenChange={handleOpenChange}
1070
+ onOpenToggle={onOpenToggle}
1071
+ triggerRef={triggerRef}
1072
+ id={id}
1073
+ contentId={contentId}
1074
+ hasCustomAnchor={hasCustomAnchor}
1075
+ onCustomAnchorAdd={onCustomAnchorAdd}
1076
+ onCustomAnchorRemove={onCustomAnchorRemove}
1077
+ anchorTo={anchorTo}
1078
+ adaptScope={adaptScope}
1079
+ breakpointActive={isAdapted}
1080
+ keepChildrenMounted={keepChildrenMounted}
1081
+ disableDismissable={disableDismissable}
1082
+ hoverable={hoverable}
1083
+ >
1084
+ <PopoverSheetController onOpenChange={setOpen} open={open} scope={scope}>
1085
+ {children}
1086
+ </PopoverSheetController>
1087
+ </PopoverContextProvider>
782
1088
  </Popper>
783
1089
  )
784
1090
 
785
- return (
1091
+ let result = (
786
1092
  <>
787
1093
  {isWeb ? (
788
1094
  <FloatingOverrideContext.Provider value={floatingContext}>
@@ -793,6 +1099,16 @@ const PopoverInner = React.forwardRef<
793
1099
  )}
794
1100
  </>
795
1101
  )
1102
+
1103
+ if (zIndex !== undefined) {
1104
+ return (
1105
+ <PopoverZIndexContext.Provider value={zIndex}>
1106
+ {result}
1107
+ </PopoverZIndexContext.Provider>
1108
+ )
1109
+ }
1110
+
1111
+ return result
796
1112
  })
797
1113
 
798
1114
  /* -----------------------------------------------------------------------------------------------*/
@@ -802,25 +1118,28 @@ function getState(open: boolean) {
802
1118
  }
803
1119
 
804
1120
  const PopoverSheetController = ({
805
- context,
1121
+ open,
1122
+ scope,
806
1123
  ...props
807
1124
  }: {
808
- context: PopoverContextValue
1125
+ open: boolean
1126
+ scope?: string
809
1127
  children: React.ReactNode
810
1128
  onOpenChange: React.Dispatch<React.SetStateAction<boolean>>
811
1129
  }) => {
812
- const showSheet = useShowPopoverSheet(context)
813
- const breakpointActive = context.breakpointActive
1130
+ const context = usePopoverContext(scope)
1131
+ const showSheet = useShowPopoverSheet(context, open)
1132
+ const breakpointActive = context?.breakpointActive
814
1133
  const getShowSheet = useGet(showSheet)
815
1134
 
816
1135
  return (
817
1136
  <SheetController
818
- onOpenChange={(val) => {
1137
+ onOpenChange={(val: boolean) => {
819
1138
  if (getShowSheet()) {
820
1139
  props.onOpenChange?.(val)
821
1140
  }
822
1141
  }}
823
- open={context.open}
1142
+ open={open}
824
1143
  hidden={!breakpointActive}
825
1144
  >
826
1145
  {props.children}
@@ -828,7 +1147,7 @@ const PopoverSheetController = ({
828
1147
  )
829
1148
  }
830
1149
 
831
- const useShowPopoverSheet = (context: PopoverContextValue) => {
1150
+ const useShowPopoverSheet = (context: PopoverContextValue, open: boolean) => {
832
1151
  const isAdapted = useAdaptIsActive(context.adaptScope)
833
- return context.open === false ? false : isAdapted
1152
+ return open === false ? false : isAdapted
834
1153
  }