@cdx-ui/components 0.3.5 → 0.3.6
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/README.md +1 -1
- package/coverage.svg +1 -1
- package/docs/components/bar-chart/content.md +703 -0
- package/docs/components/content.md +9 -1
- package/docs/components/line-chart/content.md +567 -0
- package/docs/components/pie-chart/content.md +493 -0
- package/docs/components/progress-segmented/content.md +1 -1
- package/docs/components/stepper/content.md +599 -0
- package/docs/components/system-alert/content.md +533 -0
- package/docs/foundations/styles/tokens/all-tokens/content.md +2 -1
- package/docs/llms.txt +9 -0
- package/docs/patterns/content.md +1 -0
- package/docs/patterns/dashboard/content.md +423 -19
- package/docs/patterns/insights/content.md +590 -0
- package/docs/resources/release-notes/0.3.5/content.md +7 -0
- package/docs/resources/release-notes/content.md +1 -0
- package/lib/commonjs/components/BarChart/geometry.js +1130 -0
- package/lib/commonjs/components/BarChart/geometry.js.map +1 -0
- package/lib/commonjs/components/BarChart/index.js +942 -0
- package/lib/commonjs/components/BarChart/index.js.map +1 -0
- package/lib/commonjs/components/BarChart/styles.js +26 -0
- package/lib/commonjs/components/BarChart/styles.js.map +1 -0
- package/lib/commonjs/components/Button/index.js +2 -2
- package/lib/commonjs/components/Button/index.js.map +1 -1
- package/lib/commonjs/components/LineChart/geometry.js +698 -0
- package/lib/commonjs/components/LineChart/geometry.js.map +1 -0
- package/lib/commonjs/components/LineChart/index.js +747 -0
- package/lib/commonjs/components/LineChart/index.js.map +1 -0
- package/lib/commonjs/components/LineChart/styles.js +25 -0
- package/lib/commonjs/components/LineChart/styles.js.map +1 -0
- package/lib/commonjs/components/PieChart/geometry.js +216 -0
- package/lib/commonjs/components/PieChart/geometry.js.map +1 -0
- package/lib/commonjs/components/PieChart/index.js +476 -0
- package/lib/commonjs/components/PieChart/index.js.map +1 -0
- package/lib/commonjs/components/PieChart/styles.js +88 -0
- package/lib/commonjs/components/PieChart/styles.js.map +1 -0
- package/lib/commonjs/components/ProgressCircle/ProgressCircleRing.js +9 -31
- package/lib/commonjs/components/ProgressCircle/ProgressCircleRing.js.map +1 -1
- package/lib/commonjs/components/ProgressCircle/ProgressRing.js +52 -0
- package/lib/commonjs/components/ProgressCircle/ProgressRing.js.map +1 -0
- package/lib/commonjs/components/ScrollShadow/useScrollShadow.js +8 -7
- package/lib/commonjs/components/ScrollShadow/useScrollShadow.js.map +1 -1
- package/lib/commonjs/components/Stepper/index.js +369 -0
- package/lib/commonjs/components/Stepper/index.js.map +1 -0
- package/lib/commonjs/components/Stepper/styles.js +211 -0
- package/lib/commonjs/components/Stepper/styles.js.map +1 -0
- package/lib/commonjs/components/Stepper/useStepperLabelFit.js +22 -0
- package/lib/commonjs/components/Stepper/useStepperLabelFit.js.map +1 -0
- package/lib/commonjs/components/Stepper/useStepperLabelFit.web.js +52 -0
- package/lib/commonjs/components/Stepper/useStepperLabelFit.web.js.map +1 -0
- package/lib/commonjs/components/Stepper/useStepperRingColors.js +31 -0
- package/lib/commonjs/components/Stepper/useStepperRingColors.js.map +1 -0
- package/lib/commonjs/components/SystemAlert/animation.js +218 -0
- package/lib/commonjs/components/SystemAlert/animation.js.map +1 -0
- package/lib/commonjs/components/SystemAlert/index.js +525 -0
- package/lib/commonjs/components/SystemAlert/index.js.map +1 -0
- package/lib/commonjs/components/SystemAlert/styles.js +178 -0
- package/lib/commonjs/components/SystemAlert/styles.js.map +1 -0
- package/lib/commonjs/components/SystemAlert/useSystemAlertStatusBarStyle.js +53 -0
- package/lib/commonjs/components/SystemAlert/useSystemAlertStatusBarStyle.js.map +1 -0
- package/lib/commonjs/components/Tabs/index.js +4 -0
- package/lib/commonjs/components/Tabs/index.js.map +1 -1
- package/lib/commonjs/components/Tabs/styles.js +1 -1
- package/lib/commonjs/components/Tabs/styles.js.map +1 -1
- package/lib/commonjs/components/chart/ChartInspector.js +610 -0
- package/lib/commonjs/components/chart/ChartInspector.js.map +1 -0
- package/lib/commonjs/components/chart/format.js +93 -0
- package/lib/commonjs/components/chart/format.js.map +1 -0
- package/lib/commonjs/components/chart/index.js +2 -0
- package/lib/commonjs/components/chart/index.js.map +1 -0
- package/lib/commonjs/components/chart/motion.js +97 -0
- package/lib/commonjs/components/chart/motion.js.map +1 -0
- package/lib/commonjs/components/chart/palette.js +178 -0
- package/lib/commonjs/components/chart/palette.js.map +1 -0
- package/lib/commonjs/components/chart/size.js +34 -0
- package/lib/commonjs/components/chart/size.js.map +1 -0
- package/lib/commonjs/components/chart/ticks.js +59 -0
- package/lib/commonjs/components/chart/ticks.js.map +1 -0
- package/lib/commonjs/components/chart/types.js +6 -0
- package/lib/commonjs/components/chart/types.js.map +1 -0
- package/lib/commonjs/components/chart/useChartHostWidth.js +27 -0
- package/lib/commonjs/components/chart/useChartHostWidth.js.map +1 -0
- package/lib/commonjs/components/index.js +60 -0
- package/lib/commonjs/components/index.js.map +1 -1
- package/lib/commonjs/figma/SystemAlert.Button.figma.js +28 -0
- package/lib/commonjs/figma/SystemAlert.Button.figma.js.map +1 -0
- package/lib/commonjs/figma/SystemAlert.Close.figma.js +22 -0
- package/lib/commonjs/figma/SystemAlert.Close.figma.js.map +1 -0
- package/lib/commonjs/figma/SystemAlert.Description.figma.js +20 -0
- package/lib/commonjs/figma/SystemAlert.Description.figma.js.map +1 -0
- package/lib/commonjs/figma/SystemAlert.IconButton.figma.js +31 -0
- package/lib/commonjs/figma/SystemAlert.IconButton.figma.js.map +1 -0
- package/lib/commonjs/figma/SystemAlert.figma.js +48 -0
- package/lib/commonjs/figma/SystemAlert.figma.js.map +1 -0
- package/lib/module/components/BarChart/geometry.js +1110 -0
- package/lib/module/components/BarChart/geometry.js.map +1 -0
- package/lib/module/components/BarChart/index.js +936 -0
- package/lib/module/components/BarChart/index.js.map +1 -0
- package/lib/module/components/BarChart/styles.js +22 -0
- package/lib/module/components/BarChart/styles.js.map +1 -0
- package/lib/module/components/Button/index.js +2 -2
- package/lib/module/components/Button/index.js.map +1 -1
- package/lib/module/components/LineChart/geometry.js +685 -0
- package/lib/module/components/LineChart/geometry.js.map +1 -0
- package/lib/module/components/LineChart/index.js +741 -0
- package/lib/module/components/LineChart/index.js.map +1 -0
- package/lib/module/components/LineChart/styles.js +21 -0
- package/lib/module/components/LineChart/styles.js.map +1 -0
- package/lib/module/components/PieChart/geometry.js +209 -0
- package/lib/module/components/PieChart/geometry.js.map +1 -0
- package/lib/module/components/PieChart/index.js +471 -0
- package/lib/module/components/PieChart/index.js.map +1 -0
- package/lib/module/components/PieChart/styles.js +84 -0
- package/lib/module/components/PieChart/styles.js.map +1 -0
- package/lib/module/components/ProgressCircle/ProgressCircleRing.js +10 -32
- package/lib/module/components/ProgressCircle/ProgressCircleRing.js.map +1 -1
- package/lib/module/components/ProgressCircle/ProgressRing.js +48 -0
- package/lib/module/components/ProgressCircle/ProgressRing.js.map +1 -0
- package/lib/module/components/ScrollShadow/useScrollShadow.js +8 -7
- package/lib/module/components/ScrollShadow/useScrollShadow.js.map +1 -1
- package/lib/module/components/Stepper/index.js +360 -0
- package/lib/module/components/Stepper/index.js.map +1 -0
- package/lib/module/components/Stepper/styles.js +206 -0
- package/lib/module/components/Stepper/styles.js.map +1 -0
- package/lib/module/components/Stepper/useStepperLabelFit.js +18 -0
- package/lib/module/components/Stepper/useStepperLabelFit.js.map +1 -0
- package/lib/module/components/Stepper/useStepperLabelFit.web.js +48 -0
- package/lib/module/components/Stepper/useStepperLabelFit.web.js.map +1 -0
- package/lib/module/components/Stepper/useStepperRingColors.js +28 -0
- package/lib/module/components/Stepper/useStepperRingColors.js.map +1 -0
- package/lib/module/components/SystemAlert/animation.js +213 -0
- package/lib/module/components/SystemAlert/animation.js.map +1 -0
- package/lib/module/components/SystemAlert/index.js +515 -0
- package/lib/module/components/SystemAlert/index.js.map +1 -0
- package/lib/module/components/SystemAlert/styles.js +174 -0
- package/lib/module/components/SystemAlert/styles.js.map +1 -0
- package/lib/module/components/SystemAlert/useSystemAlertStatusBarStyle.js +49 -0
- package/lib/module/components/SystemAlert/useSystemAlertStatusBarStyle.js.map +1 -0
- package/lib/module/components/Tabs/index.js +4 -0
- package/lib/module/components/Tabs/index.js.map +1 -1
- package/lib/module/components/Tabs/styles.js +1 -1
- package/lib/module/components/Tabs/styles.js.map +1 -1
- package/lib/module/components/chart/ChartInspector.js +600 -0
- package/lib/module/components/chart/ChartInspector.js.map +1 -0
- package/lib/module/components/chart/format.js +84 -0
- package/lib/module/components/chart/format.js.map +1 -0
- package/lib/module/components/chart/index.js +2 -0
- package/lib/module/components/chart/index.js.map +1 -0
- package/lib/module/components/chart/motion.js +89 -0
- package/lib/module/components/chart/motion.js.map +1 -0
- package/lib/module/components/chart/palette.js +168 -0
- package/lib/module/components/chart/palette.js.map +1 -0
- package/lib/module/components/chart/size.js +29 -0
- package/lib/module/components/chart/size.js.map +1 -0
- package/lib/module/components/chart/ticks.js +55 -0
- package/lib/module/components/chart/ticks.js.map +1 -0
- package/lib/module/components/chart/types.js +4 -0
- package/lib/module/components/chart/types.js.map +1 -0
- package/lib/module/components/chart/useChartHostWidth.js +23 -0
- package/lib/module/components/chart/useChartHostWidth.js.map +1 -0
- package/lib/module/components/index.js +5 -0
- package/lib/module/components/index.js.map +1 -1
- package/lib/module/figma/SystemAlert.Button.figma.js +22 -0
- package/lib/module/figma/SystemAlert.Button.figma.js.map +1 -0
- package/lib/module/figma/SystemAlert.Close.figma.js +18 -0
- package/lib/module/figma/SystemAlert.Close.figma.js.map +1 -0
- package/lib/module/figma/SystemAlert.Description.figma.js +14 -0
- package/lib/module/figma/SystemAlert.Description.figma.js.map +1 -0
- package/lib/module/figma/SystemAlert.IconButton.figma.js +25 -0
- package/lib/module/figma/SystemAlert.IconButton.figma.js.map +1 -0
- package/lib/module/figma/SystemAlert.figma.js +42 -0
- package/lib/module/figma/SystemAlert.figma.js.map +1 -0
- package/lib/typescript/components/BarChart/geometry.d.ts +236 -0
- package/lib/typescript/components/BarChart/geometry.d.ts.map +1 -0
- package/lib/typescript/components/BarChart/index.d.ts +193 -0
- package/lib/typescript/components/BarChart/index.d.ts.map +1 -0
- package/lib/typescript/components/BarChart/styles.d.ts +32 -0
- package/lib/typescript/components/BarChart/styles.d.ts.map +1 -0
- package/lib/typescript/components/LineChart/geometry.d.ts +248 -0
- package/lib/typescript/components/LineChart/geometry.d.ts.map +1 -0
- package/lib/typescript/components/LineChart/index.d.ts +105 -0
- package/lib/typescript/components/LineChart/index.d.ts.map +1 -0
- package/lib/typescript/components/LineChart/styles.d.ts +12 -0
- package/lib/typescript/components/LineChart/styles.d.ts.map +1 -0
- package/lib/typescript/components/PieChart/geometry.d.ts +43 -0
- package/lib/typescript/components/PieChart/geometry.d.ts.map +1 -0
- package/lib/typescript/components/PieChart/index.d.ts +147 -0
- package/lib/typescript/components/PieChart/index.d.ts.map +1 -0
- package/lib/typescript/components/PieChart/styles.d.ts +27 -0
- package/lib/typescript/components/PieChart/styles.d.ts.map +1 -0
- package/lib/typescript/components/ProgressCircle/ProgressCircleRing.d.ts.map +1 -1
- package/lib/typescript/components/ProgressCircle/ProgressRing.d.ts +17 -0
- package/lib/typescript/components/ProgressCircle/ProgressRing.d.ts.map +1 -0
- package/lib/typescript/components/ScrollShadow/useScrollShadow.d.ts.map +1 -1
- package/lib/typescript/components/Stepper/index.d.ts +68 -0
- package/lib/typescript/components/Stepper/index.d.ts.map +1 -0
- package/lib/typescript/components/Stepper/styles.d.ts +46 -0
- package/lib/typescript/components/Stepper/styles.d.ts.map +1 -0
- package/lib/typescript/components/Stepper/useStepperLabelFit.d.ts +15 -0
- package/lib/typescript/components/Stepper/useStepperLabelFit.d.ts.map +1 -0
- package/lib/typescript/components/Stepper/useStepperLabelFit.web.d.ts +10 -0
- package/lib/typescript/components/Stepper/useStepperLabelFit.web.d.ts.map +1 -0
- package/lib/typescript/components/Stepper/useStepperRingColors.d.ts +6 -0
- package/lib/typescript/components/Stepper/useStepperRingColors.d.ts.map +1 -0
- package/lib/typescript/components/SystemAlert/animation.d.ts +56 -0
- package/lib/typescript/components/SystemAlert/animation.d.ts.map +1 -0
- package/lib/typescript/components/SystemAlert/index.d.ts +110 -0
- package/lib/typescript/components/SystemAlert/index.d.ts.map +1 -0
- package/lib/typescript/components/SystemAlert/styles.d.ts +38 -0
- package/lib/typescript/components/SystemAlert/styles.d.ts.map +1 -0
- package/lib/typescript/components/SystemAlert/useSystemAlertStatusBarStyle.d.ts +14 -0
- package/lib/typescript/components/SystemAlert/useSystemAlertStatusBarStyle.d.ts.map +1 -0
- package/lib/typescript/components/Tabs/index.d.ts.map +1 -1
- package/lib/typescript/components/chart/ChartInspector.d.ts +101 -0
- package/lib/typescript/components/chart/ChartInspector.d.ts.map +1 -0
- package/lib/typescript/components/chart/format.d.ts +16 -0
- package/lib/typescript/components/chart/format.d.ts.map +1 -0
- package/lib/typescript/components/chart/index.d.ts +3 -0
- package/lib/typescript/components/chart/index.d.ts.map +1 -0
- package/lib/typescript/components/chart/motion.d.ts +27 -0
- package/lib/typescript/components/chart/motion.d.ts.map +1 -0
- package/lib/typescript/components/chart/palette.d.ts +44 -0
- package/lib/typescript/components/chart/palette.d.ts.map +1 -0
- package/lib/typescript/components/chart/size.d.ts +25 -0
- package/lib/typescript/components/chart/size.d.ts.map +1 -0
- package/lib/typescript/components/chart/ticks.d.ts +3 -0
- package/lib/typescript/components/chart/ticks.d.ts.map +1 -0
- package/lib/typescript/components/chart/types.d.ts +163 -0
- package/lib/typescript/components/chart/types.d.ts.map +1 -0
- package/lib/typescript/components/chart/useChartHostWidth.d.ts +9 -0
- package/lib/typescript/components/chart/useChartHostWidth.d.ts.map +1 -0
- package/lib/typescript/components/index.d.ts +5 -0
- package/lib/typescript/components/index.d.ts.map +1 -1
- package/lib/typescript/figma/SystemAlert.Button.figma.d.ts +11 -0
- package/lib/typescript/figma/SystemAlert.Button.figma.d.ts.map +1 -0
- package/lib/typescript/figma/SystemAlert.Close.figma.d.ts +11 -0
- package/lib/typescript/figma/SystemAlert.Close.figma.d.ts.map +1 -0
- package/lib/typescript/figma/SystemAlert.Description.figma.d.ts +8 -0
- package/lib/typescript/figma/SystemAlert.Description.figma.d.ts.map +1 -0
- package/lib/typescript/figma/SystemAlert.IconButton.figma.d.ts +11 -0
- package/lib/typescript/figma/SystemAlert.IconButton.figma.d.ts.map +1 -0
- package/lib/typescript/figma/SystemAlert.figma.d.ts +8 -0
- package/lib/typescript/figma/SystemAlert.figma.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/components/BarChart/geometry.ts +1469 -0
- package/src/components/BarChart/index.tsx +1424 -0
- package/src/components/BarChart/styles.ts +31 -0
- package/src/components/Button/index.tsx +1 -1
- package/src/components/LineChart/geometry.ts +957 -0
- package/src/components/LineChart/index.tsx +1109 -0
- package/src/components/LineChart/styles.ts +26 -0
- package/src/components/PieChart/geometry.ts +295 -0
- package/src/components/PieChart/index.tsx +715 -0
- package/src/components/PieChart/styles.ts +101 -0
- package/src/components/ProgressCircle/ProgressCircleRing.tsx +10 -39
- package/src/components/ProgressCircle/ProgressRing.tsx +60 -0
- package/src/components/ScrollShadow/useScrollShadow.ts +12 -11
- package/src/components/Stepper/index.tsx +452 -0
- package/src/components/Stepper/styles.ts +234 -0
- package/src/components/Stepper/useStepperLabelFit.ts +21 -0
- package/src/components/Stepper/useStepperLabelFit.web.ts +53 -0
- package/src/components/Stepper/useStepperRingColors.ts +29 -0
- package/src/components/SystemAlert/animation.ts +252 -0
- package/src/components/SystemAlert/index.tsx +684 -0
- package/src/components/SystemAlert/styles.ts +212 -0
- package/src/components/SystemAlert/useSystemAlertStatusBarStyle.ts +57 -0
- package/src/components/Tabs/index.tsx +4 -0
- package/src/components/Tabs/styles.ts +1 -1
- package/src/components/chart/ChartInspector.tsx +726 -0
- package/src/components/chart/format.ts +95 -0
- package/src/components/chart/index.ts +24 -0
- package/src/components/chart/motion.ts +98 -0
- package/src/components/chart/palette.tsx +247 -0
- package/src/components/chart/size.ts +21 -0
- package/src/components/chart/ticks.ts +53 -0
- package/src/components/chart/types.ts +183 -0
- package/src/components/chart/useChartHostWidth.ts +26 -0
- package/src/components/index.ts +5 -0
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { PanResponder, Platform, Pressable, Text, View } from 'react-native';
|
|
3
|
+
import { dataAttributes } from '@cdx-ui/primitives';
|
|
4
|
+
import type { ChartTooltipContent, ChartTooltipItem } from './types';
|
|
5
|
+
|
|
6
|
+
export interface ChartInspectorTarget {
|
|
7
|
+
readonly key: string;
|
|
8
|
+
/** Hit box in overlay coordinates. */
|
|
9
|
+
readonly left: number;
|
|
10
|
+
readonly top: number;
|
|
11
|
+
readonly width: number;
|
|
12
|
+
readonly height: number;
|
|
13
|
+
/** Point the tooltip sits beside. */
|
|
14
|
+
readonly anchorX: number;
|
|
15
|
+
readonly anchorY: number;
|
|
16
|
+
/** Half-width of the mark at the anchor, so the tooltip clears it. */
|
|
17
|
+
readonly anchorRadius?: number;
|
|
18
|
+
readonly tooltip: ChartTooltipContent;
|
|
19
|
+
readonly accessibilityLabel: string;
|
|
20
|
+
readonly selected: boolean;
|
|
21
|
+
/** Keyboard focus ring. Defaults to the hit box. */
|
|
22
|
+
readonly outline?: {
|
|
23
|
+
readonly left: number;
|
|
24
|
+
readonly top: number;
|
|
25
|
+
readonly width: number;
|
|
26
|
+
readonly height: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface InspectorKeyEvent {
|
|
31
|
+
readonly nativeEvent: { readonly key: string; readonly shiftKey?: boolean };
|
|
32
|
+
readonly key?: string;
|
|
33
|
+
readonly shiftKey?: boolean;
|
|
34
|
+
readonly preventDefault?: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface InspectorBlurEvent {
|
|
38
|
+
nativeEvent: { relatedTarget?: unknown };
|
|
39
|
+
relatedTarget?: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const TOOLTIP_GAP = 8;
|
|
43
|
+
const TOOLTIP_MAX_WIDTH = 192;
|
|
44
|
+
/** WCAG 2.5.8 (AA) is 24px; 44px matches Apple HIG / WCAG 2.5.5 for touch. */
|
|
45
|
+
export const MIN_POINTER_TARGET_PX = 44;
|
|
46
|
+
/** Hold before scrubbing a scrollable plot, so a flick can still scroll (Apple Health). */
|
|
47
|
+
export const CHART_SCRUB_HOLD_MS = 250;
|
|
48
|
+
const SCRUB_MOVE_SLOP_PX = 10;
|
|
49
|
+
|
|
50
|
+
interface InspectorTouchEvent {
|
|
51
|
+
nativeEvent: {
|
|
52
|
+
locationX?: number;
|
|
53
|
+
locationY?: number;
|
|
54
|
+
pageX?: number;
|
|
55
|
+
pageY?: number;
|
|
56
|
+
touches?: readonly {
|
|
57
|
+
locationX?: number;
|
|
58
|
+
locationY?: number;
|
|
59
|
+
pageX?: number;
|
|
60
|
+
pageY?: number;
|
|
61
|
+
}[];
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function touchPoint(event: InspectorTouchEvent): {
|
|
66
|
+
locationX: number;
|
|
67
|
+
locationY: number;
|
|
68
|
+
pageX: number;
|
|
69
|
+
pageY: number;
|
|
70
|
+
} {
|
|
71
|
+
const touch = event.nativeEvent.touches?.[0];
|
|
72
|
+
const locationX = touch?.locationX ?? event.nativeEvent.locationX ?? 0;
|
|
73
|
+
const locationY = touch?.locationY ?? event.nativeEvent.locationY ?? 0;
|
|
74
|
+
return {
|
|
75
|
+
locationX,
|
|
76
|
+
locationY,
|
|
77
|
+
pageX: touch?.pageX ?? event.nativeEvent.pageX ?? locationX,
|
|
78
|
+
pageY: touch?.pageY ?? event.nativeEvent.pageY ?? locationY,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function expandHitBox(
|
|
83
|
+
left: number,
|
|
84
|
+
top: number,
|
|
85
|
+
width: number,
|
|
86
|
+
height: number,
|
|
87
|
+
minSize: number = MIN_POINTER_TARGET_PX,
|
|
88
|
+
): { left: number; top: number; width: number; height: number } {
|
|
89
|
+
const nextWidth = Math.max(width, minSize);
|
|
90
|
+
const nextHeight = Math.max(height, minSize);
|
|
91
|
+
return {
|
|
92
|
+
left: left - (nextWidth - width) / 2,
|
|
93
|
+
top: top - (nextHeight - height) / 2,
|
|
94
|
+
width: nextWidth,
|
|
95
|
+
height: nextHeight,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function targetIndexAt(
|
|
100
|
+
targets: readonly ChartInspectorTarget[],
|
|
101
|
+
x: number,
|
|
102
|
+
y: number,
|
|
103
|
+
): number | null {
|
|
104
|
+
const hit = targets.findIndex(
|
|
105
|
+
(target) =>
|
|
106
|
+
x >= target.left &&
|
|
107
|
+
x <= target.left + target.width &&
|
|
108
|
+
y >= target.top &&
|
|
109
|
+
y <= target.top + target.height,
|
|
110
|
+
);
|
|
111
|
+
return hit >= 0 ? hit : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function chartValueText(item: ChartTooltipItem): string {
|
|
115
|
+
return item.label.length > 0 ? `${item.label} ${item.formattedValue}` : item.formattedValue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface ChartTooltipSize {
|
|
119
|
+
readonly width: number;
|
|
120
|
+
readonly height: number;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function clampInto(value: number, min: number, max: number): number {
|
|
124
|
+
// Wider than the frame: center it so it overflows both edges equally.
|
|
125
|
+
if (max < min) {
|
|
126
|
+
return (min + max) / 2;
|
|
127
|
+
}
|
|
128
|
+
return Math.min(Math.max(value, min), max);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Top-left of a tooltip in frame coordinates. Beside the anchor on the roomier side when it
|
|
133
|
+
* fits, otherwise above or below it; always clamped inside the frame so a clipping ancestor
|
|
134
|
+
* (a `Card`, a `ScrollView`) cannot cut it off.
|
|
135
|
+
*/
|
|
136
|
+
export function placeChartTooltip(
|
|
137
|
+
anchor: { readonly x: number; readonly y: number; readonly radius?: number },
|
|
138
|
+
tip: ChartTooltipSize,
|
|
139
|
+
frame: ChartTooltipSize,
|
|
140
|
+
): { left: number; top: number } {
|
|
141
|
+
const gap = TOOLTIP_GAP + (anchor.radius ?? 0);
|
|
142
|
+
const rightOf = anchor.x + gap;
|
|
143
|
+
const leftOf = anchor.x - gap - tip.width;
|
|
144
|
+
const fitsRight = rightOf + tip.width <= frame.width;
|
|
145
|
+
const fitsLeft = leftOf >= 0;
|
|
146
|
+
const maxLeft = frame.width - tip.width;
|
|
147
|
+
const maxTop = frame.height - tip.height;
|
|
148
|
+
if (fitsLeft || fitsRight) {
|
|
149
|
+
const useLeft = anchor.x > frame.width / 2 ? fitsLeft : !fitsRight;
|
|
150
|
+
const top = anchor.y > frame.height / 2 ? anchor.y - tip.height : anchor.y;
|
|
151
|
+
return { left: useLeft ? leftOf : rightOf, top: clampInto(top, 0, maxTop) };
|
|
152
|
+
}
|
|
153
|
+
const above = anchor.y - TOOLTIP_GAP - tip.height;
|
|
154
|
+
const top = above >= 0 ? above : anchor.y + TOOLTIP_GAP;
|
|
155
|
+
return {
|
|
156
|
+
left: clampInto(anchor.x - tip.width / 2, 0, maxLeft),
|
|
157
|
+
top: clampInto(top, 0, maxTop),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Renders only from `content`, so a consumer tooltip slot can replace it later. */
|
|
162
|
+
function ChartTooltip({
|
|
163
|
+
content,
|
|
164
|
+
noValueText,
|
|
165
|
+
slot,
|
|
166
|
+
onSize,
|
|
167
|
+
}: {
|
|
168
|
+
content: ChartTooltipContent;
|
|
169
|
+
noValueText: string;
|
|
170
|
+
slot: string;
|
|
171
|
+
onSize: (size: ChartTooltipSize) => void;
|
|
172
|
+
}) {
|
|
173
|
+
return (
|
|
174
|
+
<View
|
|
175
|
+
className="gap-1 rounded-md bg-surface-neutral-strong px-3 py-2 shadow-lg"
|
|
176
|
+
onLayout={(event) => {
|
|
177
|
+
const { width, height } = event.nativeEvent.layout;
|
|
178
|
+
onSize({ width, height });
|
|
179
|
+
}}
|
|
180
|
+
aria-hidden
|
|
181
|
+
accessibilityElementsHidden
|
|
182
|
+
importantForAccessibility="no-hide-descendants"
|
|
183
|
+
{...dataAttributes({ slot: `${slot}-tooltip` })}
|
|
184
|
+
>
|
|
185
|
+
<Text className="body-sm font-medium text-content-neutral-on-strong">{content.title}</Text>
|
|
186
|
+
{content.items.length === 0 ? (
|
|
187
|
+
<Text className="body-sm text-content-neutral-on-strong">{noValueText}</Text>
|
|
188
|
+
) : (
|
|
189
|
+
content.items.map((item) => (
|
|
190
|
+
<View key={item.seriesId} className="flex-row items-center gap-1.5">
|
|
191
|
+
<View className="size-2 rounded-full" style={{ backgroundColor: item.color }} />
|
|
192
|
+
<Text className="body-sm text-content-neutral-on-strong">{chartValueText(item)}</Text>
|
|
193
|
+
</View>
|
|
194
|
+
))
|
|
195
|
+
)}
|
|
196
|
+
</View>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Chart tooltip, measured and clamped inside `width` × `height`. Mount it in the chart's
|
|
202
|
+
* outer plot frame, never inside the plot `ScrollView`, which clips its content.
|
|
203
|
+
*/
|
|
204
|
+
export function ChartTooltipLayer({
|
|
205
|
+
content,
|
|
206
|
+
noValueText,
|
|
207
|
+
slot,
|
|
208
|
+
anchorX,
|
|
209
|
+
anchorY,
|
|
210
|
+
anchorRadius,
|
|
211
|
+
width,
|
|
212
|
+
height,
|
|
213
|
+
}: {
|
|
214
|
+
content: ChartTooltipContent;
|
|
215
|
+
noValueText: string;
|
|
216
|
+
slot: string;
|
|
217
|
+
/** Anchor in frame coordinates. */
|
|
218
|
+
anchorX: number;
|
|
219
|
+
anchorY: number;
|
|
220
|
+
anchorRadius?: number;
|
|
221
|
+
width: number;
|
|
222
|
+
height: number;
|
|
223
|
+
}) {
|
|
224
|
+
const [size, setSize] = useState<ChartTooltipSize | null>(null);
|
|
225
|
+
const position =
|
|
226
|
+
size === null
|
|
227
|
+
? { left: 0, top: 0 }
|
|
228
|
+
: placeChartTooltip({ x: anchorX, y: anchorY, radius: anchorRadius }, size, {
|
|
229
|
+
width,
|
|
230
|
+
height,
|
|
231
|
+
});
|
|
232
|
+
// A fixed-width box at the frame origin lets the tooltip take its natural width wherever it
|
|
233
|
+
// lands; `left`/`right` offsets shrink it to the space left before the frame edge.
|
|
234
|
+
return (
|
|
235
|
+
<View
|
|
236
|
+
pointerEvents="none"
|
|
237
|
+
className="absolute left-0 top-0 z-20 items-start"
|
|
238
|
+
style={{
|
|
239
|
+
width: TOOLTIP_MAX_WIDTH,
|
|
240
|
+
opacity: size === null ? 0 : 1,
|
|
241
|
+
transform: [{ translateX: position.left }, { translateY: position.top }],
|
|
242
|
+
}}
|
|
243
|
+
>
|
|
244
|
+
<ChartTooltip
|
|
245
|
+
content={content}
|
|
246
|
+
noValueText={noValueText}
|
|
247
|
+
slot={slot}
|
|
248
|
+
onSize={(next) => {
|
|
249
|
+
setSize((prev) =>
|
|
250
|
+
prev?.width === next.width && prev.height === next.height ? prev : next,
|
|
251
|
+
);
|
|
252
|
+
}}
|
|
253
|
+
/>
|
|
254
|
+
</View>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Host-element hit targets over an SVG plot. SVG nodes are poor focus and hover targets under
|
|
260
|
+
* React Native Web, so each target is a `Pressable` with roving focus: one tab stop per chart.
|
|
261
|
+
* Web: native `mousemove` (RN Web `Pressable` has no hover). Native: press-and-drag on the
|
|
262
|
+
* overlay; the tooltip tracks the finger and clears on lift. When the plot itself scrolls,
|
|
263
|
+
* scrubbing waits for a short hold so a flick can still pan. Keyboard and VoiceOver still use
|
|
264
|
+
* the buttons (Escape / two-finger Z).
|
|
265
|
+
*/
|
|
266
|
+
export function ChartInspector({
|
|
267
|
+
targets,
|
|
268
|
+
summary,
|
|
269
|
+
selectable,
|
|
270
|
+
activeIndex,
|
|
271
|
+
selectedIndex,
|
|
272
|
+
slot,
|
|
273
|
+
holdToScrub = false,
|
|
274
|
+
onActivate,
|
|
275
|
+
onDeactivate,
|
|
276
|
+
onDismiss,
|
|
277
|
+
onSelectIndex,
|
|
278
|
+
hitTest,
|
|
279
|
+
neighborIndex,
|
|
280
|
+
onScrubLockChange,
|
|
281
|
+
}: {
|
|
282
|
+
targets: readonly ChartInspectorTarget[];
|
|
283
|
+
summary: string;
|
|
284
|
+
selectable: boolean;
|
|
285
|
+
activeIndex: number | null;
|
|
286
|
+
selectedIndex: number | null;
|
|
287
|
+
slot: string;
|
|
288
|
+
/** Wait for a hold before capturing the pan, so a scrollable plot can still flick-scroll. */
|
|
289
|
+
holdToScrub?: boolean;
|
|
290
|
+
onActivate: (index: number) => void;
|
|
291
|
+
/** Called with the target that lost hover or focus. Ignore it if another target is already active. */
|
|
292
|
+
onDeactivate: (index: number) => void;
|
|
293
|
+
onDismiss: () => void;
|
|
294
|
+
onSelectIndex: (index: number) => void;
|
|
295
|
+
/** When set, pointer hits use this instead of the rectangular target boxes (line charts). */
|
|
296
|
+
hitTest?: (x: number, y: number) => number | null;
|
|
297
|
+
/** Override linear arrow-key movement (grouped / stacked bar charts). */
|
|
298
|
+
neighborIndex?: (index: number, key: string) => number | null;
|
|
299
|
+
/**
|
|
300
|
+
* Fires when a hold locks the finger to scrubbing (`true`) and when it lifts (`false`).
|
|
301
|
+
* The parent must disable its `ScrollView` while locked, or the native pan steals the drag.
|
|
302
|
+
*/
|
|
303
|
+
onScrubLockChange?: (locked: boolean) => void;
|
|
304
|
+
}) {
|
|
305
|
+
const refs = useRef<(View | null)[]>([]);
|
|
306
|
+
const overlayRef = useRef<View>(null);
|
|
307
|
+
const [keyboardFocus, setKeyboardFocus] = useState(false);
|
|
308
|
+
const pointerRef = useRef({
|
|
309
|
+
targets,
|
|
310
|
+
hitTest,
|
|
311
|
+
selectable,
|
|
312
|
+
holdToScrub,
|
|
313
|
+
onActivate,
|
|
314
|
+
onDeactivate,
|
|
315
|
+
onDismiss,
|
|
316
|
+
onSelectIndex,
|
|
317
|
+
activeIndex,
|
|
318
|
+
neighborIndex,
|
|
319
|
+
onScrubLockChange,
|
|
320
|
+
});
|
|
321
|
+
pointerRef.current = {
|
|
322
|
+
targets,
|
|
323
|
+
hitTest,
|
|
324
|
+
selectable,
|
|
325
|
+
holdToScrub,
|
|
326
|
+
onActivate,
|
|
327
|
+
onDeactivate,
|
|
328
|
+
onDismiss,
|
|
329
|
+
onSelectIndex,
|
|
330
|
+
activeIndex,
|
|
331
|
+
neighborIndex,
|
|
332
|
+
onScrubLockChange,
|
|
333
|
+
};
|
|
334
|
+
const scrubRef = useRef({
|
|
335
|
+
locked: false,
|
|
336
|
+
holdTimer: null as ReturnType<typeof setTimeout> | null,
|
|
337
|
+
startPageX: 0,
|
|
338
|
+
startPageY: 0,
|
|
339
|
+
lastX: 0,
|
|
340
|
+
lastY: 0,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const hitAtPoint = (x: number, y: number) => {
|
|
344
|
+
const { targets: nextTargets, hitTest: test } = pointerRef.current;
|
|
345
|
+
return test ? test(x, y) : targetIndexAt(nextTargets, x, y);
|
|
346
|
+
};
|
|
347
|
+
const activateAtPoint = (x: number, y: number) => {
|
|
348
|
+
const next = hitAtPoint(x, y);
|
|
349
|
+
const {
|
|
350
|
+
onActivate: activate,
|
|
351
|
+
activeIndex: index,
|
|
352
|
+
onDeactivate: deactivate,
|
|
353
|
+
} = pointerRef.current;
|
|
354
|
+
if (next === null) {
|
|
355
|
+
if (index !== null) {
|
|
356
|
+
deactivate(index);
|
|
357
|
+
}
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
activate(next);
|
|
361
|
+
};
|
|
362
|
+
const clearHoldTimer = () => {
|
|
363
|
+
if (scrubRef.current.holdTimer != null) {
|
|
364
|
+
clearTimeout(scrubRef.current.holdTimer);
|
|
365
|
+
scrubRef.current.holdTimer = null;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
const clearPointer = () => {
|
|
369
|
+
const { activeIndex: index, onDeactivate: deactivate, onDismiss: dismiss } = pointerRef.current;
|
|
370
|
+
dismiss();
|
|
371
|
+
if (index !== null) {
|
|
372
|
+
deactivate(index);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
const finishScrub = () => {
|
|
376
|
+
const wasLocked = scrubRef.current.locked;
|
|
377
|
+
// A tap that never passed the scroll slop still selects. A move that cancelled the
|
|
378
|
+
// hold is a scroll and leaves the chart alone.
|
|
379
|
+
const wasTap =
|
|
380
|
+
pointerRef.current.holdToScrub && !wasLocked && scrubRef.current.holdTimer != null;
|
|
381
|
+
clearHoldTimer();
|
|
382
|
+
const shouldFinish = wasLocked || wasTap || !pointerRef.current.holdToScrub;
|
|
383
|
+
scrubRef.current.locked = false;
|
|
384
|
+
if (wasLocked) {
|
|
385
|
+
pointerRef.current.onScrubLockChange?.(false);
|
|
386
|
+
}
|
|
387
|
+
if (!shouldFinish) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const {
|
|
391
|
+
activeIndex: index,
|
|
392
|
+
selectable: canSelect,
|
|
393
|
+
onSelectIndex: select,
|
|
394
|
+
onActivate: activate,
|
|
395
|
+
} = pointerRef.current;
|
|
396
|
+
const hit = wasTap ? hitAtPoint(scrubRef.current.lastX, scrubRef.current.lastY) : index;
|
|
397
|
+
if (wasTap && hit !== null) {
|
|
398
|
+
activate(hit);
|
|
399
|
+
}
|
|
400
|
+
if (canSelect && hit !== null) {
|
|
401
|
+
select(hit);
|
|
402
|
+
}
|
|
403
|
+
clearPointer();
|
|
404
|
+
};
|
|
405
|
+
const beginHold = (event: InspectorTouchEvent) => {
|
|
406
|
+
if (Platform.OS === 'web') {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const point = touchPoint(event);
|
|
410
|
+
clearHoldTimer();
|
|
411
|
+
scrubRef.current.locked = false;
|
|
412
|
+
scrubRef.current.startPageX = point.pageX;
|
|
413
|
+
scrubRef.current.startPageY = point.pageY;
|
|
414
|
+
scrubRef.current.lastX = point.locationX;
|
|
415
|
+
scrubRef.current.lastY = point.locationY;
|
|
416
|
+
if (!pointerRef.current.holdToScrub) {
|
|
417
|
+
activateAtPoint(point.locationX, point.locationY);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
scrubRef.current.holdTimer = setTimeout(() => {
|
|
421
|
+
scrubRef.current.holdTimer = null;
|
|
422
|
+
scrubRef.current.locked = true;
|
|
423
|
+
pointerRef.current.onScrubLockChange?.(true);
|
|
424
|
+
activateAtPoint(scrubRef.current.lastX, scrubRef.current.lastY);
|
|
425
|
+
}, CHART_SCRUB_HOLD_MS);
|
|
426
|
+
};
|
|
427
|
+
const noteHoldMove = (event: InspectorTouchEvent) => {
|
|
428
|
+
const point = touchPoint(event);
|
|
429
|
+
scrubRef.current.lastX = point.locationX;
|
|
430
|
+
scrubRef.current.lastY = point.locationY;
|
|
431
|
+
if (!pointerRef.current.holdToScrub) {
|
|
432
|
+
activateAtPoint(point.locationX, point.locationY);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (scrubRef.current.locked || scrubRef.current.holdTimer == null) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const dist = Math.hypot(
|
|
439
|
+
point.pageX - scrubRef.current.startPageX,
|
|
440
|
+
point.pageY - scrubRef.current.startPageY,
|
|
441
|
+
);
|
|
442
|
+
if (dist > SCRUB_MOVE_SLOP_PX) {
|
|
443
|
+
clearHoldTimer();
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const panResponder = useRef(
|
|
448
|
+
PanResponder.create({
|
|
449
|
+
onStartShouldSetPanResponder: () => false,
|
|
450
|
+
onMoveShouldSetPanResponder: () => {
|
|
451
|
+
if (Platform.OS === 'web') {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
return pointerRef.current.holdToScrub && scrubRef.current.locked;
|
|
455
|
+
},
|
|
456
|
+
onPanResponderTerminationRequest: () => !scrubRef.current.locked,
|
|
457
|
+
onPanResponderGrant: (event) => {
|
|
458
|
+
activateAtPoint(event.nativeEvent.locationX, event.nativeEvent.locationY);
|
|
459
|
+
},
|
|
460
|
+
onPanResponderMove: (event) => {
|
|
461
|
+
activateAtPoint(event.nativeEvent.locationX, event.nativeEvent.locationY);
|
|
462
|
+
},
|
|
463
|
+
onPanResponderRelease: finishScrub,
|
|
464
|
+
onPanResponderTerminate: finishScrub,
|
|
465
|
+
}),
|
|
466
|
+
).current;
|
|
467
|
+
|
|
468
|
+
const count = targets.length;
|
|
469
|
+
const rovingIndex = Math.min(activeIndex ?? selectedIndex ?? 0, Math.max(count - 1, 0));
|
|
470
|
+
|
|
471
|
+
// RN Web Pressable does not emit hover. Bind the DOM node directly so mousemove
|
|
472
|
+
// along the line (and across category bands) opens the tooltip.
|
|
473
|
+
useEffect(() => {
|
|
474
|
+
const node = overlayRef.current as unknown as HTMLElement | null;
|
|
475
|
+
if (node == null || typeof node.addEventListener !== 'function') {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const hitAt = (event: MouseEvent) => {
|
|
479
|
+
const { targets: nextTargets, hitTest: test } = pointerRef.current;
|
|
480
|
+
const rect = node.getBoundingClientRect();
|
|
481
|
+
const x = event.clientX - rect.left;
|
|
482
|
+
const y = event.clientY - rect.top;
|
|
483
|
+
return test ? test(x, y) : targetIndexAt(nextTargets, x, y);
|
|
484
|
+
};
|
|
485
|
+
const clearActive = () => {
|
|
486
|
+
const {
|
|
487
|
+
activeIndex: index,
|
|
488
|
+
onDeactivate: deactivate,
|
|
489
|
+
onDismiss: dismiss,
|
|
490
|
+
} = pointerRef.current;
|
|
491
|
+
dismiss();
|
|
492
|
+
if (index !== null) {
|
|
493
|
+
deactivate(index);
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
const move = (event: MouseEvent) => {
|
|
497
|
+
setKeyboardFocus(false);
|
|
498
|
+
const next = hitAt(event);
|
|
499
|
+
const {
|
|
500
|
+
onActivate: activate,
|
|
501
|
+
activeIndex: index,
|
|
502
|
+
onDeactivate: deactivate,
|
|
503
|
+
} = pointerRef.current;
|
|
504
|
+
if (next === null) {
|
|
505
|
+
if (index !== null) {
|
|
506
|
+
deactivate(index);
|
|
507
|
+
}
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
activate(next);
|
|
511
|
+
};
|
|
512
|
+
const down = (event: MouseEvent) => {
|
|
513
|
+
if (event.button !== 0) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
setKeyboardFocus(false);
|
|
517
|
+
const next = hitAt(event);
|
|
518
|
+
const {
|
|
519
|
+
onActivate: activate,
|
|
520
|
+
selectable: canSelect,
|
|
521
|
+
onSelectIndex: select,
|
|
522
|
+
} = pointerRef.current;
|
|
523
|
+
if (next === null) {
|
|
524
|
+
clearActive();
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
activate(next);
|
|
528
|
+
if (canSelect) {
|
|
529
|
+
select(next);
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
const leave = (event: MouseEvent) => {
|
|
533
|
+
if (node.contains(event.relatedTarget as Node | null)) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const { activeIndex: index, onDeactivate: deactivate } = pointerRef.current;
|
|
537
|
+
if (index !== null) {
|
|
538
|
+
deactivate(index);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
const docDown = (event: MouseEvent) => {
|
|
542
|
+
if (node.contains(event.target as Node | null)) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
clearActive();
|
|
546
|
+
};
|
|
547
|
+
node.addEventListener('mousemove', move);
|
|
548
|
+
node.addEventListener('mousedown', down);
|
|
549
|
+
node.addEventListener('mouseleave', leave);
|
|
550
|
+
document.addEventListener('mousedown', docDown);
|
|
551
|
+
return () => {
|
|
552
|
+
node.removeEventListener('mousemove', move);
|
|
553
|
+
node.removeEventListener('mousedown', down);
|
|
554
|
+
node.removeEventListener('mouseleave', leave);
|
|
555
|
+
document.removeEventListener('mousedown', docDown);
|
|
556
|
+
if (scrubRef.current.holdTimer != null) {
|
|
557
|
+
clearTimeout(scrubRef.current.holdTimer);
|
|
558
|
+
scrubRef.current.holdTimer = null;
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
}, []);
|
|
562
|
+
|
|
563
|
+
const handleKeyDown = (index: number, event: InspectorKeyEvent) => {
|
|
564
|
+
const key = event.nativeEvent.key ?? event.key ?? '';
|
|
565
|
+
const shiftKey = event.shiftKey === true || event.nativeEvent.shiftKey === true;
|
|
566
|
+
if (
|
|
567
|
+
key === 'Escape' ||
|
|
568
|
+
key === 'Enter' ||
|
|
569
|
+
key === ' ' ||
|
|
570
|
+
key === 'Tab' ||
|
|
571
|
+
key === 'ArrowRight' ||
|
|
572
|
+
key === 'ArrowLeft' ||
|
|
573
|
+
key === 'ArrowUp' ||
|
|
574
|
+
key === 'ArrowDown' ||
|
|
575
|
+
key === 'Home' ||
|
|
576
|
+
key === 'End'
|
|
577
|
+
) {
|
|
578
|
+
setKeyboardFocus(true);
|
|
579
|
+
}
|
|
580
|
+
if (key === 'Escape') {
|
|
581
|
+
event.preventDefault?.();
|
|
582
|
+
onDismiss();
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (key === 'Enter' || key === ' ') {
|
|
586
|
+
event.preventDefault?.();
|
|
587
|
+
onActivate(index);
|
|
588
|
+
if (selectable) {
|
|
589
|
+
onSelectIndex(index);
|
|
590
|
+
}
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (key === 'Tab') {
|
|
594
|
+
const next = neighborIndex?.(index, shiftKey ? 'Shift+Tab' : 'Tab');
|
|
595
|
+
if (next === undefined || next === null || next === index) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
event.preventDefault?.();
|
|
599
|
+
onActivate(next);
|
|
600
|
+
refs.current[next]?.focus();
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
let next: number | null = neighborIndex?.(index, key) ?? null;
|
|
604
|
+
if (next === null) {
|
|
605
|
+
if (key === 'ArrowRight' || key === 'ArrowDown') {
|
|
606
|
+
next = Math.min(index + 1, count - 1);
|
|
607
|
+
} else if (key === 'ArrowLeft' || key === 'ArrowUp') {
|
|
608
|
+
next = Math.max(index - 1, 0);
|
|
609
|
+
} else if (key === 'Home') {
|
|
610
|
+
next = 0;
|
|
611
|
+
} else if (key === 'End') {
|
|
612
|
+
next = count - 1;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (next === null) {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
event.preventDefault?.();
|
|
619
|
+
onActivate(next);
|
|
620
|
+
refs.current[next]?.focus();
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const blurInspector = (event: InspectorBlurEvent | undefined) => {
|
|
624
|
+
const related = event?.nativeEvent?.relatedTarget ?? event?.relatedTarget;
|
|
625
|
+
const node = overlayRef.current as unknown as {
|
|
626
|
+
contains?: (target: unknown) => boolean;
|
|
627
|
+
} | null;
|
|
628
|
+
if (node?.contains?.(related) === true) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
setKeyboardFocus(false);
|
|
632
|
+
if (activeIndex !== null) {
|
|
633
|
+
onDeactivate(activeIndex);
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const outline =
|
|
638
|
+
keyboardFocus && activeIndex !== null
|
|
639
|
+
? (targets.at(activeIndex)?.outline ?? targets.at(activeIndex))
|
|
640
|
+
: undefined;
|
|
641
|
+
|
|
642
|
+
return (
|
|
643
|
+
<View
|
|
644
|
+
ref={overlayRef}
|
|
645
|
+
className="absolute inset-0 z-10"
|
|
646
|
+
role="group"
|
|
647
|
+
aria-label={summary}
|
|
648
|
+
accessibilityLabel={summary}
|
|
649
|
+
onAccessibilityEscape={onDismiss}
|
|
650
|
+
{...(Platform.OS === 'web'
|
|
651
|
+
? {}
|
|
652
|
+
: {
|
|
653
|
+
...panResponder.panHandlers,
|
|
654
|
+
onTouchStart: beginHold,
|
|
655
|
+
onTouchMove: noteHoldMove,
|
|
656
|
+
onTouchEnd: finishScrub,
|
|
657
|
+
onTouchCancel: finishScrub,
|
|
658
|
+
})}
|
|
659
|
+
{...dataAttributes({ slot: `${slot}-inspector` })}
|
|
660
|
+
>
|
|
661
|
+
{outline !== undefined ? (
|
|
662
|
+
<View
|
|
663
|
+
pointerEvents="none"
|
|
664
|
+
className="absolute rounded-sm border-2 border-[var(--color-stroke-ring)]"
|
|
665
|
+
style={{
|
|
666
|
+
left: outline.left,
|
|
667
|
+
top: outline.top,
|
|
668
|
+
width: outline.width,
|
|
669
|
+
height: outline.height,
|
|
670
|
+
}}
|
|
671
|
+
accessibilityElementsHidden
|
|
672
|
+
importantForAccessibility="no-hide-descendants"
|
|
673
|
+
{...dataAttributes({ slot: `${slot}-focus` })}
|
|
674
|
+
/>
|
|
675
|
+
) : null}
|
|
676
|
+
{targets.map((target, index) => (
|
|
677
|
+
<Pressable
|
|
678
|
+
key={target.key}
|
|
679
|
+
ref={(node) => {
|
|
680
|
+
refs.current[index] = node;
|
|
681
|
+
}}
|
|
682
|
+
className="absolute rounded-sm web:outline-none"
|
|
683
|
+
style={{
|
|
684
|
+
left: target.left,
|
|
685
|
+
top: target.top,
|
|
686
|
+
width: target.width,
|
|
687
|
+
height: target.height,
|
|
688
|
+
}}
|
|
689
|
+
accessibilityRole="button"
|
|
690
|
+
accessibilityLabel={target.accessibilityLabel}
|
|
691
|
+
accessibilityState={
|
|
692
|
+
selectable && Platform.OS !== 'web' ? { selected: target.selected } : undefined
|
|
693
|
+
}
|
|
694
|
+
aria-pressed={selectable && Platform.OS === 'web' ? target.selected : undefined}
|
|
695
|
+
pointerEvents="none"
|
|
696
|
+
focusable={Platform.OS !== 'web' || index === rovingIndex}
|
|
697
|
+
tabIndex={index === rovingIndex ? 0 : -1}
|
|
698
|
+
onHoverIn={() => {
|
|
699
|
+
onActivate(index);
|
|
700
|
+
}}
|
|
701
|
+
onHoverOut={() => {
|
|
702
|
+
onDeactivate(index);
|
|
703
|
+
}}
|
|
704
|
+
onFocus={() => {
|
|
705
|
+
setKeyboardFocus(true);
|
|
706
|
+
onActivate(index);
|
|
707
|
+
}}
|
|
708
|
+
onBlur={(event: unknown) => blurInspector(event as InspectorBlurEvent)}
|
|
709
|
+
onPress={() => {
|
|
710
|
+
onActivate(index);
|
|
711
|
+
if (selectable) {
|
|
712
|
+
onSelectIndex(index);
|
|
713
|
+
}
|
|
714
|
+
}}
|
|
715
|
+
onAccessibilityEscape={onDismiss}
|
|
716
|
+
{...{
|
|
717
|
+
onKeyDown: (event: InspectorKeyEvent) => {
|
|
718
|
+
handleKeyDown(index, event);
|
|
719
|
+
},
|
|
720
|
+
}}
|
|
721
|
+
{...dataAttributes({ slot: `${slot}-point`, selected: target.selected })}
|
|
722
|
+
/>
|
|
723
|
+
))}
|
|
724
|
+
</View>
|
|
725
|
+
);
|
|
726
|
+
}
|