panelui-native 0.89.0 → 0.90.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.
@@ -37,6 +37,7 @@ export {
37
37
  } from './use-skeleton-handoff';
38
38
  export {
39
39
  useScrollSections,
40
+ type ScrollSectionsPosition,
40
41
  type UseScrollSectionsOptions,
41
42
  type UseScrollSectionsResult,
42
43
  } from './use-scroll-sections';
@@ -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
- () => ({ onScroll, onMomentumScrollEnd, scrollEventThrottle: 16 }),
172
- [onScroll, onMomentumScrollEnd]
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
@@ -386,6 +386,14 @@ export {
386
386
  type SectionRailPlacement,
387
387
  type SectionRailAlign,
388
388
  } from './components/section-rail';
389
+ export {
390
+ SectionProgress,
391
+ type SectionProgressProps,
392
+ type SectionProgressItemProps,
393
+ type SectionProgressPlacement,
394
+ type SectionProgressColor,
395
+ type SectionProgressScroll,
396
+ } from './components/section-progress';
389
397
  export {
390
398
  Tabs,
391
399
  type TabsProps,