diff --git a/packages/bui-core/src/Calendar/Calendar.tsx b/packages/bui-core/src/Calendar/Calendar.tsx index 2ac7a343f..409c5670b 100644 --- a/packages/bui-core/src/Calendar/Calendar.tsx +++ b/packages/bui-core/src/Calendar/Calendar.tsx @@ -1,11 +1,21 @@ import { CaretLeftIcon, CaretRightIcon } from '@bifrostui/icons'; -import { useDidMountEffect, useValue } from '@bifrostui/utils'; +import { useValue } from '@bifrostui/utils'; import clsx from 'clsx'; import dayjs from 'dayjs'; import isoWeek from 'dayjs/plugin/isoWeek'; import customParseFormat from 'dayjs/plugin/customParseFormat'; -import React, { SyntheticEvent, useMemo, useState } from 'react'; -import { CalendarProps, ICalendarInstance } from './Calendar.types'; +import React, { + SyntheticEvent, + useImperativeHandle, + useMemo, + useRef, + useState, +} from 'react'; +import { + CalendarProps, + CalendarRef, + ICalendarInstance, +} from './Calendar.types'; import { formatDate, isRange, isSame } from './utils'; import { useLocaleText } from '../locales'; import './Calendar.less'; @@ -21,350 +31,346 @@ const classes = { disabled: 'bui-calendar-disabled', }; -const Calendar = React.forwardRef( - (props, ref) => { - const { - className, - defaultValue, - value, - minDate, - maxDate, - mode, - hideDaysOutsideCurrentMonth, - headerBarFormat, - headerBarLeftIcon, - headerBarRightIcon, - disabledDate, - highlightDate, - headerVisible = false, - dateRender, - weekRender, - onMonthChange, - onChange, - ...others - } = props; - const { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday } = - useLocaleText('calendar'); - const SUNDAY_WEEK_DATA = [ - Sunday, - Monday, - Tuesday, - Wednesday, - Thursday, - Friday, - Saturday, - ]; - const isRangeMode = mode === 'range'; - /** @type undefined | Array */ - const formattedValue = formatDate(mode, value, minDate, maxDate); - /** @type undefined | Array */ - const formattedDefaultValue = formatDate( - mode, - defaultValue, - minDate, - maxDate, - ); - - // 头部操作栏月份 - const [renderMonth, setRenderMonth] = useState(() => { - const initMonth = - formattedValue === undefined - ? formattedDefaultValue?.[0] - : formattedValue?.[0]; - return dayjs(initMonth || minDate).toDate(); - }); - /** - * 日历状态值 - * @type Array - */ - const [calendarValue, triggerChange] = useValue({ - value: formattedValue, - defaultValue: formattedDefaultValue, - onChange, - }); - // 根据calendarValue计算选中开始/结束日期 - const selectedStartDate = useMemo(() => { - return calendarValue?.[0]; - }, [calendarValue]); - const selectedEndDate = useMemo(() => { - return calendarValue?.[1]; - }, [calendarValue]); - - const isMinMonth = dayjs(minDate).isSame(renderMonth, 'month'); - const isMaxMonth = dayjs(maxDate).isSame(renderMonth, 'month'); - - // 头部操作栏左右图标 - const headerBarIcon = { - left: headerBarLeftIcon ? ( - headerBarLeftIcon({ isMinMonth }) - ) : ( - - ), - right: headerBarRightIcon ? ( - headerBarRightIcon({ isMaxMonth }) - ) : ( - - ), - }; - - useDidMountEffect(() => { - const initMonth = - formattedValue === undefined - ? formattedDefaultValue?.[0] - : formattedValue?.[0]; - setRenderMonth(dayjs(initMonth || minDate).toDate()); - }, [JSON.stringify(value), JSON.stringify(defaultValue)]); - - const getDaysInMonth = (args: Date) => { - const m = dayjs(args).format('YYYY/MM'); - // 获取当前月份包含的天数 - const count = dayjs(m).daysInMonth(); - // 获取月初ISO星期几 - const week = dayjs(m).isoWeekday(); - // 获取月末ISO星期几 - const eneWeek = dayjs(m).endOf('month').isoWeekday(); - // 获取上月天数 - const prevMount = dayjs(m).subtract(1, 'month'); - - const list = []; - - // 填补上月日期 - if (week !== 7) { - const prevMountCount = prevMount.daysInMonth(); - [...Array(week)].forEach((_, i) => { - const day = dayjs( - `${prevMount.format('YYYY/MM')}/${prevMountCount - i}`, - ).toDate(); - list.unshift({ - day: hideDaysOutsideCurrentMonth ? null : day, - disabled: true, - }); - }); - } - - [...Array(count)].forEach((_, i) => { - const day = dayjs(`${m}/${i + 1}`).toDate(); - - // 先判断是否minDate与maxDate之间,再判断传入的方法 - const defaultDisable = !isRange(day, minDate, maxDate); - const propsDisable = disabledDate ? !!disabledDate?.(day) : false; - - list.push({ - day, - disabled: defaultDisable ? true : propsDisable, - }); - }); - - // 填补下月日期 - const end = eneWeek === 7 ? 6 : 6 - eneWeek; - [...Array(end)].forEach((_, i) => { - const nextMonth = dayjs(m).add(1, 'month'); +const Calendar = React.forwardRef((props, ref) => { + const { + className, + defaultValue, + value, + minDate, + maxDate, + mode, + hideDaysOutsideCurrentMonth, + headerBarFormat, + headerBarLeftIcon, + headerBarRightIcon, + disabledDate, + highlightDate, + headerVisible = false, + dateRender, + weekRender, + onMonthChange, + onChange, + ...others + } = props; + const { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday } = + useLocaleText('calendar'); + const SUNDAY_WEEK_DATA = [ + Sunday, + Monday, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday, + ]; + const isRangeMode = mode === 'range'; + /** @type undefined | Array */ + const formattedValue = formatDate(mode, value, minDate, maxDate); + /** @type undefined | Array */ + const formattedDefaultValue = formatDate( + mode, + defaultValue, + minDate, + maxDate, + ); + + // 头部操作栏月份 + const [renderMonth, setRenderMonth] = useState(() => { + const initMonth = + formattedValue === undefined + ? formattedDefaultValue?.[0] + : formattedValue?.[0]; + return dayjs(initMonth || minDate).toDate(); + }); + /** + * 日历状态值 + * @type Array + */ + const [calendarValue, triggerChange] = useValue({ + value: formattedValue, + defaultValue: formattedDefaultValue, + onChange, + }); + // 根据calendarValue计算选中开始/结束日期 + const selectedStartDate = useMemo(() => { + return calendarValue?.[0]; + }, [calendarValue]); + const selectedEndDate = useMemo(() => { + return calendarValue?.[1]; + }, [calendarValue]); + const rootRef = useRef(null); + + const isMinMonth = dayjs(minDate).isSame(renderMonth, 'month'); + const isMaxMonth = dayjs(maxDate).isSame(renderMonth, 'month'); + + useImperativeHandle(ref, () => ({ + jumpTo: (date: Date) => { + setRenderMonth(date); + }, + rootRef: rootRef.current, + })); + + // 头部操作栏左右图标 + const headerBarIcon = { + left: headerBarLeftIcon ? ( + headerBarLeftIcon({ isMinMonth }) + ) : ( + + ), + right: headerBarRightIcon ? ( + headerBarRightIcon({ isMaxMonth }) + ) : ( + + ), + }; + + const getDaysInMonth = (args: Date) => { + const m = dayjs(args).format('YYYY/MM'); + // 获取当前月份包含的天数 + const count = dayjs(m).daysInMonth(); + // 获取月初ISO星期几 + const week = dayjs(m).isoWeekday(); + // 获取月末ISO星期几 + const eneWeek = dayjs(m).endOf('month').isoWeekday(); + // 获取上月天数 + const prevMount = dayjs(m).subtract(1, 'month'); + + const list = []; + + // 填补上月日期 + if (week !== 7) { + const prevMountCount = prevMount.daysInMonth(); + [...Array(week)].forEach((_, i) => { const day = dayjs( - `${dayjs(nextMonth).format('YYYY/MM')}/${i + 1}`, + `${prevMount.format('YYYY/MM')}/${prevMountCount - i}`, ).toDate(); - list.push({ + list.unshift({ day: hideDaysOutsideCurrentMonth ? null : day, disabled: true, }); }); + } - return list; - }; + [...Array(count)].forEach((_, i) => { + const day = dayjs(`${m}/${i + 1}`).toDate(); - const getDayClassName = ({ day: itemDate, disabled }) => { - let result = ''; - if (disabled) return result; + // 先判断是否minDate与maxDate之间,再判断传入的方法 + const defaultDisable = !isRange(day, minDate, maxDate); + const propsDisable = disabledDate ? !!disabledDate?.(day) : false; - const isToday = - dayjs(itemDate).diff(dayjs().format('YYYYMMDD'), 'day') === 0; + list.push({ + day, + disabled: defaultDisable ? true : propsDisable, + }); + }); - if (selectedStartDate && isSame(selectedStartDate, itemDate)) { - result = `${classes.root}-start`; - // 此时endDate已选中 - if (selectedEndDate) { - result += ` ${classes.root}-range`; - } - } else if (selectedEndDate && isSame(selectedEndDate, itemDate)) { - result = `${classes.root}-end`; - } else if ( - selectedStartDate && - selectedEndDate && - dayjs(itemDate).diff(selectedStartDate) > 0 && - dayjs(itemDate).diff(selectedEndDate) < 0 - ) { - result = `${classes.root}-middle`; - } + // 填补下月日期 + const end = eneWeek === 7 ? 6 : 6 - eneWeek; + [...Array(end)].forEach((_, i) => { + const nextMonth = dayjs(m).add(1, 'month'); + const day = dayjs( + `${dayjs(nextMonth).format('YYYY/MM')}/${i + 1}`, + ).toDate(); + list.push({ + day: hideDaysOutsideCurrentMonth ? null : day, + disabled: true, + }); + }); - if (highlightDate === 'today' && isToday && !result) { - result = `${classes.root}-today`; - } + return list; + }; - return result; - }; + const getDayClassName = ({ day: itemDate, disabled }) => { + let result = ''; + if (disabled) return result; - const defaultDateRender = (ins: ICalendarInstance) => { - const dayClassName = getDayClassName(ins); + const isToday = + dayjs(itemDate).diff(dayjs().format('YYYYMMDD'), 'day') === 0; - return ( -
- {ins.day && dayjs(ins.day).format('D')} -
- ); - }; + if (selectedStartDate && isSame(selectedStartDate, itemDate)) { + result = `${classes.root}-start`; + // 此时endDate已选中 + if (selectedEndDate) { + result += ` ${classes.root}-range`; + } + } else if (selectedEndDate && isSame(selectedEndDate, itemDate)) { + result = `${classes.root}-end`; + } else if ( + selectedStartDate && + selectedEndDate && + dayjs(itemDate).diff(selectedStartDate) > 0 && + dayjs(itemDate).diff(selectedEndDate) < 0 + ) { + result = `${classes.root}-middle`; + } - const onClickDay = (e: SyntheticEvent, ins: ICalendarInstance) => { - if (ins?.disabled) return; + if (highlightDate === 'today' && isToday && !result) { + result = `${classes.root}-today`; + } - if (isRangeMode) { - // 都无值 - if (!selectedStartDate && !selectedEndDate) { - triggerChange?.(e, [ins.day, null]); - } - // start有值,end无值 - else if (selectedStartDate && !selectedEndDate) { - let result; - // 选中了start,此时置空start - if (isSame(ins.day, selectedStartDate)) { - result = [null, null]; - } else { - // 比较当前选中日期与start,重新赋值start、end - result = - dayjs(ins.day).diff(dayjs(selectedStartDate)) < 0 - ? [ins.day, selectedStartDate] - : [selectedStartDate, ins.day]; - } - triggerChange?.(e, result); - } - // start、end都有值,选中日期为start - else if (selectedStartDate && selectedEndDate) { - triggerChange?.(e, [ins.day, null]); - } + return result; + }; - return; - } + const defaultDateRender = (ins: ICalendarInstance) => { + const dayClassName = getDayClassName(ins); - // 单选模式使用selectedStartDate判断是否已选中 - const hasSelectedDate = - selectedStartDate && isSame(ins.day, selectedStartDate); - triggerChange?.(e, hasSelectedDate ? null : ins.day); - }; + return ( +
+ {ins.day && dayjs(ins.day).format('D')} +
+ ); + }; - const renderDayList = () => - getDaysInMonth(renderMonth).map( - (ins: ICalendarInstance, index: number) => { - const dayStr = dayjs(ins.day).format('YYYYMMDD'); - return ( -
onClickDay(e, ins)} - > - {dateRender ? dateRender(ins) : defaultDateRender(ins)} -
- ); - }, - ); + const onClickDay = (e: SyntheticEvent, ins: ICalendarInstance) => { + if (ins?.disabled) return; - /** - * 切换上一个月 - */ - const onClickPrev = (e) => { - if (!isMinMonth) { - const month = dayjs(renderMonth).subtract(1, 'month').toDate(); - setRenderMonth(month); - onMonthChange?.(e, { - type: 'prev', - month: dayjs(month).format(headerBarFormat), - }); + if (isRangeMode) { + // 都无值 + if (!selectedStartDate && !selectedEndDate) { + triggerChange?.(e, [ins.day, null]); } - }; - - /** - * 切换下一个月 - */ - const onClickNext = (e) => { - if (!isMaxMonth) { - const month = dayjs(renderMonth).add(1, 'month').toDate(); - setRenderMonth(month); - onMonthChange?.(e, { - type: 'next', - month: dayjs(month).format(headerBarFormat), - }); + // start有值,end无值 + else if (selectedStartDate && !selectedEndDate) { + let result; + // 选中了start,此时置空start + if (isSame(ins.day, selectedStartDate)) { + result = [null, null]; + } else { + // 比较当前选中日期与start,重新赋值start、end + result = + dayjs(ins.day).diff(dayjs(selectedStartDate)) < 0 + ? [ins.day, selectedStartDate] + : [selectedStartDate, ins.day]; + } + triggerChange?.(e, result); + } + // start、end都有值,选中日期为start + else if (selectedStartDate && selectedEndDate) { + triggerChange?.(e, [ins.day, null]); } - }; - let data: Record = {}; - if (isRangeMode) { - data = { - 'data-start': dayjs(selectedStartDate).format('YYYYMMDD'), - 'data-end': dayjs(selectedEndDate).format('YYYYMMDD'), - }; - } else { - data = { - 'data-selected': dayjs(selectedStartDate).format('YYYYMMDD'), - }; + return; } - return ( -
- {/* 顶部操作栏 */} - {!headerVisible && ( -
-
- {headerBarIcon.left} -
-
- {dayjs(renderMonth).format(headerBarFormat)} -
-
- {headerBarIcon.right} -
-
- )} + // 单选模式使用selectedStartDate判断是否已选中 + const hasSelectedDate = + selectedStartDate && isSame(ins.day, selectedStartDate); + triggerChange?.(e, hasSelectedDate ? null : ins.day); + }; - {/* 周横条 */} -
- {SUNDAY_WEEK_DATA?.map((w) => { - return weekRender ? ( - weekRender(w) - ) : ( -
- {w} -
- ); + const renderDayList = () => + getDaysInMonth(renderMonth).map((ins: ICalendarInstance, index: number) => { + const dayStr = dayjs(ins.day).format('YYYYMMDD'); + return ( +
onClickDay(e, ins)} + > + {dateRender ? dateRender(ins) : defaultDateRender(ins)}
+ ); + }); -
{renderDayList()}
+ /** + * 切换上一个月 + */ + const onClickPrev = (e) => { + if (!isMinMonth) { + const month = dayjs(renderMonth).subtract(1, 'month').toDate(); + setRenderMonth(month); + onMonthChange?.(e, { + type: 'prev', + month: dayjs(month).format(headerBarFormat), + }); + } + }; + + /** + * 切换下一个月 + */ + const onClickNext = (e) => { + if (!isMaxMonth) { + const month = dayjs(renderMonth).add(1, 'month').toDate(); + setRenderMonth(month); + onMonthChange?.(e, { + type: 'next', + month: dayjs(month).format(headerBarFormat), + }); + } + }; + + let data: Record = {}; + if (isRangeMode) { + data = { + 'data-start': dayjs(selectedStartDate).format('YYYYMMDD'), + 'data-end': dayjs(selectedEndDate).format('YYYYMMDD'), + }; + } else { + data = { + 'data-selected': dayjs(selectedStartDate).format('YYYYMMDD'), + }; + } + + return ( +
+ {/* 顶部操作栏 */} + {!headerVisible && ( +
+
+ {headerBarIcon.left} +
+
+ {dayjs(renderMonth).format(headerBarFormat)} +
+
+ {headerBarIcon.right} +
+
+ )} + + {/* 周横条 */} +
+ {SUNDAY_WEEK_DATA?.map((w) => { + return weekRender ? ( + weekRender(w) + ) : ( +
+ {w} +
+ ); + })}
- ); - }, -); + +
{renderDayList()}
+
+ ); +}); Calendar.displayName = 'BuiCalendar'; Calendar.defaultProps = { diff --git a/packages/bui-core/src/Calendar/Calendar.types.ts b/packages/bui-core/src/Calendar/Calendar.types.ts index a932ac8c1..bbb62ac99 100644 --- a/packages/bui-core/src/Calendar/Calendar.types.ts +++ b/packages/bui-core/src/Calendar/Calendar.types.ts @@ -116,3 +116,8 @@ export type CalendarProps< }, D >; + +export type CalendarRef = { + /** 跳转到指定月份 */ + jumpTo?: (date: Date) => void; +}; diff --git a/packages/bui-core/src/Calendar/__tests__/Calendar.test.tsx b/packages/bui-core/src/Calendar/__tests__/Calendar.test.tsx index dd532922b..ef0e3ae20 100644 --- a/packages/bui-core/src/Calendar/__tests__/Calendar.test.tsx +++ b/packages/bui-core/src/Calendar/__tests__/Calendar.test.tsx @@ -25,6 +25,7 @@ describe('Calendar', () => { minDate: dayjs('20230401').toDate(), maxDate: dayjs('20230429').toDate(), }, + skip: ['component-has-root-ref'], }); it('should render date range in minDate to maxDate', () => { diff --git a/packages/bui-core/src/Calendar/index.en-US.md b/packages/bui-core/src/Calendar/index.en-US.md index 472aeb78e..cfe36b6cf 100644 --- a/packages/bui-core/src/Calendar/index.en-US.md +++ b/packages/bui-core/src/Calendar/index.en-US.md @@ -281,7 +281,7 @@ Control the calendar component using the `value` property. ```tsx import { Button, Calendar, Stack } from '@bifrostui/react'; import dayjs from 'dayjs/esm/index'; -import React, { useState } from 'react'; +import React, { useState, useRef } from 'react'; export default () => { const [value, setValue] = useState(dayjs().add(1, 'month').toDate()); @@ -289,9 +289,12 @@ export default () => { dayjs().add(30, 'day').toDate(), dayjs().add(33, 'day').toDate(), ]); + const ref = useRef(); const onSingleClick = () => { + const today = dayjs().toDate(); setValue(dayjs().toDate()); + ref.current.jumpTo(today); }; const onRangeClick = () => { @@ -430,6 +433,36 @@ export default () => { }; ``` +### Jump to Specified Date's Month Page + +Use `ref.current.jumpTo` to jumpto specified date's month page. + +```tsx +import { Button, Calendar, Stack } from '@bifrostui/react'; +import dayjs from 'dayjs/esm/index'; +import React, { useState, useRef } from 'react'; + +export default () => { + const [value, setValue] = useState(dayjs().add(1, 'month').toDate()); + const ref = useRef(); + + const onSingleClick = () => { + const today = dayjs().toDate(); + setValue(dayjs().toDate()); + ref.current.jumpTo(today); + }; + + return ( + +
+ + +
+
+ ); +}; +``` + ## API | Property | Description | Type | Default Value | diff --git a/packages/bui-core/src/Calendar/index.zh-CN.md b/packages/bui-core/src/Calendar/index.zh-CN.md index e122d9b4c..d71828dbf 100644 --- a/packages/bui-core/src/Calendar/index.zh-CN.md +++ b/packages/bui-core/src/Calendar/index.zh-CN.md @@ -311,7 +311,7 @@ export default () => { ```tsx import { Button, Calendar, Stack } from '@bifrostui/react'; import dayjs from 'dayjs/esm/index'; -import React, { useState } from 'react'; +import React, { useState, useRef } from 'react'; export default () => { const [value, setValue] = useState(dayjs().add(1, 'month').toDate()); @@ -319,9 +319,12 @@ export default () => { dayjs().add(30, 'day').toDate(), dayjs().add(33, 'day').toDate(), ]); + const ref = useRef(); const onSingleClick = () => { + const today = dayjs().toDate(); setValue(dayjs().toDate()); + ref.current.jumpTo(today); }; const onRangeClick = () => { @@ -332,7 +335,7 @@ export default () => {
- +
@@ -460,6 +463,36 @@ export default () => { }; ``` +### 跳转到指定日期的月份页面 + +通过 `ref.current.jumpTo` 方法跳转到指定日期的月份页面。 + +```tsx +import { Button, Calendar, Stack } from '@bifrostui/react'; +import dayjs from 'dayjs/esm/index'; +import React, { useState, useRef } from 'react'; + +export default () => { + const [value, setValue] = useState(dayjs().add(1, 'month').toDate()); + const ref = useRef(); + + const onSingleClick = () => { + const today = dayjs().toDate(); + setValue(dayjs().toDate()); + ref.current.jumpTo(today); + }; + + return ( + +
+ + +
+
+ ); +}; +``` + ## API | 属性 | 说明 | 类型 | 默认值 |