@oxyhq/bloom 1.0.6 → 1.2.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 (220) hide show
  1. package/docs/level-picker.mdx +105 -0
  2. package/docs/media-flight.mdx +272 -0
  3. package/docs/migrating-the-media-gallery.mdx +103 -0
  4. package/docs/migrating-to-1.0.mdx +1 -1
  5. package/docs/slider.mdx +3 -1
  6. package/docs/zoomable-media-gallery.mdx +116 -0
  7. package/lib/commonjs/floating/index.js.map +1 -1
  8. package/lib/commonjs/image-aspect-ratio-cache/aspect-ratio-cache.js +1 -1
  9. package/lib/commonjs/index.js +8 -0
  10. package/lib/commonjs/index.js.map +1 -1
  11. package/lib/commonjs/index.web.js +33 -25
  12. package/lib/commonjs/index.web.js.map +1 -1
  13. package/lib/commonjs/level-picker/LevelPicker.js +349 -0
  14. package/lib/commonjs/level-picker/LevelPicker.js.map +1 -0
  15. package/lib/commonjs/level-picker/LevelPicker.web.js +346 -0
  16. package/lib/commonjs/level-picker/LevelPicker.web.js.map +1 -0
  17. package/lib/commonjs/level-picker/constants.js +137 -0
  18. package/lib/commonjs/level-picker/constants.js.map +1 -0
  19. package/lib/commonjs/level-picker/index.js +13 -0
  20. package/lib/commonjs/level-picker/index.js.map +1 -0
  21. package/lib/commonjs/level-picker/index.web.js +13 -0
  22. package/lib/commonjs/level-picker/index.web.js.map +1 -0
  23. package/lib/commonjs/level-picker/types.js +6 -0
  24. package/lib/commonjs/level-picker/types.js.map +1 -0
  25. package/lib/commonjs/media-flight/MediaFlightLayer.js +151 -0
  26. package/lib/commonjs/media-flight/MediaFlightLayer.js.map +1 -0
  27. package/lib/commonjs/media-flight/MediaSurface.js +188 -0
  28. package/lib/commonjs/media-flight/MediaSurface.js.map +1 -0
  29. package/lib/commonjs/media-flight/constants.js +39 -0
  30. package/lib/commonjs/media-flight/constants.js.map +1 -0
  31. package/lib/commonjs/media-flight/expo-video-module.js +148 -0
  32. package/lib/commonjs/media-flight/expo-video-module.js.map +1 -0
  33. package/lib/commonjs/media-flight/index.js +102 -0
  34. package/lib/commonjs/media-flight/index.js.map +1 -0
  35. package/lib/commonjs/media-flight/store.js +457 -0
  36. package/lib/commonjs/media-flight/store.js.map +1 -0
  37. package/lib/commonjs/media-flight/types.js +6 -0
  38. package/lib/commonjs/media-flight/types.js.map +1 -0
  39. package/lib/commonjs/media-flight/use-media-flight.js +41 -0
  40. package/lib/commonjs/media-flight/use-media-flight.js.map +1 -0
  41. package/lib/commonjs/tab-bar/TabBarBase.js +1 -1
  42. package/lib/commonjs/{zoomable-image-gallery/ZoomableImageGallery.js → zoomable-media-gallery/ZoomableMediaGallery.js} +185 -131
  43. package/lib/commonjs/zoomable-media-gallery/ZoomableMediaGallery.js.map +1 -0
  44. package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/constants.js.map +1 -1
  45. package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/index.js +5 -5
  46. package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/index.js.map +1 -1
  47. package/lib/commonjs/zoomable-media-gallery/types.js +6 -0
  48. package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/types.js.map +1 -1
  49. package/lib/module/floating/index.js +5 -0
  50. package/lib/module/floating/index.js.map +1 -1
  51. package/lib/module/image-aspect-ratio-cache/aspect-ratio-cache.js +1 -1
  52. package/lib/module/index.js +3 -1
  53. package/lib/module/index.js.map +1 -1
  54. package/lib/module/index.web.js +3 -1
  55. package/lib/module/index.web.js.map +1 -1
  56. package/lib/module/level-picker/LevelPicker.js +343 -0
  57. package/lib/module/level-picker/LevelPicker.js.map +1 -0
  58. package/lib/module/level-picker/LevelPicker.web.js +340 -0
  59. package/lib/module/level-picker/LevelPicker.web.js.map +1 -0
  60. package/lib/module/level-picker/constants.js +131 -0
  61. package/lib/module/level-picker/constants.js.map +1 -0
  62. package/lib/module/level-picker/index.js +4 -0
  63. package/lib/module/level-picker/index.js.map +1 -0
  64. package/lib/module/level-picker/index.web.js +4 -0
  65. package/lib/module/level-picker/index.web.js.map +1 -0
  66. package/lib/module/level-picker/types.js +4 -0
  67. package/lib/module/level-picker/types.js.map +1 -0
  68. package/lib/module/media-flight/MediaFlightLayer.js +146 -0
  69. package/lib/module/media-flight/MediaFlightLayer.js.map +1 -0
  70. package/lib/module/media-flight/MediaSurface.js +180 -0
  71. package/lib/module/media-flight/MediaSurface.js.map +1 -0
  72. package/lib/module/media-flight/constants.js +35 -0
  73. package/lib/module/media-flight/constants.js.map +1 -0
  74. package/lib/module/media-flight/expo-video-module.js +142 -0
  75. package/lib/module/media-flight/expo-video-module.js.map +1 -0
  76. package/lib/module/media-flight/index.js +9 -0
  77. package/lib/module/media-flight/index.js.map +1 -0
  78. package/lib/module/media-flight/store.js +439 -0
  79. package/lib/module/media-flight/store.js.map +1 -0
  80. package/lib/module/media-flight/types.js +4 -0
  81. package/lib/module/media-flight/types.js.map +1 -0
  82. package/lib/module/media-flight/use-media-flight.js +36 -0
  83. package/lib/module/media-flight/use-media-flight.js.map +1 -0
  84. package/lib/module/tab-bar/TabBarBase.js +1 -1
  85. package/lib/module/{zoomable-image-gallery/ZoomableImageGallery.js → zoomable-media-gallery/ZoomableMediaGallery.js} +184 -130
  86. package/lib/module/zoomable-media-gallery/ZoomableMediaGallery.js.map +1 -0
  87. package/lib/module/{zoomable-image-gallery → zoomable-media-gallery}/constants.js.map +1 -1
  88. package/lib/module/zoomable-media-gallery/index.js +5 -0
  89. package/lib/module/zoomable-media-gallery/index.js.map +1 -0
  90. package/lib/module/zoomable-media-gallery/types.js +4 -0
  91. package/lib/module/{zoomable-image-gallery → zoomable-media-gallery}/types.js.map +1 -1
  92. package/lib/typescript/commonjs/floating/index.d.ts +5 -0
  93. package/lib/typescript/commonjs/floating/index.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/index.d.ts +2 -0
  95. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/index.web.d.ts +2 -0
  97. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  98. package/lib/typescript/commonjs/level-picker/LevelPicker.d.ts +6 -0
  99. package/lib/typescript/commonjs/level-picker/LevelPicker.d.ts.map +1 -0
  100. package/lib/typescript/commonjs/level-picker/LevelPicker.web.d.ts +6 -0
  101. package/lib/typescript/commonjs/level-picker/LevelPicker.web.d.ts.map +1 -0
  102. package/lib/typescript/commonjs/level-picker/constants.d.ts +93 -0
  103. package/lib/typescript/commonjs/level-picker/constants.d.ts.map +1 -0
  104. package/lib/typescript/commonjs/level-picker/index.d.ts +3 -0
  105. package/lib/typescript/commonjs/level-picker/index.d.ts.map +1 -0
  106. package/lib/typescript/commonjs/level-picker/index.web.d.ts +3 -0
  107. package/lib/typescript/commonjs/level-picker/index.web.d.ts.map +1 -0
  108. package/lib/typescript/commonjs/level-picker/types.d.ts +63 -0
  109. package/lib/typescript/commonjs/level-picker/types.d.ts.map +1 -0
  110. package/lib/typescript/commonjs/media-flight/MediaFlightLayer.d.ts +11 -0
  111. package/lib/typescript/commonjs/media-flight/MediaFlightLayer.d.ts.map +1 -0
  112. package/lib/typescript/commonjs/media-flight/MediaSurface.d.ts +106 -0
  113. package/lib/typescript/commonjs/media-flight/MediaSurface.d.ts.map +1 -0
  114. package/lib/typescript/commonjs/media-flight/constants.d.ts +18 -0
  115. package/lib/typescript/commonjs/media-flight/constants.d.ts.map +1 -0
  116. package/lib/typescript/commonjs/media-flight/expo-video-module.d.ts +103 -0
  117. package/lib/typescript/commonjs/media-flight/expo-video-module.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/media-flight/index.d.ts +9 -0
  119. package/lib/typescript/commonjs/media-flight/index.d.ts.map +1 -0
  120. package/lib/typescript/commonjs/media-flight/store.d.ts +132 -0
  121. package/lib/typescript/commonjs/media-flight/store.d.ts.map +1 -0
  122. package/lib/typescript/commonjs/media-flight/types.d.ts +195 -0
  123. package/lib/typescript/commonjs/media-flight/types.d.ts.map +1 -0
  124. package/lib/typescript/commonjs/media-flight/use-media-flight.d.ts +4 -0
  125. package/lib/typescript/commonjs/media-flight/use-media-flight.d.ts.map +1 -0
  126. package/lib/typescript/commonjs/zoomable-media-gallery/ZoomableMediaGallery.d.ts +5 -0
  127. package/lib/typescript/commonjs/zoomable-media-gallery/ZoomableMediaGallery.d.ts.map +1 -0
  128. package/lib/typescript/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/constants.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/zoomable-media-gallery/index.d.ts +4 -0
  130. package/lib/typescript/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/index.d.ts.map +1 -1
  131. package/lib/typescript/commonjs/zoomable-media-gallery/types.d.ts +90 -0
  132. package/lib/typescript/commonjs/zoomable-media-gallery/types.d.ts.map +1 -0
  133. package/lib/typescript/module/floating/index.d.ts +5 -0
  134. package/lib/typescript/module/floating/index.d.ts.map +1 -1
  135. package/lib/typescript/module/index.d.ts +2 -0
  136. package/lib/typescript/module/index.d.ts.map +1 -1
  137. package/lib/typescript/module/index.web.d.ts +2 -0
  138. package/lib/typescript/module/index.web.d.ts.map +1 -1
  139. package/lib/typescript/module/level-picker/LevelPicker.d.ts +6 -0
  140. package/lib/typescript/module/level-picker/LevelPicker.d.ts.map +1 -0
  141. package/lib/typescript/module/level-picker/LevelPicker.web.d.ts +6 -0
  142. package/lib/typescript/module/level-picker/LevelPicker.web.d.ts.map +1 -0
  143. package/lib/typescript/module/level-picker/constants.d.ts +93 -0
  144. package/lib/typescript/module/level-picker/constants.d.ts.map +1 -0
  145. package/lib/typescript/module/level-picker/index.d.ts +3 -0
  146. package/lib/typescript/module/level-picker/index.d.ts.map +1 -0
  147. package/lib/typescript/module/level-picker/index.web.d.ts +3 -0
  148. package/lib/typescript/module/level-picker/index.web.d.ts.map +1 -0
  149. package/lib/typescript/module/level-picker/types.d.ts +63 -0
  150. package/lib/typescript/module/level-picker/types.d.ts.map +1 -0
  151. package/lib/typescript/module/media-flight/MediaFlightLayer.d.ts +11 -0
  152. package/lib/typescript/module/media-flight/MediaFlightLayer.d.ts.map +1 -0
  153. package/lib/typescript/module/media-flight/MediaSurface.d.ts +106 -0
  154. package/lib/typescript/module/media-flight/MediaSurface.d.ts.map +1 -0
  155. package/lib/typescript/module/media-flight/constants.d.ts +18 -0
  156. package/lib/typescript/module/media-flight/constants.d.ts.map +1 -0
  157. package/lib/typescript/module/media-flight/expo-video-module.d.ts +103 -0
  158. package/lib/typescript/module/media-flight/expo-video-module.d.ts.map +1 -0
  159. package/lib/typescript/module/media-flight/index.d.ts +9 -0
  160. package/lib/typescript/module/media-flight/index.d.ts.map +1 -0
  161. package/lib/typescript/module/media-flight/store.d.ts +132 -0
  162. package/lib/typescript/module/media-flight/store.d.ts.map +1 -0
  163. package/lib/typescript/module/media-flight/types.d.ts +195 -0
  164. package/lib/typescript/module/media-flight/types.d.ts.map +1 -0
  165. package/lib/typescript/module/media-flight/use-media-flight.d.ts +4 -0
  166. package/lib/typescript/module/media-flight/use-media-flight.d.ts.map +1 -0
  167. package/lib/typescript/module/zoomable-media-gallery/ZoomableMediaGallery.d.ts +5 -0
  168. package/lib/typescript/module/zoomable-media-gallery/ZoomableMediaGallery.d.ts.map +1 -0
  169. package/lib/typescript/module/{zoomable-image-gallery → zoomable-media-gallery}/constants.d.ts.map +1 -1
  170. package/lib/typescript/module/zoomable-media-gallery/index.d.ts +4 -0
  171. package/lib/typescript/module/{zoomable-image-gallery → zoomable-media-gallery}/index.d.ts.map +1 -1
  172. package/lib/typescript/module/zoomable-media-gallery/types.d.ts +90 -0
  173. package/lib/typescript/module/zoomable-media-gallery/types.d.ts.map +1 -0
  174. package/package.json +61 -8
  175. package/src/floating/index.ts +5 -0
  176. package/src/image-aspect-ratio-cache/aspect-ratio-cache.ts +1 -1
  177. package/src/index.ts +4 -1
  178. package/src/index.web.ts +4 -1
  179. package/src/level-picker/LevelPicker.tsx +365 -0
  180. package/src/level-picker/LevelPicker.web.tsx +374 -0
  181. package/src/level-picker/constants.ts +131 -0
  182. package/src/level-picker/index.ts +2 -0
  183. package/src/level-picker/index.web.ts +2 -0
  184. package/src/level-picker/types.ts +63 -0
  185. package/src/media-flight/MediaFlightLayer.tsx +144 -0
  186. package/src/media-flight/MediaSurface.tsx +233 -0
  187. package/src/media-flight/constants.ts +32 -0
  188. package/src/media-flight/expo-video-module.ts +181 -0
  189. package/src/media-flight/index.ts +33 -0
  190. package/src/media-flight/store.ts +497 -0
  191. package/src/media-flight/types.ts +210 -0
  192. package/src/media-flight/use-media-flight.ts +42 -0
  193. package/src/tab-bar/TabBarBase.tsx +1 -1
  194. package/src/{zoomable-image-gallery/ZoomableImageGallery.tsx → zoomable-media-gallery/ZoomableMediaGallery.tsx} +192 -117
  195. package/src/zoomable-media-gallery/index.ts +11 -0
  196. package/src/zoomable-media-gallery/types.ts +100 -0
  197. package/docs/zoomable-image-gallery.mdx +0 -66
  198. package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js.map +0 -1
  199. package/lib/commonjs/zoomable-image-gallery/types.js +0 -2
  200. package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js.map +0 -1
  201. package/lib/module/zoomable-image-gallery/index.js +0 -5
  202. package/lib/module/zoomable-image-gallery/index.js.map +0 -1
  203. package/lib/module/zoomable-image-gallery/types.js +0 -2
  204. package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts +0 -5
  205. package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +0 -1
  206. package/lib/typescript/commonjs/zoomable-image-gallery/index.d.ts +0 -4
  207. package/lib/typescript/commonjs/zoomable-image-gallery/types.d.ts +0 -56
  208. package/lib/typescript/commonjs/zoomable-image-gallery/types.d.ts.map +0 -1
  209. package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts +0 -5
  210. package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +0 -1
  211. package/lib/typescript/module/zoomable-image-gallery/index.d.ts +0 -4
  212. package/lib/typescript/module/zoomable-image-gallery/types.d.ts +0 -56
  213. package/lib/typescript/module/zoomable-image-gallery/types.d.ts.map +0 -1
  214. package/src/zoomable-image-gallery/index.ts +0 -9
  215. package/src/zoomable-image-gallery/types.ts +0 -59
  216. /package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/constants.js +0 -0
  217. /package/lib/module/{zoomable-image-gallery → zoomable-media-gallery}/constants.js +0 -0
  218. /package/lib/typescript/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/constants.d.ts +0 -0
  219. /package/lib/typescript/module/{zoomable-image-gallery → zoomable-media-gallery}/constants.d.ts +0 -0
  220. /package/src/{zoomable-image-gallery → zoomable-media-gallery}/constants.ts +0 -0
package/src/index.web.ts CHANGED
@@ -22,7 +22,8 @@
22
22
  // `ScrollRestorationProvider` itself. `theme/adaptive-colors.ts` also
23
23
  // names expo-router, but through the optional-`require` boundary, which
24
24
  // links nothing.
25
- // · `./zoomable-image-gallery` — statically imports `expo-image`.
25
+ // · `./zoomable-media-gallery` and `./media-flight` both reach
26
+ // `expo-image` through the shared `media-flight/MediaSurface`.
26
27
  // Gate: `src/__tests__/root-barrel-graph.test.ts`.
27
28
  //
28
29
  // 2. Its exports are generic, collision-prone names. Those come in as a
@@ -238,6 +239,8 @@ export { InputGroup, InputGroupAddon } from './input-group';
238
239
  export type { InputGroupProps, InputGroupAddonProps } from './input-group';
239
240
  export { Slider } from './slider';
240
241
  export type { SliderProps } from './slider';
242
+ export { LevelPicker } from './level-picker/index.web';
243
+ export type { LevelPickerProps } from './level-picker/index.web';
241
244
  export { Combobox } from './combobox/index.web';
242
245
  export type { ComboboxProps, ComboboxOption } from './combobox/index.web';
243
246
 
@@ -0,0 +1,365 @@
1
+ /**
2
+ * `LevelPicker` — NATIVE. A stepped level slider over a disclosure that reveals
3
+ * a region of detail rows, laid out for the surface a native menu IS: a sheet.
4
+ *
5
+ * The web fork (`LevelPicker.web.tsx`) is the second half, and the split is not
6
+ * cosmetic. Three things differ, and each of them is the platform:
7
+ *
8
+ * 1. **The details region holds sub-menus, and a native sub-menu is an INLINE
9
+ * disclosure** (`floating/menu-sub-inline.tsx`) that grows the region when
10
+ * it opens. The web fork can therefore lay its regions out against a height
11
+ * it computes; this one cannot, so the picker is a plain column and every
12
+ * reveal is measured (`onLayout`) rather than assumed.
13
+ * 2. **There is no hover.** The end captions swap in while the rail is being
14
+ * DRAGGED here, which is the only "the user is working the slider" signal a
15
+ * touch screen has; the web fork adds pointer-over and keyboard focus.
16
+ * 3. **The gesture is a `PanResponder`**, not pointer capture, and the keyboard
17
+ * is an accessibility ACTION (`increment`/`decrement`) rather than arrow
18
+ * keys, because that is what an `adjustable` role exposes to VoiceOver and
19
+ * TalkBack.
20
+ *
21
+ * Everything both forks agree about — the stops, the inset, the hit mapping,
22
+ * the colour roles — is in `constants.ts` and is read from there by both, so
23
+ * the two cannot drift into disagreeing about the same control.
24
+ */
25
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
26
+ import {
27
+ Animated,
28
+ PanResponder,
29
+ type AccessibilityActionEvent,
30
+ type GestureResponderEvent,
31
+ type LayoutChangeEvent,
32
+ type PanResponderGestureState,
33
+ } from 'react-native';
34
+
35
+ import { ROW_ICON_SIZE } from '../floating/constants';
36
+ import { cx, MenuRowChevron, MenuRowShell, SUB_TRIGGER_CLASS } from '../floating/shared';
37
+ import { useControllableState } from '../hooks/use-controllable-state';
38
+ import { ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon } from '../icons/Chevron';
39
+ import { StyledText, StyledView } from '../styles/styled-primitives';
40
+ import { animation } from '../styles/tokens';
41
+ import { useTheme } from '../theme/use-theme';
42
+ import {
43
+ LEVEL_CAPTION_CLASS,
44
+ LEVEL_FILL_CLASS,
45
+ LEVEL_ROW_INSET,
46
+ LEVEL_SEPARATOR_CLASS,
47
+ LEVEL_SLIDER_ROW_HEIGHT,
48
+ LEVEL_STOP_CLASS,
49
+ LEVEL_STOP_REACHED_CLASS,
50
+ LEVEL_STOP_SIZE,
51
+ LEVEL_THUMB_CLASS,
52
+ LEVEL_THUMB_SIZE,
53
+ LEVEL_TRACK_CLASS,
54
+ LEVEL_TRACK_HEIGHT,
55
+ levelFromOffset,
56
+ levelStopPosition,
57
+ } from './constants';
58
+ import type { LevelPickerProps } from './types';
59
+
60
+ /**
61
+ * Built at MODULE scope, from `StyledView`, so one node carries the transform,
62
+ * the class and the style — `button/Button.tsx`'s rule, for its reasons: an
63
+ * element type constructed during render remounts its subtree every frame, and
64
+ * a second layout node would make LAYOUT classes inert on native.
65
+ */
66
+ const AnimatedStyledView = Animated.createAnimatedComponent(StyledView);
67
+
68
+ /**
69
+ * Drive a 0→1 `Animated.Value` from a boolean.
70
+ *
71
+ * `useNativeDriver` is off because one of the two callers animates `maxHeight`,
72
+ * which the native driver cannot own; the other animates opacity and could,
73
+ * but a single shape for both is worth more than the frame it costs on a
74
+ * cross-fade. The spring is Bloom's `gentle`, the same one `Accordion`'s reveal
75
+ * takes — the web fork's `cubic-bezier` is a CSS spelling with no RN equivalent,
76
+ * so the two curves are deliberately each their platform's own.
77
+ */
78
+ function useRevealProgress(on: boolean): Animated.Value {
79
+ const progress = useRef(new Animated.Value(on ? 1 : 0)).current;
80
+ useEffect(() => {
81
+ Animated.spring(progress, {
82
+ toValue: on ? 1 : 0,
83
+ useNativeDriver: false,
84
+ ...animation.spring.gentle,
85
+ }).start();
86
+ }, [on, progress]);
87
+ return progress;
88
+ }
89
+
90
+ /**
91
+ * What an `adjustable` exposes to VoiceOver and TalkBack. Declared once at
92
+ * module scope: a fresh array each render is a new prop value every time.
93
+ */
94
+ const ADJUST_ACTIONS = [{ name: 'increment' }, { name: 'decrement' }] as const;
95
+
96
+ export function LevelPicker({
97
+ levels,
98
+ value,
99
+ onValueChange,
100
+ accessibilityLabel,
101
+ minLabel,
102
+ maxLabel,
103
+ detailsLabel = 'Details',
104
+ expanded,
105
+ defaultExpanded = false,
106
+ onExpandedChange,
107
+ children,
108
+ className,
109
+ style,
110
+ testID,
111
+ }: LevelPickerProps) {
112
+ const theme = useTheme();
113
+ const [isExpanded, setExpanded] = useControllableState<boolean>({
114
+ value: expanded,
115
+ defaultValue: defaultExpanded,
116
+ onChange: onExpandedChange,
117
+ });
118
+ const [trackWidth, setTrackWidth] = useState(0);
119
+ const [detailsHeight, setDetailsHeight] = useState(0);
120
+ const [dragging, setDragging] = useState(false);
121
+
122
+ const reveal = useRevealProgress(isExpanded);
123
+ const active = useRevealProgress(dragging);
124
+
125
+ // The latest props, read by a `PanResponder` that is created once. Read
126
+ // inside its CALLBACKS and never during render, so no memoized position ever
127
+ // holds a stale copy.
128
+ const stateRef = useRef({ value, trackWidth, count: levels.length });
129
+ stateRef.current = { value, trackWidth, count: levels.length };
130
+ const onValueChangeRef = useRef(onValueChange);
131
+ onValueChangeRef.current = onValueChange;
132
+ // Where the gesture started, so a MOVE can be resolved as that point plus the
133
+ // gesture's own `dx`. `locationX` is only trustworthy on the grant.
134
+ const grantOffsetRef = useRef(0);
135
+
136
+ const commitOffset = useCallback((offsetX: number) => {
137
+ const { value: current, trackWidth: width, count } = stateRef.current;
138
+ const next = levelFromOffset(offsetX, width, count);
139
+ if (next !== current) onValueChangeRef.current(next);
140
+ }, []);
141
+
142
+ const step = useCallback((delta: number) => {
143
+ const { value: current, count } = stateRef.current;
144
+ const next = Math.max(0, Math.min(count - 1, current + delta));
145
+ if (next !== current) onValueChangeRef.current(next);
146
+ }, []);
147
+
148
+ const panResponder = useMemo(
149
+ () =>
150
+ PanResponder.create({
151
+ onStartShouldSetPanResponder: () => true,
152
+ onMoveShouldSetPanResponder: () => true,
153
+ onPanResponderGrant: (event: GestureResponderEvent) => {
154
+ setDragging(true);
155
+ grantOffsetRef.current = event.nativeEvent.locationX;
156
+ commitOffset(grantOffsetRef.current);
157
+ },
158
+ onPanResponderMove: (
159
+ _event: GestureResponderEvent,
160
+ gesture: PanResponderGestureState,
161
+ ) => {
162
+ commitOffset(grantOffsetRef.current + gesture.dx);
163
+ },
164
+ onPanResponderRelease: () => setDragging(false),
165
+ onPanResponderTerminate: () => setDragging(false),
166
+ }),
167
+ [commitOffset],
168
+ );
169
+
170
+ const onTrackLayout = useCallback((event: LayoutChangeEvent) => {
171
+ const next = event.nativeEvent.layout.width;
172
+ setTrackWidth((prev) => (Math.abs(prev - next) > 0.5 ? next : prev));
173
+ }, []);
174
+
175
+ const onDetailsLayout = useCallback((event: LayoutChangeEvent) => {
176
+ const next = event.nativeEvent.layout.height;
177
+ // Sub-pixel churn would re-render on every frame of the spring.
178
+ setDetailsHeight((prev) => (Math.abs(prev - next) > 0.5 ? next : prev));
179
+ }, []);
180
+
181
+ const onAccessibilityAction = useCallback(
182
+ (event: AccessibilityActionEvent) => {
183
+ if (event.nativeEvent.actionName === 'increment') step(1);
184
+ if (event.nativeEvent.actionName === 'decrement') step(-1);
185
+ },
186
+ [step],
187
+ );
188
+
189
+ /** A stop's centre, in pixels from the rail's left edge. */
190
+ const offsetOf = (index: number): number => {
191
+ const { percent, offset } = levelStopPosition(index, levels.length);
192
+ return (percent / 100) * trackWidth + offset;
193
+ };
194
+
195
+ const hasCaptions = minLabel !== undefined || maxLabel !== undefined;
196
+ const childTestId = (part: string): string | undefined =>
197
+ testID === undefined ? undefined : `${testID}-${part}`;
198
+
199
+ return (
200
+ <StyledView className={cx('w-full', className)} style={style} testID={testID}>
201
+ {/* The rail, and the row it sits in, collapsing to nothing when the
202
+ details take its place. */}
203
+ <AnimatedStyledView
204
+ aria-hidden={isExpanded}
205
+ pointerEvents={isExpanded ? 'none' : 'auto'}
206
+ style={{
207
+ overflow: 'hidden',
208
+ opacity: reveal.interpolate({ inputRange: [0, 1], outputRange: [1, 0] }),
209
+ maxHeight: reveal.interpolate({
210
+ inputRange: [0, 1],
211
+ outputRange: [LEVEL_SLIDER_ROW_HEIGHT, 0],
212
+ }),
213
+ }}
214
+ testID={childTestId('slider')}>
215
+ <StyledView
216
+ style={{
217
+ height: LEVEL_SLIDER_ROW_HEIGHT,
218
+ justifyContent: 'center',
219
+ paddingHorizontal: LEVEL_ROW_INSET,
220
+ }}>
221
+ <StyledView
222
+ accessibilityRole="adjustable"
223
+ accessibilityLabel={accessibilityLabel}
224
+ accessibilityActions={ADJUST_ACTIONS}
225
+ onAccessibilityAction={onAccessibilityAction}
226
+ // The FLAT `aria-value*` props: React Native folds them back into
227
+ // `accessibilityValue`, and they are the only spelling
228
+ // react-native-web reads. `slider/Slider.tsx` carries the same note.
229
+ aria-valuemin={0}
230
+ aria-valuemax={levels.length - 1}
231
+ aria-valuenow={value}
232
+ aria-valuetext={levels[value]}
233
+ onLayout={onTrackLayout}
234
+ className={LEVEL_TRACK_CLASS}
235
+ style={{ height: LEVEL_TRACK_HEIGHT }}
236
+ testID={childTestId('track')}
237
+ {...panResponder.panHandlers}>
238
+ {trackWidth > 0 ? (
239
+ <>
240
+ <StyledView
241
+ className={LEVEL_FILL_CLASS}
242
+ style={{ top: 0, bottom: 0, width: offsetOf(value) }}
243
+ />
244
+ {levels.map((label, index) => (
245
+ <StyledView
246
+ key={label}
247
+ className={index <= value ? LEVEL_STOP_REACHED_CLASS : LEVEL_STOP_CLASS}
248
+ style={{
249
+ width: LEVEL_STOP_SIZE,
250
+ height: LEVEL_STOP_SIZE,
251
+ top: (LEVEL_TRACK_HEIGHT - LEVEL_STOP_SIZE) / 2,
252
+ left: offsetOf(index) - LEVEL_STOP_SIZE / 2,
253
+ }}
254
+ />
255
+ ))}
256
+ <StyledView
257
+ className={LEVEL_THUMB_CLASS}
258
+ style={{
259
+ width: LEVEL_THUMB_SIZE,
260
+ height: LEVEL_THUMB_SIZE,
261
+ top: (LEVEL_TRACK_HEIGHT - LEVEL_THUMB_SIZE) / 2,
262
+ left: offsetOf(value) - LEVEL_THUMB_SIZE / 2,
263
+ }}
264
+ testID={childTestId('thumb')}
265
+ />
266
+ </>
267
+ ) : null}
268
+ </StyledView>
269
+ </StyledView>
270
+ </AnimatedStyledView>
271
+
272
+ {/* The summary row, with the end captions riding over it: one is showing
273
+ whenever the other is not. */}
274
+ <StyledView>
275
+ <AnimatedStyledView
276
+ // Only a drag takes the row out of the way — there is no hover here,
277
+ // and the row must stay pressable the moment the finger lifts.
278
+ pointerEvents={dragging ? 'none' : 'auto'}
279
+ style={{
280
+ opacity: active.interpolate({ inputRange: [0, 1], outputRange: [1, 0] }),
281
+ }}>
282
+ <MenuRowShell
283
+ role="menuitem"
284
+ expanded={isExpanded}
285
+ disabled={false}
286
+ title={detailsLabel}
287
+ trailing={
288
+ <MenuRowChevron>
289
+ <AnimatedStyledView
290
+ style={{
291
+ transform: [
292
+ {
293
+ rotate: reveal.interpolate({
294
+ inputRange: [0, 1],
295
+ outputRange: ['0deg', '90deg'],
296
+ }),
297
+ },
298
+ ],
299
+ }}>
300
+ <ChevronRightIcon
301
+ width={ROW_ICON_SIZE}
302
+ height={ROW_ICON_SIZE}
303
+ fill={theme.colors.textSecondary}
304
+ />
305
+ </AnimatedStyledView>
306
+ </MenuRowChevron>
307
+ }
308
+ onPress={() => setExpanded(!isExpanded)}
309
+ className={SUB_TRIGGER_CLASS}
310
+ testID={childTestId('summary')}
311
+ />
312
+ </AnimatedStyledView>
313
+ {hasCaptions ? (
314
+ <AnimatedStyledView
315
+ aria-hidden
316
+ pointerEvents="none"
317
+ style={{
318
+ position: 'absolute',
319
+ top: 0,
320
+ bottom: 0,
321
+ left: 0,
322
+ right: 0,
323
+ flexDirection: 'row',
324
+ alignItems: 'center',
325
+ justifyContent: 'space-between',
326
+ paddingHorizontal: LEVEL_ROW_INSET,
327
+ opacity: active,
328
+ }}
329
+ testID={childTestId('captions')}>
330
+ <StyledText className={LEVEL_CAPTION_CLASS}>{minLabel}</StyledText>
331
+ <StyledText className={LEVEL_CAPTION_CLASS}>{maxLabel}</StyledText>
332
+ </AnimatedStyledView>
333
+ ) : null}
334
+ </StyledView>
335
+
336
+ {/* The details. Measured rather than assumed: an inline sub-menu opening
337
+ inside it changes its height, and only `onLayout` sees that. */}
338
+ <AnimatedStyledView
339
+ aria-hidden={!isExpanded}
340
+ pointerEvents={isExpanded ? 'auto' : 'none'}
341
+ style={{
342
+ overflow: 'hidden',
343
+ opacity: reveal,
344
+ // Before the first measurement an OPEN region must not be clipped to
345
+ // zero — `Accordion`'s rule, for its reason: the unmeasured frame
346
+ // takes the answer its state already implies.
347
+ maxHeight:
348
+ detailsHeight === 0 && isExpanded
349
+ ? undefined
350
+ : reveal.interpolate({ inputRange: [0, 1], outputRange: [0, detailsHeight] }),
351
+ }}
352
+ testID={childTestId('details')}>
353
+ <StyledView onLayout={onDetailsLayout}>
354
+ <StyledView
355
+ className={LEVEL_SEPARATOR_CLASS}
356
+ style={{ marginHorizontal: LEVEL_ROW_INSET, marginBottom: 4 }}
357
+ />
358
+ {children}
359
+ </StyledView>
360
+ </AnimatedStyledView>
361
+ </StyledView>
362
+ );
363
+ }
364
+
365
+ LevelPicker.displayName = 'LevelPicker';