@super-calendar/native 2.0.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/LICENSE +21 -0
- package/dist/DefaultMonthEvent-86XCNCge.d.ts +326 -0
- package/dist/DefaultMonthEvent-CpRoOloh.d.mts +326 -0
- package/dist/MonthList-BCfN-UGz.js +1089 -0
- package/dist/MonthList-CyrEJ_U6.mjs +1030 -0
- package/dist/index.d.mts +386 -0
- package/dist/index.d.ts +386 -0
- package/dist/index.js +1700 -0
- package/dist/index.mjs +1543 -0
- package/dist/picker.d.mts +3 -0
- package/dist/picker.d.ts +3 -0
- package/dist/picker.js +96 -0
- package/dist/picker.mjs +3 -0
- package/package.json +92 -0
- package/src/components/Agenda.tsx +133 -0
- package/src/components/AllDayLane.tsx +98 -0
- package/src/components/Calendar.tsx +456 -0
- package/src/components/DefaultEvent.tsx +223 -0
- package/src/components/DefaultMonthEvent.tsx +105 -0
- package/src/components/MonthList.tsx +570 -0
- package/src/components/MonthPager.tsx +377 -0
- package/src/components/MonthView.tsx +518 -0
- package/src/components/TimeGrid.tsx +1943 -0
- package/src/index.tsx +70 -0
- package/src/picker.tsx +55 -0
- package/src/theme.ts +86 -0
- package/src/types.ts +62 -0
- package/src/utils/useWebGridZoom.ts +59 -0
- package/src/utils/useWebPagerKeys.ts +56 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { format, type Locale, isSameMonth, startOfDay } from "date-fns";
|
|
2
|
+
import { memo, useMemo, useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
type LayoutChangeEvent,
|
|
5
|
+
Platform,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
type StyleProp,
|
|
8
|
+
Text,
|
|
9
|
+
TouchableOpacity,
|
|
10
|
+
View,
|
|
11
|
+
type ViewStyle,
|
|
12
|
+
} from "react-native";
|
|
13
|
+
|
|
14
|
+
// Web drag-to-select relays cell pointer events up to MonthList; native drag is
|
|
15
|
+
// driven by a list-level pan there instead.
|
|
16
|
+
const isWeb = Platform.OS === "web";
|
|
17
|
+
import { useCalendarTheme } from "../theme";
|
|
18
|
+
import type { CalendarEvent, EventKeyExtractor, RenderEvent, WeekStartsOn } from "../types";
|
|
19
|
+
import { type DateRange, daySelectionState, useCalendarSelection } from "@super-calendar/core";
|
|
20
|
+
import { dayBadgeKind, rangeBandKind } from "@super-calendar/core";
|
|
21
|
+
import {
|
|
22
|
+
buildMonthWeeks,
|
|
23
|
+
getIsToday,
|
|
24
|
+
getWeekDays,
|
|
25
|
+
isSameCalendarDay,
|
|
26
|
+
isWeekend,
|
|
27
|
+
} from "@super-calendar/core";
|
|
28
|
+
import { monthEventCapacity, monthVisibleCount } from "@super-calendar/core";
|
|
29
|
+
import { compareDayEvents, groupEventsByDay, isAllDayEvent } from "@super-calendar/core";
|
|
30
|
+
|
|
31
|
+
// Day-cell metrics, mirrored from the styles below, used to estimate how many
|
|
32
|
+
// event chips fit when auto-fitting `maxVisibleEventCount`.
|
|
33
|
+
const DAY_CELL_PADDING_TOP = 4;
|
|
34
|
+
const DATE_BADGE_HEIGHT = 24;
|
|
35
|
+
// Vertical centre of the date badge, where the range band is centered.
|
|
36
|
+
const BAND_CENTER_Y = DAY_CELL_PADDING_TOP + DATE_BADGE_HEIGHT / 2;
|
|
37
|
+
const CELL_ROW_GAP = 2;
|
|
38
|
+
const CHIP_PADDING_V = 2;
|
|
39
|
+
// Pre-measure fallback so the first paint isn't empty or overflowing.
|
|
40
|
+
const FALLBACK_VISIBLE_COUNT = 3;
|
|
41
|
+
|
|
42
|
+
const numericStyle = (value: number | string | undefined, fallback: number) =>
|
|
43
|
+
typeof value === "number" ? value : fallback;
|
|
44
|
+
|
|
45
|
+
export type MonthViewProps<T> = {
|
|
46
|
+
date: Date;
|
|
47
|
+
events: CalendarEvent<T>[];
|
|
48
|
+
/**
|
|
49
|
+
* Max event chips per day cell. Omit to auto-fit as many as the cell height
|
|
50
|
+
* allows (the default); set a number for a fixed cap. Extra events collapse
|
|
51
|
+
* into a "+N more" label. Auto-fit assumes the built-in chip size — pass an
|
|
52
|
+
* explicit value when using a custom `renderEvent`.
|
|
53
|
+
*/
|
|
54
|
+
maxVisibleEventCount?: number;
|
|
55
|
+
weekStartsOn: WeekStartsOn;
|
|
56
|
+
locale?: Locale;
|
|
57
|
+
/** Sort each day's events by start time before slicing. Default true. */
|
|
58
|
+
sortedMonthView?: boolean;
|
|
59
|
+
/** Template for the overflow label; `{moreCount}` is replaced. Default "{moreCount} More". */
|
|
60
|
+
moreLabel?: string;
|
|
61
|
+
/** Show dimmed days from adjacent months in the grid. Default true. */
|
|
62
|
+
showAdjacentMonths?: boolean;
|
|
63
|
+
/** Ignore taps on month-cell events (day-cell taps still fire). Default false. */
|
|
64
|
+
disableMonthEventCellPress?: boolean;
|
|
65
|
+
/** Reverse the day order within each week (right-to-left). Default false. */
|
|
66
|
+
isRTL?: boolean;
|
|
67
|
+
/** Always render six week rows, for a fixed-height grid. Default false. */
|
|
68
|
+
showSixWeeks?: boolean;
|
|
69
|
+
/** Render the "MMMM yyyy" title above the grid. Default true. */
|
|
70
|
+
showTitle?: boolean;
|
|
71
|
+
/** Render the weekday-label header row above the grid. Default true. */
|
|
72
|
+
showWeekdays?: boolean;
|
|
73
|
+
/** Highlight this date instead of the real "today". */
|
|
74
|
+
activeDate?: Date;
|
|
75
|
+
/** Days drawn as selected (a filled badge), in the month grid. */
|
|
76
|
+
selectedDates?: Date[];
|
|
77
|
+
/** A selected span: endpoints get a filled badge, the span gets the range band. */
|
|
78
|
+
selectedRange?: DateRange;
|
|
79
|
+
/**
|
|
80
|
+
* Fill the whole cell with the range band instead of the default centered
|
|
81
|
+
* rounded "pill" strip. Default false.
|
|
82
|
+
*/
|
|
83
|
+
fillCellOnSelection?: boolean;
|
|
84
|
+
/** Earliest selectable day (inclusive); earlier days render disabled. */
|
|
85
|
+
minDate?: Date;
|
|
86
|
+
/** Latest selectable day (inclusive); later days render disabled. */
|
|
87
|
+
maxDate?: Date;
|
|
88
|
+
/** Return true to render a specific day disabled (dimmed, taps ignored). */
|
|
89
|
+
isDateDisabled?: (date: Date) => boolean;
|
|
90
|
+
/** Web drag-to-select relay: a pointer pressed down on this day's cell. */
|
|
91
|
+
onDayPointerDown?: (date: Date) => void;
|
|
92
|
+
/** Web drag-to-select relay: a pressed pointer entered this day's cell. */
|
|
93
|
+
onDayPointerEnter?: (date: Date) => void;
|
|
94
|
+
/** Per-date style merged onto the day cell. */
|
|
95
|
+
calendarCellStyle?: (date: Date) => StyleProp<ViewStyle>;
|
|
96
|
+
renderEvent: RenderEvent<T>;
|
|
97
|
+
keyExtractor: EventKeyExtractor<T>;
|
|
98
|
+
onPressDay?: (date: Date) => void;
|
|
99
|
+
onLongPressDay?: (date: Date) => void;
|
|
100
|
+
onPressEvent: (event: CalendarEvent<T>) => void;
|
|
101
|
+
onLongPressEvent?: (event: CalendarEvent<T>) => void;
|
|
102
|
+
onPressMore?: (events: CalendarEvent<T>[], date: Date) => void;
|
|
103
|
+
/**
|
|
104
|
+
* Replace the default date badge in each day cell. Receives the day; return
|
|
105
|
+
* your own date label. Event chips and the "+N more" label still render below.
|
|
106
|
+
*/
|
|
107
|
+
renderCustomDateForMonth?: (date: Date) => React.ReactNode;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
function MonthViewInner<T>({
|
|
111
|
+
date,
|
|
112
|
+
events,
|
|
113
|
+
maxVisibleEventCount,
|
|
114
|
+
weekStartsOn,
|
|
115
|
+
locale,
|
|
116
|
+
sortedMonthView = true,
|
|
117
|
+
moreLabel = "{moreCount} More",
|
|
118
|
+
showAdjacentMonths = true,
|
|
119
|
+
disableMonthEventCellPress = false,
|
|
120
|
+
isRTL = false,
|
|
121
|
+
showSixWeeks = false,
|
|
122
|
+
showTitle = true,
|
|
123
|
+
showWeekdays = true,
|
|
124
|
+
activeDate,
|
|
125
|
+
selectedDates: selectedDatesProp,
|
|
126
|
+
selectedRange: selectedRangeProp,
|
|
127
|
+
fillCellOnSelection = false,
|
|
128
|
+
minDate: minDateProp,
|
|
129
|
+
maxDate: maxDateProp,
|
|
130
|
+
isDateDisabled: isDateDisabledProp,
|
|
131
|
+
calendarCellStyle,
|
|
132
|
+
renderEvent,
|
|
133
|
+
keyExtractor,
|
|
134
|
+
onPressDay,
|
|
135
|
+
onLongPressDay,
|
|
136
|
+
onPressEvent,
|
|
137
|
+
onLongPressEvent,
|
|
138
|
+
onPressMore,
|
|
139
|
+
renderCustomDateForMonth,
|
|
140
|
+
onDayPointerDown,
|
|
141
|
+
onDayPointerEnter,
|
|
142
|
+
}: MonthViewProps<T>) {
|
|
143
|
+
const theme = useCalendarTheme();
|
|
144
|
+
// Selection comes from context (so cached pages still repaint), but explicit
|
|
145
|
+
// props win for direct/standalone use of MonthView.
|
|
146
|
+
const selection = useCalendarSelection();
|
|
147
|
+
const selectedDates = selectedDatesProp ?? selection.selectedDates;
|
|
148
|
+
const selectedRange = selectedRangeProp ?? selection.selectedRange;
|
|
149
|
+
const minDate = minDateProp ?? selection.minDate;
|
|
150
|
+
const maxDate = maxDateProp ?? selection.maxDate;
|
|
151
|
+
const isDateDisabled = isDateDisabledProp ?? selection.isDateDisabled;
|
|
152
|
+
const RenderEventComponent = renderEvent;
|
|
153
|
+
// Measured grid height, used to auto-fit the event chips per cell.
|
|
154
|
+
const [gridHeight, setGridHeight] = useState(0);
|
|
155
|
+
// Web-only hover highlight on the day badge (mouse pointers); stays null on
|
|
156
|
+
// touch/native, so it never re-renders there. Mirrors the dom renderer.
|
|
157
|
+
const [hoveredKey, setHoveredKey] = useState<string | null>(null);
|
|
158
|
+
// Picker: which day is being pressed, so the tap dims only its badge (the circle),
|
|
159
|
+
// not the whole cell background. Stays null in the events calendar.
|
|
160
|
+
const [pressedKey, setPressedKey] = useState<string | null>(null);
|
|
161
|
+
|
|
162
|
+
const weeks = useMemo(
|
|
163
|
+
() => buildMonthWeeks(date, weekStartsOn, { showSixWeeks, isRTL }),
|
|
164
|
+
[date, weekStartsOn, isRTL, showSixWeeks],
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Weekday labels for the header row (any week works; reuse this month). Reversed
|
|
168
|
+
// in RTL so they line up with the mirrored day columns.
|
|
169
|
+
const weekdayLabels = useMemo(() => {
|
|
170
|
+
const days = getWeekDays(date, weekStartsOn);
|
|
171
|
+
return isRTL ? days.reverse() : days;
|
|
172
|
+
}, [date, weekStartsOn, isRTL]);
|
|
173
|
+
|
|
174
|
+
// How many chips fit per cell: a fixed cap when `maxVisibleEventCount` is set,
|
|
175
|
+
// else derived from the measured cell height and the (default) chip metrics.
|
|
176
|
+
const capacity = useMemo(() => {
|
|
177
|
+
if (maxVisibleEventCount != null) {
|
|
178
|
+
return { full: maxVisibleEventCount, withMore: maxVisibleEventCount };
|
|
179
|
+
}
|
|
180
|
+
if (gridHeight <= 0 || weeks.length === 0) {
|
|
181
|
+
return { full: FALLBACK_VISIBLE_COUNT, withMore: FALLBACK_VISIBLE_COUNT };
|
|
182
|
+
}
|
|
183
|
+
const rowHeight = gridHeight / weeks.length;
|
|
184
|
+
const fontSize = numericStyle(theme.text.eventTitle.fontSize, 12);
|
|
185
|
+
const lineHeight = numericStyle(theme.text.eventTitle.lineHeight, Math.ceil(fontSize * 1.3));
|
|
186
|
+
const chipRowHeight = lineHeight + CHIP_PADDING_V * 2 + CELL_ROW_GAP;
|
|
187
|
+
const moreFontSize = numericStyle(theme.text.more.fontSize, 11);
|
|
188
|
+
const moreRowHeight = Math.ceil(moreFontSize * 1.3) + CELL_ROW_GAP;
|
|
189
|
+
const available = rowHeight - DAY_CELL_PADDING_TOP - DATE_BADGE_HEIGHT;
|
|
190
|
+
return monthEventCapacity(available, chipRowHeight, moreRowHeight);
|
|
191
|
+
}, [maxVisibleEventCount, gridHeight, weeks.length, theme]);
|
|
192
|
+
|
|
193
|
+
// Group events by calendar day once per `events` change (shared with the dom
|
|
194
|
+
// renderer via core's `groupEventsByDay`), rather than scanning the whole list
|
|
195
|
+
// inside every one of the (up to) 42 day cells on each render. Multi-day events
|
|
196
|
+
// are indexed under every day they span.
|
|
197
|
+
const eventsByDay = useMemo(() => {
|
|
198
|
+
const map = groupEventsByDay(events);
|
|
199
|
+
if (sortedMonthView) {
|
|
200
|
+
// All-day events head the day, then timed events by start (shared with dom).
|
|
201
|
+
for (const list of map.values()) list.sort(compareDayEvents);
|
|
202
|
+
}
|
|
203
|
+
return map;
|
|
204
|
+
}, [events, sortedMonthView]);
|
|
205
|
+
|
|
206
|
+
// Draw the day-cell grid only for an events calendar; the events-free date
|
|
207
|
+
// picker reads cleaner without it (matching the dom renderer).
|
|
208
|
+
const showGrid = events.length > 0;
|
|
209
|
+
|
|
210
|
+
const renderDay = (day: Date) => {
|
|
211
|
+
const isCurrentMonth = isSameMonth(day, date);
|
|
212
|
+
|
|
213
|
+
// Blank out adjacent-month days when they're hidden, keeping the grid shape.
|
|
214
|
+
if (!isCurrentMonth && !showAdjacentMonths) {
|
|
215
|
+
return (
|
|
216
|
+
<View
|
|
217
|
+
key={day.toISOString()}
|
|
218
|
+
style={[
|
|
219
|
+
styles.dayCell,
|
|
220
|
+
showGrid && {
|
|
221
|
+
borderTopWidth: StyleSheet.hairlineWidth,
|
|
222
|
+
borderRightWidth: StyleSheet.hairlineWidth,
|
|
223
|
+
borderColor: theme.colors.gridLine,
|
|
224
|
+
},
|
|
225
|
+
// No weekend tint on blank placeholders, so the shading doesn't bleed
|
|
226
|
+
// into the empty cells of non-existent days.
|
|
227
|
+
]}
|
|
228
|
+
/>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const dayEvents = eventsByDay.get(startOfDay(day).toISOString()) ?? [];
|
|
233
|
+
const isToday = getIsToday(day);
|
|
234
|
+
// Highlight the chosen `activeDate` when supplied, else the real today.
|
|
235
|
+
const isHighlighted = activeDate ? isSameCalendarDay(day, activeDate) : isToday;
|
|
236
|
+
// Selection band wins over the weekend tint; the today badge shows unless the
|
|
237
|
+
// day is selected. Shared with the headless grid so they never diverge.
|
|
238
|
+
const { isDisabled, isSelected, isInRange, isRangeStart, isRangeEnd } = daySelectionState(
|
|
239
|
+
day,
|
|
240
|
+
{ selectedDates, selectedRange },
|
|
241
|
+
{ minDate, maxDate, isDateDisabled },
|
|
242
|
+
);
|
|
243
|
+
const visibleCount = monthVisibleCount(dayEvents.length, capacity);
|
|
244
|
+
const hiddenCount = dayEvents.length - visibleCount;
|
|
245
|
+
|
|
246
|
+
// The range shows as a band behind the days; endpoints and discrete selected
|
|
247
|
+
// days get a filled badge on top. Today's badge wins when it coincides. The
|
|
248
|
+
// band/badge decisions come from core so both renderers can't disagree.
|
|
249
|
+
const isFilledBadge = dayBadgeKind({ isSelected }, isHighlighted) !== "none";
|
|
250
|
+
const hasBand =
|
|
251
|
+
rangeBandKind({ isInRange, isRangeStart, isRangeEnd }, fillCellOnSelection) !== "none";
|
|
252
|
+
const dayKey = day.toISOString();
|
|
253
|
+
// A hovered, non-filled day gets the subtle badge highlight on the web, but only
|
|
254
|
+
// in the events-free picker. The events calendar (month and list views) has no
|
|
255
|
+
// hover, matching the dom renderer (its events-mode cell omits it).
|
|
256
|
+
const isHovered = isWeb && !isDisabled && !showGrid && hoveredKey === dayKey;
|
|
257
|
+
const dateColor = isDisabled
|
|
258
|
+
? theme.colors.textDisabled
|
|
259
|
+
: isFilledBadge
|
|
260
|
+
? isHighlighted
|
|
261
|
+
? theme.colors.todayText
|
|
262
|
+
: theme.colors.selectedText
|
|
263
|
+
: isCurrentMonth
|
|
264
|
+
? theme.colors.text
|
|
265
|
+
: theme.colors.textDisabled;
|
|
266
|
+
|
|
267
|
+
// Disabled days ignore taps; pass the guards through so a press never fires.
|
|
268
|
+
const handlePressDay = isDisabled || !onPressDay ? undefined : () => onPressDay(day);
|
|
269
|
+
const handleLongPressDay =
|
|
270
|
+
isDisabled || !onLongPressDay ? undefined : () => onLongPressDay(day);
|
|
271
|
+
|
|
272
|
+
// Summarise the cell for screen readers: full date, today marker, and how
|
|
273
|
+
// many events it holds (the chips inside are grouped under this cell).
|
|
274
|
+
const eventCount = dayEvents.length;
|
|
275
|
+
const accessibilityLabel = `${format(day, "EEEE, d LLLL yyyy", { locale })}${isToday ? ", today" : ""}${isSelected ? ", selected" : ""}${isDisabled ? ", unavailable" : ""}, ${eventCount} ${eventCount === 1 ? "event" : "events"}`;
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<TouchableOpacity
|
|
279
|
+
key={day.toISOString()}
|
|
280
|
+
style={[
|
|
281
|
+
styles.dayCell,
|
|
282
|
+
// Events mode mirrors the dom renderer: left-aligned cell content with
|
|
283
|
+
// the date badge in the top-right. The picker (no grid) stays centered
|
|
284
|
+
// so the selection range band lines up with the centered badge.
|
|
285
|
+
showGrid && styles.dayCellEvents,
|
|
286
|
+
showGrid && {
|
|
287
|
+
borderTopWidth: StyleSheet.hairlineWidth,
|
|
288
|
+
borderRightWidth: StyleSheet.hairlineWidth,
|
|
289
|
+
borderColor: theme.colors.gridLine,
|
|
290
|
+
},
|
|
291
|
+
isWeekend(day) && { backgroundColor: theme.colors.weekendBackground },
|
|
292
|
+
calendarCellStyle?.(day),
|
|
293
|
+
]}
|
|
294
|
+
// In the picker, don't dim the whole cell on press: the tap should show on
|
|
295
|
+
// the badge (the circle) only, not the cell background. `onPressIn/Out` drive
|
|
296
|
+
// the badge's own opacity below. The events calendar keeps the default
|
|
297
|
+
// whole-cell press feedback (a tap there opens the day).
|
|
298
|
+
activeOpacity={showGrid ? 0.2 : 1}
|
|
299
|
+
{...(showGrid
|
|
300
|
+
? null
|
|
301
|
+
: {
|
|
302
|
+
onPressIn: () => setPressedKey(dayKey),
|
|
303
|
+
onPressOut: () => setPressedKey((k) => (k === dayKey ? null : k)),
|
|
304
|
+
})}
|
|
305
|
+
onPress={handlePressDay}
|
|
306
|
+
onLongPress={handleLongPressDay}
|
|
307
|
+
disabled={isDisabled || (!onPressDay && !onLongPressDay)}
|
|
308
|
+
// Web only: track hover for the badge highlight, and (when drag-select is
|
|
309
|
+
// wired) relay pointer down/enter so MonthList can extend a range as the
|
|
310
|
+
// pressed pointer sweeps across cells. Native uses a pan, no hover.
|
|
311
|
+
{...(isWeb && !isDisabled
|
|
312
|
+
? {
|
|
313
|
+
onPointerEnter: () => {
|
|
314
|
+
// Hover highlight is picker-only; the events calendar matches dom (none).
|
|
315
|
+
if (!showGrid) setHoveredKey(dayKey);
|
|
316
|
+
if (onDayPointerDown) onDayPointerEnter?.(day);
|
|
317
|
+
},
|
|
318
|
+
onPointerLeave: () => {
|
|
319
|
+
if (!showGrid) setHoveredKey((k) => (k === dayKey ? null : k));
|
|
320
|
+
},
|
|
321
|
+
...(onDayPointerDown ? { onPointerDown: () => onDayPointerDown(day) } : {}),
|
|
322
|
+
}
|
|
323
|
+
: null)}
|
|
324
|
+
// A cell, not a button — it contains the event-chip buttons, and a nested
|
|
325
|
+
// <button> is invalid HTML on web. `cell` is also closer to the correct
|
|
326
|
+
// semantics for a calendar day than `button`.
|
|
327
|
+
role="cell"
|
|
328
|
+
// On web, the events calendar's day cells are not tab stops, so keyboard
|
|
329
|
+
// focus moves through the event chips (real buttons) only, not every empty
|
|
330
|
+
// day — matching the dom renderer. A pointer tap still opens the day. The
|
|
331
|
+
// events-free picker layout (no grid) stays keyboard-navigable for selection.
|
|
332
|
+
{...(isWeb && showGrid ? { focusable: false } : null)}
|
|
333
|
+
accessibilityLabel={accessibilityLabel}
|
|
334
|
+
>
|
|
335
|
+
{hasBand ? (
|
|
336
|
+
<View
|
|
337
|
+
testID="month-range-band"
|
|
338
|
+
style={[
|
|
339
|
+
styles.rangeBand,
|
|
340
|
+
{ pointerEvents: "none" },
|
|
341
|
+
{ backgroundColor: theme.colors.rangeBackground },
|
|
342
|
+
fillCellOnSelection
|
|
343
|
+
? { top: 0, bottom: 0 }
|
|
344
|
+
: { top: BAND_CENTER_Y - theme.rangeBandHeight / 2, height: theme.rangeBandHeight },
|
|
345
|
+
// Cap the pill at the endpoint circle (half a badge in from centre)
|
|
346
|
+
// instead of spilling to the cell edge, so no band shows beside it.
|
|
347
|
+
!fillCellOnSelection &&
|
|
348
|
+
isRangeStart && {
|
|
349
|
+
left: "50%",
|
|
350
|
+
marginLeft: -DATE_BADGE_HEIGHT / 2,
|
|
351
|
+
borderTopLeftRadius: theme.rangeBandHeight / 2,
|
|
352
|
+
borderBottomLeftRadius: theme.rangeBandHeight / 2,
|
|
353
|
+
},
|
|
354
|
+
!fillCellOnSelection &&
|
|
355
|
+
isRangeEnd && {
|
|
356
|
+
right: "50%",
|
|
357
|
+
marginRight: -DATE_BADGE_HEIGHT / 2,
|
|
358
|
+
borderTopRightRadius: theme.rangeBandHeight / 2,
|
|
359
|
+
borderBottomRightRadius: theme.rangeBandHeight / 2,
|
|
360
|
+
},
|
|
361
|
+
]}
|
|
362
|
+
/>
|
|
363
|
+
) : null}
|
|
364
|
+
{renderCustomDateForMonth ? (
|
|
365
|
+
renderCustomDateForMonth(day)
|
|
366
|
+
) : (
|
|
367
|
+
<View
|
|
368
|
+
style={[
|
|
369
|
+
styles.dateBadge,
|
|
370
|
+
showGrid && styles.dateBadgeEvents,
|
|
371
|
+
isFilledBadge && {
|
|
372
|
+
backgroundColor: isHighlighted
|
|
373
|
+
? theme.colors.todayBackground
|
|
374
|
+
: theme.colors.selectedBackground,
|
|
375
|
+
borderRadius: theme.todayBadgeRadius,
|
|
376
|
+
},
|
|
377
|
+
isHovered &&
|
|
378
|
+
!isFilledBadge && {
|
|
379
|
+
backgroundColor: theme.colors.hoverBackground,
|
|
380
|
+
borderRadius: theme.todayBadgeRadius,
|
|
381
|
+
},
|
|
382
|
+
// Tap feedback lives on the badge, not the cell (picker only).
|
|
383
|
+
!showGrid && pressedKey === dayKey && { opacity: 0.2 },
|
|
384
|
+
]}
|
|
385
|
+
>
|
|
386
|
+
<Text style={[theme.text.dateCell, { color: dateColor }]} allowFontScaling={false}>
|
|
387
|
+
{format(day, "d")}
|
|
388
|
+
</Text>
|
|
389
|
+
</View>
|
|
390
|
+
)}
|
|
391
|
+
{dayEvents.slice(0, visibleCount).map((event, index) => (
|
|
392
|
+
<View key={keyExtractor(event, index)} style={styles.monthEvent}>
|
|
393
|
+
<RenderEventComponent
|
|
394
|
+
event={event}
|
|
395
|
+
mode="month"
|
|
396
|
+
isAllDay={isAllDayEvent(event)}
|
|
397
|
+
onPress={disableMonthEventCellPress ? () => {} : () => onPressEvent(event)}
|
|
398
|
+
onLongPress={
|
|
399
|
+
disableMonthEventCellPress || !onLongPressEvent
|
|
400
|
+
? undefined
|
|
401
|
+
: () => onLongPressEvent(event)
|
|
402
|
+
}
|
|
403
|
+
/>
|
|
404
|
+
</View>
|
|
405
|
+
))}
|
|
406
|
+
{hiddenCount > 0 ? (
|
|
407
|
+
<Text
|
|
408
|
+
style={[theme.text.more, styles.moreLabel, { color: theme.colors.textMuted }]}
|
|
409
|
+
onPress={onPressMore ? () => onPressMore(dayEvents, day) : undefined}
|
|
410
|
+
accessibilityRole="button"
|
|
411
|
+
accessibilityLabel={`Show ${hiddenCount} more events`}
|
|
412
|
+
allowFontScaling={false}
|
|
413
|
+
>
|
|
414
|
+
{moreLabel.replace("{moreCount}", String(hiddenCount))}
|
|
415
|
+
</Text>
|
|
416
|
+
) : null}
|
|
417
|
+
</TouchableOpacity>
|
|
418
|
+
);
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const handleLayout = (event: LayoutChangeEvent) => {
|
|
422
|
+
const next = event.nativeEvent.layout.height;
|
|
423
|
+
setGridHeight((prev) => (prev === next ? prev : next));
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
return (
|
|
427
|
+
<View style={styles.root}>
|
|
428
|
+
{showTitle ? (
|
|
429
|
+
<Text style={[styles.title, { color: theme.colors.text }]} allowFontScaling={false}>
|
|
430
|
+
{format(date, "MMMM yyyy", locale ? { locale } : undefined)}
|
|
431
|
+
</Text>
|
|
432
|
+
) : null}
|
|
433
|
+
{showWeekdays ? (
|
|
434
|
+
<View style={styles.weekdayHeader}>
|
|
435
|
+
{weekdayLabels.map((day) => (
|
|
436
|
+
<Text
|
|
437
|
+
key={day.toISOString()}
|
|
438
|
+
style={[theme.text.weekday, styles.weekdayLabel, { color: theme.colors.textMuted }]}
|
|
439
|
+
allowFontScaling={false}
|
|
440
|
+
>
|
|
441
|
+
{format(day, "EEE", { locale })}
|
|
442
|
+
</Text>
|
|
443
|
+
))}
|
|
444
|
+
</View>
|
|
445
|
+
) : null}
|
|
446
|
+
<View style={styles.container} onLayout={handleLayout}>
|
|
447
|
+
{weeks.map((week) => (
|
|
448
|
+
<View style={styles.weekRow} key={week[0].toISOString()}>
|
|
449
|
+
{week.map((day) => renderDay(day))}
|
|
450
|
+
</View>
|
|
451
|
+
))}
|
|
452
|
+
</View>
|
|
453
|
+
</View>
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export const MonthView = memo(MonthViewInner) as typeof MonthViewInner;
|
|
458
|
+
|
|
459
|
+
const styles = StyleSheet.create({
|
|
460
|
+
root: {
|
|
461
|
+
flex: 1,
|
|
462
|
+
},
|
|
463
|
+
// Matches the dom MonthView title: "MMMM yyyy" above the grid.
|
|
464
|
+
title: {
|
|
465
|
+
fontSize: 17,
|
|
466
|
+
fontWeight: "700",
|
|
467
|
+
paddingTop: 10,
|
|
468
|
+
paddingHorizontal: 14,
|
|
469
|
+
paddingBottom: 6,
|
|
470
|
+
},
|
|
471
|
+
weekdayHeader: {
|
|
472
|
+
flexDirection: "row",
|
|
473
|
+
paddingBottom: 4,
|
|
474
|
+
},
|
|
475
|
+
weekdayLabel: {
|
|
476
|
+
flex: 1,
|
|
477
|
+
textAlign: "center",
|
|
478
|
+
},
|
|
479
|
+
container: {
|
|
480
|
+
flex: 1,
|
|
481
|
+
},
|
|
482
|
+
weekRow: {
|
|
483
|
+
flex: 1,
|
|
484
|
+
flexDirection: "row",
|
|
485
|
+
},
|
|
486
|
+
dayCell: {
|
|
487
|
+
flex: 1,
|
|
488
|
+
alignItems: "center",
|
|
489
|
+
paddingTop: 4,
|
|
490
|
+
gap: 2,
|
|
491
|
+
overflow: "hidden",
|
|
492
|
+
},
|
|
493
|
+
dayCellEvents: {
|
|
494
|
+
alignItems: "stretch",
|
|
495
|
+
},
|
|
496
|
+
dateBadge: {
|
|
497
|
+
justifyContent: "center",
|
|
498
|
+
alignItems: "center",
|
|
499
|
+
height: 24,
|
|
500
|
+
width: 24,
|
|
501
|
+
},
|
|
502
|
+
dateBadgeEvents: {
|
|
503
|
+
alignSelf: "flex-end",
|
|
504
|
+
marginRight: 4,
|
|
505
|
+
},
|
|
506
|
+
rangeBand: {
|
|
507
|
+
position: "absolute",
|
|
508
|
+
left: 0,
|
|
509
|
+
right: 0,
|
|
510
|
+
},
|
|
511
|
+
monthEvent: {
|
|
512
|
+
marginHorizontal: 4,
|
|
513
|
+
},
|
|
514
|
+
moreLabel: {
|
|
515
|
+
marginTop: 2,
|
|
516
|
+
marginHorizontal: 4,
|
|
517
|
+
},
|
|
518
|
+
});
|