react-native-terra-ui 0.11.15 → 0.11.17

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 (46) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/lib/module/components/button/Button.js +10 -0
  3. package/lib/module/components/button/Button.js.map +1 -1
  4. package/lib/module/components/chip/Chip.js +48 -5
  5. package/lib/module/components/chip/Chip.js.map +1 -1
  6. package/lib/module/components/chip/utils.js +16 -1
  7. package/lib/module/components/chip/utils.js.map +1 -1
  8. package/lib/module/components/header/parts/HeaderGradientOverlay.js +1 -1
  9. package/lib/module/components/header/parts/HeaderGradientOverlay.js.map +1 -1
  10. package/lib/module/components/header/parts/HeaderSurface.js +1 -1
  11. package/lib/module/components/header/parts/HeaderSurface.js.map +1 -1
  12. package/lib/module/components/page-indicator/hooks/use-page-indicator-window.js +60 -0
  13. package/lib/module/components/page-indicator/hooks/use-page-indicator-window.js.map +1 -0
  14. package/lib/module/components/page-indicator/utils.js +60 -0
  15. package/lib/module/components/page-indicator/utils.js.map +1 -1
  16. package/lib/module/components/page-indicator/variants/DotIndicator/index.js +13 -2
  17. package/lib/module/components/page-indicator/variants/DotIndicator/index.js.map +1 -1
  18. package/lib/module/components/page-indicator/variants/PillIndicator/index.js +29 -7
  19. package/lib/module/components/page-indicator/variants/PillIndicator/index.js.map +1 -1
  20. package/lib/typescript/src/components/button/Button.d.ts +1 -1
  21. package/lib/typescript/src/components/button/Button.d.ts.map +1 -1
  22. package/lib/typescript/src/components/chip/Chip.d.ts.map +1 -1
  23. package/lib/typescript/src/components/chip/types.d.ts +1 -1
  24. package/lib/typescript/src/components/chip/types.d.ts.map +1 -1
  25. package/lib/typescript/src/components/chip/utils.d.ts +1 -0
  26. package/lib/typescript/src/components/chip/utils.d.ts.map +1 -1
  27. package/lib/typescript/src/components/header/parts/HeaderGradientOverlay.d.ts +1 -1
  28. package/lib/typescript/src/components/header/parts/HeaderGradientOverlay.d.ts.map +1 -1
  29. package/lib/typescript/src/components/page-indicator/hooks/use-page-indicator-window.d.ts +34 -0
  30. package/lib/typescript/src/components/page-indicator/hooks/use-page-indicator-window.d.ts.map +1 -0
  31. package/lib/typescript/src/components/page-indicator/utils.d.ts +41 -0
  32. package/lib/typescript/src/components/page-indicator/utils.d.ts.map +1 -1
  33. package/lib/typescript/src/components/page-indicator/variants/DotIndicator/index.d.ts.map +1 -1
  34. package/lib/typescript/src/components/page-indicator/variants/PillIndicator/index.d.ts +1 -1
  35. package/lib/typescript/src/components/page-indicator/variants/PillIndicator/index.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/components/button/Button.tsx +11 -1
  38. package/src/components/chip/Chip.tsx +58 -6
  39. package/src/components/chip/types.ts +5 -0
  40. package/src/components/chip/utils.ts +16 -1
  41. package/src/components/header/parts/HeaderGradientOverlay.tsx +1 -1
  42. package/src/components/header/parts/HeaderSurface.tsx +1 -1
  43. package/src/components/page-indicator/hooks/use-page-indicator-window.ts +84 -0
  44. package/src/components/page-indicator/utils.ts +70 -0
  45. package/src/components/page-indicator/variants/DotIndicator/index.tsx +12 -1
  46. package/src/components/page-indicator/variants/PillIndicator/index.tsx +32 -2
@@ -73,6 +73,17 @@ export function pageIndicatorViewportClipStyle(
73
73
  };
74
74
  }
75
75
 
76
+ /**
77
+ * Holds the room of dots a variant chose not to mount.
78
+ *
79
+ * The track keeps its full width and keeps scrolling by the same amount, so
80
+ * skipped dots still have to occupy their slots — otherwise the mounted ones
81
+ * slide up the track and the window shows the wrong pages.
82
+ */
83
+ export function pageIndicatorSpacerStyle(vertical: boolean, mainSize: number): ViewStyle {
84
+ return vertical ? { height: mainSize } : { width: mainSize };
85
+ }
86
+
76
87
  function scaleCurveValue(curve: readonly number[], distance: number, tailScale: number): number {
77
88
  "worklet";
78
89
  if (distance < curve.length) return curve[distance] ?? tailScale;
@@ -178,3 +189,62 @@ export function pageIndicatorWindowScale(
178
189
 
179
190
  return pageIndicatorWindowSlotScale(positionInWindow, activeSlot, maxVisible);
180
191
  }
192
+
193
+ /**
194
+ * Dots kept mounted beyond each edge of the visible window.
195
+ *
196
+ * One full window's worth. The window itself is `maxVisible` slots wide, so
197
+ * this leaves a whole screenful of slack on either side — enough that ordinary
198
+ * navigation (a swipe, a few pages at a time) never outruns the mounted range
199
+ * between the page change and React re-rendering with a new one.
200
+ */
201
+ export const PAGE_INDICATOR_WINDOW_OVERSCAN = 5;
202
+
203
+ /**
204
+ * Which dot indices are worth mounting, inclusive, for an active page of
205
+ * `anchor`.
206
+ *
207
+ * An indicator draws `maxVisible` slots and clips the rest, and every dot more
208
+ * than one page from the active one is drawn identically — same size, same
209
+ * scale, same opacity, same colour. Mounting one view per page therefore costs
210
+ * an animated style per page to render something nobody can see: at 500 pages
211
+ * that is 500 worklets for five visible dots, and it lands on every page
212
+ * change. Bounding what is mounted is what stops the indicator's cost scaling
213
+ * with the length of the thing it indicates.
214
+ *
215
+ * Derived from `computePageIndicatorWindowTranslate`, the same function that
216
+ * positions the track, so the mounted range and the visible window cannot
217
+ * disagree about where the window is — including at the ends, where the
218
+ * translate clamps and the window stops following the active page.
219
+ *
220
+ * Below the overflow threshold every dot is visible, so the range is the whole
221
+ * track and nothing changes.
222
+ */
223
+ export function pageIndicatorMountedRange(
224
+ count: number,
225
+ anchor: number,
226
+ slot: number,
227
+ dotSize: number,
228
+ maxVisible: number,
229
+ activeMainSize = dotSize,
230
+ overscan = PAGE_INDICATOR_WINDOW_OVERSCAN
231
+ ): { start: number; end: number } {
232
+ const last = Math.max(0, count - 1);
233
+ if (count <= maxVisible) return { start: 0, end: last };
234
+
235
+ const translate = computePageIndicatorWindowTranslate(
236
+ count,
237
+ anchor,
238
+ slot,
239
+ dotSize,
240
+ maxVisible,
241
+ activeMainSize
242
+ );
243
+ // The same expression `pageIndicatorWindowScale` reads the window from.
244
+ const visibleStart = -translate / slot;
245
+
246
+ return {
247
+ start: Math.max(0, Math.floor(visibleStart) - overscan),
248
+ end: Math.min(last, Math.ceil(visibleStart) + maxVisible - 1 + overscan),
249
+ };
250
+ }
@@ -6,6 +6,7 @@ import Animated, { useAnimatedStyle } from "react-native-reanimated";
6
6
 
7
7
  import type { ColorToken } from "#theme/types";
8
8
  import { usePageIndicatorCore } from "../../hooks/use-page-indicator-core";
9
+ import { usePageIndicatorWindow } from "../../hooks/use-page-indicator-window";
9
10
  import { PageIndicatorRoot } from "../../PageIndicatorRoot";
10
11
  import {
11
12
  computePageIndicatorWindowTranslate,
@@ -71,7 +72,17 @@ export function DotIndicator({
71
72
  const fullTrackMainSize = pageIndicatorTrackMainSize(count, slot, dotSize);
72
73
  const overflows = count > maxVisible;
73
74
 
74
- const indices = useMemo(() => Array.from({ length: count }, (_, index) => index), [count]);
75
+ /*
76
+ No spacers here, unlike the pill variant: an `InactiveDot` is absolutely
77
+ positioned from its own index, so a dot that is not mounted leaves a gap
78
+ exactly where it would have been and the rest do not move.
79
+ */
80
+ const { start, end } = usePageIndicatorWindow(count, current, progress, slot, dotSize, maxVisible);
81
+
82
+ const indices = useMemo(
83
+ () => Array.from({ length: end - start + 1 }, (_, offset) => start + offset),
84
+ [start, end]
85
+ );
75
86
 
76
87
  const trackStyle = useAnimatedStyle(() => {
77
88
  const translate = computePageIndicatorWindowTranslate(count, progress.value, slot, dotSize, maxVisible);
@@ -1,17 +1,19 @@
1
1
  import { useMemo } from "react";
2
- import type { StyleProp, ViewStyle } from "react-native";
2
+ import { type StyleProp, View, type ViewStyle } from "react-native";
3
3
 
4
4
  import type { SharedValue } from "react-native-reanimated";
5
5
  import Animated, { useAnimatedStyle } from "react-native-reanimated";
6
6
 
7
7
  import type { ColorToken } from "#theme/types";
8
8
  import { usePageIndicatorCore } from "../../hooks/use-page-indicator-core";
9
+ import { usePageIndicatorWindow } from "../../hooks/use-page-indicator-window";
9
10
  import { PageIndicatorRoot } from "../../PageIndicatorRoot";
10
11
  import {
11
12
  computePageIndicatorWindowTranslate,
12
13
  DEFAULT_PAGE_INDICATOR_MAX_VISIBLE,
13
14
  PAGE_INDICATOR_SINGLE_PAGE_DURATION,
14
15
  pageIndicatorAxisTranslateStyle,
16
+ pageIndicatorSpacerStyle,
15
17
  pageIndicatorTrackMainSize,
16
18
  pageIndicatorViewportMainSize,
17
19
  } from "../../utils";
@@ -62,7 +64,33 @@ export function PillIndicator({
62
64
  const fullTrackMainSize = pageIndicatorTrackMainSize(count, slot, dotSize, activeWidth);
63
65
  const overflows = count > maxVisible;
64
66
 
65
- const indices = useMemo(() => Array.from({ length: count }, (_, index) => index), [count]);
67
+ const { start, end } = usePageIndicatorWindow(
68
+ count,
69
+ current,
70
+ progress,
71
+ slot,
72
+ dotSize,
73
+ maxVisible,
74
+ activeWidth
75
+ );
76
+
77
+ const indices = useMemo(
78
+ () => Array.from({ length: end - start + 1 }, (_, offset) => start + offset),
79
+ [start, end]
80
+ );
81
+
82
+ /*
83
+ The track is still `count` slots wide and still scrolls the same way, so the
84
+ dots that are not mounted have to leave their room behind. Each unmounted
85
+ dot occupies exactly one slot — it is inactive, and only the active dot is
86
+ ever wider than `dotSize` — so the space before the window is `start` slots
87
+ and the space after it is whatever is left.
88
+
89
+ Less one gap apiece, because the row's own `gap` supplies the space between
90
+ a spacer and the first dot beside it.
91
+ */
92
+ const leadMainSize = start * slot - gap;
93
+ const tailMainSize = (count - 1 - end) * slot - gap;
66
94
 
67
95
  const trackStyle = useAnimatedStyle(() => {
68
96
  const displayProgress = pillIndicatorJumpDisplayProgress(
@@ -107,6 +135,7 @@ export function PillIndicator({
107
135
  overflows ? trackStyle : null,
108
136
  ]}
109
137
  >
138
+ {leadMainSize > 0 && <View style={pageIndicatorSpacerStyle(vertical, leadMainSize)} />}
110
139
  {indices.map((index) => (
111
140
  <PillIndicatorDot
112
141
  key={index}
@@ -121,6 +150,7 @@ export function PillIndicator({
121
150
  maxVisible={maxVisible}
122
151
  />
123
152
  ))}
153
+ {tailMainSize > 0 && <View style={pageIndicatorSpacerStyle(vertical, tailMainSize)} />}
124
154
  </Animated.View>
125
155
  </PageIndicatorRoot>
126
156
  );