|
should match the snapshot 1`] = `
|
should match the snapshot 1`] = `
|
should match the snapshot 1`] = `
|
should match the snapshot 1`] = `
|
should match the snapshot 1`] = `
|
should match the snapshot 1`] = `
|
', () => {
+ // ── Existing tests ──────────────────────────────────────────────────────
+
it('should match the snapshot', () => {
const { asFragment } = render();
expect(asFragment()).toMatchSnapshot();
@@ -40,10 +42,182 @@ describe('', () => {
});
it('should switch to year mode', () => {
- const { container, getByText } = render();
+ const { container } = render();
const titleBtn = container.querySelector('.ty-calendar__title-btn');
fireEvent.click(titleBtn!);
- // Should show months
expect(container.querySelector('.ty-calendar__months')).toBeTruthy();
});
+
+ // ── weekStartsOn ───────────────────────────────────────────────────────
+
+ it('should start weeks on Monday when weekStartsOn={1}', () => {
+ const { container } = render();
+ const headers = container.querySelectorAll('.ty-calendar__cell-header');
+ expect(headers[0].textContent).toBe('Mo');
+ expect(headers[6].textContent).toBe('Su');
+ });
+
+ // ── showWeekNumber ─────────────────────────────────────────────────────
+
+ it('should render week numbers when showWeekNumber is true', () => {
+ const { container } = render();
+ const weekNumCells = container.querySelectorAll('.ty-calendar__week-number');
+ expect(weekNumCells.length).toBeGreaterThan(0);
+ // Week number header
+ const weekNumHeader = container.querySelector('.ty-calendar__week-number-header');
+ expect(weekNumHeader).toBeTruthy();
+ expect(weekNumHeader!.textContent).toBe('#');
+ });
+
+ // ── Range selection ────────────────────────────────────────────────────
+
+ it('should support range selection mode', () => {
+ const onRangeChange = jest.fn();
+ const { container } = render(
+
+ );
+ const cells = container.querySelectorAll('.ty-calendar__cell_in-view');
+ // Click first date to start range
+ fireEvent.click(cells[4]);
+ // Click second date to end range
+ fireEvent.click(cells[10]);
+ expect(onRangeChange).toHaveBeenCalledTimes(1);
+ const args = onRangeChange.mock.calls[0][0] as [Date, Date];
+ expect(args[0]).toBeInstanceOf(Date);
+ expect(args[1]).toBeInstanceOf(Date);
+ });
+
+ // ── Multiple selection ─────────────────────────────────────────────────
+
+ it('should support multiple selection mode', () => {
+ const onMultipleChange = jest.fn();
+ const { container } = render(
+
+ );
+ const cells = container.querySelectorAll('.ty-calendar__cell_in-view');
+ fireEvent.click(cells[2]);
+ expect(onMultipleChange).toHaveBeenCalledTimes(1);
+ expect(onMultipleChange.mock.calls[0][0]).toHaveLength(1);
+ fireEvent.click(cells[5]);
+ expect(onMultipleChange).toHaveBeenCalledTimes(2);
+ expect(onMultipleChange.mock.calls[1][0]).toHaveLength(2);
+ });
+
+ // ── showToday ──────────────────────────────────────────────────────────
+
+ it('should render Today button when showToday is true', () => {
+ const { container } = render();
+ const todayBtn = container.querySelector('.ty-calendar__today-link');
+ expect(todayBtn).toBeTruthy();
+ expect(todayBtn!.textContent).toBe('Today');
+ });
+
+ // ── Decade panel ───────────────────────────────────────────────────────
+
+ it('should navigate to decade panel', () => {
+ const { container } = render();
+ const titleBtn = container.querySelector('.ty-calendar__title-btn');
+ // month → year
+ fireEvent.click(titleBtn!);
+ expect(container.querySelector('.ty-calendar__months')).toBeTruthy();
+ // year → decade
+ const titleBtn2 = container.querySelector('.ty-calendar__title-btn');
+ fireEvent.click(titleBtn2!);
+ expect(container.querySelector('.ty-calendar__decades')).toBeTruthy();
+ const decadeCells = container.querySelectorAll('.ty-calendar__decade-cell');
+ expect(decadeCells.length).toBe(12);
+ });
+
+ // ── validRange ─────────────────────────────────────────────────────────
+
+ it('should disable dates outside validRange', () => {
+ const min = new Date(2024, 0, 10);
+ const max = new Date(2024, 0, 20);
+ const { container } = render();
+ const disabledCells = container.querySelectorAll('.ty-calendar__cell_disabled');
+ expect(disabledCells.length).toBeGreaterThan(0);
+ });
+
+ it('should disable nav buttons at validRange boundaries', () => {
+ const min = new Date(2024, 0, 1);
+ const max = new Date(2024, 0, 31);
+ const { container } = render();
+ const navBtns = container.querySelectorAll('.ty-calendar__nav-btn');
+ // Both prev and next should be disabled since we can only view Jan 2024
+ expect(navBtns[0]).toBeDisabled();
+ expect(navBtns[1]).toBeDisabled();
+ });
+
+ // ── Keyboard navigation ────────────────────────────────────────────────
+
+ it('should handle keyboard navigation', () => {
+ const onChange = jest.fn();
+ const { container } = render();
+ const calendarEl = container.firstChild as HTMLElement;
+ fireEvent.keyDown(calendarEl, { key: 'ArrowRight' });
+ // Check that a cell is now focused
+ const focused = container.querySelector('.ty-calendar__cell_focused');
+ expect(focused).toBeTruthy();
+ // Press Enter to select
+ fireEvent.keyDown(calendarEl, { key: 'Enter' });
+ expect(onChange).toHaveBeenCalled();
+ });
+
+ // ── cellClassName ──────────────────────────────────────────────────────
+
+ it('should apply cellClassName to cells', () => {
+ const { container } = render(
+ (date.getDay() === 0 ? 'sunday' : undefined)}
+ />
+ );
+ const sundayCells = container.querySelectorAll('.sunday');
+ expect(sundayCells.length).toBeGreaterThan(0);
+ });
+
+ // ── dotRender ──────────────────────────────────────────────────────────
+
+ it('should render dot indicators', () => {
+ const { container } = render(
+ date.getDate() === 15}
+ />
+ );
+ const dots = container.querySelectorAll('.ty-calendar__cell-dot');
+ expect(dots.length).toBeGreaterThan(0);
+ });
+
+ it('should render dot with custom color', () => {
+ const { container } = render(
+ (date.getDate() === 15 ? '#ff0000' : false)}
+ />
+ );
+ const dot = container.querySelector('.ty-calendar__cell-dot') as HTMLElement;
+ expect(dot).toBeTruthy();
+ expect(dot.style.backgroundColor).toBe('rgb(255, 0, 0)');
+ });
+
+ // ── onMonthChange / onYearChange ───────────────────────────────────────
+
+ it('should fire onMonthChange when navigating months', () => {
+ const onMonthChange = jest.fn();
+ const { container } = render(
+
+ );
+ const navBtns = container.querySelectorAll('.ty-calendar__nav-btn');
+ fireEvent.click(navBtns[1]); // next
+ expect(onMonthChange).toHaveBeenCalledTimes(1);
+ });
});
diff --git a/packages/react/src/calendar/calendar.tsx b/packages/react/src/calendar/calendar.tsx
index 7239995c..a56c25dc 100755
--- a/packages/react/src/calendar/calendar.tsx
+++ b/packages/react/src/calendar/calendar.tsx
@@ -1,12 +1,14 @@
-import React, { useState, useEffect, useContext } from 'react';
+import React, { useState, useEffect, useContext, useCallback, useRef } from 'react';
import classNames from 'classnames';
import { ConfigContext } from '../config-provider/config-context';
import { getPrefixCls } from '../_utils/general';
import { useLocale } from '../_utils/use-locale';
import {
getMonthDaysArray,
+ getISOWeekNumber,
isSameDate,
isToday,
+ compareDate,
getPrevMonthDate,
getNextMonthDate,
getPrevYearDate,
@@ -14,16 +16,27 @@ import {
} from '../date-picker/utils';
import { CalendarProps, CalendarMode } from './types';
-const MONTH_NAMES_EN = [
- 'January', 'February', 'March', 'April', 'May', 'June',
- 'July', 'August', 'September', 'October', 'November', 'December',
-];
+// ─── helpers ────────────────────────────────────────────────────────────────
+
+const dateKey = (d: Date) => `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
+
+const getIntlMonthName = (date: Date, locale: string, style: 'long' | 'short' = 'long') => {
+ try {
+ return new Intl.DateTimeFormat(locale, { month: style }).format(date);
+ } catch {
+ return '';
+ }
+};
+
+const DEFAULT_WEEKS_EN = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
+
+// ─── component ──────────────────────────────────────────────────────────────
const Calendar = React.forwardRef((props, ref) => {
const {
defaultValue,
fullscreen = true,
- disabledDate,
+ disabledDate: disabledDateProp,
dateCellRender,
monthCellRender,
headerRender,
@@ -36,12 +49,33 @@ const Calendar = React.forwardRef((props, ref) =>
onPanelChange,
value: _value,
mode: _mode,
+ // NEW PROPS
+ selectionMode = 'single',
+ rangeValue: _rangeValue,
+ defaultRangeValue,
+ onRangeChange,
+ multipleValue: _multipleValue,
+ defaultMultipleValue,
+ onMultipleChange,
+ weekStartsOn = 0,
+ showWeekNumber = false,
+ showToday = false,
+ validRange,
+ cellClassName,
+ cellStyle: cellStyleProp,
+ dotRender,
+ weekNumberRender,
+ onMonthChange,
+ onYearChange,
...otherProps
} = props;
const configContext = useContext(ConfigContext);
const prefixCls = getPrefixCls('calendar', configContext.prefixCls, customisedCls);
const locale = useLocale();
+ const calendarRef = useRef(null);
+
+ // ── state ──────────────────────────────────────────────────────────────────
const [selectedDate, setSelectedDate] = useState(
'value' in props ? (props.value as Date) : (defaultValue ?? new Date())
@@ -51,6 +85,29 @@ const Calendar = React.forwardRef((props, ref) =>
'mode' in props ? (props.mode as CalendarMode) : defaultMode
);
+ // Range selection state
+ const [rangeStart, setRangeStart] = useState(
+ _rangeValue?.[0] ?? defaultRangeValue?.[0] ?? null
+ );
+ const [rangeEnd, setRangeEnd] = useState(
+ _rangeValue?.[1] ?? defaultRangeValue?.[1] ?? null
+ );
+ const [rangeHover, setRangeHover] = useState(null);
+
+ // Multiple selection state
+ const [multipleSelected, setMultipleSelected] = useState |