@tamagui/adapt 2.7.6 → 3.0.0-beta.637.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Adapt.tsx CHANGED
@@ -8,53 +8,49 @@ import {
8
8
  import type { AllPlatforms, MediaQueryKey } from '@tamagui/core'
9
9
  import { createStyledContext, useMedia } from '@tamagui/core'
10
10
  import { withStaticProperties } from '@tamagui/helpers'
11
- import { getPortal } from '@tamagui/native'
12
- import { PortalHost, PortalItem } from '@tamagui/portal'
13
11
  import { StackZIndexContext } from '@tamagui/z-index-stack'
14
- import React, { createContext, useContext, useId, useMemo } from 'react'
15
-
16
- /**
17
- * External store for passing children from AdaptPortalContents to Adapt.Contents
18
- * when teleport is enabled. This bypasses PortalItem to avoid nested teleport
19
- * (inner portal inside an already-teleported Sheet) which breaks touch
20
- * coordinate mapping on iOS.
21
- */
22
- type AdaptChildrenStore = {
23
- set(children: React.ReactNode): void
24
- get(): React.ReactNode
12
+ import React, { createContext, useContext, useId } from 'react'
13
+
14
+ type AdaptSlotStore = {
15
+ element: React.ReactNode
16
+ version: number
17
+ publish(element: React.ReactNode): void
18
+ clear(): void
19
+ notify(): void
20
+ getSnapshot(): number
25
21
  subscribe(callback: () => void): () => void
26
22
  }
27
23
 
28
- function createAdaptChildrenStore(): AdaptChildrenStore {
29
- let children: React.ReactNode = null
24
+ function createAdaptSlotStore(): AdaptSlotStore {
30
25
  const listeners = new Set<() => void>()
31
- return {
32
- set(c) {
33
- children = c
34
- for (const l of listeners) l()
26
+
27
+ const store: AdaptSlotStore = {
28
+ element: null,
29
+ version: 0,
30
+ publish(element) {
31
+ store.element = element
32
+ },
33
+ clear() {
34
+ store.element = null
35
+ },
36
+ notify() {
37
+ store.version += 1
38
+ for (const listener of listeners) {
39
+ listener()
40
+ }
41
+ },
42
+ getSnapshot() {
43
+ return store.version
35
44
  },
36
- get: () => children,
37
45
  subscribe(callback) {
38
46
  listeners.add(callback)
39
- return () => listeners.delete(callback)
47
+ return () => {
48
+ listeners.delete(callback)
49
+ }
40
50
  },
41
51
  }
42
- }
43
-
44
- const AdaptChildrenStoreContext = createContext<AdaptChildrenStore | null>(null)
45
52
 
46
- const emptySubscribe = () => () => {}
47
- const emptyGet = () => null
48
-
49
- /** Renders adapt children from external store (used when teleport is enabled) */
50
- function TeleportAdaptContents() {
51
- const store = useContext(AdaptChildrenStoreContext)
52
- const children = React.useSyncExternalStore(
53
- store?.subscribe ?? emptySubscribe,
54
- store?.get ?? emptyGet,
55
- store?.get ?? emptyGet
56
- )
57
- return <>{children}</>
53
+ return store
58
54
  }
59
55
 
60
56
  /**
@@ -63,6 +59,34 @@ function TeleportAdaptContents() {
63
59
 
64
60
  export type AdaptWhen = MediaQueryKeyString | boolean | null
65
61
  export type AdaptPlatform = AllPlatforms | 'touch' | null
62
+ export type AdaptCapabilitiesValue = {
63
+ scroll?: boolean
64
+ overlay?: boolean
65
+ dismiss?: boolean
66
+ }
67
+
68
+ // structurally matches @tamagui/sheet's SheetTransitionEvent (adapt cannot
69
+ // import it — sheet depends on adapt). the adapt target reports its position
70
+ // transition here so the parent can release the presence latch on real close
71
+ // completion instead of a timer.
72
+ export type AdaptTargetTransitionEvent = {
73
+ phase: 'start' | 'end'
74
+ cause: 'open' | 'close' | 'snap'
75
+ finished?: boolean
76
+ }
77
+
78
+ export type AdaptTargetHandoff = {
79
+ hidden: boolean
80
+ skipNextAnimation?: boolean
81
+ onTransition: (e: AdaptTargetTransitionEvent) => void
82
+ }
83
+
84
+ export type AdaptTarget<State = unknown> = {
85
+ open?: boolean
86
+ onOpenChange?: (open: boolean) => void
87
+ handoff: AdaptTargetHandoff
88
+ state: State
89
+ }
66
90
 
67
91
  export type AdaptParentContextI = {
68
92
  Contents: Component
@@ -71,30 +95,102 @@ export type AdaptParentContextI = {
71
95
  setPlatform: (when: AdaptPlatform) => any
72
96
  when: AdaptWhen
73
97
  setWhen: (when: AdaptWhen) => any
98
+ active: boolean
99
+ rawActive: boolean
100
+ setRawActive: (active: boolean) => void
74
101
  portalName?: string
75
102
  lastScope?: string
103
+ slot: AdaptSlotStore | null
104
+ open?: boolean
105
+ onOpenChange?: (open: boolean) => void
106
+ state?: unknown
107
+ handoff: AdaptTargetHandoff
108
+ targetFullyHidden: boolean
109
+ registerTarget: () => void
110
+ unregisterTarget: () => void
111
+ registerContents: () => void
112
+ unregisterContents: () => void
113
+ registerRenderCallback: () => void
114
+ unregisterRenderCallback: () => void
76
115
  }
77
116
 
78
117
  type MediaQueryKeyString = MediaQueryKey extends string ? MediaQueryKey : never
79
118
 
119
+ export type AdaptRenderState<State = unknown> = {
120
+ active: boolean
121
+ open?: boolean
122
+ onOpenChange?: (open: boolean) => void
123
+ state: State
124
+ handoff: AdaptTargetHandoff
125
+ }
126
+
80
127
  export type AdaptProps = {
81
128
  scope?: string
82
129
  when?: AdaptWhen
83
130
  platform?: AdaptPlatform
84
- children: React.JSX.Element | ((children: React.ReactNode) => React.ReactNode)
131
+ children:
132
+ | React.JSX.Element
133
+ | ((contents: React.ReactNode, adapt: AdaptRenderState) => React.ReactNode)
85
134
  }
86
135
 
87
136
  type Component = (props: any) => any
88
137
 
89
- export const AdaptContext = createStyledContext<AdaptParentContextI>({
90
- Contents: null as any,
91
- scopeName: '',
92
- portalName: '',
93
- platform: null as any,
94
- setPlatform: (x: AdaptPlatform) => {},
95
- when: null as any,
96
- setWhen: () => {},
97
- })
138
+ const adaptContextKeys = [
139
+ 'Contents',
140
+ 'scopeName',
141
+ 'portalName',
142
+ 'platform',
143
+ 'setPlatform',
144
+ 'when',
145
+ 'setWhen',
146
+ 'active',
147
+ 'rawActive',
148
+ 'setRawActive',
149
+ 'slot',
150
+ 'handoff',
151
+ 'targetFullyHidden',
152
+ 'registerTarget',
153
+ 'unregisterTarget',
154
+ 'registerContents',
155
+ 'unregisterContents',
156
+ 'registerRenderCallback',
157
+ 'unregisterRenderCallback',
158
+ ] as const
159
+
160
+ export const AdaptContext = createStyledContext<
161
+ AdaptParentContextI,
162
+ (typeof adaptContextKeys)[number]
163
+ >(
164
+ {
165
+ Contents: null as any,
166
+ scopeName: '',
167
+ portalName: '',
168
+ platform: null as any,
169
+ setPlatform: (x: AdaptPlatform) => {},
170
+ when: null as any,
171
+ setWhen: () => {},
172
+ active: false,
173
+ rawActive: false,
174
+ setRawActive: () => {},
175
+ slot: null,
176
+ handoff: {
177
+ hidden: true,
178
+ onTransition: () => {},
179
+ },
180
+ targetFullyHidden: true,
181
+ registerTarget: () => {},
182
+ unregisterTarget: () => {},
183
+ registerContents: () => {},
184
+ unregisterContents: () => {},
185
+ registerRenderCallback: () => {},
186
+ unregisterRenderCallback: () => {},
187
+ },
188
+ {
189
+ keys: adaptContextKeys,
190
+ }
191
+ )
192
+
193
+ const AdaptCapabilitiesContext = createContext<AdaptCapabilitiesValue>({})
98
194
 
99
195
  const LastAdaptContextScope = createContext('')
100
196
 
@@ -132,6 +228,10 @@ type AdaptParentProps = {
132
228
  children?: React.ReactNode
133
229
  Contents?: AdaptParentContextI['Contents']
134
230
  scope: string
231
+ open?: boolean
232
+ onOpenChange?: (open: boolean) => void
233
+ state?: unknown
234
+ // unused, removed with the old paths in PRs B-E
135
235
  portal?:
136
236
  | boolean
137
237
  | {
@@ -139,79 +239,196 @@ type AdaptParentProps = {
139
239
  }
140
240
  }
141
241
 
142
- const AdaptPortals = new Map()
143
-
144
- export const AdaptParent = ({ children, Contents, scope, portal }: AdaptParentProps) => {
242
+ export const AdaptParent = ({
243
+ children,
244
+ Contents,
245
+ scope,
246
+ open,
247
+ onOpenChange,
248
+ state,
249
+ }: AdaptParentProps) => {
145
250
  const id = useId()
146
251
  const portalName = `AdaptPortal${scope}${id}`
147
252
 
148
- const childrenStoreRef = React.useRef<AdaptChildrenStore | null>(null)
149
- if (!childrenStoreRef.current) {
150
- childrenStoreRef.current = createAdaptChildrenStore()
253
+ const slotRef = React.useRef<AdaptSlotStore | null>(null)
254
+ if (!slotRef.current) {
255
+ slotRef.current = createAdaptSlotStore()
151
256
  }
152
257
 
153
- const isTeleport =
154
- process.env.TAMAGUI_TARGET === 'native' && getPortal().state.type === 'teleport'
258
+ const [when, setWhen] = React.useState<AdaptWhen>(null)
259
+ const [platform, setPlatform] = React.useState<AdaptPlatform>(null)
260
+ const [rawActive, setRawActive] = React.useState(false)
261
+ const [exiting, setExiting] = React.useState(false)
262
+ const [present, setPresent] = React.useState(false)
263
+ const [targetFullyHidden, setTargetFullyHidden] = React.useState(!open)
264
+ const targetCountRef = React.useRef(0)
265
+ const contentsCountRef = React.useRef(0)
266
+ const renderCallbackCountRef = React.useRef(0)
267
+ const rawActiveRef = React.useRef(false)
268
+ const openRef = React.useRef(open)
269
+ const wasTargetHiddenRef = React.useRef(!rawActive)
270
+ const hasHadActiveTargetRef = React.useRef(false)
271
+
272
+ const shouldStartExit = !rawActive && present && Boolean(open)
273
+ const active = rawActive || exiting || shouldStartExit
274
+ const targetHidden = !rawActive
275
+ const skipNextAnimation = Boolean(
276
+ rawActive && wasTargetHiddenRef.current && hasHadActiveTargetRef.current && open
277
+ )
278
+
279
+ rawActiveRef.current = rawActive
280
+ openRef.current = open
281
+ if (rawActive) {
282
+ hasHadActiveTargetRef.current = true
283
+ }
284
+ wasTargetHiddenRef.current = targetHidden
155
285
 
156
- const FinalContents = useMemo(() => {
157
- if (Contents) {
158
- return Contents
286
+ const releasePresenceLatch = React.useCallback(() => {
287
+ setExiting(false)
288
+ if (!rawActiveRef.current) {
289
+ setPresent(false)
159
290
  }
291
+ }, [])
160
292
 
161
- // when teleport is enabled, use store-based children passing to avoid
162
- // nested teleport (inner PortalItem inside already-teleported Sheet)
163
- // which breaks touch coordinate mapping on iOS.
164
- if (isTeleport) {
165
- return TeleportAdaptContents
293
+ useIsomorphicLayoutEffect(() => {
294
+ if (shouldStartExit) {
295
+ setExiting(true)
166
296
  }
297
+ }, [shouldStartExit])
167
298
 
168
- if (AdaptPortals.has(portalName)) {
169
- return AdaptPortals.get(portalName)
299
+ useIsomorphicLayoutEffect(() => {
300
+ if (open && rawActive) {
301
+ setTargetFullyHidden(false)
302
+ return
170
303
  }
171
304
 
172
- const element = () => {
173
- return (
174
- <PortalHost
175
- key={id}
176
- name={portalName}
177
- forwardProps={typeof portal === 'boolean' ? undefined : portal?.forwardProps}
178
- />
179
- )
305
+ // once closed and the target is no longer active (exit finished or never
306
+ // started), mark it hidden. an active exit waits for the target's
307
+ // close-complete transition to fire the handoff below.
308
+ if (!open && !active) {
309
+ setTargetFullyHidden(true)
180
310
  }
311
+ }, [active, open, rawActive])
181
312
 
182
- AdaptPortals.set(portalName, element)
313
+ useIsomorphicLayoutEffect(() => {
314
+ if (rawActive) {
315
+ setPresent(true)
316
+ } else if (!active) {
317
+ setPresent(false)
318
+ }
183
319
 
184
- return element
185
- }, [portalName, Contents, isTeleport])
320
+ if (rawActive && exiting) {
321
+ setExiting(false)
322
+ }
323
+ }, [active, exiting, rawActive])
324
+
325
+ const handoff = React.useMemo<AdaptTargetHandoff>(
326
+ () => ({
327
+ hidden: targetHidden,
328
+ skipNextAnimation,
329
+ onTransition(e) {
330
+ // only act on completed transitions; a started or interrupted
331
+ // (finished === false) transition leaves the latch as-is.
332
+ if (e.phase !== 'end' || e.finished === false) return
333
+
334
+ if (e.cause !== 'close') {
335
+ setTargetFullyHidden(false)
336
+ return
337
+ }
338
+
339
+ if (openRef.current && rawActiveRef.current) {
340
+ setTargetFullyHidden(false)
341
+ return
342
+ }
343
+
344
+ setTargetFullyHidden(true)
345
+ releasePresenceLatch()
346
+ },
347
+ }),
348
+ [releasePresenceLatch, skipNextAnimation, targetHidden]
349
+ )
186
350
 
187
- useIsomorphicLayoutEffect(() => {
188
- if (!isTeleport) {
189
- AdaptPortals.set(portalName, FinalContents)
190
- return () => {
191
- AdaptPortals.delete(portalName)
192
- }
351
+ const registerTarget = React.useCallback(() => {
352
+ targetCountRef.current += 1
353
+
354
+ if (process.env.NODE_ENV === 'development' && targetCountRef.current > 1) {
355
+ console.error(
356
+ `Adapt expected exactly one target in scope "${scope}", but ${targetCountRef.current} targets registered.`
357
+ )
193
358
  }
194
- }, [portalName, isTeleport])
359
+ }, [scope])
360
+
361
+ const unregisterTarget = React.useCallback(() => {
362
+ targetCountRef.current = Math.max(0, targetCountRef.current - 1)
363
+ }, [])
364
+
365
+ const registerContents = React.useCallback(() => {
366
+ contentsCountRef.current += 1
367
+ }, [])
368
+
369
+ const unregisterContents = React.useCallback(() => {
370
+ contentsCountRef.current = Math.max(0, contentsCountRef.current - 1)
371
+ }, [])
372
+
373
+ const registerRenderCallback = React.useCallback(() => {
374
+ renderCallbackCountRef.current += 1
375
+ }, [])
376
+
377
+ const unregisterRenderCallback = React.useCallback(() => {
378
+ renderCallbackCountRef.current = Math.max(0, renderCallbackCountRef.current - 1)
379
+ }, [])
380
+
381
+ React.useEffect(() => {
382
+ if (process.env.NODE_ENV !== 'development') return
383
+ if (!active) return
384
+
385
+ const timer = setTimeout(() => {
386
+ if (
387
+ rawActiveRef.current &&
388
+ targetCountRef.current === 0 &&
389
+ contentsCountRef.current === 0 &&
390
+ renderCallbackCountRef.current === 0
391
+ ) {
392
+ console.error(
393
+ `Adapt is active in scope "${scope}" but no target registered with useAdaptTarget(), Adapt.Contents marker, or render callback consumed it.`
394
+ )
395
+ }
396
+ })
195
397
 
196
- const [when, setWhen] = React.useState<AdaptWhen>(null)
197
- const [platform, setPlatform] = React.useState<AdaptPlatform>(null)
398
+ return () => clearTimeout(timer)
399
+ }, [active, scope])
400
+
401
+ const FinalContents = Contents || AdaptSlotContents
198
402
 
199
403
  return (
200
- <AdaptChildrenStoreContext.Provider value={childrenStoreRef.current}>
201
- <LastAdaptContextScope.Provider value={scope}>
202
- <ProvideAdaptContext
203
- Contents={FinalContents}
204
- when={when}
205
- platform={platform}
206
- setPlatform={setPlatform}
207
- setWhen={setWhen}
208
- portalName={portalName}
209
- scopeName={scope}
210
- >
211
- {children}
212
- </ProvideAdaptContext>
213
- </LastAdaptContextScope.Provider>
214
- </AdaptChildrenStoreContext.Provider>
404
+ <LastAdaptContextScope.Provider value={scope}>
405
+ <ProvideAdaptContext
406
+ Contents={FinalContents}
407
+ when={when}
408
+ platform={platform}
409
+ setPlatform={setPlatform}
410
+ setWhen={setWhen}
411
+ active={active}
412
+ rawActive={rawActive}
413
+ setRawActive={setRawActive}
414
+ portalName={portalName}
415
+ scopeName={scope}
416
+ slot={slotRef.current}
417
+ open={open}
418
+ onOpenChange={onOpenChange}
419
+ state={state}
420
+ handoff={handoff}
421
+ targetFullyHidden={targetFullyHidden}
422
+ registerTarget={registerTarget}
423
+ unregisterTarget={unregisterTarget}
424
+ registerContents={registerContents}
425
+ unregisterContents={unregisterContents}
426
+ registerRenderCallback={registerRenderCallback}
427
+ unregisterRenderCallback={unregisterRenderCallback}
428
+ >
429
+ {children}
430
+ </ProvideAdaptContext>
431
+ </LastAdaptContextScope.Provider>
215
432
  )
216
433
  }
217
434
 
@@ -230,8 +447,17 @@ export const AdaptContents = ({ scope, ...rest }: { scope?: string }) => {
230
447
  )
231
448
  }
232
449
 
450
+ useIsomorphicLayoutEffect(() => {
451
+ if (!context.active) return
452
+
453
+ context.registerContents()
454
+ return () => {
455
+ context.unregisterContents()
456
+ }
457
+ }, [context.active, context.registerContents, context.unregisterContents])
458
+
233
459
  // forwards props - see shouldForwardSpace
234
- return React.createElement(context.Contents, { ...rest, key: `stable` })
460
+ return React.createElement(context.Contents, { ...rest, scope, key: `stable` })
235
461
  }
236
462
 
237
463
  AdaptContents.shouldForwardSpace = true
@@ -240,25 +466,50 @@ export const Adapt = withStaticProperties(
240
466
  function Adapt(props: AdaptProps) {
241
467
  const { platform, when, children, scope } = props
242
468
  const context = useAdaptContext(scope)
243
- const enabled = useAdaptIsActiveGiven(props)
469
+ const rawEnabled = useAdaptIsActiveGiven(props)
470
+ const enabled = rawEnabled || context.active
471
+ const isRenderCallback = typeof children === 'function'
244
472
 
245
473
  useIsomorphicLayoutEffect(() => {
246
- context?.setWhen?.((when || enabled) as AdaptWhen)
474
+ context?.setWhen?.((when || rawEnabled) as AdaptWhen)
247
475
  context?.setPlatform?.(platform || null)
248
- }, [when, platform, enabled, context.setWhen, context.setPlatform])
476
+ context?.setRawActive?.(rawEnabled)
477
+ }, [
478
+ when,
479
+ platform,
480
+ rawEnabled,
481
+ context.setWhen,
482
+ context.setPlatform,
483
+ context.setRawActive,
484
+ ])
485
+
486
+ useIsomorphicLayoutEffect(() => {
487
+ if (!enabled || !isRenderCallback) return
488
+
489
+ context.registerRenderCallback()
490
+ return () => {
491
+ context.unregisterRenderCallback()
492
+ }
493
+ }, [
494
+ enabled,
495
+ isRenderCallback,
496
+ context.registerRenderCallback,
497
+ context.unregisterRenderCallback,
498
+ ])
249
499
 
250
500
  useIsomorphicLayoutEffect(() => {
251
501
  return () => {
252
502
  context?.setWhen?.(null)
253
503
  context?.setPlatform?.(null)
504
+ context?.setRawActive?.(false)
254
505
  }
255
506
  }, [])
256
507
 
257
508
  let output: React.ReactNode
258
509
 
259
- if (typeof children === 'function') {
510
+ if (isRenderCallback) {
260
511
  const Component = context?.Contents
261
- output = children(Component ? <Component /> : null)
512
+ output = children(Component ? <Component /> : null, getAdaptRenderState(context))
262
513
  } else {
263
514
  output = children
264
515
  }
@@ -276,55 +527,70 @@ export const AdaptPortalContents = (props: {
276
527
  passThrough?: boolean
277
528
  }) => {
278
529
  const isActive = useAdaptIsActive(props.scope)
279
- const { portalName } = useAdaptContext(props.scope)
280
- const childrenStore = useContext(AdaptChildrenStoreContext)
281
- const isTeleport = !isWeb && getPortal().state.type === 'teleport'
282
-
283
- // when teleport is enabled, bypass PortalItem to avoid nested teleport
284
- // (inner portal inside already-teleported Sheet) which breaks touch
285
- // coordinate mapping on iOS. children are passed via external store
286
- // to TeleportAdaptContents rendered at Adapt.Contents.
287
- if (isTeleport && childrenStore) {
288
- return (
289
- <AdaptPortalTeleport isActive={isActive} store={childrenStore}>
290
- {props.children}
291
- </AdaptPortalTeleport>
292
- )
293
- }
530
+ const { slot } = useAdaptContext(props.scope)
294
531
 
295
532
  return (
296
- <PortalItem passThrough={!isActive} hostName={portalName}>
533
+ <AdaptSlotPublisher isActive={isActive} slot={slot}>
297
534
  {props.children}
298
- </PortalItem>
535
+ </AdaptSlotPublisher>
299
536
  )
300
537
  }
301
538
 
302
- function AdaptPortalTeleport({
539
+ function AdaptSlotContents({ scope }: { scope?: string }) {
540
+ const { slot } = useAdaptContext(scope)
541
+
542
+ React.useSyncExternalStore(
543
+ slot?.subscribe ?? emptySubscribe,
544
+ slot?.getSnapshot ?? emptySnapshot,
545
+ slot?.getSnapshot ?? emptySnapshot
546
+ )
547
+
548
+ return <>{slot?.element ?? null}</>
549
+ }
550
+
551
+ function AdaptSlotPublisher({
303
552
  isActive,
304
- store,
553
+ slot,
305
554
  children,
306
555
  }: {
307
556
  isActive: boolean
308
- store: AdaptChildrenStore
557
+ slot: AdaptSlotStore | null
309
558
  children: React.ReactNode
310
559
  }) {
311
- // keep the teleport store in sync without clearing it between parent
312
- // renders. clearing only on real unmount or inactive lets React reconcile
313
- // sheet/dialog/popover contents instead of remounting the subtree.
560
+ const publishedRef = React.useRef(false)
561
+
562
+ // Publish the live element value after every commit. Do not memoize this handoff:
563
+ // stale element deps caused the Sheet overlay-hoist regression this replaces.
314
564
  useIsomorphicLayoutEffect(() => {
565
+ if (!slot) return
566
+
315
567
  if (isActive) {
316
- store.set(children)
568
+ slot.publish(children)
569
+ publishedRef.current = true
317
570
  } else {
318
- store.set(null)
571
+ slot.clear()
572
+ publishedRef.current = false
319
573
  }
574
+
575
+ slot.notify()
320
576
  })
577
+
321
578
  useIsomorphicLayoutEffect(() => {
322
- return () => store.set(null)
323
- }, [store])
579
+ return () => {
580
+ if (publishedRef.current) {
581
+ slot?.clear()
582
+ slot?.notify()
583
+ publishedRef.current = false
584
+ }
585
+ }
586
+ }, [slot])
324
587
 
325
588
  return isActive ? null : <>{children}</>
326
589
  }
327
590
 
591
+ const emptySubscribe = () => () => {}
592
+ const emptySnapshot = () => 0
593
+
328
594
  const useAdaptIsActiveGiven = ({
329
595
  when,
330
596
  platform,
@@ -360,5 +626,69 @@ const useAdaptIsActiveGiven = ({
360
626
 
361
627
  export const useAdaptIsActive = (scope?: string) => {
362
628
  const props = useAdaptContext(scope)
363
- return useAdaptIsActiveGiven(props)
629
+ const isActiveGiven = useAdaptIsActiveGiven(props)
630
+ return props.active || isActiveGiven
631
+ }
632
+
633
+ export function useAdaptTarget<State = unknown>(
634
+ scope?: string
635
+ ): AdaptTarget<State> | null {
636
+ const context = useAdaptContext(scope)
637
+
638
+ useIsomorphicLayoutEffect(() => {
639
+ if (!context.active) return
640
+
641
+ context.registerTarget()
642
+ return () => {
643
+ context.unregisterTarget()
644
+ }
645
+ }, [context.active, context.registerTarget, context.unregisterTarget])
646
+
647
+ if (!context.active) {
648
+ return null
649
+ }
650
+
651
+ return {
652
+ open: context.open,
653
+ onOpenChange: context.onOpenChange,
654
+ handoff: context.handoff,
655
+ state: context.state as State,
656
+ }
657
+ }
658
+
659
+ export const AdaptCapabilities = ({
660
+ children,
661
+ scroll,
662
+ overlay,
663
+ dismiss,
664
+ }: AdaptCapabilitiesValue & { children?: React.ReactNode }) => {
665
+ const parent = useContext(AdaptCapabilitiesContext)
666
+ const value = React.useMemo(
667
+ () => ({
668
+ scroll: scroll ?? parent.scroll,
669
+ overlay: overlay ?? parent.overlay,
670
+ dismiss: dismiss ?? parent.dismiss,
671
+ }),
672
+ [dismiss, overlay, parent.dismiss, parent.overlay, parent.scroll, scroll]
673
+ )
674
+
675
+ return (
676
+ <AdaptCapabilitiesContext.Provider value={value}>
677
+ {children}
678
+ </AdaptCapabilitiesContext.Provider>
679
+ )
680
+ }
681
+
682
+ export const useAdaptedCapabilities = () => {
683
+ return useContext(AdaptCapabilitiesContext)
684
+ }
685
+
686
+ function getAdaptRenderState(context: AdaptParentContextI): AdaptRenderState {
687
+ return {
688
+ active: context.active,
689
+ open: context.open,
690
+ onOpenChange: context.onOpenChange,
691
+ state: context.state,
692
+ handoff: context.handoff,
693
+ }
364
694
  }