@lotics/ui 11.0.0 → 11.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "11.0.0",
3
+ "version": "11.1.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./tokens": "./src/tokens.ts",
@@ -31,20 +31,40 @@ export interface SectionNavHandle<K extends string> {
31
31
  *
32
32
  * Contract: every key's section stays MOUNTED (an unmounted section leaves its
33
33
  * last offset registered — conditional sections belong inside an always-mounted
34
- * wrapper that carries the `onLayout`), and offsets refresh only when a
35
- * section's own layout changes react-native-web's `onLayout` won't refire on
36
- * a pure position shift, so content above the sections should settle before
37
- * precision matters.
34
+ * wrapper that carries the `onLayout`). On WEB, `jumpTo` re-measures the live
35
+ * DOM position, so async content growing ABOVE a section (a photo grid, a
36
+ * late-loading table) can never stale a jump; the cached offsets only drive
37
+ * the scroll-spy highlight, which self-corrects as jumps refresh them. On
38
+ * native there is no re-measure — content above should settle before precision
39
+ * matters (`onLayout` won't refire on a pure position shift).
38
40
  */
39
41
  export function useSectionNav<K extends string>(keys: readonly [K, ...K[]]): SectionNavHandle<K> {
40
42
  const scrollRef = useRef<ScrollView>(null);
41
43
  const sectionY = useRef<Partial<Record<K, number>>>({});
44
+ // The registered wrapper's DOM node (web only) — RN-web's layout event
45
+ // exposes it as `nativeEvent.target`, which lets jumpTo re-measure live.
46
+ const sectionEl = useRef<Partial<Record<K, Element>>>({});
42
47
  const [activeKey, setActiveKey] = useState<K>(keys[0]);
43
48
  const register = (key: K) => (e: LayoutChangeEvent) => {
44
49
  sectionY.current[key] = e.nativeEvent.layout.y;
50
+ const target: unknown = (e.nativeEvent as { target?: unknown }).target;
51
+ if (typeof Element !== "undefined" && target instanceof Element) sectionEl.current[key] = target;
52
+ };
53
+ /** The section's CURRENT content-relative offset — measured live on web
54
+ * (immune to async content shifting the page), cached `layout.y` otherwise. */
55
+ const currentY = (key: K): number => {
56
+ const el = sectionEl.current[key];
57
+ const sv = scrollRef.current as (ScrollView & { getScrollableNode?: () => unknown }) | null;
58
+ const sc = typeof sv?.getScrollableNode === "function" ? sv.getScrollableNode() : null;
59
+ if (typeof Element !== "undefined" && el instanceof Element && sc instanceof Element) {
60
+ const y = el.getBoundingClientRect().top - sc.getBoundingClientRect().top + sc.scrollTop;
61
+ sectionY.current[key] = y; // refresh the spy's cache while we're here
62
+ return y;
63
+ }
64
+ return sectionY.current[key] ?? 0;
45
65
  };
46
66
  const jumpTo = (key: K) => {
47
- scrollRef.current?.scrollTo({ y: Math.max(0, (sectionY.current[key] ?? 0) - 12), animated: true });
67
+ scrollRef.current?.scrollTo({ y: Math.max(0, currentY(key) - 12), animated: true });
48
68
  };
49
69
  const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
50
70
  const y = e.nativeEvent.contentOffset.y + 80;