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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
interface PlannerMonthAnnouncementOptions {
|
|
4
|
+
/** The settled month currently visible in the grid. */
|
|
5
|
+
monthKey: number;
|
|
6
|
+
monthLabel: string;
|
|
7
|
+
announce: (label: string) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Announces a month only when the next committed render accepts a user request.
|
|
12
|
+
*
|
|
13
|
+
* Prop-driven months stay silent. In a controlled planner, a render that keeps
|
|
14
|
+
* the previous month rejects the pending request and clears it without speaking.
|
|
15
|
+
*/
|
|
16
|
+
export function usePlannerMonthAnnouncement({
|
|
17
|
+
monthKey,
|
|
18
|
+
monthLabel,
|
|
19
|
+
announce,
|
|
20
|
+
}: PlannerMonthAnnouncementOptions): (month: Date) => void {
|
|
21
|
+
const pendingMonth = useRef<number | null>(null);
|
|
22
|
+
|
|
23
|
+
const expectMonth = useCallback((month: Date) => {
|
|
24
|
+
pendingMonth.current = month.getTime();
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
// Deliberately runs after every commit: an unchanged controlled render is
|
|
28
|
+
// how a parent rejects a request, while a changed matching render accepts it.
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const expected = pendingMonth.current;
|
|
31
|
+
if (expected === null) return;
|
|
32
|
+
pendingMonth.current = null;
|
|
33
|
+
if (expected === monthKey) announce(monthLabel);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return expectMonth;
|
|
37
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parts of a planner that are arithmetic rather than rendering: which
|
|
3
|
+
* entries fall on which day, which category an entry belongs to, what a day
|
|
4
|
+
* says to a screen reader, and what the month adds up to.
|
|
5
|
+
*
|
|
6
|
+
* Kept out of the component so they can be tested without a renderer, and so
|
|
7
|
+
* the day cell stays a function of its inputs rather than of when it ran.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here imports anything. The tests run this file through `node --test`
|
|
10
|
+
* with type stripping rather than a bundler, so an extensionless import of a
|
|
11
|
+
* sibling module — which is how the rest of the library is written — does not
|
|
12
|
+
* resolve. That is why the one line of date arithmetic below is here instead
|
|
13
|
+
* of coming from `utils/date`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The minimum an entry has to be for the grid to place it. */
|
|
17
|
+
export interface PlannerDatedEntry {
|
|
18
|
+
date: Date;
|
|
19
|
+
/** Matches a `PlannerCategory` id. An entry without one is uncategorised. */
|
|
20
|
+
category?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PlannerCountedCategory {
|
|
24
|
+
id: string;
|
|
25
|
+
label: string;
|
|
26
|
+
count: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A day's key: local midnight, so two times on the same day agree. */
|
|
30
|
+
export function dayKey(date: Date): number {
|
|
31
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Entries grouped by the day they fall on, each day in the order it was given.
|
|
36
|
+
*
|
|
37
|
+
* A map rather than a lookup per cell: a month draws 42 cells, and asking each
|
|
38
|
+
* of them to filter the whole list is 42 passes over it every render.
|
|
39
|
+
*/
|
|
40
|
+
export function bucketByDay<T extends PlannerDatedEntry>(
|
|
41
|
+
entries: readonly T[]
|
|
42
|
+
): Map<number, T[]> {
|
|
43
|
+
const days = new Map<number, T[]>();
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
const key = dayKey(entry.date);
|
|
46
|
+
const bucket = days.get(key);
|
|
47
|
+
if (bucket) bucket.push(entry);
|
|
48
|
+
else days.set(key, [entry]);
|
|
49
|
+
}
|
|
50
|
+
return days;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** What falls on one day. Always an array, so a cell has nothing to guard. */
|
|
54
|
+
export function entriesOn<T extends PlannerDatedEntry>(
|
|
55
|
+
days: Map<number, T[]>,
|
|
56
|
+
date: Date
|
|
57
|
+
): T[] {
|
|
58
|
+
return days.get(dayKey(date)) ?? EMPTY;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/*
|
|
62
|
+
* One frozen array for every empty day, rather than a new `[]` each call. A
|
|
63
|
+
* month is mostly empty days, and a fresh array per cell per render defeats
|
|
64
|
+
* every memo below it for a day where nothing changed.
|
|
65
|
+
*/
|
|
66
|
+
const EMPTY: never[] = Object.freeze([]) as never[];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* How many entries a cell draws before it stops and says how many are left.
|
|
70
|
+
*
|
|
71
|
+
* `limit` of 0 means draw none of them — the day still carries its marker and
|
|
72
|
+
* its spoken label, which is the whole content of a compact planner.
|
|
73
|
+
*/
|
|
74
|
+
export function visibleEntries<T>(entries: readonly T[], limit: number): {
|
|
75
|
+
shown: T[];
|
|
76
|
+
overflow: number;
|
|
77
|
+
} {
|
|
78
|
+
const cap = Math.max(0, Math.floor(limit));
|
|
79
|
+
if (entries.length <= cap) return { shown: [...entries], overflow: 0 };
|
|
80
|
+
return { shown: entries.slice(0, cap), overflow: entries.length - cap };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* What a day says when it is read out.
|
|
85
|
+
*
|
|
86
|
+
* The date on its own is what a date picker says, and it is not enough here: a
|
|
87
|
+
* day in a planner differs from the one beside it by what is on it, and a
|
|
88
|
+
* marker that carries that difference in colour alone carries it to nobody who
|
|
89
|
+
* cannot see the colour. So the count and the categories are spoken.
|
|
90
|
+
*
|
|
91
|
+
* `dateLabel` is passed in rather than formatted here, so the grid and the
|
|
92
|
+
* label agree about the calendar system and the locale.
|
|
93
|
+
*/
|
|
94
|
+
export function dayAccessibilityLabel<T extends PlannerDatedEntry>(
|
|
95
|
+
dateLabel: string,
|
|
96
|
+
entries: readonly T[],
|
|
97
|
+
categoryLabels: ReadonlyMap<string, string>
|
|
98
|
+
): string {
|
|
99
|
+
if (entries.length === 0) return `${dateLabel}, nothing planned`;
|
|
100
|
+
|
|
101
|
+
const named: string[] = [];
|
|
102
|
+
const seen = new Set<string>();
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
const label = entry.category ? categoryLabels.get(entry.category) : undefined;
|
|
105
|
+
if (label && !seen.has(label)) {
|
|
106
|
+
seen.add(label);
|
|
107
|
+
named.push(label);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const count = `${entries.length} ${entries.length === 1 ? 'entry' : 'entries'}`;
|
|
112
|
+
return named.length > 0
|
|
113
|
+
? `${dateLabel}, ${count}: ${named.join(', ')}`
|
|
114
|
+
: `${dateLabel}, ${count}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* What the month adds up to, per category, in the order the categories were
|
|
119
|
+
* declared — so the legend and the totals beside it never disagree about which
|
|
120
|
+
* one comes first.
|
|
121
|
+
*
|
|
122
|
+
* Entries outside `month` are left out. The grid draws the days either side of
|
|
123
|
+
* the month it is showing, and counting those would make a total that changes
|
|
124
|
+
* with the week the month happens to start on.
|
|
125
|
+
*/
|
|
126
|
+
export function summariseMonth<T extends PlannerDatedEntry>(
|
|
127
|
+
days: Map<number, T[]>,
|
|
128
|
+
grid: readonly (readonly Date[])[],
|
|
129
|
+
categories: readonly { id: string; label: string }[],
|
|
130
|
+
isInMonth: (date: Date) => boolean
|
|
131
|
+
): { total: number; categories: PlannerCountedCategory[] } {
|
|
132
|
+
const counts = new Map<string, number>();
|
|
133
|
+
let total = 0;
|
|
134
|
+
|
|
135
|
+
for (const week of grid) {
|
|
136
|
+
for (const date of week) {
|
|
137
|
+
if (!isInMonth(date)) continue;
|
|
138
|
+
for (const entry of entriesOn(days, date)) {
|
|
139
|
+
total += 1;
|
|
140
|
+
if (entry.category) {
|
|
141
|
+
counts.set(entry.category, (counts.get(entry.category) ?? 0) + 1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
total,
|
|
149
|
+
categories: categories.map(({ id, label }) => ({
|
|
150
|
+
id,
|
|
151
|
+
label,
|
|
152
|
+
count: counts.get(id) ?? 0,
|
|
153
|
+
})),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type PlannerGridNavigationKey =
|
|
2
|
+
| 'ArrowLeft'
|
|
3
|
+
| 'ArrowRight'
|
|
4
|
+
| 'ArrowUp'
|
|
5
|
+
| 'ArrowDown'
|
|
6
|
+
| 'Home'
|
|
7
|
+
| 'End';
|
|
8
|
+
|
|
9
|
+
/** Returns the next visible cell for a bounded, row-major month grid. */
|
|
10
|
+
export function plannerGridTarget(
|
|
11
|
+
key: string,
|
|
12
|
+
index: number,
|
|
13
|
+
count: number,
|
|
14
|
+
columns = 7
|
|
15
|
+
): number | null {
|
|
16
|
+
if (!Number.isInteger(index) || index < 0 || index >= count || columns < 1) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let target: number;
|
|
21
|
+
switch (key as PlannerGridNavigationKey) {
|
|
22
|
+
case 'ArrowLeft':
|
|
23
|
+
target = index - 1;
|
|
24
|
+
break;
|
|
25
|
+
case 'ArrowRight':
|
|
26
|
+
target = index + 1;
|
|
27
|
+
break;
|
|
28
|
+
case 'ArrowUp':
|
|
29
|
+
target = index - columns;
|
|
30
|
+
break;
|
|
31
|
+
case 'ArrowDown':
|
|
32
|
+
target = index + columns;
|
|
33
|
+
break;
|
|
34
|
+
case 'Home':
|
|
35
|
+
target = index - (index % columns);
|
|
36
|
+
break;
|
|
37
|
+
case 'End':
|
|
38
|
+
target = index + (columns - 1 - (index % columns));
|
|
39
|
+
break;
|
|
40
|
+
default:
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return target >= 0 && target < count ? target : index;
|
|
45
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
interface PlannerMonthLifecycleOptions {
|
|
4
|
+
month: Date | undefined;
|
|
5
|
+
defaultMonth: Date | undefined;
|
|
6
|
+
settleMonth: (month: Date) => Date;
|
|
7
|
+
onMonthChange: ((month: Date) => void) | undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Keeps controlled requests separate from the month a parent has accepted. */
|
|
11
|
+
export function usePlannerMonthLifecycle({
|
|
12
|
+
month: monthProp,
|
|
13
|
+
defaultMonth,
|
|
14
|
+
settleMonth,
|
|
15
|
+
onMonthChange,
|
|
16
|
+
}: PlannerMonthLifecycleOptions): readonly [Date, (month: Date) => void] {
|
|
17
|
+
const [internalMonth, setInternalMonth] = useState(() =>
|
|
18
|
+
settleMonth(defaultMonth ?? new Date())
|
|
19
|
+
);
|
|
20
|
+
const isControlled = monthProp !== undefined;
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
* Parents commonly create an equivalent Date on every render. Keying this
|
|
24
|
+
* derivation by its instant keeps the month object stable, so an unrelated
|
|
25
|
+
* parent render does not rebuild the grid and its month summary.
|
|
26
|
+
*/
|
|
27
|
+
const controlledMonth = useMemo(
|
|
28
|
+
() =>
|
|
29
|
+
monthProp === undefined
|
|
30
|
+
? undefined
|
|
31
|
+
: settleMonth(monthProp),
|
|
32
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
33
|
+
[monthProp?.getTime(), settleMonth]
|
|
34
|
+
);
|
|
35
|
+
const month = controlledMonth ?? internalMonth;
|
|
36
|
+
|
|
37
|
+
const setMonth = useCallback(
|
|
38
|
+
(next: Date) => {
|
|
39
|
+
const settled = settleMonth(next);
|
|
40
|
+
if (!isControlled) setInternalMonth(settled);
|
|
41
|
+
onMonthChange?.(settled);
|
|
42
|
+
},
|
|
43
|
+
[isControlled, onMonthChange, settleMonth]
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return [month, setMonth] as const;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface PlannerSelectionLifecycleOptions {
|
|
50
|
+
selected: Date | null | undefined;
|
|
51
|
+
defaultSelected: Date | null;
|
|
52
|
+
onSelectedChange: ((date: Date | null) => void) | undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Commits locally only when selection is uncontrolled, but always reports a request. */
|
|
56
|
+
export function usePlannerSelectionLifecycle({
|
|
57
|
+
selected: selectedProp,
|
|
58
|
+
defaultSelected,
|
|
59
|
+
onSelectedChange,
|
|
60
|
+
}: PlannerSelectionLifecycleOptions): readonly [
|
|
61
|
+
Date | null,
|
|
62
|
+
(date: Date | null) => void,
|
|
63
|
+
] {
|
|
64
|
+
const [internalSelected, setInternalSelected] = useState<Date | null>(defaultSelected);
|
|
65
|
+
const isControlled = selectedProp !== undefined;
|
|
66
|
+
const selected = isControlled ? selectedProp : internalSelected;
|
|
67
|
+
const select = useCallback(
|
|
68
|
+
(date: Date | null) => {
|
|
69
|
+
if (!isControlled) setInternalSelected(date);
|
|
70
|
+
onSelectedChange?.(date);
|
|
71
|
+
},
|
|
72
|
+
[isControlled, onSelectedChange]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return [selected, select] as const;
|
|
76
|
+
}
|
|
@@ -59,7 +59,7 @@ import Animated, {
|
|
|
59
59
|
withTiming,
|
|
60
60
|
} from 'react-native-reanimated';
|
|
61
61
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
62
|
-
import {
|
|
62
|
+
import { FocusRestorePortal } from '../../primitives/portal';
|
|
63
63
|
import { Scrim } from '../../primitives/scrim';
|
|
64
64
|
import { useBackHandler } from '../../hooks/use-back-handler';
|
|
65
65
|
import { BottomSheet } from '../bottom-sheet';
|
|
@@ -535,7 +535,7 @@ function PopoverContent({
|
|
|
535
535
|
if (!open || !trigger) return null;
|
|
536
536
|
|
|
537
537
|
return (
|
|
538
|
-
<
|
|
538
|
+
<FocusRestorePortal>
|
|
539
539
|
{/* Portal content mounts under PortalHost, outside this provider's
|
|
540
540
|
subtree — re-provide the context so Popover.Close and Popover.Arrow
|
|
541
541
|
keep working. */}
|
|
@@ -609,7 +609,7 @@ function PopoverContent({
|
|
|
609
609
|
</Animated.View>
|
|
610
610
|
</View>
|
|
611
611
|
</PopoverContext.Provider>
|
|
612
|
-
</
|
|
612
|
+
</FocusRestorePortal>
|
|
613
613
|
);
|
|
614
614
|
}
|
|
615
615
|
|
|
@@ -59,6 +59,7 @@ import Animated, {
|
|
|
59
59
|
} from 'react-native-reanimated';
|
|
60
60
|
import { useCSSVariable } from 'uniwind';
|
|
61
61
|
import { Text } from '../../primitives/text';
|
|
62
|
+
import { ChartAccessibilityData, type ChartAccessibilityProps } from '../../primitives/chart-accessibility';
|
|
62
63
|
import { cn } from '../../utils/cn';
|
|
63
64
|
import {
|
|
64
65
|
polarPoint,
|
|
@@ -212,6 +213,9 @@ export interface RadarChartProps extends Omit<ViewProps, 'children'> {
|
|
|
212
213
|
/** Drop the room reserved for axis labels, for a radar with none. */
|
|
213
214
|
compact?: boolean;
|
|
214
215
|
children?: ReactNode;
|
|
216
|
+
/** Accessible data labels and optional activation for each axis row. */
|
|
217
|
+
accessibilityLabelForDatum?: ChartAccessibilityProps<RadarChartDatum>['accessibilityLabelForDatum'];
|
|
218
|
+
onAccessibilityDatumPress?: ChartAccessibilityProps<RadarChartDatum>['onAccessibilityDatumPress'];
|
|
215
219
|
}
|
|
216
220
|
|
|
217
221
|
/** Imperative handle: re-run the reveal on demand, for a "replay" control. */
|
|
@@ -231,6 +235,11 @@ const RadarChartRoot = forwardRef<RadarChartHandle, RadarChartProps>(
|
|
|
231
235
|
domain,
|
|
232
236
|
animationDuration = REVEAL_DURATION,
|
|
233
237
|
compact = false,
|
|
238
|
+
accessible,
|
|
239
|
+
accessibilityLabel,
|
|
240
|
+
accessibilityHint,
|
|
241
|
+
accessibilityLabelForDatum,
|
|
242
|
+
onAccessibilityDatumPress,
|
|
234
243
|
children,
|
|
235
244
|
...props
|
|
236
245
|
},
|
|
@@ -393,6 +402,19 @@ const RadarChartRoot = forwardRef<RadarChartHandle, RadarChartProps>(
|
|
|
393
402
|
<RadarChartContext.Provider value={context}>
|
|
394
403
|
<View {...props} style={props.style} className={cn('w-full', className)}>
|
|
395
404
|
{header}
|
|
405
|
+
<ChartAccessibilityData
|
|
406
|
+
chart="Radar chart"
|
|
407
|
+
data={data}
|
|
408
|
+
disabled={accessible === false || status === 'loading'}
|
|
409
|
+
accessibilityLabel={accessibilityLabel}
|
|
410
|
+
accessibilityHint={accessibilityHint}
|
|
411
|
+
accessibilityLabelForDatum={accessibilityLabelForDatum}
|
|
412
|
+
onAccessibilityDatumPress={onAccessibilityDatumPress}
|
|
413
|
+
valueOf={(datum) => [
|
|
414
|
+
[axisKey, datum[axisKey]],
|
|
415
|
+
...series.map(([key]) => [key, datum[key]] as [string, unknown]),
|
|
416
|
+
]}
|
|
417
|
+
/>
|
|
396
418
|
<View
|
|
397
419
|
onLayout={onLayout}
|
|
398
420
|
style={
|
|
@@ -407,6 +429,8 @@ const RadarChartRoot = forwardRef<RadarChartHandle, RadarChartProps>(
|
|
|
407
429
|
: { aspectRatio }
|
|
408
430
|
}
|
|
409
431
|
className={fixedSize ? 'self-center' : 'w-full'}
|
|
432
|
+
accessibilityElementsHidden
|
|
433
|
+
importantForAccessibility="no-hide-descendants"
|
|
410
434
|
>
|
|
411
435
|
{radius > 0 ? (
|
|
412
436
|
<>
|
|
@@ -67,6 +67,7 @@ import Animated, {
|
|
|
67
67
|
import Svg, { Circle, G, Line as SvgLine } from 'react-native-svg';
|
|
68
68
|
import { useCSSVariable } from 'uniwind';
|
|
69
69
|
import { Text } from '../../primitives/text';
|
|
70
|
+
import { ChartAccessibilityData, type ChartAccessibilityProps } from '../../primitives/chart-accessibility';
|
|
70
71
|
import { compactNumber, useSeriesColor, xAt, yOf, type Plot } from '../../utils/chart';
|
|
71
72
|
import { cn } from '../../utils/cn';
|
|
72
73
|
|
|
@@ -191,7 +192,7 @@ export function useScatterChart() {
|
|
|
191
192
|
return { activePoint, xDataKey };
|
|
192
193
|
}
|
|
193
194
|
|
|
194
|
-
export interface ScatterChartProps extends ViewProps {
|
|
195
|
+
export interface ScatterChartProps extends ViewProps, ChartAccessibilityProps<ScatterChartDatum> {
|
|
195
196
|
className?: string;
|
|
196
197
|
/** The rows. Each one is a point, placed by two of its values. */
|
|
197
198
|
data: ScatterChartDatum[];
|
|
@@ -244,6 +245,11 @@ const ScatterChartRoot = forwardRef<ScatterChartHandle, ScatterChartProps>(
|
|
|
244
245
|
xDomain,
|
|
245
246
|
yDomain,
|
|
246
247
|
onActivePointChange,
|
|
248
|
+
accessible,
|
|
249
|
+
accessibilityLabel,
|
|
250
|
+
accessibilityHint,
|
|
251
|
+
accessibilityLabelForDatum,
|
|
252
|
+
onAccessibilityDatumPress,
|
|
247
253
|
compact = false,
|
|
248
254
|
children,
|
|
249
255
|
...props
|
|
@@ -476,7 +482,26 @@ const ScatterChartRoot = forwardRef<ScatterChartHandle, ScatterChartProps>(
|
|
|
476
482
|
<ScatterChartContext.Provider value={context}>
|
|
477
483
|
<View {...props} style={props.style} className={cn('w-full', className)}>
|
|
478
484
|
{header}
|
|
479
|
-
<
|
|
485
|
+
<ChartAccessibilityData
|
|
486
|
+
chart="Scatter chart"
|
|
487
|
+
data={data}
|
|
488
|
+
disabled={accessible === false || status === 'loading'}
|
|
489
|
+
accessibilityLabel={accessibilityLabel}
|
|
490
|
+
accessibilityHint={accessibilityHint}
|
|
491
|
+
accessibilityLabelForDatum={accessibilityLabelForDatum}
|
|
492
|
+
onAccessibilityDatumPress={onAccessibilityDatumPress}
|
|
493
|
+
valueOf={(datum) => [
|
|
494
|
+
[xDataKey, datum[xDataKey]],
|
|
495
|
+
...series.map(([key]) => [key, datum[key]] as [string, unknown]),
|
|
496
|
+
]}
|
|
497
|
+
/>
|
|
498
|
+
<View
|
|
499
|
+
onLayout={onLayout}
|
|
500
|
+
style={{ aspectRatio }}
|
|
501
|
+
className="w-full"
|
|
502
|
+
accessibilityElementsHidden
|
|
503
|
+
importantForAccessibility="no-hide-descendants"
|
|
504
|
+
>
|
|
480
505
|
{plot.width > 0 ? (
|
|
481
506
|
<>
|
|
482
507
|
<Svg width="100%" height="100%" style={StyleSheet.absoluteFill}>
|
|
@@ -2,31 +2,85 @@ import { memo, useEffect } from 'react';
|
|
|
2
2
|
import Animated, {
|
|
3
3
|
cancelAnimation,
|
|
4
4
|
useAnimatedStyle,
|
|
5
|
+
useReducedMotion,
|
|
5
6
|
useSharedValue,
|
|
6
7
|
withRepeat,
|
|
7
8
|
withTiming,
|
|
8
9
|
} from 'react-native-reanimated';
|
|
9
10
|
import { cn } from '../../utils/cn';
|
|
10
11
|
|
|
12
|
+
/** Milliseconds for one half of the pulse. */
|
|
13
|
+
const PULSE_DURATION = 700;
|
|
14
|
+
/** How far down the pulse goes. */
|
|
15
|
+
const PULSE_FLOOR = 0.45;
|
|
16
|
+
/**
|
|
17
|
+
* Where the pulse rests when it is not running. Between the two ends of it, so
|
|
18
|
+
* a still skeleton looks like the same placeholder rather than a solid block.
|
|
19
|
+
*/
|
|
20
|
+
const RESTING_OPACITY = 0.7;
|
|
21
|
+
|
|
11
22
|
export interface SkeletonProps {
|
|
12
23
|
className?: string;
|
|
24
|
+
/**
|
|
25
|
+
* What is loading, for a screen reader. Setting it makes this skeleton
|
|
26
|
+
* announce as a busy status; leaving it unset keeps the placeholder out of
|
|
27
|
+
* the accessibility tree entirely.
|
|
28
|
+
*
|
|
29
|
+
* A screen full of placeholders needs **one** of them labelled, not all of
|
|
30
|
+
* them — put it on the skeleton standing for the region and leave the rest
|
|
31
|
+
* silent.
|
|
32
|
+
*/
|
|
33
|
+
label?: string;
|
|
13
34
|
}
|
|
14
35
|
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Pulsing placeholder for content that has not arrived. The opacity animation
|
|
38
|
+
* runs on the UI thread.
|
|
39
|
+
*
|
|
40
|
+
* A placeholder is a picture of absent content, so it is hidden from assistive
|
|
41
|
+
* technology by default — an unlabelled grey box announces nothing worth
|
|
42
|
+
* hearing, and a screen of them announces it many times over. Pass `label` on
|
|
43
|
+
* the one skeleton that stands for the region to have the wait announced once.
|
|
44
|
+
*
|
|
45
|
+
* Under the platform's reduce-motion setting the pulse stops and the
|
|
46
|
+
* placeholder holds at a middle opacity. Unlike a spinner, a skeleton that
|
|
47
|
+
* stops moving does not read as hung: it is a shape where content will be, and
|
|
48
|
+
* the shape says that on its own.
|
|
49
|
+
*/
|
|
50
|
+
export const Skeleton = memo(function Skeleton({
|
|
51
|
+
className,
|
|
52
|
+
label,
|
|
53
|
+
}: SkeletonProps) {
|
|
54
|
+
const reducedMotion = useReducedMotion();
|
|
55
|
+
const opacity = useSharedValue(reducedMotion ? RESTING_OPACITY : 1);
|
|
18
56
|
|
|
19
57
|
useEffect(() => {
|
|
20
|
-
|
|
58
|
+
if (reducedMotion) {
|
|
59
|
+
opacity.value = RESTING_OPACITY;
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
opacity.value = 1;
|
|
63
|
+
opacity.value = withRepeat(
|
|
64
|
+
withTiming(PULSE_FLOOR, { duration: PULSE_DURATION }),
|
|
65
|
+
-1,
|
|
66
|
+
true
|
|
67
|
+
);
|
|
21
68
|
return () => cancelAnimation(opacity);
|
|
22
|
-
}, [opacity]);
|
|
69
|
+
}, [opacity, reducedMotion]);
|
|
23
70
|
|
|
24
71
|
const animatedStyle = useAnimatedStyle(() => ({
|
|
25
72
|
opacity: opacity.value,
|
|
26
73
|
}));
|
|
27
74
|
|
|
75
|
+
const announced = label != null;
|
|
76
|
+
|
|
28
77
|
return (
|
|
29
78
|
<Animated.View
|
|
79
|
+
accessibilityRole={announced ? 'progressbar' : undefined}
|
|
80
|
+
accessibilityLabel={label}
|
|
81
|
+
accessibilityState={announced ? { busy: true } : undefined}
|
|
82
|
+
accessibilityElementsHidden={!announced}
|
|
83
|
+
importantForAccessibility={announced ? 'auto' : 'no-hide-descendants'}
|
|
30
84
|
style={animatedStyle}
|
|
31
85
|
className={cn('rounded-md bg-skeleton', className)}
|
|
32
86
|
/>
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { memo, useEffect } from 'react';
|
|
2
2
|
import Animated, {
|
|
3
3
|
Easing,
|
|
4
|
+
cancelAnimation,
|
|
5
|
+
interpolate,
|
|
4
6
|
useAnimatedStyle,
|
|
7
|
+
useReducedMotion,
|
|
5
8
|
useSharedValue,
|
|
6
9
|
withRepeat,
|
|
7
10
|
withTiming,
|
|
8
|
-
cancelAnimation,
|
|
9
11
|
} from 'react-native-reanimated';
|
|
10
12
|
import { tv, type VariantProps } from 'tailwind-variants';
|
|
11
13
|
|
|
14
|
+
/** Milliseconds for one full turn. */
|
|
15
|
+
const SPIN_DURATION = 800;
|
|
16
|
+
/** Milliseconds for one half of the fade that stands in for the turn. */
|
|
17
|
+
const PULSE_DURATION = 900;
|
|
18
|
+
/** How far down the fade goes. Faint enough to read as a pulse, not a flicker. */
|
|
19
|
+
const PULSE_FLOOR = 0.35;
|
|
20
|
+
|
|
12
21
|
const spinnerVariants = tv({
|
|
13
22
|
base: 'rounded-full border-2 border-muted border-t-primary',
|
|
14
23
|
variants: {
|
|
@@ -25,27 +34,85 @@ const spinnerVariants = tv({
|
|
|
25
34
|
|
|
26
35
|
export interface SpinnerProps extends VariantProps<typeof spinnerVariants> {
|
|
27
36
|
className?: string;
|
|
37
|
+
/**
|
|
38
|
+
* What is loading, for a screen reader. Setting it makes the spinner
|
|
39
|
+
* announce as a busy status; leaving it unset keeps it out of the
|
|
40
|
+
* accessibility tree.
|
|
41
|
+
*
|
|
42
|
+
* Leave it unset wherever something around the spinner already says the
|
|
43
|
+
* wait is on — a loading button, a row with its own caption. Set it where
|
|
44
|
+
* the spinner is the only sign, or the wait passes in silence.
|
|
45
|
+
*/
|
|
46
|
+
label?: string;
|
|
28
47
|
}
|
|
29
48
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Rotating ring for work of unknown length. The animation runs entirely on the
|
|
51
|
+
* UI thread.
|
|
52
|
+
*
|
|
53
|
+
* A bare ring carries no words, so it is hidden from assistive technology
|
|
54
|
+
* unless `label` is given — an unnamed progress indicator announces its role
|
|
55
|
+
* and nothing else, which is noise standing in for information.
|
|
56
|
+
*
|
|
57
|
+
* Under the platform's reduce-motion setting the ring stops turning and fades
|
|
58
|
+
* in place instead. Stopping it dead is the other option and it is the wrong
|
|
59
|
+
* one: a spinner that holds still reads as one that has hung, which is the
|
|
60
|
+
* single thing a spinner exists to rule out. Reduce motion asks for less
|
|
61
|
+
* travel, not for less news.
|
|
62
|
+
*/
|
|
63
|
+
export const Spinner = memo(function Spinner({
|
|
64
|
+
className,
|
|
65
|
+
size,
|
|
66
|
+
label,
|
|
67
|
+
}: SpinnerProps) {
|
|
68
|
+
const reducedMotion = useReducedMotion();
|
|
69
|
+
const progress = useSharedValue(0);
|
|
33
70
|
|
|
34
71
|
useEffect(() => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
72
|
+
progress.value = 0;
|
|
73
|
+
progress.value = withRepeat(
|
|
74
|
+
withTiming(1, {
|
|
75
|
+
duration: reducedMotion ? PULSE_DURATION : SPIN_DURATION,
|
|
76
|
+
easing: reducedMotion ? Easing.inOut(Easing.ease) : Easing.linear,
|
|
77
|
+
}),
|
|
78
|
+
-1,
|
|
79
|
+
// The turn restarts where it began, because a full circle already ends
|
|
80
|
+
// where it started. The fade turns around, because a pulse that snapped
|
|
81
|
+
// back to full brightness would be the flicker the setting is there to
|
|
82
|
+
// avoid.
|
|
83
|
+
reducedMotion
|
|
38
84
|
);
|
|
39
|
-
return () => cancelAnimation(
|
|
40
|
-
}, [
|
|
85
|
+
return () => cancelAnimation(progress);
|
|
86
|
+
}, [progress, reducedMotion]);
|
|
87
|
+
|
|
88
|
+
/*
|
|
89
|
+
* One style sets both properties either case needs. The two are swapped on
|
|
90
|
+
* the same view when the setting changes under a mounted spinner, and an
|
|
91
|
+
* animated property that one case drops is not reset by the other — it is
|
|
92
|
+
* left at whatever it was last given, which would strand a half-faded ring
|
|
93
|
+
* at that opacity for the rest of its life.
|
|
94
|
+
*/
|
|
95
|
+
const animatedStyle = useAnimatedStyle(() =>
|
|
96
|
+
reducedMotion
|
|
97
|
+
? {
|
|
98
|
+
opacity: interpolate(progress.value, [0, 1], [1, PULSE_FLOOR]),
|
|
99
|
+
transform: [{ rotate: '0deg' }],
|
|
100
|
+
}
|
|
101
|
+
: {
|
|
102
|
+
opacity: 1,
|
|
103
|
+
transform: [{ rotate: `${progress.value * 360}deg` }],
|
|
104
|
+
}
|
|
105
|
+
);
|
|
41
106
|
|
|
42
|
-
const
|
|
43
|
-
transform: [{ rotate: `${rotation.value}deg` }],
|
|
44
|
-
}));
|
|
107
|
+
const announced = label != null;
|
|
45
108
|
|
|
46
109
|
return (
|
|
47
110
|
<Animated.View
|
|
48
|
-
accessibilityRole=
|
|
111
|
+
accessibilityRole={announced ? 'progressbar' : undefined}
|
|
112
|
+
accessibilityLabel={label}
|
|
113
|
+
accessibilityState={announced ? { busy: true } : undefined}
|
|
114
|
+
accessibilityElementsHidden={!announced}
|
|
115
|
+
importantForAccessibility={announced ? 'auto' : 'no-hide-descendants'}
|
|
49
116
|
style={animatedStyle}
|
|
50
117
|
className={spinnerVariants({ size, className })}
|
|
51
118
|
/>
|