@object-ui/plugin-calendar 3.0.3 → 3.1.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.
- package/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +13 -0
- package/dist/index.js +959 -618
- package/dist/index.umd.cjs +2 -2
- package/dist/src/CalendarView.d.ts.map +1 -1
- package/dist/src/ObjectCalendar.d.ts +2 -0
- package/dist/src/ObjectCalendar.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +11 -10
- package/src/CalendarView.test.tsx +1 -1
- package/src/CalendarView.tsx +117 -25
- package/src/ObjectCalendar.tsx +20 -14
- package/src/__tests__/accessibility.test.tsx +2 -2
- package/src/__tests__/calendar-bugfixes.test.tsx +230 -0
- package/src/__tests__/calendar-optimizations.test.tsx +178 -0
- package/src/__tests__/view-states.test.tsx +3 -3
- package/src/calendar-view-renderer.tsx +4 -10
- package/src/index.tsx +3 -3
package/src/ObjectCalendar.tsx
CHANGED
|
@@ -22,12 +22,13 @@
|
|
|
22
22
|
* - Works with object/api/value data providers
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
|
25
|
+
import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react';
|
|
26
26
|
import type { ObjectGridSchema, DataSource, ViewData, CalendarConfig } from '@object-ui/types';
|
|
27
27
|
import { CalendarView, type CalendarEvent } from './CalendarView';
|
|
28
28
|
import { usePullToRefresh } from '@object-ui/mobile';
|
|
29
29
|
import { useNavigationOverlay } from '@object-ui/react';
|
|
30
30
|
import { NavigationOverlay } from '@object-ui/components';
|
|
31
|
+
import { extractRecords, buildExpandFields } from '@object-ui/core';
|
|
31
32
|
|
|
32
33
|
export interface CalendarSchema {
|
|
33
34
|
type: 'calendar';
|
|
@@ -53,6 +54,8 @@ export interface ObjectCalendarProps {
|
|
|
53
54
|
onDelete?: (record: any) => void;
|
|
54
55
|
onNavigate?: (date: Date) => void;
|
|
55
56
|
onViewChange?: (view: 'month' | 'week' | 'day') => void;
|
|
57
|
+
onEventDrop?: (record: any, newStart: Date, newEnd?: Date) => void;
|
|
58
|
+
locale?: string;
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
/**
|
|
@@ -144,6 +147,8 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
|
|
|
144
147
|
onDateClick,
|
|
145
148
|
onNavigate,
|
|
146
149
|
onViewChange,
|
|
150
|
+
onEventDrop,
|
|
151
|
+
locale,
|
|
147
152
|
...rest
|
|
148
153
|
}) => {
|
|
149
154
|
const [data, setData] = useState<any[]>([]);
|
|
@@ -178,6 +183,10 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
|
|
|
178
183
|
]);
|
|
179
184
|
const hasInlineData = dataConfig?.provider === 'value';
|
|
180
185
|
|
|
186
|
+
// Use ref for objectSchema to avoid double-fetch on mount
|
|
187
|
+
const objectSchemaRef = useRef<any>(null);
|
|
188
|
+
objectSchemaRef.current = objectSchema;
|
|
189
|
+
|
|
181
190
|
// Fetch data based on provider
|
|
182
191
|
useEffect(() => {
|
|
183
192
|
let isMounted = true;
|
|
@@ -204,28 +213,21 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
|
|
|
204
213
|
}
|
|
205
214
|
}
|
|
206
215
|
|
|
207
|
-
if (!dataSource) {
|
|
216
|
+
if (!dataSource || typeof dataSource.find !== 'function') {
|
|
208
217
|
throw new Error('DataSource required for object/api providers');
|
|
209
218
|
}
|
|
210
219
|
|
|
211
220
|
if (dataConfig?.provider === 'object') {
|
|
212
221
|
const objectName = dataConfig.object;
|
|
222
|
+
// Auto-inject $expand for lookup/master_detail fields
|
|
223
|
+
const expand = buildExpandFields(objectSchemaRef.current?.fields);
|
|
213
224
|
const result = await dataSource.find(objectName, {
|
|
214
225
|
$filter: schema.filter,
|
|
215
226
|
$orderby: convertSortToQueryParams(schema.sort),
|
|
227
|
+
...(expand.length > 0 ? { $expand: expand } : {}),
|
|
216
228
|
});
|
|
217
229
|
|
|
218
|
-
let items: any[] =
|
|
219
|
-
|
|
220
|
-
if (Array.isArray(result)) {
|
|
221
|
-
items = result;
|
|
222
|
-
} else if (result && typeof result === 'object') {
|
|
223
|
-
if (Array.isArray((result as any).data)) {
|
|
224
|
-
items = (result as any).data;
|
|
225
|
-
} else if (Array.isArray((result as any).value)) {
|
|
226
|
-
items = (result as any).value;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
230
|
+
let items: any[] = extractRecords(result);
|
|
229
231
|
|
|
230
232
|
if (isMounted) {
|
|
231
233
|
setData(items);
|
|
@@ -362,6 +364,7 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
|
|
|
362
364
|
events={events}
|
|
363
365
|
currentDate={currentDate}
|
|
364
366
|
view={(schema as any).defaultView || 'month'}
|
|
367
|
+
locale={locale}
|
|
365
368
|
onEventClick={(event) => {
|
|
366
369
|
navigation.handleClick(event.data);
|
|
367
370
|
onEventClick?.(event.data);
|
|
@@ -375,7 +378,10 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
|
|
|
375
378
|
setView(v);
|
|
376
379
|
onViewChange?.(v);
|
|
377
380
|
}}
|
|
378
|
-
onAddClick={
|
|
381
|
+
onAddClick={undefined}
|
|
382
|
+
onEventDrop={onEventDrop ? (event, newStart, newEnd) => {
|
|
383
|
+
onEventDrop(event.data, newStart, newEnd);
|
|
384
|
+
} : undefined}
|
|
379
385
|
/>
|
|
380
386
|
</div>
|
|
381
387
|
{navigation.isOverlay && (
|
|
@@ -276,7 +276,7 @@ describe('CalendarView: Screen Reader & Accessibility', () => {
|
|
|
276
276
|
/>
|
|
277
277
|
);
|
|
278
278
|
|
|
279
|
-
const newButton = screen.getByText('New');
|
|
279
|
+
const newButton = screen.getByText('New event');
|
|
280
280
|
expect(newButton).toBeInTheDocument();
|
|
281
281
|
expect(newButton.closest('button')).toBeInTheDocument();
|
|
282
282
|
});
|
|
@@ -284,7 +284,7 @@ describe('CalendarView: Screen Reader & Accessibility', () => {
|
|
|
284
284
|
it('add button is not shown when onAddClick is absent', () => {
|
|
285
285
|
render(<CalendarView currentDate={defaultDate} locale="en-US" />);
|
|
286
286
|
|
|
287
|
-
expect(screen.queryByText('New')).not.toBeInTheDocument();
|
|
287
|
+
expect(screen.queryByText('New event')).not.toBeInTheDocument();
|
|
288
288
|
});
|
|
289
289
|
});
|
|
290
290
|
});
|
|
@@ -0,0 +1,230 @@
|
|
|
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
|
+
* Tests for calendar bug fixes:
|
|
11
|
+
* - Event click always dispatches action (no schema.onEventClick guard)
|
|
12
|
+
* - i18n integration with useObjectTranslation
|
|
13
|
+
* - Tooltip (title attribute) on truncated event titles
|
|
14
|
+
* - Cross-month date visual distinction (opacity)
|
|
15
|
+
* - Today highlight spacing improvement
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
19
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
20
|
+
import '@testing-library/jest-dom';
|
|
21
|
+
import React from 'react';
|
|
22
|
+
import { CalendarView, type CalendarEvent } from '../CalendarView';
|
|
23
|
+
import { I18nProvider } from '@object-ui/i18n';
|
|
24
|
+
|
|
25
|
+
// Mock ResizeObserver
|
|
26
|
+
class ResizeObserver {
|
|
27
|
+
observe() {}
|
|
28
|
+
unobserve() {}
|
|
29
|
+
disconnect() {}
|
|
30
|
+
}
|
|
31
|
+
global.ResizeObserver = ResizeObserver;
|
|
32
|
+
|
|
33
|
+
// Mock PointerEvents for Radix
|
|
34
|
+
if (!global.PointerEvent) {
|
|
35
|
+
class PointerEvent extends Event {
|
|
36
|
+
button: number;
|
|
37
|
+
ctrlKey: boolean;
|
|
38
|
+
metaKey: boolean;
|
|
39
|
+
shiftKey: boolean;
|
|
40
|
+
constructor(type: string, props: any = {}) {
|
|
41
|
+
super(type, props);
|
|
42
|
+
this.button = props.button || 0;
|
|
43
|
+
this.ctrlKey = props.ctrlKey || false;
|
|
44
|
+
this.metaKey = props.metaKey || false;
|
|
45
|
+
this.shiftKey = props.shiftKey || false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// @ts-expect-error Mocking global PointerEvent
|
|
49
|
+
global.PointerEvent = PointerEvent as any;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Mock HTMLElement.offsetParent for Radix Popper
|
|
53
|
+
Object.defineProperty(HTMLElement.prototype, 'offsetParent', {
|
|
54
|
+
get() {
|
|
55
|
+
return this.parentElement;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const defaultDate = new Date(2024, 0, 15); // Jan 15, 2024
|
|
60
|
+
|
|
61
|
+
const sampleEvents: CalendarEvent[] = [
|
|
62
|
+
{
|
|
63
|
+
id: '1',
|
|
64
|
+
title: 'Design dashboard widget layout and structure',
|
|
65
|
+
start: new Date(2024, 0, 15, 10, 0),
|
|
66
|
+
end: new Date(2024, 0, 15, 11, 0),
|
|
67
|
+
color: '#3b82f6',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: '2',
|
|
71
|
+
title: 'Short Event',
|
|
72
|
+
start: new Date(2024, 0, 15, 14, 0),
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
function renderWithI18n(ui: React.ReactElement, lang = 'en') {
|
|
77
|
+
return render(
|
|
78
|
+
<I18nProvider config={{ defaultLanguage: lang, detectBrowserLanguage: false }}>
|
|
79
|
+
{ui}
|
|
80
|
+
</I18nProvider>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe('Calendar Bug Fixes', () => {
|
|
85
|
+
describe('event click dispatches action', () => {
|
|
86
|
+
it('fires onEventClick when event is clicked in month view', () => {
|
|
87
|
+
const onClick = vi.fn();
|
|
88
|
+
renderWithI18n(
|
|
89
|
+
<CalendarView
|
|
90
|
+
currentDate={defaultDate}
|
|
91
|
+
events={sampleEvents}
|
|
92
|
+
view="month"
|
|
93
|
+
onEventClick={onClick}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
fireEvent.click(screen.getByText('Short Event'));
|
|
97
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
98
|
+
expect(onClick).toHaveBeenCalledWith(sampleEvents[1]);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('tooltip on truncated event titles', () => {
|
|
103
|
+
it('renders title attribute on event cards in month view', () => {
|
|
104
|
+
renderWithI18n(
|
|
105
|
+
<CalendarView
|
|
106
|
+
currentDate={defaultDate}
|
|
107
|
+
events={sampleEvents}
|
|
108
|
+
view="month"
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
const eventEl = screen.getByText('Short Event');
|
|
112
|
+
expect(eventEl.closest('[title]')).toHaveAttribute('title', 'Short Event');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('renders title attribute with long title in month view', () => {
|
|
116
|
+
renderWithI18n(
|
|
117
|
+
<CalendarView
|
|
118
|
+
currentDate={defaultDate}
|
|
119
|
+
events={sampleEvents}
|
|
120
|
+
view="month"
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
const eventEl = screen.getByText('Design dashboard widget layout and structure');
|
|
124
|
+
expect(eventEl.closest('[title]')).toHaveAttribute(
|
|
125
|
+
'title',
|
|
126
|
+
'Design dashboard widget layout and structure'
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('renders title attribute on event cards in day view', () => {
|
|
131
|
+
renderWithI18n(
|
|
132
|
+
<CalendarView
|
|
133
|
+
currentDate={defaultDate}
|
|
134
|
+
events={sampleEvents}
|
|
135
|
+
view="day"
|
|
136
|
+
/>
|
|
137
|
+
);
|
|
138
|
+
const eventEl = screen.getByText('Short Event');
|
|
139
|
+
expect(eventEl.closest('[title]')).toHaveAttribute('title', 'Short Event');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('i18n integration', () => {
|
|
144
|
+
it('renders labels from i18n when language is en', () => {
|
|
145
|
+
const onAdd = vi.fn();
|
|
146
|
+
renderWithI18n(
|
|
147
|
+
<CalendarView
|
|
148
|
+
currentDate={defaultDate}
|
|
149
|
+
events={[]}
|
|
150
|
+
onAddClick={onAdd}
|
|
151
|
+
/>,
|
|
152
|
+
'en'
|
|
153
|
+
);
|
|
154
|
+
expect(screen.getByText('Today')).toBeInTheDocument();
|
|
155
|
+
expect(screen.getByText('New event')).toBeInTheDocument();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('renders labels from i18n when language is zh', () => {
|
|
159
|
+
const onAdd = vi.fn();
|
|
160
|
+
renderWithI18n(
|
|
161
|
+
<CalendarView
|
|
162
|
+
currentDate={defaultDate}
|
|
163
|
+
events={[]}
|
|
164
|
+
onAddClick={onAdd}
|
|
165
|
+
/>,
|
|
166
|
+
'zh'
|
|
167
|
+
);
|
|
168
|
+
expect(screen.getByText('今天')).toBeInTheDocument();
|
|
169
|
+
expect(screen.getByText('新建事件')).toBeInTheDocument();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('renders locale-aware weekday headers for zh', () => {
|
|
173
|
+
renderWithI18n(
|
|
174
|
+
<CalendarView
|
|
175
|
+
currentDate={defaultDate}
|
|
176
|
+
events={[]}
|
|
177
|
+
view="month"
|
|
178
|
+
/>,
|
|
179
|
+
'zh'
|
|
180
|
+
);
|
|
181
|
+
expect(screen.getByText('周日')).toBeInTheDocument();
|
|
182
|
+
expect(screen.getByText('周一')).toBeInTheDocument();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('cross-month date styling', () => {
|
|
187
|
+
it('applies opacity to non-current-month dates', () => {
|
|
188
|
+
const { container } = renderWithI18n(
|
|
189
|
+
<CalendarView
|
|
190
|
+
currentDate={new Date(2024, 1, 15)} // Feb 15, 2024
|
|
191
|
+
events={[]}
|
|
192
|
+
view="month"
|
|
193
|
+
/>
|
|
194
|
+
);
|
|
195
|
+
const gridCells = container.querySelectorAll('[role="gridcell"]');
|
|
196
|
+
const firstCell = gridCells[0];
|
|
197
|
+
expect(firstCell.className).toContain('opacity-50');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('does not apply opacity to current-month dates', () => {
|
|
201
|
+
const { container } = renderWithI18n(
|
|
202
|
+
<CalendarView
|
|
203
|
+
currentDate={new Date(2024, 1, 15)} // Feb 15, 2024
|
|
204
|
+
events={[]}
|
|
205
|
+
view="month"
|
|
206
|
+
/>
|
|
207
|
+
);
|
|
208
|
+
const gridCells = container.querySelectorAll('[role="gridcell"]');
|
|
209
|
+
// Feb 2024 starts on Thursday, so index 4 (0-based) is Feb 1
|
|
210
|
+
const feb1Cell = gridCells[4];
|
|
211
|
+
expect(feb1Cell.className).not.toContain('opacity-50');
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('today highlight spacing', () => {
|
|
216
|
+
it('renders today date with mb-2 spacing for better separation', () => {
|
|
217
|
+
const today = new Date();
|
|
218
|
+
const { container } = renderWithI18n(
|
|
219
|
+
<CalendarView
|
|
220
|
+
currentDate={today}
|
|
221
|
+
events={[]}
|
|
222
|
+
view="month"
|
|
223
|
+
/>
|
|
224
|
+
);
|
|
225
|
+
const todayEl = container.querySelector('[aria-current="date"]');
|
|
226
|
+
expect(todayEl).toBeInTheDocument();
|
|
227
|
+
expect(todayEl?.className).toContain('mb-2');
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -0,0 +1,178 @@
|
|
|
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
|
+
* Tests for CalendarView optimizations:
|
|
9
|
+
* - Event index (Map-based lookup instead of O(N) per cell)
|
|
10
|
+
* - Stable default date reference
|
|
11
|
+
* - HEX color text-white fix
|
|
12
|
+
* - onEventDrop / locale passthrough from ObjectCalendar
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
16
|
+
import { render, screen } 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 @object-ui/components
|
|
22
|
+
vi.mock('@object-ui/components', () => ({
|
|
23
|
+
cn: (...classes: (string | undefined | boolean)[]) => classes.filter(Boolean).join(' '),
|
|
24
|
+
Button: ({ children, ...props }: any) => <button {...props}>{children}</button>,
|
|
25
|
+
Select: ({ children, value, onValueChange }: any) => <div data-testid="select">{children}</div>,
|
|
26
|
+
SelectContent: ({ children }: any) => <div>{children}</div>,
|
|
27
|
+
SelectItem: ({ children, value }: any) => <div data-value={value}>{children}</div>,
|
|
28
|
+
SelectTrigger: ({ children }: any) => <div>{children}</div>,
|
|
29
|
+
SelectValue: () => <span>Month</span>,
|
|
30
|
+
Calendar: (props: any) => <div data-testid="calendar-picker">Calendar Picker</div>,
|
|
31
|
+
Popover: ({ children }: any) => <div>{children}</div>,
|
|
32
|
+
PopoverContent: ({ children }: any) => <div>{children}</div>,
|
|
33
|
+
PopoverTrigger: ({ children }: any) => <div>{children}</div>,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
// Mock lucide-react icons
|
|
37
|
+
vi.mock('lucide-react', () => ({
|
|
38
|
+
ChevronLeftIcon: (props: any) => <svg data-testid="chevron-left" {...props} />,
|
|
39
|
+
ChevronRightIcon: (props: any) => <svg data-testid="chevron-right" {...props} />,
|
|
40
|
+
CalendarIcon: (props: any) => <svg data-testid="calendar-icon" {...props} />,
|
|
41
|
+
PlusIcon: (props: any) => <svg data-testid="plus-icon" {...props} />,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
// Mock ResizeObserver
|
|
45
|
+
class ResizeObserver {
|
|
46
|
+
observe() {}
|
|
47
|
+
unobserve() {}
|
|
48
|
+
disconnect() {}
|
|
49
|
+
}
|
|
50
|
+
global.ResizeObserver = ResizeObserver;
|
|
51
|
+
|
|
52
|
+
const baseDate = new Date(2024, 0, 15); // Jan 15, 2024
|
|
53
|
+
|
|
54
|
+
describe('CalendarView optimizations', () => {
|
|
55
|
+
describe('event index (Map-based lookup)', () => {
|
|
56
|
+
it('renders single-day events correctly with the new index', () => {
|
|
57
|
+
const events: CalendarEvent[] = [
|
|
58
|
+
{ id: '1', title: 'Morning Meeting', start: new Date(2024, 0, 15, 9, 0), end: new Date(2024, 0, 15, 10, 0) },
|
|
59
|
+
{ id: '2', title: 'Lunch', start: new Date(2024, 0, 15, 12, 0), end: new Date(2024, 0, 15, 13, 0) },
|
|
60
|
+
];
|
|
61
|
+
render(<CalendarView currentDate={baseDate} events={events} view="month" locale="en-US" />);
|
|
62
|
+
expect(screen.getByText('Morning Meeting')).toBeInTheDocument();
|
|
63
|
+
expect(screen.getByText('Lunch')).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('renders multi-day events on all spanned days', () => {
|
|
67
|
+
const events: CalendarEvent[] = [
|
|
68
|
+
{
|
|
69
|
+
id: 'multi-1',
|
|
70
|
+
title: 'Conference',
|
|
71
|
+
start: new Date(2024, 0, 15, 9, 0),
|
|
72
|
+
end: new Date(2024, 0, 17, 17, 0),
|
|
73
|
+
allDay: true,
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
const { container } = render(
|
|
77
|
+
<CalendarView currentDate={baseDate} events={events} view="month" locale="en-US" />
|
|
78
|
+
);
|
|
79
|
+
// The multi-day event should appear on Jan 15, 16, and 17 — 3 cells
|
|
80
|
+
const eventElements = container.querySelectorAll('[role="button"][aria-label="Conference"]');
|
|
81
|
+
expect(eventElements.length).toBe(3);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('renders events with no end date on their start day only', () => {
|
|
85
|
+
const events: CalendarEvent[] = [
|
|
86
|
+
{ id: 'no-end', title: 'No End Event', start: new Date(2024, 0, 20, 14, 0) },
|
|
87
|
+
];
|
|
88
|
+
const { container } = render(
|
|
89
|
+
<CalendarView currentDate={baseDate} events={events} view="month" locale="en-US" />
|
|
90
|
+
);
|
|
91
|
+
const eventElements = container.querySelectorAll('[role="button"][aria-label="No End Event"]');
|
|
92
|
+
expect(eventElements.length).toBe(1);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('HEX color text-white fix', () => {
|
|
97
|
+
it('applies text-white class when event color is a HEX value in month view', () => {
|
|
98
|
+
const events: CalendarEvent[] = [
|
|
99
|
+
{ id: 'hex-1', title: 'HEX Event', start: new Date(2024, 0, 15), color: '#3b82f6' },
|
|
100
|
+
];
|
|
101
|
+
const { container } = render(
|
|
102
|
+
<CalendarView currentDate={baseDate} events={events} view="month" locale="en-US" />
|
|
103
|
+
);
|
|
104
|
+
const eventEl = container.querySelector('[aria-label="HEX Event"]');
|
|
105
|
+
expect(eventEl).toBeInTheDocument();
|
|
106
|
+
expect(eventEl!.className).toContain('text-white');
|
|
107
|
+
expect((eventEl as HTMLElement).style.backgroundColor).toBe('#3b82f6');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('uses default color class when event color is a Tailwind class', () => {
|
|
111
|
+
const events: CalendarEvent[] = [
|
|
112
|
+
{ id: 'tw-1', title: 'Tailwind Event', start: new Date(2024, 0, 15), color: 'bg-red-500 text-white' },
|
|
113
|
+
];
|
|
114
|
+
const { container } = render(
|
|
115
|
+
<CalendarView currentDate={baseDate} events={events} view="month" locale="en-US" />
|
|
116
|
+
);
|
|
117
|
+
const eventEl = container.querySelector('[aria-label="Tailwind Event"]');
|
|
118
|
+
expect(eventEl).toBeInTheDocument();
|
|
119
|
+
expect(eventEl!.className).toContain('bg-red-500');
|
|
120
|
+
expect(eventEl!.className).toContain('text-white');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('falls back to DEFAULT_EVENT_COLOR when no color is specified', () => {
|
|
124
|
+
const events: CalendarEvent[] = [
|
|
125
|
+
{ id: 'no-color', title: 'No Color', start: new Date(2024, 0, 15) },
|
|
126
|
+
];
|
|
127
|
+
const { container } = render(
|
|
128
|
+
<CalendarView currentDate={baseDate} events={events} view="month" locale="en-US" />
|
|
129
|
+
);
|
|
130
|
+
const eventEl = container.querySelector('[aria-label="No Color"]');
|
|
131
|
+
expect(eventEl).toBeInTheDocument();
|
|
132
|
+
expect(eventEl!.className).toContain('bg-blue-500');
|
|
133
|
+
expect(eventEl!.className).toContain('text-white');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('stable default date', () => {
|
|
138
|
+
it('renders without currentDate prop without errors', () => {
|
|
139
|
+
const { container } = render(<CalendarView events={[]} locale="en-US" />);
|
|
140
|
+
expect(container).toBeTruthy();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('does not trigger re-render loop when currentDate is not provided', () => {
|
|
144
|
+
const onNavigate = vi.fn();
|
|
145
|
+
const { rerender } = render(<CalendarView events={[]} locale="en-US" onNavigate={onNavigate} />);
|
|
146
|
+
// Re-render the same component — should NOT trigger onNavigate from the effect
|
|
147
|
+
rerender(<CalendarView events={[]} locale="en-US" onNavigate={onNavigate} />);
|
|
148
|
+
expect(onNavigate).not.toHaveBeenCalled();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('drag-and-drop enablement', () => {
|
|
153
|
+
it('events are draggable when onEventDrop is provided', () => {
|
|
154
|
+
const events: CalendarEvent[] = [
|
|
155
|
+
{ id: 'd-1', title: 'Drag Event', start: new Date(2024, 0, 15) },
|
|
156
|
+
];
|
|
157
|
+
const onEventDrop = vi.fn();
|
|
158
|
+
const { container } = render(
|
|
159
|
+
<CalendarView currentDate={baseDate} events={events} view="month" onEventDrop={onEventDrop} locale="en-US" />
|
|
160
|
+
);
|
|
161
|
+
const eventEl = container.querySelector('[aria-label="Drag Event"]');
|
|
162
|
+
expect(eventEl).toBeInTheDocument();
|
|
163
|
+
expect(eventEl!.getAttribute('draggable')).toBe('true');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('events are not draggable when onEventDrop is not provided', () => {
|
|
167
|
+
const events: CalendarEvent[] = [
|
|
168
|
+
{ id: 'nd-1', title: 'No Drag Event', start: new Date(2024, 0, 15) },
|
|
169
|
+
];
|
|
170
|
+
const { container } = render(
|
|
171
|
+
<CalendarView currentDate={baseDate} events={events} view="month" locale="en-US" />
|
|
172
|
+
);
|
|
173
|
+
const eventEl = container.querySelector('[aria-label="No Drag Event"]');
|
|
174
|
+
expect(eventEl).toBeInTheDocument();
|
|
175
|
+
expect(eventEl!.getAttribute('draggable')).toBe('false');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -265,8 +265,8 @@ describe('P3.3 Calendar View States', () => {
|
|
|
265
265
|
onAddClick={onAdd}
|
|
266
266
|
/>
|
|
267
267
|
);
|
|
268
|
-
// The "New" button should be visible
|
|
269
|
-
const newButton = screen.getByText('New');
|
|
268
|
+
// The "New event" button should be visible
|
|
269
|
+
const newButton = screen.getByText('New event');
|
|
270
270
|
expect(newButton).toBeInTheDocument();
|
|
271
271
|
fireEvent.click(newButton);
|
|
272
272
|
expect(onAdd).toHaveBeenCalledTimes(1);
|
|
@@ -280,7 +280,7 @@ describe('P3.3 Calendar View States', () => {
|
|
|
280
280
|
locale="en-US"
|
|
281
281
|
/>
|
|
282
282
|
);
|
|
283
|
-
expect(screen.queryByText('New')).not.toBeInTheDocument();
|
|
283
|
+
expect(screen.queryByText('New event')).not.toBeInTheDocument();
|
|
284
284
|
});
|
|
285
285
|
|
|
286
286
|
it('accepts className prop', () => {
|
|
@@ -43,15 +43,10 @@ ComponentRegistry.register('calendar-view',
|
|
|
43
43
|
}, [schema.data, schema.titleField, schema.startDateField, schema.endDateField, schema.colorField, schema.allDayField]);
|
|
44
44
|
|
|
45
45
|
const handleEventClick = (event: CalendarEvent) => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
onAction?.({
|
|
51
|
-
type: 'event-click',
|
|
52
|
-
payload: event
|
|
53
|
-
});
|
|
54
|
-
}
|
|
46
|
+
onAction?.({
|
|
47
|
+
type: 'event-click',
|
|
48
|
+
payload: event
|
|
49
|
+
});
|
|
55
50
|
};
|
|
56
51
|
|
|
57
52
|
const handleAddClick = () => {
|
|
@@ -67,7 +62,6 @@ ComponentRegistry.register('calendar-view',
|
|
|
67
62
|
className={className}
|
|
68
63
|
events={events}
|
|
69
64
|
onEventClick={handleEventClick}
|
|
70
|
-
onAddClick={handleAddClick}
|
|
71
65
|
// Pass validation or other props
|
|
72
66
|
{...props}
|
|
73
67
|
/>
|
package/src/index.tsx
CHANGED
|
@@ -24,9 +24,9 @@ export type { CalendarViewProps, CalendarEvent } from './CalendarView';
|
|
|
24
24
|
import './calendar-view-renderer';
|
|
25
25
|
|
|
26
26
|
// Register object-calendar component
|
|
27
|
-
export const ObjectCalendarRenderer: React.FC<{ schema: any }> = ({ schema }) => {
|
|
28
|
-
const { dataSource } = useSchemaContext();
|
|
29
|
-
return <ObjectCalendar schema={schema} dataSource={dataSource} />;
|
|
27
|
+
export const ObjectCalendarRenderer: React.FC<{ schema: any; [key: string]: any }> = ({ schema, data: _data, loading: _loading, ...props }) => {
|
|
28
|
+
const { dataSource } = useSchemaContext() || {};
|
|
29
|
+
return <ObjectCalendar schema={schema} dataSource={dataSource} {...props} />;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
ComponentRegistry.register('object-calendar', ObjectCalendarRenderer, {
|