react-native-divkit 1.6.5 → 1.7.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.
Files changed (63) hide show
  1. package/README.md +3 -1
  2. package/dist/DivKit.d.ts.map +1 -1
  3. package/dist/DivKit.js +6 -3
  4. package/dist/DivKit.js.map +1 -1
  5. package/dist/components/DivComponent.d.ts.map +1 -1
  6. package/dist/components/DivComponent.js +6 -2
  7. package/dist/components/DivComponent.js.map +1 -1
  8. package/dist/components/index.d.ts +4 -0
  9. package/dist/components/index.d.ts.map +1 -1
  10. package/dist/components/index.js +2 -0
  11. package/dist/components/index.js.map +1 -1
  12. package/dist/components/indicator/DivIndicator.d.ts +19 -0
  13. package/dist/components/indicator/DivIndicator.d.ts.map +1 -0
  14. package/dist/components/indicator/DivIndicator.js +112 -0
  15. package/dist/components/indicator/DivIndicator.js.map +1 -0
  16. package/dist/components/indicator/index.d.ts +3 -0
  17. package/dist/components/indicator/index.d.ts.map +1 -0
  18. package/dist/components/indicator/index.js +2 -0
  19. package/dist/components/indicator/index.js.map +1 -0
  20. package/dist/components/indicator/utils.d.ts +61 -0
  21. package/dist/components/indicator/utils.d.ts.map +1 -0
  22. package/dist/components/indicator/utils.js +104 -0
  23. package/dist/components/indicator/utils.js.map +1 -0
  24. package/dist/components/pager/DivPager.d.ts +22 -0
  25. package/dist/components/pager/DivPager.d.ts.map +1 -0
  26. package/dist/components/pager/DivPager.js +269 -0
  27. package/dist/components/pager/DivPager.js.map +1 -0
  28. package/dist/components/pager/index.d.ts +3 -0
  29. package/dist/components/pager/index.d.ts.map +1 -0
  30. package/dist/components/pager/index.js +2 -0
  31. package/dist/components/pager/index.js.map +1 -0
  32. package/dist/components/pager/utils.d.ts +96 -0
  33. package/dist/components/pager/utils.d.ts.map +1 -0
  34. package/dist/components/pager/utils.js +142 -0
  35. package/dist/components/pager/utils.js.map +1 -0
  36. package/dist/components/utilities/Outer.d.ts.map +1 -1
  37. package/dist/components/utilities/Outer.js +4 -3
  38. package/dist/components/utilities/Outer.js.map +1 -1
  39. package/dist/context/PagerContext.d.ts +30 -0
  40. package/dist/context/PagerContext.d.ts.map +1 -0
  41. package/dist/context/PagerContext.js +76 -0
  42. package/dist/context/PagerContext.js.map +1 -0
  43. package/dist/context/index.d.ts +1 -0
  44. package/dist/context/index.d.ts.map +1 -1
  45. package/dist/context/index.js +1 -0
  46. package/dist/context/index.js.map +1 -1
  47. package/package.json +2 -1
  48. package/src/DivKit.tsx +6 -3
  49. package/src/components/DivComponent.tsx +8 -2
  50. package/src/components/README.md +59 -5
  51. package/src/components/index.ts +4 -0
  52. package/src/components/indicator/DivIndicator.tsx +175 -0
  53. package/src/components/indicator/index.ts +2 -0
  54. package/src/components/indicator/utils.ts +149 -0
  55. package/src/components/pager/DivPager.tsx +393 -0
  56. package/src/components/pager/index.ts +2 -0
  57. package/src/components/pager/utils.ts +214 -0
  58. package/src/components/utilities/Outer.tsx +4 -2
  59. package/src/context/PagerContext.tsx +108 -0
  60. package/src/context/index.ts +8 -0
  61. package/src/types/indicator.d.ts +32 -0
  62. package/src/types/pager.d.ts +36 -0
  63. package/src/types/shape.d.ts +26 -0
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Pure helpers for DivPager. Extracted so they can be unit-tested without
3
+ * having to render the React tree.
4
+ */
5
+
6
+ export type ScrollAxisAlignment = 'start' | 'center' | 'end';
7
+
8
+ export interface LayoutModeFixed {
9
+ type: 'fixed';
10
+ neighbour_page_width?: { value?: number };
11
+ }
12
+ export interface LayoutModePercentage {
13
+ type: 'percentage';
14
+ page_width?: { value?: number };
15
+ }
16
+ export interface LayoutModeWrap {
17
+ type: 'wrap_content';
18
+ }
19
+ export type AnyLayoutMode =
20
+ | LayoutModeFixed
21
+ | LayoutModePercentage
22
+ | LayoutModeWrap
23
+ | { type?: string; [k: string]: unknown }
24
+ | null
25
+ | undefined;
26
+
27
+ export interface ComputePageSizeArgs {
28
+ containerSize: number;
29
+ layoutMode: AnyLayoutMode;
30
+ scrollAxisAlignment: ScrollAxisAlignment;
31
+ itemSpacing: number;
32
+ innerPadStart: number;
33
+ innerPadEnd: number;
34
+ }
35
+
36
+ /**
37
+ * Compute the size of a single page along the main axis. Mirrors the Web
38
+ * Pager.svelte autoSizeVal calculation:
39
+ * - fixed + center: containerSize − 2·neighbour − 2·spacing
40
+ * - fixed + start/end: containerSize − neighbour − spacing
41
+ * - percentage: containerSize · page_width / 100
42
+ * - wrap_content / unknown: usable area (containerSize − paddings)
43
+ */
44
+ export function computePageSize(args: ComputePageSizeArgs): number {
45
+ const { containerSize, layoutMode, scrollAxisAlignment, itemSpacing, innerPadStart, innerPadEnd } =
46
+ args;
47
+ if (containerSize <= 0) return 0;
48
+ const usable = containerSize - innerPadStart - innerPadEnd;
49
+
50
+ const lm = layoutMode as { type?: string; neighbour_page_width?: { value?: number }; page_width?: { value?: number } } | null | undefined;
51
+
52
+ if (lm && lm.type === 'fixed') {
53
+ const neighbourW = lm.neighbour_page_width?.value ?? 0;
54
+ if (scrollAxisAlignment === 'center') {
55
+ return Math.max(0, containerSize - 2 * neighbourW - 2 * itemSpacing);
56
+ }
57
+ return Math.max(0, containerSize - neighbourW - itemSpacing);
58
+ }
59
+ if (lm && lm.type === 'percentage') {
60
+ const pageW = lm.page_width?.value ?? 100;
61
+ return Math.max(0, (containerSize * pageW) / 100);
62
+ }
63
+ return Math.max(0, usable);
64
+ }
65
+
66
+ export interface ComputeContentPadArgs extends ComputePageSizeArgs {
67
+ pageSize: number;
68
+ isInfinite: boolean;
69
+ }
70
+
71
+ /**
72
+ * Compute the contentContainer paddings the inner ScrollView needs so that the
73
+ * first/last items snap to the right visual position (centre/start/end). In
74
+ * infinite mode the duplicates take that role and we use zero padding.
75
+ */
76
+ export function computeContentPad(args: ComputeContentPadArgs): { start: number; end: number } {
77
+ const {
78
+ containerSize,
79
+ pageSize,
80
+ innerPadStart,
81
+ innerPadEnd,
82
+ layoutMode,
83
+ scrollAxisAlignment,
84
+ itemSpacing,
85
+ isInfinite
86
+ } = args;
87
+
88
+ if (containerSize <= 0 || pageSize <= 0) {
89
+ return { start: innerPadStart, end: innerPadEnd };
90
+ }
91
+ if (isInfinite) {
92
+ return { start: 0, end: 0 };
93
+ }
94
+ const lm = layoutMode as { type?: string; neighbour_page_width?: { value?: number } } | null | undefined;
95
+ if (lm && lm.type === 'fixed') {
96
+ const neighbourW = lm.neighbour_page_width?.value ?? 0;
97
+ if (scrollAxisAlignment === 'center') {
98
+ const pad = neighbourW + itemSpacing;
99
+ return { start: pad, end: pad };
100
+ }
101
+ if (scrollAxisAlignment === 'start') {
102
+ return { start: innerPadStart, end: neighbourW + itemSpacing + innerPadEnd };
103
+ }
104
+ if (scrollAxisAlignment === 'end') {
105
+ return { start: neighbourW + itemSpacing + innerPadStart, end: innerPadEnd };
106
+ }
107
+ }
108
+ return { start: innerPadStart, end: innerPadEnd };
109
+ }
110
+
111
+ export const DUPLICATES_IN_INFINITE = 2;
112
+
113
+ /**
114
+ * Map a "real" item index (0..size-1) to its rendered position.
115
+ * In infinite mode the real items live in [DUPLICATES, DUPLICATES + size).
116
+ */
117
+ export function realToPosition(realIdx: number, isInfinite: boolean, duplicates = DUPLICATES_IN_INFINITE): number {
118
+ return isInfinite ? duplicates + realIdx : realIdx;
119
+ }
120
+
121
+ /**
122
+ * Map a rendered position back to the real index. Wraps modulo `size` when
123
+ * the position lands inside the duplicate region.
124
+ */
125
+ export function positionToReal(
126
+ pos: number,
127
+ isInfinite: boolean,
128
+ size: number,
129
+ duplicates = DUPLICATES_IN_INFINITE
130
+ ): number {
131
+ if (size <= 0) return 0;
132
+ if (!isInfinite) {
133
+ return Math.max(0, Math.min(size - 1, pos));
134
+ }
135
+ const inner = pos - duplicates;
136
+ return ((inner % size) + size) % size;
137
+ }
138
+
139
+ /**
140
+ * True when `pos` corresponds to one of the duplicate entries (only meaningful
141
+ * in infinite mode).
142
+ */
143
+ export function isInDuplicateRegion(
144
+ pos: number,
145
+ isInfinite: boolean,
146
+ size: number,
147
+ duplicates = DUPLICATES_IN_INFINITE
148
+ ): boolean {
149
+ if (!isInfinite) return false;
150
+ return pos < duplicates || pos >= duplicates + size;
151
+ }
152
+
153
+ export interface RenderedItemEntry<T> {
154
+ item: T;
155
+ realIndex: number;
156
+ key: string;
157
+ }
158
+
159
+ /**
160
+ * Build the list of items to render. In infinite mode this prefixes the array
161
+ * with `duplicates` copies of the last items and suffixes it with `duplicates`
162
+ * copies of the first items, so the user can swipe past either edge and land
163
+ * on something visually identical to the wrap-around target.
164
+ */
165
+ export function buildRenderedItems<T extends { id?: string }>(
166
+ items: T[],
167
+ isInfinite: boolean,
168
+ duplicates = DUPLICATES_IN_INFINITE
169
+ ): RenderedItemEntry<T>[] {
170
+ if (!items.length) return [];
171
+ if (!isInfinite) {
172
+ return items.map((item, index) => ({ item, realIndex: index, key: `r-${index}` }));
173
+ }
174
+ const size = items.length;
175
+ const head: RenderedItemEntry<T>[] = [];
176
+ const tail: RenderedItemEntry<T>[] = [];
177
+ for (let i = 0; i < duplicates; i++) {
178
+ const realIdx = (size - duplicates + i + size) % size;
179
+ head.push({ item: items[realIdx], realIndex: realIdx, key: `dup-h-${i}` });
180
+ }
181
+ for (let i = 0; i < duplicates; i++) {
182
+ const realIdx = i % size;
183
+ tail.push({ item: items[realIdx], realIndex: realIdx, key: `dup-t-${i}` });
184
+ }
185
+ const real: RenderedItemEntry<T>[] = items.map((item, index) => ({
186
+ item,
187
+ realIndex: index,
188
+ key: `r-${index}`
189
+ }));
190
+ return [...head, ...real, ...tail];
191
+ }
192
+
193
+ /**
194
+ * Decide whether infinite_scroll should actually be active.
195
+ * Mirrors Web's correctBooleanInt + the `items.length >= DUPLICATES_IN_INFINITE`
196
+ * gate.
197
+ */
198
+ export function isInfiniteEnabled(infiniteValue: unknown, itemsLength: number): boolean {
199
+ const truthy =
200
+ infiniteValue === true ||
201
+ infiniteValue === 1 ||
202
+ infiniteValue === '1' ||
203
+ infiniteValue === 'true';
204
+ return truthy && itemsLength >= DUPLICATES_IN_INFINITE;
205
+ }
206
+
207
+ /**
208
+ * Convert a scroll offset (in px) into a snap position (rounded). Returns 0
209
+ * when snapInterval <= 0.
210
+ */
211
+ export function offsetToPosition(offset: number, snapInterval: number): number {
212
+ if (snapInterval <= 0) return 0;
213
+ return Math.round(offset / snapInterval);
214
+ }
@@ -122,6 +122,7 @@ export function Outer<T extends DivBaseData = DivBaseData>({
122
122
  const { direction } = useDivKitContext();
123
123
  const layoutParams = useLayoutParams();
124
124
  const { json, variables } = componentContext;
125
+ const testID = (json as any).id as string | undefined;
125
126
 
126
127
  // Only use reactive hooks for truly dynamic properties (visibility, alpha)
127
128
  const visibility = useDerivedFromVarsSimple<Visibility>(json.visibility || 'visible', variables || new Map());
@@ -518,6 +519,7 @@ export function Outer<T extends DivBaseData = DivBaseData>({
518
519
  onPressIn={onPressIn}
519
520
  onPressOut={onPressOut}
520
521
  style={outerStyle}
522
+ testID={testID}
521
523
  >
522
524
  <Animated.View style={animatedStyle}>
523
525
  <Background layers={background as any} style={borderStyle} />
@@ -528,7 +530,7 @@ export function Outer<T extends DivBaseData = DivBaseData>({
528
530
  }
529
531
 
530
532
  return (
531
- <Pressable onPress={handlePress} style={finalStyle}>
533
+ <Pressable onPress={handlePress} style={finalStyle} testID={testID}>
532
534
  <Background layers={background as any} style={borderStyle} />
533
535
  {children}
534
536
  </Pressable>
@@ -536,7 +538,7 @@ export function Outer<T extends DivBaseData = DivBaseData>({
536
538
  }
537
539
 
538
540
  return (
539
- <View style={finalStyle}>
541
+ <View style={finalStyle} testID={testID}>
540
542
  <Background layers={background as any} style={borderStyle} />
541
543
  {children}
542
544
  </View>
@@ -0,0 +1,108 @@
1
+ import { createContext, useContext, useCallback, useMemo, useRef, ReactNode, createElement } from 'react';
2
+ import type { PagerData, PagerListener, PagerRegisterData } from '../types/componentContext';
3
+
4
+ /**
5
+ * PagerContext — shared registry for pagers and their indicators.
6
+ *
7
+ * Based on Web Root.svelte: registerPager / listenPager.
8
+ *
9
+ * - Pager registers itself with `registerPager(pagerId)` and gets back an object with
10
+ * `update(data)` (called whenever current item / size changes) and `destroy()`.
11
+ * - Indicator subscribes to a pager via `listenPager(pagerId, cb)`.
12
+ * - Subscribers receive the current pager state immediately if it's already known
13
+ * (so indicator works regardless of mount order).
14
+ */
15
+ export interface PagerContextValue {
16
+ registerPager(pagerId: string | undefined): PagerRegisterData;
17
+ listenPager(pagerId: string | undefined, listener: PagerListener): () => void;
18
+ }
19
+
20
+ export const PagerContext = createContext<PagerContextValue | null>(null);
21
+
22
+ export function usePagerContextOptional(): PagerContextValue | null {
23
+ return useContext(PagerContext);
24
+ }
25
+
26
+ export function usePagerContext(): PagerContextValue {
27
+ const context = useContext(PagerContext);
28
+ if (!context) {
29
+ throw new Error('usePagerContext must be used within PagerContext.Provider');
30
+ }
31
+ return context;
32
+ }
33
+
34
+ export interface PagerProviderProps {
35
+ children: ReactNode;
36
+ }
37
+
38
+ /**
39
+ * Provider that holds pager state in stable refs. Mirrors the Web behaviour
40
+ * where registration / listening is keyed by pagerId (with `undefined` allowed
41
+ * so indicators without an explicit pager_id still work).
42
+ */
43
+ export function PagerProvider({ children }: PagerProviderProps) {
44
+ const pagersRef = useRef<Map<string | undefined, PagerData | null>>(new Map());
45
+ const listenersRef = useRef<Map<string | undefined, PagerListener[]>>(new Map());
46
+
47
+ const notify = useCallback((pagerId: string | undefined, data: PagerData) => {
48
+ const list = listenersRef.current.get(pagerId);
49
+ if (!list) return;
50
+ for (const fn of list) {
51
+ try {
52
+ fn(data);
53
+ } catch (err) {
54
+ // eslint-disable-next-line no-console
55
+ console.error('[DivKit Pager] listener error', err);
56
+ }
57
+ }
58
+ }, []);
59
+
60
+ const registerPager = useCallback((pagerId: string | undefined): PagerRegisterData => {
61
+ return {
62
+ update: (data: PagerData) => {
63
+ pagersRef.current.set(pagerId, data);
64
+ notify(pagerId, data);
65
+ },
66
+ destroy: () => {
67
+ pagersRef.current.set(pagerId, null);
68
+ }
69
+ };
70
+ }, [notify]);
71
+
72
+ const listenPager = useCallback(
73
+ (pagerId: string | undefined, listener: PagerListener): (() => void) => {
74
+ let list = listenersRef.current.get(pagerId);
75
+ if (!list) {
76
+ list = [];
77
+ listenersRef.current.set(pagerId, list);
78
+ }
79
+ list.push(listener);
80
+
81
+ // Replay last known state so indicator gets current pager position immediately
82
+ const current = pagersRef.current.get(pagerId);
83
+ if (current) {
84
+ try {
85
+ listener(current);
86
+ } catch (err) {
87
+ // eslint-disable-next-line no-console
88
+ console.error('[DivKit Pager] listener error', err);
89
+ }
90
+ }
91
+
92
+ return () => {
93
+ const arr = listenersRef.current.get(pagerId);
94
+ if (!arr) return;
95
+ const idx = arr.indexOf(listener);
96
+ if (idx >= 0) arr.splice(idx, 1);
97
+ };
98
+ },
99
+ []
100
+ );
101
+
102
+ const value = useMemo<PagerContextValue>(
103
+ () => ({ registerPager, listenPager }),
104
+ [registerPager, listenPager]
105
+ );
106
+
107
+ return createElement(PagerContext.Provider, { value }, children);
108
+ }
@@ -22,3 +22,11 @@ export {
22
22
  useIsEnabled,
23
23
  type EnabledContextValue
24
24
  } from './EnabledContext';
25
+
26
+ export {
27
+ PagerContext,
28
+ PagerProvider,
29
+ usePagerContext,
30
+ usePagerContextOptional,
31
+ type PagerContextValue
32
+ } from './PagerContext';
@@ -0,0 +1,32 @@
1
+ import type { DivBaseData } from './base';
2
+ import type { FixedSize } from './sizes';
3
+ import type { Shape } from './shape';
4
+
5
+ export interface DivIndicatorDefaultItemPlacement {
6
+ type: 'default';
7
+ space_between_centers?: FixedSize;
8
+ }
9
+
10
+ export interface DivIndicatorStretchItemPlacement {
11
+ type: 'stretch';
12
+ item_spacing?: FixedSize;
13
+ max_visible_items?: number;
14
+ }
15
+
16
+ export type DivIndicatorItemsPlacement = DivIndicatorDefaultItemPlacement | DivIndicatorStretchItemPlacement;
17
+
18
+ export interface DivIndicatorData extends DivBaseData {
19
+ type: 'indicator';
20
+ pager_id?: string;
21
+ /** @deprecated */
22
+ space_between_centers?: FixedSize;
23
+ inactive_item_color?: string;
24
+ active_item_color?: string;
25
+ /** @deprecated */
26
+ shape?: Shape;
27
+ active_shape?: Shape;
28
+ inactive_shape?: Shape;
29
+ /** @deprecated */
30
+ active_item_size?: number;
31
+ items_placement?: DivIndicatorItemsPlacement;
32
+ }
@@ -0,0 +1,36 @@
1
+ import type { DivBaseData } from './base';
2
+ import type { FixedSize, PercentageSize } from './sizes';
3
+ import type { BooleanInt } from '../../typings/common';
4
+
5
+ export interface PageSize {
6
+ type: 'percentage';
7
+ page_width: PercentageSize;
8
+ }
9
+
10
+ export interface NeighbourPageSize {
11
+ type: 'fixed';
12
+ neighbour_page_width: FixedSize;
13
+ }
14
+
15
+ export interface PageContentSize {
16
+ type: 'wrap_content';
17
+ }
18
+
19
+ export type PagerLayoutMode = PageSize | NeighbourPageSize | PageContentSize;
20
+
21
+ export type PagerItemAlignment = 'start' | 'center' | 'end';
22
+
23
+ export type PagerOrientation = 'vertical' | 'horizontal';
24
+
25
+ export interface DivPagerData extends DivBaseData {
26
+ type: 'pager';
27
+ scroll_axis_alignment?: PagerItemAlignment;
28
+ cross_axis_alignment?: PagerItemAlignment;
29
+ layout_mode: PagerLayoutMode;
30
+ item_spacing?: FixedSize;
31
+ items?: DivBaseData[];
32
+ orientation?: PagerOrientation;
33
+ restrict_parent_scroll?: BooleanInt;
34
+ default_item?: number;
35
+ infinite_scroll?: BooleanInt;
36
+ }
@@ -0,0 +1,26 @@
1
+ import type { FixedSize } from './sizes';
2
+
3
+ export interface Stroke {
4
+ color?: string;
5
+ width?: number;
6
+ unit?: 'sp' | 'dp' | 'px';
7
+ }
8
+
9
+ export interface ShapeBase {
10
+ background_color?: string;
11
+ stroke?: Stroke;
12
+ }
13
+
14
+ export interface RoundedRectangle extends ShapeBase {
15
+ type: 'rounded_rectangle';
16
+ item_width?: FixedSize;
17
+ item_height?: FixedSize;
18
+ corner_radius?: FixedSize;
19
+ }
20
+
21
+ export interface Circle extends ShapeBase {
22
+ type: 'circle';
23
+ radius?: FixedSize;
24
+ }
25
+
26
+ export type Shape = RoundedRectangle | Circle;