@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/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'
@@ -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
@@ -0,0 +1,2 @@
1
+ export * from './Adapt';
2
+ //# sourceMappingURL=index.d.ts.map