@oxy.so/bloom 7.4.0 → 7.6.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/docs/adoption-matrix.mdx +5 -5
- package/docs/layout.mdx +80 -0
- package/docs/page-footer.mdx +13 -0
- package/docs/page-header.mdx +7 -0
- package/lib/commonjs/index.js +28 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +28 -0
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/layout/ScrollArea.js +41 -0
- package/lib/commonjs/layout/ScrollArea.js.map +1 -0
- package/lib/commonjs/layout/index.js +28 -0
- package/lib/commonjs/layout/index.js.map +1 -1
- package/lib/commonjs/layout/screen-scope.js +6 -2
- package/lib/commonjs/layout/screen-scope.js.map +1 -1
- package/lib/commonjs/layout/scroll-metrics-events.js +19 -0
- package/lib/commonjs/layout/scroll-metrics-events.js.map +1 -0
- package/lib/commonjs/layout/scroll-metrics.js +50 -0
- package/lib/commonjs/layout/scroll-metrics.js.map +1 -0
- package/lib/commonjs/layout/scroll-offset.js +5 -1
- package/lib/commonjs/layout/scroll-offset.js.map +1 -1
- package/lib/commonjs/layout/use-animated-scroll-metrics-binding.js +50 -0
- package/lib/commonjs/layout/use-animated-scroll-metrics-binding.js.map +1 -0
- package/lib/commonjs/layout/use-scroll-metrics-binding.js +50 -0
- package/lib/commonjs/layout/use-scroll-metrics-binding.js.map +1 -0
- package/lib/commonjs/page-footer/PageFooter.js +10 -2
- package/lib/commonjs/page-footer/PageFooter.js.map +1 -1
- package/lib/commonjs/page-header/PageHeader.js +4 -2
- package/lib/commonjs/page-header/PageHeader.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +1 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/layout/ScrollArea.js +36 -0
- package/lib/module/layout/ScrollArea.js.map +1 -0
- package/lib/module/layout/index.js +4 -0
- package/lib/module/layout/index.js.map +1 -1
- package/lib/module/layout/screen-scope.js +6 -2
- package/lib/module/layout/screen-scope.js.map +1 -1
- package/lib/module/layout/scroll-metrics-events.js +15 -0
- package/lib/module/layout/scroll-metrics-events.js.map +1 -0
- package/lib/module/layout/scroll-metrics.js +42 -0
- package/lib/module/layout/scroll-metrics.js.map +1 -0
- package/lib/module/layout/scroll-offset.js +5 -1
- package/lib/module/layout/scroll-offset.js.map +1 -1
- package/lib/module/layout/use-animated-scroll-metrics-binding.js +46 -0
- package/lib/module/layout/use-animated-scroll-metrics-binding.js.map +1 -0
- package/lib/module/layout/use-scroll-metrics-binding.js +47 -0
- package/lib/module/layout/use-scroll-metrics-binding.js.map +1 -0
- package/lib/module/page-footer/PageFooter.js +10 -2
- package/lib/module/page-footer/PageFooter.js.map +1 -1
- package/lib/module/page-header/PageHeader.js +4 -2
- package/lib/module/page-header/PageHeader.js.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -2
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +2 -2
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/ScrollArea.d.ts +11 -0
- package/lib/typescript/commonjs/layout/ScrollArea.d.ts.map +1 -0
- package/lib/typescript/commonjs/layout/index.d.ts +7 -0
- package/lib/typescript/commonjs/layout/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/screen-scope.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/scroll-metrics-events.d.ts +5 -0
- package/lib/typescript/commonjs/layout/scroll-metrics-events.d.ts.map +1 -0
- package/lib/typescript/commonjs/layout/scroll-metrics.d.ts +15 -0
- package/lib/typescript/commonjs/layout/scroll-metrics.d.ts.map +1 -0
- package/lib/typescript/commonjs/layout/scroll-offset.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/use-animated-scroll-metrics-binding.d.ts +19 -0
- package/lib/typescript/commonjs/layout/use-animated-scroll-metrics-binding.d.ts.map +1 -0
- package/lib/typescript/commonjs/layout/use-scroll-metrics-binding.d.ts +11 -0
- package/lib/typescript/commonjs/layout/use-scroll-metrics-binding.d.ts.map +1 -0
- package/lib/typescript/commonjs/page-footer/PageFooter.d.ts +1 -1
- package/lib/typescript/commonjs/page-footer/PageFooter.d.ts.map +1 -1
- package/lib/typescript/commonjs/page-footer/types.d.ts +8 -2
- package/lib/typescript/commonjs/page-footer/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/page-header/PageHeader.d.ts.map +1 -1
- package/lib/typescript/commonjs/page-header/types.d.ts +3 -4
- package/lib/typescript/commonjs/page-header/types.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -2
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +2 -2
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/layout/ScrollArea.d.ts +11 -0
- package/lib/typescript/module/layout/ScrollArea.d.ts.map +1 -0
- package/lib/typescript/module/layout/index.d.ts +7 -0
- package/lib/typescript/module/layout/index.d.ts.map +1 -1
- package/lib/typescript/module/layout/screen-scope.d.ts.map +1 -1
- package/lib/typescript/module/layout/scroll-metrics-events.d.ts +5 -0
- package/lib/typescript/module/layout/scroll-metrics-events.d.ts.map +1 -0
- package/lib/typescript/module/layout/scroll-metrics.d.ts +15 -0
- package/lib/typescript/module/layout/scroll-metrics.d.ts.map +1 -0
- package/lib/typescript/module/layout/scroll-offset.d.ts.map +1 -1
- package/lib/typescript/module/layout/use-animated-scroll-metrics-binding.d.ts +19 -0
- package/lib/typescript/module/layout/use-animated-scroll-metrics-binding.d.ts.map +1 -0
- package/lib/typescript/module/layout/use-scroll-metrics-binding.d.ts +11 -0
- package/lib/typescript/module/layout/use-scroll-metrics-binding.d.ts.map +1 -0
- package/lib/typescript/module/page-footer/PageFooter.d.ts +1 -1
- package/lib/typescript/module/page-footer/PageFooter.d.ts.map +1 -1
- package/lib/typescript/module/page-footer/types.d.ts +8 -2
- package/lib/typescript/module/page-footer/types.d.ts.map +1 -1
- package/lib/typescript/module/page-header/PageHeader.d.ts.map +1 -1
- package/lib/typescript/module/page-header/types.d.ts +3 -4
- package/lib/typescript/module/page-header/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +5 -1
- package/src/index.web.ts +5 -1
- package/src/layout/ScrollArea.tsx +25 -0
- package/src/layout/index.ts +8 -0
- package/src/layout/screen-scope.tsx +2 -1
- package/src/layout/scroll-metrics-events.ts +14 -0
- package/src/layout/scroll-metrics.tsx +35 -0
- package/src/layout/scroll-offset.ts +5 -1
- package/src/layout/use-animated-scroll-metrics-binding.ts +42 -0
- package/src/layout/use-scroll-metrics-binding.ts +37 -0
- package/src/page-footer/PageFooter.tsx +11 -4
- package/src/page-footer/types.ts +8 -2
- package/src/page-header/PageHeader.tsx +4 -2
- package/src/page-header/types.ts +3 -4
package/src/index.web.ts
CHANGED
|
@@ -174,13 +174,17 @@ export {
|
|
|
174
174
|
useClaimTopEdge,
|
|
175
175
|
useTopEdgeInset,
|
|
176
176
|
ScrollOffsetProvider,
|
|
177
|
+
ScrollMetricsProvider,
|
|
178
|
+
useScrollMetricsBinding,
|
|
179
|
+
useAnimatedScrollMetricsBinding,
|
|
180
|
+
ScrollArea,
|
|
177
181
|
useScrollOffset,
|
|
178
182
|
ScreenScope,
|
|
179
183
|
HeaderDockProvider,
|
|
180
184
|
useHeaderDockInset,
|
|
181
185
|
StickySection,
|
|
182
186
|
} from './layout';
|
|
183
|
-
export type { StickySectionProps } from './layout';
|
|
187
|
+
export type { StickySectionProps, ScrollMetricsBindingOptions, AnimatedScrollMetricsBindingOptions, ScrollAreaProps } from './layout';
|
|
184
188
|
export { Fab } from './fab/index.web';
|
|
185
189
|
export type { FabProps, FabSize } from './fab/index.web';
|
|
186
190
|
export { FrostedIconButton } from './frosted-icon-button/index.web';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React, { forwardRef, type ComponentType, type Ref } from 'react';
|
|
2
|
+
import { ScrollView, type ScrollViewProps } from 'react-native';
|
|
3
|
+
import { styled } from 'react-native-css';
|
|
4
|
+
import { useScrollMetricsBinding } from './use-scroll-metrics-binding';
|
|
5
|
+
|
|
6
|
+
export interface ScrollAreaProps extends ScrollViewProps {
|
|
7
|
+
/** Utilities for the scrolling viewport, resolved by Bloom's own interop. */
|
|
8
|
+
className?: string;
|
|
9
|
+
/** Utilities for the inner content container. */
|
|
10
|
+
contentContainerClassName?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Narrow only the styled mapping input to avoid ScrollViewProps' recursive
|
|
14
|
+
// dot-path union. The public component retains the complete ScrollView contract.
|
|
15
|
+
const ScrollAreaBase: ComponentType<Pick<ScrollViewProps, 'style' | 'contentContainerStyle'>> = ScrollView;
|
|
16
|
+
const StyledScrollView: ComponentType<ScrollAreaProps & { ref?: Ref<ScrollView> }> = styled(ScrollAreaBase, {
|
|
17
|
+
className: 'style',
|
|
18
|
+
contentContainerClassName: 'contentContainerStyle',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/** A ScrollView that publishes its measurements to surrounding page chrome. */
|
|
22
|
+
export const ScrollArea = forwardRef<ScrollView, ScrollAreaProps>(function ScrollArea({ onScroll, onLayout, onContentSizeChange, scrollEventThrottle, ...props }, ref) {
|
|
23
|
+
const binding = useScrollMetricsBinding({ onScroll, onLayout, onContentSizeChange });
|
|
24
|
+
return <StyledScrollView {...props} {...binding} scrollEventThrottle={scrollEventThrottle ?? binding.scrollEventThrottle} ref={ref} />;
|
|
25
|
+
});
|
package/src/layout/index.ts
CHANGED
|
@@ -18,3 +18,11 @@ export { ScreenScope } from './screen-scope';
|
|
|
18
18
|
export { HeaderDockProvider, useHeaderDockInset } from './header-dock';
|
|
19
19
|
export { StickySection } from './StickySection';
|
|
20
20
|
export type { StickySectionProps } from './StickySection';
|
|
21
|
+
|
|
22
|
+
export { ScrollMetricsProvider } from './scroll-metrics';
|
|
23
|
+
export { useScrollMetricsBinding } from './use-scroll-metrics-binding';
|
|
24
|
+
export type { ScrollMetricsBindingOptions } from './use-scroll-metrics-binding';
|
|
25
|
+
export { ScrollArea } from './ScrollArea';
|
|
26
|
+
export type { ScrollAreaProps } from './ScrollArea';
|
|
27
|
+
export { useAnimatedScrollMetricsBinding } from './use-animated-scroll-metrics-binding';
|
|
28
|
+
export type { AnimatedScrollMetricsBindingOptions } from './use-animated-scroll-metrics-binding';
|
|
@@ -39,6 +39,7 @@ import { ScreenContext, ScreenNavigationContext } from '../screen/context';
|
|
|
39
39
|
import type { PropsWithChildren } from 'react';
|
|
40
40
|
|
|
41
41
|
import { BottomEdgeProvider } from './bottom-edge';
|
|
42
|
+
import { ScrollMetricsContext } from './scroll-metrics';
|
|
42
43
|
import { ScrollOffsetProvider } from './scroll-offset';
|
|
43
44
|
import { TopEdgeProvider } from './top-edge';
|
|
44
45
|
|
|
@@ -49,7 +50,7 @@ export function ScreenScope({ children }: PropsWithChildren) {
|
|
|
49
50
|
<TopEdgeProvider>
|
|
50
51
|
<BottomEdgeProvider>
|
|
51
52
|
{/* null means no scroller has claimed this scope yet. */}
|
|
52
|
-
<ScrollOffsetProvider value={null}>{children}</ScrollOffsetProvider>
|
|
53
|
+
<ScrollMetricsContext.Provider value={null}><ScrollOffsetProvider value={null}>{children}</ScrollOffsetProvider></ScrollMetricsContext.Provider>
|
|
53
54
|
</BottomEdgeProvider>
|
|
54
55
|
</TopEdgeProvider>
|
|
55
56
|
</ScreenNavigationContext.Provider>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { NativeScrollEvent } from 'react-native';
|
|
2
|
+
import type { ScrollMetrics } from './scroll-metrics';
|
|
3
|
+
|
|
4
|
+
/** Shared by ordinary RN events and UI-thread animated events. */
|
|
5
|
+
export function recordScrollMetrics(metrics: ScrollMetrics | null, event: NativeScrollEvent): void {
|
|
6
|
+
'worklet';
|
|
7
|
+
if (!metrics) return;
|
|
8
|
+
const contentHeight = Number.isFinite(event.contentSize.height) ? Math.max(0, event.contentSize.height) : metrics.contentHeight.value;
|
|
9
|
+
const viewportHeight = Number.isFinite(event.layoutMeasurement.height) ? Math.max(0, event.layoutMeasurement.height) : metrics.viewportHeight.value;
|
|
10
|
+
metrics.contentHeight.value = contentHeight;
|
|
11
|
+
metrics.viewportHeight.value = viewportHeight;
|
|
12
|
+
const max = Math.max(0, contentHeight - viewportHeight);
|
|
13
|
+
if (Number.isFinite(event.contentOffset.y)) metrics.scrollY.value = Math.min(max, Math.max(0, event.contentOffset.y));
|
|
14
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo, type PropsWithChildren } from 'react';
|
|
2
|
+
import { useSharedValue, type SharedValue } from 'react-native-reanimated';
|
|
3
|
+
import { ScrollOffsetProvider } from './scroll-offset';
|
|
4
|
+
|
|
5
|
+
export interface ScrollMetrics {
|
|
6
|
+
scrollY: SharedValue<number>;
|
|
7
|
+
contentHeight: SharedValue<number>;
|
|
8
|
+
viewportHeight: SharedValue<number>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Internal scope boundary; overlay screens must not follow the page beneath them. */
|
|
12
|
+
export const ScrollMetricsContext = createContext<ScrollMetrics | null>(null);
|
|
13
|
+
|
|
14
|
+
/** Pair one scroller with its surrounding header/footer without adding a layout box. */
|
|
15
|
+
export function ScrollMetricsProvider({ children }: PropsWithChildren) {
|
|
16
|
+
const scrollY = useSharedValue(0);
|
|
17
|
+
const contentHeight = useSharedValue(-1);
|
|
18
|
+
const viewportHeight = useSharedValue(0);
|
|
19
|
+
const value = useMemo(() => ({ scrollY, contentHeight, viewportHeight }), [scrollY, contentHeight, viewportHeight]);
|
|
20
|
+
return <ScrollMetricsContext.Provider value={value}>
|
|
21
|
+
<ScrollOffsetProvider value={scrollY}>{children}</ScrollOffsetProvider>
|
|
22
|
+
</ScrollMetricsContext.Provider>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useScrollMetricsValue(): ScrollMetrics | null {
|
|
26
|
+
return useContext(ScrollMetricsContext);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Unmeasured and non-scrollable content has no hidden bottom edge. */
|
|
30
|
+
export function remainingScrollDistance(metrics: ScrollMetrics | null): number {
|
|
31
|
+
'worklet';
|
|
32
|
+
if (!metrics || metrics.viewportHeight.value <= 0 || metrics.contentHeight.value < 0) return 0;
|
|
33
|
+
const max = Math.max(0, metrics.contentHeight.value - metrics.viewportHeight.value);
|
|
34
|
+
return Math.max(0, max - Math.max(0, metrics.scrollY.value));
|
|
35
|
+
}
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
* one scroll container has moved. The container is the screen's, and the chrome
|
|
9
9
|
* is usually not inside it, so the number has to travel.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* Prefer ScrollMetricsProvider with ScrollArea when the footer also needs to
|
|
12
|
+
* know the remaining content. This lower-level offset-only provider remains
|
|
13
|
+
* useful for an existing animated scroll owner.
|
|
14
|
+
*
|
|
15
|
+
* Originally it travelled as a prop, and the default when the prop was omitted
|
|
12
16
|
* was `window.scrollY` on web and "nothing moves" on native. That default is
|
|
13
17
|
* right for exactly one layout — a web page whose document IS the scroller —
|
|
14
18
|
* and silently wrong for the two that a Bloom app usually has: a desktop panel
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import type { NativeScrollEvent, NativeSyntheticEvent, ScrollViewProps } from 'react-native';
|
|
3
|
+
import { runOnJS, useAnimatedScrollHandler, useComposedEventHandler, type ScrollHandlerProcessed } from 'react-native-reanimated';
|
|
4
|
+
import { useScrollMetricsValue } from './scroll-metrics';
|
|
5
|
+
import { recordScrollMetrics } from './scroll-metrics-events';
|
|
6
|
+
import { useScrollMetricsBinding } from './use-scroll-metrics-binding';
|
|
7
|
+
|
|
8
|
+
export interface AnimatedScrollMetricsBindingOptions<Context extends Record<string, unknown> = Record<string, unknown>>
|
|
9
|
+
extends Pick<ScrollViewProps, 'onLayout' | 'onContentSizeChange'> {
|
|
10
|
+
/** Optional ordinary RN observer, bridged to JS only when supplied (e.g. restoration.onScroll). */
|
|
11
|
+
onScroll?: ScrollViewProps['onScroll'];
|
|
12
|
+
/** Existing Reanimated handler, including tab-bar minimize or other animated scroll handlers. */
|
|
13
|
+
handler?: ScrollHandlerProcessed<Context> | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Bind an Animated ScrollView / virtualized list with metrics updated on the UI
|
|
18
|
+
* thread. Composition preserves consumer drag and momentum subscriptions.
|
|
19
|
+
*/
|
|
20
|
+
export function useAnimatedScrollMetricsBinding<Context extends Record<string, unknown> = Record<string, unknown>>({
|
|
21
|
+
handler = null, onScroll: observeScroll, onLayout, onContentSizeChange,
|
|
22
|
+
}: AnimatedScrollMetricsBindingOptions<Context> = {}) {
|
|
23
|
+
const metrics = useScrollMetricsValue();
|
|
24
|
+
if (observeScroll != null && typeof observeScroll !== 'function') {
|
|
25
|
+
throw new Error('Bloom useAnimatedScrollMetricsBinding onScroll expects a plain RN callback; pass a Reanimated handler through the handler option.');
|
|
26
|
+
}
|
|
27
|
+
// The optional ordinary RN observer (for example scroll restoration) runs on
|
|
28
|
+
// JS. Metrics and composed animated consumers remain on the UI thread.
|
|
29
|
+
const reportScroll = useCallback((event: NativeScrollEvent) => {
|
|
30
|
+
observeScroll?.({ nativeEvent: event } as NativeSyntheticEvent<NativeScrollEvent>);
|
|
31
|
+
}, [observeScroll]);
|
|
32
|
+
const reportsScroll = observeScroll != null;
|
|
33
|
+
const measurements = useScrollMetricsBinding({ onLayout, onContentSizeChange });
|
|
34
|
+
const ownHandler = useAnimatedScrollHandler<Context>({
|
|
35
|
+
onScroll: event => {
|
|
36
|
+
recordScrollMetrics(metrics, event);
|
|
37
|
+
if (reportsScroll) runOnJS(reportScroll)(event);
|
|
38
|
+
},
|
|
39
|
+
}, [metrics, reportsScroll, reportScroll]);
|
|
40
|
+
const onScroll = useComposedEventHandler([ownHandler, handler]);
|
|
41
|
+
return { ...measurements, onScroll };
|
|
42
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import type { ScrollViewProps } from 'react-native';
|
|
3
|
+
import { recordScrollMetrics } from './scroll-metrics-events';
|
|
4
|
+
import { useScrollMetricsValue } from './scroll-metrics';
|
|
5
|
+
|
|
6
|
+
/** Events accepted by ordinary RN ScrollView, FlatList and SectionList. */
|
|
7
|
+
export type ScrollMetricsBindingOptions = Pick<ScrollViewProps, 'onScroll' | 'onLayout' | 'onContentSizeChange'>;
|
|
8
|
+
|
|
9
|
+
/** Bind one scrolling owner to the nearest ScrollMetricsProvider. No React updates per frame. */
|
|
10
|
+
export function useScrollMetricsBinding({ onScroll, onLayout, onContentSizeChange }: ScrollMetricsBindingOptions = {}) {
|
|
11
|
+
const metrics = useScrollMetricsValue();
|
|
12
|
+
// Reanimated types its processed handler as a function, but returns an object.
|
|
13
|
+
// Reject that mismatch before an event instead of attempting to call it on JS.
|
|
14
|
+
if (onScroll != null && typeof onScroll !== 'function') {
|
|
15
|
+
throw new Error('Bloom useScrollMetricsBinding expects a plain RN onScroll callback. Use useAnimatedScrollMetricsBinding({ handler }) for Reanimated handlers.');
|
|
16
|
+
}
|
|
17
|
+
const handleScroll = useCallback<NonNullable<ScrollViewProps['onScroll']>>(event => {
|
|
18
|
+
recordScrollMetrics(metrics, event.nativeEvent);
|
|
19
|
+
onScroll?.(event);
|
|
20
|
+
}, [metrics, onScroll]);
|
|
21
|
+
const handleLayout = useCallback<NonNullable<ScrollViewProps['onLayout']>>(event => {
|
|
22
|
+
const height = event.nativeEvent.layout.height;
|
|
23
|
+
if (metrics && Number.isFinite(height)) {
|
|
24
|
+
metrics.viewportHeight.value = Math.max(0, height);
|
|
25
|
+
if (metrics.contentHeight.value >= 0) metrics.scrollY.value = Math.min(metrics.scrollY.value, Math.max(0, metrics.contentHeight.value - height));
|
|
26
|
+
}
|
|
27
|
+
onLayout?.(event);
|
|
28
|
+
}, [metrics, onLayout]);
|
|
29
|
+
const handleContentSizeChange = useCallback<NonNullable<ScrollViewProps['onContentSizeChange']>>((width, height) => {
|
|
30
|
+
if (metrics && Number.isFinite(height)) {
|
|
31
|
+
metrics.contentHeight.value = Math.max(0, height);
|
|
32
|
+
metrics.scrollY.value = Math.min(metrics.scrollY.value, Math.max(0, height - metrics.viewportHeight.value));
|
|
33
|
+
}
|
|
34
|
+
onContentSizeChange?.(width, height);
|
|
35
|
+
}, [metrics, onContentSizeChange]);
|
|
36
|
+
return { onScroll: handleScroll, onLayout: handleLayout, onContentSizeChange: handleContentSizeChange, scrollEventThrottle: 16 };
|
|
37
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React, { memo, useContext, useEffect, useId, useState } from 'react';
|
|
2
2
|
import { Platform, StyleSheet, View, useWindowDimensions, type LayoutChangeEvent } from 'react-native';
|
|
3
3
|
import { SafeAreaInsetsContext } from 'react-native-safe-area-context';
|
|
4
|
+
import Animated, { useAnimatedStyle } from 'react-native-reanimated';
|
|
5
|
+
import { remainingScrollDistance, useScrollMetricsValue } from '../layout/scroll-metrics';
|
|
4
6
|
import { BloomScope } from '../appearance';
|
|
5
7
|
import { useBottomEdgeInset } from '../layout/bottom-edge';
|
|
6
8
|
import { EdgeScrim, SCRIM_TAIL_RATIO } from '../page-header/EdgeScrim';
|
|
@@ -15,9 +17,14 @@ const BAR_HEIGHT = 56;
|
|
|
15
17
|
|
|
16
18
|
/** Page-local controls floating over a bounded scroller, above app navigation. */
|
|
17
19
|
function PageFooterComponent({
|
|
18
|
-
children, actions, bottomInset, safeArea, scrim = 'always', scrimColor, style, testID,
|
|
20
|
+
children, actions, bottomInset, safeArea, scrim = 'always', scrimColor, scrollThreshold = 20, style, testID,
|
|
19
21
|
}: PageFooterProps) {
|
|
20
22
|
const theme = useTheme();
|
|
23
|
+
const metrics = useScrollMetricsValue();
|
|
24
|
+
const threshold = Math.max(1, Number.isFinite(scrollThreshold) ? scrollThreshold : 20);
|
|
25
|
+
const scrimStyle = useAnimatedStyle(() => ({
|
|
26
|
+
opacity: scrim === 'auto' ? Math.min(1, remainingScrollDistance(metrics) / threshold) : 1,
|
|
27
|
+
}), [metrics, scrim, threshold]);
|
|
21
28
|
const fill = useSurfaceFill() ?? theme.colors.background;
|
|
22
29
|
const occupied = useBottomEdgeInset();
|
|
23
30
|
const rawBottom = bottomInset ?? occupied;
|
|
@@ -51,13 +58,13 @@ function PageFooterComponent({
|
|
|
51
58
|
style={[styles.container, { bottom, paddingBottom: padBottom }, style]}
|
|
52
59
|
>
|
|
53
60
|
{scrim !== 'none' ? (
|
|
54
|
-
<View
|
|
61
|
+
<Animated.View
|
|
55
62
|
pointerEvents="none"
|
|
56
|
-
style={[styles.scrim, { height: height * (1 + SCRIM_TAIL_RATIO) }]}
|
|
63
|
+
style={[styles.scrim, { height: height * (1 + SCRIM_TAIL_RATIO) }, scrimStyle]}
|
|
57
64
|
testID={testID ? `${testID}-scrim` : undefined}
|
|
58
65
|
>
|
|
59
66
|
<EdgeScrim edge="bottom" color={scrimColor ?? fill} testID={testID ? `${testID}-scrim-gradient` : undefined} />
|
|
60
|
-
</View>
|
|
67
|
+
</Animated.View>
|
|
61
68
|
) : null}
|
|
62
69
|
<View
|
|
63
70
|
onLayout={onLayout}
|
package/src/page-footer/types.ts
CHANGED
|
@@ -17,8 +17,14 @@ export interface PageFooterProps {
|
|
|
17
17
|
* true on native, false on web. Disable when the containing shell owns it.
|
|
18
18
|
*/
|
|
19
19
|
safeArea?: boolean;
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Mirrored PageHeader gradient. Default always preserves existing behavior.
|
|
22
|
+
* auto follows ScrollMetricsProvider: hidden at the bottom or for short /
|
|
23
|
+
* unmeasured content, fading in over scrollThreshold of remaining content.
|
|
24
|
+
*/
|
|
25
|
+
scrim?: 'auto' | 'always' | 'none';
|
|
26
|
+
/** Distance from the bottom over which the automatic scrim fades. Default 20. */
|
|
27
|
+
scrollThreshold?: number;
|
|
22
28
|
/** Opaque gradient color; defaults to the containing surface's fill. */
|
|
23
29
|
scrimColor?: string;
|
|
24
30
|
style?: StyleProp<ViewStyle>;
|
|
@@ -24,6 +24,7 @@ import { BUTTON_SHADOW } from '../button/shared';
|
|
|
24
24
|
import { BloomScope } from "../appearance";
|
|
25
25
|
import { GlassIsland } from '../glass';
|
|
26
26
|
import { RiArrowLeftLine } from '../icons/remix/RiArrowLeftLine';
|
|
27
|
+
import { useScrollMetricsValue } from '../layout/scroll-metrics';
|
|
27
28
|
import { useClaimTopEdge, useScrollOffset } from '../layout';
|
|
28
29
|
import { BREAKPOINTS } from '../styles/breakpoints';
|
|
29
30
|
import { WEB_POSITION_STICKY, WEB_SURFACE_STICKY_TOP, type WebCssStyle } from '../styles/web-view-style';
|
|
@@ -186,14 +187,15 @@ function PageHeaderComponent({
|
|
|
186
187
|
|
|
187
188
|
// ── The scroll owner ──────────────────────────────────────────────────────
|
|
188
189
|
//
|
|
189
|
-
//
|
|
190
|
+
// Explicit offset, dock, local metrics, Screen, legacy offset, then the web document.
|
|
190
191
|
// The context step is the one that matters for a Bloom composition: without
|
|
191
192
|
// it a header inside a scrolling panel followed `window.scrollY`, which on a
|
|
192
193
|
// desktop shell never moves, so the header looked deliberately inert.
|
|
193
194
|
const screen = useScreenContext();
|
|
194
195
|
const contextScrollY = useScrollOffset();
|
|
196
|
+
const metrics = useScrollMetricsValue();
|
|
195
197
|
const internalScrollY = useSharedValue(0);
|
|
196
|
-
const scrollY = externalScrollY ?? dock?.scrollY ?? screen?.scrollY ?? contextScrollY ?? internalScrollY;
|
|
198
|
+
const scrollY = externalScrollY ?? dock?.scrollY ?? metrics?.scrollY ?? screen?.scrollY ?? contextScrollY ?? internalScrollY;
|
|
197
199
|
const followsWindow = !externalScrollY && !dock && !screen && !contextScrollY;
|
|
198
200
|
|
|
199
201
|
useEffect(() => {
|
package/src/page-header/types.ts
CHANGED
|
@@ -142,10 +142,9 @@ export interface PageHeaderProps {
|
|
|
142
142
|
/**
|
|
143
143
|
* The scroll offset driving the scrim, the separator and the shadow.
|
|
144
144
|
*
|
|
145
|
-
* Resolution order: this prop,
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* On native with none of the three, the header stays at rest.
|
|
145
|
+
* Resolution order: this prop, HeaderDockProvider, ScrollMetricsProvider,
|
|
146
|
+
* the enclosing Screen, then ScrollOffsetProvider. On web with no local
|
|
147
|
+
* owner it follows window.scrollY; on native it stays at rest.
|
|
149
148
|
*/
|
|
150
149
|
scrollY?: SharedValue<number>;
|
|
151
150
|
/** The offset (px) over which the scrim, separator and shadow fade in. Default 20. */
|