panelui-native 0.89.0 → 0.91.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/lib/module/components/fab/index.js +798 -53
- package/lib/module/components/fab/index.js.map +1 -1
- package/lib/module/components/section-progress/index.js +637 -0
- package/lib/module/components/section-progress/index.js.map +1 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/use-scroll-sections.js +58 -4
- package/lib/module/hooks/use-scroll-sections.js.map +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/index.js +55 -0
- package/lib/module/native/index.js.map +1 -1
- package/lib/module/primitives/glass.js +92 -9
- package/lib/module/primitives/glass.js.map +1 -1
- package/lib/typescript/src/components/fab/index.d.ts +133 -1
- package/lib/typescript/src/components/fab/index.d.ts.map +1 -1
- package/lib/typescript/src/components/section-progress/index.d.ts +156 -0
- package/lib/typescript/src/components/section-progress/index.d.ts.map +1 -0
- package/lib/typescript/src/hooks/index.d.ts +1 -1
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-scroll-sections.d.ts +24 -0
- package/lib/typescript/src/hooks/use-scroll-sections.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +3 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native/index.d.ts +115 -0
- package/lib/typescript/src/native/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/glass.d.ts +40 -2
- package/lib/typescript/src/primitives/glass.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/fab/index.tsx +967 -66
- package/src/components/section-progress/index.tsx +813 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-scroll-sections.ts +84 -5
- package/src/index.ts +21 -1
- package/src/native/index.ts +146 -0
- package/src/primitives/glass.tsx +116 -11
package/src/hooks/index.ts
CHANGED
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
*
|
|
24
24
|
* <SectionRail value={sections.active} onValueChange={sections.scrollTo}>…</SectionRail>
|
|
25
25
|
* ```
|
|
26
|
+
*
|
|
27
|
+
* It also publishes the scroll position itself, as `scroll`, so a component
|
|
28
|
+
* that wants a percentage as well as a section name does not need a second
|
|
29
|
+
* scroll listener to get one. The three values are everything a fraction is
|
|
30
|
+
* made of, and they come out of the event this hook is already handling.
|
|
26
31
|
*/
|
|
27
32
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
28
33
|
import type {
|
|
@@ -31,6 +36,7 @@ import type {
|
|
|
31
36
|
NativeSyntheticEvent,
|
|
32
37
|
ScrollView,
|
|
33
38
|
} from 'react-native';
|
|
39
|
+
import { useSharedValue, type SharedValue } from 'react-native-reanimated';
|
|
34
40
|
|
|
35
41
|
export interface UseScrollSectionsOptions {
|
|
36
42
|
/** Section ids, in the order they appear down the page. */
|
|
@@ -46,15 +52,39 @@ export interface UseScrollSectionsOptions {
|
|
|
46
52
|
scrollPadding?: number;
|
|
47
53
|
}
|
|
48
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Where the scroller is, as shared values.
|
|
57
|
+
*
|
|
58
|
+
* Written from the scroll handler on the JavaScript thread, which is where the
|
|
59
|
+
* section tracking already runs — so they arrive at `scrollEventThrottle`
|
|
60
|
+
* rather than every frame, and anything reading them should ease towards the
|
|
61
|
+
* value rather than jumping to it.
|
|
62
|
+
*/
|
|
63
|
+
export interface ScrollSectionsPosition {
|
|
64
|
+
/** Distance scrolled, in points. */
|
|
65
|
+
offset: SharedValue<number>;
|
|
66
|
+
/** Height of the visible area. */
|
|
67
|
+
viewport: SharedValue<number>;
|
|
68
|
+
/** Total height of the content. */
|
|
69
|
+
content: SharedValue<number>;
|
|
70
|
+
}
|
|
71
|
+
|
|
49
72
|
export interface UseScrollSectionsResult {
|
|
50
73
|
/** Attach to the ScrollView, so `scrollTo` has something to drive. */
|
|
51
74
|
ref: React.RefObject<ScrollView | null>;
|
|
52
75
|
/** The section being read. */
|
|
53
76
|
active: string | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* The scroll position, for anything that needs how far through the page the
|
|
79
|
+
* reader is rather than which part of it they are in.
|
|
80
|
+
*/
|
|
81
|
+
scroll: ScrollSectionsPosition;
|
|
54
82
|
/** Spread onto the ScrollView. */
|
|
55
83
|
scrollProps: {
|
|
56
84
|
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
|
57
85
|
onMomentumScrollEnd: () => void;
|
|
86
|
+
onLayout: (event: LayoutChangeEvent) => void;
|
|
87
|
+
onContentSizeChange: (width: number, height: number) => void;
|
|
58
88
|
scrollEventThrottle: number;
|
|
59
89
|
};
|
|
60
90
|
/** `onLayout` for a section's wrapper: `onLayout={measure(id)}`. */
|
|
@@ -81,6 +111,10 @@ export function useScrollSections({
|
|
|
81
111
|
const settle = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
82
112
|
const [active, setActive] = useState<string | undefined>(ids[0]);
|
|
83
113
|
|
|
114
|
+
const scrollOffset = useSharedValue(0);
|
|
115
|
+
const viewport = useSharedValue(0);
|
|
116
|
+
const content = useSharedValue(0);
|
|
117
|
+
|
|
84
118
|
// Read inside the scroll handler, which must not be re-created on every
|
|
85
119
|
// render — a new handler each frame would defeat the throttle.
|
|
86
120
|
const idsRef = useRef(ids);
|
|
@@ -107,10 +141,24 @@ export function useScrollSections({
|
|
|
107
141
|
|
|
108
142
|
const onScroll = useCallback(
|
|
109
143
|
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
144
|
+
const { contentOffset, layoutMeasurement, contentSize } = event.nativeEvent;
|
|
145
|
+
|
|
146
|
+
/*
|
|
147
|
+
* The position is published before the jump guard, not after it.
|
|
148
|
+
*
|
|
149
|
+
* A jump is still travel: the page really is moving under the reader, and
|
|
150
|
+
* a progress indicator that froze for the length of the animation would
|
|
151
|
+
* be reporting something that is not true. What the guard protects is the
|
|
152
|
+
* *section*, which the reader chose and which should not flicker through
|
|
153
|
+
* every heading the scroll passes on the way there.
|
|
154
|
+
*/
|
|
155
|
+
scrollOffset.value = contentOffset.y;
|
|
156
|
+
viewport.value = layoutMeasurement.height;
|
|
157
|
+
content.value = contentSize.height;
|
|
158
|
+
|
|
110
159
|
// Mid-jump, the position is the animation's and not the reader's.
|
|
111
160
|
if (jumpingTo.current !== null) return;
|
|
112
161
|
|
|
113
|
-
const { contentOffset, layoutMeasurement, contentSize } = event.nativeEvent;
|
|
114
162
|
const list = idsRef.current;
|
|
115
163
|
if (!list.length) return;
|
|
116
164
|
|
|
@@ -129,7 +177,27 @@ export function useScrollSections({
|
|
|
129
177
|
}
|
|
130
178
|
setActive(current);
|
|
131
179
|
},
|
|
132
|
-
[offset, endThreshold]
|
|
180
|
+
[offset, endThreshold, scrollOffset, viewport, content]
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
/*
|
|
184
|
+
* Seeded at layout as well as on scroll, so a page that has not been touched
|
|
185
|
+
* yet still reports a viewport and a content height. Without these the
|
|
186
|
+
* fraction is zero over zero until the first scroll event, and anything
|
|
187
|
+
* drawing it starts by being wrong rather than by being empty.
|
|
188
|
+
*/
|
|
189
|
+
const onLayout = useCallback(
|
|
190
|
+
(event: LayoutChangeEvent) => {
|
|
191
|
+
viewport.value = event.nativeEvent.layout.height;
|
|
192
|
+
},
|
|
193
|
+
[viewport]
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const onContentSizeChange = useCallback(
|
|
197
|
+
(_width: number, height: number) => {
|
|
198
|
+
content.value = height;
|
|
199
|
+
},
|
|
200
|
+
[content]
|
|
133
201
|
);
|
|
134
202
|
|
|
135
203
|
const scrollTo = useCallback(
|
|
@@ -168,9 +236,20 @@ export function useScrollSections({
|
|
|
168
236
|
);
|
|
169
237
|
|
|
170
238
|
const scrollProps = useMemo(
|
|
171
|
-
() => ({
|
|
172
|
-
|
|
239
|
+
() => ({
|
|
240
|
+
onScroll,
|
|
241
|
+
onMomentumScrollEnd,
|
|
242
|
+
onLayout,
|
|
243
|
+
onContentSizeChange,
|
|
244
|
+
scrollEventThrottle: 16,
|
|
245
|
+
}),
|
|
246
|
+
[onScroll, onMomentumScrollEnd, onLayout, onContentSizeChange]
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const scroll = useMemo<ScrollSectionsPosition>(
|
|
250
|
+
() => ({ offset: scrollOffset, viewport, content }),
|
|
251
|
+
[scrollOffset, viewport, content]
|
|
173
252
|
);
|
|
174
253
|
|
|
175
|
-
return { ref, active, scrollProps, measure, scrollTo };
|
|
254
|
+
return { ref, active, scroll, scrollProps, measure, scrollTo };
|
|
176
255
|
}
|
package/src/index.ts
CHANGED
|
@@ -22,7 +22,16 @@ export { Portal, PortalHost, PortalProvider } from './primitives/portal';
|
|
|
22
22
|
export { Scrim, hasBlur, type ScrimProps } from './primitives/scrim';
|
|
23
23
|
// The material iOS draws its floating controls in, with a solid fallback where
|
|
24
24
|
// there is none. Public so a surface of your own gets the same treatment.
|
|
25
|
-
export {
|
|
25
|
+
export {
|
|
26
|
+
Glass,
|
|
27
|
+
GlassContainer,
|
|
28
|
+
hasGlass,
|
|
29
|
+
useGlassMaterial,
|
|
30
|
+
type GlassProps,
|
|
31
|
+
type GlassContainerProps,
|
|
32
|
+
type GlassVariant,
|
|
33
|
+
type GlassRadius,
|
|
34
|
+
} from './primitives/glass';
|
|
26
35
|
export { Text, type TextProps } from './primitives/text';
|
|
27
36
|
export { Collapse, type CollapseProps } from './primitives/collapse';
|
|
28
37
|
export {
|
|
@@ -218,6 +227,9 @@ export {
|
|
|
218
227
|
type FabPlacement,
|
|
219
228
|
type FabSize,
|
|
220
229
|
type FabVariant,
|
|
230
|
+
type FabGroupLayout,
|
|
231
|
+
type FabMenuAppearance,
|
|
232
|
+
type FabMenuIconPlacement,
|
|
221
233
|
} from './components/fab';
|
|
222
234
|
export {
|
|
223
235
|
Flow,
|
|
@@ -386,6 +398,14 @@ export {
|
|
|
386
398
|
type SectionRailPlacement,
|
|
387
399
|
type SectionRailAlign,
|
|
388
400
|
} from './components/section-rail';
|
|
401
|
+
export {
|
|
402
|
+
SectionProgress,
|
|
403
|
+
type SectionProgressProps,
|
|
404
|
+
type SectionProgressItemProps,
|
|
405
|
+
type SectionProgressPlacement,
|
|
406
|
+
type SectionProgressColor,
|
|
407
|
+
type SectionProgressScroll,
|
|
408
|
+
} from './components/section-progress';
|
|
389
409
|
export {
|
|
390
410
|
Tabs,
|
|
391
411
|
type TabsProps,
|
package/src/native/index.ts
CHANGED
|
@@ -182,6 +182,27 @@ interface SwiftUIModifiers {
|
|
|
182
182
|
* changing any code on a report of "the colour did nothing".
|
|
183
183
|
*/
|
|
184
184
|
presentationBackground: (color: string) => unknown;
|
|
185
|
+
/**
|
|
186
|
+
* Greys a control out and stops it answering.
|
|
187
|
+
*
|
|
188
|
+
* Optional because the module is cast whole rather than feature-checked, and
|
|
189
|
+
* an older `@expo/ui` without it would otherwise be a crash instead of a
|
|
190
|
+
* menu row that is merely still tappable. Call it through a guard.
|
|
191
|
+
*/
|
|
192
|
+
disabled?: (disabled: boolean) => unknown;
|
|
193
|
+
/**
|
|
194
|
+
* Fires when the view is put on screen, and when it is taken off again.
|
|
195
|
+
*
|
|
196
|
+
* The way to find out that a menu has opened. SwiftUI builds a menu's
|
|
197
|
+
* content only once it is presented, so an item inside one appears exactly
|
|
198
|
+
* when the menu does — which is the only signal the control gives, since it
|
|
199
|
+
* owns its open state and reports nothing about it.
|
|
200
|
+
*
|
|
201
|
+
* Optional for the same reason as `disabled`: the module is cast whole, and
|
|
202
|
+
* a version without these should lose the backdrop rather than crash.
|
|
203
|
+
*/
|
|
204
|
+
onAppear?: (handler: () => void) => unknown;
|
|
205
|
+
onDisappear?: (handler: () => void) => unknown;
|
|
185
206
|
}
|
|
186
207
|
|
|
187
208
|
/**
|
|
@@ -241,6 +262,131 @@ export function getSwiftUI(): SwiftUIComponents | null {
|
|
|
241
262
|
return swiftUI;
|
|
242
263
|
}
|
|
243
264
|
|
|
265
|
+
/**
|
|
266
|
+
* A hosting boundary, as both toolkits declare it. The same three props matter
|
|
267
|
+
* on either side — see `NativeUIModule.Host` above for what each one costs.
|
|
268
|
+
*/
|
|
269
|
+
type NativeHostComponent = ComponentType<{
|
|
270
|
+
children?: ReactNode;
|
|
271
|
+
matchContents?: boolean | { vertical?: boolean; horizontal?: boolean };
|
|
272
|
+
ignoreSafeArea?: 'all' | 'container' | 'keyboard';
|
|
273
|
+
colorScheme?: 'light' | 'dark';
|
|
274
|
+
style?: unknown;
|
|
275
|
+
}>;
|
|
276
|
+
|
|
277
|
+
type RNHostComponent = ComponentType<{
|
|
278
|
+
children?: ReactNode;
|
|
279
|
+
matchContents?: boolean;
|
|
280
|
+
style?: unknown;
|
|
281
|
+
}>;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* SwiftUI's menu, and the button that fills a row of it.
|
|
285
|
+
*
|
|
286
|
+
* Separate from `getSwiftUI` on purpose: that resolver refuses a module with no
|
|
287
|
+
* popover in it, and a version that ships one control but not the other would
|
|
288
|
+
* take the menu down with it. Each native path asks only for what it needs.
|
|
289
|
+
*/
|
|
290
|
+
interface SwiftUIMenuComponents {
|
|
291
|
+
Host: NativeHostComponent;
|
|
292
|
+
RNHostView: RNHostComponent;
|
|
293
|
+
/**
|
|
294
|
+
* `label` takes a React element as well as a string — the element is passed
|
|
295
|
+
* through a native slot and becomes the thing you press. That is what lets
|
|
296
|
+
* the trigger stay a real Fab rather than a platform button wearing its name.
|
|
297
|
+
*/
|
|
298
|
+
Menu: ComponentType<{
|
|
299
|
+
label?: ReactNode;
|
|
300
|
+
children?: ReactNode;
|
|
301
|
+
modifiers?: unknown[];
|
|
302
|
+
}>;
|
|
303
|
+
Button: ComponentType<{
|
|
304
|
+
label?: string;
|
|
305
|
+
systemImage?: string;
|
|
306
|
+
role?: 'default' | 'cancel' | 'destructive';
|
|
307
|
+
onPress?: () => void;
|
|
308
|
+
modifiers?: unknown[];
|
|
309
|
+
}>;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let swiftUIMenuResolved = false;
|
|
313
|
+
let swiftUIMenu: SwiftUIMenuComponents | null = null;
|
|
314
|
+
|
|
315
|
+
export function getSwiftUIMenu(): SwiftUIMenuComponents | null {
|
|
316
|
+
if (swiftUIMenuResolved) return swiftUIMenu;
|
|
317
|
+
swiftUIMenuResolved = true;
|
|
318
|
+
|
|
319
|
+
if (Platform.OS !== 'ios') return null;
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
323
|
+
const module = require('@expo/ui/swift-ui') as Partial<SwiftUIMenuComponents>;
|
|
324
|
+
swiftUIMenu =
|
|
325
|
+
module.Host && module.RNHostView && module.Menu && module.Button
|
|
326
|
+
? (module as SwiftUIMenuComponents)
|
|
327
|
+
: null;
|
|
328
|
+
} catch {
|
|
329
|
+
swiftUIMenu = null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return swiftUIMenu;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Compose's dropdown menu — the same instruction, answered by a different
|
|
337
|
+
* control.
|
|
338
|
+
*
|
|
339
|
+
* It is not the shape SwiftUI's menu is. This one is controlled: it takes the
|
|
340
|
+
* open state rather than owning it, and it splits the trigger and the items
|
|
341
|
+
* into named slots instead of reading the label off a prop. Both differences
|
|
342
|
+
* reach the caller, so they are written out here rather than smoothed over.
|
|
343
|
+
*/
|
|
344
|
+
interface ComposeMenuComponents {
|
|
345
|
+
Host: NativeHostComponent;
|
|
346
|
+
RNHostView: RNHostComponent;
|
|
347
|
+
Text: ComponentType<{ children?: ReactNode; color?: string }>;
|
|
348
|
+
DropdownMenu: ComponentType<{
|
|
349
|
+
children?: ReactNode;
|
|
350
|
+
expanded?: boolean;
|
|
351
|
+
onDismissRequest?: () => void;
|
|
352
|
+
}> & {
|
|
353
|
+
Trigger: ComponentType<{ children?: ReactNode }>;
|
|
354
|
+
Items: ComponentType<{ children?: ReactNode }>;
|
|
355
|
+
};
|
|
356
|
+
DropdownMenuItem: ComponentType<{
|
|
357
|
+
children?: ReactNode;
|
|
358
|
+
enabled?: boolean;
|
|
359
|
+
onClick?: () => void;
|
|
360
|
+
elementColors?: { textColor?: string; disabledTextColor?: string };
|
|
361
|
+
}> & {
|
|
362
|
+
Text: ComponentType<{ children?: ReactNode }>;
|
|
363
|
+
LeadingIcon: ComponentType<{ children?: ReactNode }>;
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let composeMenuResolved = false;
|
|
368
|
+
let composeMenu: ComposeMenuComponents | null = null;
|
|
369
|
+
|
|
370
|
+
export function getComposeMenu(): ComposeMenuComponents | null {
|
|
371
|
+
if (composeMenuResolved) return composeMenu;
|
|
372
|
+
composeMenuResolved = true;
|
|
373
|
+
|
|
374
|
+
if (Platform.OS !== 'android') return null;
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
378
|
+
const module = require('@expo/ui/jetpack-compose') as Partial<ComposeMenuComponents>;
|
|
379
|
+
composeMenu =
|
|
380
|
+
module.Host && module.RNHostView && module.DropdownMenu && module.DropdownMenuItem
|
|
381
|
+
? (module as ComposeMenuComponents)
|
|
382
|
+
: null;
|
|
383
|
+
} catch {
|
|
384
|
+
composeMenu = null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return composeMenu;
|
|
388
|
+
}
|
|
389
|
+
|
|
244
390
|
let modifiersResolved = false;
|
|
245
391
|
let modifiers: SwiftUIModifiers | null = null;
|
|
246
392
|
|
package/src/primitives/glass.tsx
CHANGED
|
@@ -36,13 +36,18 @@
|
|
|
36
36
|
* its own corners — clipping a square one to a rounded parent throws away the
|
|
37
37
|
* lit edge that makes it read as glass.
|
|
38
38
|
*
|
|
39
|
+
* `interactive` is the one exception to the layer: the platform only animates
|
|
40
|
+
* the glass under a touch it can see, so with it on the material is the box
|
|
41
|
+
* and the children are hosted inside it, in normal flow. Reach for it when
|
|
42
|
+
* the glass *is* the button.
|
|
43
|
+
*
|
|
39
44
|
* ## Do not fade it
|
|
40
45
|
*
|
|
41
46
|
* Setting `opacity` to `0` on the material or on anything above it stops it
|
|
42
47
|
* rendering at all, and it does not come back when the opacity does. Move it,
|
|
43
48
|
* or unmount it; never animate it out.
|
|
44
49
|
*/
|
|
45
|
-
import type
|
|
50
|
+
import { forwardRef, type ComponentType, type ReactNode } from 'react';
|
|
46
51
|
import { Platform, StyleSheet, View, type StyleProp, type ViewProps, type ViewStyle } from 'react-native';
|
|
47
52
|
import { useThemeMode } from '../theme/use-theme';
|
|
48
53
|
import { cn } from '../utils/cn';
|
|
@@ -71,18 +76,27 @@ function shapeOf(radius: GlassRadius | undefined) {
|
|
|
71
76
|
};
|
|
72
77
|
}
|
|
73
78
|
|
|
74
|
-
interface GlassViewProps {
|
|
79
|
+
interface GlassViewProps extends ViewProps {
|
|
75
80
|
glassEffectStyle?: GlassVariant | 'none';
|
|
76
81
|
tintColor?: string;
|
|
82
|
+
isInteractive?: boolean;
|
|
77
83
|
colorScheme?: 'auto' | 'light' | 'dark';
|
|
78
84
|
style?: StyleProp<ViewStyle>;
|
|
79
|
-
pointerEvents?: ViewProps['pointerEvents'];
|
|
80
85
|
children?: ReactNode;
|
|
81
86
|
}
|
|
82
87
|
|
|
88
|
+
interface GlassContainerViewProps extends ViewProps {
|
|
89
|
+
spacing?: number;
|
|
90
|
+
ref?: React.Ref<View>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface GlassModule {
|
|
94
|
+
GlassView: ComponentType<GlassViewProps>;
|
|
95
|
+
GlassContainer: ComponentType<GlassContainerViewProps>;
|
|
96
|
+
}
|
|
97
|
+
|
|
83
98
|
/**
|
|
84
|
-
* `expo-glass-effect
|
|
85
|
-
* here.
|
|
99
|
+
* `expo-glass-effect`, or null when the material cannot be drawn here.
|
|
86
100
|
*
|
|
87
101
|
* Resolved once at module load, behind three gates that all have to pass. The
|
|
88
102
|
* package is optional, so the require can fail; the API is missing from some
|
|
@@ -91,7 +105,7 @@ interface GlassViewProps {
|
|
|
91
105
|
* cheaper than a try/catch on every render, and there is no answer that can
|
|
92
106
|
* change while the process is alive.
|
|
93
107
|
*/
|
|
94
|
-
const
|
|
108
|
+
const glassModule: GlassModule | null = (() => {
|
|
95
109
|
if (Platform.OS !== 'ios') return null;
|
|
96
110
|
try {
|
|
97
111
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -102,12 +116,15 @@ const GlassView: ComponentType<GlassViewProps> | null = (() => {
|
|
|
102
116
|
if (typeof mod?.isLiquidGlassAvailable === 'function' && !mod.isLiquidGlassAvailable()) {
|
|
103
117
|
return null;
|
|
104
118
|
}
|
|
105
|
-
return
|
|
119
|
+
return mod?.GlassView ? (mod as GlassModule) : null;
|
|
106
120
|
} catch {
|
|
107
121
|
return null;
|
|
108
122
|
}
|
|
109
123
|
})();
|
|
110
124
|
|
|
125
|
+
const GlassView = glassModule?.GlassView ?? null;
|
|
126
|
+
const GlassContainerView = glassModule?.GlassContainer ?? null;
|
|
127
|
+
|
|
111
128
|
/**
|
|
112
129
|
* True when the real material can be drawn — for a caller that wants to know
|
|
113
130
|
* before it commits to a look. It says nothing about Reduce Transparency, which
|
|
@@ -115,6 +132,21 @@ const GlassView: ComponentType<GlassViewProps> | null = (() => {
|
|
|
115
132
|
*/
|
|
116
133
|
export const hasGlass = GlassView !== null;
|
|
117
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Whether the material will actually be drawn right now: the API is present
|
|
137
|
+
* *and* the user has not switched Reduce Transparency on.
|
|
138
|
+
*
|
|
139
|
+
* For a component that changes shape around the material — dropping a fill, a
|
|
140
|
+
* border or a shadow the glass replaces — so that it makes the same decision
|
|
141
|
+
* `Glass` makes and never strips the fill while leaving nothing behind it.
|
|
142
|
+
*/
|
|
143
|
+
export function useGlassMaterial(): boolean {
|
|
144
|
+
const reduceTransparency = useReduceTransparency();
|
|
145
|
+
// Not knowing yet counts as "do not draw it": the material arriving a frame
|
|
146
|
+
// late is invisible, and one flashing at somebody who opted out is not.
|
|
147
|
+
return hasGlass && reduceTransparency === false;
|
|
148
|
+
}
|
|
149
|
+
|
|
118
150
|
export interface GlassProps extends ViewProps {
|
|
119
151
|
/**
|
|
120
152
|
* How much of what is behind shows through. `regular` is the everyday
|
|
@@ -129,6 +161,16 @@ export interface GlassProps extends ViewProps {
|
|
|
129
161
|
* to a rounded parent throws away the lit edge that makes it read as glass.
|
|
130
162
|
*/
|
|
131
163
|
radius?: GlassRadius;
|
|
164
|
+
/**
|
|
165
|
+
* Let the material answer touch the way the platform's own controls do:
|
|
166
|
+
* it brightens and swells under the finger and the highlight follows it.
|
|
167
|
+
*
|
|
168
|
+
* For a material that *is* a button. The platform only tracks touches that
|
|
169
|
+
* land inside the glass view, so with this on the content is hosted inside
|
|
170
|
+
* the material rather than above it — a pressable written as a child still
|
|
171
|
+
* gets its press, and the glass reacts to the same touch.
|
|
172
|
+
*/
|
|
173
|
+
interactive?: boolean;
|
|
132
174
|
/** Applied only when the material cannot be drawn. Give it a real surface. */
|
|
133
175
|
fallbackClassName?: string;
|
|
134
176
|
className?: string;
|
|
@@ -138,13 +180,13 @@ export function Glass({
|
|
|
138
180
|
variant = 'regular',
|
|
139
181
|
tint,
|
|
140
182
|
radius,
|
|
183
|
+
interactive = false,
|
|
141
184
|
fallbackClassName = 'bg-card',
|
|
142
185
|
className,
|
|
143
186
|
children,
|
|
144
187
|
style,
|
|
145
188
|
...props
|
|
146
189
|
}: GlassProps) {
|
|
147
|
-
const reduceTransparency = useReduceTransparency();
|
|
148
190
|
/*
|
|
149
191
|
* Which appearance the material is drawn in, from the app's theme rather
|
|
150
192
|
* than the phone's.
|
|
@@ -155,11 +197,33 @@ export function Glass({
|
|
|
155
197
|
* appearance never moved.
|
|
156
198
|
*/
|
|
157
199
|
const { mode } = useThemeMode();
|
|
158
|
-
|
|
159
|
-
// late is invisible, and one flashing at somebody who opted out is not.
|
|
160
|
-
const material = GlassView !== null && reduceTransparency === false;
|
|
200
|
+
const material = useGlassMaterial();
|
|
161
201
|
const shape = shapeOf(radius);
|
|
162
202
|
|
|
203
|
+
/*
|
|
204
|
+
* Interactive, the material is the box rather than a layer in it. The
|
|
205
|
+
* platform only tracks a touch that lands inside the glass view, so the
|
|
206
|
+
* content has to be hosted in it — and hosted in normal flow, so that a
|
|
207
|
+
* box sized by its content still is. A layer pinned to the box's edges
|
|
208
|
+
* could not size it, and a button as wide as its label would collapse to
|
|
209
|
+
* its minimum. The classes go on a view inside, because the native view is
|
|
210
|
+
* not a styled one; only the positional style stays on the outside.
|
|
211
|
+
*/
|
|
212
|
+
if (material && GlassView && interactive) {
|
|
213
|
+
return (
|
|
214
|
+
<GlassView
|
|
215
|
+
glassEffectStyle={variant}
|
|
216
|
+
tintColor={tint}
|
|
217
|
+
isInteractive
|
|
218
|
+
colorScheme={mode}
|
|
219
|
+
style={[shape, style]}
|
|
220
|
+
{...props}
|
|
221
|
+
>
|
|
222
|
+
<View className={className}>{children}</View>
|
|
223
|
+
</GlassView>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
163
227
|
return (
|
|
164
228
|
<View
|
|
165
229
|
className={cn('overflow-hidden', material ? null : fallbackClassName, className)}
|
|
@@ -181,3 +245,44 @@ export function Glass({
|
|
|
181
245
|
}
|
|
182
246
|
|
|
183
247
|
Glass.displayName = 'Glass';
|
|
248
|
+
|
|
249
|
+
export interface GlassContainerProps extends ViewProps {
|
|
250
|
+
/**
|
|
251
|
+
* How close two pieces of glass have to be before they merge, in points.
|
|
252
|
+
* Inside it their edges flow into one another; a piece moving past
|
|
253
|
+
* another blends with it and pulls free as it leaves.
|
|
254
|
+
*/
|
|
255
|
+
spacing?: number;
|
|
256
|
+
className?: string;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Lets the glass inside it merge.
|
|
261
|
+
*
|
|
262
|
+
* On its own each piece of the material is a separate object with its own
|
|
263
|
+
* lit edge. Inside a container, pieces within `spacing` of each other flow
|
|
264
|
+
* together — which is what makes a button that opens into other buttons look
|
|
265
|
+
* like one thing dividing rather than several things arriving. A plain view
|
|
266
|
+
* wherever the material is not drawn, so it can be written unconditionally.
|
|
267
|
+
*/
|
|
268
|
+
export const GlassContainer = forwardRef<View, GlassContainerProps>(
|
|
269
|
+
({ spacing, className, style, children, ...props }, ref) => {
|
|
270
|
+
const material = useGlassMaterial();
|
|
271
|
+
if (material && GlassContainerView) {
|
|
272
|
+
// The native container is not a styled view, so the classes go on a
|
|
273
|
+
// view inside it and only the positional style stays on the outside.
|
|
274
|
+
return (
|
|
275
|
+
<GlassContainerView ref={ref} spacing={spacing} style={style} {...props}>
|
|
276
|
+
<View className={className}>{children}</View>
|
|
277
|
+
</GlassContainerView>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
return (
|
|
281
|
+
<View ref={ref} className={className} style={style} {...props}>
|
|
282
|
+
{children}
|
|
283
|
+
</View>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
GlassContainer.displayName = 'GlassContainer';
|