Skip to content

Commit 11d1efd

Browse files
authored
feat(calendar): support switchMode (#663)
* feat(calendar): support switchMode * feat(calendar): support readonly
1 parent 29c4585 commit 11d1efd

File tree

12 files changed

+1134
-122
lines changed

12 files changed

+1134
-122
lines changed

.github/workflows/typos-config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ default.check-filename = true
33
[default.extend-words]
44
actived = "actived"
55
colum = "colum"
6+
loosing = "loosing"
67

78
[files]
89
extend-exclude = ["CHANGELOG.md", "*.snap", "test/config/jest.base.conf.js"]

src/calendar/CalendarTemplate.tsx

Lines changed: 205 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import React, { useEffect, useState, useContext, useMemo, forwardRef } from 'react';
2-
import { CloseIcon } from 'tdesign-icons-react';
2+
import {
3+
CloseIcon,
4+
ChevronLeftIcon,
5+
ChevronRightIcon,
6+
ChevronLeftDoubleIcon,
7+
ChevronRightDoubleIcon,
8+
} from 'tdesign-icons-react';
9+
import classNames from 'classnames';
10+
311
import parseTNode from '../_util/parseTNode';
412
import { Button, ButtonProps } from '../button';
513
import { TDateType, TCalendarValue } from './type';
@@ -8,6 +16,12 @@ import useDefaultProps from '../hooks/useDefaultProps';
816
import { calendarDefaultProps } from './defaultProps';
917
import { CalendarContext, CalendarProps } from './Calendar';
1018
import { useLocaleReceiver } from '../locale/LocalReceiver';
19+
import { getPrevMonth, getPrevYear, getNextMonth, getNextYear } from './utils';
20+
21+
const getCurrentYearAndMonth = (v: Date) => {
22+
const date = new Date(v);
23+
return { year: date.getFullYear(), month: date.getMonth() };
24+
};
1125

1226
const CalendarTemplate = forwardRef<HTMLDivElement, CalendarProps>((_props, ref) => {
1327
const calendarClass = usePrefixClass('calendar');
@@ -16,6 +30,20 @@ const CalendarTemplate = forwardRef<HTMLDivElement, CalendarProps>((_props, ref)
1630
const props = useDefaultProps(context ? context.inject(_props) : _props, calendarDefaultProps);
1731

1832
const [selectedDate, setSelectedDate] = useState<number | Date | TCalendarValue[]>();
33+
const [currentMonth, setCurrentMonth] = useState<
34+
Array<{
35+
year: number;
36+
month: number;
37+
months: TDateType[];
38+
weekdayOfFirstDay: number;
39+
}>
40+
>([]);
41+
const [headerButtons, setHeaderButtons] = useState({
42+
preYearBtnDisable: false,
43+
prevMonthBtnDisable: false,
44+
nextYearBtnDisable: false,
45+
nextMonthBtnDisable: false,
46+
});
1947
const firstDayOfWeek = props.firstDayOfWeek || 0;
2048

2149
useEffect(() => {
@@ -87,41 +115,16 @@ const CalendarTemplate = forwardRef<HTMLDivElement, CalendarProps>((_props, ref)
87115
return className;
88116
};
89117

90-
// 选择日期
91-
const handleSelect = (year, month, date, dateItem) => {
92-
if (dateItem.type === 'disabled') return;
93-
const selected = new Date(year, month, date);
94-
let newSelected: TCalendarValue | TCalendarValue[];
95-
if (props.type === 'range' && Array.isArray(selectedDate)) {
96-
if (selectedDate.length === 1) {
97-
if (selectedDate[0] > selected) {
98-
newSelected = [selected];
99-
} else {
100-
newSelected = [selectedDate[0], selected];
101-
}
102-
} else {
103-
newSelected = [selected];
104-
if (!confirmBtn && selectedDate.length === 2) {
105-
props.onChange?.(new Date(selectedDate[0]));
106-
}
107-
}
108-
} else if (props.type === 'multiple') {
109-
const newVal = [...(Array.isArray(selectedDate) ? selectedDate : [selectedDate])];
110-
const index = newVal.findIndex((item) => isSameDate(item, selected));
111-
if (index > -1) {
112-
newVal.splice(index, 1);
113-
} else {
114-
newVal.push(selected);
115-
}
116-
newSelected = newVal;
117-
} else {
118-
newSelected = selected;
119-
if (!confirmBtn) {
120-
props.onChange?.(selected);
121-
}
118+
const getCurrentDate = () => {
119+
let time = Array.isArray(selectedDate) ? selectedDate[0] : selectedDate;
120+
121+
if (currentMonth?.length > 0) {
122+
const year = currentMonth[0]?.year;
123+
const month = currentMonth[0]?.month;
124+
time = new Date(year, month, 1).getTime();
122125
}
123-
setSelectedDate(newSelected);
124-
props.onSelect?.(newSelected as any);
126+
127+
return time;
125128
};
126129

127130
// 计算月份
@@ -204,6 +207,82 @@ const CalendarTemplate = forwardRef<HTMLDivElement, CalendarProps>((_props, ref)
204207
// eslint-disable-next-line react-hooks/exhaustive-deps
205208
}, [selectedDate]);
206209

210+
const updateActionButton = (value: Date) => {
211+
const min = getCurrentYearAndMonth(minDate);
212+
const max = getCurrentYearAndMonth(maxDate);
213+
214+
const minTimestamp = new Date(min.year, min.month, 1).getTime();
215+
const maxTimestamp = new Date(max.year, max.month, 1).getTime();
216+
217+
const prevYearTimestamp = getPrevYear(value).getTime();
218+
const prevMonthTimestamp = getPrevMonth(value).getTime();
219+
const nextMonthTimestamp = getNextMonth(value).getTime();
220+
const nextYearTimestamp = getNextYear(value).getTime();
221+
222+
const preYearBtnDisable = prevYearTimestamp < minTimestamp || prevMonthTimestamp < minTimestamp;
223+
const prevMonthBtnDisable = prevMonthTimestamp < minTimestamp;
224+
const nextYearBtnDisable = nextMonthTimestamp > maxTimestamp || nextYearTimestamp > maxTimestamp;
225+
const nextMonthBtnDisable = nextMonthTimestamp > maxTimestamp;
226+
227+
setHeaderButtons({
228+
preYearBtnDisable,
229+
prevMonthBtnDisable,
230+
nextYearBtnDisable,
231+
nextMonthBtnDisable,
232+
});
233+
};
234+
235+
const calcCurrentMonth = (newValue?: any) => {
236+
const date = newValue || getCurrentDate();
237+
const { year, month } = getCurrentYearAndMonth(date);
238+
setCurrentMonth(months.filter((item) => item.year === year && item.month === month));
239+
updateActionButton(date);
240+
// eslint-disable-next-line react-hooks/exhaustive-deps
241+
};
242+
243+
// 选择日期
244+
const handleSelect = (year, month, date, dateItem) => {
245+
if (dateItem.type === 'disabled' || props.readonly) return;
246+
const selected = new Date(year, month, date);
247+
let newSelected: TCalendarValue | TCalendarValue[];
248+
if (props.type === 'range' && Array.isArray(selectedDate)) {
249+
if (selectedDate.length === 1) {
250+
if (selectedDate[0] > selected) {
251+
newSelected = [selected];
252+
} else {
253+
newSelected = [selectedDate[0], selected];
254+
}
255+
} else {
256+
newSelected = [selected];
257+
if (!confirmBtn && selectedDate.length === 2) {
258+
props.onChange?.(new Date(selectedDate[0]));
259+
}
260+
}
261+
} else if (props.type === 'multiple') {
262+
const newVal = [...(Array.isArray(selectedDate) ? selectedDate : [selectedDate])];
263+
const index = newVal.findIndex((item) => isSameDate(item, selected));
264+
if (index > -1) {
265+
newVal.splice(index, 1);
266+
} else {
267+
newVal.push(selected);
268+
}
269+
newSelected = newVal;
270+
} else {
271+
newSelected = selected;
272+
if (!confirmBtn) {
273+
props.onChange?.(selected);
274+
}
275+
}
276+
setSelectedDate(newSelected);
277+
278+
if (props.switchMode !== 'none') {
279+
const date = getCurrentDate();
280+
calcCurrentMonth(date);
281+
}
282+
283+
props.onSelect?.(newSelected as any);
284+
};
285+
207286
const handleConfirm = () => {
208287
props.onClose?.('confirm-btn');
209288
props.onConfirm?.(new Date(Array.isArray(selectedDate) ? selectedDate[0] : selectedDate));
@@ -244,10 +323,94 @@ const CalendarTemplate = forwardRef<HTMLDivElement, CalendarProps>((_props, ref)
244323
[],
245324
);
246325

326+
useEffect(() => {
327+
if (props.switchMode !== 'none') {
328+
calcCurrentMonth();
329+
}
330+
// eslint-disable-next-line react-hooks/exhaustive-deps
331+
}, [props.switchMode, selectedDate]);
332+
333+
const handleSwitchModeChange = (type: 'pre-year' | 'pre-month' | 'next-month' | 'next-year', disabled?: boolean) => {
334+
if (disabled) return;
335+
const date = getCurrentDate();
336+
337+
const funcMap = {
338+
'pre-year': () => getPrevYear(date),
339+
'pre-month': () => getPrevMonth(date),
340+
'next-month': () => getNextMonth(date),
341+
'next-year': () => getNextYear(date),
342+
};
343+
const newValue = funcMap[type]();
344+
if (!newValue) return;
345+
346+
const { year, month } = getCurrentYearAndMonth(newValue);
347+
348+
props.onPanelChange?.({ year, month: month + 1 });
349+
350+
calcCurrentMonth(newValue);
351+
};
352+
353+
const onScroll = (e: React.UIEvent<HTMLDivElement>) => {
354+
props.onScroll?.({ e });
355+
};
356+
247357
return (
248358
<div ref={ref} className={`${className}`}>
249359
<div className={`${calendarClass}__title`}>{parseTNode(props.title, null, local.title)}</div>
250360
{props.usePopup && <CloseIcon className={`${calendarClass}__close-btn`} size={24} onClick={handleClose} />}
361+
{props.switchMode !== 'none' && (
362+
<div className={`${calendarClass}-header`}>
363+
<div className={`${calendarClass}-header__action`}>
364+
{props.switchMode === 'year-month' && (
365+
<div
366+
className={classNames([
367+
`${calendarClass}-header__icon`,
368+
{ [`${calendarClass}-header__icon--disabled`]: headerButtons.preYearBtnDisable },
369+
])}
370+
onClick={() => handleSwitchModeChange('pre-year', headerButtons.preYearBtnDisable)}
371+
>
372+
<ChevronLeftDoubleIcon />
373+
</div>
374+
)}
375+
376+
<div
377+
className={classNames([
378+
`${calendarClass}-header__icon`,
379+
{ [`${calendarClass}-header__icon--disabled`]: headerButtons.prevMonthBtnDisable },
380+
])}
381+
onClick={() => handleSwitchModeChange('pre-month', headerButtons.prevMonthBtnDisable)}
382+
>
383+
<ChevronLeftIcon />
384+
</div>
385+
</div>
386+
<div className={`${calendarClass}-header__title`}>
387+
{t(local.monthTitle, { year: currentMonth[0]?.year, month: local.months[currentMonth[0]?.month] })}
388+
</div>
389+
<div className={`${calendarClass}-header__action`}>
390+
<div
391+
className={classNames([
392+
`${calendarClass}-header__icon`,
393+
{ [`${calendarClass}-header__icon--disabled`]: headerButtons.nextMonthBtnDisable },
394+
])}
395+
onClick={() => handleSwitchModeChange('next-month', headerButtons.nextMonthBtnDisable)}
396+
>
397+
<ChevronRightIcon />
398+
</div>
399+
400+
{props.switchMode === 'year-month' && (
401+
<div
402+
className={classNames([
403+
`${calendarClass}-header__icon`,
404+
{ [`${calendarClass}-header__icon--disabled`]: headerButtons.nextYearBtnDisable },
405+
])}
406+
onClick={() => handleSwitchModeChange('next-year', headerButtons.nextYearBtnDisable)}
407+
>
408+
<ChevronRightDoubleIcon />
409+
</div>
410+
)}
411+
</div>
412+
</div>
413+
)}
251414
<div className={`${calendarClass}__days`}>
252415
{days.map((item, index) => (
253416
<div key={index} className={`${calendarClass}__days-item`}>
@@ -256,12 +419,14 @@ const CalendarTemplate = forwardRef<HTMLDivElement, CalendarProps>((_props, ref)
256419
))}
257420
</div>
258421

259-
<div className={`${calendarClass}__months`} style={{ overflow: 'auto' }}>
260-
{months.map((item, index) => (
422+
<div className={`${calendarClass}__months`} style={{ overflow: 'auto' }} onScroll={onScroll}>
423+
{(props.switchMode === 'none' ? months : currentMonth).map((item, index) => (
261424
<React.Fragment key={`month-${item.year}-${item.month}-${index}`}>
262-
<div className={`${calendarClass}__month`}>
263-
{t(local.monthTitle, { year: item.year, month: local.months[item.month] })}
264-
</div>
425+
{props.switchMode === 'none' && (
426+
<div className={`${calendarClass}__month`}>
427+
{t(local.monthTitle, { year: item.year, month: local.months[item.month] })}
428+
</div>
429+
)}
265430
<div className={`${calendarClass}__dates`}>
266431
{new Array((item.weekdayOfFirstDay - firstDayOfWeek + 7) % 7).fill(0).map((_, emptyIndex) => (
267432
<div key={`empty-${item.year}-${item.month}-${emptyIndex}`} /> // 为空 div 添加唯一 key

src/calendar/_example/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import CustomTextDemo from './custom-text';
88
import CustomButtonDemo from './custom-button';
99
import CustomRangeDemo from './custom-range';
1010
import WithoutPopupDemo from './without-popup';
11+
import SwitchModeDemo from './switch-mode';
1112
import './style/index.less';
1213

1314
export default function CheckboxDemo() {
@@ -23,6 +24,7 @@ export default function CheckboxDemo() {
2324
<CustomTextDemo />
2425
<CustomButtonDemo />
2526
<CustomRangeDemo />
27+
<SwitchModeDemo />
2628
</TDemoBlock>
2729
<TDemoBlock summary="不使用 Popup">
2830
<WithoutPopupDemo />
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
.is-holiday:not(.t-calendar__dates-item--selected) {
22
color: #e34d59;
3-
}
3+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React, { useState } from 'react';
2+
import { Calendar, Cell } from 'tdesign-mobile-react';
3+
4+
export default function () {
5+
const [visible, setVisible] = useState(false);
6+
const [dataNote, setDataNote] = useState('');
7+
8+
const format = (val: Date) => {
9+
const date = new Date(val);
10+
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
11+
};
12+
13+
const handleConfirm = (val: Date) => {
14+
console.log('confirm: ', val);
15+
setVisible(false);
16+
setDataNote(format(val));
17+
};
18+
const handleSelect = (val: Date) => {
19+
console.log('select: ', val);
20+
};
21+
const onClose = (trigger: string) => {
22+
setVisible(false);
23+
console.log('closed by: ', trigger);
24+
};
25+
const onPanelChange = ({ year, month }: { year: number; month: number }) => {
26+
console.log('panel change: ', year, month);
27+
};
28+
const onScroll = ({ e }: { e: React.UIEvent }) => {
29+
console.log('scroll: ', e);
30+
};
31+
32+
return (
33+
<div>
34+
<Calendar
35+
visible={visible}
36+
switchMode="year-month"
37+
value={new Date(2022, 0, 15)}
38+
minDate={new Date(2022, 0, 1)}
39+
maxDate={new Date(2025, 10, 30)}
40+
onConfirm={handleConfirm}
41+
onSelect={handleSelect}
42+
onClose={onClose}
43+
onPanelChange={onPanelChange}
44+
onScroll={onScroll}
45+
></Calendar>
46+
<Cell title="带翻页功能" arrow note={dataNote} onClick={() => setVisible(true)}></Cell>
47+
</div>
48+
);
49+
}

0 commit comments

Comments
 (0)