@tamagui/adapt 2.7.7 → 3.0.0-beta.1093.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 +460 -206
- package/dist/cjs/Adapt.native.cjs +551 -0
- package/dist/cjs/Adapt.native.js +517 -253
- package/dist/cjs/Adapt.native.js.map +1 -1
- package/dist/cjs/index.cjs +9 -11
- package/dist/cjs/index.native.cjs +20 -0
- package/dist/cjs/index.native.js +9 -10
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/esm/Adapt.mjs +437 -177
- package/dist/esm/Adapt.mjs.map +1 -1
- package/dist/esm/Adapt.native.js +494 -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 +9 -8
- package/src/Adapt.tsx +505 -152
- package/types/Adapt.d.ts +74 -29
- 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
52
|
|
|
44
|
-
|
|
45
|
-
|
|
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,38 +59,125 @@ 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
|
+
}
|
|
90
|
+
|
|
91
|
+
export type AdaptConfig = Pick<AdaptProps, 'when' | 'platform'>
|
|
66
92
|
|
|
67
93
|
export type AdaptParentContextI = {
|
|
68
94
|
Contents: Component
|
|
69
95
|
scopeName: string
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
when: AdaptWhen
|
|
73
|
-
setWhen: (when: AdaptWhen) => any
|
|
96
|
+
active: boolean
|
|
97
|
+
setAdaptConfig: (config: AdaptConfig | null) => void
|
|
74
98
|
portalName?: string
|
|
75
99
|
lastScope?: string
|
|
100
|
+
slot: AdaptSlotStore | null
|
|
101
|
+
open?: boolean
|
|
102
|
+
onOpenChange?: (open: boolean) => void
|
|
103
|
+
state?: unknown
|
|
104
|
+
handoff: AdaptTargetHandoff
|
|
105
|
+
targetFullyHidden: boolean
|
|
106
|
+
registerTarget: () => void
|
|
107
|
+
unregisterTarget: () => void
|
|
108
|
+
registerContents: () => void
|
|
109
|
+
unregisterContents: () => void
|
|
110
|
+
registerRenderCallback: () => void
|
|
111
|
+
unregisterRenderCallback: () => void
|
|
76
112
|
}
|
|
77
113
|
|
|
78
114
|
type MediaQueryKeyString = MediaQueryKey extends string ? MediaQueryKey : never
|
|
79
115
|
|
|
116
|
+
export type AdaptRenderState<State = unknown> = {
|
|
117
|
+
active: boolean
|
|
118
|
+
open?: boolean
|
|
119
|
+
onOpenChange?: (open: boolean) => void
|
|
120
|
+
state: State
|
|
121
|
+
handoff: AdaptTargetHandoff
|
|
122
|
+
}
|
|
123
|
+
|
|
80
124
|
export type AdaptProps = {
|
|
81
125
|
scope?: string
|
|
82
126
|
when?: AdaptWhen
|
|
83
127
|
platform?: AdaptPlatform
|
|
84
|
-
children:
|
|
128
|
+
children:
|
|
129
|
+
| React.JSX.Element
|
|
130
|
+
| ((contents: React.ReactNode, adapt: AdaptRenderState) => React.ReactNode)
|
|
85
131
|
}
|
|
86
132
|
|
|
87
133
|
type Component = (props: any) => any
|
|
88
134
|
|
|
89
|
-
|
|
90
|
-
Contents
|
|
91
|
-
scopeName
|
|
92
|
-
portalName
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
135
|
+
const adaptContextKeys = [
|
|
136
|
+
'Contents',
|
|
137
|
+
'scopeName',
|
|
138
|
+
'portalName',
|
|
139
|
+
'active',
|
|
140
|
+
'setAdaptConfig',
|
|
141
|
+
'slot',
|
|
142
|
+
'handoff',
|
|
143
|
+
'targetFullyHidden',
|
|
144
|
+
'registerTarget',
|
|
145
|
+
'unregisterTarget',
|
|
146
|
+
'registerContents',
|
|
147
|
+
'unregisterContents',
|
|
148
|
+
'registerRenderCallback',
|
|
149
|
+
'unregisterRenderCallback',
|
|
150
|
+
] as const
|
|
151
|
+
|
|
152
|
+
export const AdaptContext = createStyledContext<
|
|
153
|
+
AdaptParentContextI,
|
|
154
|
+
(typeof adaptContextKeys)[number]
|
|
155
|
+
>(
|
|
156
|
+
{
|
|
157
|
+
Contents: null as any,
|
|
158
|
+
scopeName: '',
|
|
159
|
+
portalName: '',
|
|
160
|
+
active: false,
|
|
161
|
+
setAdaptConfig: () => {},
|
|
162
|
+
slot: null,
|
|
163
|
+
handoff: {
|
|
164
|
+
hidden: true,
|
|
165
|
+
onTransition: () => {},
|
|
166
|
+
},
|
|
167
|
+
targetFullyHidden: true,
|
|
168
|
+
registerTarget: () => {},
|
|
169
|
+
unregisterTarget: () => {},
|
|
170
|
+
registerContents: () => {},
|
|
171
|
+
unregisterContents: () => {},
|
|
172
|
+
registerRenderCallback: () => {},
|
|
173
|
+
unregisterRenderCallback: () => {},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
keys: adaptContextKeys,
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
const AdaptCapabilitiesContext = createContext<AdaptCapabilitiesValue>({})
|
|
98
181
|
|
|
99
182
|
const LastAdaptContextScope = createContext('')
|
|
100
183
|
|
|
@@ -130,88 +213,252 @@ export const useAdaptContext = (scope?: string) => {
|
|
|
130
213
|
|
|
131
214
|
type AdaptParentProps = {
|
|
132
215
|
children?: React.ReactNode
|
|
216
|
+
// the children the user gave the adapting component, where their <Adapt /> lives.
|
|
217
|
+
// AdaptParent's own children are that component's internals, one wrapper deep.
|
|
218
|
+
adaptChildren?: React.ReactNode
|
|
133
219
|
Contents?: AdaptParentContextI['Contents']
|
|
134
220
|
scope: string
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
forwardProps?: any
|
|
139
|
-
}
|
|
221
|
+
open?: boolean
|
|
222
|
+
onOpenChange?: (open: boolean) => void
|
|
223
|
+
state?: unknown
|
|
140
224
|
}
|
|
141
225
|
|
|
142
|
-
|
|
226
|
+
// an <Adapt /> written into the adapting component's children is read off the
|
|
227
|
+
// element tree during render. learning it from the child's layout effect left
|
|
228
|
+
// every consumer seeing "inactive" on the first commit, so an adapted Select
|
|
229
|
+
// mounted SelectInlineImpl and swapped it for SelectSheetImpl one commit later,
|
|
230
|
+
// remounting the whole subtree. only direct children are scanned, descending
|
|
231
|
+
// into fragments: any deeper and a nested adapting component's own <Adapt />
|
|
232
|
+
// would adapt this one too. Children.toArray already flattens arrays and drops
|
|
233
|
+
// false, so `{cond && <Adapt />}` still reads. an <Adapt /> that a userland
|
|
234
|
+
// component renders is not in this tree, so it reports itself from its effect
|
|
235
|
+
// instead and still costs that extra commit.
|
|
236
|
+
function findAdaptConfig(children: React.ReactNode, scope: string): AdaptConfig | null {
|
|
237
|
+
for (const child of React.Children.toArray(children)) {
|
|
238
|
+
if (!React.isValidElement(child)) continue
|
|
239
|
+
|
|
240
|
+
if (child.type === React.Fragment) {
|
|
241
|
+
const found = findAdaptConfig(
|
|
242
|
+
(child.props as { children?: React.ReactNode }).children,
|
|
243
|
+
scope
|
|
244
|
+
)
|
|
245
|
+
if (found) return found
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (child.type === Adapt) {
|
|
250
|
+
const childProps = child.props as AdaptProps
|
|
251
|
+
if (childProps.scope == null || childProps.scope === scope) {
|
|
252
|
+
return childProps
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
143
256
|
|
|
144
|
-
|
|
257
|
+
return null
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export const AdaptParent = ({
|
|
261
|
+
children,
|
|
262
|
+
adaptChildren,
|
|
263
|
+
Contents,
|
|
264
|
+
scope,
|
|
265
|
+
open,
|
|
266
|
+
onOpenChange,
|
|
267
|
+
state,
|
|
268
|
+
}: AdaptParentProps) => {
|
|
145
269
|
const id = useId()
|
|
146
270
|
const portalName = `AdaptPortal${scope}${id}`
|
|
147
271
|
|
|
148
|
-
const
|
|
149
|
-
if (!
|
|
150
|
-
|
|
272
|
+
const slotRef = React.useRef<AdaptSlotStore | null>(null)
|
|
273
|
+
if (!slotRef.current) {
|
|
274
|
+
slotRef.current = createAdaptSlotStore()
|
|
151
275
|
}
|
|
152
276
|
|
|
153
|
-
const
|
|
154
|
-
|
|
277
|
+
const staticConfig = React.useMemo(
|
|
278
|
+
() => findAdaptConfig(adaptChildren, scope) ?? findAdaptConfig(children, scope),
|
|
279
|
+
[adaptChildren, children, scope]
|
|
280
|
+
)
|
|
281
|
+
const [publishedConfig, setPublishedConfig] = React.useState<AdaptConfig | null>(null)
|
|
282
|
+
const rawActive = useAdaptIsActiveGiven(staticConfig ?? publishedConfig)
|
|
283
|
+
|
|
284
|
+
// written during render because a child's layout effect calls setAdaptConfig
|
|
285
|
+
// before this component's own effects run
|
|
286
|
+
const staticConfigRef = React.useRef(staticConfig)
|
|
287
|
+
staticConfigRef.current = staticConfig
|
|
288
|
+
|
|
289
|
+
const setAdaptConfig = React.useCallback((config: AdaptConfig | null) => {
|
|
290
|
+
if (staticConfigRef.current) return
|
|
291
|
+
setPublishedConfig(config)
|
|
292
|
+
}, [])
|
|
293
|
+
|
|
294
|
+
const [exiting, setExiting] = React.useState(false)
|
|
295
|
+
const [present, setPresent] = React.useState(false)
|
|
296
|
+
const [targetFullyHidden, setTargetFullyHidden] = React.useState(!open)
|
|
297
|
+
const targetCountRef = React.useRef(0)
|
|
298
|
+
const contentsCountRef = React.useRef(0)
|
|
299
|
+
const renderCallbackCountRef = React.useRef(0)
|
|
300
|
+
const rawActiveRef = React.useRef(false)
|
|
301
|
+
const openRef = React.useRef(open)
|
|
302
|
+
const wasTargetHiddenRef = React.useRef(!rawActive)
|
|
303
|
+
const hasHadActiveTargetRef = React.useRef(false)
|
|
304
|
+
|
|
305
|
+
const shouldStartExit = !rawActive && present && Boolean(open)
|
|
306
|
+
const active = rawActive || exiting || shouldStartExit
|
|
307
|
+
const targetHidden = !rawActive
|
|
308
|
+
const skipNextAnimation = Boolean(
|
|
309
|
+
rawActive && wasTargetHiddenRef.current && hasHadActiveTargetRef.current && open
|
|
310
|
+
)
|
|
155
311
|
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
312
|
+
const releasePresenceLatch = React.useCallback(() => {
|
|
313
|
+
setExiting(false)
|
|
314
|
+
if (!rawActiveRef.current) {
|
|
315
|
+
setPresent(false)
|
|
159
316
|
}
|
|
317
|
+
}, [])
|
|
160
318
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (isTeleport) {
|
|
165
|
-
return TeleportAdaptContents
|
|
319
|
+
useIsomorphicLayoutEffect(() => {
|
|
320
|
+
if (shouldStartExit) {
|
|
321
|
+
setExiting(true)
|
|
166
322
|
}
|
|
323
|
+
}, [shouldStartExit])
|
|
167
324
|
|
|
168
|
-
|
|
169
|
-
|
|
325
|
+
useIsomorphicLayoutEffect(() => {
|
|
326
|
+
// committed snapshot: skipNextAnimation compares the next render against
|
|
327
|
+
// these, and onTransition reads them non-reactively
|
|
328
|
+
rawActiveRef.current = rawActive
|
|
329
|
+
openRef.current = open
|
|
330
|
+
if (rawActive) {
|
|
331
|
+
hasHadActiveTargetRef.current = true
|
|
170
332
|
}
|
|
333
|
+
wasTargetHiddenRef.current = targetHidden
|
|
171
334
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
key={id}
|
|
176
|
-
name={portalName}
|
|
177
|
-
forwardProps={typeof portal === 'boolean' ? undefined : portal?.forwardProps}
|
|
178
|
-
/>
|
|
179
|
-
)
|
|
335
|
+
if (open && rawActive) {
|
|
336
|
+
setTargetFullyHidden(false)
|
|
337
|
+
return
|
|
180
338
|
}
|
|
181
339
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
340
|
+
// once closed and the target is no longer active (exit finished or never
|
|
341
|
+
// started), mark it hidden. an active exit waits for the target's
|
|
342
|
+
// close-complete transition to fire the handoff below.
|
|
343
|
+
if (!open && !active) {
|
|
344
|
+
setTargetFullyHidden(true)
|
|
345
|
+
}
|
|
346
|
+
}, [active, open, rawActive, targetHidden])
|
|
186
347
|
|
|
187
348
|
useIsomorphicLayoutEffect(() => {
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
349
|
+
if (rawActive) {
|
|
350
|
+
setPresent(true)
|
|
351
|
+
} else if (!active) {
|
|
352
|
+
setPresent(false)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (rawActive && exiting) {
|
|
356
|
+
setExiting(false)
|
|
193
357
|
}
|
|
194
|
-
}, [
|
|
358
|
+
}, [active, exiting, rawActive])
|
|
359
|
+
|
|
360
|
+
const handoff = React.useMemo<AdaptTargetHandoff>(
|
|
361
|
+
() => ({
|
|
362
|
+
hidden: targetHidden,
|
|
363
|
+
skipNextAnimation,
|
|
364
|
+
onTransition(e) {
|
|
365
|
+
// only act on completed transitions; a started or interrupted
|
|
366
|
+
// (finished === false) transition leaves the latch as-is.
|
|
367
|
+
if (e.phase !== 'end' || e.finished === false) return
|
|
368
|
+
|
|
369
|
+
if (e.cause !== 'close') {
|
|
370
|
+
setTargetFullyHidden(false)
|
|
371
|
+
return
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (openRef.current && rawActiveRef.current) {
|
|
375
|
+
setTargetFullyHidden(false)
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
setTargetFullyHidden(true)
|
|
380
|
+
releasePresenceLatch()
|
|
381
|
+
},
|
|
382
|
+
}),
|
|
383
|
+
[releasePresenceLatch, skipNextAnimation, targetHidden]
|
|
384
|
+
)
|
|
195
385
|
|
|
196
|
-
const
|
|
197
|
-
|
|
386
|
+
const registerTarget = React.useCallback(() => {
|
|
387
|
+
targetCountRef.current += 1
|
|
388
|
+
|
|
389
|
+
if (process.env.NODE_ENV === 'development' && targetCountRef.current > 1) {
|
|
390
|
+
console.error(
|
|
391
|
+
`Adapt expected exactly one target in scope "${scope}", but ${targetCountRef.current} targets registered.`
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
}, [scope])
|
|
395
|
+
|
|
396
|
+
const unregisterTarget = React.useCallback(() => {
|
|
397
|
+
targetCountRef.current = Math.max(0, targetCountRef.current - 1)
|
|
398
|
+
}, [])
|
|
399
|
+
|
|
400
|
+
const registerContents = React.useCallback(() => {
|
|
401
|
+
contentsCountRef.current += 1
|
|
402
|
+
}, [])
|
|
403
|
+
|
|
404
|
+
const unregisterContents = React.useCallback(() => {
|
|
405
|
+
contentsCountRef.current = Math.max(0, contentsCountRef.current - 1)
|
|
406
|
+
}, [])
|
|
407
|
+
|
|
408
|
+
const registerRenderCallback = React.useCallback(() => {
|
|
409
|
+
renderCallbackCountRef.current += 1
|
|
410
|
+
}, [])
|
|
411
|
+
|
|
412
|
+
const unregisterRenderCallback = React.useCallback(() => {
|
|
413
|
+
renderCallbackCountRef.current = Math.max(0, renderCallbackCountRef.current - 1)
|
|
414
|
+
}, [])
|
|
415
|
+
|
|
416
|
+
React.useEffect(() => {
|
|
417
|
+
if (process.env.NODE_ENV !== 'development') return
|
|
418
|
+
if (!active) return
|
|
419
|
+
|
|
420
|
+
const timer = setTimeout(() => {
|
|
421
|
+
if (
|
|
422
|
+
rawActiveRef.current &&
|
|
423
|
+
targetCountRef.current === 0 &&
|
|
424
|
+
contentsCountRef.current === 0 &&
|
|
425
|
+
renderCallbackCountRef.current === 0
|
|
426
|
+
) {
|
|
427
|
+
console.error(
|
|
428
|
+
`Adapt is active in scope "${scope}" but no target registered with useAdaptTarget(), Adapt.Contents marker, or render callback consumed it.`
|
|
429
|
+
)
|
|
430
|
+
}
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
return () => clearTimeout(timer)
|
|
434
|
+
}, [active, scope])
|
|
435
|
+
|
|
436
|
+
const FinalContents = Contents || AdaptSlotContents
|
|
198
437
|
|
|
199
438
|
return (
|
|
200
|
-
<
|
|
201
|
-
<
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
439
|
+
<LastAdaptContextScope.Provider value={scope}>
|
|
440
|
+
<ProvideAdaptContext
|
|
441
|
+
Contents={FinalContents}
|
|
442
|
+
active={active}
|
|
443
|
+
setAdaptConfig={setAdaptConfig}
|
|
444
|
+
portalName={portalName}
|
|
445
|
+
scopeName={scope}
|
|
446
|
+
slot={slotRef.current}
|
|
447
|
+
open={open}
|
|
448
|
+
onOpenChange={onOpenChange}
|
|
449
|
+
state={state}
|
|
450
|
+
handoff={handoff}
|
|
451
|
+
targetFullyHidden={targetFullyHidden}
|
|
452
|
+
registerTarget={registerTarget}
|
|
453
|
+
unregisterTarget={unregisterTarget}
|
|
454
|
+
registerContents={registerContents}
|
|
455
|
+
unregisterContents={unregisterContents}
|
|
456
|
+
registerRenderCallback={registerRenderCallback}
|
|
457
|
+
unregisterRenderCallback={unregisterRenderCallback}
|
|
458
|
+
>
|
|
459
|
+
{children}
|
|
460
|
+
</ProvideAdaptContext>
|
|
461
|
+
</LastAdaptContextScope.Provider>
|
|
215
462
|
)
|
|
216
463
|
}
|
|
217
464
|
|
|
@@ -230,35 +477,56 @@ export const AdaptContents = ({ scope, ...rest }: { scope?: string }) => {
|
|
|
230
477
|
)
|
|
231
478
|
}
|
|
232
479
|
|
|
480
|
+
useIsomorphicLayoutEffect(() => {
|
|
481
|
+
if (!context.active) return
|
|
482
|
+
|
|
483
|
+
context.registerContents()
|
|
484
|
+
return () => {
|
|
485
|
+
context.unregisterContents()
|
|
486
|
+
}
|
|
487
|
+
}, [context.active, context.registerContents, context.unregisterContents])
|
|
488
|
+
|
|
233
489
|
// forwards props - see shouldForwardSpace
|
|
234
|
-
return React.createElement(context.Contents, { ...rest, key: `stable` })
|
|
490
|
+
return React.createElement(context.Contents, { ...rest, scope, key: `stable` })
|
|
235
491
|
}
|
|
236
492
|
|
|
237
493
|
AdaptContents.shouldForwardSpace = true
|
|
238
494
|
|
|
239
495
|
export const Adapt = withStaticProperties(
|
|
240
496
|
function Adapt(props: AdaptProps) {
|
|
241
|
-
const {
|
|
497
|
+
const { children, platform, scope, when } = props
|
|
242
498
|
const context = useAdaptContext(scope)
|
|
243
|
-
const enabled =
|
|
499
|
+
const enabled = context.active
|
|
500
|
+
const isRenderCallback = typeof children === 'function'
|
|
244
501
|
|
|
502
|
+
// only reaches the parent when it could not read this element off its own
|
|
503
|
+
// children, i.e. a userland component renders the <Adapt />
|
|
245
504
|
useIsomorphicLayoutEffect(() => {
|
|
246
|
-
context
|
|
247
|
-
|
|
248
|
-
|
|
505
|
+
context.setAdaptConfig({ when, platform })
|
|
506
|
+
return () => {
|
|
507
|
+
context.setAdaptConfig(null)
|
|
508
|
+
}
|
|
509
|
+
}, [when, platform, context.setAdaptConfig])
|
|
249
510
|
|
|
250
511
|
useIsomorphicLayoutEffect(() => {
|
|
512
|
+
if (!enabled || !isRenderCallback) return
|
|
513
|
+
|
|
514
|
+
context.registerRenderCallback()
|
|
251
515
|
return () => {
|
|
252
|
-
context
|
|
253
|
-
context?.setPlatform?.(null)
|
|
516
|
+
context.unregisterRenderCallback()
|
|
254
517
|
}
|
|
255
|
-
}, [
|
|
518
|
+
}, [
|
|
519
|
+
enabled,
|
|
520
|
+
isRenderCallback,
|
|
521
|
+
context.registerRenderCallback,
|
|
522
|
+
context.unregisterRenderCallback,
|
|
523
|
+
])
|
|
256
524
|
|
|
257
525
|
let output: React.ReactNode
|
|
258
526
|
|
|
259
|
-
if (
|
|
527
|
+
if (isRenderCallback) {
|
|
260
528
|
const Component = context?.Contents
|
|
261
|
-
output = children(Component ? <Component /> : null)
|
|
529
|
+
output = children(Component ? <Component /> : null, getAdaptRenderState(context))
|
|
262
530
|
} else {
|
|
263
531
|
output = children
|
|
264
532
|
}
|
|
@@ -273,63 +541,86 @@ export const Adapt = withStaticProperties(
|
|
|
273
541
|
export const AdaptPortalContents = (props: {
|
|
274
542
|
children: React.ReactNode
|
|
275
543
|
scope?: string
|
|
276
|
-
passThrough?: boolean
|
|
277
544
|
}) => {
|
|
278
545
|
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
|
-
}
|
|
546
|
+
const { slot } = useAdaptContext(props.scope)
|
|
294
547
|
|
|
295
548
|
return (
|
|
296
|
-
<
|
|
549
|
+
<AdaptSlotPublisher isActive={isActive} slot={slot}>
|
|
297
550
|
{props.children}
|
|
298
|
-
</
|
|
551
|
+
</AdaptSlotPublisher>
|
|
299
552
|
)
|
|
300
553
|
}
|
|
301
554
|
|
|
302
|
-
function
|
|
555
|
+
function AdaptSlotContents({ scope }: { scope?: string }) {
|
|
556
|
+
const { slot } = useAdaptContext(scope)
|
|
557
|
+
|
|
558
|
+
React.useSyncExternalStore(
|
|
559
|
+
slot?.subscribe ?? emptySubscribe,
|
|
560
|
+
slot?.getSnapshot ?? emptySnapshot,
|
|
561
|
+
slot?.getSnapshot ?? emptySnapshot
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
return <>{slot?.element ?? null}</>
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function AdaptSlotPublisher({
|
|
303
568
|
isActive,
|
|
304
|
-
|
|
569
|
+
slot,
|
|
305
570
|
children,
|
|
306
571
|
}: {
|
|
307
572
|
isActive: boolean
|
|
308
|
-
|
|
573
|
+
slot: AdaptSlotStore | null
|
|
309
574
|
children: React.ReactNode
|
|
310
575
|
}) {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
576
|
+
const publishedRef = React.useRef<{
|
|
577
|
+
isActive: boolean
|
|
578
|
+
element: React.ReactNode
|
|
579
|
+
} | null>(null)
|
|
580
|
+
|
|
581
|
+
// Publish the live element value after every commit. Do not memoize this handoff
|
|
582
|
+
// on a dep array: stale element deps caused the Sheet overlay-hoist regression
|
|
583
|
+
// this replaces. Comparing against what was actually published is safe, and
|
|
584
|
+
// keeps an unchanged element from re-rendering every slot consumer.
|
|
314
585
|
useIsomorphicLayoutEffect(() => {
|
|
586
|
+
if (!slot) return
|
|
587
|
+
|
|
588
|
+
const published = publishedRef.current
|
|
589
|
+
if (published && published.isActive === isActive && published.element === children) {
|
|
590
|
+
return
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
publishedRef.current = { isActive, element: children }
|
|
594
|
+
|
|
315
595
|
if (isActive) {
|
|
316
|
-
|
|
596
|
+
slot.publish(children)
|
|
317
597
|
} else {
|
|
318
|
-
|
|
598
|
+
slot.clear()
|
|
319
599
|
}
|
|
600
|
+
|
|
601
|
+
slot.notify()
|
|
320
602
|
})
|
|
603
|
+
|
|
321
604
|
useIsomorphicLayoutEffect(() => {
|
|
322
|
-
return () =>
|
|
323
|
-
|
|
605
|
+
return () => {
|
|
606
|
+
if (publishedRef.current?.isActive) {
|
|
607
|
+
slot?.clear()
|
|
608
|
+
slot?.notify()
|
|
609
|
+
}
|
|
610
|
+
publishedRef.current = null
|
|
611
|
+
}
|
|
612
|
+
}, [slot])
|
|
324
613
|
|
|
325
614
|
return isActive ? null : <>{children}</>
|
|
326
615
|
}
|
|
327
616
|
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
617
|
+
const emptySubscribe = () => () => {}
|
|
618
|
+
const emptySnapshot = () => 0
|
|
619
|
+
|
|
620
|
+
const useAdaptIsActiveGiven = (config: AdaptConfig | null) => {
|
|
332
621
|
const media = useMedia()
|
|
622
|
+
const when = config?.when
|
|
623
|
+
const platform = config?.platform
|
|
333
624
|
|
|
334
625
|
if (when == null && platform == null) {
|
|
335
626
|
return false
|
|
@@ -359,6 +650,68 @@ const useAdaptIsActiveGiven = ({
|
|
|
359
650
|
}
|
|
360
651
|
|
|
361
652
|
export const useAdaptIsActive = (scope?: string) => {
|
|
362
|
-
|
|
363
|
-
|
|
653
|
+
return useAdaptContext(scope).active
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export function useAdaptTarget<State = unknown>(
|
|
657
|
+
scope?: string
|
|
658
|
+
): AdaptTarget<State> | null {
|
|
659
|
+
const context = useAdaptContext(scope)
|
|
660
|
+
|
|
661
|
+
useIsomorphicLayoutEffect(() => {
|
|
662
|
+
if (!context.active) return
|
|
663
|
+
|
|
664
|
+
context.registerTarget()
|
|
665
|
+
return () => {
|
|
666
|
+
context.unregisterTarget()
|
|
667
|
+
}
|
|
668
|
+
}, [context.active, context.registerTarget, context.unregisterTarget])
|
|
669
|
+
|
|
670
|
+
if (!context.active) {
|
|
671
|
+
return null
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return {
|
|
675
|
+
open: context.open,
|
|
676
|
+
onOpenChange: context.onOpenChange,
|
|
677
|
+
handoff: context.handoff,
|
|
678
|
+
state: context.state as State,
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export const AdaptCapabilities = ({
|
|
683
|
+
children,
|
|
684
|
+
scroll,
|
|
685
|
+
overlay,
|
|
686
|
+
dismiss,
|
|
687
|
+
}: AdaptCapabilitiesValue & { children?: React.ReactNode }) => {
|
|
688
|
+
const parent = useContext(AdaptCapabilitiesContext)
|
|
689
|
+
const value = React.useMemo(
|
|
690
|
+
() => ({
|
|
691
|
+
scroll: scroll ?? parent.scroll,
|
|
692
|
+
overlay: overlay ?? parent.overlay,
|
|
693
|
+
dismiss: dismiss ?? parent.dismiss,
|
|
694
|
+
}),
|
|
695
|
+
[dismiss, overlay, parent.dismiss, parent.overlay, parent.scroll, scroll]
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
return (
|
|
699
|
+
<AdaptCapabilitiesContext.Provider value={value}>
|
|
700
|
+
{children}
|
|
701
|
+
</AdaptCapabilitiesContext.Provider>
|
|
702
|
+
)
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
export const useAdaptedCapabilities = () => {
|
|
706
|
+
return useContext(AdaptCapabilitiesContext)
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function getAdaptRenderState(context: AdaptParentContextI): AdaptRenderState {
|
|
710
|
+
return {
|
|
711
|
+
active: context.active,
|
|
712
|
+
open: context.open,
|
|
713
|
+
onOpenChange: context.onOpenChange,
|
|
714
|
+
state: context.state,
|
|
715
|
+
handoff: context.handoff,
|
|
716
|
+
}
|
|
364
717
|
}
|