@hanzogui/adapt 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.
- package/LICENSE +21 -0
- package/dist/cjs/Adapt.cjs +230 -0
- package/dist/cjs/Adapt.native.js +274 -0
- package/dist/cjs/Adapt.native.js.map +1 -0
- package/dist/cjs/index.cjs +18 -0
- package/dist/cjs/index.native.js +21 -0
- package/dist/cjs/index.native.js.map +1 -0
- package/dist/esm/Adapt.mjs +189 -0
- package/dist/esm/Adapt.mjs.map +1 -0
- package/dist/esm/Adapt.native.js +230 -0
- package/dist/esm/Adapt.native.js.map +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/index.mjs +2 -0
- package/dist/esm/index.mjs.map +1 -0
- package/dist/esm/index.native.js +2 -0
- package/dist/esm/index.native.js.map +1 -0
- package/package.json +51 -0
- package/src/Adapt.tsx +356 -0
- package/src/index.tsx +1 -0
- package/types/Adapt.d.ts +67 -0
- package/types/index.d.ts +2 -0
package/src/Adapt.tsx
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isAndroid,
|
|
3
|
+
isIos,
|
|
4
|
+
isTouchable,
|
|
5
|
+
isWeb,
|
|
6
|
+
useIsomorphicLayoutEffect,
|
|
7
|
+
} from '@hanzogui/constants'
|
|
8
|
+
import type { AllPlatforms, MediaQueryKey } from '@hanzogui/core'
|
|
9
|
+
import { createStyledContext, useMedia } from '@hanzogui/core'
|
|
10
|
+
import { withStaticProperties } from '@hanzogui/helpers'
|
|
11
|
+
import { getPortal } from '@hanzogui/native'
|
|
12
|
+
import { PortalHost, PortalItem } from '@hanzogui/portal'
|
|
13
|
+
import { StackZIndexContext } from '@hanzogui/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
|
|
25
|
+
subscribe(callback: () => void): () => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createAdaptChildrenStore(): AdaptChildrenStore {
|
|
29
|
+
let children: React.ReactNode = null
|
|
30
|
+
const listeners = new Set<() => void>()
|
|
31
|
+
return {
|
|
32
|
+
set(c) {
|
|
33
|
+
children = c
|
|
34
|
+
for (const l of listeners) l()
|
|
35
|
+
},
|
|
36
|
+
get: () => children,
|
|
37
|
+
subscribe(callback) {
|
|
38
|
+
listeners.add(callback)
|
|
39
|
+
return () => listeners.delete(callback)
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const AdaptChildrenStoreContext = createContext<AdaptChildrenStore | null>(null)
|
|
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}</>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Interfaces
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
export type AdaptWhen = MediaQueryKeyString | boolean | null
|
|
65
|
+
export type AdaptPlatform = AllPlatforms | 'touch' | null
|
|
66
|
+
|
|
67
|
+
export type AdaptParentContextI = {
|
|
68
|
+
Contents: Component
|
|
69
|
+
scopeName: string
|
|
70
|
+
platform: AdaptPlatform
|
|
71
|
+
setPlatform: (when: AdaptPlatform) => any
|
|
72
|
+
when: AdaptWhen
|
|
73
|
+
setWhen: (when: AdaptWhen) => any
|
|
74
|
+
portalName?: string
|
|
75
|
+
lastScope?: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type MediaQueryKeyString = MediaQueryKey extends string ? MediaQueryKey : never
|
|
79
|
+
|
|
80
|
+
export type AdaptProps = {
|
|
81
|
+
scope?: string
|
|
82
|
+
when?: AdaptWhen
|
|
83
|
+
platform?: AdaptPlatform
|
|
84
|
+
children: React.JSX.Element | ((children: React.ReactNode) => React.ReactNode)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type Component = (props: any) => any
|
|
88
|
+
|
|
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
|
+
})
|
|
98
|
+
|
|
99
|
+
const LastAdaptContextScope = createContext('')
|
|
100
|
+
|
|
101
|
+
export const ProvideAdaptContext = ({
|
|
102
|
+
children,
|
|
103
|
+
...context
|
|
104
|
+
}: AdaptParentContextI & { children: any }) => {
|
|
105
|
+
const scope = context.scopeName || ''
|
|
106
|
+
const lastScope = useContext(LastAdaptContextScope)
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<LastAdaptContextScope.Provider value={lastScope || context.lastScope || ''}>
|
|
110
|
+
<AdaptContext.Provider
|
|
111
|
+
scope={scope}
|
|
112
|
+
lastScope={lastScope || context.lastScope}
|
|
113
|
+
{...context}
|
|
114
|
+
>
|
|
115
|
+
{children}
|
|
116
|
+
</AdaptContext.Provider>
|
|
117
|
+
</LastAdaptContextScope.Provider>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const useAdaptContext = (scope?: string) => {
|
|
122
|
+
const lastScope = useContext(LastAdaptContextScope)
|
|
123
|
+
const adaptScope = scope ?? lastScope
|
|
124
|
+
return AdaptContext.useStyledContext(adaptScope)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Hooks
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
type AdaptParentProps = {
|
|
132
|
+
children?: React.ReactNode
|
|
133
|
+
Contents?: AdaptParentContextI['Contents']
|
|
134
|
+
scope: string
|
|
135
|
+
portal?:
|
|
136
|
+
| boolean
|
|
137
|
+
| {
|
|
138
|
+
forwardProps?: any
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const AdaptPortals = new Map()
|
|
143
|
+
|
|
144
|
+
export const AdaptParent = ({ children, Contents, scope, portal }: AdaptParentProps) => {
|
|
145
|
+
const id = useId()
|
|
146
|
+
const portalName = `AdaptPortal${scope}${id}`
|
|
147
|
+
|
|
148
|
+
const childrenStoreRef = React.useRef<AdaptChildrenStore | null>(null)
|
|
149
|
+
if (!childrenStoreRef.current) {
|
|
150
|
+
childrenStoreRef.current = createAdaptChildrenStore()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const isTeleport =
|
|
154
|
+
process.env.HANZO_GUI_TARGET === 'native' && getPortal().state.type === 'teleport'
|
|
155
|
+
|
|
156
|
+
const FinalContents = useMemo(() => {
|
|
157
|
+
if (Contents) {
|
|
158
|
+
return Contents
|
|
159
|
+
}
|
|
160
|
+
|
|
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
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (AdaptPortals.has(portalName)) {
|
|
169
|
+
return AdaptPortals.get(portalName)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const element = () => {
|
|
173
|
+
return (
|
|
174
|
+
<PortalHost
|
|
175
|
+
key={id}
|
|
176
|
+
name={portalName}
|
|
177
|
+
forwardProps={typeof portal === 'boolean' ? undefined : portal?.forwardProps}
|
|
178
|
+
/>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
AdaptPortals.set(portalName, element)
|
|
183
|
+
|
|
184
|
+
return element
|
|
185
|
+
}, [portalName, Contents, isTeleport])
|
|
186
|
+
|
|
187
|
+
useIsomorphicLayoutEffect(() => {
|
|
188
|
+
if (!isTeleport) {
|
|
189
|
+
AdaptPortals.set(portalName, FinalContents)
|
|
190
|
+
return () => {
|
|
191
|
+
AdaptPortals.delete(portalName)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}, [portalName, isTeleport])
|
|
195
|
+
|
|
196
|
+
const [when, setWhen] = React.useState<AdaptWhen>(null)
|
|
197
|
+
const [platform, setPlatform] = React.useState<AdaptPlatform>(null)
|
|
198
|
+
|
|
199
|
+
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>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Components
|
|
220
|
+
*/
|
|
221
|
+
|
|
222
|
+
export const AdaptContents = ({ scope, ...rest }: { scope?: string }) => {
|
|
223
|
+
const context = useAdaptContext(scope)
|
|
224
|
+
|
|
225
|
+
if (!context?.Contents) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
process.env.NODE_ENV === 'production'
|
|
228
|
+
? `gui.dev/docs/intro/errors#warning-002`
|
|
229
|
+
: `You're rendering a Gui <Adapt /> component without nesting it inside a parent that is able to adapt.`
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// forwards props - see shouldForwardSpace
|
|
234
|
+
return React.createElement(context.Contents, { ...rest, key: `stable` })
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
AdaptContents.shouldForwardSpace = true
|
|
238
|
+
|
|
239
|
+
export const Adapt = withStaticProperties(
|
|
240
|
+
function Adapt(props: AdaptProps) {
|
|
241
|
+
const { platform, when, children, scope } = props
|
|
242
|
+
const context = useAdaptContext(scope)
|
|
243
|
+
const enabled = useAdaptIsActiveGiven(props)
|
|
244
|
+
|
|
245
|
+
useIsomorphicLayoutEffect(() => {
|
|
246
|
+
context?.setWhen?.((when || enabled) as AdaptWhen)
|
|
247
|
+
context?.setPlatform?.(platform || null)
|
|
248
|
+
}, [when, platform, enabled, context.setWhen, context.setPlatform])
|
|
249
|
+
|
|
250
|
+
useIsomorphicLayoutEffect(() => {
|
|
251
|
+
return () => {
|
|
252
|
+
context?.setWhen?.(null)
|
|
253
|
+
context?.setPlatform?.(null)
|
|
254
|
+
}
|
|
255
|
+
}, [])
|
|
256
|
+
|
|
257
|
+
let output: React.ReactNode
|
|
258
|
+
|
|
259
|
+
if (typeof children === 'function') {
|
|
260
|
+
const Component = context?.Contents
|
|
261
|
+
output = children(Component ? <Component /> : null)
|
|
262
|
+
} else {
|
|
263
|
+
output = children
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return <StackZIndexContext>{!enabled ? null : output}</StackZIndexContext>
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
Contents: AdaptContents,
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
export const AdaptPortalContents = (props: {
|
|
274
|
+
children: React.ReactNode
|
|
275
|
+
scope?: string
|
|
276
|
+
passThrough?: boolean
|
|
277
|
+
}) => {
|
|
278
|
+
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
|
+
}
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
<PortalItem passThrough={!isActive} hostName={portalName}>
|
|
297
|
+
{props.children}
|
|
298
|
+
</PortalItem>
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function AdaptPortalTeleport({
|
|
303
|
+
isActive,
|
|
304
|
+
store,
|
|
305
|
+
children,
|
|
306
|
+
}: {
|
|
307
|
+
isActive: boolean
|
|
308
|
+
store: AdaptChildrenStore
|
|
309
|
+
children: React.ReactNode
|
|
310
|
+
}) {
|
|
311
|
+
useIsomorphicLayoutEffect(() => {
|
|
312
|
+
if (!isActive) return
|
|
313
|
+
store.set(children)
|
|
314
|
+
return () => store.set(null)
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
return isActive ? null : <>{children}</>
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const useAdaptIsActiveGiven = ({
|
|
321
|
+
when,
|
|
322
|
+
platform,
|
|
323
|
+
}: Pick<AdaptProps, 'when' | 'platform'>) => {
|
|
324
|
+
const media = useMedia()
|
|
325
|
+
|
|
326
|
+
if (when == null && platform == null) {
|
|
327
|
+
return false
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (when === true) {
|
|
331
|
+
return true
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let enabled = false
|
|
335
|
+
|
|
336
|
+
if (platform === 'touch') enabled = isTouchable
|
|
337
|
+
else if (platform === 'native') enabled = !isWeb
|
|
338
|
+
else if (platform === 'web') enabled = isWeb
|
|
339
|
+
else if (platform === 'ios') enabled = isIos
|
|
340
|
+
else if (platform === 'android') enabled = isAndroid
|
|
341
|
+
|
|
342
|
+
if (platform && enabled == false) {
|
|
343
|
+
return false
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (when && typeof when === 'string') {
|
|
347
|
+
enabled = media[when]
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return enabled
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export const useAdaptIsActive = (scope?: string) => {
|
|
354
|
+
const props = useAdaptContext(scope)
|
|
355
|
+
return useAdaptIsActiveGiven(props)
|
|
356
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Adapt'
|
package/types/Adapt.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { AllPlatforms, MediaQueryKey } from '@gui/core';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Interfaces
|
|
5
|
+
*/
|
|
6
|
+
export type AdaptWhen = MediaQueryKeyString | boolean | null;
|
|
7
|
+
export type AdaptPlatform = AllPlatforms | 'touch' | null;
|
|
8
|
+
export type AdaptParentContextI = {
|
|
9
|
+
Contents: Component;
|
|
10
|
+
scopeName: string;
|
|
11
|
+
platform: AdaptPlatform;
|
|
12
|
+
setPlatform: (when: AdaptPlatform) => any;
|
|
13
|
+
when: AdaptWhen;
|
|
14
|
+
setWhen: (when: AdaptWhen) => any;
|
|
15
|
+
portalName?: string;
|
|
16
|
+
lastScope?: string;
|
|
17
|
+
};
|
|
18
|
+
type MediaQueryKeyString = MediaQueryKey extends string ? MediaQueryKey : never;
|
|
19
|
+
export type AdaptProps = {
|
|
20
|
+
scope?: string;
|
|
21
|
+
when?: AdaptWhen;
|
|
22
|
+
platform?: AdaptPlatform;
|
|
23
|
+
children: React.JSX.Element | ((children: React.ReactNode) => React.ReactNode);
|
|
24
|
+
};
|
|
25
|
+
type Component = (props: any) => any;
|
|
26
|
+
export declare const AdaptContext: import("@gui/core").StyledContext<AdaptParentContextI>;
|
|
27
|
+
export declare const ProvideAdaptContext: ({ children, ...context }: AdaptParentContextI & {
|
|
28
|
+
children: any;
|
|
29
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
30
|
+
export declare const useAdaptContext: (scope?: string) => AdaptParentContextI;
|
|
31
|
+
/**
|
|
32
|
+
* Hooks
|
|
33
|
+
*/
|
|
34
|
+
type AdaptParentProps = {
|
|
35
|
+
children?: React.ReactNode;
|
|
36
|
+
Contents?: AdaptParentContextI['Contents'];
|
|
37
|
+
scope: string;
|
|
38
|
+
portal?: boolean | {
|
|
39
|
+
forwardProps?: any;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
export declare const AdaptParent: ({ children, Contents, scope, portal }: AdaptParentProps) => import("react/jsx-runtime").JSX.Element;
|
|
43
|
+
/**
|
|
44
|
+
* Components
|
|
45
|
+
*/
|
|
46
|
+
export declare const AdaptContents: {
|
|
47
|
+
({ scope, ...rest }: {
|
|
48
|
+
scope?: string;
|
|
49
|
+
}): React.FunctionComponentElement<any>;
|
|
50
|
+
shouldForwardSpace: boolean;
|
|
51
|
+
};
|
|
52
|
+
export declare const Adapt: ((props: AdaptProps) => import("react/jsx-runtime").JSX.Element) & {
|
|
53
|
+
Contents: {
|
|
54
|
+
({ scope, ...rest }: {
|
|
55
|
+
scope?: string;
|
|
56
|
+
}): React.FunctionComponentElement<any>;
|
|
57
|
+
shouldForwardSpace: boolean;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
export declare const AdaptPortalContents: (props: {
|
|
61
|
+
children: React.ReactNode;
|
|
62
|
+
scope?: string;
|
|
63
|
+
passThrough?: boolean;
|
|
64
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
65
|
+
export declare const useAdaptIsActive: (scope?: string) => boolean;
|
|
66
|
+
export {};
|
|
67
|
+
//# sourceMappingURL=Adapt.d.ts.map
|
package/types/index.d.ts
ADDED