panelui-native 0.66.1 → 0.71.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/README.md +31 -1
- package/lib/module/components/accordion/index.js +28 -9
- package/lib/module/components/accordion/index.js.map +1 -1
- package/lib/module/components/area-chart/index.js +18 -1
- package/lib/module/components/area-chart/index.js.map +1 -1
- package/lib/module/components/bar-chart/index.js +18 -1
- package/lib/module/components/bar-chart/index.js.map +1 -1
- package/lib/module/components/button/index.js +14 -7
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/candlestick-chart/index.js +18 -1
- package/lib/module/components/candlestick-chart/index.js.map +1 -1
- package/lib/module/components/collapsible/index.js +280 -0
- package/lib/module/components/collapsible/index.js.map +1 -0
- package/lib/module/components/flow/index.js +66 -3
- package/lib/module/components/flow/index.js.map +1 -1
- package/lib/module/components/form/index.js +7 -5
- package/lib/module/components/form/index.js.map +1 -1
- package/lib/module/components/form/typed-form.js +11 -0
- package/lib/module/components/form/typed-form.js.map +1 -0
- package/lib/module/components/form/use-form.js +4 -0
- package/lib/module/components/form/use-form.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +27 -1
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/line-chart/index.js +18 -1
- package/lib/module/components/line-chart/index.js.map +1 -1
- package/lib/module/components/live-line-chart/index.js +103 -9
- package/lib/module/components/live-line-chart/index.js.map +1 -1
- package/lib/module/components/live-line-chart/live-line-accessibility.js +47 -0
- package/lib/module/components/live-line-chart/live-line-accessibility.js.map +1 -0
- package/lib/module/components/live-line-chart/live-line-lifecycle.js +33 -0
- package/lib/module/components/live-line-chart/live-line-lifecycle.js.map +1 -0
- package/lib/module/components/marquee/index.js +194 -0
- package/lib/module/components/marquee/index.js.map +1 -0
- package/lib/module/components/message-scroller/index.js +172 -10
- package/lib/module/components/message-scroller/index.js.map +1 -1
- package/lib/module/components/message-scroller/message-scroller-math.js +23 -0
- package/lib/module/components/message-scroller/message-scroller-math.js.map +1 -1
- package/lib/module/components/meter/index.js +268 -0
- package/lib/module/components/meter/index.js.map +1 -0
- package/lib/module/components/meter/meter-scale.js +142 -0
- package/lib/module/components/meter/meter-scale.js.map +1 -0
- package/lib/module/components/planner/index.js +788 -0
- package/lib/module/components/planner/index.js.map +1 -0
- package/lib/module/components/planner/planner-announcement.js +30 -0
- package/lib/module/components/planner/planner-announcement.js.map +1 -0
- package/lib/module/components/planner/planner-entries.js +132 -0
- package/lib/module/components/planner/planner-entries.js.map +1 -0
- package/lib/module/components/planner/planner-grid-navigation.js +33 -0
- package/lib/module/components/planner/planner-grid-navigation.js.map +1 -0
- package/lib/module/components/planner/planner-lifecycle.js +45 -0
- package/lib/module/components/planner/planner-lifecycle.js.map +1 -0
- package/lib/module/components/popover/index.js +2 -2
- package/lib/module/components/popover/index.js.map +1 -1
- package/lib/module/components/radar-chart/index.js +18 -1
- package/lib/module/components/radar-chart/index.js.map +1 -1
- package/lib/module/components/scatter-chart/index.js +18 -1
- package/lib/module/components/scatter-chart/index.js.map +1 -1
- package/lib/module/components/skeleton/index.js +45 -7
- package/lib/module/components/skeleton/index.js.map +1 -1
- package/lib/module/components/spinner/index.js +63 -14
- package/lib/module/components/spinner/index.js.map +1 -1
- package/lib/module/components/time-picker/index.js +46 -21
- package/lib/module/components/time-picker/index.js.map +1 -1
- package/lib/module/components/time-picker/ruler-window.js +25 -0
- package/lib/module/components/time-picker/ruler-window.js.map +1 -0
- package/lib/module/hooks/breakpoint-contract.js +29 -0
- package/lib/module/hooks/breakpoint-contract.js.map +1 -0
- package/lib/module/hooks/index.js +1 -1
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/use-breakpoint.js +46 -1
- package/lib/module/hooks/use-breakpoint.js.map +1 -1
- package/lib/module/index.js +5 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/primitives/chart-accessibility.js +68 -0
- package/lib/module/primitives/chart-accessibility.js.map +1 -0
- package/lib/module/primitives/focus-restoration-store.js +52 -0
- package/lib/module/primitives/focus-restoration-store.js.map +1 -0
- package/lib/module/primitives/portal.js +36 -2
- package/lib/module/primitives/portal.js.map +1 -1
- package/lib/typescript/src/components/accordion/index.d.ts +4 -0
- package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
- package/lib/typescript/src/components/area-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bar-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/candlestick-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/candlestick-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/collapsible/index.d.ts +76 -0
- package/lib/typescript/src/components/collapsible/index.d.ts.map +1 -0
- package/lib/typescript/src/components/flow/index.d.ts +7 -1
- package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
- package/lib/typescript/src/components/form/index.d.ts +18 -5
- package/lib/typescript/src/components/form/index.d.ts.map +1 -1
- package/lib/typescript/src/components/form/typed-form.d.ts +7 -0
- package/lib/typescript/src/components/form/typed-form.d.ts.map +1 -0
- package/lib/typescript/src/components/form/use-form.d.ts +4 -0
- package/lib/typescript/src/components/form/use-form.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/line-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/live-line-chart/index.d.ts +21 -7
- package/lib/typescript/src/components/live-line-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/live-line-chart/live-line-accessibility.d.ts +22 -0
- package/lib/typescript/src/components/live-line-chart/live-line-accessibility.d.ts.map +1 -0
- package/lib/typescript/src/components/live-line-chart/live-line-lifecycle.d.ts +23 -0
- package/lib/typescript/src/components/live-line-chart/live-line-lifecycle.d.ts.map +1 -0
- package/lib/typescript/src/components/marquee/index.d.ts +73 -0
- package/lib/typescript/src/components/marquee/index.d.ts.map +1 -0
- package/lib/typescript/src/components/message-scroller/index.d.ts +30 -2
- package/lib/typescript/src/components/message-scroller/index.d.ts.map +1 -1
- package/lib/typescript/src/components/message-scroller/message-scroller-math.d.ts +10 -0
- package/lib/typescript/src/components/message-scroller/message-scroller-math.d.ts.map +1 -1
- package/lib/typescript/src/components/meter/index.d.ts +273 -0
- package/lib/typescript/src/components/meter/index.d.ts.map +1 -0
- package/lib/typescript/src/components/meter/meter-scale.d.ts +96 -0
- package/lib/typescript/src/components/meter/meter-scale.d.ts.map +1 -0
- package/lib/typescript/src/components/planner/index.d.ts +274 -0
- package/lib/typescript/src/components/planner/index.d.ts.map +1 -0
- package/lib/typescript/src/components/planner/planner-announcement.d.ts +15 -0
- package/lib/typescript/src/components/planner/planner-announcement.d.ts.map +1 -0
- package/lib/typescript/src/components/planner/planner-entries.d.ts +75 -0
- package/lib/typescript/src/components/planner/planner-entries.d.ts.map +1 -0
- package/lib/typescript/src/components/planner/planner-grid-navigation.d.ts +4 -0
- package/lib/typescript/src/components/planner/planner-grid-navigation.d.ts.map +1 -0
- package/lib/typescript/src/components/planner/planner-lifecycle.d.ts +20 -0
- package/lib/typescript/src/components/planner/planner-lifecycle.d.ts.map +1 -0
- package/lib/typescript/src/components/radar-chart/index.d.ts +4 -0
- package/lib/typescript/src/components/radar-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/scatter-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/scatter-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/skeleton/index.d.ts +25 -2
- package/lib/typescript/src/components/skeleton/index.d.ts.map +1 -1
- package/lib/typescript/src/components/spinner/index.d.ts +25 -2
- package/lib/typescript/src/components/spinner/index.d.ts.map +1 -1
- package/lib/typescript/src/components/time-picker/index.d.ts +5 -6
- package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/time-picker/ruler-window.d.ts +12 -0
- package/lib/typescript/src/components/time-picker/ruler-window.d.ts.map +1 -0
- package/lib/typescript/src/hooks/breakpoint-contract.d.ts +6 -0
- package/lib/typescript/src/hooks/breakpoint-contract.d.ts.map +1 -0
- package/lib/typescript/src/hooks/index.d.ts +1 -1
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-breakpoint.d.ts +22 -0
- package/lib/typescript/src/hooks/use-breakpoint.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +5 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/chart-accessibility.d.ts +33 -0
- package/lib/typescript/src/primitives/chart-accessibility.d.ts.map +1 -0
- package/lib/typescript/src/primitives/focus-restoration-store.d.ts +24 -0
- package/lib/typescript/src/primitives/focus-restoration-store.d.ts.map +1 -0
- package/lib/typescript/src/primitives/portal.d.ts +4 -0
- package/lib/typescript/src/primitives/portal.d.ts.map +1 -1
- package/lib/typescript/types/form-api.type-test.d.ts +2 -0
- package/lib/typescript/types/form-api.type-test.d.ts.map +1 -0
- package/package.json +63 -4
- package/src/components/accordion/index.tsx +31 -8
- package/src/components/area-chart/index.tsx +27 -2
- package/src/components/bar-chart/index.tsx +27 -2
- package/src/components/button/index.tsx +11 -4
- package/src/components/candlestick-chart/index.tsx +30 -2
- package/src/components/collapsible/index.tsx +278 -0
- package/src/components/flow/index.tsx +93 -14
- package/src/components/form/index.tsx +32 -5
- package/src/components/form/typed-form.ts +13 -0
- package/src/components/form/use-form.ts +9 -0
- package/src/components/heatmap-chart/index.tsx +49 -3
- package/src/components/line-chart/index.tsx +27 -2
- package/src/components/live-line-chart/index.tsx +130 -13
- package/src/components/live-line-chart/live-line-accessibility.ts +61 -0
- package/src/components/live-line-chart/live-line-lifecycle.ts +63 -0
- package/src/components/marquee/index.tsx +232 -0
- package/src/components/message-scroller/index.tsx +234 -6
- package/src/components/message-scroller/message-scroller-math.ts +38 -0
- package/src/components/meter/index.tsx +341 -0
- package/src/components/meter/meter-scale.ts +185 -0
- package/src/components/planner/index.tsx +997 -0
- package/src/components/planner/planner-announcement.ts +37 -0
- package/src/components/planner/planner-entries.ts +155 -0
- package/src/components/planner/planner-grid-navigation.ts +45 -0
- package/src/components/planner/planner-lifecycle.ts +76 -0
- package/src/components/popover/index.tsx +3 -3
- package/src/components/radar-chart/index.tsx +24 -0
- package/src/components/scatter-chart/index.tsx +27 -2
- package/src/components/skeleton/index.tsx +59 -5
- package/src/components/spinner/index.tsx +80 -13
- package/src/components/time-picker/index.tsx +57 -29
- package/src/components/time-picker/ruler-window.ts +24 -0
- package/src/hooks/breakpoint-contract.ts +31 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/use-breakpoint.ts +61 -10
- package/src/index.ts +43 -0
- package/src/primitives/chart-accessibility.ts +102 -0
- package/src/primitives/focus-restoration-store.ts +70 -0
- package/src/primitives/portal.tsx +34 -2
|
@@ -61,6 +61,7 @@ import Animated, {
|
|
|
61
61
|
import Svg, { ClipPath, Defs, G, Line as SvgLine, Rect } from 'react-native-svg';
|
|
62
62
|
import { useCSSVariable } from 'uniwind';
|
|
63
63
|
import { Text } from '../../primitives/text';
|
|
64
|
+
import { ChartAccessibilityData, type ChartAccessibilityProps } from '../../primitives/chart-accessibility';
|
|
64
65
|
import { cn } from '../../utils/cn';
|
|
65
66
|
import { normalizeWeekStart, startOfDay } from '../../utils/date';
|
|
66
67
|
|
|
@@ -309,7 +310,7 @@ function deriveLevels(data: HeatmapColumn[]): number[] {
|
|
|
309
310
|
/* Root */
|
|
310
311
|
/* -------------------------------------------------------------------------- */
|
|
311
312
|
|
|
312
|
-
export interface HeatmapChartProps extends ViewProps {
|
|
313
|
+
export interface HeatmapChartProps extends ViewProps, ChartAccessibilityProps<HeatmapCell> {
|
|
313
314
|
className?: string;
|
|
314
315
|
/** One column per period, with its row bins inside. */
|
|
315
316
|
data: HeatmapColumn[];
|
|
@@ -396,6 +397,11 @@ const HeatmapChartRoot = forwardRef<View, HeatmapChartProps>(function HeatmapCha
|
|
|
396
397
|
animationDuration = 900,
|
|
397
398
|
inactiveOpacity = 1,
|
|
398
399
|
onActiveCellChange,
|
|
400
|
+
accessible,
|
|
401
|
+
accessibilityLabel,
|
|
402
|
+
accessibilityHint,
|
|
403
|
+
accessibilityLabelForDatum,
|
|
404
|
+
onAccessibilityDatumPress,
|
|
399
405
|
children,
|
|
400
406
|
...props
|
|
401
407
|
},
|
|
@@ -491,6 +497,20 @@ const HeatmapChartRoot = forwardRef<View, HeatmapChartProps>(function HeatmapCha
|
|
|
491
497
|
[data, levelOf]
|
|
492
498
|
);
|
|
493
499
|
|
|
500
|
+
const accessibilityCells = useMemo(
|
|
501
|
+
() =>
|
|
502
|
+
data.flatMap((column) =>
|
|
503
|
+
column.bins.map((bin) => ({
|
|
504
|
+
column: column.bin,
|
|
505
|
+
row: bin.bin,
|
|
506
|
+
count: bin.count,
|
|
507
|
+
level: levelOf(bin.count),
|
|
508
|
+
date: bin.date,
|
|
509
|
+
}))
|
|
510
|
+
),
|
|
511
|
+
[data, levelOf]
|
|
512
|
+
);
|
|
513
|
+
|
|
494
514
|
const setActiveCell = useMemo(
|
|
495
515
|
() => (cell: HeatmapCell | null) => {
|
|
496
516
|
setActiveCellState(cell);
|
|
@@ -570,11 +590,37 @@ const HeatmapChartRoot = forwardRef<View, HeatmapChartProps>(function HeatmapCha
|
|
|
570
590
|
whole chart, so it starts at the chart's edge, not the grid's. */}
|
|
571
591
|
{parts.header}
|
|
572
592
|
|
|
593
|
+
<ChartAccessibilityData
|
|
594
|
+
chart="Heatmap chart"
|
|
595
|
+
data={accessibilityCells}
|
|
596
|
+
disabled={accessible === false}
|
|
597
|
+
accessibilityLabel={accessibilityLabel}
|
|
598
|
+
accessibilityHint={accessibilityHint}
|
|
599
|
+
accessibilityLabelForDatum={accessibilityLabelForDatum}
|
|
600
|
+
onAccessibilityDatumPress={onAccessibilityDatumPress}
|
|
601
|
+
valueOf={(cell) => [
|
|
602
|
+
['date', cell.date],
|
|
603
|
+
['column', cell.column],
|
|
604
|
+
['row', cell.row],
|
|
605
|
+
['count', cell.count],
|
|
606
|
+
]}
|
|
607
|
+
/>
|
|
608
|
+
|
|
573
609
|
{parts.x ? (
|
|
574
|
-
<View
|
|
610
|
+
<View
|
|
611
|
+
style={{ paddingLeft: axisWidth }}
|
|
612
|
+
accessibilityElementsHidden
|
|
613
|
+
importantForAccessibility="no-hide-descendants"
|
|
614
|
+
>
|
|
615
|
+
{parts.x}
|
|
616
|
+
</View>
|
|
575
617
|
) : null}
|
|
576
618
|
|
|
577
|
-
<View
|
|
619
|
+
<View
|
|
620
|
+
className="flex-row"
|
|
621
|
+
accessibilityElementsHidden
|
|
622
|
+
importantForAccessibility="no-hide-descendants"
|
|
623
|
+
>
|
|
578
624
|
{parts.y}
|
|
579
625
|
<View style={{ width: grid.width, height: grid.height }}>
|
|
580
626
|
{grid.width > 0 ? (
|
|
@@ -82,6 +82,7 @@ import Svg, {
|
|
|
82
82
|
} from 'react-native-svg';
|
|
83
83
|
import { useCSSVariable } from 'uniwind';
|
|
84
84
|
import { Text } from '../../primitives/text';
|
|
85
|
+
import { ChartAccessibilityData, type ChartAccessibilityProps } from '../../primitives/chart-accessibility';
|
|
85
86
|
import {
|
|
86
87
|
areaPath,
|
|
87
88
|
columnValues,
|
|
@@ -172,7 +173,7 @@ export function useLineChart() {
|
|
|
172
173
|
};
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
export interface LineChartProps extends ViewProps {
|
|
176
|
+
export interface LineChartProps extends ViewProps, ChartAccessibilityProps<LineChartDatum> {
|
|
176
177
|
className?: string;
|
|
177
178
|
/** The rows. Each one is a point along the x-axis. */
|
|
178
179
|
data: LineChartDatum[];
|
|
@@ -227,6 +228,11 @@ const LineChartRoot = forwardRef<LineChartHandle, LineChartProps>(function LineC
|
|
|
227
228
|
yDomain,
|
|
228
229
|
curve = 'monotone',
|
|
229
230
|
onActiveIndexChange,
|
|
231
|
+
accessible,
|
|
232
|
+
accessibilityLabel,
|
|
233
|
+
accessibilityHint,
|
|
234
|
+
accessibilityLabelForDatum,
|
|
235
|
+
onAccessibilityDatumPress,
|
|
230
236
|
compact = false,
|
|
231
237
|
children,
|
|
232
238
|
...props
|
|
@@ -443,7 +449,26 @@ const LineChartRoot = forwardRef<LineChartHandle, LineChartProps>(function LineC
|
|
|
443
449
|
<LineChartContext.Provider value={context}>
|
|
444
450
|
<View {...props} style={props.style} className={cn('w-full', className)}>
|
|
445
451
|
{header}
|
|
446
|
-
<
|
|
452
|
+
<ChartAccessibilityData
|
|
453
|
+
chart="Line chart"
|
|
454
|
+
data={data}
|
|
455
|
+
disabled={accessible === false || loading}
|
|
456
|
+
accessibilityLabel={accessibilityLabel}
|
|
457
|
+
accessibilityHint={accessibilityHint}
|
|
458
|
+
accessibilityLabelForDatum={accessibilityLabelForDatum}
|
|
459
|
+
onAccessibilityDatumPress={onAccessibilityDatumPress}
|
|
460
|
+
valueOf={(datum) => [
|
|
461
|
+
[xDataKey, datum[xDataKey]],
|
|
462
|
+
...series.map(([key]) => [key, datum[key]] as [string, unknown]),
|
|
463
|
+
]}
|
|
464
|
+
/>
|
|
465
|
+
<View
|
|
466
|
+
onLayout={onLayout}
|
|
467
|
+
style={{ aspectRatio }}
|
|
468
|
+
className="w-full"
|
|
469
|
+
accessibilityElementsHidden
|
|
470
|
+
importantForAccessibility="no-hide-descendants"
|
|
471
|
+
>
|
|
447
472
|
{plot.width > 0 ? (
|
|
448
473
|
<>
|
|
449
474
|
<Svg width="100%" height="100%" style={StyleSheet.absoluteFill}>
|
|
@@ -30,10 +30,17 @@
|
|
|
30
30
|
* they mean opposite things.
|
|
31
31
|
*
|
|
32
32
|
* The cost is a frame callback for as long as the chart is mounted. It is
|
|
33
|
-
* stopped by `paused`, by `status="loading"
|
|
34
|
-
* started when the platform asks for reduced
|
|
35
|
-
* advances as each point arrives instead,
|
|
36
|
-
* less often.
|
|
33
|
+
* stopped by `paused`, by `status="loading"`, while the app is backgrounded
|
|
34
|
+
* and on unmount, and it is never started when the platform asks for reduced
|
|
35
|
+
* motion — in that case the window advances as each point arrives instead,
|
|
36
|
+
* which is the same picture sampled less often.
|
|
37
|
+
*
|
|
38
|
+
* Screen readers receive one image-role snapshot: its name, current or
|
|
39
|
+
* selected value, direction, time window and paused state. It changes when the
|
|
40
|
+
* React data changes, never on the UI-thread clock frame, and requests no live
|
|
41
|
+
* announcement. The visual axes, badges and tooltip repeat that snapshot and
|
|
42
|
+
* stay out of the accessibility tree; controls placed in Header remain normal
|
|
43
|
+
* controls.
|
|
37
44
|
*
|
|
38
45
|
* ## Colour follows the recent direction
|
|
39
46
|
*
|
|
@@ -56,7 +63,13 @@ import {
|
|
|
56
63
|
useState,
|
|
57
64
|
type ReactNode,
|
|
58
65
|
} from 'react';
|
|
59
|
-
import {
|
|
66
|
+
import {
|
|
67
|
+
AppState,
|
|
68
|
+
StyleSheet,
|
|
69
|
+
View,
|
|
70
|
+
type LayoutChangeEvent,
|
|
71
|
+
type ViewProps,
|
|
72
|
+
} from 'react-native';
|
|
60
73
|
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
61
74
|
import Animated, {
|
|
62
75
|
runOnJS,
|
|
@@ -92,6 +105,13 @@ import {
|
|
|
92
105
|
type Plot,
|
|
93
106
|
} from '../../utils/chart';
|
|
94
107
|
import { cn } from '../../utils/cn';
|
|
108
|
+
import { liveLineAccessibility } from './live-line-accessibility';
|
|
109
|
+
import {
|
|
110
|
+
liveLineClockRuns,
|
|
111
|
+
normalizeLiveLinePoints,
|
|
112
|
+
normalizeLiveLineWindow,
|
|
113
|
+
reconcileLiveLineActivePoint,
|
|
114
|
+
} from './live-line-lifecycle';
|
|
95
115
|
|
|
96
116
|
const AnimatedPath = Animated.createAnimatedComponent(Path);
|
|
97
117
|
|
|
@@ -224,9 +244,16 @@ function runOf(
|
|
|
224
244
|
|
|
225
245
|
export interface LiveLineChartProps extends ViewProps {
|
|
226
246
|
className?: string;
|
|
227
|
-
/**
|
|
247
|
+
/**
|
|
248
|
+
* Names the chart's single screen-reader snapshot. Falls back to the Header
|
|
249
|
+
* title, then to "Live line chart".
|
|
250
|
+
*/
|
|
251
|
+
accessibilityLabel?: string;
|
|
252
|
+
/** Additional guidance after the snapshot. No gesture is invented for it. */
|
|
253
|
+
accessibilityHint?: string;
|
|
254
|
+
/** The readings so far. Invalid values are dropped and timestamps are ordered. */
|
|
228
255
|
data: LiveLinePoint[];
|
|
229
|
-
/** How much time the plot spans, in seconds. */
|
|
256
|
+
/** How much time the plot spans, in seconds. Invalid values use 30. */
|
|
230
257
|
window?: number;
|
|
231
258
|
/** Freeze the window where it is. The readings still arrive; the clock stops. */
|
|
232
259
|
paused?: boolean;
|
|
@@ -239,7 +266,7 @@ export interface LiveLineChartProps extends ViewProps {
|
|
|
239
266
|
/**
|
|
240
267
|
* The most readings kept. Older ones are dropped, since they are off the
|
|
241
268
|
* window and cannot come back — an unbounded feed otherwise grows an array
|
|
242
|
-
* for as long as the screen is open.
|
|
269
|
+
* for as long as the screen is open. Must be positive and finite.
|
|
243
270
|
*/
|
|
244
271
|
maxPoints?: number;
|
|
245
272
|
/** Width ÷ height of the plot. */
|
|
@@ -276,6 +303,8 @@ const LiveLineChartRoot = forwardRef<LiveLineChartHandle, LiveLineChartProps>(
|
|
|
276
303
|
momentumColors,
|
|
277
304
|
color,
|
|
278
305
|
onActivePointChange,
|
|
306
|
+
accessibilityLabel,
|
|
307
|
+
accessibilityHint,
|
|
279
308
|
children,
|
|
280
309
|
...props
|
|
281
310
|
},
|
|
@@ -290,12 +319,13 @@ const LiveLineChartRoot = forwardRef<LiveLineChartHandle, LiveLineChartProps>(
|
|
|
290
319
|
const domainMax = useSharedValue(0);
|
|
291
320
|
const activeTime = useSharedValue(-1);
|
|
292
321
|
const reducedMotion = useReducedMotion();
|
|
322
|
+
const [appState, setAppState] = useState(AppState.currentState ?? 'active');
|
|
293
323
|
// Stripped of punctuation: `useId` returns something like `:r1:`, and a
|
|
294
324
|
// colon inside a `url(#…)` reference does not resolve — which in RN SVG is
|
|
295
325
|
// a clip that silently does nothing rather than an error.
|
|
296
326
|
const clipId = `panelui-live-clip-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
297
327
|
|
|
298
|
-
const windowMs =
|
|
328
|
+
const windowMs = normalizeLiveLineWindow(windowSeconds) * 1000;
|
|
299
329
|
const loading = status === 'loading';
|
|
300
330
|
|
|
301
331
|
const hasYAxis = useMemo(() => {
|
|
@@ -308,6 +338,34 @@ const LiveLineChartRoot = forwardRef<LiveLineChartHandle, LiveLineChartProps>(
|
|
|
308
338
|
return found;
|
|
309
339
|
}, [children]);
|
|
310
340
|
|
|
341
|
+
const semanticParts = useMemo(() => {
|
|
342
|
+
let title: string | undefined;
|
|
343
|
+
let value: string | undefined;
|
|
344
|
+
let headerFormat: ((reading: number) => string) | undefined;
|
|
345
|
+
let tipFormat: ((reading: number) => string) | undefined;
|
|
346
|
+
let tooltipFormat: ((reading: number) => string) | undefined;
|
|
347
|
+
Children.forEach(children, (child) => {
|
|
348
|
+
if (!isValidElement(child)) return;
|
|
349
|
+
const part = (child.type as { displayName?: string }).displayName;
|
|
350
|
+
if (part === 'LiveLineChart.Header') {
|
|
351
|
+
const header = child.props as LiveLineChartHeaderProps;
|
|
352
|
+
title ??= header.title;
|
|
353
|
+
value ??= header.value;
|
|
354
|
+
headerFormat ??= header.formatValue;
|
|
355
|
+
} else if (part === 'LiveLineChart.Tip') {
|
|
356
|
+
tipFormat ??= (child.props as LiveLineChartTipProps).formatValue;
|
|
357
|
+
} else if (part === 'LiveLineChart.Tooltip') {
|
|
358
|
+
tooltipFormat ??= (child.props as LiveLineChartTooltipProps).formatValue;
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
title,
|
|
363
|
+
value,
|
|
364
|
+
formatLatest: headerFormat ?? tipFormat,
|
|
365
|
+
formatActive: tooltipFormat ?? headerFormat ?? tipFormat,
|
|
366
|
+
};
|
|
367
|
+
}, [children]);
|
|
368
|
+
|
|
311
369
|
const pad = { ...PADDING, left: hasYAxis ? Y_AXIS_WIDTH : PADDING.left };
|
|
312
370
|
const plot: Plot = {
|
|
313
371
|
left: pad.left,
|
|
@@ -322,7 +380,7 @@ const LiveLineChartRoot = forwardRef<LiveLineChartHandle, LiveLineChartProps>(
|
|
|
322
380
|
* the one chart nobody bounded to be the one left running overnight.
|
|
323
381
|
*/
|
|
324
382
|
const points = useMemo(
|
|
325
|
-
() => (data
|
|
383
|
+
() => normalizeLiveLinePoints(data, maxPoints),
|
|
326
384
|
[data, maxPoints]
|
|
327
385
|
);
|
|
328
386
|
|
|
@@ -331,6 +389,11 @@ const LiveLineChartRoot = forwardRef<LiveLineChartHandle, LiveLineChartProps>(
|
|
|
331
389
|
values.value = points.map((point) => point.value);
|
|
332
390
|
}, [points, times, values]);
|
|
333
391
|
|
|
392
|
+
useEffect(() => {
|
|
393
|
+
const subscription = AppState.addEventListener('change', setAppState);
|
|
394
|
+
return () => subscription.remove();
|
|
395
|
+
}, []);
|
|
396
|
+
|
|
334
397
|
const liveLatest = points.length ? points[points.length - 1]! : null;
|
|
335
398
|
|
|
336
399
|
/*
|
|
@@ -393,7 +456,7 @@ const LiveLineChartRoot = forwardRef<LiveLineChartHandle, LiveLineChartProps>(
|
|
|
393
456
|
now.value = Date.now();
|
|
394
457
|
}, false);
|
|
395
458
|
|
|
396
|
-
const running =
|
|
459
|
+
const running = liveLineClockRuns({ paused, loading, reducedMotion, appState });
|
|
397
460
|
|
|
398
461
|
useEffect(() => {
|
|
399
462
|
frame.setActive(running);
|
|
@@ -411,6 +474,13 @@ const LiveLineChartRoot = forwardRef<LiveLineChartHandle, LiveLineChartProps>(
|
|
|
411
474
|
now.value = Date.now();
|
|
412
475
|
}, [running, paused, points, now]);
|
|
413
476
|
|
|
477
|
+
// A frame callback is suspended with the app. Land on the wall clock as
|
|
478
|
+
// soon as it becomes active instead of showing the backgrounded window for
|
|
479
|
+
// one frame and relying on the platform to schedule a callback promptly.
|
|
480
|
+
useEffect(() => {
|
|
481
|
+
if (appState === 'active' && !paused) now.value = Date.now();
|
|
482
|
+
}, [appState, paused, now]);
|
|
483
|
+
|
|
414
484
|
useImperativeHandle(
|
|
415
485
|
ref,
|
|
416
486
|
() => ({
|
|
@@ -434,6 +504,21 @@ const LiveLineChartRoot = forwardRef<LiveLineChartHandle, LiveLineChartProps>(
|
|
|
434
504
|
if (!paused) heldMomentum.current = liveMomentum;
|
|
435
505
|
const momentum = paused ? heldMomentum.current : liveMomentum;
|
|
436
506
|
|
|
507
|
+
const defaultFormat = (reading: number) => compactNumber(reading);
|
|
508
|
+
const semantic = liveLineAccessibility({
|
|
509
|
+
name: accessibilityLabel ?? semanticParts.title,
|
|
510
|
+
status,
|
|
511
|
+
latest,
|
|
512
|
+
activePoint,
|
|
513
|
+
momentum,
|
|
514
|
+
windowSeconds: windowMs / 1000,
|
|
515
|
+
paused,
|
|
516
|
+
now: Date.now(),
|
|
517
|
+
valueOverride: semanticParts.value,
|
|
518
|
+
formatLatest: semanticParts.formatLatest ?? defaultFormat,
|
|
519
|
+
formatActive: semanticParts.formatActive ?? defaultFormat,
|
|
520
|
+
});
|
|
521
|
+
|
|
437
522
|
const base = useSeriesColor(color, 1);
|
|
438
523
|
const successToken = useCSSVariable('--color-success');
|
|
439
524
|
const destructiveToken = useCSSVariable('--color-destructive');
|
|
@@ -458,6 +543,13 @@ const LiveLineChartRoot = forwardRef<LiveLineChartHandle, LiveLineChartProps>(
|
|
|
458
543
|
[onActivePointChange]
|
|
459
544
|
);
|
|
460
545
|
|
|
546
|
+
useEffect(() => {
|
|
547
|
+
const next = reconcileLiveLineActivePoint(activePoint, points);
|
|
548
|
+
if (next === activePoint) return;
|
|
549
|
+
if (!next) activeTime.value = -1;
|
|
550
|
+
setActivePoint(next);
|
|
551
|
+
}, [activePoint, points, activeTime, setActivePoint]);
|
|
552
|
+
|
|
461
553
|
const onLayout = (event: LayoutChangeEvent) => {
|
|
462
554
|
const { width, height } = event.nativeEvent.layout;
|
|
463
555
|
setSize((current) =>
|
|
@@ -519,8 +611,21 @@ const LiveLineChartRoot = forwardRef<LiveLineChartHandle, LiveLineChartProps>(
|
|
|
519
611
|
return (
|
|
520
612
|
<LiveLineChartContext.Provider value={context}>
|
|
521
613
|
<View {...props} style={props.style} className={cn('w-full', className)}>
|
|
614
|
+
<View
|
|
615
|
+
accessible
|
|
616
|
+
accessibilityRole="image"
|
|
617
|
+
accessibilityLabel={semantic.label}
|
|
618
|
+
accessibilityHint={accessibilityHint}
|
|
619
|
+
style={{ position: 'absolute', left: -10_000, width: 1, height: 1 }}
|
|
620
|
+
/>
|
|
522
621
|
{header}
|
|
523
|
-
<View
|
|
622
|
+
<View
|
|
623
|
+
onLayout={onLayout}
|
|
624
|
+
style={{ aspectRatio }}
|
|
625
|
+
className="w-full"
|
|
626
|
+
accessibilityElementsHidden
|
|
627
|
+
importantForAccessibility="no-hide-descendants"
|
|
628
|
+
>
|
|
524
629
|
{plot.width > 0 ? (
|
|
525
630
|
<>
|
|
526
631
|
<Svg width="100%" height="100%" style={StyleSheet.absoluteFill}>
|
|
@@ -1026,6 +1131,14 @@ function LiveLineChartTooltip({ formatValue, className }: LiveLineChartTooltipPr
|
|
|
1026
1131
|
[setActivePoint]
|
|
1027
1132
|
);
|
|
1028
1133
|
|
|
1134
|
+
useEffect(
|
|
1135
|
+
() => () => {
|
|
1136
|
+
activeTime.value = -1;
|
|
1137
|
+
setActivePoint(null);
|
|
1138
|
+
},
|
|
1139
|
+
[activeTime, setActivePoint]
|
|
1140
|
+
);
|
|
1141
|
+
|
|
1029
1142
|
const pan = useMemo(() => {
|
|
1030
1143
|
const resolve = (x: number) => {
|
|
1031
1144
|
'worklet';
|
|
@@ -1259,7 +1372,11 @@ function LiveLineChartHeader({
|
|
|
1259
1372
|
{...props}
|
|
1260
1373
|
className={cn('flex-row items-start justify-between gap-3 pb-3', className)}
|
|
1261
1374
|
>
|
|
1262
|
-
<View
|
|
1375
|
+
<View
|
|
1376
|
+
className="flex-1 gap-0.5"
|
|
1377
|
+
accessibilityElementsHidden
|
|
1378
|
+
importantForAccessibility="no-hide-descendants"
|
|
1379
|
+
>
|
|
1263
1380
|
{title ? (
|
|
1264
1381
|
<Text size="xs" muted>
|
|
1265
1382
|
{title}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface LiveLineAccessiblePoint {
|
|
2
|
+
time: number;
|
|
3
|
+
value: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface LiveLineAccessibilityInput {
|
|
7
|
+
name?: string;
|
|
8
|
+
status: 'loading' | 'ready';
|
|
9
|
+
latest: LiveLineAccessiblePoint | null;
|
|
10
|
+
activePoint: LiveLineAccessiblePoint | null;
|
|
11
|
+
momentum: 'up' | 'down' | 'flat';
|
|
12
|
+
windowSeconds: number;
|
|
13
|
+
paused: boolean;
|
|
14
|
+
now: number;
|
|
15
|
+
valueOverride?: string;
|
|
16
|
+
formatLatest: (value: number) => string;
|
|
17
|
+
formatActive: (value: number) => string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** One bounded snapshot for a stream, rather than one node per arriving point. */
|
|
21
|
+
export function liveLineAccessibility({
|
|
22
|
+
name,
|
|
23
|
+
status,
|
|
24
|
+
latest,
|
|
25
|
+
activePoint,
|
|
26
|
+
momentum,
|
|
27
|
+
windowSeconds,
|
|
28
|
+
paused,
|
|
29
|
+
now,
|
|
30
|
+
valueOverride,
|
|
31
|
+
formatLatest,
|
|
32
|
+
formatActive,
|
|
33
|
+
}: LiveLineAccessibilityInput) {
|
|
34
|
+
const parts = [name?.trim() || 'Live line chart'];
|
|
35
|
+
|
|
36
|
+
if (status === 'loading') {
|
|
37
|
+
parts.push('Loading');
|
|
38
|
+
if (paused) parts.push('Paused');
|
|
39
|
+
return { label: parts.join('. ') };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!latest) {
|
|
43
|
+
parts.push('No readings');
|
|
44
|
+
if (paused) parts.push('Paused');
|
|
45
|
+
return { label: parts.join('. ') };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (activePoint) {
|
|
49
|
+
const secondsAgo = Math.max(0, Math.round((now - activePoint.time) / 1000));
|
|
50
|
+
parts.push(`Selected value, ${formatActive(activePoint.value)}`);
|
|
51
|
+
parts.push(secondsAgo === 0 ? 'Just now' : `${secondsAgo} seconds ago`);
|
|
52
|
+
} else {
|
|
53
|
+
parts.push(`Current value, ${valueOverride ?? formatLatest(latest.value)}`);
|
|
54
|
+
const direction = momentum === 'up' ? 'rising' : momentum === 'down' ? 'falling' : 'steady';
|
|
55
|
+
parts.push(`Trend, ${direction}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
parts.push(`${windowSeconds}-second window`);
|
|
59
|
+
if (paused) parts.push('Paused');
|
|
60
|
+
return { label: parts.join('. ') };
|
|
61
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { AppStateStatus } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export interface LiveLineReading {
|
|
4
|
+
time: number;
|
|
5
|
+
value: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_LIVE_LINE_WINDOW = 30;
|
|
9
|
+
export const DEFAULT_LIVE_LINE_MAX_POINTS = 500;
|
|
10
|
+
|
|
11
|
+
export function normalizeLiveLineWindow(seconds: number): number {
|
|
12
|
+
return Number.isFinite(seconds) && seconds > 0
|
|
13
|
+
? Math.max(seconds, 1)
|
|
14
|
+
: DEFAULT_LIVE_LINE_WINDOW;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function normalizeLiveLineMaxPoints(maxPoints: number): number {
|
|
18
|
+
return Number.isFinite(maxPoints) && maxPoints > 0
|
|
19
|
+
? Math.max(1, Math.floor(maxPoints))
|
|
20
|
+
: DEFAULT_LIVE_LINE_MAX_POINTS;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Builds the one ordered, finite buffer every renderer and interaction reads.
|
|
25
|
+
* For duplicate timestamps the last input wins, matching a corrected reading
|
|
26
|
+
* arriving with the same identity as the one it replaces.
|
|
27
|
+
*/
|
|
28
|
+
export function normalizeLiveLinePoints<T extends LiveLineReading>(
|
|
29
|
+
data: readonly T[],
|
|
30
|
+
maxPoints: number,
|
|
31
|
+
): T[] {
|
|
32
|
+
const byTime = new Map<number, T>();
|
|
33
|
+
for (const point of data) {
|
|
34
|
+
if (Number.isFinite(point.time) && Number.isFinite(point.value)) {
|
|
35
|
+
byTime.set(point.time, point);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return [...byTime.values()]
|
|
39
|
+
.sort((left, right) => left.time - right.time)
|
|
40
|
+
.slice(-normalizeLiveLineMaxPoints(maxPoints));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function liveLineClockRuns(options: {
|
|
44
|
+
paused: boolean;
|
|
45
|
+
loading: boolean;
|
|
46
|
+
reducedMotion: boolean;
|
|
47
|
+
appState: AppStateStatus;
|
|
48
|
+
}): boolean {
|
|
49
|
+
return (
|
|
50
|
+
!options.paused &&
|
|
51
|
+
!options.loading &&
|
|
52
|
+
!options.reducedMotion &&
|
|
53
|
+
options.appState === 'active'
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function reconcileLiveLineActivePoint<T extends LiveLineReading>(
|
|
58
|
+
active: T | null,
|
|
59
|
+
points: readonly T[],
|
|
60
|
+
): T | null {
|
|
61
|
+
if (!active) return null;
|
|
62
|
+
return points.find((point) => point.time === active.time) ?? null;
|
|
63
|
+
}
|