@super-calendar/dom 2.3.2 → 2.5.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/dist/index.d.mts +94 -223
- package/dist/index.d.ts +94 -223
- package/dist/index.js +640 -139
- package/dist/index.mjs +643 -142
- package/package.json +2 -2
- package/src/Calendar.tsx +192 -50
- package/src/ResourceTimeline.tsx +406 -11
- package/src/TimeGrid.tsx +345 -72
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@super-calendar/dom",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Virtualized month / week / day calendar and date picker for react-dom. No React Native, no react-native-web.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"calendar",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"access": "public"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@super-calendar/core": "2.
|
|
51
|
+
"@super-calendar/core": "2.5.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@legendapp/list": ">=3",
|
package/src/Calendar.tsx
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import {
|
|
2
|
+
addDays,
|
|
3
|
+
addMonths,
|
|
4
|
+
endOfDay,
|
|
5
|
+
endOfMonth,
|
|
6
|
+
endOfWeek,
|
|
7
|
+
type Locale,
|
|
8
|
+
startOfDay,
|
|
9
|
+
startOfMonth,
|
|
10
|
+
startOfWeek,
|
|
11
|
+
} from "date-fns";
|
|
12
|
+
import {
|
|
13
|
+
type CSSProperties,
|
|
14
|
+
type KeyboardEvent as ReactKeyboardEvent,
|
|
15
|
+
type ReactElement,
|
|
16
|
+
type ReactNode,
|
|
17
|
+
useMemo,
|
|
18
|
+
} from "react";
|
|
19
|
+
import {
|
|
20
|
+
type BusinessHours,
|
|
21
|
+
type CalendarEvent,
|
|
22
|
+
type DateRange,
|
|
23
|
+
type DateSelectionConstraints,
|
|
24
|
+
type EventAccessibilityLabeler,
|
|
25
|
+
eventsInTimeZone,
|
|
26
|
+
expandRecurringEvents,
|
|
27
|
+
getViewDays,
|
|
28
|
+
type TimeGridMode,
|
|
29
|
+
type WeekdayFormat,
|
|
30
|
+
type WeekStartsOn,
|
|
12
31
|
} from "@super-calendar/core";
|
|
13
32
|
import { Agenda, type AgendaSlot, type DomAgendaEvent } from "./Agenda";
|
|
14
33
|
import { type DomMonthEvent, MonthView, type MonthViewSlot } from "./MonthView";
|
|
@@ -33,6 +52,13 @@ export interface CalendarProps<T = unknown>
|
|
|
33
52
|
mode?: "month" | "schedule" | TimeGridMode;
|
|
34
53
|
/** Controlled anchor date. Change it (e.g. from your own header) to navigate. */
|
|
35
54
|
date: Date;
|
|
55
|
+
/**
|
|
56
|
+
* Fires with the next/previous period's date when the user pages the focused
|
|
57
|
+
* calendar with **PageDown** / **PageUp** (a month in `month`, a week in
|
|
58
|
+
* `schedule`, else the view's day span). It's controlled, so update `date` in
|
|
59
|
+
* response. Omit it to leave paging to your own controls.
|
|
60
|
+
*/
|
|
61
|
+
onChangeDate?: (date: Date) => void;
|
|
36
62
|
/** Your events. */
|
|
37
63
|
events?: CalendarEvent<T>[];
|
|
38
64
|
/** First day of the week. Sunday = 0 (default) … Saturday = 6. */
|
|
@@ -41,6 +67,12 @@ export interface CalendarProps<T = unknown>
|
|
|
41
67
|
numberOfDays?: number;
|
|
42
68
|
/** date-fns locale for titles, headers, and time labels. */
|
|
43
69
|
locale?: Locale;
|
|
70
|
+
/**
|
|
71
|
+
* Display events in this IANA time zone (e.g. `"America/New_York"`), DST-correct
|
|
72
|
+
* and independent of the device zone. Display-only: it shifts the wall-clock the
|
|
73
|
+
* grid lays out from, it doesn't change your `Date`s. Uses `eventsInTimeZone`.
|
|
74
|
+
*/
|
|
75
|
+
timeZone?: string;
|
|
44
76
|
/** Theme overrides; falls back to the default light theme. */
|
|
45
77
|
theme?: Partial<DomCalendarTheme>;
|
|
46
78
|
/** Height of the scroll viewport, in px. */
|
|
@@ -68,6 +100,16 @@ export interface CalendarProps<T = unknown>
|
|
|
68
100
|
scrollOffsetMinutes?: number;
|
|
69
101
|
/** Sub-divisions per hour for the grid lines. */
|
|
70
102
|
timeslots?: number;
|
|
103
|
+
/** First hour shown (0–23). Default 0. */
|
|
104
|
+
minHour?: number;
|
|
105
|
+
/** Last hour shown, exclusive (1–24). Default 24. */
|
|
106
|
+
maxHour?: number;
|
|
107
|
+
/** Hide the left hour-axis column (lines stay, labels/gutter go). Default false. */
|
|
108
|
+
hideHours?: boolean;
|
|
109
|
+
/** Show the ISO week number in the header gutter. Default false. */
|
|
110
|
+
showWeekNumber?: boolean;
|
|
111
|
+
/** Prefix for the week number, e.g. "W" → "W28". Default "W". */
|
|
112
|
+
weekNumberPrefix?: string;
|
|
71
113
|
/** Shade the hours outside business hours. */
|
|
72
114
|
businessHours?: BusinessHours;
|
|
73
115
|
/** Show the current-time indicator (default true). */
|
|
@@ -90,6 +132,11 @@ export interface CalendarProps<T = unknown>
|
|
|
90
132
|
renderTimeEvent?: DomRenderEvent<T>;
|
|
91
133
|
/** Replace the hour-axis label. Receives the hour (0–23) and the `ampm` flag. */
|
|
92
134
|
hourComponent?: (hour: number, ampm: boolean) => ReactNode;
|
|
135
|
+
/**
|
|
136
|
+
* Add arrow-key navigation between time-grid events (a convenience for sighted
|
|
137
|
+
* keyboard users; every event stays individually tabbable). Time-grid modes only.
|
|
138
|
+
*/
|
|
139
|
+
keyboardEventNavigation?: boolean;
|
|
93
140
|
|
|
94
141
|
// --- Month mode ---
|
|
95
142
|
/** Max chips per day before a "+N more" row. */
|
|
@@ -124,6 +171,48 @@ export interface CalendarProps<T = unknown>
|
|
|
124
171
|
renderScheduleEvent?: DomAgendaEvent<T>;
|
|
125
172
|
}
|
|
126
173
|
|
|
174
|
+
// Stable empty array so a missing `events` prop doesn't churn `displayEvents`
|
|
175
|
+
// identity (and bust child memoization) on every re-render.
|
|
176
|
+
const EMPTY_EVENTS: CalendarEvent<never>[] = [];
|
|
177
|
+
|
|
178
|
+
// Recurrence is expanded over the range a mode renders: the month grid for
|
|
179
|
+
// `month`, the day columns for the time-grid modes, and a forward window for the
|
|
180
|
+
// `schedule` agenda (which has no bounded viewport of its own).
|
|
181
|
+
function expansionRange(
|
|
182
|
+
mode: "month" | "schedule" | TimeGridMode,
|
|
183
|
+
date: Date,
|
|
184
|
+
weekStartsOn: WeekStartsOn,
|
|
185
|
+
numberOfDays: number | undefined,
|
|
186
|
+
): [Date, Date] {
|
|
187
|
+
if (mode === "month") {
|
|
188
|
+
return [
|
|
189
|
+
startOfWeek(startOfMonth(date), { weekStartsOn }),
|
|
190
|
+
endOfWeek(endOfMonth(date), { weekStartsOn }),
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
if (mode === "schedule") {
|
|
194
|
+
// The agenda lists forward from the anchor date; three months is a sensible
|
|
195
|
+
// default look-ahead. Pre-expand for a different window.
|
|
196
|
+
return [startOfDay(date), endOfDay(addMonths(date, 3))];
|
|
197
|
+
}
|
|
198
|
+
const days = getViewDays(mode, date, weekStartsOn, numberOfDays ?? 1);
|
|
199
|
+
return [startOfDay(days[0]), endOfDay(days[days.length - 1])];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// The date one page away in `direction` (+1 next, −1 previous): a month for
|
|
203
|
+
// `month`, a week for `schedule`, else the view's day span.
|
|
204
|
+
function pageStep(
|
|
205
|
+
mode: "month" | "schedule" | TimeGridMode,
|
|
206
|
+
date: Date,
|
|
207
|
+
direction: number,
|
|
208
|
+
weekStartsOn: WeekStartsOn,
|
|
209
|
+
numberOfDays: number | undefined,
|
|
210
|
+
): Date {
|
|
211
|
+
if (mode === "month") return addMonths(date, direction);
|
|
212
|
+
if (mode === "schedule") return addDays(date, direction * 7);
|
|
213
|
+
return addDays(date, direction * getViewDays(mode, date, weekStartsOn, numberOfDays ?? 1).length);
|
|
214
|
+
}
|
|
215
|
+
|
|
127
216
|
/**
|
|
128
217
|
* Batteries-included entry point for the react-dom renderer: it picks the right
|
|
129
218
|
* view for `mode`. `month` renders a single {@link MonthView}; `schedule` renders
|
|
@@ -139,9 +228,11 @@ export function Calendar<T = unknown>({
|
|
|
139
228
|
mode = "week",
|
|
140
229
|
date,
|
|
141
230
|
events,
|
|
231
|
+
onChangeDate,
|
|
142
232
|
weekStartsOn = 0,
|
|
143
233
|
numberOfDays,
|
|
144
234
|
locale,
|
|
235
|
+
timeZone,
|
|
145
236
|
theme,
|
|
146
237
|
height,
|
|
147
238
|
className,
|
|
@@ -153,6 +244,11 @@ export function Calendar<T = unknown>({
|
|
|
153
244
|
hourHeight,
|
|
154
245
|
scrollOffsetMinutes,
|
|
155
246
|
timeslots,
|
|
247
|
+
minHour,
|
|
248
|
+
maxHour,
|
|
249
|
+
hideHours,
|
|
250
|
+
showWeekNumber,
|
|
251
|
+
weekNumberPrefix,
|
|
156
252
|
businessHours,
|
|
157
253
|
showNowIndicator,
|
|
158
254
|
showAllDayEventCell,
|
|
@@ -164,6 +260,7 @@ export function Calendar<T = unknown>({
|
|
|
164
260
|
onPressDateHeader,
|
|
165
261
|
renderTimeEvent,
|
|
166
262
|
hourComponent,
|
|
263
|
+
keyboardEventNavigation,
|
|
167
264
|
// month
|
|
168
265
|
maxVisibleEventCount,
|
|
169
266
|
moreLabel,
|
|
@@ -185,10 +282,41 @@ export function Calendar<T = unknown>({
|
|
|
185
282
|
classNames,
|
|
186
283
|
styles,
|
|
187
284
|
}: CalendarProps<T>): ReactElement {
|
|
285
|
+
// Materialise recurring events over the range this mode renders, then apply the
|
|
286
|
+
// display zone. Non-recurring and already-expanded events pass through
|
|
287
|
+
// untouched, so identity is preserved for the common (no-recurrence) case.
|
|
288
|
+
const displayEvents = useMemo(() => {
|
|
289
|
+
let out: CalendarEvent<T>[] = events ?? EMPTY_EVENTS;
|
|
290
|
+
if (out.some((e) => e.recurrence)) {
|
|
291
|
+
let [start, end] = expansionRange(mode, date, weekStartsOn, numberOfDays);
|
|
292
|
+
// A display zone shifts each occurrence's wall-clock by up to ~a day, which
|
|
293
|
+
// can move an edge occurrence into a visible column; widen so it's generated.
|
|
294
|
+
if (timeZone) {
|
|
295
|
+
start = addDays(start, -1);
|
|
296
|
+
end = addDays(end, 1);
|
|
297
|
+
}
|
|
298
|
+
out = expandRecurringEvents(out, start, end);
|
|
299
|
+
}
|
|
300
|
+
if (timeZone) out = eventsInTimeZone(out, timeZone);
|
|
301
|
+
return out;
|
|
302
|
+
// `date.getTime()`: recompute on the instant, not a re-created Date identity.
|
|
303
|
+
}, [events, mode, date.getTime(), weekStartsOn, numberOfDays, timeZone]);
|
|
304
|
+
|
|
305
|
+
// PageDown / PageUp page the calendar when the consumer opts in with
|
|
306
|
+
// `onChangeDate`; keydowns bubble up from the focused grid to this handler.
|
|
307
|
+
const handlePageKeys = (e: ReactKeyboardEvent) => {
|
|
308
|
+
if (!onChangeDate) return;
|
|
309
|
+
if (e.key === "PageDown") onChangeDate(pageStep(mode, date, 1, weekStartsOn, numberOfDays));
|
|
310
|
+
else if (e.key === "PageUp") onChangeDate(pageStep(mode, date, -1, weekStartsOn, numberOfDays));
|
|
311
|
+
else return;
|
|
312
|
+
e.preventDefault();
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
let view: ReactElement;
|
|
188
316
|
if (mode === "schedule") {
|
|
189
|
-
|
|
317
|
+
view = (
|
|
190
318
|
<Agenda<T>
|
|
191
|
-
events={
|
|
319
|
+
events={displayEvents}
|
|
192
320
|
locale={locale}
|
|
193
321
|
ampm={ampm}
|
|
194
322
|
theme={theme}
|
|
@@ -204,13 +332,11 @@ export function Calendar<T = unknown>({
|
|
|
204
332
|
onPressDay={onPressDay}
|
|
205
333
|
/>
|
|
206
334
|
);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (mode === "month") {
|
|
210
|
-
return (
|
|
335
|
+
} else if (mode === "month") {
|
|
336
|
+
view = (
|
|
211
337
|
<MonthView<T>
|
|
212
338
|
date={date}
|
|
213
|
-
events={
|
|
339
|
+
events={displayEvents}
|
|
214
340
|
weekStartsOn={weekStartsOn}
|
|
215
341
|
weekdayFormat={weekdayFormat}
|
|
216
342
|
locale={locale}
|
|
@@ -237,40 +363,56 @@ export function Calendar<T = unknown>({
|
|
|
237
363
|
eventAccessibilityLabel={eventAccessibilityLabel}
|
|
238
364
|
/>
|
|
239
365
|
);
|
|
366
|
+
} else {
|
|
367
|
+
view = (
|
|
368
|
+
<TimeGrid<T>
|
|
369
|
+
date={date}
|
|
370
|
+
mode={mode}
|
|
371
|
+
events={displayEvents}
|
|
372
|
+
weekStartsOn={weekStartsOn}
|
|
373
|
+
weekdayFormat={weekdayFormat}
|
|
374
|
+
numberOfDays={numberOfDays}
|
|
375
|
+
locale={locale}
|
|
376
|
+
theme={theme}
|
|
377
|
+
height={height}
|
|
378
|
+
className={className}
|
|
379
|
+
style={style}
|
|
380
|
+
classNames={classNames}
|
|
381
|
+
styles={styles}
|
|
382
|
+
ampm={ampm}
|
|
383
|
+
hourHeight={hourHeight}
|
|
384
|
+
scrollOffsetMinutes={scrollOffsetMinutes}
|
|
385
|
+
timeslots={timeslots}
|
|
386
|
+
minHour={minHour}
|
|
387
|
+
maxHour={maxHour}
|
|
388
|
+
hideHours={hideHours}
|
|
389
|
+
showWeekNumber={showWeekNumber}
|
|
390
|
+
weekNumberPrefix={weekNumberPrefix}
|
|
391
|
+
businessHours={businessHours}
|
|
392
|
+
showNowIndicator={showNowIndicator}
|
|
393
|
+
showAllDayEventCell={showAllDayEventCell}
|
|
394
|
+
dragStepMinutes={dragStepMinutes}
|
|
395
|
+
onPressEvent={onPressEvent}
|
|
396
|
+
onPressCell={onPressCell}
|
|
397
|
+
onCreateEvent={onCreateEvent}
|
|
398
|
+
onDragStart={onDragStart}
|
|
399
|
+
onDragEvent={onDragEvent}
|
|
400
|
+
onPressDateHeader={onPressDateHeader}
|
|
401
|
+
renderEvent={renderTimeEvent}
|
|
402
|
+
eventAccessibilityLabel={eventAccessibilityLabel}
|
|
403
|
+
hourComponent={hourComponent}
|
|
404
|
+
keyboardEventNavigation={keyboardEventNavigation}
|
|
405
|
+
/>
|
|
406
|
+
);
|
|
240
407
|
}
|
|
241
408
|
|
|
409
|
+
// `display: contents` adds no layout box, so the view renders exactly as before
|
|
410
|
+
// while keydowns from the focused grid still bubble to the paging handler (a
|
|
411
|
+
// no-op when `onChangeDate` is unset). Rendered unconditionally so toggling the
|
|
412
|
+
// handler never remounts the view subtree.
|
|
242
413
|
return (
|
|
243
|
-
<
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
events={events}
|
|
247
|
-
weekStartsOn={weekStartsOn}
|
|
248
|
-
weekdayFormat={weekdayFormat}
|
|
249
|
-
numberOfDays={numberOfDays}
|
|
250
|
-
locale={locale}
|
|
251
|
-
theme={theme}
|
|
252
|
-
height={height}
|
|
253
|
-
className={className}
|
|
254
|
-
style={style}
|
|
255
|
-
classNames={classNames}
|
|
256
|
-
styles={styles}
|
|
257
|
-
ampm={ampm}
|
|
258
|
-
hourHeight={hourHeight}
|
|
259
|
-
scrollOffsetMinutes={scrollOffsetMinutes}
|
|
260
|
-
timeslots={timeslots}
|
|
261
|
-
businessHours={businessHours}
|
|
262
|
-
showNowIndicator={showNowIndicator}
|
|
263
|
-
showAllDayEventCell={showAllDayEventCell}
|
|
264
|
-
dragStepMinutes={dragStepMinutes}
|
|
265
|
-
onPressEvent={onPressEvent}
|
|
266
|
-
onPressCell={onPressCell}
|
|
267
|
-
onCreateEvent={onCreateEvent}
|
|
268
|
-
onDragStart={onDragStart}
|
|
269
|
-
onDragEvent={onDragEvent}
|
|
270
|
-
onPressDateHeader={onPressDateHeader}
|
|
271
|
-
renderEvent={renderTimeEvent}
|
|
272
|
-
eventAccessibilityLabel={eventAccessibilityLabel}
|
|
273
|
-
hourComponent={hourComponent}
|
|
274
|
-
/>
|
|
414
|
+
<div style={{ display: "contents" }} onKeyDown={handlePageKeys}>
|
|
415
|
+
{view}
|
|
416
|
+
</div>
|
|
275
417
|
);
|
|
276
418
|
}
|