@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/README.md +17 -0
- package/dist/index.d.mts +327 -12
- package/dist/index.d.ts +327 -12
- package/dist/index.js +1111 -286
- package/dist/index.mjs +1087 -289
- package/package.json +2 -2
- package/src/Agenda.tsx +77 -46
- package/src/Calendar.tsx +40 -4
- package/src/DateRangePicker.tsx +360 -0
- package/src/MonthList.tsx +43 -11
- package/src/MonthView.tsx +329 -130
- package/src/ResourceTimeline.tsx +431 -0
- package/src/TimeGrid.tsx +247 -148
- package/src/index.ts +37 -3
- package/src/slots.ts +87 -0
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>
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
180
|
+
function rangeBandDefault(
|
|
134
181
|
day: MonthGridDay,
|
|
135
182
|
theme: DomCalendarTheme,
|
|
136
183
|
fillCell: boolean,
|
|
137
|
-
):
|
|
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
|
|
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
|
-
|
|
159
|
-
|
|
204
|
+
base.borderTopLeftRadius = radius;
|
|
205
|
+
base.borderBottomLeftRadius = radius;
|
|
160
206
|
}
|
|
161
207
|
if (rounding.end) {
|
|
162
|
-
|
|
163
|
-
|
|
208
|
+
base.borderTopRightRadius = radius;
|
|
209
|
+
base.borderBottomRightRadius = radius;
|
|
164
210
|
}
|
|
165
|
-
return
|
|
211
|
+
return { base, themed: { background: theme.rangeBackground } };
|
|
166
212
|
}
|
|
167
213
|
|
|
168
|
-
function
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
244
|
+
function eventCellDefault(day: MonthGridDay, theme: DomCalendarTheme, height: number): SlotDefault {
|
|
195
245
|
return {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
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
|
|
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
|
-
|
|
411
|
-
display: "grid",
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
478
|
-
|
|
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
|
|
493
|
-
<span
|
|
494
|
-
|
|
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
|
-
|
|
690
|
+
{...slot("chipButton", chipButtonDefault)}
|
|
506
691
|
title={event.title}
|
|
507
|
-
aria-label={
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
551
|
-
<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
|
})}
|