panelui-native 0.68.0 → 0.73.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 +13 -2
- package/lib/module/components/accordion/index.js +38 -13
- package/lib/module/components/accordion/index.js.map +1 -1
- package/lib/module/components/area-chart/index.js +1 -0
- package/lib/module/components/area-chart/index.js.map +1 -1
- package/lib/module/components/breadcrumb/index.js +2 -1
- package/lib/module/components/breadcrumb/index.js.map +1 -1
- package/lib/module/components/button/index.js +21 -12
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/carousel/index.js +1 -1
- package/lib/module/components/carousel/index.js.map +1 -1
- package/lib/module/components/chip/index.js +2 -2
- package/lib/module/components/chip/index.js.map +1 -1
- package/lib/module/components/code-block/index.js.map +1 -1
- package/lib/module/components/collapsible/index.js +284 -0
- package/lib/module/components/collapsible/index.js.map +1 -0
- package/lib/module/components/color-picker/index.js.map +1 -1
- package/lib/module/components/fab/index.js +7 -6
- package/lib/module/components/fab/index.js.map +1 -1
- package/lib/module/components/flow/index.js +67 -4
- package/lib/module/components/flow/index.js.map +1 -1
- package/lib/module/components/frame/index.js +2 -2
- package/lib/module/components/frame/index.js.map +1 -1
- package/lib/module/components/grid-item/index.js +2 -2
- package/lib/module/components/grid-item/index.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +1 -0
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/hex-chart/index.js +1 -0
- package/lib/module/components/hex-chart/index.js.map +1 -1
- package/lib/module/components/input-group/index.js +7 -2
- package/lib/module/components/input-group/index.js.map +1 -1
- package/lib/module/components/item/index.js +2 -2
- package/lib/module/components/item/index.js.map +1 -1
- package/lib/module/components/line-chart/index.js +1 -0
- 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/marker/index.js +2 -2
- package/lib/module/components/marker/index.js.map +1 -1
- package/lib/module/components/marquee/index.js +191 -0
- package/lib/module/components/marquee/index.js.map +1 -0
- package/lib/module/components/marquee/marquee-math.js +39 -0
- package/lib/module/components/marquee/marquee-math.js.map +1 -0
- package/lib/module/components/menu/index.js +11 -3
- package/lib/module/components/menu/index.js.map +1 -1
- package/lib/module/components/meter/index.js +38 -20
- package/lib/module/components/meter/index.js.map +1 -1
- package/lib/module/components/meter/meter-scale.js +62 -8
- package/lib/module/components/meter/meter-scale.js.map +1 -1
- package/lib/module/components/pagination/index.js +27 -11
- package/lib/module/components/pagination/index.js.map +1 -1
- package/lib/module/components/panelside/index.js +7 -2
- package/lib/module/components/panelside/index.js.map +1 -1
- package/lib/module/components/planner/index.js +159 -64
- package/lib/module/components/planner/index.js.map +1 -1
- 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 +15 -7
- package/lib/module/components/planner/planner-entries.js.map +1 -1
- 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/plot/index.js +105 -42
- package/lib/module/components/plot/index.js.map +1 -1
- package/lib/module/components/popover/index.js +2 -2
- package/lib/module/components/popover/index.js.map +1 -1
- package/lib/module/components/post/index.js +4 -4
- package/lib/module/components/post/index.js.map +1 -1
- package/lib/module/components/search-bar/index.js +331 -0
- package/lib/module/components/search-bar/index.js.map +1 -0
- package/lib/module/components/table/index.js +3 -4
- package/lib/module/components/table/index.js.map +1 -1
- package/lib/module/components/timeline/index.js +288 -39
- package/lib/module/components/timeline/index.js.map +1 -1
- package/lib/module/components/toggle-button/index.js +1 -1
- package/lib/module/components/toggle-button/index.js.map +1 -1
- package/lib/module/components/tree/index.js +13 -8
- package/lib/module/components/tree/index.js.map +1 -1
- package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
- package/lib/module/index.js +3 -0
- package/lib/module/index.js.map +1 -1
- 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/module/utils/chart.js +50 -7
- package/lib/module/utils/chart.js.map +1 -1
- package/lib/typescript/src/components/accordion/index.d.ts +6 -2
- package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
- package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/breadcrumb/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/code-block/index.d.ts +3 -3
- package/lib/typescript/src/components/code-block/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/color-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/fab/index.d.ts +4 -2
- package/lib/typescript/src/components/fab/index.d.ts.map +1 -1
- 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/heatmap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/input-group/index.d.ts.map +1 -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/marquee/marquee-math.d.ts +21 -0
- package/lib/typescript/src/components/marquee/marquee-math.d.ts.map +1 -0
- package/lib/typescript/src/components/menu/index.d.ts +1 -1
- package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
- package/lib/typescript/src/components/meter/index.d.ts +7 -4
- package/lib/typescript/src/components/meter/index.d.ts.map +1 -1
- package/lib/typescript/src/components/meter/meter-scale.d.ts +39 -0
- package/lib/typescript/src/components/meter/meter-scale.d.ts.map +1 -1
- package/lib/typescript/src/components/pagination/index.d.ts.map +1 -1
- package/lib/typescript/src/components/panelside/index.d.ts +1 -1
- package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
- package/lib/typescript/src/components/plan/index.d.ts +2 -2
- package/lib/typescript/src/components/planner/index.d.ts.map +1 -1
- 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 +1 -1
- package/lib/typescript/src/components/planner/planner-entries.d.ts.map +1 -1
- 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/plot/index.d.ts +59 -5
- package/lib/typescript/src/components/plot/index.d.ts.map +1 -1
- package/lib/typescript/src/components/search-bar/index.d.ts +216 -0
- package/lib/typescript/src/components/search-bar/index.d.ts.map +1 -0
- package/lib/typescript/src/components/timeline/index.d.ts +35 -2
- package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
- package/lib/typescript/src/components/tree/index.d.ts +3 -4
- package/lib/typescript/src/components/tree/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +2 -2
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- 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/src/utils/chart.d.ts +28 -3
- package/lib/typescript/src/utils/chart.d.ts.map +1 -1
- package/package.json +36 -3
- package/src/components/accordion/index.tsx +48 -15
- package/src/components/area-chart/index.tsx +8 -0
- package/src/components/breadcrumb/index.tsx +2 -1
- package/src/components/button/index.tsx +14 -6
- package/src/components/carousel/index.tsx +1 -1
- package/src/components/chip/index.tsx +2 -2
- package/src/components/code-block/index.tsx +3 -3
- package/src/components/collapsible/index.tsx +287 -0
- package/src/components/color-picker/index.tsx +3 -2
- package/src/components/fab/index.tsx +21 -11
- package/src/components/flow/index.tsx +94 -15
- package/src/components/frame/index.tsx +2 -2
- package/src/components/grid-item/index.tsx +2 -2
- package/src/components/heatmap-chart/index.tsx +7 -1
- package/src/components/hex-chart/index.tsx +7 -1
- package/src/components/input-group/index.tsx +7 -2
- package/src/components/item/index.tsx +2 -2
- package/src/components/line-chart/index.tsx +9 -0
- 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/marker/index.tsx +2 -2
- package/src/components/marquee/index.tsx +234 -0
- package/src/components/marquee/marquee-math.ts +37 -0
- package/src/components/menu/index.tsx +11 -3
- package/src/components/meter/index.tsx +49 -26
- package/src/components/meter/meter-scale.ts +84 -8
- package/src/components/pagination/index.tsx +26 -13
- package/src/components/panelside/index.tsx +7 -2
- package/src/components/plan/index.tsx +2 -2
- package/src/components/planner/index.tsx +202 -76
- package/src/components/planner/planner-announcement.ts +37 -0
- package/src/components/planner/planner-entries.ts +16 -7
- package/src/components/planner/planner-grid-navigation.ts +45 -0
- package/src/components/planner/planner-lifecycle.ts +76 -0
- package/src/components/plot/index.tsx +183 -39
- package/src/components/popover/index.tsx +3 -3
- package/src/components/post/index.tsx +4 -4
- package/src/components/search-bar/index.tsx +436 -0
- package/src/components/table/index.tsx +2 -2
- package/src/components/timeline/index.tsx +335 -39
- package/src/components/toggle-button/index.tsx +1 -1
- package/src/components/tree/index.tsx +17 -12
- package/src/hooks/use-keyboard-avoidance.ts +7 -2
- package/src/index.ts +15 -0
- package/src/primitives/focus-restoration-store.ts +70 -0
- package/src/primitives/portal.tsx +34 -2
- package/src/utils/chart.ts +66 -7
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
useContext,
|
|
65
65
|
useEffect,
|
|
66
66
|
useMemo,
|
|
67
|
+
useRef,
|
|
67
68
|
useState,
|
|
68
69
|
type ReactElement,
|
|
69
70
|
type ReactNode,
|
|
@@ -71,6 +72,7 @@ import {
|
|
|
71
72
|
import {
|
|
72
73
|
AccessibilityInfo,
|
|
73
74
|
AppState,
|
|
75
|
+
Platform,
|
|
74
76
|
Pressable,
|
|
75
77
|
View,
|
|
76
78
|
type ViewProps,
|
|
@@ -107,6 +109,12 @@ import {
|
|
|
107
109
|
visibleEntries,
|
|
108
110
|
type PlannerCountedCategory,
|
|
109
111
|
} from './planner-entries';
|
|
112
|
+
import { usePlannerMonthAnnouncement } from './planner-announcement';
|
|
113
|
+
import { plannerGridTarget } from './planner-grid-navigation';
|
|
114
|
+
import {
|
|
115
|
+
usePlannerMonthLifecycle,
|
|
116
|
+
usePlannerSelectionLifecycle,
|
|
117
|
+
} from './planner-lifecycle';
|
|
110
118
|
|
|
111
119
|
/** How many of a day's entries a cell draws before it stops and counts. */
|
|
112
120
|
const DEFAULT_ENTRY_LIMIT = 2;
|
|
@@ -114,6 +122,11 @@ const DEFAULT_ENTRY_LIMIT = 2;
|
|
|
114
122
|
/** The palette a category takes its dot from when it does not name a colour. */
|
|
115
123
|
const PALETTE_SIZE = 5;
|
|
116
124
|
|
|
125
|
+
/** Stable wrapper keeps the native method's receiver and the hook dependency steady. */
|
|
126
|
+
const announceMonth = (label: string) => {
|
|
127
|
+
AccessibilityInfo.announceForAccessibility(label);
|
|
128
|
+
};
|
|
129
|
+
|
|
117
130
|
const plannerVariants = tv({
|
|
118
131
|
slots: {
|
|
119
132
|
grid: 'gap-1 px-3 pb-3 pt-1',
|
|
@@ -136,7 +149,13 @@ const dayVariants = tv({
|
|
|
136
149
|
* Square-ish because a day here holds a number, a marker and sometimes an
|
|
137
150
|
* icon, and a circle wastes the corners it needs for them.
|
|
138
151
|
*/
|
|
139
|
-
|
|
152
|
+
/*
|
|
153
|
+
* The transparent border is load-bearing: today and the open day both draw
|
|
154
|
+
* one, and a cell that only grows a border when it is picked shifts its
|
|
155
|
+
* contents by a point at the moment you look at it. Every cell reserves the
|
|
156
|
+
* point; the variants below only colour it.
|
|
157
|
+
*/
|
|
158
|
+
cell: 'mx-0.5 h-14 flex-1 rounded-xl border border-transparent px-1.5 pt-1.5',
|
|
140
159
|
number: 'text-xs leading-none',
|
|
141
160
|
marker: 'absolute right-1.5 top-1.5 h-1.5 w-1.5 rounded-full',
|
|
142
161
|
body: 'flex-1 flex-row items-center justify-center gap-0.5 pb-1',
|
|
@@ -148,13 +167,22 @@ const dayVariants = tv({
|
|
|
148
167
|
false: { cell: 'bg-muted/15', number: 'text-muted-foreground/40' },
|
|
149
168
|
},
|
|
150
169
|
/*
|
|
151
|
-
* Today
|
|
152
|
-
*
|
|
170
|
+
* Today rings the tile; the open day fills it. Two rings would be the
|
|
171
|
+
* problem — two channels are not, and the ring is the one that has to
|
|
172
|
+
* survive being read at a glance across forty-two tiles.
|
|
153
173
|
*/
|
|
154
|
-
today: { true: {
|
|
155
|
-
selected: { true: { cell: 'border
|
|
174
|
+
today: { true: { cell: 'border-foreground', number: 'font-semibold' } },
|
|
175
|
+
selected: { true: { cell: 'border-primary/60 bg-primary/10' } },
|
|
156
176
|
disabled: { true: { cell: 'opacity-40' } },
|
|
157
177
|
},
|
|
178
|
+
compoundVariants: [
|
|
179
|
+
/*
|
|
180
|
+
* Today, and open. Declaration order would otherwise hand the border to
|
|
181
|
+
* `selected` and lose the one mark that says which day it actually is —
|
|
182
|
+
* so the ring is restated over the fill.
|
|
183
|
+
*/
|
|
184
|
+
{ today: true, selected: true, class: { cell: 'border-foreground bg-primary/10' } },
|
|
185
|
+
],
|
|
158
186
|
defaultVariants: { inMonth: true },
|
|
159
187
|
});
|
|
160
188
|
|
|
@@ -203,6 +231,7 @@ interface PlannerContextValue {
|
|
|
203
231
|
summary: { total: number; categories: PlannerCountedCategory[] };
|
|
204
232
|
entryLimit: number;
|
|
205
233
|
weekStartsOn: number;
|
|
234
|
+
grid: Date[][];
|
|
206
235
|
locale: DateLocale;
|
|
207
236
|
system: 'gregory' | 'islamic';
|
|
208
237
|
isInMonth: (date: Date) => boolean;
|
|
@@ -315,34 +344,51 @@ const PlannerRoot = forwardRef<View, PlannerProps>(
|
|
|
315
344
|
const system = resolveCalendar(calendar, locale);
|
|
316
345
|
const palette = usePalette();
|
|
317
346
|
const today = useToday();
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
347
|
+
const settleMonth = useCallback(
|
|
348
|
+
(date: Date) => startOfCalendarMonth(date, system, locale),
|
|
349
|
+
[system, locale]
|
|
321
350
|
);
|
|
322
|
-
const month = monthProp
|
|
323
|
-
? startOfCalendarMonth(monthProp, system, locale)
|
|
324
|
-
: internalMonth;
|
|
325
351
|
|
|
352
|
+
const [month, requestMonth] = usePlannerMonthLifecycle({
|
|
353
|
+
month: monthProp,
|
|
354
|
+
defaultMonth,
|
|
355
|
+
settleMonth,
|
|
356
|
+
onMonthChange,
|
|
357
|
+
});
|
|
358
|
+
/*
|
|
359
|
+
* The announcement is registered against the month a press asks for, and
|
|
360
|
+
* spoken only once a commit arrives carrying it. That is what keeps a
|
|
361
|
+
* controlled parent's rejection silent, so it wraps the lifecycle's setter
|
|
362
|
+
* rather than living inside it — the hook owns which month is current, and
|
|
363
|
+
* this owns whether the change was the user's to hear about.
|
|
364
|
+
*/
|
|
365
|
+
const expectMonthAnnouncement = usePlannerMonthAnnouncement({
|
|
366
|
+
monthKey: month.getTime(),
|
|
367
|
+
monthLabel: calendarMonthLabel(month, system, locale),
|
|
368
|
+
announce: announceMonth,
|
|
369
|
+
});
|
|
326
370
|
const setMonth = useCallback(
|
|
327
371
|
(next: Date) => {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
onMonthChange?.(settled);
|
|
372
|
+
expectMonthAnnouncement(settleMonth(next));
|
|
373
|
+
requestMonth(next);
|
|
331
374
|
},
|
|
332
|
-
[
|
|
333
|
-
);
|
|
334
|
-
|
|
335
|
-
const [internalSelected, setInternalSelected] = useState<Date | null>(defaultSelected);
|
|
336
|
-
const selected = selectedProp !== undefined ? selectedProp : internalSelected;
|
|
337
|
-
const select = useCallback(
|
|
338
|
-
(date: Date | null) => {
|
|
339
|
-
if (selectedProp === undefined) setInternalSelected(date);
|
|
340
|
-
onSelectedChange?.(date);
|
|
341
|
-
},
|
|
342
|
-
[selectedProp, onSelectedChange]
|
|
375
|
+
[expectMonthAnnouncement, requestMonth, settleMonth]
|
|
343
376
|
);
|
|
377
|
+
const [selected, select] = usePlannerSelectionLifecycle({
|
|
378
|
+
selected: selectedProp,
|
|
379
|
+
defaultSelected,
|
|
380
|
+
onSelectedChange,
|
|
381
|
+
});
|
|
344
382
|
|
|
345
383
|
const days = useMemo(() => bucketByDay(entries), [entries]);
|
|
384
|
+
const normalizedWeekStart =
|
|
385
|
+
weekStartsOn === 'auto'
|
|
386
|
+
? localeWeekStart(locale)
|
|
387
|
+
: normalizeWeekStart(weekStartsOn);
|
|
388
|
+
const grid = useMemo(
|
|
389
|
+
() => monthGrid(month, normalizedWeekStart, system, locale),
|
|
390
|
+
[month, normalizedWeekStart, system, locale]
|
|
391
|
+
);
|
|
346
392
|
|
|
347
393
|
const isInMonth = useCallback(
|
|
348
394
|
(date: Date) => isSameCalendarMonth(date, month, system, locale),
|
|
@@ -354,34 +400,33 @@ const PlannerRoot = forwardRef<View, PlannerProps>(
|
|
|
354
400
|
[categories]
|
|
355
401
|
);
|
|
356
402
|
|
|
357
|
-
const
|
|
358
|
-
(
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
403
|
+
const categoryColors = useMemo(
|
|
404
|
+
() => {
|
|
405
|
+
const colors = new Map<string, string | undefined>();
|
|
406
|
+
categories.forEach((category, index) => {
|
|
407
|
+
// `findIndex` used to make the first duplicate id authoritative.
|
|
408
|
+
if (colors.has(category.id)) return;
|
|
409
|
+
const slot = (category.colorIndex ?? index + 1) - 1;
|
|
410
|
+
colors.set(
|
|
411
|
+
category.id,
|
|
412
|
+
category.color ??
|
|
413
|
+
palette[((slot % PALETTE_SIZE) + PALETTE_SIZE) % PALETTE_SIZE]
|
|
414
|
+
);
|
|
415
|
+
});
|
|
416
|
+
return colors;
|
|
366
417
|
},
|
|
367
418
|
[categories, palette]
|
|
368
419
|
);
|
|
420
|
+
const colorOf = useCallback(
|
|
421
|
+
(id: string | undefined) => id ? categoryColors.get(id) : undefined,
|
|
422
|
+
[categoryColors]
|
|
423
|
+
);
|
|
369
424
|
|
|
370
425
|
const summary = useMemo(
|
|
371
|
-
() => summariseMonth(
|
|
372
|
-
[
|
|
426
|
+
() => summariseMonth(days, grid, categories, isInMonth),
|
|
427
|
+
[days, grid, categories, isInMonth]
|
|
373
428
|
);
|
|
374
429
|
|
|
375
|
-
/*
|
|
376
|
-
* The month is announced rather than left to the cells. Paging moves 42
|
|
377
|
-
* labels at once and a screen reader reads none of them, so without this
|
|
378
|
-
* the only thing that changes is silent.
|
|
379
|
-
*/
|
|
380
|
-
const monthLabel = calendarMonthLabel(month, system, locale);
|
|
381
|
-
useEffect(() => {
|
|
382
|
-
AccessibilityInfo.announceForAccessibility(monthLabel);
|
|
383
|
-
}, [monthLabel]);
|
|
384
|
-
|
|
385
430
|
const context = useMemo<PlannerContextValue>(
|
|
386
431
|
() => ({
|
|
387
432
|
month,
|
|
@@ -395,10 +440,8 @@ const PlannerRoot = forwardRef<View, PlannerProps>(
|
|
|
395
440
|
colorOf,
|
|
396
441
|
summary,
|
|
397
442
|
entryLimit,
|
|
398
|
-
weekStartsOn:
|
|
399
|
-
|
|
400
|
-
? localeWeekStart(locale)
|
|
401
|
-
: normalizeWeekStart(weekStartsOn),
|
|
443
|
+
weekStartsOn: normalizedWeekStart,
|
|
444
|
+
grid,
|
|
402
445
|
locale,
|
|
403
446
|
system,
|
|
404
447
|
isInMonth,
|
|
@@ -406,7 +449,8 @@ const PlannerRoot = forwardRef<View, PlannerProps>(
|
|
|
406
449
|
}),
|
|
407
450
|
[
|
|
408
451
|
month, setMonth, today, selected, select, days, categories, categoryLabels,
|
|
409
|
-
colorOf, summary, entryLimit,
|
|
452
|
+
colorOf, summary, entryLimit, normalizedWeekStart, grid, locale, system,
|
|
453
|
+
isInMonth, onDayPress,
|
|
410
454
|
]
|
|
411
455
|
);
|
|
412
456
|
|
|
@@ -600,6 +644,21 @@ export interface PlannerGridProps {
|
|
|
600
644
|
renderDay?: PlannerDayRenderer;
|
|
601
645
|
}
|
|
602
646
|
|
|
647
|
+
interface PlannerGridKeyDownEvent {
|
|
648
|
+
nativeEvent: { key?: string };
|
|
649
|
+
preventDefault: () => void;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
interface PlannerGridNavigationContextValue {
|
|
653
|
+
activeIndex: number;
|
|
654
|
+
indexByDay: ReadonlyMap<number, number>;
|
|
655
|
+
register: (index: number, node: View | null) => void;
|
|
656
|
+
focus: (index: number, event: PlannerGridKeyDownEvent) => void;
|
|
657
|
+
makeActive: (index: number) => void;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const PlannerGridNavigationContext = createContext<PlannerGridNavigationContextValue | null>(null);
|
|
661
|
+
|
|
603
662
|
/**
|
|
604
663
|
* The weekday row and the six weeks below it.
|
|
605
664
|
*
|
|
@@ -609,40 +668,77 @@ export interface PlannerGridProps {
|
|
|
609
668
|
* weekday headings are hidden rather than read out 42 times over.
|
|
610
669
|
*/
|
|
611
670
|
function PlannerGrid({ className, renderDay }: PlannerGridProps) {
|
|
612
|
-
const {
|
|
671
|
+
const { grid: weeks, weekStartsOn, locale, selected, today, isInMonth } =
|
|
672
|
+
usePlanner('Planner.Grid');
|
|
613
673
|
const { grid, week: weekRow, heading } = plannerVariants();
|
|
614
|
-
|
|
615
|
-
const weeks = useMemo(
|
|
616
|
-
() => monthGrid(month, weekStartsOn, system, locale),
|
|
617
|
-
[month, weekStartsOn, system, locale]
|
|
618
|
-
);
|
|
619
674
|
const headings = useMemo(
|
|
620
675
|
() => weekdayNames(locale, weekStartsOn),
|
|
621
676
|
[locale, weekStartsOn]
|
|
622
677
|
);
|
|
678
|
+
const dates = useMemo(() => weeks.flat(), [weeks]);
|
|
679
|
+
const indexByDay = useMemo(
|
|
680
|
+
() => new Map(dates.map((date, index) => [date.getTime(), index])),
|
|
681
|
+
[dates]
|
|
682
|
+
);
|
|
683
|
+
const initialDay = selected ?? today;
|
|
684
|
+
const [activeDay, setActiveDay] = useState(initialDay.getTime());
|
|
685
|
+
const storedIndex = indexByDay.get(activeDay);
|
|
686
|
+
const preferredIndex =
|
|
687
|
+
(selected ? indexByDay.get(startOfDay(selected).getTime()) : undefined) ??
|
|
688
|
+
indexByDay.get(today.getTime()) ??
|
|
689
|
+
dates.findIndex(isInMonth);
|
|
690
|
+
const activeIndex = storedIndex ?? Math.max(0, preferredIndex);
|
|
691
|
+
const refs = useRef(new Map<number, View>());
|
|
692
|
+
const register = useCallback((index: number, node: View | null) => {
|
|
693
|
+
if (node) refs.current.set(index, node);
|
|
694
|
+
else refs.current.delete(index);
|
|
695
|
+
}, []);
|
|
696
|
+
const makeActive = useCallback(
|
|
697
|
+
(index: number) => {
|
|
698
|
+
const date = dates[index];
|
|
699
|
+
if (date) setActiveDay(date.getTime());
|
|
700
|
+
},
|
|
701
|
+
[dates]
|
|
702
|
+
);
|
|
703
|
+
const focus = useCallback(
|
|
704
|
+
(index: number, event: PlannerGridKeyDownEvent) => {
|
|
705
|
+
const target = plannerGridTarget(event.nativeEvent.key ?? '', index, dates.length);
|
|
706
|
+
if (target === null) return;
|
|
707
|
+
event.preventDefault();
|
|
708
|
+
makeActive(target);
|
|
709
|
+
refs.current.get(target)?.focus();
|
|
710
|
+
},
|
|
711
|
+
[dates.length, makeActive]
|
|
712
|
+
);
|
|
713
|
+
const navigation = useMemo<PlannerGridNavigationContextValue>(
|
|
714
|
+
() => ({ activeIndex, indexByDay, register, focus, makeActive }),
|
|
715
|
+
[activeIndex, indexByDay, register, focus, makeActive]
|
|
716
|
+
);
|
|
623
717
|
|
|
624
718
|
return (
|
|
625
|
-
<
|
|
626
|
-
<View
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
{label
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
</View>
|
|
637
|
-
|
|
638
|
-
{weeks.map((week, index) => (
|
|
639
|
-
<View key={index} className={weekRow()}>
|
|
640
|
-
{week.map((date) => (
|
|
641
|
-
<PlannerDay key={date.getTime()} date={date} renderDay={renderDay} />
|
|
719
|
+
<PlannerGridNavigationContext.Provider value={renderDay ? null : navigation}>
|
|
720
|
+
<View className={grid({ className })}>
|
|
721
|
+
<View
|
|
722
|
+
className={weekRow()}
|
|
723
|
+
accessibilityElementsHidden
|
|
724
|
+
importantForAccessibility="no-hide-descendants"
|
|
725
|
+
>
|
|
726
|
+
{headings.map((label) => (
|
|
727
|
+
<Text key={label} size="xs" muted className={heading()}>
|
|
728
|
+
{label.toUpperCase()}
|
|
729
|
+
</Text>
|
|
642
730
|
))}
|
|
643
731
|
</View>
|
|
644
|
-
|
|
645
|
-
|
|
732
|
+
|
|
733
|
+
{weeks.map((week, index) => (
|
|
734
|
+
<View key={index} className={weekRow()}>
|
|
735
|
+
{week.map((date) => (
|
|
736
|
+
<PlannerDay key={date.getTime()} date={date} renderDay={renderDay} />
|
|
737
|
+
))}
|
|
738
|
+
</View>
|
|
739
|
+
))}
|
|
740
|
+
</View>
|
|
741
|
+
</PlannerGridNavigationContext.Provider>
|
|
646
742
|
);
|
|
647
743
|
}
|
|
648
744
|
PlannerGrid.displayName = 'Planner.Grid';
|
|
@@ -658,6 +754,10 @@ function PlannerDay({ date, renderDay }: PlannerDayProps) {
|
|
|
658
754
|
days, today, selected, select, colorOf, categoryLabels,
|
|
659
755
|
entryLimit, locale, system, isInMonth, onDayPress,
|
|
660
756
|
} = usePlanner('Planner.Day');
|
|
757
|
+
const navigation = useContext(PlannerGridNavigationContext);
|
|
758
|
+
const registerGridCell = navigation?.register;
|
|
759
|
+
const focusGridCell = navigation?.focus;
|
|
760
|
+
const makeGridCellActive = navigation?.makeActive;
|
|
661
761
|
|
|
662
762
|
const entries = entriesOn(days, date);
|
|
663
763
|
const inMonth = isInMonth(date);
|
|
@@ -677,6 +777,31 @@ function PlannerDay({ date, renderDay }: PlannerDayProps) {
|
|
|
677
777
|
onDayPress?.(date, entries);
|
|
678
778
|
select(date);
|
|
679
779
|
};
|
|
780
|
+
const gridIndex = navigation?.indexByDay.get(date.getTime());
|
|
781
|
+
const setRef = useCallback(
|
|
782
|
+
(node: View | null) => {
|
|
783
|
+
if (gridIndex !== undefined) registerGridCell?.(gridIndex, node);
|
|
784
|
+
},
|
|
785
|
+
[gridIndex, registerGridCell]
|
|
786
|
+
);
|
|
787
|
+
const onFocus = useCallback(() => {
|
|
788
|
+
if (gridIndex !== undefined) makeGridCellActive?.(gridIndex);
|
|
789
|
+
}, [gridIndex, makeGridCellActive]);
|
|
790
|
+
const onKeyDown = useCallback(
|
|
791
|
+
(event: PlannerGridKeyDownEvent) => {
|
|
792
|
+
if (gridIndex !== undefined) focusGridCell?.(gridIndex, event);
|
|
793
|
+
},
|
|
794
|
+
[gridIndex, focusGridCell]
|
|
795
|
+
);
|
|
796
|
+
const webGridProps =
|
|
797
|
+
Platform.OS === 'web' && gridIndex !== undefined && navigation
|
|
798
|
+
? {
|
|
799
|
+
ref: setRef,
|
|
800
|
+
tabIndex: navigation.activeIndex === gridIndex ? (0 as const) : (-1 as const),
|
|
801
|
+
onFocus,
|
|
802
|
+
onKeyDown,
|
|
803
|
+
}
|
|
804
|
+
: {};
|
|
680
805
|
|
|
681
806
|
if (renderDay) {
|
|
682
807
|
return (
|
|
@@ -688,6 +813,7 @@ function PlannerDay({ date, renderDay }: PlannerDayProps) {
|
|
|
688
813
|
|
|
689
814
|
return (
|
|
690
815
|
<Pressable
|
|
816
|
+
{...(webGridProps as ViewProps)}
|
|
691
817
|
onPress={press}
|
|
692
818
|
accessibilityRole="button"
|
|
693
819
|
accessibilityLabel={label}
|
|
@@ -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
|
+
}
|
|
@@ -99,9 +99,13 @@ export function dayAccessibilityLabel<T extends PlannerDatedEntry>(
|
|
|
99
99
|
if (entries.length === 0) return `${dateLabel}, nothing planned`;
|
|
100
100
|
|
|
101
101
|
const named: string[] = [];
|
|
102
|
+
const seen = new Set<string>();
|
|
102
103
|
for (const entry of entries) {
|
|
103
104
|
const label = entry.category ? categoryLabels.get(entry.category) : undefined;
|
|
104
|
-
if (label && !
|
|
105
|
+
if (label && !seen.has(label)) {
|
|
106
|
+
seen.add(label);
|
|
107
|
+
named.push(label);
|
|
108
|
+
}
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
const count = `${entries.length} ${entries.length === 1 ? 'entry' : 'entries'}`;
|
|
@@ -120,18 +124,23 @@ export function dayAccessibilityLabel<T extends PlannerDatedEntry>(
|
|
|
120
124
|
* with the week the month happens to start on.
|
|
121
125
|
*/
|
|
122
126
|
export function summariseMonth<T extends PlannerDatedEntry>(
|
|
123
|
-
|
|
127
|
+
days: Map<number, T[]>,
|
|
128
|
+
grid: readonly (readonly Date[])[],
|
|
124
129
|
categories: readonly { id: string; label: string }[],
|
|
125
130
|
isInMonth: (date: Date) => boolean
|
|
126
131
|
): { total: number; categories: PlannerCountedCategory[] } {
|
|
127
132
|
const counts = new Map<string, number>();
|
|
128
133
|
let total = 0;
|
|
129
134
|
|
|
130
|
-
for (const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
}
|
|
135
144
|
}
|
|
136
145
|
}
|
|
137
146
|
|
|
@@ -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
|
+
}
|