@object-ui/plugin-calendar 3.1.5 → 3.3.1

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +21 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +27 -17
  5. package/dist/index.umd.cjs +2 -2
  6. package/dist/packages/plugin-calendar/src/CalendarView.d.ts.map +1 -0
  7. package/dist/packages/plugin-calendar/src/ObjectCalendar.d.ts.map +1 -0
  8. package/dist/packages/plugin-calendar/src/calendar-view-renderer.d.ts.map +1 -0
  9. package/dist/packages/plugin-calendar/src/index.d.ts.map +1 -0
  10. package/package.json +37 -14
  11. package/.turbo/turbo-build.log +0 -22
  12. package/dist/src/CalendarView.d.ts.map +0 -1
  13. package/dist/src/ObjectCalendar.d.ts.map +0 -1
  14. package/dist/src/calendar-view-renderer.d.ts.map +0 -1
  15. package/dist/src/index.d.ts.map +0 -1
  16. package/src/CalendarView.test.tsx +0 -118
  17. package/src/CalendarView.tsx +0 -821
  18. package/src/ObjectCalendar.msw.test.tsx +0 -100
  19. package/src/ObjectCalendar.stories.tsx +0 -82
  20. package/src/ObjectCalendar.tsx +0 -420
  21. package/src/__tests__/accessibility.test.tsx +0 -290
  22. package/src/__tests__/calendar-bugfixes.test.tsx +0 -230
  23. package/src/__tests__/calendar-optimizations.test.tsx +0 -178
  24. package/src/__tests__/performance-benchmark.test.tsx +0 -227
  25. package/src/__tests__/view-states.test.tsx +0 -377
  26. package/src/calendar-view-renderer.tsx +0 -181
  27. package/src/index.tsx +0 -50
  28. package/src/registration.test.tsx +0 -41
  29. package/test/setup.ts +0 -32
  30. package/tsconfig.json +0 -18
  31. package/vite.config.ts +0 -56
  32. package/vitest.config.ts +0 -13
  33. package/vitest.setup.ts +0 -1
  34. /package/dist/{src → packages/plugin-calendar/src}/CalendarView.d.ts +0 -0
  35. /package/dist/{src → packages/plugin-calendar/src}/ObjectCalendar.d.ts +0 -0
  36. /package/dist/{src → packages/plugin-calendar/src}/calendar-view-renderer.d.ts +0 -0
  37. /package/dist/{src → packages/plugin-calendar/src}/index.d.ts +0 -0
@@ -1,227 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- *
8
- * Performance benchmark tests for CalendarView.
9
- * Part of P2.4 Performance at Scale roadmap.
10
- */
11
-
12
- import { describe, it, expect, vi } from 'vitest';
13
- import { render, screen } from '@testing-library/react';
14
- import '@testing-library/jest-dom';
15
- import React from 'react';
16
- import { CalendarView, type CalendarEvent, type CalendarViewProps } from '../CalendarView';
17
-
18
- // Mock @object-ui/components
19
- vi.mock('@object-ui/components', () => ({
20
- cn: (...classes: (string | undefined | boolean)[]) => classes.filter(Boolean).join(' '),
21
- Button: ({ children, ...props }: any) => <button {...props}>{children}</button>,
22
- Select: ({ children, value, onValueChange }: any) => <div data-testid="select">{children}</div>,
23
- SelectContent: ({ children }: any) => <div>{children}</div>,
24
- SelectItem: ({ children, value }: any) => <div data-value={value}>{children}</div>,
25
- SelectTrigger: ({ children }: any) => <div>{children}</div>,
26
- SelectValue: () => <span>Month</span>,
27
- Calendar: (props: any) => <div data-testid="calendar-picker">Calendar Picker</div>,
28
- Popover: ({ children }: any) => <div>{children}</div>,
29
- PopoverContent: ({ children }: any) => <div>{children}</div>,
30
- PopoverTrigger: ({ children }: any) => <div>{children}</div>,
31
- }));
32
-
33
- // Mock lucide-react icons
34
- vi.mock('lucide-react', () => ({
35
- ChevronLeftIcon: (props: any) => <svg data-testid="chevron-left" {...props} />,
36
- ChevronRightIcon: (props: any) => <svg data-testid="chevron-right" {...props} />,
37
- CalendarIcon: (props: any) => <svg data-testid="calendar-icon" {...props} />,
38
- PlusIcon: (props: any) => <svg data-testid="plus-icon" {...props} />,
39
- }));
40
-
41
- // Mock ResizeObserver
42
- class ResizeObserver {
43
- observe() {}
44
- unobserve() {}
45
- disconnect() {}
46
- }
47
- global.ResizeObserver = ResizeObserver;
48
-
49
- // --- Data generators ---
50
-
51
- const baseDate = new Date(2024, 0, 15); // Jan 15, 2024
52
-
53
- function generateEvents(count: number): CalendarEvent[] {
54
- const events: CalendarEvent[] = [];
55
- for (let i = 0; i < count; i++) {
56
- const day = (i % 28) + 1;
57
- const hour = (i % 12) + 8;
58
- events.push({
59
- id: `event-${i}`,
60
- title: `Event ${i}`,
61
- start: new Date(2024, 0, day, hour, 0),
62
- end: new Date(2024, 0, day, hour + 1, 0),
63
- allDay: i % 10 === 0,
64
- });
65
- }
66
- return events;
67
- }
68
-
69
- function generateMultiDayEvents(count: number): CalendarEvent[] {
70
- const events: CalendarEvent[] = [];
71
- for (let i = 0; i < count; i++) {
72
- const startDay = (i % 25) + 1;
73
- const spanDays = (i % 4) + 2; // 2-5 day span
74
- events.push({
75
- id: `multi-event-${i}`,
76
- title: `Multi-day Event ${i}`,
77
- start: new Date(2024, 0, startDay, 9, 0),
78
- end: new Date(2024, 0, startDay + spanDays, 17, 0),
79
- allDay: true,
80
- });
81
- }
82
- return events;
83
- }
84
-
85
- function renderCalendar(overrides: Partial<CalendarViewProps> = {}) {
86
- const props: CalendarViewProps = {
87
- currentDate: baseDate,
88
- locale: 'en-US',
89
- ...overrides,
90
- };
91
- return render(<CalendarView {...props} />);
92
- }
93
-
94
- // =========================================================================
95
- // Performance Benchmarks
96
- // =========================================================================
97
-
98
- describe('CalendarView: performance benchmarks', () => {
99
- it('renders with 100 events under 500ms', () => {
100
- const events = generateEvents(100);
101
-
102
- const start = performance.now();
103
- const { container } = renderCalendar({ events });
104
- const elapsed = performance.now() - start;
105
-
106
- expect(container).toBeTruthy();
107
- expect(elapsed).toBeLessThan(500);
108
- });
109
-
110
- it('renders with 500 events under 1,000ms', () => {
111
- const events = generateEvents(500);
112
-
113
- const start = performance.now();
114
- const { container } = renderCalendar({ events });
115
- const elapsed = performance.now() - start;
116
-
117
- expect(container).toBeTruthy();
118
- expect(elapsed).toBeLessThan(1_000);
119
- });
120
-
121
- it('renders with 1,000 events under 2,000ms', () => {
122
- const events = generateEvents(1_000);
123
-
124
- const start = performance.now();
125
- const { container } = renderCalendar({ events });
126
- const elapsed = performance.now() - start;
127
-
128
- expect(container).toBeTruthy();
129
- expect(elapsed).toBeLessThan(2_000);
130
- });
131
-
132
- it('renders with no events instantly', () => {
133
- const start = performance.now();
134
- const { container } = renderCalendar({ events: [] });
135
- const elapsed = performance.now() - start;
136
-
137
- expect(container).toBeTruthy();
138
- expect(elapsed).toBeLessThan(200);
139
- });
140
-
141
- it('data generation for 1,000 events is fast (< 100ms)', () => {
142
- const start = performance.now();
143
- const events = generateEvents(1_000);
144
- const elapsed = performance.now() - start;
145
-
146
- expect(events).toHaveLength(1_000);
147
- expect(elapsed).toBeLessThan(100);
148
- });
149
- });
150
-
151
- // =========================================================================
152
- // Multi-day event scaling
153
- // =========================================================================
154
-
155
- describe('CalendarView: multi-day event performance', () => {
156
- it('renders 100 multi-day events under 500ms', () => {
157
- const events = generateMultiDayEvents(100);
158
-
159
- const start = performance.now();
160
- const { container } = renderCalendar({ events });
161
- const elapsed = performance.now() - start;
162
-
163
- expect(container).toBeTruthy();
164
- expect(elapsed).toBeLessThan(500);
165
- });
166
-
167
- it('renders 500 multi-day events under 1,500ms', () => {
168
- const events = generateMultiDayEvents(500);
169
-
170
- const start = performance.now();
171
- const { container } = renderCalendar({ events });
172
- const elapsed = performance.now() - start;
173
-
174
- expect(container).toBeTruthy();
175
- expect(elapsed).toBeLessThan(1_500);
176
- });
177
-
178
- it('renders mix of single-day and multi-day events at scale', () => {
179
- const singleDay = generateEvents(500);
180
- const multiDay = generateMultiDayEvents(500);
181
- const events = [...singleDay, ...multiDay];
182
-
183
- const start = performance.now();
184
- const { container } = renderCalendar({ events });
185
- const elapsed = performance.now() - start;
186
-
187
- expect(container).toBeTruthy();
188
- expect(elapsed).toBeLessThan(2_000);
189
- });
190
- });
191
-
192
- // =========================================================================
193
- // Scaling across views
194
- // =========================================================================
195
-
196
- describe('CalendarView: scaling across views', () => {
197
- it('renders month view with 500 events under 1,000ms', () => {
198
- const events = generateEvents(500);
199
-
200
- const start = performance.now();
201
- renderCalendar({ events, view: 'month' });
202
- const elapsed = performance.now() - start;
203
-
204
- expect(elapsed).toBeLessThan(1_000);
205
- });
206
-
207
- it('renders day view with 500 events under 500ms', () => {
208
- const events = generateEvents(500);
209
-
210
- const start = performance.now();
211
- renderCalendar({ events, view: 'day' });
212
- const elapsed = performance.now() - start;
213
-
214
- expect(elapsed).toBeLessThan(500);
215
- });
216
-
217
- it('handles events with all-day flag at scale', () => {
218
- const events = generateEvents(200).map((e) => ({ ...e, allDay: true }));
219
-
220
- const start = performance.now();
221
- const { container } = renderCalendar({ events });
222
- const elapsed = performance.now() - start;
223
-
224
- expect(container).toBeTruthy();
225
- expect(elapsed).toBeLessThan(1_000);
226
- });
227
- });
@@ -1,377 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- /**
10
- * P3.3 Plugin View Robustness - Calendar View States
11
- *
12
- * Tests empty, populated, and edge-case states for CalendarView component.
13
- */
14
-
15
- import { describe, it, expect, vi } from 'vitest';
16
- import { render, screen, fireEvent } from '@testing-library/react';
17
- import '@testing-library/jest-dom';
18
- import React from 'react';
19
- import { CalendarView, type CalendarEvent } from '../CalendarView';
20
-
21
- // Mock ResizeObserver
22
- class ResizeObserver {
23
- observe() {}
24
- unobserve() {}
25
- disconnect() {}
26
- }
27
- global.ResizeObserver = ResizeObserver;
28
-
29
- // Mock PointerEvents for Radix
30
- if (!global.PointerEvent) {
31
- class PointerEvent extends Event {
32
- button: number;
33
- ctrlKey: boolean;
34
- metaKey: boolean;
35
- shiftKey: boolean;
36
- constructor(type: string, props: any = {}) {
37
- super(type, props);
38
- this.button = props.button || 0;
39
- this.ctrlKey = props.ctrlKey || false;
40
- this.metaKey = props.metaKey || false;
41
- this.shiftKey = props.shiftKey || false;
42
- }
43
- }
44
- // @ts-expect-error Mocking global PointerEvent
45
- global.PointerEvent = PointerEvent as any;
46
- }
47
-
48
- // Mock HTMLElement.offsetParent for Radix Popper
49
- Object.defineProperty(HTMLElement.prototype, 'offsetParent', {
50
- get() {
51
- return this.parentElement;
52
- },
53
- });
54
-
55
- const defaultDate = new Date(2024, 0, 15); // Jan 15, 2024
56
-
57
- describe('P3.3 Calendar View States', () => {
58
- // ---------------------------------------------------------------
59
- // Empty state (no events)
60
- // ---------------------------------------------------------------
61
- describe('empty state', () => {
62
- it('renders header with no events', () => {
63
- render(<CalendarView currentDate={defaultDate} events={[]} locale="en-US" />);
64
- expect(screen.getByText('January 2024')).toBeInTheDocument();
65
- });
66
-
67
- it('renders navigation controls with no events', () => {
68
- render(<CalendarView currentDate={defaultDate} events={[]} locale="en-US" />);
69
- expect(screen.getByText('Today')).toBeInTheDocument();
70
- });
71
-
72
- it('renders day headers in month view with no events', () => {
73
- render(<CalendarView currentDate={defaultDate} events={[]} locale="en-US" view="month" />);
74
- expect(screen.getByText('Sun')).toBeInTheDocument();
75
- expect(screen.getByText('Mon')).toBeInTheDocument();
76
- expect(screen.getByText('Sat')).toBeInTheDocument();
77
- });
78
-
79
- it('renders with undefined events prop', () => {
80
- render(<CalendarView currentDate={defaultDate} locale="en-US" />);
81
- expect(screen.getByText('January 2024')).toBeInTheDocument();
82
- });
83
- });
84
-
85
- // ---------------------------------------------------------------
86
- // Populated state
87
- // ---------------------------------------------------------------
88
- describe('populated state', () => {
89
- const events: CalendarEvent[] = [
90
- {
91
- id: '1',
92
- title: 'Team Meeting',
93
- start: new Date(2024, 0, 15, 10, 0),
94
- end: new Date(2024, 0, 15, 11, 0),
95
- },
96
- {
97
- id: '2',
98
- title: 'Lunch Break',
99
- start: new Date(2024, 0, 15, 12, 0),
100
- end: new Date(2024, 0, 15, 13, 0),
101
- },
102
- ];
103
-
104
- it('renders events in month view', () => {
105
- render(
106
- <CalendarView
107
- currentDate={defaultDate}
108
- events={events}
109
- view="month"
110
- locale="en-US"
111
- />
112
- );
113
- expect(screen.getByText('Team Meeting')).toBeInTheDocument();
114
- expect(screen.getByText('Lunch Break')).toBeInTheDocument();
115
- });
116
-
117
- it('fires onEventClick when event is clicked', () => {
118
- const onClick = vi.fn();
119
- render(
120
- <CalendarView
121
- currentDate={defaultDate}
122
- events={events}
123
- view="month"
124
- locale="en-US"
125
- onEventClick={onClick}
126
- />
127
- );
128
- fireEvent.click(screen.getByText('Team Meeting'));
129
- expect(onClick).toHaveBeenCalledTimes(1);
130
- expect(onClick).toHaveBeenCalledWith(events[0]);
131
- });
132
- });
133
-
134
- // ---------------------------------------------------------------
135
- // Navigation
136
- // ---------------------------------------------------------------
137
- describe('navigation', () => {
138
- it('navigates to previous month', () => {
139
- const onNavigate = vi.fn();
140
- render(
141
- <CalendarView
142
- currentDate={defaultDate}
143
- events={[]}
144
- locale="en-US"
145
- onNavigate={onNavigate}
146
- />
147
- );
148
- // Click prev button (first icon button after Today)
149
- const buttons = screen.getAllByRole('button');
150
- // Find the prev chevron - it's the button after Today
151
- const todayIdx = buttons.findIndex(b => b.textContent === 'Today');
152
- fireEvent.click(buttons[todayIdx + 1]);
153
- expect(onNavigate).toHaveBeenCalledTimes(1);
154
- });
155
-
156
- it('navigates to today', () => {
157
- const onNavigate = vi.fn();
158
- render(
159
- <CalendarView
160
- currentDate={new Date(2024, 5, 15)}
161
- events={[]}
162
- locale="en-US"
163
- onNavigate={onNavigate}
164
- />
165
- );
166
- fireEvent.click(screen.getByText('Today'));
167
- expect(onNavigate).toHaveBeenCalledTimes(1);
168
- });
169
- });
170
-
171
- // ---------------------------------------------------------------
172
- // View modes
173
- // ---------------------------------------------------------------
174
- describe('view modes', () => {
175
- it('renders month view by default', () => {
176
- render(<CalendarView currentDate={defaultDate} events={[]} locale="en-US" />);
177
- // Month view has day-of-week headers
178
- expect(screen.getByText('Sun')).toBeInTheDocument();
179
- expect(screen.getByText('Mon')).toBeInTheDocument();
180
- });
181
-
182
- it('renders week view', () => {
183
- render(
184
- <CalendarView
185
- currentDate={defaultDate}
186
- events={[]}
187
- view="week"
188
- locale="en-US"
189
- />
190
- );
191
- // Week view should show a range header and a 7-column grid
192
- const grids = screen.getAllByRole('grid');
193
- expect(grids.length).toBeGreaterThanOrEqual(1);
194
- });
195
-
196
- it('renders day view', () => {
197
- render(
198
- <CalendarView
199
- currentDate={defaultDate}
200
- events={[]}
201
- view="day"
202
- locale="en-US"
203
- />
204
- );
205
- // Day view shows time slots
206
- expect(screen.getByText('12 AM')).toBeInTheDocument();
207
- expect(screen.getByText('12 PM')).toBeInTheDocument();
208
- });
209
- });
210
-
211
- // ---------------------------------------------------------------
212
- // Edge cases
213
- // ---------------------------------------------------------------
214
- describe('edge cases', () => {
215
- it('handles event spanning multiple days', () => {
216
- const multiDayEvent: CalendarEvent[] = [
217
- {
218
- id: 'multi',
219
- title: 'Conference',
220
- start: new Date(2024, 0, 15),
221
- end: new Date(2024, 0, 17),
222
- allDay: true,
223
- },
224
- ];
225
- render(
226
- <CalendarView
227
- currentDate={defaultDate}
228
- events={multiDayEvent}
229
- view="month"
230
- locale="en-US"
231
- />
232
- );
233
- // Multi-day events render on each day they span
234
- const elements = screen.getAllByText('Conference');
235
- expect(elements.length).toBeGreaterThanOrEqual(1);
236
- });
237
-
238
- it('handles event with color', () => {
239
- const coloredEvents: CalendarEvent[] = [
240
- {
241
- id: 'red',
242
- title: 'Red Event',
243
- start: new Date(2024, 0, 15, 9, 0),
244
- color: 'bg-red-500 text-white',
245
- },
246
- ];
247
- render(
248
- <CalendarView
249
- currentDate={defaultDate}
250
- events={coloredEvents}
251
- view="month"
252
- locale="en-US"
253
- />
254
- );
255
- expect(screen.getByText('Red Event')).toBeInTheDocument();
256
- });
257
-
258
- it('renders onAddClick button when provided', () => {
259
- const onAdd = vi.fn();
260
- render(
261
- <CalendarView
262
- currentDate={defaultDate}
263
- events={[]}
264
- locale="en-US"
265
- onAddClick={onAdd}
266
- />
267
- );
268
- // The "New event" button should be visible
269
- const newButton = screen.getByText('New event');
270
- expect(newButton).toBeInTheDocument();
271
- fireEvent.click(newButton);
272
- expect(onAdd).toHaveBeenCalledTimes(1);
273
- });
274
-
275
- it('does not render New button without onAddClick', () => {
276
- render(
277
- <CalendarView
278
- currentDate={defaultDate}
279
- events={[]}
280
- locale="en-US"
281
- />
282
- );
283
- expect(screen.queryByText('New event')).not.toBeInTheDocument();
284
- });
285
-
286
- it('accepts className prop', () => {
287
- const { container } = render(
288
- <CalendarView
289
- currentDate={defaultDate}
290
- events={[]}
291
- locale="en-US"
292
- className="my-calendar"
293
- />
294
- );
295
- expect(container.firstElementChild!.className).toContain('my-calendar');
296
- });
297
-
298
- it('handles many events without crashing', () => {
299
- const manyEvents: CalendarEvent[] = Array.from({ length: 100 }, (_, i) => ({
300
- id: String(i),
301
- title: `Event ${i}`,
302
- start: new Date(2024, 0, (i % 28) + 1, 9, 0),
303
- }));
304
- const { container } = render(
305
- <CalendarView
306
- currentDate={defaultDate}
307
- events={manyEvents}
308
- view="month"
309
- locale="en-US"
310
- />
311
- );
312
- expect(container).toBeInTheDocument();
313
- });
314
-
315
- it('handles date at month boundary (last day of month)', () => {
316
- const boundaryDate = new Date(2024, 0, 31); // Jan 31, 2024
317
- render(
318
- <CalendarView
319
- currentDate={boundaryDate}
320
- events={[]}
321
- view="month"
322
- locale="en-US"
323
- />
324
- );
325
- expect(screen.getByText('January 2024')).toBeInTheDocument();
326
- });
327
-
328
- it('handles date at month boundary (first day of month)', () => {
329
- const firstDay = new Date(2024, 1, 1); // Feb 1, 2024
330
- render(
331
- <CalendarView
332
- currentDate={firstDay}
333
- events={[]}
334
- view="month"
335
- locale="en-US"
336
- />
337
- );
338
- expect(screen.getByText('February 2024')).toBeInTheDocument();
339
- });
340
-
341
- it('handles leap year date', () => {
342
- const leapDate = new Date(2024, 1, 29); // Feb 29, 2024 (leap year)
343
- render(
344
- <CalendarView
345
- currentDate={leapDate}
346
- events={[]}
347
- view="month"
348
- locale="en-US"
349
- />
350
- );
351
- expect(screen.getByText('February 2024')).toBeInTheDocument();
352
- });
353
-
354
- it('handles events spanning across month boundary', () => {
355
- const crossMonthEvent: CalendarEvent[] = [
356
- {
357
- id: 'cross',
358
- title: 'Cross-Month Event',
359
- start: new Date(2024, 0, 30),
360
- end: new Date(2024, 1, 2),
361
- allDay: true,
362
- },
363
- ];
364
- const { container } = render(
365
- <CalendarView
366
- currentDate={new Date(2024, 0, 15)}
367
- events={crossMonthEvent}
368
- view="month"
369
- locale="en-US"
370
- />
371
- );
372
- // Should render without crashing; the event spans into next month padding days
373
- const elements = container.querySelectorAll('[role="button"]');
374
- expect(elements.length).toBeGreaterThanOrEqual(1);
375
- });
376
- });
377
- });