@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.
- package/docs/level-picker.mdx +105 -0
- package/docs/media-flight.mdx +272 -0
- package/docs/migrating-the-media-gallery.mdx +103 -0
- package/docs/migrating-to-1.0.mdx +1 -1
- package/docs/slider.mdx +3 -1
- package/docs/zoomable-media-gallery.mdx +116 -0
- package/lib/commonjs/floating/index.js.map +1 -1
- package/lib/commonjs/image-aspect-ratio-cache/aspect-ratio-cache.js +1 -1
- package/lib/commonjs/index.js +8 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +33 -25
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/level-picker/LevelPicker.js +349 -0
- package/lib/commonjs/level-picker/LevelPicker.js.map +1 -0
- package/lib/commonjs/level-picker/LevelPicker.web.js +346 -0
- package/lib/commonjs/level-picker/LevelPicker.web.js.map +1 -0
- package/lib/commonjs/level-picker/constants.js +137 -0
- package/lib/commonjs/level-picker/constants.js.map +1 -0
- package/lib/commonjs/level-picker/index.js +13 -0
- package/lib/commonjs/level-picker/index.js.map +1 -0
- package/lib/commonjs/level-picker/index.web.js +13 -0
- package/lib/commonjs/level-picker/index.web.js.map +1 -0
- package/lib/commonjs/level-picker/types.js +6 -0
- package/lib/commonjs/level-picker/types.js.map +1 -0
- package/lib/commonjs/media-flight/MediaFlightLayer.js +151 -0
- package/lib/commonjs/media-flight/MediaFlightLayer.js.map +1 -0
- package/lib/commonjs/media-flight/MediaSurface.js +188 -0
- package/lib/commonjs/media-flight/MediaSurface.js.map +1 -0
- package/lib/commonjs/media-flight/constants.js +39 -0
- package/lib/commonjs/media-flight/constants.js.map +1 -0
- package/lib/commonjs/media-flight/expo-video-module.js +148 -0
- package/lib/commonjs/media-flight/expo-video-module.js.map +1 -0
- package/lib/commonjs/media-flight/index.js +102 -0
- package/lib/commonjs/media-flight/index.js.map +1 -0
- package/lib/commonjs/media-flight/store.js +457 -0
- package/lib/commonjs/media-flight/store.js.map +1 -0
- package/lib/commonjs/media-flight/types.js +6 -0
- package/lib/commonjs/media-flight/types.js.map +1 -0
- package/lib/commonjs/media-flight/use-media-flight.js +41 -0
- package/lib/commonjs/media-flight/use-media-flight.js.map +1 -0
- package/lib/commonjs/tab-bar/TabBarBase.js +1 -1
- package/lib/commonjs/{zoomable-image-gallery/ZoomableImageGallery.js → zoomable-media-gallery/ZoomableMediaGallery.js} +185 -131
- package/lib/commonjs/zoomable-media-gallery/ZoomableMediaGallery.js.map +1 -0
- package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/constants.js.map +1 -1
- package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/index.js +5 -5
- package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/index.js.map +1 -1
- package/lib/commonjs/zoomable-media-gallery/types.js +6 -0
- package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/types.js.map +1 -1
- package/lib/module/floating/index.js +5 -0
- package/lib/module/floating/index.js.map +1 -1
- package/lib/module/image-aspect-ratio-cache/aspect-ratio-cache.js +1 -1
- package/lib/module/index.js +3 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +3 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/level-picker/LevelPicker.js +343 -0
- package/lib/module/level-picker/LevelPicker.js.map +1 -0
- package/lib/module/level-picker/LevelPicker.web.js +340 -0
- package/lib/module/level-picker/LevelPicker.web.js.map +1 -0
- package/lib/module/level-picker/constants.js +131 -0
- package/lib/module/level-picker/constants.js.map +1 -0
- package/lib/module/level-picker/index.js +4 -0
- package/lib/module/level-picker/index.js.map +1 -0
- package/lib/module/level-picker/index.web.js +4 -0
- package/lib/module/level-picker/index.web.js.map +1 -0
- package/lib/module/level-picker/types.js +4 -0
- package/lib/module/level-picker/types.js.map +1 -0
- package/lib/module/media-flight/MediaFlightLayer.js +146 -0
- package/lib/module/media-flight/MediaFlightLayer.js.map +1 -0
- package/lib/module/media-flight/MediaSurface.js +180 -0
- package/lib/module/media-flight/MediaSurface.js.map +1 -0
- package/lib/module/media-flight/constants.js +35 -0
- package/lib/module/media-flight/constants.js.map +1 -0
- package/lib/module/media-flight/expo-video-module.js +142 -0
- package/lib/module/media-flight/expo-video-module.js.map +1 -0
- package/lib/module/media-flight/index.js +9 -0
- package/lib/module/media-flight/index.js.map +1 -0
- package/lib/module/media-flight/store.js +439 -0
- package/lib/module/media-flight/store.js.map +1 -0
- package/lib/module/media-flight/types.js +4 -0
- package/lib/module/media-flight/types.js.map +1 -0
- package/lib/module/media-flight/use-media-flight.js +36 -0
- package/lib/module/media-flight/use-media-flight.js.map +1 -0
- package/lib/module/tab-bar/TabBarBase.js +1 -1
- package/lib/module/{zoomable-image-gallery/ZoomableImageGallery.js → zoomable-media-gallery/ZoomableMediaGallery.js} +184 -130
- package/lib/module/zoomable-media-gallery/ZoomableMediaGallery.js.map +1 -0
- package/lib/module/{zoomable-image-gallery → zoomable-media-gallery}/constants.js.map +1 -1
- package/lib/module/zoomable-media-gallery/index.js +5 -0
- package/lib/module/zoomable-media-gallery/index.js.map +1 -0
- package/lib/module/zoomable-media-gallery/types.js +4 -0
- package/lib/module/{zoomable-image-gallery → zoomable-media-gallery}/types.js.map +1 -1
- package/lib/typescript/commonjs/floating/index.d.ts +5 -0
- package/lib/typescript/commonjs/floating/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +2 -0
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/level-picker/LevelPicker.d.ts +6 -0
- package/lib/typescript/commonjs/level-picker/LevelPicker.d.ts.map +1 -0
- package/lib/typescript/commonjs/level-picker/LevelPicker.web.d.ts +6 -0
- package/lib/typescript/commonjs/level-picker/LevelPicker.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/level-picker/constants.d.ts +93 -0
- package/lib/typescript/commonjs/level-picker/constants.d.ts.map +1 -0
- package/lib/typescript/commonjs/level-picker/index.d.ts +3 -0
- package/lib/typescript/commonjs/level-picker/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/level-picker/index.web.d.ts +3 -0
- package/lib/typescript/commonjs/level-picker/index.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/level-picker/types.d.ts +63 -0
- package/lib/typescript/commonjs/level-picker/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/MediaFlightLayer.d.ts +11 -0
- package/lib/typescript/commonjs/media-flight/MediaFlightLayer.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/MediaSurface.d.ts +106 -0
- package/lib/typescript/commonjs/media-flight/MediaSurface.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/constants.d.ts +18 -0
- package/lib/typescript/commonjs/media-flight/constants.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/expo-video-module.d.ts +103 -0
- package/lib/typescript/commonjs/media-flight/expo-video-module.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/index.d.ts +9 -0
- package/lib/typescript/commonjs/media-flight/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/store.d.ts +132 -0
- package/lib/typescript/commonjs/media-flight/store.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/types.d.ts +195 -0
- package/lib/typescript/commonjs/media-flight/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/use-media-flight.d.ts +4 -0
- package/lib/typescript/commonjs/media-flight/use-media-flight.d.ts.map +1 -0
- package/lib/typescript/commonjs/zoomable-media-gallery/ZoomableMediaGallery.d.ts +5 -0
- package/lib/typescript/commonjs/zoomable-media-gallery/ZoomableMediaGallery.d.ts.map +1 -0
- package/lib/typescript/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/constants.d.ts.map +1 -1
- package/lib/typescript/commonjs/zoomable-media-gallery/index.d.ts +4 -0
- package/lib/typescript/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/zoomable-media-gallery/types.d.ts +90 -0
- package/lib/typescript/commonjs/zoomable-media-gallery/types.d.ts.map +1 -0
- package/lib/typescript/module/floating/index.d.ts +5 -0
- package/lib/typescript/module/floating/index.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +2 -0
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/level-picker/LevelPicker.d.ts +6 -0
- package/lib/typescript/module/level-picker/LevelPicker.d.ts.map +1 -0
- package/lib/typescript/module/level-picker/LevelPicker.web.d.ts +6 -0
- package/lib/typescript/module/level-picker/LevelPicker.web.d.ts.map +1 -0
- package/lib/typescript/module/level-picker/constants.d.ts +93 -0
- package/lib/typescript/module/level-picker/constants.d.ts.map +1 -0
- package/lib/typescript/module/level-picker/index.d.ts +3 -0
- package/lib/typescript/module/level-picker/index.d.ts.map +1 -0
- package/lib/typescript/module/level-picker/index.web.d.ts +3 -0
- package/lib/typescript/module/level-picker/index.web.d.ts.map +1 -0
- package/lib/typescript/module/level-picker/types.d.ts +63 -0
- package/lib/typescript/module/level-picker/types.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/MediaFlightLayer.d.ts +11 -0
- package/lib/typescript/module/media-flight/MediaFlightLayer.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/MediaSurface.d.ts +106 -0
- package/lib/typescript/module/media-flight/MediaSurface.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/constants.d.ts +18 -0
- package/lib/typescript/module/media-flight/constants.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/expo-video-module.d.ts +103 -0
- package/lib/typescript/module/media-flight/expo-video-module.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/index.d.ts +9 -0
- package/lib/typescript/module/media-flight/index.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/store.d.ts +132 -0
- package/lib/typescript/module/media-flight/store.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/types.d.ts +195 -0
- package/lib/typescript/module/media-flight/types.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/use-media-flight.d.ts +4 -0
- package/lib/typescript/module/media-flight/use-media-flight.d.ts.map +1 -0
- package/lib/typescript/module/zoomable-media-gallery/ZoomableMediaGallery.d.ts +5 -0
- package/lib/typescript/module/zoomable-media-gallery/ZoomableMediaGallery.d.ts.map +1 -0
- package/lib/typescript/module/{zoomable-image-gallery → zoomable-media-gallery}/constants.d.ts.map +1 -1
- package/lib/typescript/module/zoomable-media-gallery/index.d.ts +4 -0
- package/lib/typescript/module/{zoomable-image-gallery → zoomable-media-gallery}/index.d.ts.map +1 -1
- package/lib/typescript/module/zoomable-media-gallery/types.d.ts +90 -0
- package/lib/typescript/module/zoomable-media-gallery/types.d.ts.map +1 -0
- package/package.json +61 -8
- package/src/floating/index.ts +5 -0
- package/src/image-aspect-ratio-cache/aspect-ratio-cache.ts +1 -1
- package/src/index.ts +4 -1
- package/src/index.web.ts +4 -1
- package/src/level-picker/LevelPicker.tsx +365 -0
- package/src/level-picker/LevelPicker.web.tsx +374 -0
- package/src/level-picker/constants.ts +131 -0
- package/src/level-picker/index.ts +2 -0
- package/src/level-picker/index.web.ts +2 -0
- package/src/level-picker/types.ts +63 -0
- package/src/media-flight/MediaFlightLayer.tsx +144 -0
- package/src/media-flight/MediaSurface.tsx +233 -0
- package/src/media-flight/constants.ts +32 -0
- package/src/media-flight/expo-video-module.ts +181 -0
- package/src/media-flight/index.ts +33 -0
- package/src/media-flight/store.ts +497 -0
- package/src/media-flight/types.ts +210 -0
- package/src/media-flight/use-media-flight.ts +42 -0
- package/src/tab-bar/TabBarBase.tsx +1 -1
- package/src/{zoomable-image-gallery/ZoomableImageGallery.tsx → zoomable-media-gallery/ZoomableMediaGallery.tsx} +192 -117
- package/src/zoomable-media-gallery/index.ts +11 -0
- package/src/zoomable-media-gallery/types.ts +100 -0
- package/docs/zoomable-image-gallery.mdx +0 -66
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js.map +0 -1
- package/lib/commonjs/zoomable-image-gallery/types.js +0 -2
- package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js.map +0 -1
- package/lib/module/zoomable-image-gallery/index.js +0 -5
- package/lib/module/zoomable-image-gallery/index.js.map +0 -1
- package/lib/module/zoomable-image-gallery/types.js +0 -2
- package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts +0 -5
- package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +0 -1
- package/lib/typescript/commonjs/zoomable-image-gallery/index.d.ts +0 -4
- package/lib/typescript/commonjs/zoomable-image-gallery/types.d.ts +0 -56
- package/lib/typescript/commonjs/zoomable-image-gallery/types.d.ts.map +0 -1
- package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts +0 -5
- package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +0 -1
- package/lib/typescript/module/zoomable-image-gallery/index.d.ts +0 -4
- package/lib/typescript/module/zoomable-image-gallery/types.d.ts +0 -56
- package/lib/typescript/module/zoomable-image-gallery/types.d.ts.map +0 -1
- package/src/zoomable-image-gallery/index.ts +0 -9
- package/src/zoomable-image-gallery/types.ts +0 -59
- /package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/constants.js +0 -0
- /package/lib/module/{zoomable-image-gallery → zoomable-media-gallery}/constants.js +0 -0
- /package/lib/typescript/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/constants.d.ts +0 -0
- /package/lib/typescript/module/{zoomable-image-gallery → zoomable-media-gallery}/constants.d.ts +0 -0
- /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-
|
|
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';
|