@super-calendar/native 2.3.2 → 2.4.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 +21 -0
- package/dist/{DefaultMonthEvent-BW6qy50X.d.ts → DefaultMonthEvent-BSvuJUFk.d.ts} +174 -173
- package/dist/{DefaultMonthEvent-BbCMc60z.d.mts → DefaultMonthEvent-zDj9dHEv.d.mts} +174 -173
- package/dist/{MonthList-CFXm7BK_.js → MonthList-Aczb2RSY.js} +219 -108
- package/dist/{MonthList-BidXJMfm.mjs → MonthList-zPLklHAY.mjs} +203 -110
- package/dist/index.d.mts +221 -263
- package/dist/index.d.ts +221 -263
- package/dist/index.js +549 -209
- package/dist/index.mjs +542 -214
- package/dist/picker.d.mts +1 -1
- package/dist/picker.d.ts +1 -1
- package/dist/picker.js +1 -1
- package/dist/picker.mjs +1 -1
- package/package.json +2 -2
- package/src/components/Agenda.tsx +52 -11
- package/src/components/AllDayLane.tsx +22 -8
- package/src/components/Calendar.tsx +84 -5
- package/src/components/DefaultEvent.tsx +4 -0
- package/src/components/MonthList.tsx +52 -10
- package/src/components/MonthPager.tsx +45 -6
- package/src/components/MonthView.tsx +133 -74
- package/src/components/ResourceTimeline.tsx +276 -35
- package/src/components/TimeGrid.tsx +282 -162
- package/src/index.tsx +12 -4
- package/src/types.ts +16 -1
- package/src/utils/slots.ts +83 -0
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "date-fns";
|
|
18
18
|
import { memo, type ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
19
19
|
import {
|
|
20
|
+
type AccessibilityActionEvent,
|
|
20
21
|
type GestureResponderEvent,
|
|
21
22
|
Platform,
|
|
22
23
|
Pressable,
|
|
@@ -37,6 +38,7 @@ import Animated, {
|
|
|
37
38
|
useAnimatedScrollHandler,
|
|
38
39
|
useAnimatedStyle,
|
|
39
40
|
useDerivedValue,
|
|
41
|
+
useReducedMotion,
|
|
40
42
|
useSharedValue,
|
|
41
43
|
} from "react-native-reanimated";
|
|
42
44
|
import { useCalendarTheme } from "../theme";
|
|
@@ -65,6 +67,7 @@ import {
|
|
|
65
67
|
import { formatHour, layoutDayEvents, type PositionedEvent } from "@super-calendar/core";
|
|
66
68
|
import type { EventAccessibilityLabeler } from "@super-calendar/core";
|
|
67
69
|
import { type WeekdayFormat, weekdayFormatToken } from "@super-calendar/core";
|
|
70
|
+
import { SlotStylesProvider, type SlotStyleProps, useSlots } from "../utils/slots";
|
|
68
71
|
import { useWebGridZoom } from "../utils/useWebGridZoom";
|
|
69
72
|
import { useWebPagerKeys } from "../utils/useWebPagerKeys";
|
|
70
73
|
import { withEventAccessibilityLabel } from "../utils/withEventAccessibilityLabel";
|
|
@@ -197,6 +200,7 @@ function AnimatedEventBox<T>({
|
|
|
197
200
|
}: AnimatedEventBoxProps<T>) {
|
|
198
201
|
const RenderEventComponent = renderEvent;
|
|
199
202
|
const theme = useCalendarTheme();
|
|
203
|
+
const slot = useSlots<TimeGridSlot>();
|
|
200
204
|
// Drag-to-move/resize. Native picks the event up on long-press (so a tap or
|
|
201
205
|
// scroll isn't hijacked); web activates after a small drag threshold, so a
|
|
202
206
|
// plain click still selects and a right-click still opens a context menu.
|
|
@@ -372,16 +376,55 @@ function AnimatedEventBox<T>({
|
|
|
372
376
|
const handleLongPress =
|
|
373
377
|
!draggable && onLongPress ? () => onLongPress(positioned.event) : undefined;
|
|
374
378
|
|
|
379
|
+
// Dragging is gesture-only, so expose the same move/resize commit path as
|
|
380
|
+
// discrete screen-reader actions (VoiceOver/TalkBack invoke them from the
|
|
381
|
+
// actions menu). Steps are one `snapMinutes` unit, matching a drag snap.
|
|
382
|
+
const unit = (n: number) => `${n} minute${n === 1 ? "" : "s"}`;
|
|
383
|
+
const accessibilityActions = draggable
|
|
384
|
+
? [
|
|
385
|
+
{ name: "move-later", label: `Move ${unit(snapMinutes)} later` },
|
|
386
|
+
{ name: "move-earlier", label: `Move ${unit(snapMinutes)} earlier` },
|
|
387
|
+
...(resizable
|
|
388
|
+
? [
|
|
389
|
+
{ name: "extend", label: `Extend by ${unit(snapMinutes)}` },
|
|
390
|
+
{ name: "shrink", label: `Shorten by ${unit(snapMinutes)}` },
|
|
391
|
+
]
|
|
392
|
+
: []),
|
|
393
|
+
]
|
|
394
|
+
: undefined;
|
|
395
|
+
const handleAccessibilityAction = draggable
|
|
396
|
+
? (e: AccessibilityActionEvent) => {
|
|
397
|
+
switch (e.nativeEvent.actionName) {
|
|
398
|
+
case "move-later":
|
|
399
|
+
commitDrag(snapMinutes, snapMinutes);
|
|
400
|
+
break;
|
|
401
|
+
case "move-earlier":
|
|
402
|
+
commitDrag(-snapMinutes, -snapMinutes);
|
|
403
|
+
break;
|
|
404
|
+
case "extend":
|
|
405
|
+
commitDrag(0, snapMinutes);
|
|
406
|
+
break;
|
|
407
|
+
case "shrink":
|
|
408
|
+
commitDrag(0, -snapMinutes);
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
: undefined;
|
|
413
|
+
|
|
414
|
+
const eventSlot = slot("event", {
|
|
415
|
+
base: [styles.eventBox, { left, width }],
|
|
416
|
+
themed: theme.containers.timeGridEvent,
|
|
417
|
+
});
|
|
375
418
|
const box = (
|
|
376
|
-
<Animated.View
|
|
377
|
-
style={[styles.eventBox, { left, width }, boxStyle, theme.containers.timeGridEvent]}
|
|
378
|
-
>
|
|
419
|
+
<Animated.View {...eventSlot} style={[eventSlot.style, boxStyle]}>
|
|
379
420
|
<RenderEventComponent
|
|
380
421
|
event={positioned.event}
|
|
381
422
|
mode={mode}
|
|
382
423
|
boxHeight={boxHeight}
|
|
383
424
|
continuesBefore={positioned.continuesBefore}
|
|
384
425
|
continuesAfter={positioned.continuesAfter}
|
|
426
|
+
accessibilityActions={accessibilityActions}
|
|
427
|
+
onAccessibilityAction={handleAccessibilityAction}
|
|
385
428
|
onPress={handlePress}
|
|
386
429
|
onLongPress={handleLongPress}
|
|
387
430
|
/>
|
|
@@ -426,6 +469,7 @@ const HourRow = ({
|
|
|
426
469
|
hourComponent,
|
|
427
470
|
}: HourRowProps) => {
|
|
428
471
|
const theme = useCalendarTheme();
|
|
472
|
+
const slot = useSlots<TimeGridSlot>();
|
|
429
473
|
// Position via `top` (a layout prop), not a transform. The per-row layout pass
|
|
430
474
|
// as cellHeight animates keeps the ScrollView's content size in sync while
|
|
431
475
|
// zooming; a transform is composited and leaves the scroll range stale.
|
|
@@ -440,17 +484,21 @@ const HourRow = ({
|
|
|
440
484
|
<View style={{ width: hourColumnWidth }}>{hourComponent(hour, ampm)}</View>
|
|
441
485
|
) : (
|
|
442
486
|
<Text
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
]}
|
|
487
|
+
{...slot("hourLabel", {
|
|
488
|
+
base: [styles.hourLabel, { width: hourColumnWidth }],
|
|
489
|
+
themed: [theme.text.hourLabel, { color: theme.colors.textMuted }],
|
|
490
|
+
})}
|
|
448
491
|
allowFontScaling={false}
|
|
449
492
|
>
|
|
450
493
|
{label}
|
|
451
494
|
</Text>
|
|
452
495
|
)}
|
|
453
|
-
<View
|
|
496
|
+
<View
|
|
497
|
+
{...slot("gridLines", {
|
|
498
|
+
base: styles.hourLine,
|
|
499
|
+
themed: { backgroundColor: theme.colors.gridLine },
|
|
500
|
+
})}
|
|
501
|
+
/>
|
|
454
502
|
</Animated.View>
|
|
455
503
|
);
|
|
456
504
|
};
|
|
@@ -472,20 +520,16 @@ const TimeslotLine = ({
|
|
|
472
520
|
hourColumnWidth,
|
|
473
521
|
}: TimeslotLineProps) => {
|
|
474
522
|
const theme = useCalendarTheme();
|
|
523
|
+
const slot = useSlots<TimeGridSlot>();
|
|
475
524
|
const animatedStyle = useAnimatedStyle(
|
|
476
525
|
() => ({ top: (hour - minHour + fraction) * cellHeight.value }),
|
|
477
526
|
[hour, minHour, fraction],
|
|
478
527
|
);
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
{ left: hourColumnWidth, backgroundColor: theme.colors.gridLine },
|
|
485
|
-
animatedStyle,
|
|
486
|
-
]}
|
|
487
|
-
/>
|
|
488
|
-
);
|
|
528
|
+
const lineSlot = slot("gridLines", {
|
|
529
|
+
base: [styles.timeslotLine, styles.nonInteractive, { left: hourColumnWidth }],
|
|
530
|
+
themed: { backgroundColor: theme.colors.gridLine },
|
|
531
|
+
});
|
|
532
|
+
return <Animated.View {...lineSlot} style={[lineSlot.style, animatedStyle]} />;
|
|
489
533
|
};
|
|
490
534
|
|
|
491
535
|
type NowIndicatorProps = {
|
|
@@ -499,22 +543,17 @@ type NowIndicatorProps = {
|
|
|
499
543
|
|
|
500
544
|
const NowIndicator = ({ cellHeight, nowHours, minHour, left, width, color }: NowIndicatorProps) => {
|
|
501
545
|
const theme = useCalendarTheme();
|
|
546
|
+
const slot = useSlots<TimeGridSlot>();
|
|
502
547
|
const animatedStyle = useAnimatedStyle(
|
|
503
548
|
() => ({ top: (nowHours - minHour) * cellHeight.value }),
|
|
504
549
|
[nowHours, minHour],
|
|
505
550
|
);
|
|
506
551
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
{ left, width, backgroundColor: color },
|
|
513
|
-
animatedStyle,
|
|
514
|
-
theme.containers.nowIndicator,
|
|
515
|
-
]}
|
|
516
|
-
/>
|
|
517
|
-
);
|
|
552
|
+
const lineSlot = slot("nowIndicator", {
|
|
553
|
+
base: [styles.nowIndicator, styles.nonInteractive, { left, width }],
|
|
554
|
+
themed: [{ backgroundColor: color }, theme.containers.nowIndicator],
|
|
555
|
+
});
|
|
556
|
+
return <Animated.View {...lineSlot} style={[lineSlot.style, animatedStyle]} />;
|
|
518
557
|
};
|
|
519
558
|
|
|
520
559
|
type ShadeBandProps = {
|
|
@@ -538,6 +577,7 @@ const ShadeBand = ({
|
|
|
538
577
|
width,
|
|
539
578
|
color,
|
|
540
579
|
}: ShadeBandProps) => {
|
|
580
|
+
const slot = useSlots<TimeGridSlot>();
|
|
541
581
|
const animatedStyle = useAnimatedStyle(
|
|
542
582
|
() => ({
|
|
543
583
|
top: (startHour - minHour) * cellHeight.value,
|
|
@@ -545,15 +585,15 @@ const ShadeBand = ({
|
|
|
545
585
|
}),
|
|
546
586
|
[startHour, endHour, minHour],
|
|
547
587
|
);
|
|
588
|
+
const bandSlot = slot("businessHours", {
|
|
589
|
+
base: [styles.shadeBand, styles.nonInteractive, { left, width }],
|
|
590
|
+
themed: { backgroundColor: color },
|
|
591
|
+
});
|
|
548
592
|
return (
|
|
549
593
|
<Animated.View
|
|
550
594
|
testID="business-hours-shade"
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
styles.nonInteractive,
|
|
554
|
-
{ left, width, backgroundColor: color },
|
|
555
|
-
animatedStyle,
|
|
556
|
-
]}
|
|
595
|
+
{...bandSlot}
|
|
596
|
+
style={[bandSlot.style, animatedStyle]}
|
|
557
597
|
/>
|
|
558
598
|
);
|
|
559
599
|
};
|
|
@@ -654,6 +694,7 @@ function TimetablePageInner<T>({
|
|
|
654
694
|
onCreateEvent,
|
|
655
695
|
}: TimetablePageProps<T>) {
|
|
656
696
|
const theme = useCalendarTheme();
|
|
697
|
+
const slot = useSlots<TimeGridSlot>();
|
|
657
698
|
const scrollRef = useAnimatedRef<Animated.ScrollView>();
|
|
658
699
|
|
|
659
700
|
// The visible page tracks the live cellHeight (animates every pinch frame);
|
|
@@ -959,6 +1000,14 @@ function TimetablePageInner<T>({
|
|
|
959
1000
|
opacity: createActive.value,
|
|
960
1001
|
}));
|
|
961
1002
|
|
|
1003
|
+
const ghostSlot = slot("createGhost", {
|
|
1004
|
+
base: [styles.createGhost, { pointerEvents: "none" }],
|
|
1005
|
+
themed: {
|
|
1006
|
+
backgroundColor: theme.colors.eventBackground,
|
|
1007
|
+
borderColor: theme.colors.todayBackground,
|
|
1008
|
+
},
|
|
1009
|
+
});
|
|
1010
|
+
|
|
962
1011
|
// The tap/long-press layer behind events; wrapped in the create gesture when
|
|
963
1012
|
// drag-to-create is on. Hidden from screen readers (a convenience gesture).
|
|
964
1013
|
const cellLayer =
|
|
@@ -1012,20 +1061,24 @@ function TimetablePageInner<T>({
|
|
|
1012
1061
|
cellLayer
|
|
1013
1062
|
)}
|
|
1014
1063
|
|
|
1015
|
-
{days.map((day, dayIndex) =>
|
|
1016
|
-
isWeekend(day)
|
|
1064
|
+
{days.map((day, dayIndex) => {
|
|
1065
|
+
if (!isWeekend(day)) return null;
|
|
1066
|
+
const shadeSlot = slot("weekendShade", {
|
|
1067
|
+
base: [
|
|
1068
|
+
styles.weekendColumn,
|
|
1069
|
+
styles.nonInteractive,
|
|
1070
|
+
{ left: dayLeft(dayIndex), width: dayWidth },
|
|
1071
|
+
],
|
|
1072
|
+
themed: { backgroundColor: theme.colors.weekendBackground },
|
|
1073
|
+
});
|
|
1074
|
+
return (
|
|
1017
1075
|
<Animated.View
|
|
1018
1076
|
key={`weekend-${day.toISOString()}`}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
styles.nonInteractive,
|
|
1022
|
-
{ backgroundColor: theme.colors.weekendBackground },
|
|
1023
|
-
{ left: dayLeft(dayIndex), width: dayWidth },
|
|
1024
|
-
fullHeightStyle,
|
|
1025
|
-
]}
|
|
1077
|
+
{...shadeSlot}
|
|
1078
|
+
style={[shadeSlot.style, fullHeightStyle]}
|
|
1026
1079
|
/>
|
|
1027
|
-
)
|
|
1028
|
-
)}
|
|
1080
|
+
);
|
|
1081
|
+
})}
|
|
1029
1082
|
|
|
1030
1083
|
{calendarCellStyle
|
|
1031
1084
|
? days.map((day, dayIndex) => {
|
|
@@ -1062,18 +1115,19 @@ function TimetablePageInner<T>({
|
|
|
1062
1115
|
)
|
|
1063
1116
|
: null}
|
|
1064
1117
|
|
|
1065
|
-
{days.map((day, dayIndex) =>
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
{
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1118
|
+
{days.map((day, dayIndex) => {
|
|
1119
|
+
const separatorSlot = slot("daySeparator", {
|
|
1120
|
+
base: [styles.daySeparator, styles.nonInteractive, { left: dayLeft(dayIndex) }],
|
|
1121
|
+
themed: { backgroundColor: theme.colors.gridLine },
|
|
1122
|
+
});
|
|
1123
|
+
return (
|
|
1124
|
+
<Animated.View
|
|
1125
|
+
key={`separator-${day.toISOString()}`}
|
|
1126
|
+
{...separatorSlot}
|
|
1127
|
+
style={[separatorSlot.style, fullHeightStyle]}
|
|
1128
|
+
/>
|
|
1129
|
+
);
|
|
1130
|
+
})}
|
|
1077
1131
|
|
|
1078
1132
|
{hoursRange.map((hour) => (
|
|
1079
1133
|
<HourRow
|
|
@@ -1148,17 +1202,7 @@ function TimetablePageInner<T>({
|
|
|
1148
1202
|
) : null}
|
|
1149
1203
|
|
|
1150
1204
|
{createEnabled ? (
|
|
1151
|
-
<Animated.View
|
|
1152
|
-
style={[
|
|
1153
|
-
styles.createGhost,
|
|
1154
|
-
{ pointerEvents: "none" },
|
|
1155
|
-
{
|
|
1156
|
-
backgroundColor: theme.colors.eventBackground,
|
|
1157
|
-
borderColor: theme.colors.todayBackground,
|
|
1158
|
-
},
|
|
1159
|
-
createGhostStyle,
|
|
1160
|
-
]}
|
|
1161
|
-
/>
|
|
1205
|
+
<Animated.View {...ghostSlot} style={[ghostSlot.style, createGhostStyle]} />
|
|
1162
1206
|
) : null}
|
|
1163
1207
|
</Animated.View>
|
|
1164
1208
|
</Animated.ScrollView>
|
|
@@ -1169,8 +1213,38 @@ function TimetablePageInner<T>({
|
|
|
1169
1213
|
|
|
1170
1214
|
const TimetablePage = memo(TimetablePageInner) as typeof TimetablePageInner;
|
|
1171
1215
|
|
|
1216
|
+
/**
|
|
1217
|
+
* The styleable parts of {@link TimeGrid}. Mirrors the dom renderer's slot names
|
|
1218
|
+
* where the structure matches; `columnHeaderDateText`, `weekendShade` and
|
|
1219
|
+
* `daySeparator` are native-only (the dom grid styles those through its
|
|
1220
|
+
* `dayColumn`/`columnHeaderDate` elements). Slots rendered as Reanimated views
|
|
1221
|
+
* (`gridLines` sub-hour dividers, `businessHours`, `weekendShade`,
|
|
1222
|
+
* `daySeparator`, `event`, `nowIndicator`, `createGhost`) always honour the
|
|
1223
|
+
* `styles` map; their `className` reaches the element but needs a Tailwind
|
|
1224
|
+
* runtime that styles Animated components.
|
|
1225
|
+
*/
|
|
1226
|
+
export type TimeGridSlot =
|
|
1227
|
+
| "header"
|
|
1228
|
+
| "weekNumber"
|
|
1229
|
+
| "columnHeader"
|
|
1230
|
+
| "columnHeaderWeekday"
|
|
1231
|
+
| "columnHeaderDate"
|
|
1232
|
+
| "columnHeaderDateText"
|
|
1233
|
+
| "allDayLane"
|
|
1234
|
+
| "allDayLabel"
|
|
1235
|
+
| "allDayColumn"
|
|
1236
|
+
| "allDayEvent"
|
|
1237
|
+
| "hourLabel"
|
|
1238
|
+
| "gridLines"
|
|
1239
|
+
| "businessHours"
|
|
1240
|
+
| "weekendShade"
|
|
1241
|
+
| "daySeparator"
|
|
1242
|
+
| "event"
|
|
1243
|
+
| "nowIndicator"
|
|
1244
|
+
| "createGhost";
|
|
1245
|
+
|
|
1172
1246
|
/** Props for {@link TimeGrid}, the day/week timetable view. */
|
|
1173
|
-
export type TimeGridProps<T> = {
|
|
1247
|
+
export type TimeGridProps<T> = SlotStyleProps<TimeGridSlot> & {
|
|
1174
1248
|
mode: TimeGridMode;
|
|
1175
1249
|
/** Day columns to show in `custom` mode. Ignored by day/3days/week. Default 1. */
|
|
1176
1250
|
numberOfDays?: number;
|
|
@@ -1314,6 +1388,8 @@ function TimeGridInner<T>({
|
|
|
1314
1388
|
onPressDateHeader,
|
|
1315
1389
|
onChangeDate,
|
|
1316
1390
|
renderHeader,
|
|
1391
|
+
classNames,
|
|
1392
|
+
styles: styleOverrides,
|
|
1317
1393
|
}: TimeGridProps<T>): ReactElement {
|
|
1318
1394
|
// Guard against an inverted/out-of-range window so the grid never collapses.
|
|
1319
1395
|
const clampedMinHour = Math.max(0, Math.min(minHour, HOURS_PER_DAY - 1));
|
|
@@ -1568,6 +1644,10 @@ function TimeGridInner<T>({
|
|
|
1568
1644
|
);
|
|
1569
1645
|
useWebPagerKeys(swipeEnabled, goToPage);
|
|
1570
1646
|
|
|
1647
|
+
// Honour the OS "reduce motion" setting: the pager's one animated transition
|
|
1648
|
+
// (the snap-back below) becomes an instant jump when it's on.
|
|
1649
|
+
const reduceMotion = useReducedMotion();
|
|
1650
|
+
|
|
1571
1651
|
// Optionally snap the pager back to the active page after an empty-cell press
|
|
1572
1652
|
// (so tapping a far-swiped page returns to the committed date).
|
|
1573
1653
|
const handlePressCell = useMemo(() => {
|
|
@@ -1575,9 +1655,9 @@ function TimeGridInner<T>({
|
|
|
1575
1655
|
if (!resetPageOnPressCell) return onPressCell;
|
|
1576
1656
|
return (cellDate: Date) => {
|
|
1577
1657
|
onPressCell(cellDate);
|
|
1578
|
-
void listRef.current?.scrollToIndex({ index: activeIndex, animated:
|
|
1658
|
+
void listRef.current?.scrollToIndex({ index: activeIndex, animated: !reduceMotion });
|
|
1579
1659
|
};
|
|
1580
|
-
}, [onPressCell, resetPageOnPressCell, activeIndex]);
|
|
1660
|
+
}, [onPressCell, resetPageOnPressCell, activeIndex, reduceMotion]);
|
|
1581
1661
|
|
|
1582
1662
|
const snapToIndices = useMemo(() => pageDates.map((_, index) => index), [pageDates]);
|
|
1583
1663
|
const keyExtractorList = useCallback((item: Date) => item.toISOString(), []);
|
|
@@ -1681,67 +1761,69 @@ function TimeGridInner<T>({
|
|
|
1681
1761
|
const listExtraData = useMemo(() => ({ events, activeIndex }), [events, activeIndex]);
|
|
1682
1762
|
|
|
1683
1763
|
return (
|
|
1684
|
-
<
|
|
1685
|
-
{
|
|
1686
|
-
renderHeader(
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1764
|
+
<SlotStylesProvider classNames={classNames} styles={styleOverrides}>
|
|
1765
|
+
<View ref={containerRef} style={styles.container}>
|
|
1766
|
+
{renderHeader ? (
|
|
1767
|
+
renderHeader(headerDays)
|
|
1768
|
+
) : (
|
|
1769
|
+
<DefaultHeader
|
|
1770
|
+
days={headerDays}
|
|
1771
|
+
mode={mode}
|
|
1772
|
+
width={containerWidth}
|
|
1773
|
+
hourColumnWidth={hourColumnWidth}
|
|
1774
|
+
showWeekNumber={showWeekNumber}
|
|
1775
|
+
weekNumberPrefix={weekNumberPrefix}
|
|
1776
|
+
weekdayFormat={weekdayFormat}
|
|
1777
|
+
locale={locale}
|
|
1778
|
+
activeDate={activeDate}
|
|
1779
|
+
onPressDateHeader={onPressDateHeader}
|
|
1780
|
+
/>
|
|
1781
|
+
)}
|
|
1782
|
+
|
|
1783
|
+
{headerComponent}
|
|
1784
|
+
|
|
1785
|
+
<View
|
|
1786
|
+
style={styles.pager}
|
|
1787
|
+
onLayout={(event) => {
|
|
1788
|
+
setPageHeight(event.nativeEvent.layout.height);
|
|
1789
|
+
setContainerWidth(event.nativeEvent.layout.width);
|
|
1790
|
+
setMeasured(true);
|
|
1791
|
+
}}
|
|
1792
|
+
>
|
|
1793
|
+
<LegendList
|
|
1794
|
+
// Remount only on the seed→measured transition (see `measured`), not on
|
|
1795
|
+
// every height change, so a day↔week header-height difference resizes the
|
|
1796
|
+
// items in place instead of remounting and blanking the page.
|
|
1797
|
+
key={measured ? "grid" : "grid-seed"}
|
|
1798
|
+
ref={listRef}
|
|
1799
|
+
style={isWeb ? [styles.pagerList, styles.webNoScroll] : styles.pagerList}
|
|
1800
|
+
data={pageDates}
|
|
1801
|
+
extraData={listExtraData}
|
|
1802
|
+
horizontal
|
|
1803
|
+
recycleItems={false}
|
|
1804
|
+
keyExtractor={keyExtractorList}
|
|
1805
|
+
getFixedItemSize={getFixedItemSize}
|
|
1806
|
+
// On web LegendList ignores these RN scroll props (it leaks them to the
|
|
1807
|
+
// DOM as unknown attributes), so omit them there and disable horizontal
|
|
1808
|
+
// scroll via `webNoScroll`; paging is driven by the arrow keys instead.
|
|
1809
|
+
// Native: paging makes each swipe hard-stop at the adjacent page, while
|
|
1810
|
+
// `freeSwipe` lets momentum carry across pages and snap to a boundary.
|
|
1811
|
+
{...(isWeb
|
|
1812
|
+
? null
|
|
1813
|
+
: {
|
|
1814
|
+
scrollEnabled: swipeEnabled,
|
|
1815
|
+
pagingEnabled: !freeSwipe,
|
|
1816
|
+
snapToIndices: freeSwipe ? snapToIndices : undefined,
|
|
1817
|
+
})}
|
|
1818
|
+
initialScrollIndex={activeIndex}
|
|
1819
|
+
showsHorizontalScrollIndicator={false}
|
|
1820
|
+
viewabilityConfig={PAGE_VIEWABILITY}
|
|
1821
|
+
onViewableItemsChanged={handleViewableItemsChanged}
|
|
1822
|
+
renderItem={renderItem}
|
|
1823
|
+
/>
|
|
1824
|
+
</View>
|
|
1743
1825
|
</View>
|
|
1744
|
-
</
|
|
1826
|
+
</SlotStylesProvider>
|
|
1745
1827
|
);
|
|
1746
1828
|
}
|
|
1747
1829
|
|
|
@@ -1792,18 +1874,29 @@ const DefaultHeader = ({
|
|
|
1792
1874
|
onPressDateHeader,
|
|
1793
1875
|
}: DefaultHeaderProps) => {
|
|
1794
1876
|
const theme = useCalendarTheme();
|
|
1877
|
+
const slot = useSlots<TimeGridSlot>();
|
|
1795
1878
|
// Match the grid below: an hour-column spacer, then one column per day.
|
|
1796
1879
|
const dayWidth = (width - hourColumnWidth) / days.length;
|
|
1797
1880
|
|
|
1798
1881
|
return (
|
|
1799
|
-
<View
|
|
1882
|
+
<View
|
|
1883
|
+
{...slot("header", {
|
|
1884
|
+
base: styles.headerRow,
|
|
1885
|
+
themed: { borderBottomColor: theme.colors.gridLine },
|
|
1886
|
+
})}
|
|
1887
|
+
>
|
|
1800
1888
|
<View style={[styles.weekNumberGutter, { width: hourColumnWidth }]}>
|
|
1801
1889
|
{showWeekNumber && hourColumnWidth > 0 && days[0] ? (
|
|
1802
1890
|
<Text
|
|
1803
|
-
|
|
1891
|
+
{...slot("weekNumber", {
|
|
1892
|
+
themed: [theme.text.hourLabel, { color: theme.colors.textMuted }],
|
|
1893
|
+
})}
|
|
1804
1894
|
allowFontScaling={false}
|
|
1805
1895
|
>
|
|
1806
|
-
{
|
|
1896
|
+
{/* Reference the visible Thursday: an ISO week is defined by its Thursday,
|
|
1897
|
+
so a Sunday-start week still shows the week number its Mon–Sat body
|
|
1898
|
+
belongs to. */}
|
|
1899
|
+
{`${weekNumberPrefix}${getISOWeek(days.find((d) => d.getDay() === 4) ?? days[0])}`}
|
|
1807
1900
|
</Text>
|
|
1808
1901
|
) : null}
|
|
1809
1902
|
</View>
|
|
@@ -1842,51 +1935,78 @@ const DayHeader = ({
|
|
|
1842
1935
|
onPressDateHeader,
|
|
1843
1936
|
}: DayHeaderProps) => {
|
|
1844
1937
|
const theme = useCalendarTheme();
|
|
1938
|
+
const slot = useSlots<TimeGridSlot>();
|
|
1845
1939
|
const isToday = getIsToday(day);
|
|
1846
1940
|
// Highlight the chosen `activeDate` when supplied, else the real today.
|
|
1847
1941
|
const isHighlighted = activeDate ? isSameCalendarDay(day, activeDate) : isToday;
|
|
1848
1942
|
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1943
|
+
// One accessible name for the whole header (the weekday + number below are
|
|
1944
|
+
// decorative). `accessible` groups the children so a screen reader announces
|
|
1945
|
+
// this once, not label-by-label.
|
|
1946
|
+
const accessibilityLabel = `${format(day, "EEEE d MMMM", { locale })}${isToday ? ", today" : ""}`;
|
|
1947
|
+
const headerSlot = slot("columnHeader", {
|
|
1948
|
+
base: [styles.dayHeader, { width }],
|
|
1949
|
+
themed: theme.containers.columnHeader,
|
|
1950
|
+
});
|
|
1951
|
+
// Mirrors the dom renderer's header: the muted weekday label sits above the day
|
|
1952
|
+
// number, and the number's circle fills for today / the active date. Theme text
|
|
1953
|
+
// merges after the muted colour so a themed colour wins.
|
|
1954
|
+
const content = (
|
|
1955
|
+
<>
|
|
1862
1956
|
<Text
|
|
1863
|
-
|
|
1957
|
+
{...slot("columnHeaderWeekday", {
|
|
1958
|
+
themed: [{ color: theme.colors.textMuted }, theme.text.columnHeaderWeekday],
|
|
1959
|
+
})}
|
|
1864
1960
|
allowFontScaling={false}
|
|
1865
1961
|
>
|
|
1866
1962
|
{format(day, weekdayFormatToken(weekdayFormat), { locale })}
|
|
1867
1963
|
</Text>
|
|
1868
1964
|
<View
|
|
1869
1965
|
testID="column-header-badge"
|
|
1870
|
-
|
|
1871
|
-
styles.dayHeaderBadge,
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1966
|
+
{...slot("columnHeaderDate", {
|
|
1967
|
+
base: styles.dayHeaderBadge,
|
|
1968
|
+
themed: [
|
|
1969
|
+
theme.containers.columnHeaderBadge,
|
|
1970
|
+
isHighlighted && { backgroundColor: theme.colors.todayBackground },
|
|
1971
|
+
],
|
|
1972
|
+
})}
|
|
1875
1973
|
>
|
|
1876
1974
|
<Text
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1975
|
+
{...slot("columnHeaderDateText", {
|
|
1976
|
+
themed: [
|
|
1977
|
+
theme.text.dayNumber,
|
|
1978
|
+
// The state colour stays last so a themed dayNumber can't break the
|
|
1979
|
+
// today/active contrast.
|
|
1980
|
+
{ color: isHighlighted ? theme.colors.todayText : theme.colors.text },
|
|
1981
|
+
],
|
|
1982
|
+
})}
|
|
1883
1983
|
allowFontScaling={false}
|
|
1884
|
-
{...(isToday && { accessibilityLabel: `Today, ${day.getDate()}` })}
|
|
1885
1984
|
>
|
|
1886
1985
|
{day.getDate()}
|
|
1887
1986
|
</Text>
|
|
1888
1987
|
</View>
|
|
1988
|
+
</>
|
|
1989
|
+
);
|
|
1990
|
+
// Interactive → a labelled button. Otherwise a labelled `header`, so screen
|
|
1991
|
+
// readers still perceive (and announce) which day each column is.
|
|
1992
|
+
return onPressDateHeader ? (
|
|
1993
|
+
<Pressable
|
|
1994
|
+
{...headerSlot}
|
|
1995
|
+
onPress={() => onPressDateHeader(day)}
|
|
1996
|
+
accessibilityRole="button"
|
|
1997
|
+
accessibilityLabel={accessibilityLabel}
|
|
1998
|
+
>
|
|
1999
|
+
{content}
|
|
1889
2000
|
</Pressable>
|
|
2001
|
+
) : (
|
|
2002
|
+
<View
|
|
2003
|
+
{...headerSlot}
|
|
2004
|
+
accessible
|
|
2005
|
+
accessibilityRole="header"
|
|
2006
|
+
accessibilityLabel={accessibilityLabel}
|
|
2007
|
+
>
|
|
2008
|
+
{content}
|
|
2009
|
+
</View>
|
|
1890
2010
|
);
|
|
1891
2011
|
};
|
|
1892
2012
|
|