@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.
@@ -18,8 +18,9 @@ import {
18
18
  import { useCalendarTheme } from "../theme";
19
19
  import type { CalendarEvent, EventKeyExtractor, RenderEvent, WeekStartsOn } from "../types";
20
20
  import { type WeekdayFormat, getWeekDays, weekdayFormatToken } from "@super-calendar/core";
21
+ import { createSlots, type SlotStyleProps } from "../utils/slots";
21
22
  import { useWebPagerKeys } from "../utils/useWebPagerKeys";
22
- import { MonthView } from "./MonthView";
23
+ import { MonthView, type MonthViewSlot } from "./MonthView";
23
24
 
24
25
  // Horizontal swipe paging doesn't translate to web; there we disable it and page
25
26
  // with the arrow keys instead.
@@ -64,7 +65,7 @@ export type MonthPagerProps<T> = {
64
65
  renderHeaderForMonthView?: (weekDays: Date[]) => React.ReactNode;
65
66
  /** Replace the default date badge in each day cell. Receives the day. */
66
67
  renderCustomDateForMonth?: (date: Date) => React.ReactNode;
67
- };
68
+ } & SlotStyleProps<MonthViewSlot>;
68
69
 
69
70
  function MonthPagerInner<T>({
70
71
  date,
@@ -93,8 +94,15 @@ function MonthPagerInner<T>({
93
94
  activeDate,
94
95
  renderHeaderForMonthView,
95
96
  renderCustomDateForMonth,
97
+ classNames,
98
+ styles: styleOverrides,
96
99
  }: MonthPagerProps<T>): ReactElement {
97
100
  const theme = useCalendarTheme();
101
+ // Stable across renders (it feeds the memoized renderItem below).
102
+ const slot = useMemo(
103
+ () => createSlots<MonthViewSlot>({ classNames, styles: styleOverrides }),
104
+ [classNames, styleOverrides],
105
+ );
98
106
  const { width, height } = useWindowDimensions();
99
107
  const listRef = useRef<LegendListRef>(null);
100
108
  const containerRef = useRef<View>(null);
@@ -206,6 +214,14 @@ function MonthPagerInner<T>({
206
214
  return isRTL ? days.reverse() : days;
207
215
  }, [anchor, weekStartsOn, isRTL]);
208
216
 
217
+ // Pages are keyed by month Dates that never change, so LegendList keeps the
218
+ // pages it has already rendered and only re-renders them when `data` or
219
+ // `extraData` changes — a new `renderItem` identity is not enough. Feed
220
+ // `events` (events that arrive after mount, e.g. from an async fetch, must
221
+ // repaint the mounted page) and `activeDate` (the selected-day highlight must
222
+ // move). Mirrors listExtraData in TimeGrid.
223
+ const listExtraData = useMemo(() => ({ events, activeDate }), [events, activeDate]);
224
+
209
225
  const snapToIndices = useMemo(() => monthDates.map((_, index) => index), [monthDates]);
210
226
  const keyExtractorList = useCallback((item: Date) => item.toISOString(), []);
211
227
  const getFixedItemSize = useCallback(() => containerWidth, [containerWidth]);
@@ -238,6 +254,8 @@ function MonthPagerInner<T>({
238
254
  onLongPressEvent={onLongPressEvent}
239
255
  onPressMore={onPressMore}
240
256
  renderCustomDateForMonth={renderCustomDateForMonth}
257
+ classNames={classNames}
258
+ styles={styleOverrides}
241
259
  />
242
260
  </View>
243
261
  ),
@@ -264,6 +282,8 @@ function MonthPagerInner<T>({
264
282
  onLongPressEvent,
265
283
  onPressMore,
266
284
  renderCustomDateForMonth,
285
+ classNames,
286
+ styleOverrides,
267
287
  ],
268
288
  );
269
289
 
@@ -272,7 +292,10 @@ function MonthPagerInner<T>({
272
292
  {/* The active month's title, above the (shared) weekday header — mirrors the
273
293
  dom MonthView's title. The grids below omit their own title/weekdays. */}
274
294
  <Text
275
- style={[styles.monthTitle, theme.text.monthTitle, { color: theme.colors.text }]}
295
+ {...slot("title", {
296
+ base: styles.monthTitle,
297
+ themed: [theme.text.monthTitle, { color: theme.colors.text }],
298
+ })}
276
299
  allowFontScaling={false}
277
300
  >
278
301
  {format(date, "MMMM yyyy", locale ? { locale } : undefined)}
@@ -280,7 +303,12 @@ function MonthPagerInner<T>({
280
303
  {renderHeaderForMonthView ? (
281
304
  renderHeaderForMonthView(weekDays)
282
305
  ) : (
283
- <MonthWeekdayHeader weekDays={weekDays} weekdayFormat={weekdayFormat} locale={locale} />
306
+ <MonthWeekdayHeader
307
+ weekDays={weekDays}
308
+ weekdayFormat={weekdayFormat}
309
+ locale={locale}
310
+ slot={slot}
311
+ />
284
312
  )}
285
313
  <View
286
314
  style={styles.pager}
@@ -297,6 +325,7 @@ function MonthPagerInner<T>({
297
325
  ref={listRef}
298
326
  style={isWeb ? [styles.pagerList, styles.webNoScroll] : styles.pagerList}
299
327
  data={monthDates}
328
+ extraData={listExtraData}
300
329
  horizontal
301
330
  recycleItems={false}
302
331
  keyExtractor={keyExtractorList}
@@ -348,6 +377,7 @@ type MonthWeekdayHeaderProps = {
348
377
  weekDays: Date[];
349
378
  weekdayFormat?: WeekdayFormat;
350
379
  locale?: Locale;
380
+ slot: ReturnType<typeof createSlots<MonthViewSlot>>;
351
381
  };
352
382
 
353
383
  // The default weekday-label row above the month grid (e.g. "Mon Tue Wed…"),
@@ -356,14 +386,23 @@ const MonthWeekdayHeader = ({
356
386
  weekDays,
357
387
  weekdayFormat = "short",
358
388
  locale,
389
+ slot,
359
390
  }: MonthWeekdayHeaderProps) => {
360
391
  const theme = useCalendarTheme();
361
392
  return (
362
- <View style={[styles.weekdayHeader, theme.containers.weekdayHeader]}>
393
+ <View
394
+ {...slot("weekdays", {
395
+ base: styles.weekdayHeader,
396
+ themed: theme.containers.weekdayHeader,
397
+ })}
398
+ >
363
399
  {weekDays.map((day) => (
364
400
  <Text
365
401
  key={day.toISOString()}
366
- style={[theme.text.weekday, styles.weekdayLabel, { color: theme.colors.textMuted }]}
402
+ {...slot("weekday", {
403
+ base: styles.weekdayLabel,
404
+ themed: [theme.text.weekday, { color: theme.colors.textMuted }],
405
+ })}
367
406
  allowFontScaling={false}
368
407
  >
369
408
  {format(day, weekdayFormatToken(weekdayFormat), { locale })}
@@ -16,6 +16,7 @@ import {
16
16
  const isWeb = Platform.OS === "web";
17
17
  import { useCalendarTheme } from "../theme";
18
18
  import type { CalendarEvent, EventKeyExtractor, RenderEvent, WeekStartsOn } from "../types";
19
+ import { createSlots, type SlotStyleProps } from "../utils/slots";
19
20
  import { withEventAccessibilityLabel } from "../utils/withEventAccessibilityLabel";
20
21
  import {
21
22
  type DateRange,
@@ -50,8 +51,26 @@ const FALLBACK_VISIBLE_COUNT = 3;
50
51
  const numericStyle = (value: number | string | undefined, fallback: number) =>
51
52
  typeof value === "number" ? value : fallback;
52
53
 
54
+ /**
55
+ * The styleable parts of {@link MonthView}. Mirrors the dom renderer's slot
56
+ * names where the structure matches; `dayBadgeText` is native-only (React
57
+ * Native text colour doesn't inherit from the badge). Event chips are styled
58
+ * by `renderEvent` (or the theme), not a slot.
59
+ */
60
+ export type MonthViewSlot =
61
+ | "title"
62
+ | "weekdays"
63
+ | "weekday"
64
+ | "grid"
65
+ | "week"
66
+ | "day"
67
+ | "dayBadge"
68
+ | "dayBadgeText"
69
+ | "rangeBand"
70
+ | "more";
71
+
53
72
  /** Props for {@link MonthView}, the single-month grid. */
54
- export type MonthViewProps<T> = {
73
+ export type MonthViewProps<T> = SlotStyleProps<MonthViewSlot> & {
55
74
  date: Date;
56
75
  events: CalendarEvent<T>[];
57
76
  /**
@@ -158,8 +177,11 @@ function MonthViewInner<T>({
158
177
  renderCustomDateForMonth,
159
178
  onDayPointerDown,
160
179
  onDayPointerEnter,
180
+ classNames,
181
+ styles: styleOverrides,
161
182
  }: MonthViewProps<T>): ReactElement {
162
183
  const theme = useCalendarTheme();
184
+ const slot = createSlots<MonthViewSlot>({ classNames, styles: styleOverrides });
163
185
  // Selection comes from context (so cached pages still repaint), but explicit
164
186
  // props win for direct/standalone use of MonthView.
165
187
  const selection = useCalendarSelection();
@@ -238,17 +260,19 @@ function MonthViewInner<T>({
238
260
  return (
239
261
  <View
240
262
  key={day.toISOString()}
241
- style={[
242
- styles.dayCell,
243
- showGrid && {
244
- borderTopWidth: StyleSheet.hairlineWidth,
245
- borderRightWidth: StyleSheet.hairlineWidth,
246
- borderColor: theme.colors.gridLine,
247
- },
248
- // No weekend tint on blank placeholders, so the shading doesn't bleed
249
- // into the empty cells of non-existent days.
250
- theme.containers.dayCell,
251
- ]}
263
+ {...slot("day", {
264
+ base: styles.dayCell,
265
+ themed: [
266
+ showGrid && {
267
+ borderTopWidth: StyleSheet.hairlineWidth,
268
+ borderRightWidth: StyleSheet.hairlineWidth,
269
+ borderColor: theme.colors.gridLine,
270
+ },
271
+ // No weekend tint on blank placeholders, so the shading doesn't bleed
272
+ // into the empty cells of non-existent days.
273
+ theme.containers.dayCell,
274
+ ],
275
+ })}
252
276
  />
253
277
  );
254
278
  }
@@ -298,24 +322,29 @@ function MonthViewInner<T>({
298
322
  const eventCount = dayEvents.length;
299
323
  const accessibilityLabel = `${format(day, "EEEE, d LLLL yyyy", { locale })}${isToday ? ", today" : ""}${isSelected ? ", selected" : ""}${isDisabled ? ", unavailable" : ""}, ${eventCount} ${eventCount === 1 ? "event" : "events"}`;
300
324
 
325
+ const daySlot = slot("day", {
326
+ // Events mode mirrors the dom renderer: left-aligned cell content with
327
+ // the date badge in the top-right. The picker (no grid) stays centered
328
+ // so the selection range band lines up with the centered badge.
329
+ base: [styles.dayCell, showGrid && styles.dayCellEvents],
330
+ themed: [
331
+ showGrid && {
332
+ borderTopWidth: StyleSheet.hairlineWidth,
333
+ borderRightWidth: StyleSheet.hairlineWidth,
334
+ borderColor: theme.colors.gridLine,
335
+ },
336
+ isWeekend(day) && { backgroundColor: theme.colors.weekendBackground },
337
+ theme.containers.dayCell,
338
+ ],
339
+ });
340
+
301
341
  return (
302
342
  <TouchableOpacity
303
343
  key={day.toISOString()}
304
- style={[
305
- styles.dayCell,
306
- // Events mode mirrors the dom renderer: left-aligned cell content with
307
- // the date badge in the top-right. The picker (no grid) stays centered
308
- // so the selection range band lines up with the centered badge.
309
- showGrid && styles.dayCellEvents,
310
- showGrid && {
311
- borderTopWidth: StyleSheet.hairlineWidth,
312
- borderRightWidth: StyleSheet.hairlineWidth,
313
- borderColor: theme.colors.gridLine,
314
- },
315
- isWeekend(day) && { backgroundColor: theme.colors.weekendBackground },
316
- calendarCellStyle?.(day),
317
- theme.containers.dayCell,
318
- ]}
344
+ {...daySlot}
345
+ // The consumer's per-date style is an explicit override, so it merges
346
+ // after the slot (and survives a `day` class).
347
+ style={[daySlot.style, calendarCellStyle?.(day)]}
319
348
  // In the picker, don't dim the whole cell on press: the tap should show on
320
349
  // the badge (the circle) only, not the cell background. `onPressIn/Out` drive
321
350
  // the badge's own opacity below. The events calendar keeps the default
@@ -360,56 +389,69 @@ function MonthViewInner<T>({
360
389
  {hasBand ? (
361
390
  <View
362
391
  testID="month-range-band"
363
- style={[
364
- styles.rangeBand,
365
- { pointerEvents: "none" },
366
- { backgroundColor: theme.colors.rangeBackground },
367
- fillCellOnSelection
368
- ? { top: 0, bottom: 0 }
369
- : { top: BAND_CENTER_Y - theme.rangeBandHeight / 2, height: theme.rangeBandHeight },
370
- // Cap the pill at the endpoint circle (half a badge in from centre)
371
- // instead of spilling to the cell edge, so no band shows beside it.
372
- !fillCellOnSelection &&
373
- isRangeStart && {
374
- left: "50%",
375
- marginLeft: -DATE_BADGE_HEIGHT / 2,
376
- borderTopLeftRadius: theme.rangeBandHeight / 2,
377
- borderBottomLeftRadius: theme.rangeBandHeight / 2,
378
- },
379
- !fillCellOnSelection &&
380
- isRangeEnd && {
381
- right: "50%",
382
- marginRight: -DATE_BADGE_HEIGHT / 2,
383
- borderTopRightRadius: theme.rangeBandHeight / 2,
384
- borderBottomRightRadius: theme.rangeBandHeight / 2,
385
- },
386
- ]}
392
+ {...slot("rangeBand", {
393
+ base: [
394
+ styles.rangeBand,
395
+ { pointerEvents: "none" },
396
+ fillCellOnSelection
397
+ ? { top: 0, bottom: 0 }
398
+ : {
399
+ top: BAND_CENTER_Y - theme.rangeBandHeight / 2,
400
+ height: theme.rangeBandHeight,
401
+ },
402
+ // Cap the pill at the endpoint circle (half a badge in from centre)
403
+ // instead of spilling to the cell edge, so no band shows beside it.
404
+ !fillCellOnSelection &&
405
+ isRangeStart && {
406
+ left: "50%",
407
+ marginLeft: -DATE_BADGE_HEIGHT / 2,
408
+ borderTopLeftRadius: theme.rangeBandHeight / 2,
409
+ borderBottomLeftRadius: theme.rangeBandHeight / 2,
410
+ },
411
+ !fillCellOnSelection &&
412
+ isRangeEnd && {
413
+ right: "50%",
414
+ marginRight: -DATE_BADGE_HEIGHT / 2,
415
+ borderTopRightRadius: theme.rangeBandHeight / 2,
416
+ borderBottomRightRadius: theme.rangeBandHeight / 2,
417
+ },
418
+ ],
419
+ themed: { backgroundColor: theme.colors.rangeBackground },
420
+ })}
387
421
  />
388
422
  ) : null}
389
423
  {renderCustomDateForMonth ? (
390
424
  renderCustomDateForMonth(day)
391
425
  ) : (
392
426
  <View
393
- style={[
394
- styles.dateBadge,
395
- showGrid && styles.dateBadgeEvents,
396
- isFilledBadge && {
397
- backgroundColor: isHighlighted
398
- ? theme.colors.todayBackground
399
- : theme.colors.selectedBackground,
400
- borderRadius: theme.todayBadgeRadius,
401
- },
402
- isHovered &&
403
- !isFilledBadge && {
404
- backgroundColor: theme.colors.hoverBackground,
427
+ {...slot("dayBadge", {
428
+ base: [
429
+ styles.dateBadge,
430
+ showGrid && styles.dateBadgeEvents,
431
+ // Tap feedback lives on the badge, not the cell (picker only);
432
+ // kept even under a class so the press still reads.
433
+ !showGrid && pressedKey === dayKey && { opacity: 0.2 },
434
+ ],
435
+ themed: [
436
+ isFilledBadge && {
437
+ backgroundColor: isHighlighted
438
+ ? theme.colors.todayBackground
439
+ : theme.colors.selectedBackground,
405
440
  borderRadius: theme.todayBadgeRadius,
406
441
  },
407
- // Tap feedback lives on the badge, not the cell (picker only).
408
- !showGrid && pressedKey === dayKey && { opacity: 0.2 },
409
- theme.containers.dayBadge,
410
- ]}
442
+ isHovered &&
443
+ !isFilledBadge && {
444
+ backgroundColor: theme.colors.hoverBackground,
445
+ borderRadius: theme.todayBadgeRadius,
446
+ },
447
+ theme.containers.dayBadge,
448
+ ],
449
+ })}
411
450
  >
412
- <Text style={[theme.text.dateCell, { color: dateColor }]} allowFontScaling={false}>
451
+ <Text
452
+ {...slot("dayBadgeText", { themed: [theme.text.dateCell, { color: dateColor }] })}
453
+ allowFontScaling={false}
454
+ >
413
455
  {format(day, "d")}
414
456
  </Text>
415
457
  </View>
@@ -434,7 +476,10 @@ function MonthViewInner<T>({
434
476
  ))}
435
477
  {hiddenCount > 0 ? (
436
478
  <Text
437
- style={[theme.text.more, styles.moreLabel, { color: theme.colors.textMuted }]}
479
+ {...slot("more", {
480
+ base: styles.moreLabel,
481
+ themed: [theme.text.more, { color: theme.colors.textMuted }],
482
+ })}
438
483
  onPress={onPressMore ? () => onPressMore(dayEvents, day) : undefined}
439
484
  accessibilityRole="button"
440
485
  accessibilityLabel={`Show ${hiddenCount} more events`}
@@ -456,18 +501,29 @@ function MonthViewInner<T>({
456
501
  <View style={styles.root}>
457
502
  {showTitle ? (
458
503
  <Text
459
- style={[styles.title, theme.text.monthTitle, { color: theme.colors.text }]}
504
+ {...slot("title", {
505
+ base: styles.title,
506
+ themed: [theme.text.monthTitle, { color: theme.colors.text }],
507
+ })}
460
508
  allowFontScaling={false}
461
509
  >
462
510
  {format(date, "MMMM yyyy", locale ? { locale } : undefined)}
463
511
  </Text>
464
512
  ) : null}
465
513
  {showWeekdays ? (
466
- <View style={[styles.weekdayHeader, theme.containers.weekdayHeader]}>
514
+ <View
515
+ {...slot("weekdays", {
516
+ base: styles.weekdayHeader,
517
+ themed: theme.containers.weekdayHeader,
518
+ })}
519
+ >
467
520
  {weekdayLabels.map((day) => (
468
521
  <Text
469
522
  key={day.toISOString()}
470
- style={[theme.text.weekday, styles.weekdayLabel, { color: theme.colors.textMuted }]}
523
+ {...slot("weekday", {
524
+ base: styles.weekdayLabel,
525
+ themed: [theme.text.weekday, { color: theme.colors.textMuted }],
526
+ })}
471
527
  allowFontScaling={false}
472
528
  >
473
529
  {format(day, weekdayFormatToken(weekdayFormat), { locale })}
@@ -475,9 +531,12 @@ function MonthViewInner<T>({
475
531
  ))}
476
532
  </View>
477
533
  ) : null}
478
- <View style={styles.container} onLayout={handleLayout}>
534
+ <View {...slot("grid", { base: styles.container })} onLayout={handleLayout}>
479
535
  {weeks.map((week) => (
480
- <View style={[styles.weekRow, theme.containers.weekRow]} key={week[0].toISOString()}>
536
+ <View
537
+ {...slot("week", { base: styles.weekRow, themed: theme.containers.weekRow })}
538
+ key={week[0].toISOString()}
539
+ >
481
540
  {week.map((day) => renderDay(day))}
482
541
  </View>
483
542
  ))}