@super-calendar/dom 2.1.5 → 2.2.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/src/MonthView.tsx CHANGED
@@ -25,14 +25,36 @@ import {
25
25
  dayBadgeKind,
26
26
  type DateRange,
27
27
  type DateSelectionConstraints,
28
+ type EventAccessibilityLabeler,
28
29
  groupEventsByDay,
30
+ isAllDayEvent,
29
31
  type MonthGridDay,
30
32
  monthVisibleCount,
31
33
  rangeBandKind,
34
+ type WeekdayFormat,
32
35
  type WeekStartsOn,
33
36
  } from "@super-calendar/core";
37
+ import { createSlots, dataState, type SlotDefault, type SlotStyleProps } from "./slots";
34
38
  import { type DomCalendarTheme, mergeDomTheme } from "./theme";
35
39
 
40
+ /**
41
+ * Styleable parts of {@link MonthView}. Pass a class or inline style per slot via
42
+ * the `classNames` / `styles` props to restyle just that part.
43
+ */
44
+ export type MonthViewSlot =
45
+ | "title"
46
+ | "weekdays"
47
+ | "weekday"
48
+ | "grid"
49
+ | "week"
50
+ | "day"
51
+ | "dayBadge"
52
+ | "rangeBand"
53
+ | "events"
54
+ | "chipButton"
55
+ | "chip"
56
+ | "more";
57
+
36
58
  // Chip metrics for the events layout (when `events` is provided).
37
59
  const DATE_ROW = 24;
38
60
  const CHIP_HEIGHT = 18;
@@ -51,11 +73,14 @@ export interface DomMonthEventArgs<T = unknown> {
51
73
  export type DomMonthEvent<T = unknown> = ComponentType<DomMonthEventArgs<T>>;
52
74
 
53
75
  /** Props for {@link MonthView}. */
54
- export interface MonthViewProps<T = unknown> extends DateSelectionConstraints {
76
+ export interface MonthViewProps<T = unknown>
77
+ extends DateSelectionConstraints, SlotStyleProps<MonthViewSlot> {
55
78
  /** Any day within the month to render. */
56
79
  date: Date;
57
80
  /** First day of the week. Sunday = 0 (default) … Saturday = 6. */
58
81
  weekStartsOn?: WeekStartsOn;
82
+ /** Weekday header label width: `narrow` ("M"), `short` ("Mon", default), or `long` ("Monday"). */
83
+ weekdayFormat?: WeekdayFormat;
59
84
  /**
60
85
  * Events to render as chips in each day cell. Passing this (even `[]`) switches
61
86
  * the grid to the calendar layout (date in the corner, chips below); omit it for
@@ -64,6 +89,12 @@ export interface MonthViewProps<T = unknown> extends DateSelectionConstraints {
64
89
  events?: CalendarEvent<T>[];
65
90
  /** Custom chip renderer; falls back to the built-in titled chip. */
66
91
  renderEvent?: DomMonthEvent<T>;
92
+ /**
93
+ * Override the screen-reader label for each event chip. Receives the event and a
94
+ * `{ mode: "month", isAllDay, ampm: false }` context; return the full text to
95
+ * announce. Defaults to the event title and day (e.g. "Standup, 15 July").
96
+ */
97
+ eventAccessibilityLabel?: EventAccessibilityLabeler<T>;
67
98
  /** Max chips shown per day before a "+N more" row (default 3). */
68
99
  maxVisibleEventCount?: number;
69
100
  /** Template for the overflow row; `{moreCount}` is replaced (default "{moreCount} More"). */
@@ -93,6 +124,14 @@ export interface MonthViewProps<T = unknown> extends DateSelectionConstraints {
93
124
  theme?: Partial<DomCalendarTheme>;
94
125
  /** Fired when a selectable day is clicked. */
95
126
  onPressDay?: (date: Date) => void;
127
+ /**
128
+ * In events mode, enables drag-to-create: press on a day and drag across others
129
+ * to sketch a span, then release to fire this with the all-day range (`start` at
130
+ * midnight of the first day, `end` at midnight after the last, exclusive). A plain
131
+ * click without dragging still fires `onPressDay`, not this. Days being sketched
132
+ * carry `data-creating` for styling.
133
+ */
134
+ onCreateEvent?: (start: Date, end: Date) => void;
96
135
  /**
97
136
  * When events are shown, also make the day cells keyboard-navigable: a single
98
137
  * roving tab stop, arrow keys move the focus, Enter opens the day (`onPressDay`).
@@ -106,35 +145,43 @@ export interface MonthViewProps<T = unknown> extends DateSelectionConstraints {
106
145
  style?: CSSProperties;
107
146
  }
108
147
 
109
- function dayCellStyle(day: MonthGridDay, theme: DomCalendarTheme): CSSProperties {
148
+ // Each helper returns the slot's built-in styling split into `base` (structural,
149
+ // always applied) and `themed` (colour/type/spacing, dropped when a class is set).
150
+
151
+ function dayCellDefault(day: MonthGridDay, theme: DomCalendarTheme): SlotDefault {
110
152
  return {
111
- position: "relative",
112
- height: theme.cellHeight,
113
- border: "none",
114
- // The range band is a separate layer; the cell only carries the weekend tint.
115
- background: day.isWeekend && !day.isInRange ? theme.weekendBackground : "transparent",
116
- font: "inherit",
117
- fontSize: 15,
118
- color: day.isDisabled ? theme.textDisabled : theme.text,
119
- cursor: day.isDisabled ? "default" : "pointer",
120
- display: "flex",
121
- alignItems: "center",
122
- justifyContent: "center",
123
- padding: 0,
124
- WebkitTapHighlightColor: "transparent",
153
+ base: {
154
+ position: "relative",
155
+ height: theme.cellHeight,
156
+ border: "none",
157
+ font: "inherit",
158
+ cursor: day.isDisabled ? "default" : "pointer",
159
+ display: "flex",
160
+ alignItems: "center",
161
+ justifyContent: "center",
162
+ padding: 0,
163
+ WebkitTapHighlightColor: "transparent",
164
+ },
165
+ themed: {
166
+ // The range band is a separate layer; the cell only carries the weekend tint.
167
+ background: day.isWeekend && !day.isInRange ? theme.weekendBackground : "transparent",
168
+ fontSize: 15,
169
+ color: day.isDisabled ? theme.textDisabled : theme.text,
170
+ },
125
171
  };
126
172
  }
127
173
 
128
174
  /**
129
175
  * The range band behind a day, rendered as its own layer so it can be a centered
130
176
  * rounded strip (the default) or fill the whole cell (`fillCell`). Returns null
131
- * for days with no band. Endpoints get the leading/trailing pill rounding.
177
+ * for days with no band. Endpoints get the leading/trailing pill rounding. Its
178
+ * geometry is structural; only the fill colour is themed.
132
179
  */
133
- function rangeBandStyle(
180
+ function rangeBandDefault(
134
181
  day: MonthGridDay,
135
182
  theme: DomCalendarTheme,
136
183
  fillCell: boolean,
137
- ): CSSProperties | null {
184
+ ): SlotDefault | null {
138
185
  const kind = rangeBandKind(day, fillCell);
139
186
  if (kind === "none") return null;
140
187
  const fill = kind === "fill";
@@ -145,27 +192,26 @@ function rangeBandStyle(
145
192
  // outer edge (half a badge in from the cell centre) rather than spilling to the
146
193
  // cell edge, so no band shows in the empty space beside the day circles.
147
194
  const cap = `calc(50% - ${theme.dayBadgeSize / 2}px)`;
148
- const style: CSSProperties = {
195
+ const base: CSSProperties = {
149
196
  position: "absolute",
150
197
  left: rounding.start ? cap : 0,
151
198
  right: rounding.end ? cap : 0,
152
199
  top: inset,
153
200
  bottom: inset,
154
- background: theme.rangeBackground,
155
201
  zIndex: 0,
156
202
  };
157
203
  if (rounding.start) {
158
- style.borderTopLeftRadius = radius;
159
- style.borderBottomLeftRadius = radius;
204
+ base.borderTopLeftRadius = radius;
205
+ base.borderBottomLeftRadius = radius;
160
206
  }
161
207
  if (rounding.end) {
162
- style.borderTopRightRadius = radius;
163
- style.borderBottomRightRadius = radius;
208
+ base.borderTopRightRadius = radius;
209
+ base.borderBottomRightRadius = radius;
164
210
  }
165
- return style;
211
+ return { base, themed: { background: theme.rangeBackground } };
166
212
  }
167
213
 
168
- function badgeStyle(day: MonthGridDay, theme: DomCalendarTheme, hovered: boolean): CSSProperties {
214
+ function badgeDefault(day: MonthGridDay, theme: DomCalendarTheme, hovered: boolean): SlotDefault {
169
215
  const badge = dayBadgeKind(day, day.isToday);
170
216
  const filled = badge !== "none";
171
217
  const background = filled
@@ -176,108 +222,130 @@ function badgeStyle(day: MonthGridDay, theme: DomCalendarTheme, hovered: boolean
176
222
  ? theme.hoverBackground
177
223
  : "transparent";
178
224
  return {
179
- position: "relative",
180
- zIndex: 1,
181
- width: theme.dayBadgeSize,
182
- height: theme.dayBadgeSize,
183
- borderRadius: "50%",
184
- display: "flex",
185
- alignItems: "center",
186
- justifyContent: "center",
187
- background,
188
- color: filled ? (day.isToday ? theme.todayText : theme.selectedText) : "inherit",
225
+ base: {
226
+ position: "relative",
227
+ zIndex: 1,
228
+ width: theme.dayBadgeSize,
229
+ height: theme.dayBadgeSize,
230
+ borderRadius: "50%",
231
+ display: "flex",
232
+ alignItems: "center",
233
+ justifyContent: "center",
234
+ },
235
+ themed: {
236
+ background,
237
+ color: filled ? (day.isToday ? theme.todayText : theme.selectedText) : "inherit",
238
+ },
189
239
  };
190
240
  }
191
241
 
192
242
  // --- Calendar (events) layout styles -------------------------------------
193
243
 
194
- function eventCellStyle(day: MonthGridDay, theme: DomCalendarTheme, height: number): CSSProperties {
244
+ function eventCellDefault(day: MonthGridDay, theme: DomCalendarTheme, height: number): SlotDefault {
195
245
  return {
196
- position: "relative",
197
- minHeight: height,
198
- minWidth: 0,
199
- border: "none",
200
- borderTop: `1px solid ${theme.gridLine}`,
201
- background: day.isWeekend && !day.isInRange ? theme.weekendBackground : "transparent",
202
- color: day.isDisabled ? theme.textDisabled : theme.text,
203
- cursor: day.isDisabled ? "default" : "pointer",
204
- display: "flex",
205
- flexDirection: "column",
206
- gap: CHIP_GAP,
207
- padding: CELL_PAD,
208
- boxSizing: "border-box",
209
- textAlign: "left",
210
- WebkitTapHighlightColor: "transparent",
246
+ base: {
247
+ position: "relative",
248
+ minHeight: height,
249
+ minWidth: 0,
250
+ border: "none",
251
+ cursor: day.isDisabled ? "default" : "pointer",
252
+ display: "flex",
253
+ flexDirection: "column",
254
+ gap: CHIP_GAP,
255
+ padding: CELL_PAD,
256
+ boxSizing: "border-box",
257
+ textAlign: "left",
258
+ WebkitTapHighlightColor: "transparent",
259
+ },
260
+ themed: {
261
+ borderTop: `1px solid ${theme.gridLine}`,
262
+ background: day.isWeekend && !day.isInRange ? theme.weekendBackground : "transparent",
263
+ color: day.isDisabled ? theme.textDisabled : theme.text,
264
+ },
211
265
  };
212
266
  }
213
267
 
214
- function compactBadgeStyle(day: MonthGridDay, theme: DomCalendarTheme): CSSProperties {
268
+ function compactBadgeDefault(day: MonthGridDay, theme: DomCalendarTheme): SlotDefault {
215
269
  const badge = dayBadgeKind(day, day.isToday);
216
270
  const filled = badge !== "none";
217
271
  return {
218
- position: "relative",
219
- zIndex: 1,
220
- alignSelf: "flex-end",
221
- width: DATE_ROW - 2,
222
- height: DATE_ROW - 2,
223
- borderRadius: "50%",
224
- display: "flex",
225
- alignItems: "center",
226
- justifyContent: "center",
227
- fontSize: 13,
228
- background: filled
229
- ? badge === "today"
230
- ? theme.todayBackground
231
- : theme.selectedBackground
232
- : "transparent",
233
- color: filled ? (day.isToday ? theme.todayText : theme.selectedText) : "inherit",
234
- };
235
- }
236
-
237
- const chipButtonStyle: CSSProperties = {
238
- position: "relative",
239
- zIndex: 1,
240
- border: "none",
241
- padding: 0,
242
- margin: 0,
243
- background: "transparent",
244
- cursor: "pointer",
245
- textAlign: "left",
246
- fontFamily: "inherit",
247
- };
248
-
249
- function chipStyle(theme: DomCalendarTheme): CSSProperties {
250
- return {
251
- display: "block",
252
- height: CHIP_HEIGHT,
253
- lineHeight: `${CHIP_HEIGHT}px`,
254
- padding: "0 6px",
255
- borderRadius: 4,
256
- background: theme.eventBackground,
257
- color: theme.eventText,
258
- fontSize: 11,
259
- fontWeight: 600,
260
- whiteSpace: "nowrap",
261
- overflow: "hidden",
262
- textOverflow: "ellipsis",
272
+ base: {
273
+ position: "relative",
274
+ zIndex: 1,
275
+ alignSelf: "flex-end",
276
+ width: DATE_ROW - 2,
277
+ height: DATE_ROW - 2,
278
+ borderRadius: "50%",
279
+ display: "flex",
280
+ alignItems: "center",
281
+ justifyContent: "center",
282
+ },
283
+ themed: {
284
+ fontSize: 13,
285
+ background: filled
286
+ ? badge === "today"
287
+ ? theme.todayBackground
288
+ : theme.selectedBackground
289
+ : "transparent",
290
+ color: filled ? (day.isToday ? theme.todayText : theme.selectedText) : "inherit",
291
+ },
263
292
  };
264
293
  }
265
294
 
266
- function moreButtonStyle(theme: DomCalendarTheme): CSSProperties {
267
- return {
295
+ const chipButtonDefault: SlotDefault = {
296
+ base: {
268
297
  position: "relative",
269
298
  zIndex: 1,
270
299
  border: "none",
300
+ padding: 0,
301
+ margin: 0,
271
302
  background: "transparent",
272
303
  cursor: "pointer",
273
304
  textAlign: "left",
274
- padding: "0 6px",
275
- height: CHIP_HEIGHT,
276
- lineHeight: `${CHIP_HEIGHT}px`,
277
- fontSize: 11,
278
- fontWeight: 600,
279
305
  fontFamily: "inherit",
280
- color: theme.textMuted,
306
+ },
307
+ };
308
+
309
+ function chipDefault(theme: DomCalendarTheme): SlotDefault {
310
+ return {
311
+ base: {
312
+ display: "block",
313
+ height: CHIP_HEIGHT,
314
+ lineHeight: `${CHIP_HEIGHT}px`,
315
+ whiteSpace: "nowrap",
316
+ overflow: "hidden",
317
+ textOverflow: "ellipsis",
318
+ },
319
+ themed: {
320
+ padding: "0 6px",
321
+ borderRadius: 4,
322
+ background: theme.eventBackground,
323
+ color: theme.eventText,
324
+ fontSize: 11,
325
+ fontWeight: 600,
326
+ },
327
+ };
328
+ }
329
+
330
+ function moreButtonDefault(theme: DomCalendarTheme): SlotDefault {
331
+ return {
332
+ base: {
333
+ position: "relative",
334
+ zIndex: 1,
335
+ border: "none",
336
+ background: "transparent",
337
+ cursor: "pointer",
338
+ textAlign: "left",
339
+ height: CHIP_HEIGHT,
340
+ lineHeight: `${CHIP_HEIGHT}px`,
341
+ fontFamily: "inherit",
342
+ },
343
+ themed: {
344
+ padding: "0 6px",
345
+ fontSize: 11,
346
+ fontWeight: 600,
347
+ color: theme.textMuted,
348
+ },
281
349
  };
282
350
  }
283
351
 
@@ -299,9 +367,11 @@ interface MonthViewInternalProps<T = unknown> extends MonthViewProps<T> {
299
367
  export function MonthView<T = unknown>({
300
368
  date,
301
369
  weekStartsOn = 0,
370
+ weekdayFormat = "short",
302
371
  events,
303
372
  eventsByDay: eventsByDayProp,
304
373
  renderEvent,
374
+ eventAccessibilityLabel,
305
375
  maxVisibleEventCount = 3,
306
376
  moreLabel = "{moreCount} More",
307
377
  onPressEvent,
@@ -318,11 +388,15 @@ export function MonthView<T = unknown>({
318
388
  maxDate,
319
389
  isDateDisabled,
320
390
  onPressDay,
391
+ onCreateEvent,
321
392
  keyboardDayNavigation = false,
322
393
  className,
323
394
  style,
395
+ classNames,
396
+ styles,
324
397
  }: MonthViewInternalProps<T>): ReactElement {
325
398
  const theme = useMemo(() => mergeDomTheme(themeOverrides), [themeOverrides]);
399
+ const slot = createSlots<MonthViewSlot>({ classNames, styles });
326
400
 
327
401
  // Calendar layout (date in the corner + event chips) is on whenever `events`
328
402
  // is provided; otherwise the compact picker badge layout is used.
@@ -349,6 +423,7 @@ export function MonthView<T = unknown>({
349
423
  () =>
350
424
  buildMonthGrid(date, {
351
425
  weekStartsOn,
426
+ weekdayFormat,
352
427
  selectedRange,
353
428
  selectedDates,
354
429
  minDate,
@@ -356,7 +431,17 @@ export function MonthView<T = unknown>({
356
431
  isDateDisabled,
357
432
  locale,
358
433
  }),
359
- [date, weekStartsOn, selectedRange, selectedDates, minDate, maxDate, isDateDisabled, locale],
434
+ [
435
+ date,
436
+ weekStartsOn,
437
+ weekdayFormat,
438
+ selectedRange,
439
+ selectedDates,
440
+ minDate,
441
+ maxDate,
442
+ isDateDisabled,
443
+ locale,
444
+ ],
360
445
  );
361
446
 
362
447
  // Roving tabindex: only one day is in the tab order; arrow keys move focus
@@ -379,6 +464,45 @@ export function MonthView<T = unknown>({
379
464
  const [hoveredKey, setHoveredKey] = useState<string | null>(null);
380
465
  const gridRef = useRef<HTMLDivElement>(null);
381
466
  const focusKey = format(focusedDate, "yyyy-MM-dd");
467
+
468
+ // Drag-to-create (events mode): press a day and drag across others to sketch an
469
+ // all-day span, released on a window pointerup so a drop anywhere still commits.
470
+ const [creating, setCreating] = useState<{ anchor: number; hover: number } | null>(null);
471
+ const creatingRef = useRef(creating);
472
+ creatingRef.current = creating;
473
+ const movedRef = useRef(false);
474
+ // Set when a drag commits, so the click the browser fires next is swallowed
475
+ // (it must not also open the day via onPressDay).
476
+ const suppressClickRef = useRef(false);
477
+ const beginCreate = (day: Date) => {
478
+ if (!onCreateEvent) return;
479
+ const t = startOfDay(day).getTime();
480
+ movedRef.current = false;
481
+ setCreating({ anchor: t, hover: t });
482
+ };
483
+ const extendCreate = (day: Date) => {
484
+ if (!creatingRef.current) return;
485
+ const t = startOfDay(day).getTime();
486
+ if (t !== creatingRef.current.hover) {
487
+ movedRef.current = true;
488
+ setCreating((c) => (c ? { ...c, hover: t } : c));
489
+ }
490
+ };
491
+ const isCreating = creating !== null;
492
+ useEffect(() => {
493
+ if (!isCreating) return;
494
+ const finish = () => {
495
+ const c = creatingRef.current;
496
+ setCreating(null);
497
+ if (!c || !movedRef.current) return;
498
+ suppressClickRef.current = true;
499
+ const lo = Math.min(c.anchor, c.hover);
500
+ const hi = Math.max(c.anchor, c.hover);
501
+ onCreateEvent?.(new Date(lo), addDays(new Date(hi), 1));
502
+ };
503
+ window.addEventListener("pointerup", finish);
504
+ return () => window.removeEventListener("pointerup", finish);
505
+ }, [isCreating, onCreateEvent]);
382
506
  const onKeyDown = (e: ReactKeyboardEvent<HTMLDivElement>) => {
383
507
  let next: Date | null = null;
384
508
  if (e.key === "ArrowLeft") next = addDays(focusedDate, -1);
@@ -401,23 +525,32 @@ export function MonthView<T = unknown>({
401
525
  style={{ fontFamily: theme.fontFamily, color: theme.text, ...style }}
402
526
  >
403
527
  {showTitle ? (
404
- <div style={{ fontSize: 17, fontWeight: 700, padding: "10px 14px 6px" }}>
528
+ <div
529
+ {...slot("title", {
530
+ themed: { fontSize: 17, fontWeight: 700, padding: "10px 14px 6px" },
531
+ })}
532
+ >
405
533
  {format(date, "MMMM yyyy", locale ? { locale } : undefined)}
406
534
  </div>
407
535
  ) : null}
408
536
  {showWeekdays ? (
409
537
  <div
410
- style={{
411
- display: "grid",
412
- gridTemplateColumns: "repeat(7, minmax(0, 1fr))",
413
- borderBottom: `1px solid ${theme.gridLine}`,
414
- padding: "6px 0",
415
- }}
538
+ {...slot("weekdays", {
539
+ base: { display: "grid", gridTemplateColumns: "repeat(7, minmax(0, 1fr))" },
540
+ themed: { borderBottom: `1px solid ${theme.gridLine}`, padding: "6px 0" },
541
+ })}
416
542
  >
417
543
  {weekdays.map((wd) => (
418
544
  <span
419
545
  key={wd.label}
420
- style={{ textAlign: "center", fontSize: 12, fontWeight: 600, color: theme.textMuted }}
546
+ {...slot("weekday", {
547
+ themed: {
548
+ textAlign: "center",
549
+ fontSize: 12,
550
+ fontWeight: 600,
551
+ color: theme.textMuted,
552
+ },
553
+ })}
421
554
  >
422
555
  {wd.label}
423
556
  </span>
@@ -431,12 +564,15 @@ export function MonthView<T = unknown>({
431
564
  // Arrow-key roving applies whenever the day cells are a tab stop: the
432
565
  // picker always, events mode only when keyboardDayNavigation is set.
433
566
  onKeyDown={dayRoving ? onKeyDown : undefined}
567
+ {...slot("grid")}
434
568
  >
435
569
  {weeks.map((week) => (
436
570
  <div
437
571
  key={week.id}
438
572
  role="row"
439
- style={{ display: "grid", gridTemplateColumns: "repeat(7, minmax(0, 1fr))" }}
573
+ {...slot("week", {
574
+ base: { display: "grid", gridTemplateColumns: "repeat(7, minmax(0, 1fr))" },
575
+ })}
440
576
  >
441
577
  {week.days.map((day) => {
442
578
  const hidden = !showAdjacentMonths && !day.isCurrentMonth;
@@ -446,8 +582,24 @@ export function MonthView<T = unknown>({
446
582
  <div key={day.id} role="gridcell" aria-hidden style={{ height: cellHeight }} />
447
583
  );
448
584
  }
449
- const band = rangeBandStyle(day, theme, fillCellOnSelection);
585
+ const band = rangeBandDefault(day, theme, fillCellOnSelection);
450
586
  const label = format(day.date, "EEEE, d MMMM yyyy", locale ? { locale } : undefined);
587
+ const dayTime = startOfDay(day.date).getTime();
588
+ const inCreate =
589
+ creating != null &&
590
+ dayTime >= Math.min(creating.anchor, creating.hover) &&
591
+ dayTime <= Math.max(creating.anchor, creating.hover);
592
+ // Present/absent data-* attributes so consumers can style day state
593
+ // with CSS/Tailwind variants (e.g. `data-[today]:bg-blue-500`).
594
+ const dayData = dataState({
595
+ "data-today": day.isToday,
596
+ "data-selected": day.isSelected,
597
+ "data-range": day.isInRange,
598
+ "data-weekend": day.isWeekend,
599
+ "data-outside": !day.isCurrentMonth,
600
+ "data-disabled": day.isDisabled,
601
+ "data-creating": inCreate,
602
+ });
451
603
 
452
604
  if (eventsMode) {
453
605
  const dayEvents = eventsByDay.get(startOfDay(day.date).toISOString()) ?? [];
@@ -461,6 +613,7 @@ export function MonthView<T = unknown>({
461
613
  const rest = dayEvents.slice(visible);
462
614
  const overflow = rest.length > 0;
463
615
  const dayLabel = format(day.date, "d MMMM", locale ? { locale } : undefined);
616
+ const dayCellProps = slot("day", eventCellDefault(day, theme, cellHeight));
464
617
  return (
465
618
  // Events mode: by default the cell is not a tab stop, so keyboard
466
619
  // focus moves through the event chips (real buttons) only, not
@@ -471,11 +624,37 @@ export function MonthView<T = unknown>({
471
624
  key={day.id}
472
625
  role="gridcell"
473
626
  data-day={day.id}
627
+ {...dayData}
474
628
  tabIndex={keyboardDayNavigation ? (day.id === focusKey ? 0 : -1) : undefined}
475
629
  aria-disabled={day.isDisabled || undefined}
476
630
  aria-label={label}
477
- style={eventCellStyle(day, theme, cellHeight)}
478
- onClick={day.isDisabled ? undefined : () => onPressDay?.(day.date)}
631
+ {...dayCellProps}
632
+ // Drag-to-create disables touch scroll on the cell so a swipe
633
+ // sketches a span instead of scrolling the grid.
634
+ style={
635
+ onCreateEvent
636
+ ? { ...dayCellProps.style, touchAction: "none" }
637
+ : dayCellProps.style
638
+ }
639
+ onPointerDown={
640
+ onCreateEvent && !day.isDisabled
641
+ ? (e) => {
642
+ if (e.button === 0) beginCreate(day.date);
643
+ }
644
+ : undefined
645
+ }
646
+ onPointerEnter={onCreateEvent ? () => extendCreate(day.date) : undefined}
647
+ onClick={
648
+ day.isDisabled
649
+ ? undefined
650
+ : () => {
651
+ if (suppressClickRef.current) {
652
+ suppressClickRef.current = false;
653
+ return;
654
+ }
655
+ onPressDay?.(day.date);
656
+ }
657
+ }
479
658
  onKeyDown={
480
659
  keyboardDayNavigation && !day.isDisabled
481
660
  ? (e) => {
@@ -489,9 +668,15 @@ export function MonthView<T = unknown>({
489
668
  : undefined
490
669
  }
491
670
  >
492
- {band ? <span data-band aria-hidden style={band} /> : null}
493
- <span style={compactBadgeStyle(day, theme)}>{day.label}</span>
494
- <div style={{ display: "flex", flexDirection: "column", gap: CHIP_GAP }}>
671
+ {band ? <span data-band aria-hidden {...slot("rangeBand", band)} /> : null}
672
+ <span {...dayData} {...slot("dayBadge", compactBadgeDefault(day, theme))}>
673
+ {day.label}
674
+ </span>
675
+ <div
676
+ {...slot("events", {
677
+ base: { display: "flex", flexDirection: "column", gap: CHIP_GAP },
678
+ })}
679
+ >
495
680
  {shown.map((event) => {
496
681
  const onPress = () => onPressEvent?.(event);
497
682
  return (
@@ -502,14 +687,22 @@ export function MonthView<T = unknown>({
502
687
  e.stopPropagation();
503
688
  onPress();
504
689
  }}
505
- style={chipButtonStyle}
690
+ {...slot("chipButton", chipButtonDefault)}
506
691
  title={event.title}
507
- aria-label={`${event.title}, ${dayLabel}`}
692
+ aria-label={
693
+ eventAccessibilityLabel
694
+ ? eventAccessibilityLabel(event, {
695
+ mode: "month",
696
+ isAllDay: isAllDayEvent(event),
697
+ ampm: false,
698
+ })
699
+ : `${event.title}, ${dayLabel}`
700
+ }
508
701
  >
509
702
  {Chip ? (
510
703
  <Chip event={event} onPress={onPress} />
511
704
  ) : (
512
- <span style={chipStyle(theme)}>{event.title}</span>
705
+ <span {...slot("chip", chipDefault(theme))}>{event.title}</span>
513
706
  )}
514
707
  </button>
515
708
  );
@@ -521,7 +714,7 @@ export function MonthView<T = unknown>({
521
714
  e.stopPropagation();
522
715
  onPressMore?.(rest, day.date);
523
716
  }}
524
- style={moreButtonStyle(theme)}
717
+ {...slot("more", moreButtonDefault(theme))}
525
718
  aria-label={`${rest.length} more events, ${dayLabel}`}
526
719
  >
527
720
  {moreLabel.replace("{moreCount}", String(rest.length))}
@@ -538,17 +731,23 @@ export function MonthView<T = unknown>({
538
731
  type="button"
539
732
  role="gridcell"
540
733
  data-day={day.id}
734
+ {...dayData}
541
735
  tabIndex={day.id === focusKey ? 0 : -1}
542
736
  aria-disabled={day.isDisabled || undefined}
543
737
  aria-label={label}
544
738
  aria-pressed={day.isSelected || day.isInRange}
545
- style={dayCellStyle(day, theme)}
739
+ {...slot("day", dayCellDefault(day, theme))}
546
740
  onClick={day.isDisabled ? undefined : () => onPressDay?.(day.date)}
547
741
  onMouseEnter={day.isDisabled ? undefined : () => setHoveredKey(day.id)}
548
742
  onMouseLeave={() => setHoveredKey((k) => (k === day.id ? null : k))}
549
743
  >
550
- {band ? <span data-band aria-hidden style={band} /> : null}
551
- <span style={badgeStyle(day, theme, hoveredKey === day.id)}>{day.label}</span>
744
+ {band ? <span data-band aria-hidden {...slot("rangeBand", band)} /> : null}
745
+ <span
746
+ {...dayData}
747
+ {...slot("dayBadge", badgeDefault(day, theme, hoveredKey === day.id))}
748
+ >
749
+ {day.label}
750
+ </span>
552
751
  </button>
553
752
  );
554
753
  })}