@tamagui/adapt 2.7.7 → 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/dist/cjs/Adapt.cjs +449 -206
- package/dist/cjs/Adapt.native.cjs +522 -0
- package/dist/cjs/Adapt.native.js +488 -253
- package/dist/cjs/Adapt.native.js.map +1 -1
- package/dist/cjs/index.cjs +10 -11
- package/dist/cjs/index.native.cjs +21 -0
- package/dist/cjs/index.native.js +10 -10
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/esm/Adapt.mjs +425 -177
- package/dist/esm/Adapt.mjs.map +1 -1
- package/dist/esm/Adapt.native.js +464 -224
- package/dist/esm/Adapt.native.js.map +1 -1
- package/dist/esm/index.js +1 -2
- package/dist/esm/index.mjs +1 -2
- package/dist/esm/index.native.js +1 -2
- package/package.json +7 -7
- package/src/Adapt.tsx +463 -133
- package/types/Adapt.d.ts +73 -21
- package/types/Adapt.d.ts.map +1 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/index.mjs.map +0 -1
- package/dist/esm/index.native.js.map +0 -1
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
29
|
-
let children: React.ReactNode = null
|
|
24
|
+
function createAdaptSlotStore(): AdaptSlotStore {
|
|
30
25
|
const listeners = new Set<() => void>()
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 () =>
|
|
47
|
+
return () => {
|
|
48
|
+
listeners.delete(callback)
|
|
49
|
+
}
|
|
40
50
|
},
|
|
41
51
|
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const AdaptChildrenStoreContext = createContext<AdaptChildrenStore | null>(null)
|
|
45
52
|
|
|
46
|
-
|
|
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:
|
|
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
|
-
|
|
90
|
-
Contents
|
|
91
|
-
scopeName
|
|
92
|
-
portalName
|
|
93
|
-
platform
|
|
94
|
-
setPlatform
|
|
95
|
-
when
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
149
|
-
if (!
|
|
150
|
-
|
|
253
|
+
const slotRef = React.useRef<AdaptSlotStore | null>(null)
|
|
254
|
+
if (!slotRef.current) {
|
|
255
|
+
slotRef.current = createAdaptSlotStore()
|
|
151
256
|
}
|
|
152
257
|
|
|
153
|
-
const
|
|
154
|
-
|
|
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
|
|
157
|
-
|
|
158
|
-
|
|
286
|
+
const releasePresenceLatch = React.useCallback(() => {
|
|
287
|
+
setExiting(false)
|
|
288
|
+
if (!rawActiveRef.current) {
|
|
289
|
+
setPresent(false)
|
|
159
290
|
}
|
|
291
|
+
}, [])
|
|
160
292
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (isTeleport) {
|
|
165
|
-
return TeleportAdaptContents
|
|
293
|
+
useIsomorphicLayoutEffect(() => {
|
|
294
|
+
if (shouldStartExit) {
|
|
295
|
+
setExiting(true)
|
|
166
296
|
}
|
|
297
|
+
}, [shouldStartExit])
|
|
167
298
|
|
|
168
|
-
|
|
169
|
-
|
|
299
|
+
useIsomorphicLayoutEffect(() => {
|
|
300
|
+
if (open && rawActive) {
|
|
301
|
+
setTargetFullyHidden(false)
|
|
302
|
+
return
|
|
170
303
|
}
|
|
171
304
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
313
|
+
useIsomorphicLayoutEffect(() => {
|
|
314
|
+
if (rawActive) {
|
|
315
|
+
setPresent(true)
|
|
316
|
+
} else if (!active) {
|
|
317
|
+
setPresent(false)
|
|
318
|
+
}
|
|
183
319
|
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
}, [
|
|
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
|
-
|
|
197
|
-
|
|
398
|
+
return () => clearTimeout(timer)
|
|
399
|
+
}, [active, scope])
|
|
400
|
+
|
|
401
|
+
const FinalContents = Contents || AdaptSlotContents
|
|
198
402
|
|
|
199
403
|
return (
|
|
200
|
-
<
|
|
201
|
-
<
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
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 ||
|
|
474
|
+
context?.setWhen?.((when || rawEnabled) as AdaptWhen)
|
|
247
475
|
context?.setPlatform?.(platform || null)
|
|
248
|
-
|
|
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 (
|
|
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 {
|
|
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
|
-
<
|
|
533
|
+
<AdaptSlotPublisher isActive={isActive} slot={slot}>
|
|
297
534
|
{props.children}
|
|
298
|
-
</
|
|
535
|
+
</AdaptSlotPublisher>
|
|
299
536
|
)
|
|
300
537
|
}
|
|
301
538
|
|
|
302
|
-
function
|
|
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
|
-
|
|
553
|
+
slot,
|
|
305
554
|
children,
|
|
306
555
|
}: {
|
|
307
556
|
isActive: boolean
|
|
308
|
-
|
|
557
|
+
slot: AdaptSlotStore | null
|
|
309
558
|
children: React.ReactNode
|
|
310
559
|
}) {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
//
|
|
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
|
-
|
|
568
|
+
slot.publish(children)
|
|
569
|
+
publishedRef.current = true
|
|
317
570
|
} else {
|
|
318
|
-
|
|
571
|
+
slot.clear()
|
|
572
|
+
publishedRef.current = false
|
|
319
573
|
}
|
|
574
|
+
|
|
575
|
+
slot.notify()
|
|
320
576
|
})
|
|
577
|
+
|
|
321
578
|
useIsomorphicLayoutEffect(() => {
|
|
322
|
-
return () =>
|
|
323
|
-
|
|
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
|
-
|
|
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
|
}
|