From 16bbdf7a3b0cb8bcc3529e3ee797ba31de26d679 Mon Sep 17 00:00:00 2001 From: Rainbow <1256734885@qq.com> Date: Thu, 25 Dec 2025 18:04:10 +0800 Subject: [PATCH] feat(components): [calendar] add `controller-type` and `formatter` props (#23045) * feat(components): [calendar] support controller-type prop * refactor: move handleDateChange to useCalendar * feat: update * style: update * style: update * feat: support formatter prop * feat: update * docs: update version number * chore: use ExtractPublicPropTypes * test: update * chore: improve parameter type * feat: update version number * feat: update * feat: update * Apply suggestions from code review Co-authored-by: rzzf * docs: update example --------- Co-authored-by: rzzf Co-authored-by: warmthsea <2586244885@qq.com> --- docs/en-US/component/calendar.md | 18 +++- docs/examples/calendar/controller-type.vue | 15 ++++ .../calendar/__tests__/calendar.test.tsx | 80 +++++++++++++++++ packages/components/calendar/src/calendar.ts | 16 ++++ packages/components/calendar/src/calendar.vue | 17 +++- .../calendar/src/select-controller.ts | 32 +++++++ .../calendar/src/select-controller.vue | 90 +++++++++++++++++++ .../components/calendar/src/use-calendar.ts | 9 ++ packages/components/calendar/style/css.ts | 1 + packages/components/calendar/style/index.ts | 1 + packages/theme-chalk/src/calendar.scss | 14 +++ 11 files changed, 288 insertions(+), 5 deletions(-) create mode 100644 docs/examples/calendar/controller-type.vue create mode 100644 packages/components/calendar/src/select-controller.ts create mode 100644 packages/components/calendar/src/select-controller.vue diff --git a/docs/en-US/component/calendar.md b/docs/en-US/component/calendar.md index d8f20751de..1c62895f9b 100644 --- a/docs/en-US/component/calendar.md +++ b/docs/en-US/component/calendar.md @@ -15,6 +15,14 @@ calendar/basic ::: +## Controller Type ^(2.13.1) + +:::demo You can set the type of the controller for Calendar header. When setting `select`, you can use `formatter` to customize `label`. + +calendar/controller-type + +::: + ## Custom Content :::demo Customize what is displayed in the calendar cell by setting `scoped-slot` named `date-cell`. In `scoped-slot` you can get the date (the date of the current cell), data (including the type, isSelected, day attribute). For details, please refer to the API documentation below. @@ -49,10 +57,12 @@ Note, date time locale (month name, first day of the week ...) are also configur ### Attributes -| Name | Description | Type | Default | -| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ------- | -| model-value / v-model | binding value | ^[Date] | — | -| range | time range, including start time and end time. Start time must be start day of week, end time must be end day of week, the time span cannot exceed two months. | ^[array]`[Date, Date]` | — | +| Name | Description | Type | Default | +| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------- | +| model-value / v-model | binding value | ^[Date] | — | +| range | time range, including start time and end time. Start time must be start day of week, end time must be end day of week, the time span cannot exceed two months. | ^[array]`[Date, Date]` | — | +| controller-type ^(2.13.1) | type of the controller for Calendar header | ^[enum]`'button' \| 'select'` | button | +| formatter ^(2.13.1) | format label when `controller-type` is 'select' | ^[Function]`(value: number, type: 'year' \| 'month') => string \| number` | — | ### Slots diff --git a/docs/examples/calendar/controller-type.vue b/docs/examples/calendar/controller-type.vue new file mode 100644 index 0000000000..ebbc989f44 --- /dev/null +++ b/docs/examples/calendar/controller-type.vue @@ -0,0 +1,15 @@ + + + diff --git a/packages/components/calendar/__tests__/calendar.test.tsx b/packages/components/calendar/__tests__/calendar.test.tsx index 0d27b66d22..c5b62eed6c 100644 --- a/packages/components/calendar/__tests__/calendar.test.tsx +++ b/packages/components/calendar/__tests__/calendar.test.tsx @@ -247,4 +247,84 @@ describe('Calendar.vue', () => { expect(wrapper.find('.el-calendar__header').text()).toEqual(AXIOM) expect(wrapper.find('.current.is-today').text()).toEqual(AXIOM) }) + + it('should work when controller-type is select', async () => { + const wrapper = mount( + defineComponent({ + data: () => ({ value: new Date('2025-12-09') }), + render() { + return + }, + }) + ) + + await nextTick() + const selects = wrapper.findAllComponents({ name: 'ElSelect' }) + const btn = wrapper.find('.el-button') + const yearSelect = selects[0] + const yearOptions = yearSelect.findAllComponents({ name: 'ElOption' }) + const monthSelect = selects[1] + const monthOptions = monthSelect.findAllComponents({ name: 'ElOption' }) + const yearVm = yearSelect.vm as any + const monthVm = monthSelect.vm as any + const firstRow = wrapper.element.querySelector('.el-calendar-table__row') + + expect(yearVm.modelValue).toBe(2025) + expect(monthVm.modelValue).toBe(12) + expect(firstRow?.firstElementChild?.innerHTML).toContain('30') + expect(firstRow?.lastElementChild?.innerHTML).toContain('6') + + await yearOptions[9].trigger('click') + expect(yearVm.modelValue).toBe(2024) + expect(monthVm.modelValue).toBe(12) + expect(firstRow?.firstElementChild?.innerHTML).toContain('1') + expect(firstRow?.lastElementChild?.innerHTML).toContain('7') + + await monthOptions[10].trigger('click') + expect(yearVm.modelValue).toBe(2024) + expect(monthVm.modelValue).toBe(11) + expect(firstRow?.firstElementChild?.innerHTML).toContain('27') + expect(firstRow?.lastElementChild?.innerHTML).toContain('2') + + await btn.trigger('click') + expect(yearVm.modelValue).toBe(2025) + expect(monthVm.modelValue).toBe(12) + expect(firstRow?.firstElementChild?.innerHTML).toContain('30') + expect(firstRow?.lastElementChild?.innerHTML).toContain('6') + }) + + it('should work with formatter prop', async () => { + const formatter = (value: number, type: 'year' | 'month') => { + if (type === 'year') { + return `${value}年` + } else { + return `${value}月` + } + } + + const wrapper = mount( + defineComponent({ + data: () => ({ value: new Date('2025-12-09') }), + render() { + return ( + + ) + }, + }) + ) + + await nextTick() + const selects = wrapper.findAllComponents({ name: 'ElSelect' }) + const yearSelect = selects[0] + const yearOptions = yearSelect.findAllComponents({ name: 'ElOption' }) + const monthSelect = selects[1] + const monthOptions = monthSelect.findAllComponents({ name: 'ElOption' }) + + expect(yearOptions[0].text()).toBe('2015年') + expect(monthOptions[0].text()).toBe('1月') + }) }) diff --git a/packages/components/calendar/src/calendar.ts b/packages/components/calendar/src/calendar.ts index 3c9ada3e80..bd22c8dbf5 100644 --- a/packages/components/calendar/src/calendar.ts +++ b/packages/components/calendar/src/calendar.ts @@ -33,6 +33,22 @@ export const calendarProps = buildProps({ type: definePropType<[Date, Date]>(Array), validator: isValidRange, }, + /** + * @description type of the controller for the Calendar header + */ + controllerType: { + type: String, + values: ['button', 'select'], + default: 'button', + }, + /** + * @description format label when `controller-type` is 'select' + */ + formatter: { + type: definePropType< + (value: number, type: 'year' | 'month') => string | number + >(Function), + }, } as const) export type CalendarProps = ExtractPropTypes export type CalendarPropsPublic = ExtractPublicPropTypes diff --git a/packages/components/calendar/src/calendar.vue b/packages/components/calendar/src/calendar.vue index 4be2b732b5..f93a4f9de8 100644 --- a/packages/components/calendar/src/calendar.vue +++ b/packages/components/calendar/src/calendar.vue @@ -3,7 +3,10 @@
{{ i18nDate }}
-
+
{{ t('el.datepicker.prevMonth') }} @@ -16,6 +19,16 @@
+
+ +
@@ -50,6 +63,7 @@ import { useLocale, useNamespace } from '@element-plus/hooks' import DateTable from './date-table.vue' import { useCalendar } from './use-calendar' import { calendarEmits, calendarProps } from './calendar' +import SelectController from './select-controller.vue' const ns = useNamespace('calendar') @@ -68,6 +82,7 @@ const { realSelectedDay, selectDate, validatedRange, + handleDateChange, } = useCalendar(props, emit, COMPONENT_NAME) const { t } = useLocale() diff --git a/packages/components/calendar/src/select-controller.ts b/packages/components/calendar/src/select-controller.ts new file mode 100644 index 0000000000..d76d4d7e89 --- /dev/null +++ b/packages/components/calendar/src/select-controller.ts @@ -0,0 +1,32 @@ +import { + buildProps, + definePropType, + isObject, + isString, +} from '@element-plus/utils' + +import type { ExtractPropTypes, ExtractPublicPropTypes } from 'vue' +import type { Dayjs } from 'dayjs' + +export const selectControllerProps = buildProps({ + date: { + type: definePropType(Object), + required: true, + }, + formatter: { + type: definePropType< + (value: number, type: 'year' | 'month') => string | number + >(Function), + }, +} as const) +export type SelectControllerProps = ExtractPropTypes< + typeof selectControllerProps +> +export type SelectControllerPropsPublic = ExtractPublicPropTypes< + typeof selectControllerProps +> + +export const selectControllerEmits = { + 'date-change': (date: Dayjs | 'today') => isObject(date) || isString(date), +} +export type SelectControllerEmits = typeof selectControllerEmits diff --git a/packages/components/calendar/src/select-controller.vue b/packages/components/calendar/src/select-controller.vue new file mode 100644 index 0000000000..031b64a34d --- /dev/null +++ b/packages/components/calendar/src/select-controller.vue @@ -0,0 +1,90 @@ + + + diff --git a/packages/components/calendar/src/use-calendar.ts b/packages/components/calendar/src/use-calendar.ts index 2d7bdfb70e..fa74d53b60 100644 --- a/packages/components/calendar/src/use-calendar.ts +++ b/packages/components/calendar/src/use-calendar.ts @@ -178,6 +178,14 @@ export const useCalendar = ( } } + const handleDateChange = (date: Dayjs | 'today') => { + if (date === 'today') { + selectDate('today') + } else { + pickDay(date) + } + } + return { calculateValidatedDateRange, date, @@ -185,5 +193,6 @@ export const useCalendar = ( pickDay, selectDate, validatedRange, + handleDateChange, } } diff --git a/packages/components/calendar/style/css.ts b/packages/components/calendar/style/css.ts index ee3b3433b8..a55e6ee325 100644 --- a/packages/components/calendar/style/css.ts +++ b/packages/components/calendar/style/css.ts @@ -2,3 +2,4 @@ import '@element-plus/components/base/style/css' import '@element-plus/theme-chalk/el-calendar.css' import '@element-plus/components/button/style/css' import '@element-plus/components/button-group/style/css' +import '@element-plus/components/select/style/css' diff --git a/packages/components/calendar/style/index.ts b/packages/components/calendar/style/index.ts index a4a0e8d98e..7c49659b7e 100644 --- a/packages/components/calendar/style/index.ts +++ b/packages/components/calendar/style/index.ts @@ -2,3 +2,4 @@ import '@element-plus/components/base/style' import '@element-plus/theme-chalk/src/calendar.scss' import '@element-plus/components/button/style' import '@element-plus/components/button-group/style' +import '@element-plus/components/select/style' diff --git a/packages/theme-chalk/src/calendar.scss b/packages/theme-chalk/src/calendar.scss index d0cf1d5a1f..a309ab5444 100644 --- a/packages/theme-chalk/src/calendar.scss +++ b/packages/theme-chalk/src/calendar.scss @@ -22,6 +22,20 @@ @include e(body) { padding: 12px 20px 35px; } + + @include e(select-controller) { + .#{$namespace}-select { + margin-right: 8px; + } + + .#{$namespace}-calendar-select__year { + width: 120px; + } + + .#{$namespace}-calendar-select__month { + width: 60px; + } + } } @include b(calendar-table) {