@@ -63,7 +64,7 @@ function monthCellRender(date) {
);
}
return monthLocale[date.month()];
-}
+};
ReactDOM.render(
,
diff --git a/components/calendar/__docs__/demo/default-visible-month/index.tsx b/components/calendar/__docs__/demo/default-visible-month/index.tsx
index 838ccd12d9..145b93d49b 100644
--- a/components/calendar/__docs__/demo/default-visible-month/index.tsx
+++ b/components/calendar/__docs__/demo/default-visible-month/index.tsx
@@ -2,14 +2,15 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { Calendar } from '@alifd/next';
import moment from 'moment';
+import { type CalendarProps } from '@alifd/next/lib/calendar';
-function onSelect(value) {
+const onSelect: CalendarProps['onSelect'] = value => {
console.log(value.format('L'));
-}
+};
-function onVisibleMonthChange(value, reason) {
+const onVisibleMonthChange: CalendarProps['onVisibleMonthChange'] = (value, reason) => {
console.log('Visible month changed to %s from <%s>', value.format('YYYY-MM'), reason);
-}
+};
ReactDOM.render(
currentDate.valueOf();
};
diff --git a/components/calendar/__docs__/demo/lunar/index.tsx b/components/calendar/__docs__/demo/lunar/index.tsx
index f8141fabeb..b26dd13433 100644
--- a/components/calendar/__docs__/demo/lunar/index.tsx
+++ b/components/calendar/__docs__/demo/lunar/index.tsx
@@ -3,12 +3,13 @@ import ReactDOM from 'react-dom';
import { Calendar } from '@alifd/next';
import moment from 'moment';
import solarLunar from 'solarlunar';
+import { type CalendarProps } from '@alifd/next/lib/calendar';
-function onDateChange(value) {
+const onDateChange: CalendarProps['onSelect'] = value => {
console.log(value.format('L'));
-}
+};
-function dateCellRender(value) {
+const dateCellRender: CalendarProps['dateCellRender'] = value => {
const solar2lunarData = solarLunar.solar2lunar(value.year(), value.month(), value.date());
return (
@@ -19,7 +20,7 @@ function dateCellRender(value) {
);
-}
+};
ReactDOM.render(
diff --git a/components/calendar/__docs__/index.en-us.md b/components/calendar/__docs__/index.en-us.md
index fe36158e4c..66299b9404 100644
--- a/components/calendar/__docs__/index.en-us.md
+++ b/components/calendar/__docs__/index.en-us.md
@@ -29,18 +29,52 @@ moment.locale('zh-cn');
### Calendar
-| Param | Description | Type | Default Value |
-| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------- |
-| defaultValue | Default value of calendar | custom | - |
-| shape | Shape of calendar
**option**:
'card', 'fullscreen', 'panel' | Enum | 'fullscreen' |
-| value | Value of calendar | custom | - |
-| mode | Mode of panel
**option**:
'date', 'month', 'year' | Enum | 'date' |
-| showOtherMonth | Show dates of other month in current date | Boolean | true |
-| defaultVisibleMonth | Default visible month of panel
**signature**:
Function() => void | Function | - |
-| onSelect | Callback when select a date
**signature**:
Function(value: Object) => void
**parameter**:
_value_: {Object} date object | Function | func.noop |
-| onModeChange | Callback when change mode
**签名**:
Function(mode: string) => void
**参数**:
_mode_: {string} mode type: date month year | Function | func.noop |
-| dateCellRender | Render function for date cell
**signature**:
Function(value: Object) => ReactNode
**parameter**:
_value_: {Object} date object
**return**:
{ReactNode} null
| Function | (value) => value.date() |
-| monthCellRender | Render function for month cell
**signature**:
Function(calendarDate: Object) => ReactNode
**parameter**:
_calendarDate_: {Object} current date object
**return**:
{ReactNode} null
| Function | - |
-| yearRange | Year Range,[START_YEAR, END_YEAR] \(only shape in ‘card’, 'fullscreen') | Array<Number> | - |
-| disabledDate | Function to disable dates
**signature**:
Function(calendarDate: Object) => Boolean
**parameter**:
_calendarDate_: {Object} current date object
_view_: {Enum} current view type: 'year', 'month', 'date'
**return**:
{Boolean} null
| Function | - |
+| Param | Description | Type | Default Value | Required |
+| -------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ------------------------- | -------- |
+| defaultValue | Default selected date (moment object) | Moment \| null | - | |
+| shape | Display shape | 'card' \| 'fullscreen' \| 'panel' | 'fullscreen' | |
+| value | Selected date value (moment object) | Moment \| null | - | |
+| mode | Panel mode | CalendarMode | - | |
+| showOtherMonth | Whether to show dates outside the current month | boolean | true | |
+| defaultVisibleMonth | Default displayed month | () => Moment \| null | - | |
+| onModeChange | Callback when the panel mode changes
**signature**:
**params**:
_mode_: mode | (mode: CalendarMode) => void | - | |
+| onSelect | Callback when selecting a date cell | (value: Moment) => void | - | |
+| onVisibleMonthChange | Callback when the displayed month changes | (value: Moment, reason: VisibleMonthChangeType) => void | - | |
+| dateCellRender | Customize date rendering function | (value: Moment) => React.ReactNode | value =\> value.date() | |
+| monthCellRender | Customize month rendering function | (calendarDate: Moment) => React.ReactNode | - | |
+| disabledDate | Disabled date | (calendarDate: Moment, view: CalendarMode) => boolean | - | |
+| modes | Panel mode list that can be changed, only received once at initialization | CalendarMode[] | ['date', 'month', 'year'] | |
+| format | Date value format(for date title display format) | string | 'YYYY-MM | |
+| yearRange | Year range, [START_YEAR, END_YEAR] (only effective when shape is 'card', 'fullscreen') | [start: number, end: number] | - | |
+### Calendar.RangeCalendar
+
+| Param | Description | Type | Default Value | Required |
+| -------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------- | ---------------------- | -------- |
+| mode | Panel mode | CalendarMode | 'date' | |
+| format | Date value format(for date title display format) | string | 'YYYY-MM | |
+| dateCellRender | Customize date rendering function | (value: Moment) => React.ReactNode | value =\> value.date() | |
+| onSelect | Callback when selecting a date cell | (value: Moment) => void | - | |
+| onVisibleMonthChange | Callback when the displayed month changes | (value: Moment, reason: VisibleMonthChangeType) => void | - | |
+| showOtherMonth | Whether to show dates outside the current month | boolean | true | |
+| startValue | Start date (moment object) | Moment \| null | - | |
+| endValue | End date (moment object) | Moment \| null | - | |
+| defaultStartValue | Default start date (moment object) | Moment \| null | - | |
+| defaultEndValue | Default end date (moment object) | Moment \| null | - | |
+| monthCellRender | Customize month rendering function | (calendarDate: Moment) => React.ReactNode | - | |
+| defaultVisibleMonth | Default displayed month | () => Moment \| null | - | |
+| disabledDate | Disabled date | (calendarDate: Moment, view: CalendarMode) => boolean | - | |
+| shape | Display shape | 'card' \| 'fullscreen' \| 'panel' | - | |
+| yearRange | Year range, [START_YEAR, END_YEAR] (only effective when shape is 'card', 'fullscreen') | [number, number] | - | |
+
+### CalendarMode
+
+```typescript
+export type CalendarMode = 'date' | 'month' | 'year';
+```
+
+### VisibleMonthChangeType
+
+```typescript
+export type VisibleMonthChangeType = 'cellClick' | 'buttonClick' | 'yearSelect' | 'monthSelect';
+```
diff --git a/components/calendar/__docs__/index.md b/components/calendar/__docs__/index.md
index b32019804a..a237b93b39 100644
--- a/components/calendar/__docs__/index.md
+++ b/components/calendar/__docs__/index.md
@@ -27,18 +27,52 @@ moment.locale('zh-cn');
### Calendar
-| 参数 | 说明 | 类型 | 默认值 |
-| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------- | --------------------- |
-| defaultValue | 默认选中的日期(moment 对象) | custom | - |
-| shape | 展现形态
**可选值**:
'card', 'fullscreen', 'panel' | Enum | 'fullscreen' |
-| value | 选中的日期值 (moment 对象) | custom | - |
-| mode | 面板模式 | Enum | - |
-| showOtherMonth | 是否展示非本月的日期 | Boolean | true |
-| defaultVisibleMonth | 默认展示的月份
**签名**:
Function() => void | Function | - |
-| onSelect | 选择日期单元格时的回调
**签名**:
Function(value: Object) => void
**参数**:
_value_: {Object} 对应的日期值 (moment 对象) | Function | func.noop |
-| onModeChange | 面板模式变化时的回调
**签名**:
Function(mode: String) => void
**参数**:
_mode_: {String} 对应面板模式 date month year | Function | func.noop |
-| onVisibleMonthChange | 展现的月份变化时的回调
**签名**:
Function(value: Object, reason: String) => void
**参数**:
_value_: {Object} 显示的月份 (moment 对象)
_reason_: {String} 触发月份改变原因 | Function | func.noop |
-| dateCellRender | 自定义日期渲染函数
**签名**:
Function(value: Object) => ReactNode
**参数**:
_value_: {Object} 日期值(moment对象)
**返回值**:
{ReactNode} null
| Function | value => value.date() |
-| monthCellRender | 自定义月份渲染函数
**签名**:
Function(calendarDate: Object) => ReactNode
**参数**:
_calendarDate_: {Object} 对应 Calendar 返回的自定义日期对象
**返回值**:
{ReactNode} null
| Function | - |
-| yearRange | 年份范围,[START_YEAR, END_YEAR] \(只在shape 为 ‘card’, 'fullscreen' 下生效) | Array<Number> | - |
-| disabledDate | 不可选择的日期
**签名**:
Function(calendarDate: Object, view: String) => Boolean
**参数**:
_calendarDate_: {Object} 对应 Calendar 返回的自定义日期对象
_view_: {String} 当前视图类型,year: 年, month: 月, date: 日
**返回值**:
{Boolean} null
| Function | - |
+| 参数 | 说明 | 类型 | 默认值 | 是否必填 |
+| -------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------- | ------------------------- | -------- |
+| defaultValue | 默认选中的日期(moment 对象) | Moment \| null | - | |
+| shape | 展现形态 | 'card' \| 'fullscreen' \| 'panel' | 'fullscreen' | |
+| value | 选中的日期值 (moment 对象) | Moment \| null | - | |
+| mode | 面板模式 | CalendarMode | - | |
+| showOtherMonth | 是否展示非本月的日期 | boolean | true | |
+| defaultVisibleMonth | 默认展示的月份 | () => Moment \| null | - | |
+| onModeChange | 面板模式变化时的回调
**签名**:
**参数**:
_mode_: 对应面板模式 date, month, year | (mode: CalendarMode) => void | - | |
+| onSelect | 选择日期单元格时的回调 | (value: Moment) => void | - | |
+| onVisibleMonthChange | 展现的月份变化时的回调 | (value: Moment, reason: VisibleMonthChangeType) => void | - | |
+| dateCellRender | 自定义日期渲染函数 | (value: Moment) => React.ReactNode | value =\> value.date() | |
+| monthCellRender | 自定义月份渲染函数 | (calendarDate: Moment) => React.ReactNode | - | |
+| disabledDate | 不可选择的日期 | (calendarDate: Moment, view: CalendarMode) => boolean | - | |
+| modes | 面板可变化的模式列表,仅初始化时接收一次 | CalendarMode[] | ['date', 'month', 'year'] | |
+| format | 日期值的格式(用于日期 title 显示的格式) | string | 'YYYY-MM | |
+| yearRange | 年份范围,[START_YEAR, END_YEAR] (只在 shape 为‘card’, 'fullscreen' 下生效) | [start: number, end: number] | - | |
+
+### Calendar.RangeCalendar
+
+| 参数 | 说明 | 类型 | 默认值 | 是否必填 |
+| -------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------- | ---------------------- | -------- |
+| mode | 面板模式 | CalendarMode | 'date' | |
+| format | 日期值的格式(用于日期 title 显示的格式) | string | 'YYYY-MM | |
+| dateCellRender | 自定义日期渲染函数 | (value: Moment) => React.ReactNode | value =\> value.date() | |
+| onSelect | 选择日期单元格时的回调 | (value: Moment) => void | - | |
+| onVisibleMonthChange | 展现的月份变化时的回调 | (value: Moment, reason: VisibleMonthChangeType) => void | - | |
+| showOtherMonth | 是否展示非本月的日期 | boolean | true | |
+| startValue | 开始日期(moment 对象) | Moment \| null | - | |
+| endValue | 结束日期(moment 对象) | Moment \| null | - | |
+| defaultStartValue | 默认的开始日期(moment 对象) | Moment \| null | - | |
+| defaultEndValue | 默认的结束日期(moment 对象) | Moment \| null | - | |
+| monthCellRender | 自定义月份渲染函数 | (calendarDate: Moment) => React.ReactNode | - | |
+| defaultVisibleMonth | 默认展示的月份 | () => Moment \| null | - | |
+| disabledDate | 不可选择的日期 | (calendarDate: Moment, view: CalendarMode) => boolean | - | |
+| shape | 展现形态 | 'card' \| 'fullscreen' \| 'panel' | - | |
+| yearRange | 年份范围,[START_YEAR, END_YEAR] (只在 shape 为‘card’, 'fullscreen' 下生效) | [number, number] | - | |
+
+### CalendarMode
+
+```typescript
+export type CalendarMode = 'date' | 'month' | 'year';
+```
+
+### VisibleMonthChangeType
+
+```typescript
+export type VisibleMonthChangeType = 'cellClick' | 'buttonClick' | 'yearSelect' | 'monthSelect';
+```
diff --git a/components/calendar/__docs__/theme/index.jsx b/components/calendar/__docs__/theme/index.jsx
deleted file mode 100644
index ec801c8ccb..0000000000
--- a/components/calendar/__docs__/theme/index.jsx
+++ /dev/null
@@ -1,136 +0,0 @@
-import moment from 'moment';
-import { Demo, DemoGroup, initDemo } from '../../../demo-helper';
-import Calendar from '../../index';
-import RangeCalendar from '../../range-calendar';
-import ConfigProvider from '../../../config-provider';
-import zhCN from '../../../locale/zh-cn';
-import enUS from '../../../locale/en-us';
-import '../../../demo-helper/style';
-import '../../style';
-
-const i18nMap = {
- 'zh-cn': {
- dateFullscreenCalendar: '全屏日历',
- cardCalendar: '卡片日历',
- panelCalendar: '面板日历',
- rangeCalendar: '多面板日历',
-
- date: '日',
- month: '月',
- year: '年',
-
- normal: '普通',
- },
- 'en-us': {
- dateFullscreenCalendar: 'Fullscreen',
- cardCalendar: 'Card',
- panelCalendar: 'Panel',
- rangeCalendar: 'Range Panel',
-
- date: 'Day',
- month: 'Month',
- year: 'Year',
-
- normal: 'Normal',
- }
-};
-
-const wrappedCalendarStyle = {
- width: '320px',
- overflow: 'hidden',
-};
-
-const wrappedRangeCalendarStyle = {
- width: '600px',
- overflow: 'hidden'
-};
-
-window.renderDemo = function(lang = 'en-us') {
- moment.locale(lang);
- render(i18nMap[lang], lang);
-};
-
-/* eslint-disable */
-function render(i18n, lang) {
- const currentDate = moment();
- const calendarValue = currentDate.clone().add(1, 'days');
-
- const disabledDate = function (date) {
- return date.valueOf() > currentDate.clone().add(3, 'days').valueOf();
- };
-
- return ReactDOM.render(
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- , document.getElementById('container'));
-}
-
-renderDemo();
-
-initDemo('calendar');
diff --git a/components/calendar/__docs__/theme/index.tsx b/components/calendar/__docs__/theme/index.tsx
new file mode 100644
index 0000000000..c09bd028fe
--- /dev/null
+++ b/components/calendar/__docs__/theme/index.tsx
@@ -0,0 +1,176 @@
+import moment, { type Moment } from 'moment';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Demo, DemoGroup, initDemo } from '../../../demo-helper';
+import Calendar from '../../index';
+import RangeCalendar from '../../range-calendar';
+import ConfigProvider from '../../../config-provider';
+import zhCN from '../../../locale/zh-cn';
+import enUS from '../../../locale/en-us';
+import '../../../demo-helper/style';
+import '../../style';
+
+const i18nMap = {
+ 'zh-cn': {
+ dateFullscreenCalendar: '全屏日历',
+ cardCalendar: '卡片日历',
+ panelCalendar: '面板日历',
+ rangeCalendar: '多面板日历',
+
+ date: '日',
+ month: '月',
+ year: '年',
+
+ normal: '普通',
+ },
+ 'en-us': {
+ dateFullscreenCalendar: 'Fullscreen',
+ cardCalendar: 'Card',
+ panelCalendar: 'Panel',
+ rangeCalendar: 'Range Panel',
+
+ date: 'Day',
+ month: 'Month',
+ year: 'Year',
+
+ normal: 'Normal',
+ },
+};
+
+const wrappedCalendarStyle = {
+ width: '320px',
+ overflow: 'hidden',
+};
+
+const wrappedRangeCalendarStyle = {
+ width: '600px',
+ overflow: 'hidden',
+};
+
+function render(i18n: any, lang: string) {
+ const currentDate = moment();
+ const calendarValue = currentDate.clone().add(1, 'days');
+
+ const disabledDate = function (date: Moment) {
+ return date.valueOf() > currentDate.clone().add(3, 'days').valueOf();
+ };
+
+ ReactDOM.render(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ document.getElementById('container')
+ );
+}
+
+window.renderDemo = function (lang = 'en-us') {
+ moment.locale(lang);
+ render(i18nMap[lang], lang);
+};
+
+renderDemo();
+
+initDemo('calendar');
diff --git a/components/calendar/__tests__/a11y-spec.js b/components/calendar/__tests__/a11y-spec.js
deleted file mode 100644
index d013f4fed5..0000000000
--- a/components/calendar/__tests__/a11y-spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import React from 'react';
-import Enzyme from 'enzyme';
-import Adapter from 'enzyme-adapter-react-16';
-import Calendar from '../index';
-import '../style';
-import { afterEach as a11yAfterEach, testReact } from '../../util/__tests__/legacy/a11y/validate';
-
-Enzyme.configure({ adapter: new Adapter() });
-
-/* eslint-disable no-undef, react/jsx-filename-extension */
-describe('Calendar A11y', () => {
- let wrapper;
-
- afterEach(() => {
- if (wrapper) {
- wrapper.unmount();
- wrapper = null;
- }
- a11yAfterEach();
- });
-
- // TODO Select support a11y
- it.skip('should not have any violations when default', async () => {
- wrapper = await testReact(
);
- return wrapper;
- });
- // TODO Select support a11y
- it.skip('should not have any violations when shape', async () => {
- wrapper = await testReact(
-
-
-
-
-
- );
- return wrapper;
- });
-});
diff --git a/components/calendar/__tests__/a11y-spec.tsx b/components/calendar/__tests__/a11y-spec.tsx
new file mode 100644
index 0000000000..e65646af1f
--- /dev/null
+++ b/components/calendar/__tests__/a11y-spec.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import Calendar from '../index';
+import '../style';
+import { testReact } from '../../util/__tests__/a11y/validate';
+
+describe('Calendar A11y', () => {
+ it('should not have any violations when default', async () => {
+ await testReact(
);
+ });
+ it('should not have any violations when shape', async () => {
+ await testReact(
+
+
+
+
+
+ );
+ });
+});
diff --git a/components/calendar/__tests__/index-spec.js b/components/calendar/__tests__/index-spec.js
deleted file mode 100644
index 3289961186..0000000000
--- a/components/calendar/__tests__/index-spec.js
+++ /dev/null
@@ -1,452 +0,0 @@
-import React from 'react';
-import sinon from 'sinon';
-import Enzyme, { mount } from 'enzyme';
-import Adapter from 'enzyme-adapter-react-16';
-import assert from 'power-assert';
-import moment from 'moment';
-import Calendar from '../index';
-import RangeCalendar from '../range-calendar';
-import '../style';
-import { getLocaleData } from '../utils/index';
-
-Enzyme.configure({
- adapter: new Adapter(),
-});
-moment.locale('zh-cn');
-const defaultVal = moment('2017-10-01', 'YYYY-MM-DD', true);
-
-/* eslint-disable */
-describe('Calendar', () => {
- let wrapper;
-
- afterEach(() => {
- if (wrapper) {
- wrapper.unmount();
- wrapper = null;
- }
- });
-
- describe('render', () => {
- it('should render calendar', () => {
- wrapper = mount(
);
- assert(wrapper.find('.next-calendar.next-calendar-fullscreen').length === 1);
- });
-
- it('should render with defaultVisibleMonth', () => {
- wrapper = mount(
defaultVal} />);
- assert(wrapper.find('td[title="2017-10-01"]').length === 1);
- });
-
- it('should render with default value', () => {
- wrapper = mount( defaultVal} defaultValue={defaultVal} />);
- assert(wrapper.find('td[title="2017-10-01"]').hasClass('next-selected'));
- });
-
- it('should render calendar panel', () => {
- wrapper = mount( );
- assert(wrapper.find('.next-calendar-panel-header').length === 1);
- });
-
- it('should render calendar card', () => {
- wrapper = mount( );
- assert(wrapper.find('.next-calendar-card').length === 1);
- });
-
- it('should render uncontrolled calendar', () => {
- wrapper = mount( );
- assert(wrapper.find('td[title="2017-10-01"]').hasClass('next-selected'));
- assert(wrapper.find('td[title="2017-10-02"]').length);
- wrapper.find('td[title="2017-10-02"]').simulate('click');
- assert(wrapper.find('td[title="2017-10-02"]').hasClass('next-selected'));
- });
-
- it('should render controlled calendar', () => {
- wrapper = mount( defaultVal} />);
- assert(wrapper.find('td[title="2017-10-01"]').hasClass('next-selected'));
- wrapper.setProps({ value: defaultVal.clone().add(1, 'days') });
- assert(wrapper.find('td[title="2017-10-02"]').hasClass('next-selected'));
- });
-
- it('should render controlled calendar with mode', () => {
- wrapper = mount( );
- wrapper.setProps({ mode: 'month' });
- assert(wrapper.find('.next-calendar-cell').length === 12);
- });
-
- it('should render with disabled dates', () => {
- const disabledDate = (date, view) => {
- assert(view === 'date');
- return date.valueOf() > defaultVal.valueOf();
- };
- wrapper = mount( defaultVal} disabledDate={disabledDate} />);
- assert(wrapper.find('td[title="2017-10-02"]').hasClass('next-disabled'));
- });
-
- it('should render custom content', () => {
- const dateCellRender = date => {
- const dateNum = date.date();
- if (defaultVal.month() !== date.month()) {
- return dateNum;
- }
-
- if (dateNum === 1) {
- return hello world
;
- }
- };
- wrapper = mount( defaultVal} dateCellRender={dateCellRender} />);
- assert(wrapper.find('td[title="2017-10-01"] div.test').length === 1);
- });
-
- it('should render custom format 0.x', () => {
- const locale = {
- format: {
- months: [
- '一月',
- '二月',
- '三月',
- '四月',
- '五月',
- '六月',
- '七月',
- '八月',
- '九月',
- '十月',
- '十一月',
- '十二月',
- ],
- shortMonths: [
- '一月',
- '二月',
- '三月',
- '四月',
- '五月',
- '六月',
- '七月',
- '八月',
- '九月',
- '十月',
- '十一月',
- '十二月',
- ],
- weekdays: ['星期天', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'],
- shortWeekdays: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
- veryShortWeekdays: ['日', '一', '二', '三', '四', '五', '六'],
- ampms: ['上午', '下午'],
- },
- };
- wrapper = mount( );
-
- const localeData = getLocaleData(locale.format, moment().localeData());
- assert(localeData.monthsShort() === locale.format.shortMonths);
- assert(localeData.months() === locale.format.months);
- assert(
- localeData.firstDayOfWeek() ===
- moment()
- .localeData()
- .firstDayOfWeek()
- );
- assert(localeData.weekdays() === locale.format.weekdays);
- assert(localeData.weekdaysShort() === locale.format.shortWeekdays);
- assert(localeData.weekdaysMin() === locale.format.veryShortWeekdays);
-
- assert(
- wrapper
- .find('.next-calendar-th')
- .at(0)
- .text() ===
- locale.format.shortWeekdays[
- moment()
- .localeData()
- .firstDayOfWeek()
- ]
- );
- });
- });
-
- describe('action', () => {
- it('should change mode', () => {
- const onModeChange = sinon.spy();
-
- wrapper = mount( );
- wrapper
- .find('.next-radio-wrapper input')
- .at(1)
- .simulate('change', { target: { checked: true } });
- assert(wrapper.find('td').length === 12);
- assert(wrapper.find('td[title="1月"]').length === 1);
- assert(onModeChange.calledOnce);
- });
-
- it('should change panel mode to month', () => {
- wrapper = mount( defaultVal} />);
- wrapper
- .find('.next-calendar-btn')
- .at(2)
- .simulate('click');
- assert(wrapper.find('.next-calendar-month').length === 12);
- wrapper
- .find('.next-calendar-btn')
- .at(1)
- .simulate('click');
- assert(wrapper.find('.next-calendar-panel-header button[title="2010-2019"]').length === 1);
- });
-
- it('should change panel mode to year', () => {
- wrapper = mount( );
- wrapper
- .find('.next-calendar-btn')
- .at(3)
- .simulate('click');
- assert(wrapper.find('.next-calendar-year').length === 12);
- });
-
- it('should change visible month', () => {
- wrapper = mount( defaultVal} />);
- wrapper.find('.next-calendar-btn-prev-month').simulate('click');
- assert(wrapper.find('.next-calendar-panel-header button[title="九月"]').length === 1);
- wrapper.find('.next-calendar-btn-next-month').simulate('click');
- assert(wrapper.find('.next-calendar-panel-header button[title="十月"]').length === 1);
- });
-
- it('should change visible month by year', () => {
- wrapper = mount( defaultVal} />);
- wrapper.find('.next-calendar-btn-prev-year').simulate('click');
- wrapper.find('.next-calendar-btn-next-year').simulate('click');
- assert(
- wrapper
- .find('.next-calendar-btn')
- .at(3)
- .instance().title === '2017'
- );
- });
-
- it('should change decade', () => {
- wrapper = mount( defaultVal} />);
- wrapper.find('.next-calendar-btn-prev-decade').simulate('click');
- assert(wrapper.find('.next-calendar-panel-header button[title="2000-2009"]').length === 1);
- wrapper.find('.next-calendar-btn-next-decade').simulate('click');
- assert(wrapper.find('.next-calendar-panel-header button[title="2010-2019"]').length === 1);
- });
-
- it('should select date', () => {
- const onSelect = val => {
- assert(val.format('YYYY-MM-DD') === '2017-10-02');
- };
- wrapper = mount( defaultVal} onSelect={onSelect} />);
- wrapper.find('td[title="2017-10-02"]').simulate('click');
- });
-
- it('should hide cell for other month', () => {
- let isClicked = false;
- const onSelect = val => {
- // handle click from this month
- assert(val.format('YYYY-MM-DD') === '2017-10-02');
- isClicked = true;
- };
- wrapper = mount(
- defaultVal} onSelect={onSelect} />
- );
-
- // hide cell for other month
- assert(wrapper.find('.next-calendar-cell-next-month[title="2017-11-01"]').text() === '');
- wrapper.find('td[title="2017-10-02"]').simulate('click');
- assert(isClicked === true);
- });
-
- it('should block click event from other month', () => {
- let isClicked = false;
- wrapper = mount(
- defaultVal}
- onSelect={() => {
- isClicked = true;
- }}
- />
- );
-
- wrapper.find('.next-calendar-cell-next-month[title="2017-11-01"]').simulate('click');
- assert(isClicked === false);
- });
- });
-});
-
-describe('RangeCalendar', () => {
- let wrapper;
-
- afterEach(() => {
- if (wrapper) {
- wrapper.unmount();
- }
- wrapper = null;
- });
-
- describe('render', () => {
- it('should render RangeCalendar', () => {
- wrapper = mount( );
- assert(wrapper.find('.next-calendar-table').length === 2);
- });
-
- it('should render with defaultStartValue & defaultEndValue', () => {
- wrapper = mount(
- defaultVal}
- defaultStartValue={defaultVal}
- defaultEndValue={defaultVal.clone().add(1, 'months')}
- />
- );
- assert(wrapper.find('td[title="2017-10-01"]').hasClass('next-selected'));
- assert(wrapper.find('td[title="2017-10-15"]').hasClass('next-inrange'));
- assert(
- wrapper
- .find('td[title="2017-11-01"]')
- .at(1)
- .hasClass('next-selected')
- );
- });
-
- it('should render with controlled value', () => {
- wrapper = mount(
- defaultVal}
- startValue={defaultVal}
- endValue={defaultVal.clone().add(1, 'months')}
- />
- );
- wrapper.setProps({
- startValue: defaultVal.clone().add(2, 'days'),
- endValue: defaultVal.clone().add(1, 'months'),
- });
- assert(wrapper.find('td[title="2017-10-03"]').hasClass('next-selected'));
- assert(wrapper.find('td[title="2017-10-15"]').hasClass('next-inrange'));
- assert(
- wrapper
- .find('td[title="2017-11-01"]')
- .at(1)
- .hasClass('next-selected')
- );
- });
-
- it('should render custom format 0.x', () => {
- const locale = {
- format: {
- months: [
- '一月',
- '二月',
- '三月',
- '四月',
- '五月',
- '六月',
- '七月',
- '八月',
- '九月',
- '十月',
- '十一月',
- '十二月',
- ],
- shortMonths: [
- '一月',
- '二月',
- '三月',
- '四月',
- '五月',
- '六月',
- '七月',
- '八月',
- '九月',
- '十月',
- '十一月',
- '十二月',
- ],
- weekdays: ['星期天', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'],
- shortWeekdays: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
- veryShortWeekdays: ['日', '一', '二', '三', '四', '五', '六'],
- ampms: ['上午', '下午'],
- },
- };
- wrapper = mount( );
-
- assert(
- wrapper
- .find('.next-calendar-th')
- .at(0)
- .text() ===
- locale.format.shortWeekdays[
- moment()
- .localeData()
- .firstDayOfWeek()
- ]
- );
- });
- });
-
- describe('action', () => {
- it('should change to month mode in panel', () => {
- wrapper = mount(
- defaultVal}
- defaultStartValue={defaultVal}
- defaultEndValue={defaultVal.clone().add(1, 'months')}
- />
- );
- wrapper
- .find('.next-calendar-btn')
- .at(2)
- .simulate('click');
- assert(wrapper.find('td[title="10月"]').hasClass('next-selected'));
- wrapper.find('td[title="10月"]').simulate('click');
- wrapper
- .find('.next-calendar-btn')
- .at(4)
- .simulate('click');
- assert(wrapper.find('td[title="10月"]').hasClass('next-selected'));
- });
-
- it('should change to year mode in panel', () => {
- wrapper = mount(
- defaultVal}
- defaultStartValue={defaultVal}
- defaultEndValue={defaultVal.clone().add(1, 'months')}
- />
- );
- wrapper
- .find('.next-calendar-btn')
- .at(3)
- .simulate('click');
- assert(wrapper.find('.next-calendar-panel-header button[title="2010-2019"]').length === 1);
- });
-
- it('should change visible month', () => {
- wrapper = mount( defaultVal} />);
- wrapper.find('.next-calendar-btn-prev-month').simulate('click');
- assert(wrapper.find('.next-calendar-panel-header button[title="九月"]').length === 1);
- wrapper.find('.next-calendar-btn-next-month').simulate('click');
- assert(wrapper.find('.next-calendar-panel-header button[title="十月"]').length === 1);
- });
-
- it('should change visible month by year', () => {
- wrapper = mount( defaultVal} />);
- wrapper.find('.next-calendar-btn-prev-year').simulate('click');
- wrapper.find('.next-calendar-btn-next-year').simulate('click');
- assert(
- wrapper
- .find('.next-calendar-btn')
- .at(3)
- .instance().title === '2017'
- );
- });
-
- it('should change decade', () => {
- wrapper = mount( defaultVal} />);
- wrapper
- .find('.next-calendar-btn')
- .at(3)
- .simulate('click');
- wrapper.find('.next-calendar-btn-prev-decade').simulate('click');
- assert(wrapper.find('.next-calendar-panel-header button[title="2000-2009"]').length === 1);
- wrapper.find('.next-calendar-btn-next-decade').simulate('click');
- assert(wrapper.find('.next-calendar-panel-header button[title="2010-2019"]').length === 1);
- });
- });
-});
diff --git a/components/calendar/__tests__/index-spec.tsx b/components/calendar/__tests__/index-spec.tsx
new file mode 100644
index 0000000000..c5624d51b3
--- /dev/null
+++ b/components/calendar/__tests__/index-spec.tsx
@@ -0,0 +1,412 @@
+import React from 'react';
+import moment from 'moment';
+import 'moment/locale/zh-cn';
+import Calendar, { type CalendarProps } from '../index';
+import RangeCalendar from '../range-calendar';
+import '../style';
+import { getLocaleData } from '../utils/index';
+
+moment.locale('zh-cn');
+const defaultVal = moment('2017-10-01', 'YYYY-MM-DD', true);
+
+describe('Calendar', () => {
+ describe('render', () => {
+ it('should render calendar', () => {
+ cy.mount( );
+ cy.get('.next-calendar.next-calendar-fullscreen').should('have.length', 1);
+ });
+
+ it('should render with defaultVisibleMonth', () => {
+ cy.mount( defaultVal} />);
+ cy.get('td[title="2017-10-01"]').should('have.length', 1);
+ });
+
+ it('should render with default value', () => {
+ cy.mount( defaultVal} defaultValue={defaultVal} />);
+ cy.get('td[title="2017-10-01"]').should('have.class', 'next-selected');
+ });
+
+ it('should render calendar panel', () => {
+ cy.mount( );
+ cy.get('.next-calendar-panel-header').should('have.length', 1);
+ });
+
+ it('should render calendar card', () => {
+ cy.mount( );
+ cy.get('.next-calendar-card').should('have.length', 1);
+ });
+
+ it('should render uncontrolled calendar', () => {
+ cy.mount( );
+ cy.get('td[title="2017-10-01"]').should('have.class', 'next-selected');
+ cy.get('td[title="2017-10-02"]').should('exist');
+ cy.get('td[title="2017-10-02"]').click();
+ cy.get('td[title="2017-10-02"]').should('have.class', 'next-selected');
+ });
+
+ it('should render controlled calendar', () => {
+ cy.mount( defaultVal} />).as(
+ 'Demo'
+ );
+ cy.get('td[title="2017-10-01"]').should('have.class', 'next-selected');
+ cy.rerender('Demo', { value: defaultVal.clone().add(1, 'days') });
+ cy.get('td[title="2017-10-02"]').should('have.class', 'next-selected');
+ });
+
+ it('should render controlled calendar with mode', () => {
+ cy.mount( ).as('Demo');
+ cy.rerender('Demo', { mode: 'month' });
+ cy.get('.next-calendar-cell').should('have.length', 12);
+ });
+
+ it('should render with disabled dates', () => {
+ const disabledDateHandler = cy.spy().as('disabledDateHandler');
+ const disabledDate: CalendarProps['disabledDate'] = (date, view) => {
+ disabledDateHandler(view);
+ return date.valueOf() > defaultVal.valueOf();
+ };
+ cy.mount(
+ defaultVal} disabledDate={disabledDate} />
+ );
+ cy.get('td[title="2017-10-02"]').should('have.class', 'next-disabled');
+ cy.get('@disabledDateHandler').should('be.calledWith', 'date');
+ });
+
+ it('should render custom content', () => {
+ const dateCellRender: CalendarProps['dateCellRender'] = date => {
+ const dateNum = date.date();
+ if (defaultVal.month() !== date.month()) {
+ return dateNum;
+ }
+
+ if (dateNum === 1) {
+ return hello world
;
+ }
+ };
+ cy.mount(
+ defaultVal} dateCellRender={dateCellRender} />
+ );
+ cy.get('td[title="2017-10-01"] div.test').should('have.length', 1);
+ });
+
+ it('should render custom format 0.x', () => {
+ const locale = {
+ format: {
+ months: [
+ '一月',
+ '二月',
+ '三月',
+ '四月',
+ '五月',
+ '六月',
+ '七月',
+ '八月',
+ '九月',
+ '十月',
+ '十一月',
+ '十二月',
+ ],
+ shortMonths: [
+ '一月',
+ '二月',
+ '三月',
+ '四月',
+ '五月',
+ '六月',
+ '七月',
+ '八月',
+ '九月',
+ '十月',
+ '十一月',
+ '十二月',
+ ],
+ weekdays: [
+ '星期天',
+ '星期一',
+ '星期二',
+ '星期三',
+ '星期四',
+ '星期五',
+ '星期六',
+ ],
+ shortWeekdays: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
+ veryShortWeekdays: ['日', '一', '二', '三', '四', '五', '六'],
+ ampms: ['上午', '下午'],
+ },
+ };
+ cy.mount( );
+
+ const localeData = getLocaleData(locale.format, moment().localeData());
+ cy.wrap(localeData.monthsShort()).should('equal', locale.format.shortMonths);
+ cy.wrap(localeData.months()).should('equal', locale.format.months);
+ cy.wrap(localeData.firstDayOfWeek()).should(
+ 'equal',
+ moment().localeData().firstDayOfWeek()
+ );
+ cy.wrap(localeData.weekdays()).should('equal', locale.format.weekdays);
+ cy.wrap(localeData.weekdaysShort()).should('equal', locale.format.shortWeekdays);
+ cy.wrap(localeData.weekdaysMin()).should('equal', locale.format.veryShortWeekdays);
+ cy.get('.next-calendar-th')
+ .eq(0)
+ .should(
+ 'have.text',
+ locale.format.shortWeekdays[moment().localeData().firstDayOfWeek()]
+ );
+ });
+ });
+
+ describe('action', () => {
+ it('should change mode', () => {
+ const onModeChange = cy.spy().as('onModeChange');
+ cy.mount( );
+ cy.get('.next-radio-wrapper input').eq(1).check({ force: true });
+ cy.get('td').should('have.length', 12);
+ cy.get('td[title="1月"]').should('have.length', 1);
+ cy.get('@onModeChange').should('be.calledOnce');
+ });
+
+ it('should change panel mode to month', () => {
+ cy.mount( defaultVal} />);
+ cy.get('.next-calendar-btn').eq(2).click();
+ cy.get('.next-calendar-month').should('have.length', 12);
+ cy.get('.next-calendar-btn').eq(1).click();
+ cy.get('.next-calendar-panel-header button[title="2010-2019"]').should(
+ 'have.length',
+ 1
+ );
+ });
+
+ it('should change panel mode to year', () => {
+ cy.mount( );
+ cy.get('.next-calendar-btn').eq(3).click();
+ cy.get('.next-calendar-year').should('have.length', 12);
+ });
+
+ it('should change visible month', () => {
+ cy.mount( defaultVal} />);
+ cy.get('.next-calendar-btn-prev-month').click();
+ cy.get('.next-calendar-panel-header button[title="九月"]').should('have.length', 1);
+ cy.get('.next-calendar-btn-next-month').click();
+ cy.get('.next-calendar-panel-header button[title="十月"]').should('have.length', 1);
+ });
+
+ it('should change visible month by year', () => {
+ cy.mount( defaultVal} />);
+ cy.get('.next-calendar-btn-prev-year').click();
+ cy.get('.next-calendar-btn').eq(3).should('have.attr', 'title', '2016');
+ cy.get('.next-calendar-btn-next-year').click();
+ cy.get('.next-calendar-btn').eq(3).should('have.attr', 'title', '2017');
+ });
+
+ it('should change decade', () => {
+ cy.mount( defaultVal} />);
+ cy.get('.next-calendar-btn-prev-decade').click();
+ cy.get('.next-calendar-panel-header button[title="2000-2009"]').should(
+ 'have.length',
+ 1
+ );
+ cy.get('.next-calendar-btn-next-decade').click();
+ cy.get('.next-calendar-panel-header button[title="2010-2019"]').should(
+ 'have.length',
+ 1
+ );
+ });
+
+ it('should select date', () => {
+ const onSelectHandler = cy.spy().as('onSelectHandler');
+ const onSelect: CalendarProps['onSelect'] = val => {
+ onSelectHandler(val.format('YYYY-MM-DD'));
+ };
+ cy.mount(
+ defaultVal}
+ onSelect={onSelect}
+ />
+ );
+ cy.get('td[title="2017-10-02"]').click();
+ cy.get('@onSelectHandler').should('be.calledWith', '2017-10-02');
+ });
+
+ it('should hide cell for other month', () => {
+ cy.mount( defaultVal} />);
+ cy.get('.next-calendar-cell-next-month[title="2017-11-01"]').should('have.text', '');
+ });
+
+ it('should block click event from other month', () => {
+ cy.mount(
+ defaultVal}
+ onSelect={cy.spy().as('onSelect')}
+ />
+ );
+
+ cy.get('.next-calendar-cell-next-month[title="2017-11-01"]').click();
+ cy.get('@onSelect').should('not.be.called');
+ });
+ });
+});
+
+describe('RangeCalendar', () => {
+ describe('render', () => {
+ it('should render RangeCalendar', () => {
+ cy.mount( );
+ cy.get('.next-calendar-table').should('have.length', 2);
+ // assert(wrapper.find('.next-calendar-table').length === 2);
+ });
+
+ it('should render with defaultStartValue & defaultEndValue', () => {
+ cy.mount(
+ defaultVal}
+ defaultStartValue={defaultVal}
+ defaultEndValue={defaultVal.clone().add(1, 'months')}
+ />
+ );
+ cy.get('td[title="2017-10-01"]').should('have.class', 'next-selected');
+ cy.get('td[title="2017-10-15"]').should('have.class', 'next-inrange');
+ cy.get('td[title="2017-11-01"]').should('have.class', 'next-selected');
+ });
+
+ it('should render with controlled value', () => {
+ cy.mount(
+ defaultVal}
+ startValue={defaultVal}
+ endValue={defaultVal.clone().add(1, 'months')}
+ />
+ ).as('Demo');
+ cy.rerender('Demo', {
+ startValue: defaultVal.clone().add(2, 'days'),
+ endValue: defaultVal.clone().add(1, 'months'),
+ });
+ cy.get('td[title="2017-10-03"]').should('have.class', 'next-selected');
+ cy.get('td[title="2017-10-15"]').should('have.class', 'next-inrange');
+ cy.get('td[title="2017-11-01"]').should('have.class', 'next-selected');
+ });
+
+ it('should render custom format 0.x', () => {
+ const locale = {
+ format: {
+ months: [
+ '一月',
+ '二月',
+ '三月',
+ '四月',
+ '五月',
+ '六月',
+ '七月',
+ '八月',
+ '九月',
+ '十月',
+ '十一月',
+ '十二月',
+ ],
+ shortMonths: [
+ '一月',
+ '二月',
+ '三月',
+ '四月',
+ '五月',
+ '六月',
+ '七月',
+ '八月',
+ '九月',
+ '十月',
+ '十一月',
+ '十二月',
+ ],
+ weekdays: [
+ '星期天',
+ '星期一',
+ '星期二',
+ '星期三',
+ '星期四',
+ '星期五',
+ '星期六',
+ ],
+ shortWeekdays: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
+ veryShortWeekdays: ['日', '一', '二', '三', '四', '五', '六'],
+ ampms: ['上午', '下午'],
+ },
+ };
+ cy.mount( );
+ cy.get('.next-calendar-th')
+ .eq(0)
+ .should(
+ 'have.text',
+ locale.format.shortWeekdays[moment().localeData().firstDayOfWeek()]
+ );
+ });
+ });
+
+ describe('action', () => {
+ it('should change to month mode in panel', () => {
+ cy.mount(
+ defaultVal}
+ defaultStartValue={defaultVal}
+ defaultEndValue={defaultVal.clone().add(1, 'months')}
+ />
+ );
+ cy.get('.next-calendar-btn').eq(2).click();
+ cy.get('td[title="10月"]').should('have.class', 'next-selected');
+ cy.get('td[title="10月').click();
+ cy.get('.next-calendar-btn').eq(4).click();
+ cy.get('td[title="10月"]').should('have.class', 'next-selected');
+ });
+
+ it('should change to year mode in panel', () => {
+ cy.mount(
+ defaultVal}
+ defaultStartValue={defaultVal}
+ defaultEndValue={defaultVal.clone().add(1, 'months')}
+ />
+ );
+ cy.get('.next-calendar-btn').eq(3).click();
+ cy.get('.next-calendar-panel-header button[title="2010-2019"]').should(
+ 'have.length',
+ 1
+ );
+ });
+
+ it('should change visible month', () => {
+ cy.mount( defaultVal} />);
+ cy.get('.next-calendar-btn-prev-month').click();
+ cy.get('.next-calendar-panel-header button[title="九月"]').should('have.length', 1);
+ cy.get('.next-calendar-btn-next-month').click();
+ cy.get('.next-calendar-panel-header button[title="十月"]').should('have.length', 1);
+ });
+
+ it('should change visible month by year', () => {
+ cy.mount( defaultVal} />);
+ cy.get('.next-calendar-btn-prev-year').click();
+ cy.get('.next-calendar-btn').eq(3).should('have.attr', 'title', '2016');
+ cy.get('.next-calendar-btn-next-year').click();
+ cy.get('.next-calendar-btn').eq(3).should('have.attr', 'title', '2017');
+ });
+
+ it('should change decade', () => {
+ cy.mount(
+ defaultVal}
+ />
+ );
+ cy.get('.next-calendar-btn').eq(3).click();
+ cy.get('.next-calendar-btn-prev-decade').click();
+ cy.get('.next-calendar-panel-header button[title="2000-2009"]').should(
+ 'have.length',
+ 1
+ );
+ cy.get('.next-calendar-btn-next-decade').click();
+ cy.get('.next-calendar-panel-header button[title="2010-2019"]').should(
+ 'have.length',
+ 1
+ );
+ });
+ });
+});
diff --git a/components/calendar/__tests__/issue-spec.tsx b/components/calendar/__tests__/issue-spec.tsx
new file mode 100644
index 0000000000..83ec6b448f
--- /dev/null
+++ b/components/calendar/__tests__/issue-spec.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import moment from 'moment';
+import Calendar from '../index';
+import '../style';
+
+moment.locale('zh-cn');
+const defaultVal = moment('2024-05-13', 'YYYY-MM-DD', true);
+
+describe('Calendar issues', () => {
+ // Fix: https://github.com/alibaba-fusion/next/issues/4782
+ describe('should fix #4782', () => {
+ it('should not have switch button when shape is panel and showOtherMonth is false', () => {
+ cy.mount( );
+ cy.get('.next-calendar-panel-header > button').should('have.length', 0);
+ });
+ it('should not have mode switch button when shape is fullscreen and showOtherMonth is false', () => {
+ cy.mount( );
+ cy.get('.next-radio-group').should('have.length', 0);
+ });
+
+ describe('action', () => {
+ it('should not change mode to month when showOtherMonth is false and shape is panel', () => {
+ cy.mount( );
+ cy.get('.next-calendar-btn').eq(0).click();
+ cy.get('.next-calendar-month').should('have.length', 0);
+ });
+
+ it('should not change mode to year when showOtherMonth is false and shape is panel', () => {
+ cy.mount( );
+ cy.get('.next-calendar-btn').eq(1).click();
+ cy.get('.next-calendar-year').should('have.length', 0);
+ });
+
+ it('should not change year when showOtherMonth is false and shape is fullscreen', () => {
+ cy.mount(
+
+ );
+ cy.get('.next-select').eq(0).click();
+ cy.get('.next-menu-item').should('have.length', 0);
+ });
+ it('should not change month when showOtherMonth is false and shape is card', () => {
+ cy.mount(
+
+ );
+ cy.get('.next-select').eq(1).click();
+ cy.get('.next-menu-item').should('have.length', 0);
+ });
+ });
+ });
+});
diff --git a/components/calendar/calendar.jsx b/components/calendar/calendar.jsx
deleted file mode 100644
index ee4c055043..0000000000
--- a/components/calendar/calendar.jsx
+++ /dev/null
@@ -1,351 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import { polyfill } from 'react-lifecycles-compat';
-import moment from 'moment';
-import classnames from 'classnames';
-import ConfigProvider from '../config-provider';
-import nextLocale from '../locale/zh-cn';
-import { func, obj } from '../util';
-import CardHeader from './head/card-header';
-import DatePanelHeader from './head/date-panel-header';
-import MonthPanelHeader from './head/month-panel-header';
-import YearPanelHeader from './head/year-panel-header';
-import DateTable from './table/date-table';
-import MonthTable from './table/month-table';
-import YearTable from './table/year-table';
-import {
- checkMomentObj,
- formatDateValue,
- getVisibleMonth,
- isSameYearMonth,
- CALENDAR_MODES,
- CALENDAR_MODE_DATE,
- CALENDAR_MODE_MONTH,
- CALENDAR_MODE_YEAR,
- getLocaleData,
-} from './utils';
-
-const isValueChanged = (value, oldVlaue) => {
- if (value && oldVlaue) {
- if (!moment.isMoment(value)) {
- value = moment(value);
- }
- if (!moment.isMoment(oldVlaue)) {
- oldVlaue = moment(oldVlaue);
- }
- return value.valueOf() !== oldVlaue.valueOf();
- } else {
- return value !== oldVlaue;
- }
-};
-
-/** Calendar */
-class Calendar extends Component {
- static propTypes = {
- ...ConfigProvider.propTypes,
- prefix: PropTypes.string,
- rtl: PropTypes.bool,
- /**
- * 默认选中的日期(moment 对象)
- */
- defaultValue: checkMomentObj,
- /**
- * 选中的日期值 (moment 对象)
- */
- value: checkMomentObj,
- /**
- * 面板模式
- */
- mode: PropTypes.oneOf(CALENDAR_MODES), // 生成 API 文档需要手动改回 ['date', 'month', 'year']
- // 面板可变化的模式列表,仅初始化时接收一次
- modes: PropTypes.array,
- // 禁用更改面板模式,采用 dropdown 的方式切换显示日期 (暂不正式对外透出)
- disableChangeMode: PropTypes.bool,
- // 日期值的格式(用于日期title显示的格式)
- format: PropTypes.string,
- /**
- * 是否展示非本月的日期
- */
- showOtherMonth: PropTypes.bool,
- /**
- * 默认展示的月份
- */
- defaultVisibleMonth: PropTypes.func,
- /**
- * 展现形态
- */
- shape: PropTypes.oneOf(['card', 'fullscreen', 'panel']),
- /**
- * 选择日期单元格时的回调
- * @param {Object} value 对应的日期值 (moment 对象)
- */
- onSelect: PropTypes.func,
- /**
- * 面板模式变化时的回调
- * @param {String} mode 对应面板模式 date month year
- */
- onModeChange: PropTypes.func,
- /**
- * 展现的月份变化时的回调
- * @param {Object} value 显示的月份 (moment 对象)
- * @param {String} reason 触发月份改变原因
- */
- onVisibleMonthChange: PropTypes.func,
- /**
- * 自定义样式类
- */
- className: PropTypes.string,
- /**
- * 自定义日期渲染函数
- * @param {Object} value 日期值(moment对象)
- * @returns {ReactNode}
- */
- dateCellRender: PropTypes.func,
- /**
- * 自定义月份渲染函数
- * @param {Object} calendarDate 对应 Calendar 返回的自定义日期对象
- * @returns {ReactNode}
- */
- monthCellRender: PropTypes.func,
- yearCellRender: PropTypes.func, // 兼容 0.x yearCellRender
- /**
- * 年份范围,[START_YEAR, END_YEAR] (只在shape 为 ‘card’, 'fullscreen' 下生效)
- */
- yearRange: PropTypes.arrayOf(PropTypes.number),
- /**
- * 不可选择的日期
- * @param {Object} calendarDate 对应 Calendar 返回的自定义日期对象
- * @param {String} view 当前视图类型,year: 年, month: 月, date: 日
- * @returns {Boolean}
- */
- disabledDate: PropTypes.func,
- /**
- * 国际化配置
- */
- locale: PropTypes.object,
- };
-
- static defaultProps = {
- prefix: 'next-',
- rtl: false,
- shape: 'fullscreen',
- modes: CALENDAR_MODES,
- disableChangeMode: false,
- format: 'YYYY-MM-DD',
- onSelect: func.noop,
- onVisibleMonthChange: func.noop,
- onModeChange: func.noop,
- dateCellRender: value => value.date(),
- locale: nextLocale.Calendar,
- showOtherMonth: true,
- };
-
- constructor(props, context) {
- super(props, context);
- const value = formatDateValue(props.value || props.defaultValue);
- const visibleMonth = getVisibleMonth(props.defaultVisibleMonth, value);
-
- this.MODES = props.modes;
- this.today = moment();
- this.state = {
- value,
- mode: props.mode || this.MODES[0],
- MODES: this.MODES,
- visibleMonth,
- };
- }
-
- static getDerivedStateFromProps(props, state) {
- const st = {};
- if ('value' in props) {
- const value = formatDateValue(props.value);
- if (value && isValueChanged(props.value, state.value)) {
- st.visibleMonth = value;
- }
- st.value = value;
- }
-
- if (props.mode && state.MODES.indexOf(props.mode) > -1) {
- st.mode = props.mode;
- }
-
- return st;
- }
-
- onSelectCell = (date, nextMode) => {
- const { visibleMonth } = this.state;
- const { shape, showOtherMonth } = this.props;
-
- // 点击其他月份日期不生效
- if (!showOtherMonth && !isSameYearMonth(visibleMonth, date)) {
- return;
- }
-
- this.changeVisibleMonth(date, 'cellClick');
-
- if (!('value' in this.props)) {
- // 非受控模式,直接修改当前state
- this.setState({
- value: date,
- });
- }
-
- // 当用户所在的面板为初始化面板时,则选择动作为触发 onSelect 回调
- if (this.state.mode === this.MODES[0]) {
- this.props.onSelect(date);
- }
-
- if (shape === 'panel') {
- this.changeMode(nextMode);
- }
- };
-
- changeMode = nextMode => {
- if (nextMode && this.MODES.indexOf(nextMode) > -1 && nextMode !== this.state.mode) {
- this.setState({ mode: nextMode });
- this.props.onModeChange(nextMode);
- }
- };
-
- changeVisibleMonth = (date, reason) => {
- if (!isSameYearMonth(date, this.state.visibleMonth)) {
- this.setState({ visibleMonth: date });
- this.props.onVisibleMonthChange(date, reason);
- }
- };
-
- /**
- * 根据日期偏移量设置当前展示的月份
- * @param {Number} offset 日期偏移的数量
- * @param {String} type 日期偏移的类型 days, months, years
- */
- changeVisibleMonthByOffset(offset, type) {
- const cloneValue = this.state.visibleMonth.clone();
- cloneValue.add(offset, type);
- this.changeVisibleMonth(cloneValue, 'buttonClick');
- }
-
- goPrevDecade = () => {
- this.changeVisibleMonthByOffset(-10, 'years');
- };
-
- goNextDecade = () => {
- this.changeVisibleMonthByOffset(10, 'years');
- };
-
- goPrevYear = () => {
- this.changeVisibleMonthByOffset(-1, 'years');
- };
-
- goNextYear = () => {
- this.changeVisibleMonthByOffset(1, 'years');
- };
-
- goPrevMonth = () => {
- this.changeVisibleMonthByOffset(-1, 'months');
- };
-
- goNextMonth = () => {
- this.changeVisibleMonthByOffset(1, 'months');
- };
-
- render() {
- const {
- prefix,
- rtl,
- className,
- shape,
- showOtherMonth,
- format,
- locale,
- dateCellRender,
- monthCellRender,
- yearCellRender,
- disabledDate,
- yearRange,
- disableChangeMode,
- ...others
- } = this.props;
- const state = this.state;
-
- const classNames = classnames(
- {
- [`${prefix}calendar`]: true,
- [`${prefix}calendar-${shape}`]: shape,
- },
- className
- );
-
- if (rtl) {
- others.dir = 'rtl';
- }
-
- const visibleMonth = state.visibleMonth;
-
- // reset moment locale
- if (locale.momentLocale) {
- state.value && state.value.locale(locale.momentLocale);
- visibleMonth.locale(locale.momentLocale);
- }
-
- const localeData = getLocaleData(locale.format || {}, visibleMonth.localeData());
-
- const headerProps = {
- prefix,
- value: state.value,
- mode: state.mode,
- disableChangeMode,
- yearRange,
- locale,
- rtl,
- visibleMonth,
- momentLocale: localeData,
- changeMode: this.changeMode,
- changeVisibleMonth: this.changeVisibleMonth,
- goNextDecade: this.goNextDecade,
- goNextYear: this.goNextYear,
- goNextMonth: this.goNextMonth,
- goPrevDecade: this.goPrevDecade,
- goPrevYear: this.goPrevYear,
- goPrevMonth: this.goPrevMonth,
- };
-
- const tableProps = {
- prefix,
- visibleMonth,
- showOtherMonth,
- value: state.value,
- mode: state.mode,
- locale,
- dateCellRender,
- monthCellRender,
- yearCellRender,
- disabledDate,
- momentLocale: localeData,
- today: this.today,
- goPrevDecade: this.goPrevDecade,
- goNextDecade: this.goNextDecade,
- };
-
- const tables = {
- [CALENDAR_MODE_DATE]: ,
- [CALENDAR_MODE_MONTH]: ,
- [CALENDAR_MODE_YEAR]: ,
- };
-
- const panelHeaders = {
- [CALENDAR_MODE_DATE]: ,
- [CALENDAR_MODE_MONTH]: ,
- [CALENDAR_MODE_YEAR]: ,
- };
-
- return (
-
- {shape === 'panel' ? panelHeaders[state.mode] : }
- {tables[state.mode]}
-
- );
- }
-}
-
-export default polyfill(Calendar);
diff --git a/components/calendar/calendar.tsx b/components/calendar/calendar.tsx
new file mode 100644
index 0000000000..e55b78cea8
--- /dev/null
+++ b/components/calendar/calendar.tsx
@@ -0,0 +1,311 @@
+import React, { Component, type MouseEvent } from 'react';
+import PropTypes from 'prop-types';
+import { polyfill } from 'react-lifecycles-compat';
+import moment, { type MomentInput, type Moment } from 'moment';
+import classnames from 'classnames';
+import ConfigProvider from '../config-provider';
+import nextLocale from '../locale/zh-cn';
+import { type ClassPropsWithDefault, func, obj } from '../util';
+import CardHeader from './head/card-header';
+import DatePanelHeader from './head/date-panel-header';
+import MonthPanelHeader from './head/month-panel-header';
+import YearPanelHeader from './head/year-panel-header';
+import DateTable from './table/date-table';
+import MonthTable from './table/month-table';
+import YearTable from './table/year-table';
+import {
+ checkMomentObj,
+ formatDateValue,
+ getVisibleMonth,
+ isSameYearMonth,
+ CALENDAR_MODES,
+ CALENDAR_MODE_DATE,
+ CALENDAR_MODE_MONTH,
+ CALENDAR_MODE_YEAR,
+ getLocaleData,
+} from './utils';
+import type { CalendarMode, CalendarProps, CalendarState, VisibleMonthChangeType } from './types';
+
+const isValueChanged = (value: MomentInput, oldValue: MomentInput) => {
+ if (value && oldValue) {
+ if (!moment.isMoment(value)) {
+ value = moment(value);
+ }
+ if (!moment.isMoment(oldValue)) {
+ oldValue = moment(oldValue);
+ }
+ return value.valueOf() !== oldValue.valueOf();
+ } else {
+ return value !== oldValue;
+ }
+};
+
+type InnerCalendarProps = ClassPropsWithDefault;
+
+/** Calendar */
+class Calendar extends Component {
+ static propTypes = {
+ ...ConfigProvider.propTypes,
+ prefix: PropTypes.string,
+ rtl: PropTypes.bool,
+ defaultValue: checkMomentObj,
+ value: checkMomentObj,
+ mode: PropTypes.oneOf(CALENDAR_MODES),
+ modes: PropTypes.array,
+ disableChangeMode: PropTypes.bool,
+ format: PropTypes.string,
+ showOtherMonth: PropTypes.bool,
+ defaultVisibleMonth: PropTypes.func,
+ shape: PropTypes.oneOf(['card', 'fullscreen', 'panel']),
+ onSelect: PropTypes.func,
+ onModeChange: PropTypes.func,
+ onVisibleMonthChange: PropTypes.func,
+ className: PropTypes.string,
+ dateCellRender: PropTypes.func,
+ monthCellRender: PropTypes.func,
+ yearCellRender: PropTypes.func, // 兼容 0.x yearCellRender
+ yearRange: PropTypes.arrayOf(PropTypes.number),
+ disabledDate: PropTypes.func,
+ locale: PropTypes.object,
+ onChange: PropTypes.func,
+ };
+
+ static defaultProps: CalendarProps = {
+ prefix: 'next-',
+ rtl: false,
+ shape: 'fullscreen',
+ modes: CALENDAR_MODES,
+ disableChangeMode: false,
+ format: 'YYYY-MM-DD',
+ onSelect: func.noop,
+ onVisibleMonthChange: func.noop,
+ onModeChange: func.noop,
+ dateCellRender: value => value.date(),
+ locale: nextLocale.Calendar,
+ showOtherMonth: true,
+ };
+ static displayName = 'Calendar';
+ MODES: CalendarMode[];
+ today: Moment;
+
+ readonly props: InnerCalendarProps;
+
+ constructor(props: CalendarProps) {
+ super(props);
+ const value = formatDateValue(props.value || props.defaultValue);
+ const visibleMonth = getVisibleMonth(props.defaultVisibleMonth, value);
+
+ this.MODES = props.modes!;
+ this.today = moment();
+ this.state = {
+ value,
+ mode: props.mode || this.MODES![0],
+ MODES: this.MODES,
+ visibleMonth,
+ };
+ }
+
+ static getDerivedStateFromProps(props: CalendarProps, state: CalendarState) {
+ const st: Partial = {};
+ if ('value' in props) {
+ const value = formatDateValue(props.value);
+ if (value && isValueChanged(props.value, state.value)) {
+ st.visibleMonth = value;
+ }
+ st.value = value;
+ }
+
+ if (props.mode && state.MODES.indexOf(props.mode) > -1) {
+ st.mode = props.mode;
+ }
+
+ return st;
+ }
+
+ onSelectCell = (date: Moment, nextMode: CalendarMode | MouseEvent) => {
+ const { visibleMonth } = this.state;
+ const { shape, showOtherMonth } = this.props;
+
+ // 点击其他月份日期不生效
+ if (!showOtherMonth && !isSameYearMonth(visibleMonth, date)) {
+ return;
+ }
+
+ this.changeVisibleMonth(date, 'cellClick');
+
+ if (!('value' in this.props)) {
+ // 非受控模式,直接修改当前 state
+ this.setState({
+ value: date,
+ });
+ }
+
+ // 当用户所在的面板为初始化面板时,则选择动作为触发 onSelect 回调
+ if (this.state.mode === this.MODES[0]) {
+ this.props.onSelect(date);
+ }
+
+ if (shape === 'panel') {
+ this.changeMode(nextMode as CalendarMode);
+ }
+ };
+
+ changeMode = (nextMode: CalendarMode) => {
+ if (nextMode && this.MODES.indexOf(nextMode) > -1 && nextMode !== this.state.mode) {
+ this.setState({ mode: nextMode });
+ this.props.onModeChange(nextMode);
+ }
+ };
+
+ changeVisibleMonth = (date: Moment, reason: VisibleMonthChangeType) => {
+ if (!isSameYearMonth(date, this.state.visibleMonth)) {
+ this.setState({ visibleMonth: date });
+ this.props.onVisibleMonthChange(date, reason);
+ }
+ };
+
+ /**
+ * 根据日期偏移量设置当前展示的月份
+ * @param offset - 日期偏移的数量
+ * @param type - 日期偏移的类型 days, months, years
+ */
+ changeVisibleMonthByOffset(offset: number, type: 'days' | 'months' | 'years') {
+ const cloneValue = this.state.visibleMonth.clone();
+ cloneValue.add(offset, type);
+ this.changeVisibleMonth(cloneValue, 'buttonClick');
+ }
+
+ goPrevDecade = () => {
+ this.changeVisibleMonthByOffset(-10, 'years');
+ };
+
+ goNextDecade = () => {
+ this.changeVisibleMonthByOffset(10, 'years');
+ };
+
+ goPrevYear = () => {
+ this.changeVisibleMonthByOffset(-1, 'years');
+ };
+
+ goNextYear = () => {
+ this.changeVisibleMonthByOffset(1, 'years');
+ };
+
+ goPrevMonth = () => {
+ this.changeVisibleMonthByOffset(-1, 'months');
+ };
+
+ goNextMonth = () => {
+ this.changeVisibleMonthByOffset(1, 'months');
+ };
+
+ render() {
+ const {
+ prefix,
+ rtl,
+ className,
+ shape,
+ showOtherMonth,
+ format,
+ locale,
+ dateCellRender,
+ monthCellRender,
+ yearCellRender,
+ disabledDate,
+ yearRange,
+ disableChangeMode,
+ ...others
+ } = this.props;
+ const state = this.state;
+
+ const classNames = classnames(
+ {
+ [`${prefix}calendar`]: true,
+ [`${prefix}calendar-${shape}`]: shape,
+ },
+ className
+ );
+
+ if (rtl) {
+ others.dir = 'rtl';
+ }
+
+ const visibleMonth = state.visibleMonth;
+
+ // reset moment locale
+ if (locale.momentLocale) {
+ state.value && state.value.locale(locale.momentLocale);
+ visibleMonth.locale(locale.momentLocale);
+ }
+
+ const localeData = getLocaleData(locale.format || {}, visibleMonth.localeData());
+
+ const headerProps = {
+ prefix,
+ value: state.value,
+ mode: state.mode,
+ disableChangeMode,
+ yearRange,
+ locale,
+ rtl,
+ visibleMonth,
+ momentLocale: localeData,
+ changeMode: this.changeMode,
+ changeVisibleMonth: this.changeVisibleMonth,
+ goNextDecade: this.goNextDecade,
+ goNextYear: this.goNextYear,
+ goNextMonth: this.goNextMonth,
+ goPrevDecade: this.goPrevDecade,
+ goPrevYear: this.goPrevYear,
+ goPrevMonth: this.goPrevMonth,
+ };
+
+ const tableProps = {
+ prefix,
+ visibleMonth,
+ showOtherMonth,
+ value: state.value,
+ mode: state.mode,
+ locale,
+ dateCellRender,
+ monthCellRender,
+ yearCellRender,
+ disabledDate,
+ momentLocale: localeData,
+ today: this.today,
+ goPrevDecade: this.goPrevDecade,
+ goNextDecade: this.goNextDecade,
+ };
+
+ const tables = {
+ [CALENDAR_MODE_DATE]: (
+
+ ),
+ [CALENDAR_MODE_MONTH]: ,
+ [CALENDAR_MODE_YEAR]: (
+
+ ),
+ };
+
+ const panelHeaders = {
+ [CALENDAR_MODE_DATE]: (
+
+ ),
+ [CALENDAR_MODE_MONTH]: ,
+ [CALENDAR_MODE_YEAR]: ,
+ };
+
+ return (
+
+ {shape === 'panel' ? (
+ panelHeaders[state.mode]
+ ) : (
+
+ )}
+ {tables[state.mode]}
+
+ );
+ }
+}
+
+export default polyfill(Calendar);
diff --git a/components/calendar/head/card-header.jsx b/components/calendar/head/card-header.jsx
deleted file mode 100644
index 2680e4280e..0000000000
--- a/components/calendar/head/card-header.jsx
+++ /dev/null
@@ -1,117 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import Select from '../../select';
-import Radio from '../../radio';
-import ConfigProvider from '../../config-provider';
-
-class CardHeader extends React.Component {
- static propTypes = {
- yearRange: PropTypes.arrayOf(PropTypes.number),
- yearRangeOffset: PropTypes.number,
- locale: PropTypes.object,
- };
-
- static defaultProps = {
- yearRangeOffset: 10,
- };
-
- selectContainerHandler = target => {
- const { device } = this.props;
- if (device === 'phone') {
- return document.body;
- }
- return target.parentNode;
- };
-
- getYearSelect(year) {
- const { prefix, yearRangeOffset, yearRange = [], locale } = this.props;
-
- let [startYear, endYear] = yearRange;
- if (!startYear || !endYear) {
- startYear = year - yearRangeOffset;
- endYear = year + yearRangeOffset;
- }
-
- const options = [];
- for (let i = startYear; i <= endYear; i++) {
- options.push(
-
- {i}
-
- );
- }
-
- return (
-
- {options}
-
- );
- }
-
- getMonthSelect(month) {
- const { prefix, momentLocale, locale } = this.props;
- const localeMonths = momentLocale.monthsShort();
- const options = [];
- for (let i = 0; i < 12; i++) {
- options.push(
-
- {localeMonths[i]}
-
- );
- }
- return (
-
- {options}
-
- );
- }
-
- onYearChange = year => {
- const { visibleMonth, changeVisibleMonth } = this.props;
- changeVisibleMonth(visibleMonth.clone().year(year), 'yearSelect');
- };
-
- changeVisibleMonth = month => {
- const { visibleMonth, changeVisibleMonth } = this.props;
- changeVisibleMonth(visibleMonth.clone().month(month), 'monthSelect');
- };
-
- onModePanelChange = mode => {
- this.props.changeMode(mode);
- };
-
- render() {
- const { prefix, mode, locale, visibleMonth } = this.props;
-
- const yearSelect = this.getYearSelect(visibleMonth.year());
- const monthSelect = mode === 'month' ? null : this.getMonthSelect(visibleMonth.month());
- const panelSelect = (
-
- {locale.month}
- {locale.year}
-
- );
-
- return (
-
- {yearSelect}
- {monthSelect}
- {panelSelect}
-
- );
- }
-}
-
-export default ConfigProvider.config(CardHeader);
diff --git a/components/calendar/head/card-header.tsx b/components/calendar/head/card-header.tsx
new file mode 100644
index 0000000000..0a64b4cdf1
--- /dev/null
+++ b/components/calendar/head/card-header.tsx
@@ -0,0 +1,133 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Select from '../../select';
+import Radio from '../../radio';
+import ConfigProvider from '../../config-provider';
+import type { CalendarMode, CardHeaderProps } from '../types';
+
+class CardHeader extends React.Component {
+ static propTypes = {
+ yearRange: PropTypes.arrayOf(PropTypes.number),
+ yearRangeOffset: PropTypes.number,
+ locale: PropTypes.object,
+ };
+
+ static defaultProps = {
+ yearRangeOffset: 10,
+ };
+
+ selectContainerHandler = (target: HTMLElement) => {
+ const { device } = this.props;
+ if (device === 'phone') {
+ return document.body;
+ }
+ return target.parentNode as HTMLElement;
+ };
+
+ getYearSelect(year: number) {
+ const {
+ prefix,
+ yearRangeOffset,
+ yearRange = [],
+ locale,
+ showOtherMonth,
+ mode,
+ } = this.props;
+
+ let [startYear, endYear] = yearRange;
+ if (!startYear || !endYear) {
+ startYear = year - yearRangeOffset!;
+ endYear = year + yearRangeOffset!;
+ }
+
+ const options = [];
+ for (let i = startYear; i <= endYear; i++) {
+ options.push(
+
+ {i}
+
+ );
+ }
+
+ return (
+
+ {options}
+
+ );
+ }
+
+ getMonthSelect(month: number) {
+ const { prefix, momentLocale, locale, showOtherMonth, mode } = this.props;
+ const localeMonths = momentLocale.monthsShort();
+ const options = [];
+ for (let i = 0; i < 12; i++) {
+ options.push(
+
+ {localeMonths[i]}
+
+ );
+ }
+ return (
+
+ {options}
+
+ );
+ }
+
+ onYearChange = (year: number) => {
+ const { visibleMonth, changeVisibleMonth } = this.props;
+ changeVisibleMonth(visibleMonth.clone().year(year), 'yearSelect');
+ };
+
+ changeVisibleMonth = (month: number) => {
+ const { visibleMonth, changeVisibleMonth } = this.props;
+ changeVisibleMonth(visibleMonth.clone().month(month), 'monthSelect');
+ };
+
+ onModePanelChange = (mode: CalendarMode) => {
+ this.props.changeMode(mode);
+ };
+
+ render() {
+ const { prefix, mode, locale, visibleMonth, showOtherMonth } = this.props;
+
+ const yearSelect = this.getYearSelect(visibleMonth.year());
+ const monthSelect = mode === 'month' ? null : this.getMonthSelect(visibleMonth.month());
+ const panelSelect =
+ !showOtherMonth && mode === 'date' ? null : (
+
+ {locale.month}
+ {locale.year}
+
+ );
+
+ return (
+
+ {yearSelect}
+ {monthSelect}
+ {panelSelect}
+
+ );
+ }
+}
+
+export default ConfigProvider.config(CardHeader);
diff --git a/components/calendar/head/date-panel-header.jsx b/components/calendar/head/date-panel-header.jsx
deleted file mode 100644
index cdaa0c7070..0000000000
--- a/components/calendar/head/date-panel-header.jsx
+++ /dev/null
@@ -1,167 +0,0 @@
-/* istanbul ignore file */
-import React from 'react';
-import Icon from '../../icon';
-import Dropdown from '../../dropdown';
-import SelectMenu from './menu';
-import { getMonths, getYears } from '../utils';
-
-/* eslint-disable */
-class DatePanelHeader extends React.PureComponent {
- static defaultProps = {
- yearRangeOffset: 10,
- };
-
- selectContainerHandler = target => {
- return target.parentNode;
- };
-
- onYearChange = year => {
- const { visibleMonth, changeVisibleMonth } = this.props;
- changeVisibleMonth(visibleMonth.clone().year(year), 'yearSelect');
- };
-
- changeVisibleMonth = month => {
- const { visibleMonth, changeVisibleMonth } = this.props;
- changeVisibleMonth(visibleMonth.clone().month(month), 'monthSelect');
- };
-
- render() {
- const {
- prefix,
- visibleMonth,
- momentLocale,
- locale,
- changeMode,
- goNextMonth,
- goNextYear,
- goPrevMonth,
- goPrevYear,
- disableChangeMode,
- yearRangeOffset,
- yearRange = [],
- } = this.props;
-
- const localedMonths = momentLocale.months();
- const monthLabel = localedMonths[visibleMonth.month()];
- const yearLabel = visibleMonth.year();
- const btnCls = `${prefix}calendar-btn`;
-
- let monthButton = (
- changeMode('month', 'start')}
- >
- {monthLabel}
-
- );
-
- let yearButton = (
- changeMode('year', 'start')}
- >
- {yearLabel}
-
- );
-
- if (disableChangeMode) {
- const months = getMonths(momentLocale);
- const years = getYears(yearRange, yearRangeOffset, visibleMonth.year());
-
- monthButton = (
-
- {monthLabel}
-
-
- }
- triggerType="click"
- >
- this.changeVisibleMonth(value)}
- />
-
- );
-
- yearButton = (
-
- {yearLabel}
-
-
- }
- triggerType="click"
- >
-
-
- );
- }
-
- return (
-
-
-
-
-
-
-
-
- {monthButton}
- {yearButton}
-
-
-
-
-
-
-
-
- );
- }
-}
-
-export default DatePanelHeader;
diff --git a/components/calendar/head/date-panel-header.tsx b/components/calendar/head/date-panel-header.tsx
new file mode 100644
index 0000000000..ce95b06f2a
--- /dev/null
+++ b/components/calendar/head/date-panel-header.tsx
@@ -0,0 +1,181 @@
+import React from 'react';
+import Icon from '../../icon';
+import Dropdown from '../../dropdown';
+import SelectMenu from './menu';
+import { getMonths, getYears } from '../utils';
+import { type DatePanelHeaderProps } from '../types';
+
+class DatePanelHeader extends React.PureComponent {
+ static defaultProps = {
+ yearRangeOffset: 10,
+ };
+
+ selectContainerHandler = (target: HTMLElement) => {
+ return target.parentNode;
+ };
+
+ onYearChange = (year: number) => {
+ const { visibleMonth, changeVisibleMonth } = this.props;
+ changeVisibleMonth(visibleMonth.clone().year(year), 'yearSelect');
+ };
+
+ changeVisibleMonth = (month: number) => {
+ const { visibleMonth, changeVisibleMonth } = this.props;
+ changeVisibleMonth(visibleMonth.clone().month(month), 'monthSelect');
+ };
+
+ render() {
+ const {
+ prefix,
+ visibleMonth,
+ momentLocale,
+ locale,
+ showOtherMonth,
+ changeMode,
+ goNextMonth,
+ goNextYear,
+ goPrevMonth,
+ goPrevYear,
+ disableChangeMode,
+ yearRangeOffset,
+ yearRange = [],
+ } = this.props;
+
+ const localedMonths = momentLocale.months();
+ const monthLabel = localedMonths[visibleMonth.month()];
+ const yearLabel = visibleMonth.year();
+ const btnCls = `${prefix}calendar-btn`;
+
+ let monthButton = (
+ showOtherMonth && changeMode('month', 'start')}
+ >
+ {monthLabel}
+
+ );
+
+ let yearButton = (
+ showOtherMonth && changeMode('year', 'start')}
+ >
+ {yearLabel}
+
+ );
+
+ if (disableChangeMode) {
+ const months = getMonths(momentLocale);
+ const years = getYears(yearRange, yearRangeOffset, visibleMonth.year());
+
+ monthButton = (
+
+ {monthLabel}
+
+
+ }
+ triggerType="click"
+ >
+ this.changeVisibleMonth(value)}
+ />
+
+ );
+
+ yearButton = (
+
+ {yearLabel}
+
+
+ }
+ triggerType="click"
+ >
+
+
+ );
+ }
+
+ return (
+
+ {showOtherMonth && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+ {monthButton}
+ {yearButton}
+
+ {showOtherMonth && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+ );
+ }
+}
+
+export default DatePanelHeader;
diff --git a/components/calendar/head/menu.jsx b/components/calendar/head/menu.jsx
deleted file mode 100644
index f08a1a3bb1..0000000000
--- a/components/calendar/head/menu.jsx
+++ /dev/null
@@ -1,62 +0,0 @@
-/* istanbul ignore file */
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import { findDOMNode } from 'react-dom';
-import Menu from '../../menu';
-
-export default class SelectMenu extends Component {
- static isNextMenu = true;
- static propTypes = {
- dataSource: PropTypes.arrayOf(PropTypes.object),
- value: PropTypes.number,
- prefix: PropTypes.string,
- onChange: PropTypes.func,
- children: PropTypes.node,
- };
-
- componentDidMount() {
- this.scrollToSelectedItem();
- }
-
- scrollToSelectedItem() {
- const { prefix, dataSource, value } = this.props;
-
- const selectedIndex = dataSource.findIndex(item => item.value === value);
-
- if (selectedIndex === -1) {
- return;
- }
-
- const itemSelector = `.${prefix}menu-item`;
- const menu = findDOMNode(this.menuEl);
- const targetItem = menu.querySelectorAll(itemSelector)[selectedIndex];
- if (targetItem) {
- menu.scrollTop =
- targetItem.offsetTop -
- Math.floor((menu.clientHeight / targetItem.clientHeight - 1) / 2) * targetItem.clientHeight;
- }
- }
-
- saveRef = ref => {
- this.menuEl = ref;
- };
-
- render() {
- const { prefix, dataSource, onChange, value, className, ...others } = this.props;
- return (
- onChange(Number(selectKeys[0]))}
- role="listbox"
- className={`${prefix}calendar-panel-menu ${className}`}
- >
- {dataSource.map(({ label, value }) => (
- {label}
- ))}
-
- );
- }
-}
diff --git a/components/calendar/head/menu.tsx b/components/calendar/head/menu.tsx
new file mode 100644
index 0000000000..8133ed2ec6
--- /dev/null
+++ b/components/calendar/head/menu.tsx
@@ -0,0 +1,64 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { findDOMNode } from 'react-dom';
+import Menu from '../../menu';
+import type { SelectMenuProps } from '../types';
+
+export default class SelectMenu extends Component {
+ static isNextMenu = true;
+ static propTypes = {
+ dataSource: PropTypes.arrayOf(PropTypes.object),
+ value: PropTypes.number,
+ prefix: PropTypes.string,
+ onChange: PropTypes.func,
+ children: PropTypes.node,
+ };
+ menuEl: InstanceType | null;
+
+ componentDidMount() {
+ this.scrollToSelectedItem();
+ }
+
+ scrollToSelectedItem() {
+ const { prefix, dataSource, value } = this.props;
+
+ const selectedIndex = dataSource.findIndex(item => item.value === value);
+
+ if (selectedIndex === -1) {
+ return;
+ }
+
+ const itemSelector = `.${prefix}menu-item`;
+ const menu = findDOMNode(this.menuEl) as HTMLElement;
+ const targetItem = menu!.querySelectorAll(itemSelector)[selectedIndex] as HTMLElement;
+ if (targetItem) {
+ menu.scrollTop =
+ targetItem.offsetTop -
+ Math.floor((menu.clientHeight / targetItem.clientHeight - 1) / 2) *
+ targetItem.clientHeight;
+ }
+ }
+
+ saveRef = (ref: InstanceType | null) => {
+ this.menuEl = ref;
+ };
+
+ render() {
+ const { prefix, dataSource, onChange, value, className, ...others } = this.props;
+ return (
+ onChange(Number(selectKeys[0]))}
+ role="listbox"
+ className={`${prefix}calendar-panel-menu ${className}`}
+ >
+ {dataSource.map(({ label, value }) => (
+ {label}
+ ))}
+
+ );
+ }
+}
diff --git a/components/calendar/head/month-panel-header.jsx b/components/calendar/head/month-panel-header.jsx
deleted file mode 100644
index 6d4a3b7375..0000000000
--- a/components/calendar/head/month-panel-header.jsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import React from 'react';
-import Icon from '../../icon';
-
-class MonthPanelHeader extends React.PureComponent {
- render() {
- const { prefix, visibleMonth, locale, changeMode, goPrevYear, goNextYear } = this.props;
- const yearLabel = visibleMonth.year();
- const btnCls = `${prefix}calendar-btn`;
-
- return (
-
-
-
-
-
- changeMode('year')}
- >
- {yearLabel}
-
-
-
-
-
-
- );
- }
-}
-
-export default MonthPanelHeader;
diff --git a/components/calendar/head/month-panel-header.tsx b/components/calendar/head/month-panel-header.tsx
new file mode 100644
index 0000000000..e26f9f6519
--- /dev/null
+++ b/components/calendar/head/month-panel-header.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import Icon from '../../icon';
+import { type MonthPanelHeaderProps } from '../types';
+
+class MonthPanelHeader extends React.PureComponent {
+ render() {
+ const { prefix, visibleMonth, locale, changeMode, goPrevYear, goNextYear } = this.props;
+ const yearLabel = visibleMonth.year();
+ const btnCls = `${prefix}calendar-btn`;
+
+ return (
+
+
+
+
+
+ changeMode('year')}
+ >
+ {yearLabel}
+
+
+
+
+
+
+ );
+ }
+}
+
+export default MonthPanelHeader;
diff --git a/components/calendar/head/range-panel-header.jsx b/components/calendar/head/range-panel-header.jsx
deleted file mode 100644
index 2912460858..0000000000
--- a/components/calendar/head/range-panel-header.jsx
+++ /dev/null
@@ -1,228 +0,0 @@
-/* istanbul ignore file */
-import React from 'react';
-import Icon from '../../icon';
-import Dropdown from '../../dropdown';
-import SelectMenu from './menu';
-import { getMonths, getYears } from '../utils';
-
-/* eslint-disable */
-class RangePanelHeader extends React.PureComponent {
- static defaultProps = {
- yearRangeOffset: 10,
- };
-
- selectContainerHandler = target => {
- return target.parentNode;
- };
-
- onYearChange = (visibleMonth, year, tag) => {
- const { changeVisibleMonth } = this.props;
- const startYear = visibleMonth
- .clone()
- .year(year)
- .add(tag === 'end' ? -1 : 0, 'month');
- changeVisibleMonth(startYear, 'yearSelect');
- };
-
- changeVisibleMonth = (visibleMonth, month, tag) => {
- const { changeVisibleMonth } = this.props;
- const startMonth = tag === 'end' ? month - 1 : month;
- changeVisibleMonth(visibleMonth.clone().month(startMonth), 'monthSelect');
- };
-
- render() {
- const {
- prefix,
- startVisibleMonth,
- endVisibleMonth,
- yearRange = [],
- yearRangeOffset,
- momentLocale,
- locale,
- changeMode,
- goNextMonth,
- goNextYear,
- goPrevMonth,
- goPrevYear,
- disableChangeMode,
- } = this.props;
-
- const localedMonths = momentLocale.months();
- const startMonthLabel = localedMonths[startVisibleMonth.month()];
- const endMonthLabel = localedMonths[endVisibleMonth.month()];
- const startYearLabel = startVisibleMonth.year();
- const endYearLabel = endVisibleMonth.year();
- const btnCls = `${prefix}calendar-btn`;
-
- const months = getMonths(momentLocale);
- const startYears = getYears(yearRange, yearRangeOffset, startVisibleMonth.year());
- const endYears = getYears(yearRange, yearRangeOffset, endVisibleMonth.year());
-
- return (
-
-
-
-
-
-
-
-
- {disableChangeMode ? (
-
- {startMonthLabel}
-
-
- }
- triggerType="click"
- >
- this.changeVisibleMonth(startVisibleMonth, value, 'start')}
- />
-
- ) : (
- changeMode('month', 'start')}
- >
- {startMonthLabel}
-
- )}
- {disableChangeMode ? (
-
- {startYearLabel}
-
-
- }
- triggerType="click"
- >
- this.onYearChange(startVisibleMonth, v, 'start')}
- />
-
- ) : (
- changeMode('year', 'start')}
- >
- {startYearLabel}
-
- )}
-
-
- {disableChangeMode ? (
-
- {endMonthLabel}
-
-
- }
- triggerType="click"
- >
- this.changeVisibleMonth(endVisibleMonth, value, 'end')}
- />
-
- ) : (
- changeMode('month', 'end')}
- >
- {endMonthLabel}
-
- )}
- {disableChangeMode ? (
-
- {endYearLabel}
-
-
- }
- triggerType="click"
- >
- this.onYearChange(endVisibleMonth, v, 'end')}
- />
-
- ) : (
- changeMode('year', 'end')}
- >
- {endYearLabel}
-
- )}
-
-
-
-
-
-
-
-
- );
- }
-}
-
-export default RangePanelHeader;
diff --git a/components/calendar/head/range-panel-header.tsx b/components/calendar/head/range-panel-header.tsx
new file mode 100644
index 0000000000..569836b52d
--- /dev/null
+++ b/components/calendar/head/range-panel-header.tsx
@@ -0,0 +1,242 @@
+import React from 'react';
+import { type Moment } from 'moment';
+import Icon from '../../icon';
+import Dropdown from '../../dropdown';
+import SelectMenu from './menu';
+import { getMonths, getYears } from '../utils';
+import { type RangePanelHeaderProps } from '../types';
+
+class RangePanelHeader extends React.PureComponent {
+ static defaultProps = {
+ yearRangeOffset: 10,
+ };
+
+ selectContainerHandler = (target: HTMLElement) => {
+ return target.parentNode as HTMLElement;
+ };
+
+ onYearChange = (visibleMonth: Moment, year: number, tag: 'start' | 'end') => {
+ const { changeVisibleMonth } = this.props;
+ const startYear = visibleMonth
+ .clone()
+ .year(year)
+ .add(tag === 'end' ? -1 : 0, 'month');
+ changeVisibleMonth(startYear, 'yearSelect');
+ };
+
+ changeVisibleMonth = (visibleMonth: Moment, month: number, tag: 'start' | 'end') => {
+ const { changeVisibleMonth } = this.props;
+ const startMonth = tag === 'end' ? month - 1 : month;
+ changeVisibleMonth(visibleMonth.clone().month(startMonth), 'monthSelect');
+ };
+
+ render() {
+ const {
+ prefix,
+ startVisibleMonth,
+ endVisibleMonth,
+ yearRange = [],
+ yearRangeOffset,
+ momentLocale,
+ locale,
+ changeMode,
+ goNextMonth,
+ goNextYear,
+ goPrevMonth,
+ goPrevYear,
+ disableChangeMode,
+ } = this.props;
+
+ const localedMonths = momentLocale.months();
+ const startMonthLabel = localedMonths[startVisibleMonth.month()];
+ const endMonthLabel = localedMonths[endVisibleMonth.month()];
+ const startYearLabel = startVisibleMonth.year();
+ const endYearLabel = endVisibleMonth.year();
+ const btnCls = `${prefix}calendar-btn`;
+
+ const months = getMonths(momentLocale);
+ const startYears = getYears(yearRange, yearRangeOffset!, startVisibleMonth.year());
+ const endYears = getYears(yearRange, yearRangeOffset!, endVisibleMonth.year());
+
+ return (
+
+
+
+
+
+
+
+
+ {disableChangeMode ? (
+
+ {startMonthLabel}
+
+
+ }
+ triggerType="click"
+ >
+
+ this.changeVisibleMonth(startVisibleMonth, value, 'start')
+ }
+ />
+
+ ) : (
+ changeMode('month', 'start')}
+ >
+ {startMonthLabel}
+
+ )}
+ {disableChangeMode ? (
+
+ {startYearLabel}
+
+
+ }
+ triggerType="click"
+ >
+ this.onYearChange(startVisibleMonth, v, 'start')}
+ />
+
+ ) : (
+ changeMode('year', 'start')}
+ >
+ {startYearLabel}
+
+ )}
+
+
+ {disableChangeMode ? (
+
+ {endMonthLabel}
+
+
+ }
+ triggerType="click"
+ >
+
+ this.changeVisibleMonth(endVisibleMonth, value, 'end')
+ }
+ />
+
+ ) : (
+ changeMode('month', 'end')}
+ >
+ {endMonthLabel}
+
+ )}
+ {disableChangeMode ? (
+
+ {endYearLabel}
+
+
+ }
+ triggerType="click"
+ >
+ this.onYearChange(endVisibleMonth, v, 'end')}
+ />
+
+ ) : (
+ changeMode('year', 'end')}
+ >
+ {endYearLabel}
+
+ )}
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default RangePanelHeader;
diff --git a/components/calendar/head/year-panel-header.jsx b/components/calendar/head/year-panel-header.jsx
deleted file mode 100644
index 43c1bd8077..0000000000
--- a/components/calendar/head/year-panel-header.jsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import React from 'react';
-import Icon from '../../icon';
-
-class YearPanelHeader extends React.PureComponent {
- getDecadeLabel = date => {
- const year = date.year();
- const start = parseInt(year / 10, 10) * 10;
- const end = start + 9;
- return `${start}-${end}`;
- };
-
- render() {
- const { prefix, visibleMonth, locale, goPrevDecade, goNextDecade } = this.props;
- const decadeLable = this.getDecadeLabel(visibleMonth);
- const btnCls = `${prefix}calendar-btn`;
-
- return (
-
-
-
-
-
-
- {decadeLable}
-
-
-
-
-
-
- );
- }
-}
-
-export default YearPanelHeader;
diff --git a/components/calendar/head/year-panel-header.tsx b/components/calendar/head/year-panel-header.tsx
new file mode 100644
index 0000000000..dbff012512
--- /dev/null
+++ b/components/calendar/head/year-panel-header.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import { type Moment } from 'moment';
+import Icon from '../../icon';
+import { type YearPanelHeaderProps } from '../types';
+
+class YearPanelHeader extends React.PureComponent {
+ getDecadeLabel = (date: Moment) => {
+ const year = date.year();
+ // @ts-expect-error parseInt 接收的参数类型是 string
+ const start = parseInt(year / 10, 10) * 10;
+ const end = start + 9;
+ return `${start}-${end}`;
+ };
+
+ render() {
+ const { prefix, visibleMonth, locale, goPrevDecade, goNextDecade } = this.props;
+ const decadeLable = this.getDecadeLabel(visibleMonth);
+ const btnCls = `${prefix}calendar-btn`;
+
+ return (
+
+
+
+
+
+
+ {decadeLable}
+
+
+
+
+
+
+ );
+ }
+}
+
+export default YearPanelHeader;
diff --git a/components/calendar/index.d.ts b/components/calendar/index.d.ts
deleted file mode 100644
index a52ea31b9c..0000000000
--- a/components/calendar/index.d.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-///
-
-import React from 'react';
-import { CommonProps } from '../util';
-
-interface HTMLAttributesWeak extends React.HTMLAttributes {
- defaultValue?: any;
- onSelect?: any;
-}
-
-export interface CalendarProps extends HTMLAttributesWeak, CommonProps {
- /**
- * 默认选中的日期(moment 对象)
- */
- defaultValue?: any;
-
- /**
- * 选中的日期值 (moment 对象)
- */
- value?: any;
-
- /**
- * 是否展示非本月的日期
- */
- showOtherMonth?: boolean;
-
- /**
- * 默认展示的月份
- */
- defaultVisibleMonth?: () => void;
-
- /**
- * 展现形态
- */
- shape?: 'card' | 'fullscreen' | 'panel';
-
- /**
- * 选择日期单元格时的回调
- */
- onSelect?: (value: {}) => void;
-
- /**
- * 展现的月份变化时的回调
- */
- onVisibleMonthChange?: (value: {}, reason: string) => void;
-
- /**
- * 自定义样式类
- */
- className?: string;
-
- /**
- * 自定义日期渲染函数
- */
- dateCellRender?: (value: {}) => React.ReactNode;
-
- /**
- * 自定义月份渲染函数
- */
- monthCellRender?: (calendarDate: {}) => React.ReactNode;
-
- /**
- * 不可选择的日期
- */
- disabledDate?: (calendarDate: {}, view: string) => boolean;
-}
-
-export default class Calendar extends React.Component {}
diff --git a/components/calendar/index.jsx b/components/calendar/index.jsx
deleted file mode 100644
index 8a77193b24..0000000000
--- a/components/calendar/index.jsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import ConfigProvider from '../config-provider';
-import { preFormatDateValue } from './utils';
-import Calendar from './calendar';
-import RangeCalendar from './range-calendar';
-
-/* istanbul ignore next */
-const transform = (props, deprecated) => {
- const { type, onChange, base, disabledMonth, disabledYear, ...others } = props;
- const newProps = others;
-
- if ('type' in props) {
- deprecated('type', 'shape', 'Calendar');
-
- newProps.shape = type;
-
- if ('shape' in props) {
- newProps.shape = props.shape;
- }
- }
-
- if ('base' in props) {
- deprecated('base', 'defaultVisibleMonth', 'Calendar');
-
- let newDefaultVisibleMonth = () => {
- preFormatDateValue(base, 'YYYY-MM-DD');
- };
-
- if ('defaultVisibleMonth' in props) {
- newDefaultVisibleMonth = props.defaultVisibleMonth;
- }
-
- newProps.defaultVisibleMonth = newDefaultVisibleMonth;
- }
-
- if ('onChange' in props && typeof onChange === 'function') {
- deprecated('onChange', 'onSelect', 'Calendar');
-
- const newOnSelect = date => {
- onChange({ mode: others.mode, value: date });
-
- if ('onSelect' in props) {
- props.onSelect(date);
- }
- };
-
- newProps.onSelect = newOnSelect;
- }
-
- if ('disabledMonth' in props && typeof disabledMonth === 'function') {
- deprecated('disabledMonth', 'disabledDate', 'Calendar');
- }
-
- if ('disabledYear' in props && typeof disabledYear === 'function') {
- deprecated('disabledYear', 'disabledDate', 'Calendar');
- }
-
- if ('yearCellRender' in props && typeof yearCellRender === 'function') {
- deprecated('yearCellRender', 'monthCellRender/dateCellRender', 'Calendar');
- }
-
- if ('language' in props) {
- deprecated('language', 'moment.locale', 'Calendar');
- }
-
- return newProps;
-};
-
-Calendar.RangeCalendar = RangeCalendar;
-export default ConfigProvider.config(Calendar, {
- transform,
-});
diff --git a/components/calendar/index.tsx b/components/calendar/index.tsx
new file mode 100644
index 0000000000..fe828701c1
--- /dev/null
+++ b/components/calendar/index.tsx
@@ -0,0 +1,77 @@
+import { type Moment } from 'moment';
+import ConfigProvider from '../config-provider';
+import { preFormatDateValue } from './utils';
+import Calendar from './calendar';
+import RangeCalendar from './range-calendar';
+import { assignSubComponent } from '../util/component';
+import type { CalendarProps } from './types';
+import type { log } from '../util';
+
+export type { CalendarProps, RangeCalendarProps, CalendarMode } from './types';
+
+const transform = (props: CalendarProps, deprecated: typeof log.deprecated) => {
+ const { type, onChange, base, disabledMonth, disabledYear, yearCellRender, ...others } = props;
+ const newProps = others;
+
+ if ('type' in props) {
+ deprecated('type', 'shape', 'Calendar');
+
+ newProps.shape = type;
+
+ if ('shape' in props) {
+ newProps.shape = props.shape;
+ }
+ }
+
+ if ('base' in props) {
+ deprecated('base', 'defaultVisibleMonth', 'Calendar');
+
+ let newDefaultVisibleMonth = () => {
+ return preFormatDateValue(base, 'YYYY-MM-DD');
+ };
+
+ if ('defaultVisibleMonth' in props) {
+ newDefaultVisibleMonth = props.defaultVisibleMonth!;
+ }
+
+ newProps.defaultVisibleMonth = newDefaultVisibleMonth;
+ }
+
+ if ('onChange' in props && typeof onChange === 'function') {
+ deprecated('onChange', 'onSelect', 'Calendar');
+
+ const newOnSelect = (date: Moment) => {
+ onChange({ mode: others.mode!, value: date });
+
+ if ('onSelect' in props) {
+ props.onSelect!(date);
+ }
+ };
+
+ newProps.onSelect = newOnSelect;
+ }
+
+ if ('disabledMonth' in props && typeof disabledMonth === 'function') {
+ deprecated('disabledMonth', 'disabledDate', 'Calendar');
+ }
+
+ if ('disabledYear' in props && typeof disabledYear === 'function') {
+ deprecated('disabledYear', 'disabledDate', 'Calendar');
+ }
+
+ if ('yearCellRender' in props && typeof yearCellRender === 'function') {
+ deprecated('yearCellRender', 'monthCellRender/dateCellRender', 'Calendar');
+ }
+
+ if ('language' in props) {
+ deprecated('language', 'moment.locale', 'Calendar');
+ }
+
+ return newProps;
+};
+
+const CalendarWithSub = assignSubComponent(Calendar, { RangeCalendar });
+
+export default ConfigProvider.config(CalendarWithSub, {
+ transform,
+});
diff --git a/components/calendar/mobile/index.jsx b/components/calendar/mobile/index.tsx
similarity index 100%
rename from components/calendar/mobile/index.jsx
rename to components/calendar/mobile/index.tsx
diff --git a/components/calendar/range-calendar.jsx b/components/calendar/range-calendar.jsx
deleted file mode 100644
index 244b8b77c1..0000000000
--- a/components/calendar/range-calendar.jsx
+++ /dev/null
@@ -1,384 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { polyfill } from 'react-lifecycles-compat';
-import classnames from 'classnames';
-import moment from 'moment';
-import ConfigProvider from '../config-provider';
-import nextLocale from '../locale/zh-cn';
-import { obj, func } from '../util';
-import RangePanelHeader from './head/range-panel-header';
-import MonthPanelHeader from './head/month-panel-header';
-import YearPanelHeader from './head/year-panel-header';
-import DateTable from './table/date-table';
-import MonthTable from './table/month-table';
-import YearTable from './table/year-table';
-import {
- checkMomentObj,
- formatDateValue,
- getVisibleMonth,
- isSameYearMonth,
- CALENDAR_MODES,
- CALENDAR_MODE_DATE,
- CALENDAR_MODE_MONTH,
- CALENDAR_MODE_YEAR,
- getLocaleData,
-} from './utils';
-
-class RangeCalendar extends React.Component {
- static propTypes = {
- ...ConfigProvider.propTypes,
- /**
- * 样式前缀
- */
- prefix: PropTypes.string,
- rtl: PropTypes.bool,
- /**
- * 默认的开始日期
- */
- defaultStartValue: checkMomentObj,
- /**
- * 默认的结束日期
- */
- defaultEndValue: checkMomentObj,
- /**
- * 开始日期(moment 对象)
- */
- startValue: checkMomentObj,
- /**
- * 结束日期(moment 对象)
- */
- endValue: checkMomentObj,
- // 面板模式
- mode: PropTypes.oneOf(CALENDAR_MODES),
- // 禁用更改面板模式,采用 dropdown 的方式切换显示日期 (暂不正式对外透出)
- disableChangeMode: PropTypes.bool,
- // 日期值的格式(用于日期title显示的格式)
- format: PropTypes.string,
- yearRange: PropTypes.arrayOf(PropTypes.number),
- /**
- * 是否显示非本月的日期
- */
- showOtherMonth: PropTypes.bool,
- /**
- * 模板展示的月份(起始月份)
- */
- defaultVisibleMonth: PropTypes.func,
- /**
- * 展现的月份变化时的回调
- * @param {Object} value 显示的月份 (moment 对象)
- * @param {String} reason 触发月份改变原因
- */
- onVisibleMonthChange: PropTypes.func,
- /**
- * 不可选择的日期
- * @param {Object} calendarDate 对应 Calendar 返回的自定义日期对象
- * @param {String} view 当前视图类型,year: 年, month: 月, date: 日
- * @returns {Boolean}
- */
- disabledDate: PropTypes.func,
- /**
- * 选择日期单元格时的回调
- * @param {Object} value 对应的日期值 (moment 对象)
- */
- onSelect: PropTypes.func,
- /**
- * 自定义日期单元格渲染
- */
- dateCellRender: PropTypes.func,
- /**
- * 自定义月份渲染函数
- * @param {Object} calendarDate 对应 Calendar 返回的自定义日期对象
- * @returns {ReactNode}
- */
- monthCellRender: PropTypes.func,
- yearCellRender: PropTypes.func, // 兼容 0.x yearCellRender
- locale: PropTypes.object,
- className: PropTypes.string,
- };
-
- static defaultProps = {
- prefix: 'next-',
- rtl: false,
- mode: CALENDAR_MODE_DATE,
- disableChangeMode: false,
- format: 'YYYY-MM-DD',
- dateCellRender: value => value.date(),
- onSelect: func.noop,
- onVisibleMonthChange: func.noop,
- locale: nextLocale.Calendar,
- showOtherMonth: false,
- };
-
- constructor(props, context) {
- super(props, context);
-
- const startValue = formatDateValue(props.startValue || props.defaultStartValue);
- const endValue = formatDateValue(props.endValue || props.defaultEndValue);
- const visibleMonth = getVisibleMonth(props.defaultVisibleMonth, startValue);
-
- this.state = {
- startValue,
- endValue,
- mode: props.mode,
- prevMode: props.mode,
- startVisibleMonth: visibleMonth,
- activePanel: undefined,
- lastMode: undefined,
- lastPanelType: 'start', // enum, 包括 start end
- };
- this.today = moment();
- }
-
- static getDerivedStateFromProps(props, state) {
- const st = {};
- if ('startValue' in props) {
- const startValue = formatDateValue(props.startValue);
- st.startValue = startValue;
- if (startValue && !startValue.isSame(state.startValue, 'day')) {
- st.startVisibleMonth = startValue;
- }
- }
-
- if ('endValue' in props) {
- st.endValue = formatDateValue(props.endValue);
- }
-
- if ('mode' in props && state.prevMode !== props.mode) {
- st.prevMode = props.mode;
- st.mode = props.mode;
- }
-
- return st;
- }
-
- onSelectCell = (date, nextMode) => {
- if (this.state.mode === CALENDAR_MODE_DATE) {
- this.props.onSelect(date);
- } else {
- this.changeVisibleMonth(date, 'cellClick');
- }
-
- this.changeMode(nextMode);
- };
-
- changeMode = (mode, activePanel) => {
- const { lastMode, lastPanelType } = this.state;
-
- const state = {
- lastMode: mode,
- // rangePicker的panel下,选 year -> month ,从当前函数的activePanel传来的数据已经拿不到 start end panel的状态了,需要根据 lastMode 来判断
- lastPanelType: lastMode === 'year' ? lastPanelType : activePanel,
- };
- if (typeof mode === 'string' && mode !== this.state.mode) {
- state.mode = mode;
- }
- if (activePanel && activePanel !== this.state.activePanel) {
- state.activePanel = activePanel;
- }
-
- this.setState(state);
- };
-
- changeVisibleMonth = (date, reason) => {
- const { lastPanelType } = this.state;
- if (!isSameYearMonth(date, this.state.startVisibleMonth)) {
- const startVisibleMonth = lastPanelType === 'end' ? date.clone().add(-1, 'month') : date;
- this.setState({ startVisibleMonth });
- this.props.onVisibleMonthChange(startVisibleMonth, reason);
- }
- };
-
- /**
- * 根据日期偏移量设置当前展示的月份
- * @param {Number} offset 日期偏移量
- * @param {String} type 日期偏移类型 days, months, years
- */
- changeVisibleMonthByOffset = (offset, type) => {
- const offsetDate = this.state.startVisibleMonth.clone().add(offset, type);
- this.changeVisibleMonth(offsetDate, 'buttonClick');
- };
-
- goPrevDecade = () => {
- this.changeVisibleMonthByOffset(-10, 'years');
- };
-
- goNextDecade = () => {
- this.changeVisibleMonthByOffset(10, 'years');
- };
-
- goPrevYear = () => {
- this.changeVisibleMonthByOffset(-1, 'years');
- };
-
- goNextYear = () => {
- this.changeVisibleMonthByOffset(1, 'years');
- };
-
- goPrevMonth = () => {
- this.changeVisibleMonthByOffset(-1, 'months');
- };
-
- goNextMonth = () => {
- this.changeVisibleMonthByOffset(1, 'months');
- };
-
- render() {
- const {
- prefix,
- rtl,
- dateCellRender,
- monthCellRender,
- yearCellRender,
- className,
- format,
- locale,
- showOtherMonth,
- disabledDate,
- disableChangeMode,
- yearRange,
- ...others
- } = this.props;
- const { startValue, endValue, mode, startVisibleMonth, activePanel } = this.state;
-
- // reset moment locale
- if (locale.momentLocale) {
- startValue && startValue.locale(locale.momentLocale);
- endValue && endValue.locale(locale.momentLocale);
- startVisibleMonth.locale(locale.momentLocale);
- }
-
- if (rtl) {
- others.dir = 'rtl';
- }
- const localeData = getLocaleData(locale.format || {}, startVisibleMonth.localeData());
-
- const endVisibleMonth = startVisibleMonth.clone().add(1, 'months');
-
- const headerProps = {
- prefix,
- rtl,
- mode,
- locale,
- momentLocale: localeData,
- startVisibleMonth,
- endVisibleMonth,
- changeVisibleMonth: this.changeVisibleMonth,
- changeMode: this.changeMode,
- yearRange,
- disableChangeMode,
- };
-
- const tableProps = {
- prefix,
- value: startValue,
- startValue,
- endValue,
- mode,
- locale,
- momentLocale: localeData,
- showOtherMonth,
- today: this.today,
- disabledDate,
- dateCellRender,
- monthCellRender,
- yearCellRender,
- changeMode: this.changeMode,
- changeVisibleMonth: this.changeVisibleMonth,
- };
-
- const visibleMonths = {
- start: startVisibleMonth,
- end: endVisibleMonth,
- };
-
- const visibleMonth = visibleMonths[activePanel];
-
- let header;
- let table;
-
- switch (mode) {
- case CALENDAR_MODE_DATE: {
- table = [
-
-
-
,
-
-
-
,
- ];
- header = (
-
- );
- break;
- }
- case CALENDAR_MODE_MONTH: {
- table = ;
- header = (
-
- );
- break;
- }
- case CALENDAR_MODE_YEAR: {
- table = (
-
- );
- header = (
-
- );
- break;
- }
- }
-
- const classNames = classnames(
- {
- [`${prefix}calendar`]: true,
- [`${prefix}calendar-range`]: true,
- },
- className
- );
-
- return (
-
- );
- }
-}
-
-export default ConfigProvider.config(polyfill(RangeCalendar), {
- componentName: 'Calendar',
-});
diff --git a/components/calendar/range-calendar.tsx b/components/calendar/range-calendar.tsx
new file mode 100644
index 0000000000..ac66d473f4
--- /dev/null
+++ b/components/calendar/range-calendar.tsx
@@ -0,0 +1,358 @@
+import React, { type MouseEvent } from 'react';
+import PropTypes from 'prop-types';
+import { polyfill } from 'react-lifecycles-compat';
+import classnames from 'classnames';
+import moment, { type Moment } from 'moment';
+import ConfigProvider from '../config-provider';
+import nextLocale from '../locale/zh-cn';
+import { obj, func, type ClassPropsWithDefault } from '../util';
+import RangePanelHeader from './head/range-panel-header';
+import MonthPanelHeader from './head/month-panel-header';
+import YearPanelHeader from './head/year-panel-header';
+import DateTable from './table/date-table';
+import MonthTable from './table/month-table';
+import YearTable from './table/year-table';
+import {
+ checkMomentObj,
+ formatDateValue,
+ getVisibleMonth,
+ isSameYearMonth,
+ CALENDAR_MODES,
+ CALENDAR_MODE_DATE,
+ CALENDAR_MODE_MONTH,
+ CALENDAR_MODE_YEAR,
+ getLocaleData,
+} from './utils';
+import type {
+ CalendarMode,
+ RangeCalendarProps,
+ RangeCalendarState,
+ VisibleMonthChangeType,
+} from './types';
+
+type InnerRangeCalendarProps = ClassPropsWithDefault<
+ RangeCalendarProps,
+ typeof RangeCalendar.defaultProps
+>;
+
+class RangeCalendar extends React.Component {
+ static propTypes = {
+ ...ConfigProvider.propTypes,
+ prefix: PropTypes.string,
+ rtl: PropTypes.bool,
+ defaultStartValue: checkMomentObj,
+ defaultEndValue: checkMomentObj,
+ startValue: checkMomentObj,
+ endValue: checkMomentObj,
+ mode: PropTypes.oneOf(CALENDAR_MODES),
+ disableChangeMode: PropTypes.bool,
+ format: PropTypes.string,
+ yearRange: PropTypes.arrayOf(PropTypes.number),
+ showOtherMonth: PropTypes.bool,
+ defaultVisibleMonth: PropTypes.func,
+ onVisibleMonthChange: PropTypes.func,
+ disabledDate: PropTypes.func,
+ onSelect: PropTypes.func,
+ dateCellRender: PropTypes.func,
+ monthCellRender: PropTypes.func,
+ yearCellRender: PropTypes.func, // 兼容 0.x yearCellRender
+ locale: PropTypes.object,
+ className: PropTypes.string,
+ };
+
+ static defaultProps = {
+ prefix: 'next-',
+ rtl: false,
+ mode: CALENDAR_MODE_DATE,
+ disableChangeMode: false,
+ format: 'YYYY-MM-DD',
+ dateCellRender: (value: Moment) => value.date(),
+ onSelect: func.noop,
+ onVisibleMonthChange: func.noop,
+ locale: nextLocale.Calendar,
+ showOtherMonth: false,
+ };
+
+ readonly props: InnerRangeCalendarProps;
+ today: Moment;
+
+ constructor(props: RangeCalendarProps) {
+ super(props);
+
+ const startValue = formatDateValue(props.startValue || props.defaultStartValue);
+ const endValue = formatDateValue(props.endValue || props.defaultEndValue);
+ const visibleMonth = getVisibleMonth(props.defaultVisibleMonth, startValue);
+
+ this.state = {
+ startValue,
+ endValue,
+ mode: props.mode,
+ prevMode: props.mode,
+ startVisibleMonth: visibleMonth,
+ activePanel: undefined,
+ lastMode: undefined,
+ lastPanelType: 'start', // enum, 包括 start end
+ };
+ this.today = moment();
+ }
+
+ static getDerivedStateFromProps(props: InnerRangeCalendarProps, state: RangeCalendarState) {
+ const st: Partial = {};
+ if ('startValue' in props) {
+ const startValue = formatDateValue(props.startValue);
+ st.startValue = startValue;
+ if (startValue && !startValue.isSame(state.startValue, 'day')) {
+ st.startVisibleMonth = startValue;
+ }
+ }
+
+ if ('endValue' in props) {
+ st.endValue = formatDateValue(props.endValue);
+ }
+
+ if ('mode' in props && state.prevMode !== props.mode) {
+ st.prevMode = props.mode;
+ st.mode = props.mode;
+ }
+
+ return st;
+ }
+
+ onSelectCell = (date: Moment, nextMode: CalendarMode | MouseEvent) => {
+ if (this.state.mode === CALENDAR_MODE_DATE) {
+ this.props.onSelect(date);
+ } else {
+ this.changeVisibleMonth(date, 'cellClick');
+ }
+
+ this.changeMode(nextMode as CalendarMode);
+ };
+
+ changeMode = (mode: CalendarMode, activePanel?: 'start' | 'end') => {
+ const { lastMode, lastPanelType } = this.state;
+
+ const state = {
+ lastMode: mode,
+ // rangePicker 的 panel 下,选 year -> month,从当前函数的 activePanel 传来的数据已经拿不到 start end panel 的状态了,需要根据 lastMode 来判断
+ lastPanelType: lastMode === 'year' ? lastPanelType : activePanel,
+ } as RangeCalendarState;
+ if (typeof mode === 'string' && mode !== this.state.mode) {
+ state.mode = mode;
+ }
+ if (activePanel && activePanel !== this.state.activePanel) {
+ state.activePanel = activePanel;
+ }
+
+ this.setState(state);
+ };
+
+ changeVisibleMonth = (date: Moment, reason: VisibleMonthChangeType) => {
+ const { lastPanelType } = this.state;
+ if (!isSameYearMonth(date, this.state.startVisibleMonth)) {
+ const startVisibleMonth =
+ lastPanelType === 'end' ? date.clone().add(-1, 'month') : date;
+ this.setState({ startVisibleMonth });
+ this.props.onVisibleMonthChange(startVisibleMonth, reason);
+ }
+ };
+
+ /**
+ * 根据日期偏移量设置当前展示的月份
+ * @param offset - 日期偏移量
+ * @param type - 日期偏移类型 days, months, years
+ */
+ changeVisibleMonthByOffset = (offset: number, type: 'days' | 'months' | 'years') => {
+ const offsetDate = this.state.startVisibleMonth.clone().add(offset, type);
+ this.changeVisibleMonth(offsetDate, 'buttonClick');
+ };
+
+ goPrevDecade = () => {
+ this.changeVisibleMonthByOffset(-10, 'years');
+ };
+
+ goNextDecade = () => {
+ this.changeVisibleMonthByOffset(10, 'years');
+ };
+
+ goPrevYear = () => {
+ this.changeVisibleMonthByOffset(-1, 'years');
+ };
+
+ goNextYear = () => {
+ this.changeVisibleMonthByOffset(1, 'years');
+ };
+
+ goPrevMonth = () => {
+ this.changeVisibleMonthByOffset(-1, 'months');
+ };
+
+ goNextMonth = () => {
+ this.changeVisibleMonthByOffset(1, 'months');
+ };
+
+ render() {
+ const {
+ prefix,
+ rtl,
+ dateCellRender,
+ monthCellRender,
+ yearCellRender,
+ className,
+ format,
+ locale,
+ showOtherMonth,
+ disabledDate,
+ disableChangeMode,
+ yearRange,
+ ...others
+ } = this.props;
+ const { startValue, endValue, mode, startVisibleMonth, activePanel } = this.state;
+
+ // reset moment locale
+ if (locale.momentLocale) {
+ startValue && startValue.locale(locale.momentLocale);
+ endValue && endValue.locale(locale.momentLocale);
+ startVisibleMonth.locale(locale.momentLocale);
+ }
+
+ if (rtl) {
+ others.dir = 'rtl';
+ }
+ const localeData = getLocaleData(locale.format || {}, startVisibleMonth.localeData());
+
+ const endVisibleMonth = startVisibleMonth.clone().add(1, 'months');
+
+ const headerProps = {
+ prefix,
+ rtl,
+ mode,
+ locale,
+ momentLocale: localeData,
+ startVisibleMonth,
+ endVisibleMonth,
+ changeVisibleMonth: this.changeVisibleMonth,
+ changeMode: this.changeMode,
+ yearRange,
+ disableChangeMode,
+ };
+
+ const tableProps = {
+ prefix,
+ value: startValue,
+ startValue,
+ endValue,
+ mode,
+ locale,
+ momentLocale: localeData,
+ showOtherMonth,
+ today: this.today,
+ disabledDate,
+ dateCellRender,
+ monthCellRender,
+ yearCellRender,
+ changeMode: this.changeMode,
+ changeVisibleMonth: this.changeVisibleMonth,
+ };
+
+ const visibleMonths = {
+ start: startVisibleMonth,
+ end: endVisibleMonth,
+ };
+
+ const visibleMonth = visibleMonths[activePanel!];
+
+ let header;
+ let table;
+
+ switch (mode) {
+ case CALENDAR_MODE_DATE: {
+ table = [
+
+
+
,
+
+
+
,
+ ];
+ header = (
+
+ );
+ break;
+ }
+ case CALENDAR_MODE_MONTH: {
+ table = (
+
+ );
+ header = (
+
+ );
+ break;
+ }
+ case CALENDAR_MODE_YEAR: {
+ table = (
+
+ );
+ header = (
+
+ );
+ break;
+ }
+ }
+
+ const classNames = classnames(
+ {
+ [`${prefix}calendar`]: true,
+ [`${prefix}calendar-range`]: true,
+ },
+ className
+ );
+
+ return (
+
+ );
+ }
+}
+
+export default ConfigProvider.config(polyfill(RangeCalendar), {
+ componentName: 'Calendar',
+});
diff --git a/components/calendar/style.js b/components/calendar/style.ts
similarity index 100%
rename from components/calendar/style.js
rename to components/calendar/style.ts
diff --git a/components/calendar/table/date-table-head.jsx b/components/calendar/table/date-table-head.jsx
deleted file mode 100644
index b3d1691b50..0000000000
--- a/components/calendar/table/date-table-head.jsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import React, { PureComponent } from 'react';
-import { DAYS_OF_WEEK } from '../utils';
-
-class DateTableHead extends PureComponent {
- render() {
- const { prefix, momentLocale } = this.props;
- const firstDayOfWeek = momentLocale.firstDayOfWeek();
- const weekdaysShort = momentLocale.weekdaysShort();
-
- const elements = [];
- for (let i = 0; i < DAYS_OF_WEEK; i++) {
- const index = (firstDayOfWeek + i) % DAYS_OF_WEEK;
- elements.push(
-
- {weekdaysShort[index]}
-
- );
- }
-
- return (
-
- {elements}
-
- );
- }
-}
-
-export default DateTableHead;
diff --git a/components/calendar/table/date-table-head.tsx b/components/calendar/table/date-table-head.tsx
new file mode 100644
index 0000000000..01cd15a13c
--- /dev/null
+++ b/components/calendar/table/date-table-head.tsx
@@ -0,0 +1,29 @@
+import React, { PureComponent } from 'react';
+import { DAYS_OF_WEEK } from '../utils';
+import { type DateTableHeadProps } from '../types';
+
+class DateTableHead extends PureComponent {
+ render() {
+ const { prefix, momentLocale } = this.props;
+ const firstDayOfWeek = momentLocale.firstDayOfWeek();
+ const weekdaysShort = momentLocale.weekdaysShort();
+
+ const elements = [];
+ for (let i = 0; i < DAYS_OF_WEEK; i++) {
+ const index = (firstDayOfWeek + i) % DAYS_OF_WEEK;
+ elements.push(
+
+ {weekdaysShort[index]}
+
+ );
+ }
+
+ return (
+
+ {elements}
+
+ );
+ }
+}
+
+export default DateTableHead;
diff --git a/components/calendar/table/date-table.jsx b/components/calendar/table/date-table.jsx
deleted file mode 100644
index f33d4f56ed..0000000000
--- a/components/calendar/table/date-table.jsx
+++ /dev/null
@@ -1,156 +0,0 @@
-import React, { PureComponent } from 'react';
-import classNames from 'classnames';
-import DateTableHead from './date-table-head';
-import { isDisabledDate, DAYS_OF_WEEK, CALENDAR_TABLE_COL_COUNT, CALENDAR_TABLE_ROW_COUNT } from '../utils';
-
-function isSameDay(a, b) {
- return a && b && a.isSame(b, 'day');
-}
-
-function isRangeDate(date, startDate, endDate) {
- return (
- date.format('L') !== startDate.format('L') &&
- date.format('L') !== endDate.format('L') &&
- date.valueOf() > startDate.valueOf() &&
- date.valueOf() < endDate.valueOf()
- );
-}
-
-function isLastMonthDate(date, target) {
- if (date.year() < target.year()) {
- return 1;
- }
- return date.year() === target.year() && date.month() < target.month();
-}
-
-function isNextMonthDate(date, target) {
- if (date.year() > target.year()) {
- return 1;
- }
- return date.year() === target.year() && date.month() > target.month();
-}
-
-class DateTable extends PureComponent {
- render() {
- const {
- prefix,
- visibleMonth,
- showOtherMonth,
- endValue,
- format,
- today,
- momentLocale,
- dateCellRender,
- disabledDate,
- onSelectDate,
- } = this.props;
- const startValue = this.props.startValue || this.props.value;
-
- const firstDayOfMonth = visibleMonth.clone().startOf('month'); // 该月的 1 号
- const firstDayOfMonthInWeek = firstDayOfMonth.day(); // 星期几
-
- const firstDayOfWeek = momentLocale.firstDayOfWeek();
-
- const datesOfLastMonthCount = (firstDayOfMonthInWeek + DAYS_OF_WEEK - firstDayOfWeek) % DAYS_OF_WEEK;
-
- const lastMonthDate = firstDayOfMonth.clone();
- lastMonthDate.add(0 - datesOfLastMonthCount, 'days');
-
- let counter = 0;
- let currentDate;
- const dateList = [];
- for (let i = 0; i < CALENDAR_TABLE_ROW_COUNT; i++) {
- for (let j = 0; j < CALENDAR_TABLE_COL_COUNT; j++) {
- currentDate = lastMonthDate;
- if (counter) {
- currentDate = currentDate.clone();
- currentDate.add(counter, 'days');
- }
- dateList.push(currentDate);
- counter++;
- }
- }
- counter = 0; // reset counter
- const monthElements = [];
- for (let i = 0; i < CALENDAR_TABLE_ROW_COUNT; i++) {
- const weekElements = [];
- let firstDayOfWeekInCurrentMonth = true;
- let lastDayOfWeekInCurrentMonth = true;
- for (let j = 0; j < CALENDAR_TABLE_COL_COUNT; j++) {
- currentDate = dateList[counter];
- if (j === 0) {
- // currentDate 的month 是否等于当前月 firstDayOfMonth
- firstDayOfWeekInCurrentMonth = currentDate.format('M') === firstDayOfMonth.format('M');
- }
- if (j === CALENDAR_TABLE_COL_COUNT - 1) {
- // currentDate 的month 是否等于当前月 firstDayOfMonth
- lastDayOfWeekInCurrentMonth = currentDate.format('M') === firstDayOfMonth.format('M');
- }
- const isLastMonth = isLastMonthDate(currentDate, visibleMonth);
- const isNextMonth = isNextMonthDate(currentDate, visibleMonth);
- const isCurrentMonth = !isLastMonth && !isNextMonth;
-
- const isDisabled = isDisabledDate(currentDate, disabledDate, 'date');
- const isToday = !isDisabled && isSameDay(currentDate, today) && isCurrentMonth;
- const isSelected =
- !isDisabled &&
- (isSameDay(currentDate, startValue) || isSameDay(currentDate, endValue)) &&
- isCurrentMonth;
- const isInRange =
- !isDisabled &&
- startValue &&
- endValue &&
- isRangeDate(currentDate, startValue, endValue) &&
- isCurrentMonth;
-
- const cellContent = !showOtherMonth && !isCurrentMonth ? null : dateCellRender(currentDate);
-
- const elementCls = classNames({
- [`${prefix}calendar-cell`]: true,
- [`${prefix}calendar-cell-prev-month`]: isLastMonth,
- [`${prefix}calendar-cell-next-month`]: isNextMonth,
- [`${prefix}calendar-cell-current`]: isToday,
- [`${prefix}inrange`]: isInRange,
- [`${prefix}selected`]: isSelected,
- [`${prefix}disabled`]: cellContent && isDisabled,
- });
-
- weekElements.push(
-
- {cellContent}
-
- );
- counter++;
- }
-
- if (!showOtherMonth && !lastDayOfWeekInCurrentMonth && !firstDayOfWeekInCurrentMonth) {
- break;
- }
-
- monthElements.push(
-
- {weekElements}
-
- );
- }
-
- return (
-
-
-
- {monthElements}
-
-
- );
- }
-}
-
-export default DateTable;
diff --git a/components/calendar/table/date-table.tsx b/components/calendar/table/date-table.tsx
new file mode 100644
index 0000000000..9facbcc315
--- /dev/null
+++ b/components/calendar/table/date-table.tsx
@@ -0,0 +1,165 @@
+import React, { PureComponent } from 'react';
+import classNames from 'classnames';
+import { type Moment } from 'moment';
+import DateTableHead from './date-table-head';
+import {
+ isDisabledDate,
+ DAYS_OF_WEEK,
+ CALENDAR_TABLE_COL_COUNT,
+ CALENDAR_TABLE_ROW_COUNT,
+} from '../utils';
+import type { DateTableProps } from '../types';
+
+function isSameDay(a: Moment | null | undefined, b: Moment | null | undefined) {
+ return a && b && a.isSame(b, 'day');
+}
+
+function isRangeDate(date: Moment, startDate: Moment, endDate: Moment) {
+ return (
+ date.format('L') !== startDate.format('L') &&
+ date.format('L') !== endDate.format('L') &&
+ date.valueOf() > startDate.valueOf() &&
+ date.valueOf() < endDate.valueOf()
+ );
+}
+
+function isLastMonthDate(date: Moment, target: Moment) {
+ if (date.year() < target.year()) {
+ return 1;
+ }
+ return date.year() === target.year() && date.month() < target.month();
+}
+
+function isNextMonthDate(date: Moment, target: Moment) {
+ if (date.year() > target.year()) {
+ return 1;
+ }
+ return date.year() === target.year() && date.month() > target.month();
+}
+
+class DateTable extends PureComponent {
+ render() {
+ const {
+ prefix,
+ visibleMonth,
+ showOtherMonth,
+ endValue,
+ format,
+ today,
+ momentLocale,
+ dateCellRender,
+ disabledDate,
+ onSelectDate,
+ } = this.props;
+ const startValue = this.props.startValue || this.props.value;
+
+ const firstDayOfMonth = visibleMonth.clone().startOf('month'); // 该月的 1 号
+ const firstDayOfMonthInWeek = firstDayOfMonth.day(); // 星期几
+
+ const firstDayOfWeek = momentLocale.firstDayOfWeek();
+
+ const datesOfLastMonthCount =
+ (firstDayOfMonthInWeek + DAYS_OF_WEEK - firstDayOfWeek) % DAYS_OF_WEEK;
+
+ const lastMonthDate = firstDayOfMonth.clone();
+ lastMonthDate.add(0 - datesOfLastMonthCount, 'days');
+
+ let counter = 0;
+ let currentDate;
+ const dateList = [];
+ for (let i = 0; i < CALENDAR_TABLE_ROW_COUNT; i++) {
+ for (let j = 0; j < CALENDAR_TABLE_COL_COUNT; j++) {
+ currentDate = lastMonthDate;
+ if (counter) {
+ currentDate = currentDate.clone();
+ currentDate.add(counter, 'days');
+ }
+ dateList.push(currentDate);
+ counter++;
+ }
+ }
+ counter = 0; // reset counter
+ const monthElements = [];
+ for (let i = 0; i < CALENDAR_TABLE_ROW_COUNT; i++) {
+ const weekElements = [];
+ let firstDayOfWeekInCurrentMonth = true;
+ let lastDayOfWeekInCurrentMonth = true;
+ for (let j = 0; j < CALENDAR_TABLE_COL_COUNT; j++) {
+ currentDate = dateList[counter];
+ if (j === 0) {
+ // currentDate 的month 是否等于当前月 firstDayOfMonth
+ firstDayOfWeekInCurrentMonth =
+ currentDate.format('M') === firstDayOfMonth.format('M');
+ }
+ if (j === CALENDAR_TABLE_COL_COUNT - 1) {
+ // currentDate 的month 是否等于当前月 firstDayOfMonth
+ lastDayOfWeekInCurrentMonth =
+ currentDate.format('M') === firstDayOfMonth.format('M');
+ }
+ const isLastMonth = isLastMonthDate(currentDate, visibleMonth);
+ const isNextMonth = isNextMonthDate(currentDate, visibleMonth);
+ const isCurrentMonth = !isLastMonth && !isNextMonth;
+
+ const isDisabled = isDisabledDate(currentDate, disabledDate, 'date');
+ const isToday = !isDisabled && isSameDay(currentDate, today) && isCurrentMonth;
+ const isSelected =
+ !isDisabled &&
+ (isSameDay(currentDate, startValue) || isSameDay(currentDate, endValue)) &&
+ isCurrentMonth;
+ const isInRange =
+ !isDisabled &&
+ startValue &&
+ endValue &&
+ isRangeDate(currentDate, startValue, endValue) &&
+ isCurrentMonth;
+
+ const cellContent =
+ !showOtherMonth && !isCurrentMonth ? null : dateCellRender(currentDate);
+
+ const elementCls = classNames({
+ [`${prefix}calendar-cell`]: true,
+ [`${prefix}calendar-cell-prev-month`]: isLastMonth,
+ [`${prefix}calendar-cell-next-month`]: isNextMonth,
+ [`${prefix}calendar-cell-current`]: isToday,
+ [`${prefix}inrange`]: isInRange,
+ [`${prefix}selected`]: isSelected,
+ [`${prefix}disabled`]: cellContent && isDisabled,
+ });
+
+ weekElements.push(
+
+ {cellContent}
+
+ );
+ counter++;
+ }
+
+ if (!showOtherMonth && !lastDayOfWeekInCurrentMonth && !firstDayOfWeekInCurrentMonth) {
+ break;
+ }
+
+ monthElements.push(
+
+ {weekElements}
+
+ );
+ }
+
+ return (
+
+ );
+ }
+}
+
+export default DateTable;
diff --git a/components/calendar/table/month-table.jsx b/components/calendar/table/month-table.jsx
deleted file mode 100644
index d2f58bef96..0000000000
--- a/components/calendar/table/month-table.jsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import React, { PureComponent } from 'react';
-import classnames from 'classnames';
-import { isDisabledDate, MONTH_TABLE_ROW_COUNT, MONTH_TABLE_COL_COUNT } from '../utils';
-
-function isSameMonth(currentDate, selectedDate) {
- return selectedDate && currentDate.year() === selectedDate.year() && currentDate.month() === selectedDate.month();
-}
-
-class MonthTable extends PureComponent {
- onMonthCellClick(date) {
- this.props.onSelectMonth(date, 'date');
- }
-
- render() {
- const { prefix, value, visibleMonth, disabledDate, today, momentLocale, monthCellRender } = this.props;
-
- const monthLocale = momentLocale.monthsShort();
-
- let counter = 0;
- const monthList = [];
- for (let i = 0; i < MONTH_TABLE_ROW_COUNT; i++) {
- const rowList = [];
- for (let j = 0; j < MONTH_TABLE_COL_COUNT; j++) {
- const monthDate = visibleMonth.clone().month(counter);
- const isDisabled = isDisabledDate(monthDate, disabledDate, 'month');
- const isSelected = isSameMonth(monthDate, value);
- const isThisMonth = isSameMonth(monthDate, today);
- const elementCls = classnames({
- [`${prefix}calendar-cell`]: true,
- [`${prefix}calendar-cell-current`]: isThisMonth,
- [`${prefix}selected`]: isSelected,
- [`${prefix}disabled`]: isDisabled,
- });
- const localedMonth = monthLocale[counter];
- const monthCellContent = monthCellRender ? monthCellRender(monthDate) : localedMonth;
- rowList.push(
-
- {monthCellContent}
-
- );
- counter++;
- }
- monthList.push(
-
- {rowList}
-
- );
- }
-
- return (
-
- );
- }
-}
-
-export default MonthTable;
diff --git a/components/calendar/table/month-table.tsx b/components/calendar/table/month-table.tsx
new file mode 100644
index 0000000000..0cdd3c37dd
--- /dev/null
+++ b/components/calendar/table/month-table.tsx
@@ -0,0 +1,79 @@
+import React, { PureComponent } from 'react';
+import classnames from 'classnames';
+import { type Moment } from 'moment';
+import { isDisabledDate, MONTH_TABLE_ROW_COUNT, MONTH_TABLE_COL_COUNT } from '../utils';
+import { type MonthTableProps } from '../types';
+
+function isSameMonth(currentDate: Moment, selectedDate: Moment | null | undefined) {
+ return (
+ selectedDate &&
+ currentDate.year() === selectedDate.year() &&
+ currentDate.month() === selectedDate.month()
+ );
+}
+
+class MonthTable extends PureComponent {
+ onMonthCellClick(date: Moment) {
+ this.props.onSelectMonth(date, 'date');
+ }
+
+ render() {
+ const { prefix, value, visibleMonth, disabledDate, today, momentLocale, monthCellRender } =
+ this.props;
+
+ const monthLocale = momentLocale.monthsShort();
+
+ let counter = 0;
+ const monthList = [];
+ for (let i = 0; i < MONTH_TABLE_ROW_COUNT; i++) {
+ const rowList = [];
+ for (let j = 0; j < MONTH_TABLE_COL_COUNT; j++) {
+ const monthDate = visibleMonth.clone().month(counter);
+ const isDisabled = isDisabledDate(monthDate, disabledDate, 'month');
+ const isSelected = isSameMonth(monthDate, value);
+ const isThisMonth = isSameMonth(monthDate, today);
+ const elementCls = classnames({
+ [`${prefix}calendar-cell`]: true,
+ [`${prefix}calendar-cell-current`]: isThisMonth,
+ [`${prefix}selected`]: isSelected,
+ [`${prefix}disabled`]: isDisabled,
+ });
+ const localedMonth = monthLocale[counter];
+ const monthCellContent = monthCellRender
+ ? monthCellRender(monthDate)
+ : localedMonth;
+ rowList.push(
+
+ {monthCellContent}
+
+ );
+ counter++;
+ }
+ monthList.push(
+
+ {rowList}
+
+ );
+ }
+
+ return (
+
+ );
+ }
+}
+
+export default MonthTable;
diff --git a/components/calendar/table/year-table.jsx b/components/calendar/table/year-table.jsx
deleted file mode 100644
index f28065979f..0000000000
--- a/components/calendar/table/year-table.jsx
+++ /dev/null
@@ -1,101 +0,0 @@
-import React from 'react';
-import classnames from 'classnames';
-import Icon from '../../icon';
-import { isDisabledDate, YEAR_TABLE_COL_COUNT, YEAR_TABLE_ROW_COUNT } from '../utils';
-
-class YearTable extends React.PureComponent {
- onYearCellClick(date) {
- this.props.onSelectYear(date, 'month');
- }
-
- render() {
- const {
- prefix,
- value,
- today,
- visibleMonth,
- locale,
- disabledDate,
- goPrevDecade,
- goNextDecade,
- yearCellRender,
- } = this.props;
- const currentYear = today.year();
- const selectedYear = value ? value.year() : null;
- const visibleYear = visibleMonth.year();
- const startYear = Math.floor(visibleYear / 10) * 10;
-
- const yearElements = [];
- let counter = 0;
-
- const lastRowIndex = YEAR_TABLE_ROW_COUNT - 1;
- const lastColIndex = YEAR_TABLE_COL_COUNT - 1;
-
- for (let i = 0; i < YEAR_TABLE_ROW_COUNT; i++) {
- const rowElements = [];
- for (let j = 0; j < YEAR_TABLE_COL_COUNT; j++) {
- let content;
- let year;
- let isDisabled = false;
- let onClick;
- let title;
-
- if (i === 0 && j === 0) {
- title = locale.prevDecade;
- onClick = goPrevDecade;
- content = ;
- } else if (i === lastRowIndex && j === lastColIndex) {
- title = locale.nextDecade;
- onClick = goNextDecade;
- content = ;
- } else {
- year = startYear + counter++;
- title = year;
- const yearDate = visibleMonth.clone().year(year);
- isDisabled = isDisabledDate(yearDate, disabledDate, 'year');
-
- !isDisabled && (onClick = this.onYearCellClick.bind(this, yearDate));
-
- content = yearCellRender ? yearCellRender(yearDate) : year;
- }
-
- const isSelected = year === selectedYear;
-
- const classNames = classnames({
- [`${prefix}calendar-cell`]: true,
- [`${prefix}calendar-cell-current`]: year === currentYear,
- [`${prefix}selected`]: isSelected,
- [`${prefix}disabled`]: isDisabled,
- });
-
- rowElements.push(
-
-
- {content}
-
-
- );
- }
- yearElements.push(
-
- {rowElements}
-
- );
- }
- return (
-
- );
- }
-}
-
-export default YearTable;
diff --git a/components/calendar/table/year-table.tsx b/components/calendar/table/year-table.tsx
new file mode 100644
index 0000000000..bc3cc2b34d
--- /dev/null
+++ b/components/calendar/table/year-table.tsx
@@ -0,0 +1,104 @@
+import React from 'react';
+import classnames from 'classnames';
+import { type Moment } from 'moment';
+import Icon from '../../icon';
+import { isDisabledDate, YEAR_TABLE_COL_COUNT, YEAR_TABLE_ROW_COUNT } from '../utils';
+import { type YearTableProps } from '../types';
+
+class YearTable extends React.PureComponent {
+ onYearCellClick(date: Moment) {
+ this.props.onSelectYear(date, 'month');
+ }
+
+ render() {
+ const {
+ prefix,
+ value,
+ today,
+ visibleMonth,
+ locale,
+ disabledDate,
+ goPrevDecade,
+ goNextDecade,
+ yearCellRender,
+ } = this.props;
+ const currentYear = today.year();
+ const selectedYear = value ? value.year() : null;
+ const visibleYear = visibleMonth.year();
+ const startYear = Math.floor(visibleYear / 10) * 10;
+
+ const yearElements = [];
+ let counter = 0;
+
+ const lastRowIndex = YEAR_TABLE_ROW_COUNT - 1;
+ const lastColIndex = YEAR_TABLE_COL_COUNT - 1;
+
+ for (let i = 0; i < YEAR_TABLE_ROW_COUNT; i++) {
+ const rowElements = [];
+ for (let j = 0; j < YEAR_TABLE_COL_COUNT; j++) {
+ let content;
+ let year;
+ let isDisabled = false;
+ let onClick;
+ let title;
+
+ if (i === 0 && j === 0) {
+ title = locale.prevDecade;
+ onClick = goPrevDecade;
+ content = ;
+ } else if (i === lastRowIndex && j === lastColIndex) {
+ title = locale.nextDecade;
+ onClick = goNextDecade;
+ content = ;
+ } else {
+ year = startYear + counter++;
+ title = year;
+ const yearDate = visibleMonth.clone().year(year);
+ isDisabled = isDisabledDate(yearDate, disabledDate, 'year');
+
+ !isDisabled && (onClick = this.onYearCellClick.bind(this, yearDate));
+
+ content = yearCellRender ? yearCellRender(yearDate) : year;
+ }
+
+ const isSelected = year === selectedYear;
+
+ const classNames = classnames({
+ [`${prefix}calendar-cell`]: true,
+ [`${prefix}calendar-cell-current`]: year === currentYear,
+ [`${prefix}selected`]: isSelected,
+ [`${prefix}disabled`]: isDisabled,
+ });
+
+ rowElements.push(
+
+
+ {content}
+
+
+ );
+ }
+ yearElements.push(
+
+ {rowElements}
+
+ );
+ }
+ return (
+
+ );
+ }
+}
+
+export default YearTable;
diff --git a/components/calendar/types.ts b/components/calendar/types.ts
new file mode 100644
index 0000000000..3eda3a1ad1
--- /dev/null
+++ b/components/calendar/types.ts
@@ -0,0 +1,408 @@
+import type React from 'react';
+import type { Moment, MomentInput, Locale as MomentLocale } from 'moment';
+import type { CommonProps } from '../util';
+import { type Locale } from '../locale/types';
+
+interface HTMLAttributesWeak
+ extends Omit, 'defaultValue' | 'select' | 'onSelect'> {}
+
+/**
+ * @api
+ */
+export type CalendarMode = 'date' | 'month' | 'year';
+
+/**
+ * @api
+ * @order 1
+ */
+export interface CalendarProps
+ extends Omit,
+ Omit {
+ /**
+ * 默认选中的日期(moment 对象)
+ * @en Default selected date (moment object)
+ */
+ defaultValue?: Moment | null;
+
+ /**
+ * 展现形态
+ * @en Display shape
+ * @defaultValue 'fullscreen'
+ */
+ shape?: 'card' | 'fullscreen' | 'panel';
+
+ /**
+ * 选中的日期值 (moment 对象)
+ * @en Selected date value (moment object)
+ */
+ value?: Moment | null;
+
+ /**
+ * 面板模式
+ * @en Panel mode
+ */
+ mode?: CalendarMode;
+
+ /**
+ * 是否展示非本月的日期
+ * @en Whether to show dates outside the current month
+ * @defaultValue true
+ */
+ showOtherMonth?: boolean;
+
+ /**
+ * 默认展示的月份
+ * @en Default displayed month
+ */
+ defaultVisibleMonth?: () => Moment | null;
+ /**
+ * 面板模式变化时的回调
+ * @en Callback when the panel mode changes
+ * @param mode - 对应面板模式 date, month, year
+ */
+ onModeChange?: (mode: CalendarMode) => void;
+
+ /**
+ * 选择日期单元格时的回调
+ * @en Callback when selecting a date cell
+ */
+ onSelect?: (value: Moment) => void;
+
+ /**
+ * 展现的月份变化时的回调
+ * @en Callback when the displayed month changes
+ */
+ onVisibleMonthChange?: (value: Moment, reason: VisibleMonthChangeType) => void;
+
+ /**
+ * 自定义日期渲染函数
+ * @en Customize date rendering function
+ * @defaultValue value =\> value.date()
+ */
+ dateCellRender?: (value: Moment) => React.ReactNode;
+
+ /**
+ * 自定义月份渲染函数
+ * @en Customize month rendering function
+ */
+ monthCellRender?: (calendarDate: Moment) => React.ReactNode;
+
+ /**
+ * 兼容 0.x yearCellRender
+ * @deprecated use monthCellRender/dateCellRender instead
+ * @skip
+ */
+ yearCellRender?: (calendarDate: Moment) => React.ReactNode;
+
+ /**
+ * 不可选择的日期
+ * @en Disabled date
+ */
+ disabledDate?: (calendarDate: Moment, view: CalendarMode) => boolean;
+
+ /**
+ * 面板可变化的模式列表,仅初始化时接收一次
+ * @en Panel mode list that can be changed, only received once at initialization
+ * @defaultValue ['date', 'month', 'year']
+ */
+ modes?: CalendarMode[];
+ /**
+ * 禁用更改面板模式,采用 dropdown 的方式切换显示日期 (暂不正式对外透出)
+ * @en Disable changing panel mode, use the dropdown method to switch displayed dates
+ * @defaultValue false
+ * @skip
+ */
+ disableChangeMode?: boolean;
+ /**
+ * 日期值的格式(用于日期 title 显示的格式)
+ * @en Date value format(for date title display format)
+ * @defaultValue 'YYYY-MM-DD'
+ */
+ format?: string;
+ /**
+ * 多语言文案
+ * @en International text
+ * @skip
+ */
+ locale?: Locale['Calendar'];
+ /**
+ * 年份范围,[START_YEAR, END_YEAR] (只在 shape 为‘card’, 'fullscreen' 下生效)
+ * @en Year range, [START_YEAR, END_YEAR] (only effective when shape is 'card', 'fullscreen')
+ */
+ yearRange?: [start: number, end: number];
+
+ /**
+ * @deprecated use disabledDate instead
+ * @skip
+ */
+ disabledMonth?: unknown;
+ /**
+ * @deprecated use disabledDate instead
+ * @skip
+ */
+ disabledYear?: unknown;
+
+ /**
+ * @deprecated use shape instead
+ * @skip
+ */
+ type?: CalendarProps['shape'];
+
+ /**
+ * @deprecated use onSelect instead
+ * @skip
+ */
+ onChange?: (options: { mode: CalendarMode; value: Moment }) => void;
+
+ /**
+ * @deprecated use defaultVisibleMonth instead
+ * @skip
+ */
+ base?: MomentInput;
+}
+/**
+ * @api Calendar.RangeCalendar
+ * @order 2
+ */
+export interface RangeCalendarProps extends HTMLAttributesWeak, Omit {
+ /**
+ * 多语言文案
+ * @skip
+ */
+ locale?: Locale['Calendar'];
+ /**
+ * 面板模式
+ * @en Panel mode
+ * @defaultValue 'date'
+ */
+ mode?: CalendarMode;
+ /**
+ * 禁用更改面板模式,采用 dropdown 的方式切换显示日期 (暂不正式对外透出)
+ * @en Disable changing panel mode, use the dropdown method to switch displayed dates
+ * @defaultValue false
+ * @skip
+ */
+ disableChangeMode?: boolean;
+ /**
+ * 日期值的格式(用于日期 title 显示的格式)
+ * @en Date value format(for date title display format)
+ * @defaultValue 'YYYY-MM-DD'
+ */
+ format?: string;
+ /**
+ * 自定义日期渲染函数
+ * @en Customize date rendering function
+ * @defaultValue value =\> value.date()
+ */
+ dateCellRender?: (value: Moment) => React.ReactNode;
+ /**
+ * 选择日期单元格时的回调
+ * @en Callback when selecting a date cell
+ */
+ onSelect?: (value: Moment) => void;
+ /**
+ * 展现的月份变化时的回调
+ * @en Callback when the displayed month changes
+ */
+ onVisibleMonthChange?: (value: Moment, reason: VisibleMonthChangeType) => void;
+ /**
+ * 是否展示非本月的日期
+ * @en Whether to show dates outside the current month
+ * @defaultValue true
+ */
+ showOtherMonth?: boolean;
+ /**
+ * 开始日期(moment 对象)
+ * @en Start date (moment object)
+ */
+ startValue?: Moment | null;
+ /**
+ * 结束日期(moment 对象)
+ * @en End date (moment object)
+ */
+ endValue?: Moment | null;
+ /**
+ * 默认的开始日期(moment 对象)
+ * @en Default start date (moment object)
+ */
+ defaultStartValue?: Moment | null;
+ /**
+ * 默认的结束日期(moment 对象)
+ * @en Default end date (moment object)
+ */
+ defaultEndValue?: Moment | null;
+ /**
+ * 自定义月份渲染函数
+ * @en Customize month rendering function
+ */
+ monthCellRender?: (calendarDate: Moment) => React.ReactNode;
+ /**
+ * 默认展示的月份
+ * @en Default displayed month
+ */
+ defaultVisibleMonth?: () => Moment | null;
+ /**
+ * 兼容 0.x yearCellRender
+ * @deprecated use monthCellRender/dateCellRender instead
+ * @skip
+ */
+ yearCellRender?: (calendarDate: Moment) => React.ReactNode;
+ /**
+ * 不可选择的日期
+ * @en Disabled date
+ */
+ disabledDate?: (calendarDate: Moment, view: CalendarMode) => boolean;
+ /**
+ * 展现形态
+ * @en Display shape
+ */
+ shape?: 'card' | 'fullscreen' | 'panel';
+ /**
+ * 年份范围,[START_YEAR, END_YEAR] (只在 shape 为‘card’, 'fullscreen' 下生效)
+ * @en Year range, [START_YEAR, END_YEAR] (only effective when shape is 'card', 'fullscreen')
+ */
+ yearRange?: [number, number];
+}
+
+export interface MomentLocaleLike
+ extends Omit<
+ MomentLocale,
+ 'monthsShort' | 'months' | 'firstDayOfWeek' | 'weekdays' | 'weekdaysShort' | 'weekdaysMin'
+ > {
+ monthsShort: () => string[];
+ months: () => string[];
+ firstDayOfWeek: () => number;
+ weekdays: () => string[];
+ weekdaysShort: () => string[];
+ weekdaysMin: () => string[];
+}
+
+interface CommonTableProps {
+ visibleMonth: Moment;
+ today: Moment;
+ momentLocale: MomentLocaleLike;
+}
+
+export interface DateTableProps
+ extends Pick<
+ Required,
+ 'dateCellRender' | 'showOtherMonth' | 'format' | 'value' | 'locale'
+ >,
+ Pick,
+ Pick,
+ Omit,
+ CommonTableProps {
+ onSelectDate: (value: Moment, e: React.MouseEvent) => void;
+}
+
+export interface DateTableHeadProps extends CommonProps {
+ momentLocale: MomentLocaleLike;
+}
+
+export interface MonthTableProps
+ extends Pick, 'value' | 'locale'>,
+ Pick,
+ Omit,
+ CommonTableProps {
+ onSelectMonth: (value: Moment, mode: 'date') => void;
+}
+
+export interface YearTableProps
+ extends Pick, 'value' | 'locale'>,
+ Pick,
+ Omit,
+ CommonTableProps {
+ onSelectYear: (value: Moment, mode: 'month') => void;
+ goPrevDecade: () => void;
+ goNextDecade: () => void;
+}
+
+export interface CardHeaderProps
+ extends Pick, 'yearRange' | 'locale' | 'mode' | 'showOtherMonth'>,
+ Omit {
+ yearRangeOffset?: number;
+ momentLocale: MomentLocaleLike;
+ changeMode: (mode: CalendarMode) => void;
+ visibleMonth: Moment;
+ changeVisibleMonth: (value: Moment, type: VisibleMonthChangeType) => void;
+}
+
+export interface RangePanelHeaderProps
+ extends Pick, 'locale' | 'disableChangeMode'>,
+ Pick,
+ Omit {
+ startVisibleMonth: Moment;
+ endVisibleMonth: Moment;
+ yearRangeOffset?: number;
+ momentLocale: MomentLocaleLike;
+ changeMode: (mode: CalendarMode, type: 'start' | 'end') => void;
+ goNextMonth: () => void;
+ goNextYear: () => void;
+ goPrevMonth: () => void;
+ goPrevYear: () => void;
+ changeVisibleMonth: (value: Moment, type: VisibleMonthChangeType) => void;
+}
+
+export interface DatePanelHeaderProps
+ extends Pick<
+ Required,
+ 'locale' | 'disableChangeMode' | 'yearRange' | 'showOtherMonth'
+ >,
+ Omit {
+ goNextMonth: () => void;
+ goNextYear: () => void;
+ goPrevMonth: () => void;
+ goPrevYear: () => void;
+ changeMode: (mode: CalendarMode, type: 'start' | 'end') => void;
+ momentLocale: MomentLocaleLike;
+ visibleMonth: Moment;
+ yearRangeOffset: number;
+ changeVisibleMonth: (value: Moment, type: VisibleMonthChangeType) => void;
+}
+
+export interface SelectMenuProps extends CommonProps {
+ dataSource: { value: React.Key; label: React.ReactNode }[];
+ onChange: (value: number) => void;
+ value: string | number;
+ className?: string;
+}
+
+export interface MonthPanelHeaderProps
+ extends Pick, 'locale'>,
+ Omit {
+ goNextYear: () => void;
+ goPrevYear: () => void;
+ changeMode: (mode: CalendarMode) => void;
+ visibleMonth: Moment;
+}
+
+export interface YearPanelHeaderProps
+ extends Pick, 'locale'>,
+ Omit {
+ goPrevDecade: () => void;
+ goNextDecade: () => void;
+ visibleMonth: Moment;
+}
+
+/**
+ * @api
+ */
+export type VisibleMonthChangeType = 'cellClick' | 'buttonClick' | 'yearSelect' | 'monthSelect';
+
+export interface CalendarState {
+ value: Moment | null;
+ visibleMonth: Moment;
+ mode: CalendarMode;
+ MODES: CalendarMode[];
+}
+
+export interface RangeCalendarState {
+ startValue: Moment | null;
+ startVisibleMonth: Moment;
+ endValue: Moment | null;
+ prevMode?: CalendarMode;
+ mode?: CalendarMode;
+ lastMode?: CalendarMode;
+ activePanel?: 'start' | 'end';
+ lastPanelType: 'start' | 'end';
+}
diff --git a/components/calendar/utils/index.js b/components/calendar/utils/index.js
deleted file mode 100644
index 29790cf807..0000000000
--- a/components/calendar/utils/index.js
+++ /dev/null
@@ -1,115 +0,0 @@
-import moment from 'moment';
-
-export const DAYS_OF_WEEK = 7;
-
-export const CALENDAR_TABLE_COL_COUNT = 7;
-
-export const CALENDAR_TABLE_ROW_COUNT = 6;
-
-export const MONTH_TABLE_ROW_COUNT = 4;
-
-export const MONTH_TABLE_COL_COUNT = 3;
-
-export const YEAR_TABLE_ROW_COUNT = 4;
-
-export const YEAR_TABLE_COL_COUNT = 3;
-
-export const CALENDAR_MODE_YEAR = 'year';
-
-export const CALENDAR_MODE_MONTH = 'month';
-
-export const CALENDAR_MODE_DATE = 'date';
-
-export const CALENDAR_MODES = [CALENDAR_MODE_DATE, CALENDAR_MODE_MONTH, CALENDAR_MODE_YEAR];
-
-export function isDisabledDate(date, fn, view) {
- if (typeof fn === 'function' && fn(date, view)) {
- return true;
- }
- return false;
-}
-
-export function checkMomentObj(props, propName, componentName) {
- if (props[propName] && !moment.isMoment(props[propName])) {
- return new Error(`Invalid prop ${propName} supplied to ${componentName}. Required a moment object`);
- }
-}
-
-export function formatDateValue(value, reservedValue = null) {
- if (value && moment.isMoment(value)) {
- return value;
- }
- return reservedValue;
-}
-
-export function getVisibleMonth(defaultVisibleMonth, value) {
- let getVM = defaultVisibleMonth;
- if (typeof getVM !== 'function' || !moment.isMoment(getVM())) {
- getVM = () => {
- if (value) {
- return value;
- }
- return moment();
- };
- }
- return getVM();
-}
-
-export function isSameYearMonth(dateA, dateB) {
- return dateA.month() === dateB.month() && dateA.year() === dateB.year();
-}
-
-export function preFormatDateValue(value, format) {
- const val = typeof value === 'string' ? moment(value, format, false) : value;
- if (val && moment.isMoment(val) && val.isValid()) {
- return val;
- }
-
- return null;
-}
-
-export function getLocaleData(
- { months, shortMonths, firstDayOfWeek, weekdays, shortWeekdays, veryShortWeekdays },
- localeData
-) {
- return {
- ...localeData,
- monthsShort: () => shortMonths || localeData.monthsShort(),
- months: () => months || localeData.months(),
- firstDayOfWeek: () => firstDayOfWeek || localeData.firstDayOfWeek(),
- weekdays: () => weekdays || localeData.weekdays,
- weekdaysShort: () => shortWeekdays || localeData.weekdaysShort(),
- weekdaysMin: () => veryShortWeekdays || localeData.weekdaysMin(),
- };
-}
-
-/* istanbul ignore next */
-export function getYears(yearRange, yearRangeOffset, year) {
- const options = [];
- let [startYear, endYear] = yearRange;
- if (!startYear || !endYear) {
- startYear = year - yearRangeOffset;
- endYear = year + yearRangeOffset;
- }
-
- for (let i = startYear; i <= endYear; i++) {
- options.push({
- label: i,
- value: i,
- });
- }
- return options;
-}
-
-/* istanbul ignore next */
-export function getMonths(momentLocale) {
- const localeMonths = momentLocale.monthsShort();
- const options = [];
- for (let i = 0; i < 12; i++) {
- options.push({
- value: i,
- label: localeMonths[i],
- });
- }
- return options;
-}
diff --git a/components/calendar/utils/index.ts b/components/calendar/utils/index.ts
new file mode 100644
index 0000000000..4a77895fb1
--- /dev/null
+++ b/components/calendar/utils/index.ts
@@ -0,0 +1,148 @@
+import moment, {
+ type MomentInput,
+ type Moment,
+ type MomentFormatSpecification,
+ type Locale as MomentLocale,
+} from 'moment';
+import { type CalendarMode, type MomentLocaleLike } from '../types';
+
+export const DAYS_OF_WEEK = 7;
+
+export const CALENDAR_TABLE_COL_COUNT = 7;
+
+export const CALENDAR_TABLE_ROW_COUNT = 6;
+
+export const MONTH_TABLE_ROW_COUNT = 4;
+
+export const MONTH_TABLE_COL_COUNT = 3;
+
+export const YEAR_TABLE_ROW_COUNT = 4;
+
+export const YEAR_TABLE_COL_COUNT = 3;
+
+export const CALENDAR_MODE_YEAR = 'year';
+
+export const CALENDAR_MODE_MONTH = 'month';
+
+export const CALENDAR_MODE_DATE = 'date';
+
+export const CALENDAR_MODES = [
+ CALENDAR_MODE_DATE,
+ CALENDAR_MODE_MONTH,
+ CALENDAR_MODE_YEAR,
+] as CalendarMode[];
+
+export function isDisabledDate(date: unknown, fn: unknown, view: unknown) {
+ if (typeof fn === 'function' && fn(date, view)) {
+ return true;
+ }
+ return false;
+}
+
+export function checkMomentObj(
+ props: Record,
+ propName: string,
+ componentName: string
+) {
+ if (props[propName] && !moment.isMoment(props[propName])) {
+ return new Error(
+ `Invalid prop ${propName} supplied to ${componentName}. Required a moment object`
+ );
+ }
+}
+
+export function formatDateValue(value: unknown, reservedValue = null) {
+ if (value && moment.isMoment(value)) {
+ return value;
+ }
+ return reservedValue;
+}
+
+export function getVisibleMonth(defaultVisibleMonth: () => Moment, value: unknown): Moment;
+export function getVisibleMonth(defaultVisibleMonth: unknown, value: V): Moment | NonNullable;
+export function getVisibleMonth(
+ defaultVisibleMonth: unknown,
+ value: V
+): Moment | NonNullable {
+ let getVM = defaultVisibleMonth;
+ if (typeof getVM !== 'function' || !moment.isMoment(getVM())) {
+ getVM = () => {
+ if (value) {
+ return value;
+ }
+ return moment();
+ };
+ }
+ return (getVM as () => Moment | NonNullable)();
+}
+
+export function isSameYearMonth(dateA: Moment, dateB: Moment) {
+ return dateA.month() === dateB.month() && dateA.year() === dateB.year();
+}
+
+export function preFormatDateValue(value: MomentInput | Moment, format: MomentFormatSpecification) {
+ const val = typeof value === 'string' ? moment(value, format, false) : value;
+ if (val && moment.isMoment(val) && val.isValid()) {
+ return val;
+ }
+
+ return null;
+}
+
+export function getLocaleData(
+ {
+ months,
+ shortMonths,
+ firstDayOfWeek,
+ weekdays,
+ shortWeekdays,
+ veryShortWeekdays,
+ }: {
+ months?: string[];
+ shortMonths?: string[];
+ firstDayOfWeek?: number;
+ weekdays?: string[];
+ shortWeekdays?: string[];
+ veryShortWeekdays?: string[];
+ },
+ localeData: MomentLocale
+): MomentLocaleLike {
+ return {
+ ...localeData,
+ monthsShort: () => shortMonths || localeData.monthsShort(),
+ months: () => months || localeData.months(),
+ firstDayOfWeek: () => firstDayOfWeek || localeData.firstDayOfWeek(),
+ weekdays: () => weekdays || localeData.weekdays(),
+ weekdaysShort: () => shortWeekdays || localeData.weekdaysShort(),
+ weekdaysMin: () => veryShortWeekdays || localeData.weekdaysMin(),
+ };
+}
+
+export function getYears(yearRange: [number?, number?], yearRangeOffset: number, year: number) {
+ const options = [];
+ let [startYear, endYear] = yearRange;
+ if (!startYear || !endYear) {
+ startYear = year - yearRangeOffset;
+ endYear = year + yearRangeOffset;
+ }
+
+ for (let i = startYear; i <= endYear; i++) {
+ options.push({
+ label: i,
+ value: i,
+ });
+ }
+ return options;
+}
+
+export function getMonths(momentLocale: MomentLocaleLike) {
+ const localeMonths = momentLocale.monthsShort();
+ const options = [];
+ for (let i = 0; i < 12; i++) {
+ options.push({
+ value: i,
+ label: localeMonths[i],
+ });
+ }
+ return options;
+}
diff --git a/components/calendar2/__docs__/demo/basic/index.tsx b/components/calendar2/__docs__/demo/basic/index.tsx
index ae21123824..267d53dd93 100644
--- a/components/calendar2/__docs__/demo/basic/index.tsx
+++ b/components/calendar2/__docs__/demo/basic/index.tsx
@@ -2,10 +2,11 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { Calendar2 } from '@alifd/next';
import dayjs from 'dayjs';
+import type { CalendarProps } from '@alifd/next/types/calendar2';
-function onDateChange(value) {
+const onDateChange: CalendarProps['onSelect'] = value => {
console.log(value.format('L'));
-}
+};
ReactDOM.render(
diff --git a/components/calendar2/__docs__/demo/card/index.tsx b/components/calendar2/__docs__/demo/card/index.tsx
index 4fe1227df2..b9c1446c6b 100644
--- a/components/calendar2/__docs__/demo/card/index.tsx
+++ b/components/calendar2/__docs__/demo/card/index.tsx
@@ -1,10 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Calendar2 } from '@alifd/next';
+import type { CalendarProps } from '@alifd/next/types/calendar2';
-function onDateChange(value) {
+const onDateChange: CalendarProps['onSelect'] = value => {
console.log(value);
-}
+};
ReactDOM.render(
diff --git a/components/calendar2/__docs__/demo/custom-cell/index.tsx b/components/calendar2/__docs__/demo/custom-cell/index.tsx
index 121ebf10e7..e4b3eae730 100644
--- a/components/calendar2/__docs__/demo/custom-cell/index.tsx
+++ b/components/calendar2/__docs__/demo/custom-cell/index.tsx
@@ -2,16 +2,17 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { Calendar2 } from '@alifd/next';
import dayjs from 'dayjs';
+import type { CalendarProps } from '@alifd/next/types/calendar2';
const currentDate = dayjs();
-function dateCellRender(date) {
+const dateCellRender: CalendarProps['dateCellRender'] = date => {
const dateNum = date.date();
if (currentDate.month() !== date.month()) {
return dateNum;
}
- let eventList;
+ let eventList: { type: 'primary' | 'normal'; content: string }[] = [];
switch (dateNum) {
case 1:
eventList = [
@@ -49,9 +50,9 @@ function dateCellRender(date) {
);
-}
+};
-function monthCellRender(date) {
+const monthCellRender: CalendarProps['monthCellRender'] = date => {
if (currentDate.month() === date.month()) {
return (
@@ -61,7 +62,7 @@ function monthCellRender(date) {
);
}
return date.month();
-}
+};
ReactDOM.render(
,
diff --git a/components/calendar2/__docs__/demo/default-visible-month/index.tsx b/components/calendar2/__docs__/demo/default-visible-month/index.tsx
index a8a36dc553..c245a703ce 100644
--- a/components/calendar2/__docs__/demo/default-visible-month/index.tsx
+++ b/components/calendar2/__docs__/demo/default-visible-month/index.tsx
@@ -2,19 +2,20 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { Calendar2 } from '@alifd/next';
import dayjs from 'dayjs';
+import type { CalendarProps } from '@alifd/next/types/calendar2';
-function onSelect(value) {
+const onSelect: CalendarProps['onSelect'] = value => {
console.log(value.format('L'));
-}
+};
-function onPanelChange(value, reason) {
+const onPanelChange: CalendarProps['onPanelChange'] = (value, reason) => {
console.log('Visible month changed to %s from <%s>', value.format('YYYY-MM'), reason);
-}
+};
ReactDOM.render(
dayjs('2018-01', 'YYYY-MM', true)}
+ defaultPanelValue={dayjs('2018-01', 'YYYY-MM', true)}
onPanelChange={onPanelChange}
/>,
mountNode
diff --git a/components/calendar2/__docs__/demo/disabled/index.tsx b/components/calendar2/__docs__/demo/disabled/index.tsx
index 8fc1c6eb7f..d4c324c3b8 100644
--- a/components/calendar2/__docs__/demo/disabled/index.tsx
+++ b/components/calendar2/__docs__/demo/disabled/index.tsx
@@ -2,9 +2,10 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { Calendar2 } from '@alifd/next';
import dayjs from 'dayjs';
+import type { CalendarProps } from '@alifd/next/types/calendar2';
const currentDate = dayjs();
-const disabledDate = function (date) {
+const disabledDate: CalendarProps['disabledDate'] = function (date) {
return date.valueOf() > currentDate.valueOf();
};
diff --git a/components/calendar2/__docs__/demo/lunar/index.tsx b/components/calendar2/__docs__/demo/lunar/index.tsx
index 1f43726151..e39e86eabd 100644
--- a/components/calendar2/__docs__/demo/lunar/index.tsx
+++ b/components/calendar2/__docs__/demo/lunar/index.tsx
@@ -3,12 +3,13 @@ import ReactDOM from 'react-dom';
import { Calendar2 } from '@alifd/next';
import dayjs from 'dayjs';
import solarLunar from 'solarlunar';
+import type { CalendarProps } from '@alifd/next/types/calendar2';
-function onDateChange(value) {
+const onDateChange: CalendarProps['onSelect'] = value => {
console.log(value.format());
-}
+};
-function dateCellRender(value) {
+const dateCellRender: CalendarProps['dateCellRender'] = value => {
const solar2lunarData = solarLunar.solar2lunar(value.year(), value.month(), value.date());
return (
@@ -19,7 +20,7 @@ function dateCellRender(value) {
);
-}
+};
ReactDOM.render(
diff --git a/components/calendar2/__docs__/index.en-us.md b/components/calendar2/__docs__/index.en-us.md
index f43df8bf99..738bc1d667 100644
--- a/components/calendar2/__docs__/index.en-us.md
+++ b/components/calendar2/__docs__/index.en-us.md
@@ -19,7 +19,7 @@ Calendar could be used to display dates, such as schedules, timetables, price ca
Calendar use dayjs as a core part to manipulate and display time values. For real usage, it could be used with the latest `dayjs` package. Setting dayjs's locale by:
-````js
+```js
import { DatePicker2, ConfigProvider } from '@alifd/next';
import 'dayjs/locale/en';
import en from '@alifd/next/lib/locale/en-us';
@@ -27,29 +27,74 @@ import en from '@alifd/next/lib/locale/en-us';
function App() {
return (
-
+
);
}
ReactDOM.render(
, mountNode);
-````
+```
## API
### Calendar
-| Param | Description | Type | Default Value |
-| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------- |
-| defaultValue | Default value of calendar | custom | - |
-| shape | Shape of calendar
**option**:
'card', 'fullscreen', 'panel' | Enum | 'fullscreen' |
-| value | Value of calendar | custom | - |
-| mode | Mode of panel
**option**:
'date', 'month', 'year' | Enum | 'date' |
-| showOtherMonth | Show dates of other month in current date | Boolean | true |
-| defaultVisibleMonth | Default visible month of panel
**signature**:
Function() => void | Function | - |
-| onSelect | Callback when select a date
**signature**:
Function(value: Object) => void
**parameter**:
_value_: {Object} date object | Function | func.noop |
-| onModeChange | Callback when change mode
**签名**:
Function(mode: string) => void
**参数**:
_mode_: {string} mode type: date month year | Function | func.noop |
-| dateCellRender | Render function for date cell
**signature**:
Function(value: Object) => ReactNode
**parameter**:
_value_: {Object} date object
**return**:
{ReactNode} null
| Function | (value) => value.date() |
-| monthCellRender | Render function for month cell
**signature**:
Function(calendarDate: Object) => ReactNode
**parameter**:
_calendarDate_: {Object} current date object
**return**:
{ReactNode} null
| Function | - |
-| yearRange | Year Range,[START_YEAR, END_YEAR] \(only shape in ‘card’, 'fullscreen') | Array<Number> | - |
-| disabledDate | Function to disable dates
**signature**:
Function(calendarDate: Object) => Boolean
**parameter**:
_calendarDate_: {Object} current date object
_view_: {Enum} current view type: 'year', 'month', 'date'
**return**:
{Boolean} null
| Function | - |
+| Param | Description | Type | Default Value | Required |
+| ----------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------- |
+| defaultValue | Default selected date (dayjs object) | ConfigType | - | |
+| value | Selected date value (dayjs object) | ConfigType | - | |
+| defaultPanelValue | Default displayed date | ConfigType | - | |
+| panelValue | Displayed date | ConfigType | - | |
+| shape | Display shape | 'card' \| 'fullscreen' \| 'panel' | 'fullscreen' | |
+| mode | Date mode | CalendarMode | 'month' | |
+| panelMode | Panel mode, will be inferred automatically if not specified | CalendarPanelMode | - | |
+| onSelect | Callback when selecting a date cell | (value: Dayjs, strVal: string) => void | - | |
+| onChange | Callback when value changes | (value: Dayjs, strVal: string) => void | - | |
+| onPanelChange | Callback when date panel changes | (value: Dayjs, mode: string, reason?: string) => void | - | |
+| className | Custom style class | string | - | |
+| dateCellRender | Custom date rendering | CustomCellRender | - | |
+| monthCellRender | Custom month rendering function | CustomCellRender | - | |
+| yearCellRender | Custom year rendering function | CustomCellRender | - | |
+| quarterCellRender | Custom quarter rendering function | CustomCellRender | - | |
+| disabledDate | Disabled date | (value: Dayjs, mode: CalendarPanelMode) => boolean | - | |
+| onPrev | Callback when clicking the left single arrow | OnPrevOrNext | - | |
+| onNext | Callback when clicking the right single arrow | OnPrevOrNext | - | |
+| onSuperPrev | Callback when clicking the left double arrow | OnPrevOrNext | - | |
+| onSuperNext | Callback when clicking the right double arrow | OnPrevOrNext | - | |
+| headerRender | Header custom rendering | (props: HeaderPanelProps) => React.ReactNode | - | |
+| validValue | Valid year range | [Dayjs, Dayjs] | - | |
+| renderHeaderExtra | Render header extra content | (props: HeaderPanelProps) => React.ReactNode | - | |
+| cellClassName | Cell custom style | (value: Dayjs) => Record\
\| undefined \| null | - | |
+| cellProps | Cell custom property | { onMouseEnter?: ( v: Dayjs, e: React.MouseEvent\, args: Pick\ ) => void; onMouseLeave?: ( v: Dayjs, e: React.MouseEvent\, args: Pick\ ) => void; } | - | |
+### CellData
+
+| Param | Description | Type | Default Value | Required |
+| --------- | ----------- | ---------------- | ------------- | -------- |
+| value | - | Dayjs | - | yes |
+| label | - | number \| string | - | yes |
+| isCurrent | - | boolean | - | yes |
+| key | - | string \| number | - | yes |
+
+### OnPrevOrNext
+
+```typescript
+export type OnPrevOrNext = (value: Dayjs, options: { unit: ManipulateType; num: number }) => void;
+```
+
+### CalendarMode
+
+```typescript
+export type CalendarMode = 'month' | 'year';
+```
+
+### CalendarPanelMode
+
+```typescript
+export type CalendarPanelMode = 'date' | 'week' | 'month' | 'quarter' | 'year' | 'decade';
+```
+
+### CustomCellRender
+
+```typescript
+export type CustomCellRender = (value: Dayjs) => React.ReactNode;
+```
diff --git a/components/calendar2/__docs__/index.md b/components/calendar2/__docs__/index.md
index 14546b19c3..92acc2db4f 100644
--- a/components/calendar2/__docs__/index.md
+++ b/components/calendar2/__docs__/index.md
@@ -10,6 +10,7 @@
按照日历形式展示数据的容器。
### 何时使用
+
1.22版本增加当前组件
日历组件是一个偏向于展示与受控的基础组件,可用于日程、课表、价格日历、农历展示等。
@@ -18,7 +19,7 @@
由于 `Calendar` 组件内部使用 `dayjs` 对象来设置日期(请使用最新版 dayjs),部分 `Locale` 读取自 [日期库`dayjs`的国际化](https://dayjs.gitee.io/docs/zh-CN/i18n/i18n)。
-````js
+```js
import { DatePicker2, ConfigProvider } from '@alifd/next';
import 'dayjs/locale/en';
import en from '@alifd/next/lib/locale/en-us';
@@ -26,29 +27,74 @@ import en from '@alifd/next/lib/locale/en-us';
function App() {
return (
-
+
);
}
ReactDOM.render( , mountNode);
-````
+```
## API
### Calendar
-| 参数 | 说明 | 类型 | 默认值 |
-| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | --------------------- |
-| defaultValue | 默认选中的日期(dayjs 对象) | custom | - |
-| shape | 展现形态 **可选值**: 'card', 'fullscreen', 'panel' | Enum | 'fullscreen' |
-| value | 选中的日期值 (dayjs 对象) | custom | - |
-| mode | 面板模式 | Enum | - |
-| showOtherMonth | 是否展示非本月的日期 | Boolean | true |
-| defaultVisibleMonth | 默认展示的月份 **签名**: Function() => void | Function | - |
-| onSelect | 选择日期单元格时的回调 **签名**: Function(value: Object) => void **参数**: _value_: {Object} 对应的日期值 (dayjs 对象) | Function | func.noop |
-| onModeChange | 面板模式变化时的回调 **签名**: Function(mode: String) => void **参数**: _mode_: {String} 对应面板模式 date month year | Function | func.noop |
-| onVisibleMonthChange | 展现的月份变化时的回调 **签名**: Function(value: Object, reason: String) => void **参数**: _value_: {Object} 显示的月份 (dayjs 对象) _reason_: {String} 触发月份改变原因 | Function | func.noop |
-| dateCellRender | 自定义日期渲染函数 **签名**: Function(value: Object) => ReactNode **参数**: _value_: {Object} 日期值(dayjs对象) **返回值**: {ReactNode} null | Function | value => value.date() |
-| monthCellRender | 自定义月份渲染函数 **签名**: Function(calendarDate: Object) => ReactNode **参数**: _calendarDate_: {Object} 对应 Calendar 返回的自定义日期对象 **返回值**: {ReactNode} null | Function | - |
-| yearRange | 年份范围,[START_YEAR, END_YEAR] \(只在shape 为 ‘card’, 'fullscreen' 下生效) | Array<Number> | - |
-| disabledDate | 不可选择的日期 **签名**: Function(calendarDate: Object, view: String) => Boolean **参数**: _calendarDate_: {Object} 对应 Calendar 返回的自定义日期对象 _view_: {String} 当前视图类型,year: 年, month: 月, date: 日 **返回值**: {Boolean} null | Function | - |
+| 参数 | 说明 | 类型 | 默认值 | 是否必填 |
+| ----------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | -------- |
+| defaultValue | 默认选中的日期(dayjs 对象) | ConfigType | - | |
+| value | 选中的日期值 (dayjs 对象) | ConfigType | - | |
+| defaultPanelValue | 面板默认显示的日期 | ConfigType | - | |
+| panelValue | 面板显示的日期(受控) | ConfigType | - | |
+| shape | 展现形态 | 'card' \| 'fullscreen' \| 'panel' | 'fullscreen' | |
+| mode | 日期模式 | CalendarMode | 'month' | |
+| panelMode | 面板模式,未指定时会根据 mode 自动推断 | CalendarPanelMode | - | |
+| onSelect | 选择日期单元格时的回调 | (value: Dayjs, strVal: string) => void | - | |
+| onChange | 值改变时的回调 | (value: Dayjs, strVal: string) => void | - | |
+| onPanelChange | 日期面板变化回调 | (value: Dayjs, mode: string, reason?: string) => void | - | |
+| className | 自定义样式类 | string | - | |
+| dateCellRender | 自定义日期渲染 | CustomCellRender | - | |
+| monthCellRender | 自定义月份渲染函数 | CustomCellRender | - | |
+| yearCellRender | 自定义年份渲染函数 | CustomCellRender | - | |
+| quarterCellRender | 自定义季度渲染函数 | CustomCellRender | - | |
+| disabledDate | 不可选择的日期 | (value: Dayjs, mode: CalendarPanelMode) => boolean | - | |
+| onPrev | 点击头部左单箭头时触发的回调 | OnPrevOrNext | - | |
+| onNext | 点击头部右单箭头时触发的回调 | OnPrevOrNext | - | |
+| onSuperPrev | 点击头部左双箭头时触发的回调 | OnPrevOrNext | - | |
+| onSuperNext | 点击头部右双箭头时触发的回调 | OnPrevOrNext | - | |
+| headerRender | 头部自定义渲染 | (props: HeaderPanelProps) => React.ReactNode | - | |
+| validValue | 可选择的年份的有效区间 | [Dayjs, Dayjs] | - | |
+| renderHeaderExtra | 渲染头部额外内容 | (props: HeaderPanelProps) => React.ReactNode | - | |
+| cellClassName | 单元格自定义样式 | (value: Dayjs) => Record\ \| undefined \| null | - | |
+| cellProps | 单元格自定义属性 | { onMouseEnter?: ( v: Dayjs, e: React.MouseEvent\, args: Pick\ ) => void; onMouseLeave?: ( v: Dayjs, e: React.MouseEvent\, args: Pick\ ) => void; } | - | |
+
+### CellData
+
+| 参数 | 说明 | 类型 | 默认值 | 是否必填 |
+| --------- | ---- | ---------------- | ------ | -------- |
+| value | - | Dayjs | - | 是 |
+| label | - | number \| string | - | 是 |
+| isCurrent | - | boolean | - | 是 |
+| key | - | string \| number | - | 是 |
+
+### OnPrevOrNext
+
+```typescript
+export type OnPrevOrNext = (value: Dayjs, options: { unit: ManipulateType; num: number }) => void;
+```
+
+### CalendarMode
+
+```typescript
+export type CalendarMode = 'month' | 'year';
+```
+
+### CalendarPanelMode
+
+```typescript
+export type CalendarPanelMode = 'date' | 'week' | 'month' | 'quarter' | 'year' | 'decade';
+```
+
+### CustomCellRender
+
+```typescript
+export type CustomCellRender = (value: Dayjs) => React.ReactNode;
+```
diff --git a/components/calendar2/__tests__/a11y-spec.tsx b/components/calendar2/__tests__/a11y-spec.tsx
new file mode 100644
index 0000000000..ae3de5ed14
--- /dev/null
+++ b/components/calendar2/__tests__/a11y-spec.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import Calendar2 from '../index';
+import '../style';
+import { testReact } from '../../util/__tests__/a11y/validate';
+
+describe('Calendar A11y', () => {
+ it('should not have any violations when default', async () => {
+ await testReact( );
+ });
+ it('should not have any violations when shape', async () => {
+ await testReact(
+
+
+
+
+
+ );
+ });
+});
diff --git a/components/calendar2/__tests__/index-spec.js b/components/calendar2/__tests__/index-spec.js
deleted file mode 100644
index 5b08eeeb57..0000000000
--- a/components/calendar2/__tests__/index-spec.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import React from 'react';
-import Enzyme, { mount } from 'enzyme';
-import Adapter from 'enzyme-adapter-react-16';
-import assert from 'power-assert';
-import dayjs from 'dayjs';
-import Calendar2 from '../index';
-
-Enzyme.configure({
- adapter: new Adapter(),
-});
-dayjs.locale('zh-cn');
-const defaultVal = dayjs('2017-10-01', 'YYYY-MM-DD', true);
-
-/* eslint-disable */
-describe('Calendar2', () => {
- let wrapper;
-
- afterEach(() => {
- if (wrapper) {
- wrapper.unmount();
- wrapper = null;
- }
- });
-
- describe('render', () => {
- it('should render fullscreen calendar with header', () => {
- wrapper = mount( );
-
- assert(wrapper.find('.next-calendar2-header-title'));
- });
- });
-});
diff --git a/components/calendar2/__tests__/index-spec.tsx b/components/calendar2/__tests__/index-spec.tsx
new file mode 100644
index 0000000000..bc57a23431
--- /dev/null
+++ b/components/calendar2/__tests__/index-spec.tsx
@@ -0,0 +1,140 @@
+import React from 'react';
+import dayjs from 'dayjs';
+import Calendar2, { type CalendarProps } from '../index';
+import '../style';
+
+dayjs.locale('zh-cn');
+const defaultVal = dayjs('2017-10-01', 'YYYY-MM-DD', true);
+
+describe('Calendar2', () => {
+ describe('render', () => {
+ it('should render with default value', () => {
+ cy.mount( );
+ cy.get('td[title="2017-10-01"]').should('have.class', 'next-calendar2-cell-selected');
+ });
+
+ it('should render calendar panel', () => {
+ cy.mount( );
+ cy.get('.next-calendar2-panel').should('have.length', 1);
+ });
+
+ it('should render calendar card', () => {
+ cy.mount( );
+ cy.get('.next-calendar2-card').should('have.length', 1);
+ });
+
+ it('should render uncontrolled calendar', () => {
+ cy.mount( );
+ cy.get('td[title="2017-10-01"]').should('have.class', 'next-calendar2-cell-selected');
+ cy.get('td[title="2017-10-02"]').should('exist');
+ cy.get('td[title="2017-10-02"]').click();
+ cy.get('td[title="2017-10-02"]').should('have.class', 'next-calendar2-cell-selected');
+ });
+
+ it('should render controlled calendar', () => {
+ cy.mount( ).as('Demo');
+ cy.get('td[title="2017-10-01"]').should('have.class', 'next-calendar2-cell-selected');
+ cy.rerender('Demo', { value: defaultVal.clone().add(1, 'days') });
+ cy.get('td[title="2017-10-02"]').should('have.class', 'next-calendar2-cell-selected');
+ });
+
+ it('should render controlled calendar with mode', () => {
+ cy.mount( ).as('Demo');
+ cy.get('.next-calendar2-cell').should('have.length', 12);
+ });
+
+ it('should render with disabled dates', () => {
+ const disabledDateHandler = cy.spy().as('disabledDateHandler');
+ const disabledDate: CalendarProps['disabledDate'] = (date, view) => {
+ disabledDateHandler(view);
+ return date.valueOf() > defaultVal.valueOf();
+ };
+ cy.mount( );
+ cy.get('td[title="2017-10-02"]').should('have.class', 'next-calendar2-cell-disabled');
+ cy.get('@disabledDateHandler').should('be.calledWith', 'date');
+ });
+
+ it('should render custom content', () => {
+ const dateCellRender: CalendarProps['dateCellRender'] = date => {
+ const dateNum = date.date();
+ if (defaultVal.month() !== date.month()) {
+ return dateNum;
+ }
+
+ if (dateNum === 1) {
+ return hello world
;
+ }
+ };
+ cy.mount( );
+ cy.get('td[title="2017-10-01"] div.test').should('have.length', 1);
+ });
+ });
+
+ describe('action', () => {
+ it('should change mode', () => {
+ const onModeChange = cy.spy().as('onModeChange');
+ cy.mount( );
+ cy.get('.next-radio-wrapper input').eq(1).check({ force: true });
+ cy.get('td').should('have.length', 12);
+ cy.get('td[title="2017-01"]').should('have.length', 1);
+ cy.get('@onModeChange').should('be.calledOnce');
+ });
+
+ it('should change panel mode to month', () => {
+ cy.mount( );
+ cy.get('.next-calendar2-header-text-field button').eq(1).click();
+ cy.get('.next-calendar2-cell').should('have.length', 12);
+ cy.get('.next-calendar2-header-text-field button').eq(0).click();
+ cy.get('.next-calendar2-header-text-field').should('have.text', '2010-2019');
+ });
+
+ it('should change panel mode to year', () => {
+ cy.mount( );
+ cy.get('.next-calendar2-header-text-field button').eq(0).click();
+ cy.get('.next-calendar2-cell').should('have.length', 12);
+ cy.get('.next-calendar2-cell').eq(0).should('have.text', '2009');
+ });
+
+ it('should change visible month', () => {
+ cy.mount( );
+ cy.get('.next-calendar2-header-right-btn').eq(0).click();
+ cy.get('.next-calendar2-header-text-field button').eq(1).should('have.text', '11月');
+ cy.get('.next-calendar2-header-left-btn').eq(1).click();
+ cy.get('.next-calendar2-header-text-field button').eq(1).should('have.text', '10月');
+ });
+
+ it('should change visible month by year', () => {
+ cy.mount( );
+ cy.get('.next-calendar2-header-right-btn').eq(1).click();
+ cy.get('.next-calendar2-header-text-field button').eq(0).should('have.text', '2018年');
+ cy.get('.next-calendar2-header-left-btn').eq(0).click();
+ cy.get('.next-calendar2-header-text-field button').eq(0).should('have.text', '2017年');
+ });
+
+ it('should change decade', () => {
+ cy.mount( );
+ cy.get('.next-calendar2-header-left-btn').click();
+ cy.get('.next-calendar2-header-text-field').should('have.text', '1900-1999');
+ cy.get('.next-calendar2-header-right-btn').click();
+ cy.get('.next-calendar2-header-text-field').should('have.text', '2000-2099');
+ });
+
+ it('should select date', () => {
+ const onSelectHandler = cy.spy().as('onSelectHandler');
+ const onSelect: CalendarProps['onSelect'] = val => {
+ onSelectHandler(val.format('YYYY-MM-DD'));
+ };
+ cy.mount( );
+ cy.get('td[title="2017-10-02"]').click();
+ cy.get('@onSelectHandler').should('be.calledWith', '2017-10-02');
+ });
+
+ it('should render fullscreen calendar with header', () => {
+ cy.mount(
+
+ );
+
+ cy.get('.next-calendar2-header-title').should('exist');
+ });
+ });
+});
diff --git a/components/calendar2/calendar.jsx b/components/calendar2/calendar.jsx
deleted file mode 100644
index 134b111c41..0000000000
--- a/components/calendar2/calendar.jsx
+++ /dev/null
@@ -1,263 +0,0 @@
-import React from 'react';
-import { polyfill } from 'react-lifecycles-compat';
-import PT from 'prop-types';
-import classnames from 'classnames';
-import defaultLocale from '../locale/zh-cn';
-import { func, datejs, obj } from '../util';
-import SharedPT from './prop-types';
-
-import { CALENDAR_MODE, CALENDAR_SHAPE, DATE_PANEL_MODE } from './constant';
-import HeaderPanel from './panels/header-panel';
-import DateTable from './panels/date-table';
-
-const { pickProps, pickOthers } = obj;
-
-// CALENDAR_MODE => DATE_PANEL_MODE
-function getPanelMode(mode) {
- return mode && (mode === CALENDAR_MODE.YEAR ? DATE_PANEL_MODE.MONTH : DATE_PANEL_MODE.DATE);
-}
-
-function isValueChanged(newVal, oldVal) {
- return newVal !== oldVal && !datejs(newVal).isSame(datejs(oldVal));
-}
-
-class Calendar extends React.Component {
- static propTypes = {
- rtl: PT.bool,
- name: PT.string,
- prefix: PT.string,
- locale: PT.object,
- /**
- * 展现形态
- */
- shape: SharedPT.shape,
- /*
- * 日期模式: month | year
- */
- mode: SharedPT.mode,
- /**
- * 默认选中的日期(受控)
- */
- value: SharedPT.date,
- /**
- * 默认选中的日期
- */
- defaultValue: SharedPT.date,
- /**
- * 面板显示的日期(受控)
- */
- panelValue: SharedPT.date,
- /**
- * 面板默认显示的日期
- */
- defaultPanelValue: SharedPT.date,
- /**
- * 不可选择的日期
- */
- disabledDate: PT.func,
- /**
- * 可显示的日期范围
- */
- validRange: PT.arrayOf(SharedPT.date),
- /**
- * 自定义日期渲染
- */
- dateCellRender: PT.func,
- quarterCellRender: PT.func,
- monthCellRender: PT.func,
- yearCellRender: PT.func,
- /**
- * 自定义头部渲染
- */
- headerRender: PT.func,
- /**
- * 日期变化回调
- */
- onChange: PT.func,
- /**
- * 点击选择日期回调
- */
- onSelect: PT.func,
- /**
- * 日期面板变化回调
- */
- onPanelChange: PT.func,
- cellProps: PT.object,
- cellClassName: PT.oneOfType([PT.func, PT.string]),
- panelMode: PT.any,
- onPrev: PT.func,
- onNext: PT.func,
- onSuperPrev: PT.func,
- onSuperNext: PT.func,
- colNum: PT.number,
- };
-
- static defaultProps = {
- rtl: false,
- prefix: 'next-',
- locale: defaultLocale.Calendar,
- shape: CALENDAR_SHAPE.FULLSCREEN,
- mode: CALENDAR_MODE.MONTH,
- };
-
- constructor(props) {
- super(props);
-
- const { defaultValue, mode, defaultPanelValue = datejs() } = props;
- const value = 'value' in props ? props.value : defaultValue;
- const panelValue = datejs('panelValue' in props ? props.panelValue : value || defaultPanelValue);
- const panelMode = props.panelMode || getPanelMode(mode) || DATE_PANEL_MODE.DATE;
-
- this.state = {
- mode,
- value,
- panelMode,
- panelValue: panelValue.isValid() ? panelValue : datejs(),
- };
- }
-
- static getDerivedStateFromProps(props, state) {
- let newState = null;
- let value;
- let panelValue;
-
- if ('value' in props && isValueChanged(props.value, state.value)) {
- value = props.value;
- panelValue = datejs(value);
- }
-
- if ('panelValue' in props) {
- panelValue = datejs(props.panelValue);
- }
-
- // panelValue不能是无效值
- if (panelValue) {
- panelValue = panelValue.isValid() ? panelValue : datejs();
- newState = {
- panelValue,
- };
- }
- if (value) {
- newState.value = value;
- }
-
- return newState;
- }
-
- switchPanelMode = mode => {
- const { MONTH, YEAR, DECADE } = DATE_PANEL_MODE;
- const originalPanelMode = this.props.panelMode || getPanelMode(mode);
-
- switch (mode) {
- case YEAR:
- return MONTH;
- case DECADE:
- return YEAR;
- default:
- return originalPanelMode;
- }
- };
-
- shouldSwitchPanelMode = () => {
- const { mode, shape } = this.props;
- const { panelMode } = this.state;
- const originalPanelMode = this.props.panelMode || getPanelMode(mode);
- return shape === CALENDAR_SHAPE.PANEL && panelMode !== originalPanelMode;
- };
-
- onDateSelect = (value, _, { isCurrent }) => {
- const { panelMode } = this.state;
- const unit = panelMode === 'date' ? 'day' : panelMode;
-
- if (this.shouldSwitchPanelMode()) {
- this.onPanelChange(value, this.switchPanelMode(panelMode), 'DATESELECT_VALUE_SWITCH_MODE');
- } else {
- isCurrent || this.onPanelValueChange(value, 'DATESELECT');
- value.isSame(this.state.value, unit) || this.onChange(value);
-
- func.invoke(this.props, 'onSelect', [value]);
- }
- };
-
- onModeChange = (mode, reason) => {
- this.setState({
- mode,
- });
- const panelMode = getPanelMode(mode);
-
- if (this.state.panelMode !== panelMode) {
- this.onPanelModeChange(panelMode, reason);
- }
- };
-
- onPanelValueChange = (panelValue, reason) => {
- this.onPanelChange(panelValue, this.state.panelMode, reason);
- };
-
- onPanelModeChange = (panelMode, reason) => {
- this.onPanelChange(this.state.panelValue, panelMode, reason);
- };
-
- onPanelChange = (value, mode, reason) => {
- this.setState({
- panelMode: mode,
- panelValue: value,
- });
-
- func.invoke(this.props, 'onPanelChange', [value, mode, reason]);
- };
-
- onChange = value => {
- this.setState({
- value,
- panelValue: value,
- });
- func.invoke(this.props, 'onChange', [value]);
- };
-
- render() {
- const value = 'value' in this.props ? datejs(this.props.value) : this.state.value;
- const { panelMode, mode, panelValue } = this.state;
-
- const { prefix, shape, rtl, className, ...restProps } = this.props;
-
- const sharedProps = {
- rtl,
- prefix,
- shape,
- value,
- panelValue,
- };
-
- const headerPanelProps = {
- ...pickProps(HeaderPanel.propTypes, restProps),
- ...sharedProps,
- mode,
- panelMode,
- onPanelValueChange: this.onPanelValueChange,
- onModeChange: this.onModeChange,
- onPanelModeChange: this.onPanelModeChange,
- showModeSwitch: this.props.mode !== CALENDAR_MODE.YEAR,
- };
-
- const dateTableProps = {
- ...pickProps(DateTable.propTypes, restProps),
- ...sharedProps,
- mode: panelMode,
- onSelect: this.onDateSelect,
- };
-
- const classNames = classnames([`${prefix}calendar2`, `${prefix}calendar2-${shape}`, className]);
-
- return (
-
- );
- }
-}
-
-export default polyfill(Calendar);
diff --git a/components/calendar2/calendar.tsx b/components/calendar2/calendar.tsx
new file mode 100644
index 0000000000..74b5c6efc2
--- /dev/null
+++ b/components/calendar2/calendar.tsx
@@ -0,0 +1,252 @@
+import React, { type UIEvent } from 'react';
+import { polyfill } from 'react-lifecycles-compat';
+import PT from 'prop-types';
+import classnames from 'classnames';
+import { type Dayjs, type ConfigType } from 'dayjs';
+import defaultLocale from '../locale/zh-cn';
+import { func, datejs, obj, type ClassPropsWithDefault } from '../util';
+import SharedPT from './prop-types';
+
+import { CALENDAR_MODE, CALENDAR_SHAPE, DATE_PANEL_MODE } from './constant';
+import HeaderPanel from './panels/header-panel';
+import DateTable from './panels/date-table';
+import type {
+ CalendarMode,
+ CalendarPanelMode,
+ CalendarProps,
+ CalendarState,
+ CellData,
+} from './types';
+
+const { pickProps, pickOthers } = obj;
+
+// CALENDAR_MODE => DATE_PANEL_MODE
+function getPanelMode(mode: CalendarMode | CalendarPanelMode) {
+ return mode && (mode === CALENDAR_MODE.YEAR ? DATE_PANEL_MODE.MONTH : DATE_PANEL_MODE.DATE);
+}
+
+function isValueChanged(newVal: ConfigType, oldVal: ConfigType) {
+ return newVal !== oldVal && !datejs(newVal).isSame(datejs(oldVal));
+}
+
+type CalendarPropsWithDefault = ClassPropsWithDefault;
+
+class Calendar extends React.Component {
+ static propTypes = {
+ rtl: PT.bool,
+ name: PT.string,
+ prefix: PT.string,
+ locale: PT.object,
+ shape: SharedPT.shape,
+ mode: SharedPT.mode,
+ value: SharedPT.date,
+ defaultValue: SharedPT.date,
+ panelValue: SharedPT.date,
+ defaultPanelValue: SharedPT.date,
+ disabledDate: PT.func,
+ dateCellRender: PT.func,
+ quarterCellRender: PT.func,
+ monthCellRender: PT.func,
+ yearCellRender: PT.func,
+ headerRender: PT.func,
+ onChange: PT.func,
+ onSelect: PT.func,
+ onPanelChange: PT.func,
+ cellProps: PT.object,
+ cellClassName: PT.oneOfType([PT.func, PT.string]),
+ panelMode: PT.any,
+ onPrev: PT.func,
+ onNext: PT.func,
+ onSuperPrev: PT.func,
+ onSuperNext: PT.func,
+ colNum: PT.number,
+ };
+
+ static defaultProps = {
+ rtl: false,
+ prefix: 'next-',
+ locale: defaultLocale.Calendar,
+ shape: CALENDAR_SHAPE.FULLSCREEN,
+ mode: CALENDAR_MODE.MONTH,
+ };
+
+ static displayName = 'Calendar';
+
+ readonly props: CalendarPropsWithDefault;
+
+ constructor(props: CalendarProps) {
+ super(props);
+
+ const { defaultValue, mode, defaultPanelValue = datejs() } = props;
+ const value = 'value' in props ? props.value : defaultValue;
+ const panelValue = datejs(
+ 'panelValue' in props ? props.panelValue : value || defaultPanelValue
+ );
+ const panelMode = props.panelMode || getPanelMode(mode!) || DATE_PANEL_MODE.DATE;
+
+ this.state = {
+ mode: mode!,
+ value,
+ panelMode,
+ panelValue: panelValue.isValid() ? panelValue : datejs(),
+ };
+ }
+
+ static getDerivedStateFromProps(props: CalendarPropsWithDefault, state: CalendarState) {
+ let newState: Partial = {};
+ let value;
+ let panelValue;
+
+ if ('value' in props && isValueChanged(props.value, state.value)) {
+ value = props.value;
+ panelValue = datejs(value);
+ }
+
+ if ('panelValue' in props) {
+ panelValue = datejs(props.panelValue);
+ }
+
+ // panelValue 不能是无效值
+ if (panelValue) {
+ panelValue = panelValue.isValid() ? panelValue : datejs();
+ newState = {
+ panelValue,
+ };
+ }
+ if (value) {
+ newState.value = value;
+ }
+
+ return newState;
+ }
+
+ switchPanelMode = (mode: CalendarPanelMode) => {
+ const { MONTH, YEAR, DECADE } = DATE_PANEL_MODE;
+ const originalPanelMode = this.props.panelMode || getPanelMode(mode);
+
+ switch (mode) {
+ case YEAR:
+ return MONTH;
+ case DECADE:
+ return YEAR;
+ default:
+ return originalPanelMode;
+ }
+ };
+
+ shouldSwitchPanelMode = () => {
+ const { mode, shape } = this.props;
+ const { panelMode } = this.state;
+ const originalPanelMode = this.props.panelMode || getPanelMode(mode);
+ return shape === CALENDAR_SHAPE.PANEL && panelMode !== originalPanelMode;
+ };
+
+ onDateSelect = (value: Dayjs, _: UIEvent, { isCurrent }: Pick) => {
+ const { panelMode } = this.state;
+ const unit = panelMode === 'date' ? 'day' : panelMode;
+
+ if (this.shouldSwitchPanelMode()) {
+ this.onPanelChange(
+ value,
+ this.switchPanelMode(panelMode),
+ 'DATESELECT_VALUE_SWITCH_MODE'
+ );
+ } else {
+ isCurrent || this.onPanelValueChange(value, 'DATESELECT');
+ // @ts-expect-error unit 在这里不能是 quarter 和 week
+ value.isSame(this.state.value, unit) || this.onChange(value);
+
+ func.invoke(this.props, 'onSelect', [value]);
+ }
+ };
+
+ onModeChange = (mode: CalendarMode, reason?: string) => {
+ this.setState({
+ mode,
+ });
+ const panelMode = getPanelMode(mode);
+
+ if (this.state.panelMode !== panelMode) {
+ this.onPanelModeChange(panelMode, reason);
+ }
+ };
+
+ onPanelValueChange = (panelValue: Dayjs, reason?: string) => {
+ this.onPanelChange(panelValue, this.state.panelMode, reason);
+ };
+
+ onPanelModeChange = (panelMode: CalendarPanelMode, reason?: string) => {
+ this.onPanelChange(this.state.panelValue, panelMode, reason);
+ };
+
+ onPanelChange = (value: Dayjs, mode: CalendarPanelMode, reason?: string) => {
+ this.setState({
+ panelMode: mode,
+ panelValue: value,
+ });
+
+ func.invoke(this.props, 'onPanelChange', [value, mode, reason]);
+ };
+
+ onChange = (value: Dayjs) => {
+ this.setState({
+ value,
+ panelValue: value,
+ });
+ func.invoke(this.props, 'onChange', [value]);
+ };
+
+ render() {
+ const value = 'value' in this.props ? datejs(this.props.value) : this.state.value;
+ const { panelMode, mode, panelValue } = this.state;
+
+ const { prefix, shape, rtl, className, ...restProps } = this.props;
+
+ const sharedProps = {
+ rtl,
+ prefix,
+ shape,
+ value,
+ panelValue,
+ };
+
+ const headerPanelProps = {
+ ...pickProps(HeaderPanel.propTypes, restProps),
+ ...sharedProps,
+ mode,
+ panelMode,
+ onPanelValueChange: this.onPanelValueChange,
+ onModeChange: this.onModeChange,
+ onPanelModeChange: this.onPanelModeChange,
+ showModeSwitch: this.props.mode !== CALENDAR_MODE.YEAR,
+ };
+
+ const dateTableProps = {
+ ...pickProps(DateTable.propTypes, restProps),
+ ...sharedProps,
+ mode: panelMode,
+ onSelect: this.onDateSelect,
+ };
+
+ const classNames = classnames([
+ `${prefix}calendar2`,
+ `${prefix}calendar2-${shape}`,
+ className,
+ ]);
+
+ return (
+
+ );
+ }
+}
+
+export default polyfill(Calendar);
diff --git a/components/calendar2/constant.js b/components/calendar2/constant.js
deleted file mode 100644
index 4e86b6de3e..0000000000
--- a/components/calendar2/constant.js
+++ /dev/null
@@ -1,30 +0,0 @@
-// 日历shape
-export const CALENDAR_SHAPE = {
- FULLSCREEN: 'fullscreen',
- CARD: 'card',
- PANEL: 'panel',
-};
-
-// 日历模式
-export const CALENDAR_MODE = {
- MONTH: 'month',
- YEAR: 'year',
-};
-
-// 日期面板的模式
-export const DATE_PANEL_MODE = {
- DATE: 'date',
- WEEK: 'week',
- MONTH: 'month',
- QUARTER: 'quarter',
- YEAR: 'year',
- DECADE: 'decade',
-};
-
-// 单元格选中状态
-export const CALENDAR_CELL_STATE = {
- UN_SELECTED: 0,
- SELECTED: 1,
- SELECTED_BEGIN: 2,
- SELECTED_END: 3,
-};
diff --git a/components/calendar2/constant.ts b/components/calendar2/constant.ts
new file mode 100644
index 0000000000..49f87cfa25
--- /dev/null
+++ b/components/calendar2/constant.ts
@@ -0,0 +1,32 @@
+import type { CalendarPanelMode } from './types';
+
+// 日历 shape
+export const CALENDAR_SHAPE = {
+ FULLSCREEN: 'fullscreen',
+ CARD: 'card',
+ PANEL: 'panel',
+};
+
+// 日历模式
+export const CALENDAR_MODE = {
+ MONTH: 'month',
+ YEAR: 'year',
+};
+
+// 日期面板的模式
+export const DATE_PANEL_MODE: Record = {
+ DATE: 'date',
+ WEEK: 'week',
+ MONTH: 'month',
+ QUARTER: 'quarter',
+ YEAR: 'year',
+ DECADE: 'decade',
+};
+
+// 单元格选中状态
+export const CALENDAR_CELL_STATE = {
+ UN_SELECTED: 0,
+ SELECTED: 1,
+ SELECTED_BEGIN: 2,
+ SELECTED_END: 3,
+};
diff --git a/components/calendar2/index.d.ts b/components/calendar2/index.d.ts
deleted file mode 100644
index e70d72d8f1..0000000000
--- a/components/calendar2/index.d.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-///
-
-import React from 'react';
-import { CommonProps } from '../util';
-import { Dayjs, ConfigType } from 'dayjs';
-
-interface HTMLAttributesWeak extends React.HTMLAttributes {
- defaultValue?: any;
- onSelect?: any;
- onChange?: any;
-}
-
-export interface CalendarProps extends HTMLAttributesWeak, CommonProps {
- name?: string;
- /**
- * 默认选中的日期(dayjs 对象)
- */
- defaultValue?: ConfigType;
-
- /**
- * 选中的日期值 (dayjs 对象)
- */
- value?: ConfigType;
-
- /**
- * 面板默认显示的日期
- */
- defaultPanelValue?: ConfigType;
-
- /**
- * 展现形态
- */
- shape?: 'card' | 'fullscreen' | 'panel';
-
- /**
- * 选择日期单元格时的回调
- */
- onSelect?: (value: Dayjs, strVal: string) => void;
-
- /**
- * 值改变时的回调
- */
- onChange?: (value: Dayjs, strVal: string) => void;
-
- /**
- * 日期面板变化回调
- */
- onPanelChange?: (value: Dayjs, mode: string) => void;
-
- /**
- * 自定义样式类
- */
- className?: string;
-
- /**
- * 自定义日期渲染
- */
- dateCellRender?: (value: Dayjs) => React.ReactNode;
-
- /**
- * 自定义月份渲染函数
- */
- monthCellRender?: (value: Dayjs) => React.ReactNode;
-
- /**
- * 自定义年份渲染函数
- */
- yearCellRender?: (value: Dayjs) => React.ReactNode;
-
- /**
- * 不可选择的日期
- */
- disabledDate?: (value: Dayjs, mode: string) => boolean;
-}
-
-export default class Calendar extends React.Component {}
diff --git a/components/calendar2/index.jsx b/components/calendar2/index.jsx
deleted file mode 100644
index ca336a0032..0000000000
--- a/components/calendar2/index.jsx
+++ /dev/null
@@ -1,4 +0,0 @@
-import ConfigProvider from '../config-provider';
-import Calendar from './calendar';
-
-export default ConfigProvider.config(Calendar);
diff --git a/components/calendar2/index.tsx b/components/calendar2/index.tsx
new file mode 100644
index 0000000000..7911f3b1a9
--- /dev/null
+++ b/components/calendar2/index.tsx
@@ -0,0 +1,13 @@
+import ConfigProvider from '../config-provider';
+import Calendar from './calendar';
+
+export type {
+ CalendarProps,
+ CellData,
+ OnPrevOrNext,
+ CalendarMode,
+ CalendarPanelMode,
+ CustomCellRender,
+} from './types';
+
+export default ConfigProvider.config(Calendar);
diff --git a/components/calendar2/mobile/index.jsx b/components/calendar2/mobile/index.tsx
similarity index 100%
rename from components/calendar2/mobile/index.jsx
rename to components/calendar2/mobile/index.tsx
diff --git a/components/calendar2/panels/date-table.jsx b/components/calendar2/panels/date-table.jsx
deleted file mode 100644
index 1c7b8c28ce..0000000000
--- a/components/calendar2/panels/date-table.jsx
+++ /dev/null
@@ -1,353 +0,0 @@
-import React from 'react';
-import { polyfill } from 'react-lifecycles-compat';
-import classnames from 'classnames';
-import PT from 'prop-types';
-import SharedPT from '../prop-types';
-import { DATE_PANEL_MODE } from '../constant';
-import { func, datejs, KEYCODE } from '../../util';
-
-const { bindCtx, renderNode } = func;
-const { DATE, WEEK, MONTH, QUARTER, YEAR, DECADE } = DATE_PANEL_MODE;
-
-// 面板行数
-const mode2Rows = {
- [DATE]: 7,
- [WEEK]: 7,
- [MONTH]: 4,
- [QUARTER]: 4,
- [YEAR]: 4,
- [DECADE]: 3,
-};
-
-class DateTable extends React.Component {
- static propTypes = {
- mode: SharedPT.panelMode,
- value: SharedPT.date,
- panelValue: SharedPT.date,
- dateCellRender: PT.func,
- quarterCellRender: PT.func,
- monthCellRender: PT.func,
- yearCellRender: PT.func,
- disabledDate: PT.func,
- selectedState: PT.func,
- hoveredState: PT.func,
- onSelect: PT.func,
- onDateSelect: PT.func,
- startOnSunday: PT.bool,
- cellClassName: PT.oneOfType([PT.func, PT.string]),
- colNum: PT.number,
- cellProps: PT.object,
- };
-
- constructor(props) {
- super(props);
-
- this.prefixCls = `${props.prefix}calendar2`;
-
- bindCtx(this, [
- 'getDateCellData',
- 'getMonthCellData',
- 'getQuarterCellData',
- 'getYearCellData',
- 'getDecadeData',
- 'handleKeyDown',
- 'handleSelect',
- 'handleMouseEnter',
- 'handleMouseLeave',
- ]);
-
- this.state = {
- hoverValue: null,
- };
- }
-
- handleSelect(v, e, args) {
- func.invoke(this.props, 'onSelect', [v, e, args]);
- }
-
- handleKeyDown(v, e, args) {
- switch (e.keyCode) {
- case KEYCODE.ENTER:
- this.handleSelect(v, e, args);
- break;
- case KEYCODE.RIGHT:
- break;
- }
- // e.preventDefault();
- }
-
- handleMouseEnter(v, e, args) {
- func.invoke(this.props.cellProps, 'onMouseEnter', [v, e, args]);
- }
-
- handleMouseLeave(v, e, args) {
- func.invoke(this.props.cellProps, 'onMouseLeave', [v, e, args]);
- }
-
- isSame(curDate, date, mode) {
- switch (mode) {
- case DATE:
- return curDate.isSame(date, 'day');
- case WEEK:
- return curDate.isSame(date, 'week');
- case QUARTER:
- return curDate.isSame(date, 'quarter');
- case MONTH:
- return curDate.isSame(date, 'month');
- case YEAR:
- return curDate.isSame(date, 'year');
- case DECADE: {
- const curYear = curDate.year();
- const targetYear = date.year();
- return curYear <= targetYear && targetYear < curYear + 10;
- }
- }
- }
-
- getCustomRender = mode => {
- const mode2RenderName = {
- [DATE]: 'dateCellRender',
- [QUARTER]: 'quarterCellRender',
- [MONTH]: 'monthCellRender',
- [YEAR]: 'yearCellRender',
- };
-
- return this.props[mode2RenderName[mode]];
- };
-
- /**
- * 渲染日期面板
- * @param {Object[]} cellData - 单元格数据
- * @param {String} cellData[].label - 单元格显示文本
- * @param {Object} cellData[].value - 日期对象
- * @param {Boolean} cellData[].isCurrent - 是否是当前面板时间范围内的值
- */
- renderCellContent(cellData) {
- const { props } = this;
- const { mode, hoveredState, cellClassName } = props;
- const { hoverValue } = this.state;
-
- const cellContent = [];
- const cellCls = `${this.prefixCls}-cell`;
-
- const now = datejs();
- const rowLen = mode2Rows[mode];
-
- for (let i = 0; i < cellData.length; ) {
- const children = [];
-
- let isCurrentWeek;
- for (let j = 0; j < rowLen; j++) {
- const { label, value, key, isCurrent } = cellData[i++];
- const v = value.startOf(mode);
-
- const isDisabled = props.disabledDate && props.disabledDate(v, mode);
- const hoverState = hoverValue && hoveredState && hoveredState(hoverValue);
- const className = classnames(cellCls, {
- [`${cellCls}-current`]: isCurrent, // 是否属于当前面板值
- [`${cellCls}-today`]: mode === WEEK ? this.isSame(value, now, DATE) : this.isSame(v, now, mode),
- [`${cellCls}-selected`]: this.isSame(v, props.value, mode),
- [`${cellCls}-disabled`]: isDisabled,
- [`${cellCls}-range-hover`]: hoverState,
- ...(cellClassName && cellClassName(v)),
- });
-
- let onEvents = null;
-
- if (!isDisabled) {
- onEvents = {
- onClick: e => this.handleSelect(v, e, { isCurrent, label }),
- onKeyDown: e => this.handleKeyDown(v, e, { isCurrent, label }),
- onMouseEnter: e => this.handleMouseEnter(v, e, { isCurrent, label }),
- onMouseLeave: e => this.handleMouseLeave(v, e, { isCurrent, label }),
- };
- }
-
- if (mode === WEEK && j === 0) {
- const week = v.week();
-
- children.push(
-
- {week}
-
- );
- isCurrentWeek = isCurrent;
- }
-
- const customRender = this.getCustomRender(mode);
-
- children.push(
-
-
- {renderNode(customRender,
{label}
, [v])}
-
-
- );
- }
-
- let className;
- if (mode === WEEK) {
- className = classnames(`${this.prefixCls}-week`, { [`${this.prefixCls}-week-current`]: isCurrentWeek });
- }
-
- cellContent.push(
-
- {children}
-
- );
- }
-
- return cellContent;
- }
-
- // 星期几
- renderWeekdaysHead() {
- let weekdaysMin = datejs.weekdaysMin();
- const firstDayOfWeek = datejs.localeData().firstDayOfWeek();
-
- // 默认一周的第一天是周日,否则需要调整
- if (firstDayOfWeek !== 0) {
- weekdaysMin = weekdaysMin.slice(firstDayOfWeek).concat(weekdaysMin.slice(0, firstDayOfWeek));
- }
-
- return (
-
-
- {/* 占位 */}
- {this.props.mode === WEEK ? : null}
- {weekdaysMin.map(d => {
- return {d} ;
- })}
-
-
- );
- }
-
- getDateCellData() {
- const { panelValue, colNum } = this.props;
-
- const firstDayOfMonth = panelValue.clone().startOf('month');
- const weekOfFirstDay = firstDayOfMonth.day(); // 当月第一天星期几
- const daysOfCurMonth = panelValue.endOf('month').date(); // 当月天数
- const firstDayOfWeek = datejs.localeData().firstDayOfWeek(); // 一周的第一天是星期几
-
- const cellData = [];
- const preDays = (weekOfFirstDay - firstDayOfWeek + 7) % 7;
- const nextDays = colNum
- ? colNum * mode2Rows[DATE] - preDays - daysOfCurMonth
- : (7 - ((preDays + daysOfCurMonth) % 7)) % 7;
-
- // 上个月日期
- for (let i = preDays; i > 0; i--) {
- cellData.push(firstDayOfMonth.clone().subtract(i, 'day'));
- }
-
- // 本月日期
- for (let i = 0; i < daysOfCurMonth; i++) {
- cellData.push(firstDayOfMonth.clone().add(i, 'day'));
- }
-
- // 下个月日期
- for (let i = 0; i < nextDays; i++) {
- cellData.push(firstDayOfMonth.clone().add(daysOfCurMonth + i, 'day'));
- }
-
- return cellData.map(value => {
- return {
- value,
- label: value.date(),
- isCurrent: value.isSame(firstDayOfMonth, 'month'),
- key: value.format('YYYY-MM-DD'),
- };
- });
- }
-
- getMonthCellData() {
- const { panelValue } = this.props;
-
- return datejs.monthsShort().map((label, index) => {
- const value = panelValue.clone().month(index);
-
- return {
- label,
- value,
- isCurrent: true,
- key: value.format('YYYY-MM'),
- };
- });
- }
-
- getQuarterCellData() {
- const { panelValue } = this.props;
-
- return [1, 2, 3, 4].map(i => {
- return {
- label: `Q${i}`,
- value: panelValue.clone().quarter(i),
- isCurrent: true,
- key: `Q${i}`,
- };
- });
- }
-
- getYearCellData() {
- const { panelValue } = this.props;
- const curYear = panelValue.year();
- const startYear = curYear - (curYear % 10) - 1;
- const cellData = [];
-
- for (let i = 0; i < 12; i++) {
- const y = startYear + i;
-
- cellData.push({
- value: panelValue.clone().year(y),
- label: y,
- isCurrent: i > 0 && i < 11,
- key: y,
- });
- }
-
- return cellData;
- }
-
- getDecadeData() {
- const { panelValue } = this.props;
- const curYear = panelValue.year();
- const startYear = curYear - (curYear % 100) - 10;
- const cellData = [];
-
- for (let i = 0; i < 12; i++) {
- const y = startYear + i * 10;
-
- cellData.push({
- value: panelValue.clone().year(y),
- label: `${y}-${y + 9}`,
- isCurrent: i > 0 && i < 11,
- key: `${y}-${y + 9}`,
- });
- }
-
- return cellData;
- }
-
- render() {
- const { mode } = this.props;
- const mode2Data = {
- [DATE]: this.getDateCellData,
- [WEEK]: this.getDateCellData,
- [MONTH]: this.getMonthCellData,
- [QUARTER]: this.getQuarterCellData,
- [YEAR]: this.getYearCellData,
- [DECADE]: this.getDecadeData,
- };
-
- return (
-
- {[DATE, WEEK].includes(mode) ? this.renderWeekdaysHead() : null}
- {this.renderCellContent(mode2Data[mode]())}
-
- );
- }
-}
-
-export default polyfill(DateTable);
diff --git a/components/calendar2/panels/date-table.tsx b/components/calendar2/panels/date-table.tsx
new file mode 100644
index 0000000000..075c8f5f83
--- /dev/null
+++ b/components/calendar2/panels/date-table.tsx
@@ -0,0 +1,386 @@
+import React, { type KeyboardEvent, type MouseEvent } from 'react';
+import { polyfill } from 'react-lifecycles-compat';
+import classnames from 'classnames';
+import PT from 'prop-types';
+import { type WeekdayNames, type Dayjs, type ConfigType } from 'dayjs';
+import SharedPT from '../prop-types';
+import { DATE_PANEL_MODE } from '../constant';
+import { func, datejs, KEYCODE } from '../../util';
+import type { CalendarPanelMode, CellData, DateTableProps, DateTableState } from '../types';
+
+const { bindCtx, renderNode } = func;
+const { DATE, WEEK, MONTH, QUARTER, YEAR, DECADE } = DATE_PANEL_MODE;
+
+// 面板行数
+const mode2Rows = {
+ [DATE]: 7,
+ [WEEK]: 7,
+ [MONTH]: 4,
+ [QUARTER]: 4,
+ [YEAR]: 4,
+ [DECADE]: 3,
+};
+
+class DateTable extends React.Component {
+ static propTypes = {
+ mode: SharedPT.panelMode,
+ value: SharedPT.date,
+ panelValue: SharedPT.date,
+ dateCellRender: PT.func,
+ quarterCellRender: PT.func,
+ monthCellRender: PT.func,
+ yearCellRender: PT.func,
+ disabledDate: PT.func,
+ hoveredState: PT.func,
+ onSelect: PT.func,
+ cellClassName: PT.oneOfType([PT.func, PT.string]),
+ colNum: PT.number,
+ cellProps: PT.object,
+ };
+ prefixCls: string;
+
+ constructor(props: DateTableProps) {
+ super(props);
+
+ this.prefixCls = `${props.prefix}calendar2`;
+
+ bindCtx(this, [
+ 'getDateCellData',
+ 'getMonthCellData',
+ 'getQuarterCellData',
+ 'getYearCellData',
+ 'getDecadeData',
+ 'handleKeyDown',
+ 'handleSelect',
+ 'handleMouseEnter',
+ 'handleMouseLeave',
+ ]);
+
+ this.state = {
+ hoverValue: null,
+ };
+ }
+
+ handleSelect(
+ v: Dayjs,
+ e: KeyboardEvent | MouseEvent,
+ args: Pick
+ ) {
+ func.invoke(this.props, 'onSelect', [v, e, args]);
+ }
+
+ handleKeyDown(
+ v: Dayjs,
+ e: KeyboardEvent,
+ args: Pick
+ ) {
+ switch (e.keyCode) {
+ case KEYCODE.ENTER:
+ this.handleSelect(v, e, args);
+ break;
+ case KEYCODE.RIGHT:
+ break;
+ }
+ // e.preventDefault();
+ }
+
+ handleMouseEnter(
+ v: Dayjs,
+ e: MouseEvent,
+ args: Pick
+ ) {
+ func.invoke(this.props.cellProps, 'onMouseEnter', [v, e, args]);
+ }
+
+ handleMouseLeave(
+ v: Dayjs,
+ e: MouseEvent,
+ args: Pick
+ ) {
+ func.invoke(this.props.cellProps, 'onMouseLeave', [v, e, args]);
+ }
+
+ isSame(curDate: Dayjs, date: ConfigType, mode: CalendarPanelMode) {
+ switch (mode) {
+ case DATE:
+ return curDate.isSame(date, 'day');
+ case WEEK:
+ return curDate.isSame(date, 'week');
+ case QUARTER:
+ return curDate.isSame(date, 'quarter');
+ case MONTH:
+ return curDate.isSame(date, 'month');
+ case YEAR:
+ return curDate.isSame(date, 'year');
+ case DECADE: {
+ const curYear = curDate.year();
+ // @ts-expect-error mode 为 decade 时要求 date 为 dayjs
+ const targetYear = date.year();
+ return curYear <= targetYear && targetYear < curYear + 10;
+ }
+ }
+ }
+
+ getCustomRender = (mode: CalendarPanelMode) => {
+ const mode2RenderName: Record<
+ string,
+ 'dateCellRender' | 'monthCellRender' | 'quarterCellRender' | 'yearCellRender'
+ > = {
+ [DATE]: 'dateCellRender',
+ [QUARTER]: 'quarterCellRender',
+ [MONTH]: 'monthCellRender',
+ [YEAR]: 'yearCellRender',
+ };
+
+ return this.props[mode2RenderName[mode]];
+ };
+
+ /**
+ * 渲染日期面板
+ * @param cellData - 单元格数据
+ */
+ renderCellContent(cellData: CellData[]) {
+ const { props } = this;
+ const { mode, hoveredState, cellClassName } = props;
+ const { hoverValue } = this.state;
+
+ const cellContent = [];
+ const cellCls = `${this.prefixCls}-cell`;
+
+ const now = datejs();
+ const rowLen = mode2Rows[mode];
+
+ for (let i = 0; i < cellData.length; ) {
+ const children = [];
+
+ let isCurrentWeek;
+ for (let j = 0; j < rowLen; j++) {
+ const { label, value, key, isCurrent } = cellData[i++];
+ // @ts-expect-error decade/quarter 不能作为 startOf 的参数
+ const v = value.startOf(mode);
+
+ const isDisabled = props.disabledDate && props.disabledDate(v, mode);
+ const hoverState = hoverValue && hoveredState && hoveredState(hoverValue);
+ const className = classnames(cellCls, {
+ [`${cellCls}-current`]: isCurrent, // 是否属于当前面板值
+ [`${cellCls}-today`]:
+ mode === WEEK ? this.isSame(value, now, DATE) : this.isSame(v, now, mode),
+ [`${cellCls}-selected`]: this.isSame(v, props.value, mode),
+ [`${cellCls}-disabled`]: isDisabled,
+ [`${cellCls}-range-hover`]: hoverState,
+ ...(cellClassName && cellClassName(v)),
+ });
+
+ let onEvents = null;
+
+ if (!isDisabled) {
+ onEvents = {
+ onClick: (e: MouseEvent) =>
+ this.handleSelect(v, e, { isCurrent, label }),
+ onKeyDown: (e: KeyboardEvent) =>
+ this.handleKeyDown(v, e, { isCurrent, label }),
+ onMouseEnter: (e: MouseEvent) =>
+ this.handleMouseEnter(v, e, { isCurrent, label }),
+ onMouseLeave: (e: MouseEvent) =>
+ this.handleMouseLeave(v, e, { isCurrent, label }),
+ };
+ }
+
+ if (mode === WEEK && j === 0) {
+ const week = v.week();
+
+ children.push(
+
+ {week}
+
+ );
+ isCurrentWeek = isCurrent;
+ }
+
+ const customRender = this.getCustomRender(mode);
+
+ children.push(
+
+
+ {renderNode(
+ customRender,
+
{label}
,
+ [v]
+ )}
+
+
+ );
+ }
+
+ let className;
+ if (mode === WEEK) {
+ className = classnames(`${this.prefixCls}-week`, {
+ [`${this.prefixCls}-week-current`]: isCurrentWeek,
+ });
+ }
+
+ cellContent.push(
+
+ {children}
+
+ );
+ }
+
+ return cellContent;
+ }
+
+ // 星期几
+ renderWeekdaysHead() {
+ let weekdaysMin = datejs.weekdaysMin();
+ const firstDayOfWeek = datejs.localeData().firstDayOfWeek();
+
+ // 默认一周的第一天是周日,否则需要调整
+ if (firstDayOfWeek !== 0) {
+ weekdaysMin = weekdaysMin
+ .slice(firstDayOfWeek)
+ .concat(weekdaysMin.slice(0, firstDayOfWeek)) as WeekdayNames;
+ }
+
+ return (
+
+
+ {/* 占位 */}
+ {this.props.mode === WEEK ? (
+
+ ) : null}
+ {weekdaysMin.map(d => {
+ return {d} ;
+ })}
+
+
+ );
+ }
+
+ getDateCellData() {
+ const { panelValue, colNum } = this.props;
+
+ const firstDayOfMonth = panelValue.clone().startOf('month');
+ const weekOfFirstDay = firstDayOfMonth.day(); // 当月第一天星期几
+ const daysOfCurMonth = panelValue.endOf('month').date(); // 当月天数
+ const firstDayOfWeek = datejs.localeData().firstDayOfWeek(); // 一周的第一天是星期几
+
+ const cellData: Dayjs[] = [];
+ const preDays = (weekOfFirstDay - firstDayOfWeek + 7) % 7;
+ const nextDays = colNum
+ ? colNum * mode2Rows[DATE] - preDays - daysOfCurMonth
+ : (7 - ((preDays + daysOfCurMonth) % 7)) % 7;
+
+ // 上个月日期
+ for (let i = preDays; i > 0; i--) {
+ cellData.push(firstDayOfMonth.clone().subtract(i, 'day'));
+ }
+
+ // 本月日期
+ for (let i = 0; i < daysOfCurMonth; i++) {
+ cellData.push(firstDayOfMonth.clone().add(i, 'day'));
+ }
+
+ // 下个月日期
+ for (let i = 0; i < nextDays; i++) {
+ cellData.push(firstDayOfMonth.clone().add(daysOfCurMonth + i, 'day'));
+ }
+
+ return cellData.map(value => {
+ return {
+ value,
+ label: value.date(),
+ isCurrent: value.isSame(firstDayOfMonth, 'month'),
+ key: value.format('YYYY-MM-DD'),
+ };
+ });
+ }
+
+ getMonthCellData() {
+ const { panelValue } = this.props;
+
+ return datejs.monthsShort().map((label, index) => {
+ const value = panelValue.clone().month(index);
+
+ return {
+ label,
+ value,
+ isCurrent: true,
+ key: value.format('YYYY-MM'),
+ };
+ });
+ }
+
+ getQuarterCellData() {
+ const { panelValue } = this.props;
+
+ return [1, 2, 3, 4].map(i => {
+ return {
+ label: `Q${i}`,
+ value: panelValue.clone().quarter(i),
+ isCurrent: true,
+ key: `Q${i}`,
+ };
+ });
+ }
+
+ getYearCellData() {
+ const { panelValue } = this.props;
+ const curYear = panelValue.year();
+ const startYear = curYear - (curYear % 10) - 1;
+ const cellData = [];
+
+ for (let i = 0; i < 12; i++) {
+ const y = startYear + i;
+
+ cellData.push({
+ value: panelValue.clone().year(y),
+ label: y,
+ isCurrent: i > 0 && i < 11,
+ key: y,
+ });
+ }
+
+ return cellData;
+ }
+
+ getDecadeData() {
+ const { panelValue } = this.props;
+ const curYear = panelValue.year();
+ const startYear = curYear - (curYear % 100) - 10;
+ const cellData = [];
+
+ for (let i = 0; i < 12; i++) {
+ const y = startYear + i * 10;
+
+ cellData.push({
+ value: panelValue.clone().year(y),
+ label: `${y}-${y + 9}`,
+ isCurrent: i > 0 && i < 11,
+ key: `${y}-${y + 9}`,
+ });
+ }
+
+ return cellData;
+ }
+
+ render() {
+ const { mode } = this.props;
+ const mode2Data = {
+ [DATE]: this.getDateCellData,
+ [WEEK]: this.getDateCellData,
+ [MONTH]: this.getMonthCellData,
+ [QUARTER]: this.getQuarterCellData,
+ [YEAR]: this.getYearCellData,
+ [DECADE]: this.getDecadeData,
+ };
+
+ return (
+
+ {[DATE, WEEK].includes(mode) ? this.renderWeekdaysHead() : null}
+ {this.renderCellContent(mode2Data[mode]())}
+
+ );
+ }
+}
+
+export default polyfill(DateTable);
diff --git a/components/calendar2/panels/header-panel.jsx b/components/calendar2/panels/header-panel.jsx
deleted file mode 100644
index 906f899b9a..0000000000
--- a/components/calendar2/panels/header-panel.jsx
+++ /dev/null
@@ -1,346 +0,0 @@
-import React from 'react';
-import { polyfill } from 'react-lifecycles-compat';
-import PT from 'prop-types';
-import { func, datejs } from '../../util';
-
-import { CALENDAR_MODE, DATE_PANEL_MODE, CALENDAR_SHAPE } from '../constant';
-import Radio from '../../radio';
-import Select from '../../select';
-import Button from '../../button';
-import Icon from '../../icon';
-
-const { renderNode } = func;
-const { DATE, WEEK, QUARTER, MONTH, YEAR, DECADE } = DATE_PANEL_MODE;
-
-class HeaderPanel extends React.PureComponent {
- static propTypes = {
- rtl: PT.bool,
- prefix: PT.string,
- locale: PT.object,
- mode: PT.any,
- shape: PT.string,
- value: PT.any,
- panelMode: PT.any,
- panelValue: PT.any,
- validValue: PT.any,
- showTitle: PT.bool,
- showModeSwitch: PT.bool,
- onModeChange: PT.func,
- onPanelValueChange: PT.func,
- onPanelModeChange: PT.func,
- onPrev: PT.func,
- onNext: PT.func,
- onSuperPrev: PT.func,
- onSuperNext: PT.func,
- titleRender: PT.func,
- /**
- * 扩展操作区域渲染
- */
- renderHeaderExtra: PT.func,
- /**
- * 自定义头部渲染
- */
- headerRender: PT.func,
- };
-
- static defaultProps = {
- showTitle: false,
- };
-
- constructor(props) {
- super(props);
- this.prefixCls = `${props.prefix}calendar2-header`;
- }
-
- createPanelBtns = ({ unit, num = 1, isSuper = true }) => {
- const value = this.props.panelValue.clone();
- const { prefixCls } = this;
- const iconTypes = isSuper
- ? ['arrow-double-left', 'arrow-double-right']
- : ['arrow-left', 'arrow-right'];
-
- return [
- this.handleClick(value, { num, unit, isSuper, isNext: false })}
- key={`prev-btn-${unit}`}
- >
-
- ,
- this.handleClick(value, { num, unit, isSuper, isNext: true })}
- key={`next-btn-${unit}`}
- >
-
- ,
- ];
- };
-
- handleClick(value, { unit, num, isSuper, isNext }) {
- const { onPanelValueChange, onPrev, onSuperPrev, onNext, onSuperNext } = this.props;
-
- let handler;
-
- if (isSuper) {
- handler = isNext ? onSuperNext : onSuperPrev;
- } else {
- handler = isNext ? onNext : onPrev;
- }
-
- if (handler) {
- handler(value, { unit, num });
- } else {
- const m = isNext ? 'add' : 'subtract';
- onPanelValueChange(value[m](num, unit), `PANEL`);
- }
- }
-
- renderModeSwitch = () => {
- const { mode, locale, onModeChange } = this.props;
-
- return (
-
- {locale.month}
- {locale.year}
-
- );
- };
-
- renderMonthSelect = () => {
- const { prefixCls } = this;
- const { panelValue, onPanelValueChange } = this.props;
-
- const curMonth = panelValue.month();
- const dataSource = datejs.monthsShort().map((label, value) => {
- return {
- label,
- value,
- };
- });
-
- return (
- onPanelValueChange(panelValue.month(v))}
- />
- );
- };
-
- renderYearSelect() {
- const { prefixCls } = this;
- const { validValue, panelValue, onPanelValueChange } = this.props;
- const curYear = panelValue.year();
-
- let beginYear;
- let endYear;
-
- // TODO 有效区间
- if (validValue) {
- const [begin, end] = validValue;
- beginYear = begin.year();
- endYear = end.year();
- } else {
- for (let i = 0; i < 20; i++) {
- beginYear = curYear - 10;
- endYear = curYear + 10;
- }
- }
- const dataSource = [];
- for (let i = beginYear; i < endYear; i++) {
- dataSource.push({ label: i, value: i });
- }
-
- return (
- onPanelValueChange(panelValue.year(v))}
- />
- );
- }
-
- renderTextField() {
- const { prefixCls } = this;
- const { panelValue, locale, panelMode, onPanelModeChange } = this.props;
-
- const monthBeforeYear = locale.monthBeforeYear || false;
- const localeData = datejs.localeData();
-
- const year = panelValue.year() + (locale && locale.year === '年' ? '年' : '');
- const month = localeData.monthsShort()[panelValue.month()];
- const { DATE, WEEK, QUARTER, MONTH, YEAR, DECADE } = DATE_PANEL_MODE;
-
- let nodes;
- const yearNode = (
- onPanelModeChange(YEAR)}
- >
- {year}
-
- );
-
- switch (panelMode) {
- case DATE:
- case WEEK: {
- const monthNode = (
- onPanelModeChange(MONTH)}
- >
- {month}
-
- );
- nodes = monthBeforeYear ? [monthNode, yearNode] : [yearNode, monthNode];
- break;
- }
-
- case MONTH:
- case QUARTER: {
- nodes = yearNode;
- break;
- }
-
- case YEAR: {
- const curYear = panelValue.year();
- const start = curYear - (curYear % 10);
- const end = start + 9;
- nodes = (
- onPanelModeChange(DECADE)}
- >
- {this.props.rtl ? `${end}-${start}` : `${start}-${end}`}
-
- );
- break;
- }
- case DECADE: {
- const curYear = panelValue.year();
- const start = curYear - (curYear % 100);
- const end = start + 99;
-
- nodes = this.props.rtl ? `${end}-${start}` : `${start}-${end}`;
- break;
- }
- }
-
- return (
-
- {nodes}
-
- );
- }
-
- renderPanelHeader() {
- const { createPanelBtns } = this;
- const { panelMode } = this.props;
-
- let nodes = [];
-
- const textFieldNode = this.renderTextField();
-
- switch (panelMode) {
- case DATE:
- case WEEK: {
- const [preMonthBtn, nextMonthBtn] = createPanelBtns({
- unit: 'month',
- isSuper: false,
- });
- const [preYearBtn, nextYearBtn] = createPanelBtns({
- unit: 'year',
- });
-
- nodes = [preYearBtn, preMonthBtn, textFieldNode, nextMonthBtn, nextYearBtn];
- break;
- }
- case QUARTER:
- case MONTH: {
- const [preYearBtn, nextYearBtn] = createPanelBtns({
- unit: 'year',
- });
-
- nodes = [preYearBtn, textFieldNode, nextYearBtn];
- break;
- }
- case YEAR: {
- const [preDecadeBtn, nextDecadeBtn] = createPanelBtns({
- unit: 'year',
- num: 10,
- });
-
- nodes = [preDecadeBtn, textFieldNode, nextDecadeBtn];
- break;
- }
- case DECADE: {
- const [preCenturyBtn, nextCenturyBtn] = createPanelBtns({
- unit: 'year',
- num: 100,
- });
-
- nodes = [preCenturyBtn, textFieldNode, nextCenturyBtn];
- break;
- }
- }
-
- return nodes;
- }
-
- renderInner() {
- const { prefixCls } = this;
- const { shape, showTitle, value, mode, showModeSwitch } = this.props;
-
- const nodes = [];
-
- if (shape === CALENDAR_SHAPE.PANEL) {
- return this.renderPanelHeader();
- } else {
- if (showTitle) {
- nodes.push(
-
- {renderNode(this.props.titleRender, value.format(), [value])}
-
- );
- }
- nodes.push(
-
- {this.renderYearSelect()}
- {mode !== CALENDAR_MODE.YEAR ? this.renderMonthSelect() : null}
- {showModeSwitch ? this.renderModeSwitch() : null}
- {this.props.renderHeaderExtra &&
- this.props.renderHeaderExtra({ ...this.props })}
-
- );
- }
-
- return nodes;
- }
-
- render() {
- return (
-
- {renderNode(this.props.headerRender, this.renderInner(), { ...this.props })}
-
- );
- }
-}
-
-export default polyfill(HeaderPanel);
diff --git a/components/calendar2/panels/header-panel.tsx b/components/calendar2/panels/header-panel.tsx
new file mode 100644
index 0000000000..c241ea1d7a
--- /dev/null
+++ b/components/calendar2/panels/header-panel.tsx
@@ -0,0 +1,368 @@
+import React, { type ReactElement } from 'react';
+import { polyfill } from 'react-lifecycles-compat';
+import PT from 'prop-types';
+import type { Dayjs, ManipulateType } from 'dayjs';
+import { func, datejs } from '../../util';
+
+import { CALENDAR_MODE, DATE_PANEL_MODE, CALENDAR_SHAPE } from '../constant';
+import Radio from '../../radio';
+import Select from '../../select';
+import Button from '../../button';
+import Icon from '../../icon';
+import type { HeaderPanelProps } from '../types';
+
+const { renderNode } = func;
+const { DATE, WEEK, QUARTER, MONTH, YEAR, DECADE } = DATE_PANEL_MODE;
+
+class HeaderPanel extends React.PureComponent {
+ static propTypes = {
+ rtl: PT.bool,
+ prefix: PT.string,
+ locale: PT.object,
+ mode: PT.any,
+ shape: PT.string,
+ value: PT.any,
+ panelMode: PT.any,
+ panelValue: PT.any,
+ validValue: PT.any,
+ showTitle: PT.bool,
+ showModeSwitch: PT.bool,
+ onModeChange: PT.func,
+ onPanelValueChange: PT.func,
+ onPanelModeChange: PT.func,
+ onPrev: PT.func,
+ onNext: PT.func,
+ onSuperPrev: PT.func,
+ onSuperNext: PT.func,
+ titleRender: PT.func,
+ /**
+ * 扩展操作区域渲染
+ */
+ renderHeaderExtra: PT.func,
+ /**
+ * 自定义头部渲染
+ */
+ headerRender: PT.func,
+ };
+
+ static defaultProps = {
+ showTitle: false,
+ };
+ prefixCls: string;
+
+ constructor(props: HeaderPanelProps) {
+ super(props);
+ this.prefixCls = `${props.prefix}calendar2-header`;
+ }
+
+ createPanelBtns = ({
+ unit,
+ num = 1,
+ isSuper = true,
+ }: {
+ unit: ManipulateType;
+ num?: number;
+ isSuper?: boolean;
+ }) => {
+ const value = this.props.panelValue.clone();
+ const { prefixCls } = this;
+ const iconTypes = isSuper
+ ? ['arrow-double-left', 'arrow-double-right']
+ : ['arrow-left', 'arrow-right'];
+
+ return [
+ this.handleClick(value, { num, unit, isSuper, isNext: false })}
+ key={`prev-btn-${unit}`}
+ >
+
+ ,
+ this.handleClick(value, { num, unit, isSuper, isNext: true })}
+ key={`next-btn-${unit}`}
+ >
+
+ ,
+ ];
+ };
+
+ handleClick(
+ value: Dayjs,
+ {
+ unit,
+ num,
+ isSuper,
+ isNext,
+ }: { unit: ManipulateType; num: number; isSuper?: boolean; isNext?: boolean }
+ ) {
+ const { onPanelValueChange, onPrev, onSuperPrev, onNext, onSuperNext } = this.props;
+
+ let handler;
+
+ if (isSuper) {
+ handler = isNext ? onSuperNext : onSuperPrev;
+ } else {
+ handler = isNext ? onNext : onPrev;
+ }
+
+ if (handler) {
+ handler(value, { unit, num });
+ } else {
+ const m = isNext ? 'add' : 'subtract';
+ onPanelValueChange(value[m](num, unit), `PANEL`);
+ }
+ }
+
+ renderModeSwitch = () => {
+ const { mode, locale, onModeChange } = this.props;
+
+ return (
+
+ {locale.month}
+ {locale.year}
+
+ );
+ };
+
+ renderMonthSelect = () => {
+ const { prefixCls } = this;
+ const { panelValue, onPanelValueChange } = this.props;
+
+ const curMonth = panelValue.month();
+ const dataSource = datejs.monthsShort().map((label, value) => {
+ return {
+ label,
+ value,
+ };
+ });
+
+ return (
+ onPanelValueChange(panelValue.month(v as number))}
+ />
+ );
+ };
+
+ renderYearSelect() {
+ const { prefixCls } = this;
+ const { validValue, panelValue, onPanelValueChange } = this.props;
+ const curYear = panelValue.year();
+
+ let beginYear: number;
+ let endYear: number;
+
+ // TODO 有效区间
+ if (validValue) {
+ const [begin, end] = validValue;
+ beginYear = begin.year();
+ endYear = end.year();
+ } else {
+ for (let i = 0; i < 20; i++) {
+ beginYear = curYear - 10;
+ endYear = curYear + 10;
+ }
+ }
+ const dataSource = [];
+ for (let i = beginYear!; i < endYear!; i++) {
+ dataSource.push({ label: i, value: i });
+ }
+
+ return (
+ onPanelValueChange(panelValue.year(v as number))}
+ />
+ );
+ }
+
+ renderTextField() {
+ const { prefixCls } = this;
+ const { panelValue, locale, panelMode, onPanelModeChange } = this.props;
+
+ const monthBeforeYear = locale.monthBeforeYear || false;
+ const localeData = datejs.localeData();
+
+ const year = panelValue.year() + (locale && locale.year === '年' ? '年' : '');
+ const month = localeData.monthsShort()[panelValue.month()];
+ const { DATE, WEEK, QUARTER, MONTH, YEAR, DECADE } = DATE_PANEL_MODE;
+
+ let nodes;
+ const yearNode = (
+ onPanelModeChange(YEAR)}
+ >
+ {year}
+
+ );
+
+ switch (panelMode) {
+ case DATE:
+ case WEEK: {
+ const monthNode = (
+ onPanelModeChange(MONTH)}
+ >
+ {month}
+
+ );
+ nodes = monthBeforeYear ? [monthNode, yearNode] : [yearNode, monthNode];
+ break;
+ }
+
+ case MONTH:
+ case QUARTER: {
+ nodes = yearNode;
+ break;
+ }
+
+ case YEAR: {
+ const curYear = panelValue.year();
+ const start = curYear - (curYear % 10);
+ const end = start + 9;
+ nodes = (
+ onPanelModeChange(DECADE)}
+ >
+ {this.props.rtl ? `${end}-${start}` : `${start}-${end}`}
+
+ );
+ break;
+ }
+ case DECADE: {
+ const curYear = panelValue.year();
+ const start = curYear - (curYear % 100);
+ const end = start + 99;
+
+ nodes = this.props.rtl ? `${end}-${start}` : `${start}-${end}`;
+ break;
+ }
+ }
+
+ return (
+
+ {nodes}
+
+ );
+ }
+
+ renderPanelHeader() {
+ const { createPanelBtns } = this;
+ const { panelMode } = this.props;
+
+ let nodes: ReactElement[] = [];
+
+ const textFieldNode = this.renderTextField();
+
+ switch (panelMode) {
+ case DATE:
+ case WEEK: {
+ const [preMonthBtn, nextMonthBtn] = createPanelBtns({
+ unit: 'month',
+ isSuper: false,
+ });
+ const [preYearBtn, nextYearBtn] = createPanelBtns({
+ unit: 'year',
+ });
+
+ nodes = [preYearBtn, preMonthBtn, textFieldNode, nextMonthBtn, nextYearBtn];
+ break;
+ }
+ case QUARTER:
+ case MONTH: {
+ const [preYearBtn, nextYearBtn] = createPanelBtns({
+ unit: 'year',
+ });
+
+ nodes = [preYearBtn, textFieldNode, nextYearBtn];
+ break;
+ }
+ case YEAR: {
+ const [preDecadeBtn, nextDecadeBtn] = createPanelBtns({
+ unit: 'year',
+ num: 10,
+ });
+
+ nodes = [preDecadeBtn, textFieldNode, nextDecadeBtn];
+ break;
+ }
+ case DECADE: {
+ const [preCenturyBtn, nextCenturyBtn] = createPanelBtns({
+ unit: 'year',
+ num: 100,
+ });
+
+ nodes = [preCenturyBtn, textFieldNode, nextCenturyBtn];
+ break;
+ }
+ }
+
+ return nodes;
+ }
+
+ renderInner() {
+ const { prefixCls } = this;
+ const { shape, showTitle, value, mode, showModeSwitch } = this.props;
+
+ const nodes: ReactElement[] = [];
+
+ if (shape === CALENDAR_SHAPE.PANEL) {
+ return this.renderPanelHeader();
+ } else {
+ if (showTitle) {
+ nodes.push(
+
+ {/* @ts-expect-error value 可能不是 Dayjs 形式 */}
+ {renderNode(this.props.titleRender, value!.format(), [value])}
+
+ );
+ }
+ nodes.push(
+
+ {this.renderYearSelect()}
+ {mode !== CALENDAR_MODE.YEAR ? this.renderMonthSelect() : null}
+ {showModeSwitch ? this.renderModeSwitch() : null}
+ {this.props.renderHeaderExtra &&
+ this.props.renderHeaderExtra({ ...this.props })}
+
+ );
+ }
+
+ return nodes;
+ }
+
+ render() {
+ return (
+
+ {renderNode(this.props.headerRender, this.renderInner(), { ...this.props })}
+
+ );
+ }
+}
+
+export default polyfill(HeaderPanel);
diff --git a/components/calendar2/prop-types.js b/components/calendar2/prop-types.js
deleted file mode 100644
index 9673b932a8..0000000000
--- a/components/calendar2/prop-types.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import PT from 'prop-types';
-import { CALENDAR_SHAPE, CALENDAR_MODE, DATE_PANEL_MODE } from './constant';
-import { datejs } from '../util';
-
-const error = (propName, ComponentName) =>
- new Error(`Invalid prop ${propName} supplied to ${ComponentName}. Validation failed.`);
-
-const SharedPT = {
- shape: PT.oneOf(Object.values(CALENDAR_SHAPE)),
- mode: PT.oneOf(Object.values(CALENDAR_MODE)),
- panelMode: PT.oneOf(Object.values(DATE_PANEL_MODE)),
- // 日期类型:
- // @string: 2020-11-11
- // @date: 日期对象
- // @moment: moment对象
- // @dayjs: dayjs对象
- date(props, propName, componentName) {
- if (propName in props && !datejs(props.propName).isValid()) {
- throw error(propName, componentName);
- }
- },
-};
-
-export default SharedPT;
diff --git a/components/calendar2/prop-types.ts b/components/calendar2/prop-types.ts
new file mode 100644
index 0000000000..83a0f2299c
--- /dev/null
+++ b/components/calendar2/prop-types.ts
@@ -0,0 +1,26 @@
+import PT from 'prop-types';
+import type { ConfigType } from 'dayjs';
+import { CALENDAR_SHAPE, CALENDAR_MODE, DATE_PANEL_MODE } from './constant';
+import { datejs } from '../util';
+
+const error = (propName: string, ComponentName: string) =>
+ new Error(`Invalid prop ${propName} supplied to ${ComponentName}. Validation failed.`);
+
+const SharedPT = {
+ shape: PT.oneOf(Object.values(CALENDAR_SHAPE)),
+ mode: PT.oneOf(Object.values(CALENDAR_MODE)),
+ panelMode: PT.oneOf(Object.values(DATE_PANEL_MODE)),
+ // 日期类型:
+ // @string: 2020-11-11
+ // @date: 日期对象
+ // @moment: moment 对象
+ // @dayjs: dayjs 对象
+ date(props: Record, propName: string, componentName: string) {
+ if (propName in props && !datejs(props[propName] as ConfigType).isValid()) {
+ return error(propName, componentName);
+ }
+ return null;
+ },
+};
+
+export default SharedPT;
diff --git a/components/calendar2/style.js b/components/calendar2/style.ts
similarity index 100%
rename from components/calendar2/style.js
rename to components/calendar2/style.ts
diff --git a/components/calendar2/types.ts b/components/calendar2/types.ts
new file mode 100644
index 0000000000..ea9420a351
--- /dev/null
+++ b/components/calendar2/types.ts
@@ -0,0 +1,281 @@
+import type React from 'react';
+import type { Dayjs, ConfigType, ManipulateType } from 'dayjs';
+import type { CommonProps } from '../util';
+import type { Locale } from '../locale/types';
+
+interface HTMLAttributesWeak
+ extends Omit, 'onChange' | 'defaultValue' | 'onSelect'> {}
+
+/**
+ * @api
+ */
+export interface CalendarProps extends HTMLAttributesWeak, Omit {
+ /**
+ * @deprecated use Form.Item name instead
+ * @skip
+ */
+ name?: string;
+ /**
+ * 默认选中的日期(dayjs 对象)
+ * @en Default selected date (dayjs object)
+ */
+ defaultValue?: ConfigType;
+
+ /**
+ * 选中的日期值 (dayjs 对象)
+ * @en Selected date value (dayjs object)
+ */
+ value?: ConfigType;
+
+ /**
+ * 面板默认显示的日期
+ * @en Default displayed date
+ */
+ defaultPanelValue?: ConfigType;
+
+ /**
+ * 面板显示的日期(受控)
+ * @en Displayed date
+ */
+ panelValue?: ConfigType;
+
+ /**
+ * 展现形态
+ * @en Display shape
+ * @defaultValue 'fullscreen'
+ */
+ shape?: 'card' | 'fullscreen' | 'panel';
+
+ /**
+ * 日期模式
+ * @en Date mode
+ * @defaultValue 'month'
+ */
+ mode?: CalendarMode;
+
+ /**
+ * 面板模式,未指定时会根据 mode 自动推断
+ * @en Panel mode, will be inferred automatically if not specified
+ */
+ panelMode?: CalendarPanelMode;
+
+ /**
+ * 选择日期单元格时的回调
+ * @en Callback when selecting a date cell
+ */
+ onSelect?: (value: Dayjs, strVal: string) => void;
+
+ /**
+ * 值改变时的回调
+ * @en Callback when value changes
+ */
+ onChange?: (value: Dayjs, strVal: string) => void;
+
+ /**
+ * 日期面板变化回调
+ * @en Callback when date panel changes
+ */
+ onPanelChange?: (value: Dayjs, mode: string, reason?: string) => void;
+
+ /**
+ * 自定义样式类
+ * @en Custom style class
+ */
+ className?: string;
+
+ /**
+ * 自定义日期渲染
+ * @en Custom date rendering
+ */
+ dateCellRender?: CustomCellRender;
+
+ /**
+ * 自定义月份渲染函数
+ * @en Custom month rendering function
+ */
+ monthCellRender?: CustomCellRender;
+
+ /**
+ * 自定义年份渲染函数
+ * @en Custom year rendering function
+ */
+ yearCellRender?: CustomCellRender;
+ /**
+ * 自定义季度渲染函数
+ * @en Custom quarter rendering function
+ */
+ quarterCellRender?: CustomCellRender;
+ /**
+ * 不可选择的日期
+ * @en Disabled date
+ */
+ disabledDate?: (value: Dayjs, mode: CalendarPanelMode) => boolean;
+ /**
+ * @skip
+ */
+ locale?: Locale['Calendar'];
+ /**
+ * 点击头部左单箭头时触发的回调
+ * @en Callback when clicking the left single arrow
+ */
+ onPrev?: OnPrevOrNext;
+ /**
+ * 点击头部右单箭头时触发的回调
+ * @en Callback when clicking the right single arrow
+ */
+ onNext?: OnPrevOrNext;
+ /**
+ * 点击头部左双箭头时触发的回调
+ * @en callback when clicking the left double arrow
+ */
+ onSuperPrev?: OnPrevOrNext;
+ /**
+ * 点击头部右双箭头时触发的回调
+ * @en callback when clicking the right double arrow
+ */
+ onSuperNext?: OnPrevOrNext;
+ /**
+ * 头部自定义渲染
+ * @en Header custom rendering
+ */
+ headerRender?: (props: HeaderPanelProps) => React.ReactNode;
+ /**
+ * 可选择的年份的有效区间
+ * @en Valid year range
+ */
+ validValue?: [Dayjs, Dayjs];
+ /**
+ * 渲染头部额外内容
+ * @en Render header extra content
+ */
+ renderHeaderExtra?: (props: HeaderPanelProps) => React.ReactNode;
+ /**
+ * 渲染头部标题,仅当 showTitle 为 true 时生效
+ * @en Render header title
+ * @skip
+ */
+ titleRender?: (value: ConfigType) => React.ReactNode;
+ /**
+ * 显示头部标题
+ * @en Show header title
+ * @skip
+ * @remarks 实现有问题暂不开放
+ */
+ showTitle?: boolean;
+ /**
+ * 展示的总行数
+ * @en Total rows
+ * @skip
+ * @remarks 命名有些问题,感觉应该叫 rowNum
+ */
+ colNum?: number;
+ /**
+ * @skip
+ * @deprecated not implemented
+ */
+ hoveredState?: (value: unknown) => boolean;
+ /**
+ * 单元格自定义样式
+ * @en Cell custom style
+ */
+ cellClassName?: (value: Dayjs) => Record | undefined | null;
+ /**
+ * 单元格自定义属性
+ * @en Cell custom property
+ */
+ cellProps?: {
+ onMouseEnter?: (
+ v: Dayjs,
+ e: React.MouseEvent,
+ args: Pick
+ ) => void;
+ onMouseLeave?: (
+ v: Dayjs,
+ e: React.MouseEvent,
+ args: Pick
+ ) => void;
+ };
+}
+
+/**
+ * @api
+ */
+export type OnPrevOrNext = (value: Dayjs, options: { unit: ManipulateType; num: number }) => void;
+
+/**
+ * @api
+ */
+export type CalendarMode = 'month' | 'year';
+
+/**
+ * @api
+ */
+export type CalendarPanelMode = 'date' | 'week' | 'month' | 'quarter' | 'year' | 'decade';
+
+export interface CalendarState {
+ value: CalendarProps['value'];
+ mode: CalendarMode;
+ panelMode: CalendarPanelMode;
+ panelValue: Dayjs;
+}
+
+export interface HeaderPanelProps extends Omit {
+ panelValue: Dayjs;
+ locale: Locale['Calendar'];
+ onPanelValueChange: (value: Dayjs, type?: 'PANEL') => void;
+ onPrev?: OnPrevOrNext;
+ onNext?: OnPrevOrNext;
+ onSuperPrev?: OnPrevOrNext;
+ onSuperNext?: OnPrevOrNext;
+ mode: CalendarMode;
+ onModeChange: (mode: CalendarMode) => void;
+ validValue?: CalendarProps['validValue'];
+ panelMode: CalendarPanelMode;
+ onPanelModeChange: (mode: CalendarPanelMode) => void;
+ shape: CalendarProps['shape'];
+ showTitle: boolean;
+ value?: CalendarState['value'];
+ showModeSwitch: boolean;
+ renderHeaderExtra?: CalendarProps['renderHeaderExtra'];
+ headerRender?: CalendarProps['headerRender'];
+ titleRender?: CalendarProps['titleRender'];
+}
+
+/**
+ * @api
+ */
+export type CustomCellRender = (value: Dayjs) => React.ReactNode;
+
+export interface DateTableProps extends Omit {
+ mode: CalendarPanelMode;
+ panelValue: Dayjs;
+ colNum?: number;
+ hoveredState?: CalendarProps['hoveredState'];
+ cellClassName?: CalendarProps['cellClassName'];
+ dateCellRender?: CustomCellRender;
+ quarterCellRender?: CustomCellRender;
+ monthCellRender?: CustomCellRender;
+ yearCellRender?: CustomCellRender;
+ cellProps?: CalendarProps['cellProps'];
+ disabledDate?: CalendarProps['disabledDate'];
+ onSelect?: (
+ v: Dayjs,
+ e: React.MouseEvent | React.KeyboardEvent,
+ args: Pick
+ ) => void;
+ value: ConfigType;
+}
+
+export interface DateTableState {
+ hoverValue: unknown;
+}
+
+/**
+ * @api
+ */
+export interface CellData {
+ value: Dayjs;
+ label: number | string;
+ isCurrent: boolean;
+ key: string | number;
+}
diff --git a/components/card/__docs__/adaptor/index.jsx b/components/card/__docs__/adaptor/index.jsx
deleted file mode 100644
index d6fc1a7db4..0000000000
--- a/components/card/__docs__/adaptor/index.jsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import React from 'react';
-import { Card, Button } from '@alifd/next';
-import { Types } from '@alifd/adaptor-helper';
-
-export default {
- name: 'Card',
- editor: () => ({
- props: [{
- name: 'divider',
- type: Types.bool,
- default: true,
- }, {
- name: 'width',
- type: Types.number,
- default: 300,
- }, {
- name: 'height',
- label: 'height',
- type: Types.number,
- default: 215,
- }, {
- name: 'title',
- type: Types.string,
- default: 'Title'
- }, {
- name: 'subTitle',
- label: 'Subtitle',
- type: Types.string,
- default: '',
- }, {
- name: 'extra',
- label: 'Extra Data',
- type: Types.string,
- default: '',
- }],
- data: {
- default: '',
- }
- }),
- adaptor: ({ bullet, divider, expand, width, height, title, subTitle, extra, style, data, ...others }) => {
- const cardStyle = {
- width: width === 0 ? '' : width,
- height: height === 0 ? 'auto' : height,
- ...style,
- };
-
- return (
-
- {extra}} />
- {divider && }
-
- {data}
-
-
- );
- },
- content: () => ({
- options: [{
- name: 'bullet',
- options: ['show', 'hide'],
- default: 'hide'
- }, {
- name: 'divider',
- options: ['show', 'hide'],
- default: 'show'
- }, {
- name: 'expanded',
- options: ['yes', 'no'],
- default: 'no'
- }, {
- name: 'subTitle',
- options: ['show', 'hide'],
- default: 'hide'
- }, {
- name: 'link',
- options: ['show', 'hide'],
- default: 'hide'
- }],
- transform: (props, { bullet, divider, expanded, subTitle, link }) => {
- return {
- ...props,
- bullet: bullet === 'show',
- divider: divider === 'show',
- expand: expanded === 'yes',
- subTitle: subTitle === 'show' ? 'Sub Title' : '',
- extra: link === 'show' ? 'Link' : '',
- };
- }
- })
-};
diff --git a/components/card/__docs__/adaptor/index.tsx b/components/card/__docs__/adaptor/index.tsx
new file mode 100644
index 0000000000..2f2d7fdfb7
--- /dev/null
+++ b/components/card/__docs__/adaptor/index.tsx
@@ -0,0 +1,121 @@
+import React from 'react';
+import { Card, Button } from '@alifd/next';
+import { Types } from '@alifd/adaptor-helper';
+
+export default {
+ name: 'Card',
+ editor: () => ({
+ props: [
+ {
+ name: 'divider',
+ type: Types.bool,
+ default: true,
+ },
+ {
+ name: 'width',
+ type: Types.number,
+ default: 300,
+ },
+ {
+ name: 'height',
+ label: 'height',
+ type: Types.number,
+ default: 215,
+ },
+ {
+ name: 'title',
+ type: Types.string,
+ default: 'Title',
+ },
+ {
+ name: 'subTitle',
+ label: 'Subtitle',
+ type: Types.string,
+ default: '',
+ },
+ {
+ name: 'extra',
+ label: 'Extra Data',
+ type: Types.string,
+ default: '',
+ },
+ ],
+ data: {
+ default: '',
+ },
+ }),
+ adaptor: ({
+ bullet,
+ divider,
+ expand,
+ width,
+ height,
+ title,
+ subTitle,
+ extra,
+ style,
+ data,
+ ...others
+ }: any) => {
+ const cardStyle = {
+ width: width === 0 ? '' : width,
+ height: height === 0 ? 'auto' : height,
+ ...style,
+ };
+
+ return (
+
+
+ {extra}
+
+ }
+ />
+ {divider && }
+ {data}
+
+ );
+ },
+ content: () => ({
+ options: [
+ {
+ name: 'bullet',
+ options: ['show', 'hide'],
+ default: 'hide',
+ },
+ {
+ name: 'divider',
+ options: ['show', 'hide'],
+ default: 'show',
+ },
+ {
+ name: 'expanded',
+ options: ['yes', 'no'],
+ default: 'no',
+ },
+ {
+ name: 'subTitle',
+ options: ['show', 'hide'],
+ default: 'hide',
+ },
+ {
+ name: 'link',
+ options: ['show', 'hide'],
+ default: 'hide',
+ },
+ ],
+ transform: (props: any, { bullet, divider, expanded, subTitle, link }: any) => {
+ return {
+ ...props,
+ bullet: bullet === 'show',
+ divider: divider === 'show',
+ expand: expanded === 'yes',
+ subTitle: subTitle === 'show' ? 'Sub Title' : '',
+ extra: link === 'show' ? 'Link' : '',
+ };
+ },
+ }),
+};
diff --git a/components/card/__docs__/demo/divider/index.tsx b/components/card/__docs__/demo/divider/index.tsx
index f7ff0037cf..e258fa64cf 100644
--- a/components/card/__docs__/demo/divider/index.tsx
+++ b/components/card/__docs__/demo/divider/index.tsx
@@ -3,7 +3,7 @@ import ReactDOM from 'react-dom';
import { Card, Button, Box } from '@alifd/next';
const commonProps = {
- title: 'Title',
+ title: 'Simple Card',
style: { width: 300 },
subTitle: 'Sub-title',
extra: (
@@ -16,7 +16,7 @@ const commonProps = {
ReactDOM.render(
-
+
Lorem ipsum dolor sit amet, est viderer iuvaret perfecto et. Ne petentium quaerendum
@@ -34,7 +34,7 @@ ReactDOM.render(
-
+
Lorem ipsum dolor sit amet, est viderer iuvaret perfecto et. Ne petentium quaerendum
diff --git a/components/card/__docs__/index.en-us.md b/components/card/__docs__/index.en-us.md
index c864f54cf3..9dc72bd659 100644
--- a/components/card/__docs__/index.en-us.md
+++ b/components/card/__docs__/index.en-us.md
@@ -19,50 +19,51 @@ A card could contain a photo, text, and a link about a single subject.
### Card
-| Param | Description | Type | Default Value |
-| --------------- | ------------ | ------------- | ---- |
-| title | Title of card | String | - |
-| subTitle | Sub title of card | String | - |
-| showTitleBullet | If show title bullet | Boolean | true |
-| showHeadDivider | If show head divider | Boolean | true |
-| contentHeight | Height of content | String/Number | 120 |
-| extra | Extra of card header | ReactNode | - |
-| media | Media content | ReactNode | - |
-| actions | Actions of card | ReactNode | - |
-| free | Whether to open free mode, if opened, can`t set title subTitle ..., must use Card.Header Card.Content ... to set Card | Boolean | - |
+| Param | Description | Type | Default Value | Required | Supported Version |
+| --------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------- | ------------- | -------- | ----------------- |
+| media | Media content | ReactNode | - | | - |
+| title | Title of card | ReactNode | - | | - |
+| subTitle | Sub title of card | ReactNode | - | | - |
+| actions | Actions of card | ReactNode | - | | - |
+| showTitleBullet | If show title bullet | boolean | true | | - |
+| showHeadDivider | If show head divider | boolean | true | | - |
+| contentHeight | Height of content | string \| number | 120 | | - |
+| extra | Extra of card header | ReactNode | - | | - |
+| free | Whether to open free mode, if opened, can not set title subTitle ..., must use Card.Header Card.Content ... to set Card | boolean | false | | - |
+| hasBorder | Whether to show border | boolean | true | | 1.24 |
-### Card.Actions
+### Card.Media
-| Param | Description | Type | Default Value |
-| --------- | ------ | ------ | ----- |
-| component | The html tag to be rendered | custom | 'div' |
+| Param | Description | Type | Default Value | Required |
+| --------- | --------------------------- | ----------- | ------------- | -------- |
+| component | The html tag to be rendered | ElementType | 'div' | |
+| image | Media background image | string | - | |
+| src | Media source URL | string | - | |
-### Card.Content
+### Card.Header
-| Param | Description | Type | Default Value |
-| --------- | ------ | ------ | ----- |
-| component | The html tag to be rendered | custom | 'div' |
+| Param | Description | Type | Default Value | Required |
+| --------- | --------------------------- | ----------- | ------------- | -------- |
+| title | Title of card | ReactNode | - | |
+| subTitle | Sub Title of Card | ReactNode | - | |
+| extra | Extra of card header | ReactNode | - | |
+| component | The html tag to be rendered | ElementType | 'div' | |
-### Card.Divider
+### Card.Content
-| Param | Description | Type | Default Value |
-| --------- | ------ | ------ | ---- |
-| component | The html tag to be rendered | custom | 'hr' |
-| inset | inset | Boolean | - |
+| Param | Description | Type | Default Value | Required |
+| --------- | --------------------------- | ----------- | ------------- | -------- |
+| component | The html tag to be rendered | ElementType | 'div' | |
-### Card.Header
+### Card.Divider
-| 参数 | 说明 | 类型 | 默认值 |
-| --------- | ------------ | --------- | ----- |
-| title | Title of card | ReactNode | - |
-| subTitle | Sub Title of Card | ReactNode | - |
-| extra | Extra of card header | ReactNode | - |
-| component | The html tag to be rendered | custom | 'div' |
+| Param | Description | Type | Default Value | Required |
+| --------- | --------------------------- | ----------- | ------------- | -------- |
+| component | The html tag to be rendered | ElementType | 'hr' | |
+| inset | Inset | boolean | - | |
-### Card.Media
+### Card.Actions
-| 参数 | 说明 | 类型 | 默认值 |
-| --------- | ------- | ------ | ----- |
-| component | The html tag to be rendered | custom | 'div' |
-| image | Media background image | String | - |
-| src | Media source URL | String | - |
+| Param | Description | Type | Default Value | Required |
+| --------- | --------------------------- | ----------- | ------------- | -------- |
+| component | The html tag to be rendered | ElementType | 'div' | |
diff --git a/components/card/__docs__/index.md b/components/card/__docs__/index.md
index 9267411c2f..d10d8de793 100644
--- a/components/card/__docs__/index.md
+++ b/components/card/__docs__/index.md
@@ -17,51 +17,51 @@
### Card
-| 参数 | 说明 | 类型 | 默认值 | 版本支持 |
-| --------------- | ------------------------------------------------------------ | ------------- | ----- | ---- |
-| media | 卡片的上的图片 / 视频 | ReactNode | - | |
-| title | 卡片的标题 | ReactNode | - | |
-| subTitle | 卡片的副标题 | ReactNode | - | |
-| actions | 卡片操作组,位置在卡片底部 | ReactNode | - | |
-| showTitleBullet | 是否显示标题的项目符号 | Boolean | true | |
-| showHeadDivider | 是否展示头部的分隔线 | Boolean | true | |
-| contentHeight | 内容区域的固定高度 | String/Number | 120 | |
-| extra | 标题区域的用户自定义内容 | ReactNode | - | |
-| free | 是否开启自由模式,开启后card 将使用子组件配合使用, 设置此项后 title, subtitle, 等等属性都将失效 | Boolean | false | |
-| hasBorder | 是否带边框 | Boolean | true | 1.24 |
+| 参数 | 说明 | 类型 | 默认值 | 是否必填 | 支持版本 |
+| --------------- | ------------------------------------------------------------------------------------------------ | ---------------- | ------ | -------- | -------- |
+| media | 卡片的上的图片 / 视频 | ReactNode | - | | - |
+| title | 卡片的标题 | ReactNode | - | | - |
+| subTitle | 卡片的副标题 | ReactNode | - | | - |
+| actions | 卡片操作组,位置在卡片底部 | ReactNode | - | | - |
+| showTitleBullet | 是否显示标题的项目符号 | boolean | true | | - |
+| showHeadDivider | 是否展示头部的分隔线 | boolean | true | | - |
+| contentHeight | 内容区域的固定高度 | string \| number | 120 | | - |
+| extra | 标题区域的用户自定义内容 | ReactNode | - | | - |
+| free | 是否开启自由模式,开启后 card 将使用子组件配合使用,设置此项后 title, subtitle, 等等属性都将失效 | boolean | false | | - |
+| hasBorder | 是否带边框 | boolean | true | | 1.24 |
### Card.Media
-| 参数 | 说明 | 类型 | 默认值 |
-| --------- | ------- | ------ | ----- |
-| component | 设置标签类型 | custom | 'div' |
-| image | 背景图片地址 | String | - |
-| src | 媒体源文件地址 | String | - |
+| 参数 | 说明 | 类型 | 默认值 | 是否必填 |
+| --------- | -------------- | ----------- | ------ | -------- |
+| component | 设置标签类型 | ElementType | 'div' | |
+| image | 背景图片地址 | string | - | |
+| src | 媒体源文件地址 | string | - | |
### Card.Header
-| 参数 | 说明 | 类型 | 默认值 |
-| --------- | ------------ | --------- | ----- |
-| title | 卡片的标题 | ReactNode | - |
-| subTitle | 卡片的副标题 | ReactNode | - |
-| extra | 标题区域的用户自定义内容 | ReactNode | - |
-| component | 设置标签类型 | custom | 'div' |
+| 参数 | 说明 | 类型 | 默认值 | 是否必填 |
+| --------- | ------------------------ | ----------- | ------ | -------- |
+| title | 卡片的标题 | ReactNode | - | |
+| subTitle | 卡片的副标题 | ReactNode | - | |
+| extra | 标题区域的用户自定义内容 | ReactNode | - | |
+| component | 设置标签类型 | ElementType | 'div' | |
### Card.Content
-| 参数 | 说明 | 类型 | 默认值 |
-| --------- | ------ | ------ | ----- |
-| component | 设置标签类型 | custom | 'div' |
+| 参数 | 说明 | 类型 | 默认值 | 是否必填 |
+| --------- | ------------ | ----------- | ------ | -------- |
+| component | 设置标签类型 | ElementType | 'div' | |
### Card.Divider
-| 参数 | 说明 | 类型 | 默认值 |
-| --------- | --------- | ------- | ---- |
-| component | 设置标签类型 | custom | 'hr' |
-| inset | 分割线是否向内缩进 | Boolean | - |
+| 参数 | 说明 | 类型 | 默认值 | 是否必填 |
+| --------- | ------------------ | ----------- | ------ | -------- |
+| component | 设置标签类型 | ElementType | 'hr' | |
+| inset | 分割线是否向内缩进 | boolean | - | |
### Card.Actions
-| 参数 | 说明 | 类型 | 默认值 |
-| --------- | ------ | ------ | ----- |
-| component | 设置标签类型 | custom | 'div' |
+| 参数 | 说明 | 类型 | 默认值 | 是否必填 |
+| --------- | ------------ | ----------- | ------ | -------- |
+| component | 设置标签类型 | ElementType | 'div' | |
diff --git a/components/card/__docs__/theme/index.jsx b/components/card/__docs__/theme/index.jsx
deleted file mode 100644
index 7b248e223b..0000000000
--- a/components/card/__docs__/theme/index.jsx
+++ /dev/null
@@ -1,145 +0,0 @@
-import { Demo, DemoGroup, initDemo } from '../../../demo-helper';
-import ConfigProvider from '../../../config-provider';
-import Card from '../../index';
-import Button from '../../../button';
-import zhCN from '../../../locale/zh-cn';
-import enUS from '../../../locale/en-us';
-import '../../../demo-helper/style';
-import '../../style';
-
-/*eslint-disable*/
-const i18nMap = {
- 'zh-cn': {
- card: '卡片',
- normal: '基本',
- title: '标题',
- subTitle: '副标题',
- link: '链接',
- noUnderline: '无标题分隔线',
- bullet: '带标题标识',
- },
- 'en-us': {
- card: 'Card',
- normal: 'Normal',
- title: 'Title',
- subTile: 'Description',
- link: 'Link',
- noUnderline: 'No Header Line',
- bullet: 'Bullet',
- }
-};
-
-const cardStyle = {
- width: 360,
-};
-
-const placeholderStyle = {
- textAlign: 'left',
- fontSize: '14px',
- color: '#666'
-};
-
-const extendPlaceholderStyle = {
- height: '120px',
- textAlign: 'center'
-};
-
-function CardDemo({ locale, divider, noSubtitle, noLink, demoFunction, onFunctionChange }) {
- const commonProps = {
- subTitle: noSubtitle ? '' : locale.subTile,
- extra: noLink ? '' : {locale.link} ,
- };
-
- return (
-
-
-
-
- {divider && }
-
- Lorem ipsum dolor sit amet, est viderer iuvaret perfecto et. Ne petentium quaerendum nec, eos ex recteque mediocritatem, ex usu assum legendos temporibus. Ius feugiat pertinacia an, cu verterem praesent quo.
-
- {divider && }
-
- Action 1
- Action 2
-
-
-
-
- )
-}
-
-class FunctionDemo extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- demoFunction: {
- divider: {
- label: '分割线',
- value: 'false',
- enum: [
- { label: '显示', value: 'true' },
- { label: '隐藏', value: 'false' },
- { label: '内嵌', value: 'inset' }
- ],
- },
- showSubTitle: {
- label: '副标题',
- value: 'false',
- enum: [
- { label: '显示', value: 'true' },
- { label: '隐藏', value: 'false' }
- ],
- },
- showLink: {
- label: '有无链接',
- value: 'false',
- enum: [
- { label: '显示', value: 'true' },
- { label: '隐藏', value: 'false' }
- ],
- },
- },
- };
- }
-
- onFunctionChange = (ret) => {
- this.setState({
- demoFunction: ret,
- });
- }
-
- render() {
- const { title, locale } = this.props;
- const { demoFunction } = this.state;
-
- const divider = demoFunction.divider.value === 'false' ? '' : demoFunction.divider.value;
- const noSubtitle = demoFunction.showSubTitle.value === 'false';
- const noLink = demoFunction.showLink.value === 'false';
- const cardDemoProps = {
- locale,
- divider,
- noSubtitle,
- noLink,
- demoFunction,
- onFunctionChange: this.onFunctionChange,
- };
-
- return ( );
- }
-}
-
-function render(i18n, lang) {
- return ReactDOM.render(
-
-
, document.getElementById('container'));
-}
-
-window.renderDemo = function(lang = 'en-us') {
- render(i18nMap[lang], lang);
-};
-
-renderDemo();
-
-initDemo('card');
diff --git a/components/card/__docs__/theme/index.tsx b/components/card/__docs__/theme/index.tsx
new file mode 100644
index 0000000000..8db6d6764f
--- /dev/null
+++ b/components/card/__docs__/theme/index.tsx
@@ -0,0 +1,187 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Demo, DemoGroup, initDemo, type DemoFunctionDefineForObject } from '../../../demo-helper';
+import ConfigProvider from '../../../config-provider';
+import Card from '../../index';
+import Button from '../../../button';
+import zhCN from '../../../locale/zh-cn';
+import enUS from '../../../locale/en-us';
+import '../../../demo-helper/style';
+import '../../style';
+
+/*eslint-disable*/
+const i18nMap = {
+ 'zh-cn': {
+ card: '卡片',
+ normal: '基本',
+ title: '标题',
+ subTitle: '副标题',
+ link: '链接',
+ noUnderline: '无标题分隔线',
+ bullet: '带标题标识',
+ },
+ 'en-us': {
+ card: 'Card',
+ normal: 'Normal',
+ title: 'Title',
+ subTile: 'Description',
+ link: 'Link',
+ noUnderline: 'No Header Line',
+ bullet: 'Bullet',
+ },
+};
+
+const cardStyle = {
+ width: 360,
+};
+
+const placeholderStyle = {
+ textAlign: 'left',
+ fontSize: '14px',
+ color: '#666',
+};
+
+const extendPlaceholderStyle = {
+ height: '120px',
+ textAlign: 'center',
+};
+interface RenderCardState {
+ demoFunction: Record;
+}
+
+interface RenderCardProps {
+ i18n?: Record;
+ locale: Record;
+ title?: string;
+}
+interface RenderCardDemoProps {
+ locale: Record;
+ divider: string | unknown;
+ noSubtitle: boolean;
+ noLink: boolean;
+ demoFunction: Record;
+ onFunctionChange: (ret: Record) => void;
+}
+function CardDemo({
+ locale,
+ divider,
+ noSubtitle,
+ noLink,
+ demoFunction,
+ onFunctionChange,
+}: RenderCardDemoProps) {
+ const commonProps = {
+ subTitle: noSubtitle ? '' : locale.subTile,
+ extra: noLink ? (
+ ''
+ ) : (
+
+ {locale.link}
+
+ ),
+ };
+
+ return (
+
+
+
+
+ {divider && }
+
+ Lorem ipsum dolor sit amet, est viderer iuvaret perfecto et. Ne petentium
+ quaerendum nec, eos ex recteque mediocritatem, ex usu assum legendos
+ temporibus. Ius feugiat pertinacia an, cu verterem praesent quo.
+
+ {divider && }
+
+
+ Action 1
+
+
+ Action 2
+
+
+
+
+
+ );
+}
+
+class FunctionDemo extends React.Component {
+ constructor(props: RenderCardProps) {
+ super(props);
+ this.state = {
+ demoFunction: {
+ divider: {
+ label: '分割线',
+ value: 'false',
+ enum: [
+ { label: '显示', value: 'true' },
+ { label: '隐藏', value: 'false' },
+ { label: '内嵌', value: 'inset' },
+ ],
+ },
+ showSubTitle: {
+ label: '副标题',
+ value: 'false',
+ enum: [
+ { label: '显示', value: 'true' },
+ { label: '隐藏', value: 'false' },
+ ],
+ },
+ showLink: {
+ label: '有无链接',
+ value: 'false',
+ enum: [
+ { label: '显示', value: 'true' },
+ { label: '隐藏', value: 'false' },
+ ],
+ },
+ },
+ };
+ }
+
+ onFunctionChange = (ret: Record) => {
+ this.setState({
+ demoFunction: ret,
+ });
+ };
+
+ render() {
+ const { locale } = this.props;
+ const { demoFunction } = this.state;
+
+ const divider = demoFunction.divider.value === 'false' ? '' : demoFunction.divider.value;
+ const noSubtitle = demoFunction.showSubTitle.value === 'false';
+ const noLink = demoFunction.showLink.value === 'false';
+ const cardDemoProps = {
+ locale,
+ divider,
+ noSubtitle,
+ noLink,
+ demoFunction,
+ onFunctionChange: this.onFunctionChange,
+ };
+
+ return ;
+ }
+}
+
+function render(i18n: Record, lang: string) {
+ return ReactDOM.render(
+
+
+
+
+ ,
+ document.getElementById('container')
+ );
+}
+
+window.renderDemo = function (lang = 'en-us') {
+ render(i18nMap[lang], lang);
+};
+
+renderDemo();
+
+initDemo('card');
diff --git a/components/card/__tests__/a11y-spec.js b/components/card/__tests__/a11y-spec.js
deleted file mode 100644
index 8f135c362f..0000000000
--- a/components/card/__tests__/a11y-spec.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import React from 'react';
-import Enzyme from 'enzyme';
-import Adapter from 'enzyme-adapter-react-16';
-import Card from '../index';
-import '../style';
-import { unmount, testReact } from '../../util/__tests__/legacy/a11y/validate';
-
-Enzyme.configure({ adapter: new Adapter() });
-
-/* eslint-disable no-undef, react/jsx-filename-extension */
-describe('Card A11y', () => {
- let wrapper;
-
- afterEach(() => {
- if (wrapper) {
- wrapper.unmount();
- wrapper = null;
- }
- unmount();
- });
-
- it('should not have any violations when default', async () => {
- wrapper = await testReact(
-
-
-
- );
- return wrapper;
- });
-
- it('should not have any violations when displaying images', async () => {
- wrapper = await testReact(
-
-
-
-
Father's Day
-
Thank you, papa
-
-
- );
- return wrapper;
- });
-
- it('should not have any violations when setting height', async () => {
- const commonProps = {
- style: { width: 300 },
- title: 'Title',
- subTitle: 'Sub-title',
- };
-
- wrapper = await testReact(
-
-
-
-
Card content
-
Card content
-
-
-
-
-
-
Card content
-
Card content
-
-
-
- );
- return wrapper;
- });
-
- it('should not have any violations when setting title off', async () => {
- const commonProps = {
- style: { width: 300 },
- title: 'Title',
- };
-
- wrapper = await testReact(
-
- Card Content
-
- );
- return wrapper;
- });
-});
diff --git a/components/card/__tests__/a11y-spec.tsx b/components/card/__tests__/a11y-spec.tsx
new file mode 100644
index 0000000000..f3f00d0e7a
--- /dev/null
+++ b/components/card/__tests__/a11y-spec.tsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import Card from '../index';
+import '../style';
+import { testReact } from '../../util/__tests__/a11y/validate';
+
+describe('Card A11y', () => {
+ it('should not have any violations when default', async () => {
+ await testReact(
+
+
+
+ );
+ });
+
+ it('should not have any violations when displaying images', async () => {
+ await testReact(
+
+
+
+
Father's Day
+
Thank you, papa
+
+
+ );
+ });
+
+ it('should not have any violations when setting height', async () => {
+ const commonProps = {
+ style: { width: 300 },
+ title: 'Title',
+ subTitle: 'Sub-title',
+ };
+
+ await testReact(
+
+
+
+
Card content
+
Card content
+
+
+
+
+
+
Card content
+
Card content
+
+
+
+ );
+ });
+
+ it('should not have any violations when setting title off', async () => {
+ const commonProps = {
+ style: { width: 300 },
+ title: 'Title',
+ };
+
+ await testReact(
+
+ Card Content
+
+ );
+ });
+});
diff --git a/components/card/__tests__/index-spec.js b/components/card/__tests__/index-spec.js
deleted file mode 100644
index f7b05bbbfa..0000000000
--- a/components/card/__tests__/index-spec.js
+++ /dev/null
@@ -1,181 +0,0 @@
-import React from 'react';
-import Enzyme, { mount } from 'enzyme';
-import Adapter from 'enzyme-adapter-react-16';
-import assert from 'power-assert';
-import Button from '../../button';
-import Card from '../index';
-import '../style';
-
-Enzyme.configure({ adapter: new Adapter() });
-
-/* eslint-disable */
-describe('Card', () => {
- const commonProps = {
- style: { width: 300 },
- title: 'Title',
- subTitle: 'sub title',
- extra: 'Link',
- };
-
- describe('render', () => {
- let wrapper;
-
- afterEach(() => {
- wrapper = null;
- });
-
- it('should render card', () => {
- wrapper = mount(Card content );
- assert(wrapper.find('.next-card').length === 1);
- assert(wrapper.find('.next-card-head-show-bullet').length === 1);
- assert(wrapper.find('.next-card-head-show-underline').length === 0);
- });
-
- it('should render without title bullet', () => {
- wrapper = mount(
-
- Card Content
-
- );
- assert(wrapper.find('.next-card-head-show-bullet').length === 0);
- });
-
- it('should render without head underline', () => {
- wrapper = mount(
-
- Card Content
-
- );
- assert(wrapper.find('.next-card-head-show-underline').length === 0);
- });
-
- it('should render without head', () => {
- wrapper = mount(
-
- Card Content
-
- );
- assert(wrapper.find('.next-card-head').length === 0);
- });
-
- it('should render media & actions', () => {
- wrapper = mount(
- }
- actions={
-
- Button
-
- }
- >
- Card Content
-
- );
-
- assert(wrapper.find('.next-card-media').length > 0);
- assert(wrapper.find('.next-card-actions').length > 0);
- });
-
- it('should render when contentHeight is auto', () => {
- wrapper = mount(
-
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
- Card Content
-
- );
-
- assert(wrapper.find('.next-card-content').instance().style.height === '300px');
-
- wrapper.setProps({ contentHeight: 'auto' });
- assert(wrapper.find('.next-card-content').instance().style.height === 'auto');
- });
-
- it('should render free', () => {
- wrapper = mount(
-
-
-
-
-
- Link
-
- }
- />
-
- Card Content
-
-
- Action 1
-
-
- Action 2
-
-
-
- );
-
- assert(wrapper.find('.next-card-free').length > 0);
- // assert(wrapper.find('.next-card-actions').length > 0);
- });
- });
-
- describe('action', () => {
- let wrapper, parent;
-
- beforeEach(() => {
- parent = document.createElement('div');
- parent.setAttribute('id', 'react-app');
- document.body.appendChild(parent);
- });
-
- afterEach(() => {
- document.body.removeChild(parent);
- parent = null;
- wrapper = null;
- });
-
- it('should expand card', done => {
- wrapper = mount(
-
-
- ,
- { attachTo: parent }
- );
- assert(wrapper.find('.next-icon-arrow-down.expand').length === 0);
- wrapper.find(Button).simulate('click');
- assert(wrapper.find('.next-icon-arrow-down.expand').length === 1);
- done();
- });
- });
-});
diff --git a/components/card/__tests__/index-spec.tsx b/components/card/__tests__/index-spec.tsx
new file mode 100644
index 0000000000..c845f5c373
--- /dev/null
+++ b/components/card/__tests__/index-spec.tsx
@@ -0,0 +1,152 @@
+import React from 'react';
+import Button from '../../button';
+import Card from '../index';
+import '../style';
+
+describe('Card', () => {
+ const commonProps = {
+ style: { width: 300 },
+ title: 'Title',
+ subTitle: 'sub title',
+ extra: 'Link',
+ };
+
+ describe('render', () => {
+ it('should render card', () => {
+ cy.mount(Card content );
+ cy.get('.next-card').should('have.length', 1);
+ cy.get('.next-card-head-show-bullet').should('have.length', 1);
+ cy.get('.next-card-head-show-underline').should('not.exist');
+ });
+
+ it('should render without title bullet', () => {
+ cy.mount(
+
+ Card Content
+
+ );
+ cy.get('.next-card-head-show-bullet').should('not.exist');
+ });
+
+ it('should render without head underline', () => {
+ cy.mount(
+
+ Card Content
+
+ );
+ cy.get('.next-card-head-show-underline').should('not.exist');
+ });
+
+ it('should render without head', () => {
+ cy.mount(
+
+ Card Content
+
+ );
+ cy.get('.next-card-head').should('not.exist');
+ });
+
+ it('should render media & actions', () => {
+ cy.mount(
+
+ }
+ actions={
+
+ Button
+
+ }
+ >
+ Card Content
+
+ );
+ cy.get('.next-card-media').should('exist');
+ cy.get('.next-card-actions').should('exist');
+ });
+
+ it('should render when contentHeight is auto', () => {
+ cy.mount(
+
+ Card Content
+ Card Content
+ Card Content
+ Card Content
+ Card Content
+ Card Content
+ Card Content
+ Card Content
+ Card Content
+ Card Content
+ Card Content
+ Card Content
+ Card Content
+ Card Content
+ Card Content
+ Card Content
+ Card Content
+ Card Content
+ Card Content
+ Card Content
+ Card Content
+ Card Content
+ Card Content
+
+ ).as('Demo');
+
+ cy.get('.next-card-content').should('have.css', 'height', '300px');
+
+ cy.rerender('Demo', {
+ contentHeight: 'auto',
+ });
+ cy.get('.next-card-content').should($element => {
+ const height = parseInt($element.css('height'), 10);
+ expect(height).to.be.greaterThan(400);
+ });
+ });
+
+ it('should render free', () => {
+ cy.mount(
+
+
+
+
+
+ Link
+
+ }
+ />
+
+ Card Content
+
+
+ Action 1
+
+
+ Action 2
+
+
+
+ );
+ cy.get('.next-card-free').should('exist');
+ });
+ });
+
+ describe('action', () => {
+ it('should expand card', () => {
+ cy.mount(
+
+
+
+ );
+ cy.get('.next-icon-arrow-down').should('not.have.class', 'expand');
+ cy.get('Button').click();
+ cy.get('.next-icon-arrow-down').should('have.class', 'expand');
+ });
+ });
+});
diff --git a/components/card/actions.jsx b/components/card/actions.jsx
deleted file mode 100644
index 73a5bffc61..0000000000
--- a/components/card/actions.jsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
-import ConfigProvider from '../config-provider';
-
-/**
- * Card.Actions
- * @order 5
- */
-class CardActions extends Component {
- static propTypes = {
- prefix: PropTypes.string,
- /**
- * 设置标签类型
- */
- component: PropTypes.elementType,
- className: PropTypes.string,
- };
-
- static defaultProps = {
- prefix: 'next-',
- component: 'div',
- };
-
- render() {
- const { prefix, component: Component, className, ...others } = this.props;
- return ;
- }
-}
-
-export default ConfigProvider.config(CardActions);
diff --git a/components/card/actions.tsx b/components/card/actions.tsx
new file mode 100644
index 0000000000..8a7fa2fcf5
--- /dev/null
+++ b/components/card/actions.tsx
@@ -0,0 +1,27 @@
+import React, { Component, type ElementType } from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import ConfigProvider from '../config-provider';
+import type { CardActionsProps } from './types';
+
+class CardActions extends Component {
+ static displayName = 'CardActions';
+ static propTypes = {
+ prefix: PropTypes.string,
+ component: PropTypes.elementType,
+ className: PropTypes.string,
+ };
+
+ static defaultProps = {
+ prefix: 'next-',
+ component: 'div',
+ };
+
+ render() {
+ const { prefix, component, className, ...others } = this.props;
+ const Component = component as ElementType;
+ return ;
+ }
+}
+
+export default ConfigProvider.config(CardActions);
diff --git a/components/card/bullet-header.jsx b/components/card/bullet-header.jsx
deleted file mode 100644
index cea094a50f..0000000000
--- a/components/card/bullet-header.jsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
-import ConfigProvider from '../config-provider';
-
-/**
- * Card.BulletHeader
- * @order 2
- */
-class CardBulletHeader extends Component {
- static propTypes = {
- prefix: PropTypes.string,
- /**
- * 卡片的标题
- */
- title: PropTypes.node,
- /**
- * 卡片的副标题
- */
- subTitle: PropTypes.node,
- /**
- * 是否显示标题的项目符号
- */
- showTitleBullet: PropTypes.bool,
- /**
- * 标题区域的用户自定义内容
- */
- extra: PropTypes.node,
- };
-
- static defaultProps = {
- prefix: 'next-',
- showTitleBullet: true,
- };
-
- render() {
- const { prefix, title, subTitle, extra, showTitleBullet } = this.props;
-
- if (!title) return null;
-
- const headCls = classNames({
- [`${prefix}card-head`]: true,
- [`${prefix}card-head-show-bullet`]: showTitleBullet,
- });
-
- const headExtra = extra ? {extra}
: null;
-
- return (
-
-
-
- {title}
- {subTitle ? {subTitle} : null}
-
- {headExtra}
-
-
- );
- }
-}
-
-export default ConfigProvider.config(CardBulletHeader, {
- componentName: 'Card',
-});
diff --git a/components/card/bullet-header.tsx b/components/card/bullet-header.tsx
new file mode 100644
index 0000000000..c57a3a9399
--- /dev/null
+++ b/components/card/bullet-header.tsx
@@ -0,0 +1,52 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import ConfigProvider from '../config-provider';
+import type { CardBulletHeaderProps } from './types';
+
+class CardBulletHeader extends Component {
+ static displayName = 'CardBulletHeader';
+ static propTypes = {
+ prefix: PropTypes.string,
+ title: PropTypes.node,
+ subTitle: PropTypes.node,
+ showTitleBullet: PropTypes.bool,
+ extra: PropTypes.node,
+ };
+
+ static defaultProps = {
+ prefix: 'next-',
+ showTitleBullet: true,
+ };
+
+ render() {
+ const { prefix, title, subTitle, extra, showTitleBullet } = this.props;
+
+ if (!title) return null;
+
+ const headCls = classNames({
+ [`${prefix}card-head`]: true,
+ [`${prefix}card-head-show-bullet`]: showTitleBullet,
+ });
+
+ const headExtra = extra ? {extra}
: null;
+
+ return (
+
+
+
+ {title}
+ {subTitle ? (
+ {subTitle}
+ ) : null}
+
+ {headExtra}
+
+
+ );
+ }
+}
+
+export default ConfigProvider.config(CardBulletHeader, {
+ componentName: 'Card',
+});
diff --git a/components/card/card.jsx b/components/card/card.jsx
deleted file mode 100644
index 19768c3e3a..0000000000
--- a/components/card/card.jsx
+++ /dev/null
@@ -1,121 +0,0 @@
-/* eslint-disable valid-jsdoc */
-import React from 'react';
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
-import ConfigProvider from '../config-provider';
-import BulletHeader from './bullet-header';
-import CollapseContent from './collapse-content';
-import CardMedia from './media';
-import CardActions from './actions';
-import { obj } from '../util';
-
-const { pickOthers } = obj;
-
-/**
- * Card
- * @order 0
- */
-export default class Card extends React.Component {
- static displayName = 'Card';
-
- static propTypes = {
- ...ConfigProvider.propTypes,
- prefix: PropTypes.string,
- rtl: PropTypes.bool,
- /**
- * 卡片的上的图片 / 视频
- */
- media: PropTypes.node,
- /**
- * 卡片的标题
- */
- title: PropTypes.node,
- /**
- * 卡片的副标题
- */
- subTitle: PropTypes.node,
- /**
- * 卡片操作组,位置在卡片底部
- */
- actions: PropTypes.node,
- /**
- * 是否显示标题的项目符号
- */
- showTitleBullet: PropTypes.bool,
- /**
- * 是否展示头部的分隔线
- */
- showHeadDivider: PropTypes.bool,
- /**
- * 内容区域的固定高度
- */
- contentHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- /**
- * 标题区域的用户自定义内容
- */
- extra: PropTypes.node,
- /**
- * 是否开启自由模式,开启后card 将使用子组件配合使用, 设置此项后 title, subtitle, 等等属性都将失效
- */
- free: PropTypes.bool,
- /**
- * 是否带边框
- * @version 1.24
- */
- hasBorder: PropTypes.bool,
- className: PropTypes.string,
- children: PropTypes.node,
- };
-
- static defaultProps = {
- prefix: 'next-',
- free: false,
- showTitleBullet: true,
- showHeadDivider: true,
- hasBorder: true,
- contentHeight: 120,
- };
-
- render() {
- const {
- prefix,
- className,
- title,
- subTitle,
- extra,
- showTitleBullet,
- showHeadDivider,
- children,
- rtl,
- contentHeight,
- free,
- actions,
- hasBorder,
- media,
- } = this.props;
-
- const cardCls = classNames(
- {
- [`${prefix}card`]: true,
- [`${prefix}card-free`]: free,
- [`${prefix}card-noborder`]: !hasBorder,
- [`${prefix}card-show-divider`]: showHeadDivider,
- [`${prefix}card-hide-divider`]: !showHeadDivider,
- },
- className
- );
-
- const others = pickOthers(Object.keys(Card.propTypes), this.props);
-
- others.dir = rtl ? 'rtl' : undefined;
-
- return (
-
- {media && {media} }
-
- {free ? children : {children} }
- {actions && {actions} }
-
- );
- }
-}
diff --git a/components/card/card.tsx b/components/card/card.tsx
new file mode 100644
index 0000000000..98cbe5b6b2
--- /dev/null
+++ b/components/card/card.tsx
@@ -0,0 +1,96 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+import ConfigProvider from '../config-provider';
+import BulletHeader from './bullet-header';
+import CollapseContent from './collapse-content';
+import CardMedia from './media';
+import CardActions from './actions';
+import { obj } from '../util';
+import type { CardProps } from './types';
+
+const { pickOthers } = obj;
+
+export default class Card extends Component {
+ static displayName = 'Card';
+
+ static propTypes = {
+ ...ConfigProvider.propTypes,
+ prefix: PropTypes.string,
+ rtl: PropTypes.bool,
+ media: PropTypes.node,
+ title: PropTypes.node,
+ subTitle: PropTypes.node,
+ actions: PropTypes.node,
+ showTitleBullet: PropTypes.bool,
+ showHeadDivider: PropTypes.bool,
+ contentHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ extra: PropTypes.node,
+ free: PropTypes.bool,
+ hasBorder: PropTypes.bool,
+ className: PropTypes.string,
+ children: PropTypes.node,
+ };
+
+ static defaultProps = {
+ prefix: 'next-',
+ free: false,
+ showTitleBullet: true,
+ showHeadDivider: true,
+ hasBorder: true,
+ contentHeight: 120,
+ };
+
+ render() {
+ const {
+ prefix,
+ className,
+ title,
+ subTitle,
+ extra,
+ showTitleBullet,
+ showHeadDivider,
+ children,
+ rtl,
+ contentHeight,
+ free,
+ actions,
+ hasBorder,
+ media,
+ } = this.props;
+
+ const cardCls = classNames(
+ {
+ [`${prefix}card`]: true,
+ [`${prefix}card-free`]: free,
+ [`${prefix}card-noborder`]: !hasBorder,
+ [`${prefix}card-show-divider`]: showHeadDivider,
+ [`${prefix}card-hide-divider`]: !showHeadDivider,
+ },
+ className
+ );
+
+ const others = pickOthers(Card.propTypes, this.props);
+
+ others.dir = rtl ? 'rtl' : undefined;
+
+ return (
+
+ {media && {media} }
+
+ {free ? (
+ children
+ ) : (
+ {children}
+ )}
+ {actions && {actions} }
+
+ );
+ }
+}
diff --git a/components/card/collapse-content.jsx b/components/card/collapse-content.jsx
deleted file mode 100644
index 4446677f93..0000000000
--- a/components/card/collapse-content.jsx
+++ /dev/null
@@ -1,137 +0,0 @@
-import React, { Component } from 'react';
-import ReactDOM from 'react-dom';
-import PropTypes from 'prop-types';
-import Icon from '../icon';
-import Button from '../button';
-import ConfigProvider from '../config-provider';
-import nextLocale from '../locale/zh-cn';
-
-/**
- * Card.CollapseContent
- * @order 3
- */
-class CardCollapseContent extends Component {
- static propTypes = {
- prefix: PropTypes.string,
- /**
- * 内容区域的固定高度
- */
- contentHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- locale: PropTypes.object,
- children: PropTypes.node,
- };
-
- static defaultProps = {
- prefix: 'next-',
- contentHeight: 120,
- locale: nextLocale.Card,
- };
-
- constructor(props, context) {
- super(props, context);
-
- this.state = {
- needMore: false,
- expand: false,
- contentHeight: 'auto',
- };
- }
-
- componentDidMount() {
- this._setNeedMore();
- this._setContentHeight();
- }
-
- componentDidUpdate() {
- this._setContentHeight();
- }
-
- handleToggle = () => {
- this.setState(prevState => {
- return {
- expand: !prevState.expand,
- };
- });
- };
-
- // 是否展示 More 按钮
- _setNeedMore() {
- const { contentHeight } = this.props;
- const childrenHeight = this._getNodeChildrenHeight(this.content);
- this.setState({
- needMore: contentHeight !== 'auto' && childrenHeight > contentHeight,
- });
- }
-
- // 设置 Body 的高度
- _setContentHeight() {
- if (this.props.contentHeight === 'auto') {
- this.content.style.height = 'auto';
- return;
- }
-
- if (this.state.expand) {
- const childrenHeight = this._getNodeChildrenHeight(this.content);
- this.content.style.height = `${childrenHeight}px`; // get the real height
- } else {
- const el = ReactDOM.findDOMNode(this.footer);
- let height = this.props.contentHeight;
-
- if (el) {
- height = height - el.getBoundingClientRect().height;
- }
-
- this.content.style.height = `${height}px`;
- }
- }
-
- _getNodeChildrenHeight(node) {
- if (!node) {
- return 0;
- }
-
- const contentChildNodes = node.childNodes;
- const length = contentChildNodes.length;
-
- if (!length) {
- return 0;
- }
-
- const lastNode = contentChildNodes[length - 1];
-
- return lastNode.offsetTop + lastNode.offsetHeight;
- }
-
- _contentRefHandler = ref => {
- this.content = ref;
- };
-
- saveFooter = ref => {
- this.footer = ref;
- };
-
- render() {
- const { prefix, children, locale } = this.props;
- const { needMore, expand } = this.state;
-
- return (
-
-
- {children}
-
- {needMore ? (
-
-
- {expand ? locale.fold : locale.expand}
-
-
-
- ) : null}
-
- );
- }
-}
-
-export default ConfigProvider.config(CardCollapseContent, {
- componentName: 'Card',
-});
diff --git a/components/card/collapse-content.tsx b/components/card/collapse-content.tsx
new file mode 100644
index 0000000000..812478f051
--- /dev/null
+++ b/components/card/collapse-content.tsx
@@ -0,0 +1,147 @@
+import React, { Component } from 'react';
+import ReactDOM from 'react-dom';
+import PropTypes from 'prop-types';
+import Icon from '../icon';
+import Button from '../button';
+import ConfigProvider from '../config-provider';
+import nextLocale from '../locale/zh-cn';
+import type { CardCollapseContentProps } from './types';
+
+export interface CardCollapseContentState {
+ needMore: boolean;
+ expand: boolean;
+ contentHeight: string | number;
+}
+
+class CardCollapseContent extends Component {
+ static displayName = 'CardCollapseContent';
+ static propTypes = {
+ prefix: PropTypes.string,
+ /**
+ * 内容区域的固定高度
+ */
+ contentHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ locale: PropTypes.object,
+ children: PropTypes.node,
+ };
+
+ static defaultProps = {
+ prefix: 'next-',
+ contentHeight: 120,
+ locale: nextLocale.Card,
+ };
+ content: HTMLDivElement;
+ footer: HTMLDivElement;
+
+ constructor(props: CardCollapseContentProps) {
+ super(props);
+
+ this.state = {
+ needMore: false,
+ expand: false,
+ contentHeight: 'auto',
+ };
+ }
+
+ componentDidMount() {
+ this._setNeedMore();
+ this._setContentHeight();
+ }
+
+ componentDidUpdate() {
+ this._setContentHeight();
+ }
+
+ handleToggle = () => {
+ this.setState(prevState => {
+ return {
+ expand: !prevState.expand,
+ };
+ });
+ };
+
+ // 是否展示 More 按钮
+ _setNeedMore() {
+ const { contentHeight } = this.props;
+ const childrenHeight = this._getNodeChildrenHeight(this.content);
+ this.setState({
+ needMore: contentHeight !== 'auto' && childrenHeight > (contentHeight as number),
+ });
+ }
+
+ // 设置 Body 的高度
+ _setContentHeight() {
+ if (this.props.contentHeight === 'auto') {
+ this.content.style.height = 'auto';
+ return;
+ }
+
+ if (this.state.expand) {
+ const childrenHeight = this._getNodeChildrenHeight(this.content);
+ this.content.style.height = `${childrenHeight}px`; // get the real height
+ } else {
+ const el = ReactDOM.findDOMNode(this.footer) as Element | null;
+ let height = this.props.contentHeight;
+
+ if (el) {
+ height = (height as number) - el.getBoundingClientRect().height;
+ }
+
+ this.content.style.height = `${height}px`;
+ }
+ }
+
+ _getNodeChildrenHeight(node?: HTMLDivElement) {
+ if (!node) {
+ return 0;
+ }
+
+ const contentChildNodes = node.childNodes;
+ const length = contentChildNodes.length;
+
+ if (!length) {
+ return 0;
+ }
+
+ const lastNode = contentChildNodes[length - 1] as HTMLElement;
+
+ return lastNode.offsetTop + lastNode.offsetHeight;
+ }
+
+ _contentRefHandler = (ref: HTMLDivElement) => {
+ this.content = ref;
+ };
+
+ saveFooter = (ref: HTMLDivElement) => {
+ this.footer = ref;
+ };
+
+ render() {
+ const { prefix, children, locale } = this.props;
+ const { needMore, expand } = this.state;
+
+ return (
+
+
+ {children}
+
+ {needMore ? (
+
+
+ {expand ? locale!.fold : locale!.expand}
+
+
+
+ ) : null}
+
+ );
+ }
+}
+
+export default ConfigProvider.config(CardCollapseContent, {
+ componentName: 'Card',
+});
diff --git a/components/card/content.jsx b/components/card/content.jsx
deleted file mode 100644
index 7a368d9c5a..0000000000
--- a/components/card/content.jsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
-import ConfigProvider from '../config-provider';
-
-/**
- * Card.Content
- * @order 3
- */
-class CardContent extends Component {
- static propTypes = {
- prefix: PropTypes.string,
- /**
- * 设置标签类型
- */
- component: PropTypes.elementType,
- className: PropTypes.string,
- };
-
- static defaultProps = {
- prefix: 'next-',
- component: 'div',
- };
-
- render() {
- const { prefix, className, component: Component, ...others } = this.props;
- return ;
- }
-}
-
-export default ConfigProvider.config(CardContent);
diff --git a/components/card/content.tsx b/components/card/content.tsx
new file mode 100644
index 0000000000..b2955d7415
--- /dev/null
+++ b/components/card/content.tsx
@@ -0,0 +1,35 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import ConfigProvider from '../config-provider';
+import type { CardContentProps } from './types';
+
+class CardContent extends Component {
+ static displayName = 'CardContent';
+ static propTypes = {
+ prefix: PropTypes.string,
+ /**
+ * 设置标签类型
+ */
+ component: PropTypes.elementType,
+ className: PropTypes.string,
+ };
+
+ static defaultProps = {
+ prefix: 'next-',
+ component: 'div',
+ };
+
+ render() {
+ const { prefix, className, component, ...others } = this.props;
+ const Component = component as React.ElementType;
+ return (
+
+ );
+ }
+}
+
+export default ConfigProvider.config(CardContent);
diff --git a/components/card/divider.jsx b/components/card/divider.jsx
deleted file mode 100644
index 0db14ab144..0000000000
--- a/components/card/divider.jsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
-import ConfigProvider from '../config-provider';
-
-/**
- * Card.Divider
- * @order 4
- */
-class CardDivider extends Component {
- static propTypes = {
- prefix: PropTypes.string,
- /**
- * 设置标签类型
- */
- component: PropTypes.elementType,
- /**
- * 分割线是否向内缩进
- */
- inset: PropTypes.bool,
- className: PropTypes.string,
- };
-
- static defaultProps = {
- prefix: 'next-',
- component: 'hr',
- };
-
- render() {
- const { prefix, component: Component, inset, className, ...others } = this.props;
-
- const dividerClassName = classNames(
- `${prefix}card-divider`,
- {
- [`${prefix}card-divider--inset`]: inset,
- },
- className
- );
-
- return ;
- }
-}
-
-export default ConfigProvider.config(CardDivider);
diff --git a/components/card/divider.tsx b/components/card/divider.tsx
new file mode 100644
index 0000000000..79b8e009cb
--- /dev/null
+++ b/components/card/divider.tsx
@@ -0,0 +1,42 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import ConfigProvider from '../config-provider';
+import type { CardDividerProps } from './types';
+
+class CardDivider extends Component {
+ static displayName = 'CardDivider';
+ static propTypes = {
+ prefix: PropTypes.string,
+ /**
+ * 设置标签类型
+ */
+ component: PropTypes.elementType,
+ /**
+ * 分割线是否向内缩进
+ */
+ inset: PropTypes.bool,
+ className: PropTypes.string,
+ };
+
+ static defaultProps = {
+ prefix: 'next-',
+ component: 'hr',
+ };
+
+ render() {
+ const { prefix, component, inset, className, ...others } = this.props;
+ const Component = component as React.ElementType;
+ const dividerClassName = classNames(
+ `${prefix}card-divider`,
+ {
+ [`${prefix}card-divider--inset`]: inset,
+ },
+ className
+ );
+
+ return ;
+ }
+}
+
+export default ConfigProvider.config(CardDivider);
diff --git a/components/card/header.jsx b/components/card/header.jsx
deleted file mode 100644
index 5049240496..0000000000
--- a/components/card/header.jsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
-import ConfigProvider from '../config-provider';
-
-/**
- * Card.Header
- * @order 2
- */
-class CardHeader extends Component {
- static propTypes = {
- prefix: PropTypes.string,
- /**
- * 卡片的标题
- */
- title: PropTypes.node,
- /**
- * 卡片的副标题
- */
- subTitle: PropTypes.node,
- /**
- * 标题区域的用户自定义内容
- */
- extra: PropTypes.node,
- /**
- * 设置标签类型
- */
- component: PropTypes.elementType,
- className: PropTypes.string,
- };
-
- static defaultProps = {
- prefix: 'next-',
- component: 'div',
- };
-
- render() {
- const { prefix, title, subTitle, extra, className, component: Component, ...others } = this.props;
-
- return (
-
- {extra && {extra}
}
-
- {title &&
{title}
}
- {subTitle &&
{subTitle}
}
-
-
- );
- }
-}
-
-export default ConfigProvider.config(CardHeader);
diff --git a/components/card/header.tsx b/components/card/header.tsx
new file mode 100644
index 0000000000..b6a2879a87
--- /dev/null
+++ b/components/card/header.tsx
@@ -0,0 +1,50 @@
+import React, { Component, type ElementType } from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import ConfigProvider from '../config-provider';
+import type { CardHeaderProps } from './types';
+
+class CardHeader extends Component {
+ static displayName = 'CardHeader';
+ static propTypes = {
+ prefix: PropTypes.string,
+ /**
+ * 卡片的标题
+ */
+ title: PropTypes.node,
+ /**
+ * 卡片的副标题
+ */
+ subTitle: PropTypes.node,
+ /**
+ * 标题区域的用户自定义内容
+ */
+ extra: PropTypes.node,
+ /**
+ * 设置标签类型
+ */
+ component: PropTypes.elementType,
+ className: PropTypes.string,
+ };
+
+ static defaultProps = {
+ prefix: 'next-',
+ component: 'div',
+ };
+
+ render() {
+ const { prefix, title, subTitle, extra, className, component, ...others } = this.props;
+ const Component = component as ElementType;
+ return (
+
+ {extra && {extra}
}
+
+ {title &&
{title}
}
+ {subTitle &&
{subTitle}
}
+
+
+ );
+ }
+}
+
+export default ConfigProvider.config(CardHeader);
diff --git a/components/card/index.d.ts b/components/card/index.d.ts
deleted file mode 100644
index 79ab9ce5fa..0000000000
--- a/components/card/index.d.ts
+++ /dev/null
@@ -1,147 +0,0 @@
-///
-
-import React from 'react';
-import { CommonProps } from '../util';
-
-interface HTMLAttributesWeak extends React.HTMLAttributes {
- title?: any;
-}
-
-export interface CardProps extends HTMLAttributesWeak, CommonProps {
- /**
- * 卡片的上的图片 / 视频
- */
- media?: React.ReactNode;
-
- /**
- * 卡片的标题
- */
- title?: React.ReactNode;
-
- /**
- * 卡片的副标题
- */
- subTitle?: React.ReactNode;
-
- /**
- * 卡片操作组,位置在卡片底部
- */
- actions?: React.ReactNode;
-
- /**
- * 是否显示标题的项目符号
- */
- showTitleBullet?: boolean;
-
- /**
- * 是否展示头部的分隔线
- */
- showHeadDivider?: boolean;
-
- /**
- * 内容区域的固定高度
- */
- contentHeight?: string | number;
-
- /**
- * 标题区域的用户自定义内容
- */
- extra?: React.ReactNode;
-
- /**
- * 是否开启自由模式,开启后card 将使用子组件配合使用, 设置此项后 title, subtitle, 等等属性都将失效
- */
- free?: boolean;
- hasBorder?: boolean;
-}
-
-export interface CardBulletHeaderProps extends HTMLAttributesWeak, CommonProps {
- /**
- * 卡片的标题
- */
- title?: React.ReactNode;
-
- /**
- * 卡片的副标题
- */
- subTitle?: React.ReactNode;
- /**
- * 是否显示标题的项目符号
- */
- showTitleBullet?: boolean;
- /**
- * 标题区域的用户自定义内容
- */
- extra?: React.ReactNode;
-}
-
-export interface CardCollaspeContentProps extends HTMLAttributesWeak, CommonProps {
- contentHeight?: string | number;
-}
-export interface CardCollapseContentProps extends HTMLAttributesWeak, CommonProps {
- contentHeight?: string | number;
-}
-
-export interface CardHeaderProps extends HTMLAttributesWeak, CommonProps {
- /**
- * 卡片的标题
- */
- title?: React.ReactNode;
-
- /**
- * 卡片的副标题
- */
- subTitle?: React.ReactNode;
-
- /**
- * 标题区域的用户自定义内容
- */
- extra?: React.ReactNode;
-
- /**
- * 设置标签类型
- */
- component?: React.ElementType;
-}
-
-export interface CardContentProps extends HTMLAttributesWeak, CommonProps {
- /**
- * 设置标签类型
- */
- component?: React.ElementType;
-}
-
-export interface CardMediaProps extends HTMLAttributesWeak, CommonProps {
- /**
- * 设置标签类型
- */
- component?: React.ElementType;
- /**
- * 背景图片地址
- */
- image?: string;
- /**
- * 媒体源文件地址
- */
- src?: string;
-}
-
-export interface CardActionsProps extends HTMLAttributesWeak, CommonProps {}
-
-export interface CardDividerProps extends HTMLAttributesWeak, CommonProps {
- /**
- * 分割线是否向内缩进
- */
- inset?: boolean;
-}
-
-export default class Card extends React.Component {
- static BulletHeader: React.ComponentType;
- static CollaspeContent: React.ComponentType;
- static CollapseContent: React.ComponentType;
- static Header: React.ComponentType;
- static Content: React.ComponentType;
- static Media: React.ComponentType;
- static Actions: React.ComponentType;
- static Divider: React.ComponentType;
-}
diff --git a/components/card/index.jsx b/components/card/index.jsx
deleted file mode 100644
index 00eb5c5a9f..0000000000
--- a/components/card/index.jsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import ConfigProvider from '../config-provider';
-import Card from './card';
-import CardHeader from './header';
-import CardBulletHeader from './bullet-header';
-import CardMedia from './media';
-import CardDivider from './divider';
-import CardContent from './content';
-import CollaspeContent from './collapse-content';
-import CardActions from './actions';
-
-Card.Header = CardHeader;
-Card.Media = CardMedia;
-Card.Divider = CardDivider;
-Card.Content = CardContent;
-Card.Actions = CardActions;
-Card.BulletHeader = CardBulletHeader;
-Card.CollaspeContent = CollaspeContent;
-Card.CollapseContent = CollaspeContent;
-
-export default ConfigProvider.config(Card, {
- transform: /* istanbul ignore next */ (props, deprecated) => {
- if ('titlePrefixLine' in props) {
- deprecated('titlePrefixLine', 'showTitleBullet', 'Card');
- const { titlePrefixLine, ...others } = props;
- props = { showTitleBullet: titlePrefixLine, ...others };
- }
- if ('titleBottomLine' in props) {
- deprecated('titleBottomLine', 'showHeadDivider', 'Card');
- const { titleBottomLine, ...others } = props;
- props = { showHeadDivider: titleBottomLine, ...others };
- }
- if ('bodyHeight' in props) {
- deprecated('bodyHeight', 'contentHeight', 'Card');
- const { bodyHeight, ...others } = props;
- props = { contentHeight: bodyHeight, ...others };
- }
-
- return props;
- },
-});
diff --git a/components/card/index.tsx b/components/card/index.tsx
new file mode 100644
index 0000000000..eab2c612bf
--- /dev/null
+++ b/components/card/index.tsx
@@ -0,0 +1,59 @@
+import ConfigProvider from '../config-provider';
+import { assignSubComponent } from '../util/component';
+import Card from './card';
+import CardHeader from './header';
+import CardBulletHeader from './bullet-header';
+import CardMedia from './media';
+import CardDivider from './divider';
+import CardContent from './content';
+import CollapseContent from './collapse-content';
+import CardActions from './actions';
+
+export type {
+ CardProps,
+ CardMediaProps,
+ CardHeaderProps,
+ CardContentProps,
+ CardDividerProps,
+ CardActionsProps,
+ CardBulletHeaderProps,
+ CardCollaspeContentProps,
+ CardCollapseContentProps,
+} from './types';
+
+const WithSubCard = assignSubComponent(Card, {
+ Header: CardHeader,
+ Media: CardMedia,
+ Divider: CardDivider,
+ Content: CardContent,
+ Actions: CardActions,
+ BulletHeader: CardBulletHeader,
+ /**
+ * typo of CollapseContent
+ * @deprecated Use CollapseContent instead
+ */
+ CollaspeContent: CollapseContent,
+ CollapseContent: CollapseContent,
+});
+
+export default ConfigProvider.config(WithSubCard, {
+ transform: (props, deprecated) => {
+ if ('titlePrefixLine' in props) {
+ deprecated('titlePrefixLine', 'showTitleBullet', 'Card');
+ const { titlePrefixLine, ...others } = props;
+ props = { showTitleBullet: titlePrefixLine as boolean, ...others };
+ }
+ if ('titleBottomLine' in props) {
+ deprecated('titleBottomLine', 'showHeadDivider', 'Card');
+ const { titleBottomLine, ...others } = props;
+ props = { showHeadDivider: titleBottomLine as boolean, ...others };
+ }
+ if ('bodyHeight' in props) {
+ deprecated('bodyHeight', 'contentHeight', 'Card');
+ const { bodyHeight, ...others } = props;
+ props = { contentHeight: bodyHeight as number | string, ...others };
+ }
+
+ return props;
+ },
+});
diff --git a/components/card/media.jsx b/components/card/media.jsx
deleted file mode 100644
index 8a14bea2a0..0000000000
--- a/components/card/media.jsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
-import ConfigProvider from '../config-provider';
-import { log } from '../util';
-
-const { warning } = log;
-
-const MEDIA_COMPONENTS = ['video', 'audio', 'picture', 'iframe', 'img'];
-
-/**
- * Card.Media
- * @order 1
- */
-class CardMedia extends Component {
- static propTypes = {
- prefix: PropTypes.string,
- /**
- * 设置标签类型
- */
- component: PropTypes.elementType,
- /**
- * 背景图片地址
- */
- image: PropTypes.string,
- /**
- * 媒体源文件地址
- */
- src: PropTypes.string,
- style: PropTypes.object,
- className: PropTypes.string,
- };
-
- static defaultProps = {
- prefix: 'next-',
- component: 'div',
- style: {},
- };
-
- render() {
- const { prefix, style, className, component: Component, image, src, ...others } = this.props;
-
- if (!('children' in others || Boolean(image || src))) {
- warning('either `children`, `image` or `src` prop must be specified.');
- }
-
- const isMediaComponent = MEDIA_COMPONENTS.indexOf(Component) !== -1;
- const composedStyle = !isMediaComponent && image ? { backgroundImage: `url("${image}")`, ...style } : style;
-
- return (
-
- );
- }
-}
-
-export default ConfigProvider.config(CardMedia);
diff --git a/components/card/media.tsx b/components/card/media.tsx
new file mode 100644
index 0000000000..cd72450cef
--- /dev/null
+++ b/components/card/media.tsx
@@ -0,0 +1,60 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import ConfigProvider from '../config-provider';
+import { log } from '../util';
+import type { CardMediaProps } from './types';
+
+const { warning } = log;
+
+const MEDIA_COMPONENTS = ['video', 'audio', 'picture', 'iframe', 'img'];
+
+class CardMedia extends Component {
+ static displayName = 'CardMedia';
+ static propTypes = {
+ prefix: PropTypes.string,
+ /**
+ * 设置标签类型
+ */
+ component: PropTypes.elementType,
+ /**
+ * 背景图片地址
+ */
+ image: PropTypes.string,
+ /**
+ * 媒体源文件地址
+ */
+ src: PropTypes.string,
+ style: PropTypes.object,
+ className: PropTypes.string,
+ };
+
+ static defaultProps = {
+ prefix: 'next-',
+ component: 'div',
+ style: {},
+ };
+
+ render() {
+ const { prefix, style, className, component, image, src, ...others } = this.props;
+ const Component = component as React.ElementType;
+ if (!('children' in others || Boolean(image || src))) {
+ warning('either `children`, `image` or `src` prop must be specified.');
+ }
+
+ const isMediaComponent = MEDIA_COMPONENTS.indexOf(component as string) !== -1;
+ const composedStyle =
+ !isMediaComponent && image ? { backgroundImage: `url("${image}")`, ...style } : style;
+
+ return (
+
+ );
+ }
+}
+
+export default ConfigProvider.config(CardMedia);
diff --git a/components/card/mobile/index.jsx b/components/card/mobile/index.jsx
deleted file mode 100644
index 8d5295d3c7..0000000000
--- a/components/card/mobile/index.jsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { Card as MeetCard } from '@alifd/meet-react';
-import NextCard from '../index';
-
-const Card = MeetCard ? MeetCard : NextCard;
-
-export default Card;
diff --git a/components/card/mobile/index.tsx b/components/card/mobile/index.tsx
new file mode 100644
index 0000000000..c54bcf7226
--- /dev/null
+++ b/components/card/mobile/index.tsx
@@ -0,0 +1,7 @@
+import { Card as MeetCard } from '@alifd/meet-react';
+import NextCard from '../index';
+
+// @ts-expect-error meet-react does not export Card
+const Card = MeetCard ? MeetCard : NextCard;
+
+export default Card;
diff --git a/components/card/style.js b/components/card/style.ts
similarity index 100%
rename from components/card/style.js
rename to components/card/style.ts
diff --git a/components/card/types.ts b/components/card/types.ts
new file mode 100644
index 0000000000..5bc4f0a15b
--- /dev/null
+++ b/components/card/types.ts
@@ -0,0 +1,298 @@
+import type { ElementType, HTMLAttributes, ReactNode } from 'react';
+import type { CommonProps } from '../util';
+
+type HTMLAttributesWeak = Omit, 'title'>;
+
+/**
+ * @api Card
+ * @order 0
+ */
+export interface CardProps extends HTMLAttributesWeak, CommonProps {
+ /**
+ * 设置类名前缀
+ * @en The prefix of class
+ * @defaultValue 'next-'
+ * @skip
+ */
+ prefix?: string;
+
+ /**
+ * 卡片的上的图片 / 视频
+ * @en Media content
+ */
+ media?: ReactNode;
+
+ /**
+ * 卡片的标题
+ * @en Title of card
+ */
+ title?: ReactNode;
+
+ /**
+ * 卡片的副标题
+ * @en Sub title of card
+ */
+ subTitle?: ReactNode;
+
+ /**
+ * 卡片操作组,位置在卡片底部
+ * @en Actions of card
+ */
+ actions?: ReactNode;
+
+ /**
+ * 是否显示标题的项目符号
+ * @en If show title bullet
+ * @defaultValue true
+ */
+ showTitleBullet?: boolean;
+
+ /**
+ * 是否显示标题的项目符号
+ * @en If show title bullet
+ * @deprecated Use showTitleBullet
+ * @skip
+ */
+ titlePrefixLine?: boolean;
+
+ /**
+ * 是否展示头部的分隔线
+ * @en If show head divider
+ * @defaultValue true
+ */
+ showHeadDivider?: boolean;
+
+ /**
+ * 是否展示头部的分隔线
+ * @en If show head divider
+ * @deprecated Use showHeadDivider
+ * @skip
+ */
+ titleBottomLine?: boolean;
+
+ /**
+ * 内容区域的固定高度
+ * @en Height of content
+ * @defaultValue 120
+ */
+ contentHeight?: string | number;
+
+ /**
+ * 内容区域的固定高度
+ * @en Height of content
+ * @deprecated Use contentHeight
+ * @skip
+ */
+ bodyHeight?: string | number;
+
+ /**
+ * 标题区域的用户自定义内容
+ * @en Extra of card header
+ */
+ extra?: ReactNode;
+
+ /**
+ * 是否开启自由模式,开启后 card 将使用子组件配合使用,设置此项后 title, subtitle, 等等属性都将失效
+ * @en Whether to open free mode, if opened, can not set title subTitle ..., must use Card.Header Card.Content ... to set Card
+ * @defaultValue false
+ */
+ free?: boolean;
+
+ /**
+ * 是否带边框
+ * @en Whether to show border
+ * @defaultValue true
+ * @version 1.24
+ */
+ hasBorder?: boolean;
+}
+
+/**
+ * @api Card.Media
+ * @order 1
+ */
+export interface CardMediaProps extends HTMLAttributesWeak, CommonProps {
+ /**
+ * 设置类名前缀
+ * @en The prefix of class
+ * @defaultValue 'next-'
+ * @skip
+ */
+ prefix?: string;
+ /**
+ * 设置样式
+ * @en The style of component
+ * @defaultValue \{\}
+ * @skip
+ */
+ style?: React.CSSProperties;
+ /**
+ * 设置标签类型
+ * @en The html tag to be rendered
+ * @defaultValue 'div'
+ */
+ component?: ElementType;
+ /**
+ * 背景图片地址
+ * @en Media background image
+ */
+ image?: string;
+ /**
+ * 媒体源文件地址
+ * @en Media source URL
+ */
+ src?: string;
+}
+
+/**
+ * @api Card.Header
+ * @order 2
+ */
+export interface CardHeaderProps extends HTMLAttributesWeak, CommonProps {
+ /**
+ * 设置类名前缀
+ * @en The prefix of class
+ * @defaultValue 'next-'
+ * @skip
+ */
+ prefix?: string;
+ /**
+ * 卡片的标题
+ * @en Title of card
+ */
+ title?: ReactNode;
+
+ /**
+ * 卡片的副标题
+ * @en Sub Title of Card
+ */
+ subTitle?: ReactNode;
+
+ /**
+ * 标题区域的用户自定义内容
+ * @en Extra of card header
+ */
+ extra?: ReactNode;
+
+ /**
+ * 设置标签类型
+ * @en The html tag to be rendered
+ * @defaultValue 'div'
+ */
+ component?: ElementType;
+}
+
+/**
+ * @api Card.Content
+ * @order 3
+ */
+export interface CardContentProps extends HTMLAttributesWeak, CommonProps {
+ /**
+ * 设置类名前缀
+ * @en The prefix of class
+ * @defaultValue 'next-'
+ * @skip
+ */
+ prefix?: string;
+ /**
+ * 设置标签类型
+ * @en The html tag to be rendered
+ * @defaultValue 'div'
+ */
+ component?: ElementType;
+}
+
+/**
+ * @api Card.Divider
+ * @order 4
+ */
+export interface CardDividerProps extends HTMLAttributesWeak, CommonProps {
+ /**
+ * 设置类名前缀
+ * @en The prefix of class
+ * @defaultValue 'next-'
+ * @skip
+ */
+ prefix?: string;
+ /**
+ * 设置标签类型
+ * @en The html tag to be rendered
+ * @defaultValue 'hr'
+ */
+ component?: ElementType;
+
+ /**
+ * 分割线是否向内缩进
+ * @en inset
+ */
+ inset?: boolean;
+}
+
+/**
+ * @api Card.Actions
+ * @order 5
+ */
+export interface CardActionsProps extends HTMLAttributesWeak, CommonProps {
+ /**
+ * 设置类名前缀
+ * @en The prefix of class
+ * @defaultValue 'next-'
+ * @skip
+ */
+ prefix?: string;
+ /**
+ * 设置标签类型
+ * @en The html tag to be rendered
+ * @defaultValue 'div'
+ */
+ component?: ElementType;
+}
+
+export interface CardBulletHeaderProps extends HTMLAttributesWeak, CommonProps {
+ /**
+ * 设置类名前缀
+ * @en The prefix of class
+ * @defaultValue 'next-'
+ * @skip
+ */
+ prefix?: string;
+ /**
+ * 卡片的标题
+ */
+ title?: ReactNode;
+
+ /**
+ * 卡片的副标题
+ */
+ subTitle?: ReactNode;
+ /**
+ * 是否显示标题的项目符号
+ */
+ showTitleBullet?: boolean;
+ /**
+ * 标题区域的用户自定义内容
+ */
+ extra?: ReactNode;
+}
+
+export interface CardCollapseContentProps extends HTMLAttributesWeak, CommonProps {
+ /**
+ * 设置类名前缀
+ * @en The prefix of class
+ * @defaultValue 'next-'
+ * @skip
+ */
+ prefix?: string;
+ /**
+ * 设置内容区域的固定高度
+ * @en Height of content
+ * @defaultValue 120
+ */
+ contentHeight?: string | number;
+}
+
+/**
+ * typo of CardCollapseContentProps
+ * @deprecated use CardCollapseContentProps instead
+ */
+export type CardCollaspeContentProps = CardCollapseContentProps;
diff --git a/components/cascader-select/__docs__/adaptor/index.jsx b/components/cascader-select/__docs__/adaptor/index.jsx
deleted file mode 100644
index 5008b9103f..0000000000
--- a/components/cascader-select/__docs__/adaptor/index.jsx
+++ /dev/null
@@ -1,106 +0,0 @@
-import React from 'react';
-import { Types, parseData, NodeType } from '@alifd/adaptor-helper';
-import { CascaderSelect } from '@alifd/next';
-
-let index = 1000;
-const createDataSource = (list, map = {}) => {
- if (!list) return [];
- return list.filter((item) => item.type === NodeType.node).map(({ value, children, state }) => {
- const key = String(index++);
- if (state === 'active') {
- if (!children || children.length === 0) {
- map.selecteds.push(key);
- } else {
- map.expandeds.push(key);
- }
- }
-
- return {
- value: key,
- label: value,
- disabled: state === 'disabled',
- children: createDataSource(children, map),
- };
- });
-};
-export default {
- name: 'CascaderSelect',
- editor: () => ({
- props: [{
- name: 'size',
- type: Types.enum,
- options: ['large', 'medium', 'small'],
- default: 'medium'
- }, {
- name: 'state',
- label: 'Status',
- type: Types.enum,
- options: ['normal', 'expanded', 'disabled'],
- default: 'normal'
- }, {
- name: 'width',
- type: Types.number,
- default: 300,
- }, {
- name: 'border',
- type: Types.bool,
- default: true,
- }, {
- name: 'checkbox',
- type: Types.bool,
- default: false
- }, {
- name: 'label',
- type: Types.string,
- default: ''
- }],
- data: {
- active: true,
- disabled: true,
- icon: true,
- default: '*1\n\t*1-1\n\t\t1-1-1\n\t\t1-1-2\n\t\t1-1-3\n\t\t1-1-4\n\t\t*1-1-5\n\t1-2\n\t1-3\n\t1-4\n\t1-5\n2\n\t2-1\n\t2-2\n\t2-3\n\t2-4\n\t2-5\n3\n\t3-1\n\t3-2\n\t3-3\n\t3-4\n\t3-5\n4\n\t4-1\n\t4-2\n\t4-3\n\t4-4\n\t4-5\n5\n\t5-1\n\t5-2\n\t5-3\n\t5-4\n\t5-5'
- }
- }),
- adaptor: ({ shape, size, state, width, border, checkbox, label, data, style = {}, ...others}) => {
- const list = parseData(data);
- const map = { selecteds: [], expandeds: [] };
- const dataSource = createDataSource(list, map);
- const value = map.selecteds;
-
- return (
- node} hasBorder={border} size={size} multiple={checkbox} value={value} visible={state === 'expanded'} disabled={state=== 'disabled'} dataSource={dataSource}/>
- );
- },
- content: () => ({
- options: [{
- name: 'checkbox',
- options: ['yes', 'no'],
- default: 'no'
- }, {
- name: 'border',
- options: ['show', 'hide'],
- default: 'show'
- }, {
- name: 'label',
- options: ['yes', 'no'],
- default: 'no'
- }],
- transform: (props, { checkbox, border, label }) => {
- return {
- ...props,
- checkbox: checkbox === 'yes',
- border: border === 'show',
- label: label === 'yes' ? 'Label' : ''
- };
- }
- }),
- demoOptions: (demo) => {
- const { node } = demo;
- const { props = {} } = node;
- if (props.state === 'expanded') {
- return { ...demo, height: 300 };
- }
-
- return demo;
- }
-};
diff --git a/components/cascader-select/__docs__/adaptor/index.tsx b/components/cascader-select/__docs__/adaptor/index.tsx
new file mode 100644
index 0000000000..d7179405b8
--- /dev/null
+++ b/components/cascader-select/__docs__/adaptor/index.tsx
@@ -0,0 +1,149 @@
+import React from 'react';
+import { Types, parseData, NodeType } from '@alifd/adaptor-helper';
+import { CascaderSelect } from '@alifd/next';
+
+let index = 1000;
+const createDataSource = (
+ list: Array,
+ map: { selecteds: string[]; expandeds: string[] }
+): Array => {
+ if (!list) return [];
+ return list
+ .filter(item => item.type === NodeType.node)
+ .map(({ value, children, state }) => {
+ const key = String(index++);
+ if (state === 'active') {
+ if (!children || children.length === 0) {
+ map.selecteds.push(key);
+ } else {
+ map.expandeds.push(key);
+ }
+ }
+
+ return {
+ value: key,
+ label: value,
+ disabled: state === 'disabled',
+ children: createDataSource(children, map),
+ };
+ });
+};
+export default {
+ name: 'CascaderSelect',
+ editor: () => ({
+ props: [
+ {
+ name: 'size',
+ type: Types.enum,
+ options: ['large', 'medium', 'small'],
+ default: 'medium',
+ },
+ {
+ name: 'state',
+ label: 'Status',
+ type: Types.enum,
+ options: ['normal', 'expanded', 'disabled'],
+ default: 'normal',
+ },
+ {
+ name: 'width',
+ type: Types.number,
+ default: 300,
+ },
+ {
+ name: 'border',
+ type: Types.bool,
+ default: true,
+ },
+ {
+ name: 'checkbox',
+ type: Types.bool,
+ default: false,
+ },
+ {
+ name: 'label',
+ type: Types.string,
+ default: '',
+ },
+ ],
+ data: {
+ active: true,
+ disabled: true,
+ icon: true,
+ default:
+ '*1\n\t*1-1\n\t\t1-1-1\n\t\t1-1-2\n\t\t1-1-3\n\t\t1-1-4\n\t\t*1-1-5\n\t1-2\n\t1-3\n\t1-4\n\t1-5\n2\n\t2-1\n\t2-2\n\t2-3\n\t2-4\n\t2-5\n3\n\t3-1\n\t3-2\n\t3-3\n\t3-4\n\t3-5\n4\n\t4-1\n\t4-2\n\t4-3\n\t4-4\n\t4-5\n5\n\t5-1\n\t5-2\n\t5-3\n\t5-4\n\t5-5',
+ },
+ }),
+ adaptor: ({
+ shape,
+ size,
+ state,
+ width,
+ border,
+ checkbox,
+ label,
+ data,
+ style = {},
+ ...others
+ }: any) => {
+ const list = parseData(data);
+ const map = { selecteds: [], expandeds: [] };
+ const dataSource = createDataSource(list, map);
+ const value = map.selecteds;
+
+ return (
+ node}
+ hasBorder={border}
+ size={size}
+ multiple={checkbox}
+ value={value}
+ visible={state === 'expanded'}
+ disabled={state === 'disabled'}
+ dataSource={dataSource}
+ />
+ );
+ },
+ content: () => ({
+ options: [
+ {
+ name: 'checkbox',
+ options: ['yes', 'no'],
+ default: 'no',
+ },
+ {
+ name: 'border',
+ options: ['show', 'hide'],
+ default: 'show',
+ },
+ {
+ name: 'label',
+ options: ['yes', 'no'],
+ default: 'no',
+ },
+ ],
+ transform: (props: any, { checkbox, border, label }: any) => {
+ return {
+ ...props,
+ checkbox: checkbox === 'yes',
+ border: border === 'show',
+ label: label === 'yes' ? 'Label' : '',
+ };
+ },
+ }),
+ demoOptions: (demo: any) => {
+ const { node } = demo;
+ const { props = {} } = node;
+ if (props.state === 'expanded') {
+ return { ...demo, height: 300 };
+ }
+
+ return demo;
+ },
+};
diff --git a/components/cascader-select/__docs__/demo/accessibility/index.tsx b/components/cascader-select/__docs__/demo/accessibility/index.tsx
index baa567b440..38ccffb696 100644
--- a/components/cascader-select/__docs__/demo/accessibility/index.tsx
+++ b/components/cascader-select/__docs__/demo/accessibility/index.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { CascaderSelect } from '@alifd/next';
+import type { CascaderSelectProps } from '@alifd/next/types/cascader-select';
import 'whatwg-fetch';
const data = [
@@ -48,19 +49,15 @@ const data = [
];
class Demo extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- data: [],
- };
- this.handleChange = this.handleChange.bind(this);
- }
+ state = {
+ data: [],
+ };
componentDidMount() {
this.setState({ data });
}
- handleChange(value, data, extra) {
+ handleChange: CascaderSelectProps['onChange'] = (value, data, extra) => {
console.log(value, data, extra);
- }
+ };
render() {
return (
console.log(e));
}
- handleChange = (value, data, extra) => {
+ handleChange: CascaderSelectProps['onChange'] = (value, data, extra) => {
console.log(value, data, extra);
};
diff --git a/components/cascader-select/__docs__/demo/change-on-select/index.tsx b/components/cascader-select/__docs__/demo/change-on-select/index.tsx
index e861a2d307..24168f9089 100644
--- a/components/cascader-select/__docs__/demo/change-on-select/index.tsx
+++ b/components/cascader-select/__docs__/demo/change-on-select/index.tsx
@@ -1,16 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { CascaderSelect } from '@alifd/next';
+import type { CascaderSelectProps } from '@alifd/next/types/cascader-select';
import 'whatwg-fetch';
class Demo extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {
- data: [],
- };
- }
+ state = {
+ data: [],
+ };
componentDidMount() {
fetch('https://os.alipayobjects.com/rmsportal/ODDwqcDFTLAguOvWEolX.json')
@@ -19,7 +16,7 @@ class Demo extends React.Component {
.catch(e => console.log(e));
}
- handleChange = (value, data, extra) => {
+ handleChange: CascaderSelectProps['onChange'] = (value, data, extra) => {
console.log(value, data, extra);
};
diff --git a/components/cascader-select/__docs__/demo/check-strictyle/index.tsx b/components/cascader-select/__docs__/demo/check-strictyle/index.tsx
index 549f375d52..0286b6f33e 100644
--- a/components/cascader-select/__docs__/demo/check-strictyle/index.tsx
+++ b/components/cascader-select/__docs__/demo/check-strictyle/index.tsx
@@ -1,17 +1,14 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Checkbox, CascaderSelect } from '@alifd/next';
+import type { CascaderSelectProps } from '@alifd/next/types/cascader-select';
import 'whatwg-fetch';
class Demo extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {
- data: [],
- checkStrictly: false,
- };
- }
+ state = {
+ data: [],
+ checkStrictly: false,
+ };
componentDidMount() {
fetch('https://os.alipayobjects.com/rmsportal/ODDwqcDFTLAguOvWEolX.json')
@@ -26,7 +23,7 @@ class Demo extends React.Component {
});
};
- handleChange = (value, data, extra) => {
+ handleChange: CascaderSelectProps['onChange'] = (value, data, extra) => {
console.log(value, data, extra);
};
diff --git a/components/cascader-select/__docs__/demo/custom-style/index.tsx b/components/cascader-select/__docs__/demo/custom-style/index.tsx
index 1d21f58e7d..c156f1f4a4 100644
--- a/components/cascader-select/__docs__/demo/custom-style/index.tsx
+++ b/components/cascader-select/__docs__/demo/custom-style/index.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { CascaderSelect, Icon } from '@alifd/next';
+import type { CascaderSelectProps } from '@alifd/next/types/cascader-select';
const dataSource = [
{
@@ -41,7 +42,7 @@ const dataSource = [
},
];
-const itemRender = item => {
+const itemRender: CascaderSelectProps['itemRender'] = item => {
return (
{item.label}
diff --git a/components/cascader-select/__docs__/demo/custom/index.tsx b/components/cascader-select/__docs__/demo/custom/index.tsx
index 44fd2d97ee..08d07a4fe5 100644
--- a/components/cascader-select/__docs__/demo/custom/index.tsx
+++ b/components/cascader-select/__docs__/demo/custom/index.tsx
@@ -1,18 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom';
-
import { CascaderSelect } from '@alifd/next';
+import type { CascaderSelectProps } from '@alifd/next/types/cascader-select';
import 'whatwg-fetch';
class Demo extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {
- data: [],
- };
- this.handleChange = this.handleChange.bind(this);
- }
+ state = {
+ data: [],
+ };
componentDidMount() {
fetch('https://os.alipayobjects.com/rmsportal/ODDwqcDFTLAguOvWEolX.json')
@@ -24,16 +19,16 @@ class Demo extends React.Component {
.catch(e => console.log(e));
}
- handleChange(value, data, extra) {
+ handleChange: CascaderSelectProps['onChange'] = (value, data, extra) => {
console.log(value, data, extra);
- }
+ };
- valueRender = item => {
+ valueRender: CascaderSelectProps['valueRender'] = item => {
if (item.label) {
- return item.label; // 正常的item
+ return item.label; // 正常的 item
}
- // value在 dataSouce里不存在时渲染。
+ // value 在 dataSouce 里不存在时渲染。
return item.value === '432988' ? '不存在的值' : item.value;
};
diff --git a/components/cascader-select/__docs__/demo/disabled/index.tsx b/components/cascader-select/__docs__/demo/disabled/index.tsx
index 79da58d3c2..ba68268d7b 100644
--- a/components/cascader-select/__docs__/demo/disabled/index.tsx
+++ b/components/cascader-select/__docs__/demo/disabled/index.tsx
@@ -2,46 +2,43 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { CascaderSelect } from '@alifd/next';
import 'whatwg-fetch';
+import type { CascaderSelectProps } from '@alifd/next/types/cascader-select';
class Demo extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {
- data: [
- {
- value: '2973',
- label: '陕西',
- children: [
- {
- value: '2974',
- label: '西安',
- children: [
- {
- value: '2975',
- label: '西安市',
- isLeaf: true,
- checkboxDisabled: true,
- },
- { value: '2976', label: '高陵县', isLeaf: true },
- ],
- },
- {
- value: '2980',
- label: '铜川',
- disabled: true,
- children: [
- { value: '2981', label: '铜川市', isLeaf: true },
- { value: '2982', label: '宜君县', isLeaf: true },
- ],
- },
- ],
- },
- ],
- };
- }
+ state = {
+ data: [
+ {
+ value: '2973',
+ label: '陕西',
+ children: [
+ {
+ value: '2974',
+ label: '西安',
+ children: [
+ {
+ value: '2975',
+ label: '西安市',
+ isLeaf: true,
+ checkboxDisabled: true,
+ },
+ { value: '2976', label: '高陵县', isLeaf: true },
+ ],
+ },
+ {
+ value: '2980',
+ label: '铜川',
+ disabled: true,
+ children: [
+ { value: '2981', label: '铜川市', isLeaf: true },
+ { value: '2982', label: '宜君县', isLeaf: true },
+ ],
+ },
+ ],
+ },
+ ],
+ };
- handleChange = (value, data, extra) => {
+ handleChange: CascaderSelectProps['onChange'] = (value, data, extra) => {
console.log(value, data, extra);
};
diff --git a/components/cascader-select/__docs__/demo/dynamic/index.tsx b/components/cascader-select/__docs__/demo/dynamic/index.tsx
index 4a92dfb6c4..4f549ef413 100644
--- a/components/cascader-select/__docs__/demo/dynamic/index.tsx
+++ b/components/cascader-select/__docs__/demo/dynamic/index.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { CascaderSelect } from '@alifd/next';
import 'whatwg-fetch';
+import type { CascaderSelectProps } from '@alifd/next/types/cascader-select';
const dataSource = [
{
@@ -11,20 +12,14 @@ const dataSource = [
];
class Demo extends React.Component {
- constructor(props) {
- super(props);
+ state = {
+ dataSource,
+ };
- this.state = {
- dataSource,
- };
-
- this.onLoadData = this.onLoadData.bind(this);
- }
-
- onLoadData(data) {
+ onLoadData: CascaderSelectProps['loadData'] = data => {
console.log(data);
- return new Promise(resolve => {
+ return new Promise(resolve => {
setTimeout(() => {
this.setState(
{
@@ -57,7 +52,7 @@ class Demo extends React.Component {
);
}, 500);
});
- }
+ };
render() {
return ;
diff --git a/components/cascader-select/__docs__/demo/expand-trigger/index.tsx b/components/cascader-select/__docs__/demo/expand-trigger/index.tsx
index 5846dc0410..37ad81bbcd 100644
--- a/components/cascader-select/__docs__/demo/expand-trigger/index.tsx
+++ b/components/cascader-select/__docs__/demo/expand-trigger/index.tsx
@@ -2,21 +2,16 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { Radio, CascaderSelect } from '@alifd/next';
import 'whatwg-fetch';
+import type { CascaderSelectProps } from '@alifd/next/types/cascader-select';
+import type { RadioProps } from '@alifd/next/types/radio';
const RadioGroup = Radio.Group;
class Demo extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {
- triggerType: 'click',
- data: [],
- };
-
- this.handleChange = this.handleChange.bind(this);
- this.handleTriggerTypeChange = this.handleTriggerTypeChange.bind(this);
- }
+ state = {
+ triggerType: 'click' as const,
+ data: [],
+ };
componentDidMount() {
fetch('https://os.alipayobjects.com/rmsportal/ODDwqcDFTLAguOvWEolX.json')
@@ -25,15 +20,15 @@ class Demo extends React.Component {
.catch(e => console.log(e));
}
- handleChange(value, data, extra) {
+ handleChange: CascaderSelectProps['onChange'] = (value, data, extra) => {
console.log(value, data, extra);
- }
+ };
- handleTriggerTypeChange(triggerType) {
+ handleTriggerTypeChange: RadioProps['onChange'] = triggerType => {
this.setState({
triggerType,
});
- }
+ };
render() {
return (
diff --git a/components/cascader-select/__docs__/demo/expanded-value/index.tsx b/components/cascader-select/__docs__/demo/expanded-value/index.tsx
index 0bf7bc514c..4bdb7b9c8a 100644
--- a/components/cascader-select/__docs__/demo/expanded-value/index.tsx
+++ b/components/cascader-select/__docs__/demo/expanded-value/index.tsx
@@ -2,40 +2,37 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { CascaderSelect } from '@alifd/next';
import 'whatwg-fetch';
+import type { CascaderSelectProps } from '@alifd/next/types/cascader-select';
class Demo extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {
- data: [
- {
- value: '2973',
- label: '陕西',
- children: [
- {
- value: '2974',
- label: '西安',
- children: [
- { value: '2975', label: '西安市', isLeaf: true },
- { value: '2976', label: '高陵县', isLeaf: true },
- ],
- },
- {
- value: '2980',
- label: '铜川',
- children: [
- { value: '2981', label: '铜川市', isLeaf: true },
- { value: '2982', label: '宜君县', isLeaf: true },
- ],
- },
- ],
- },
- ],
- };
- }
+ state = {
+ data: [
+ {
+ value: '2973',
+ label: '陕西',
+ children: [
+ {
+ value: '2974',
+ label: '西安',
+ children: [
+ { value: '2975', label: '西安市', isLeaf: true },
+ { value: '2976', label: '高陵县', isLeaf: true },
+ ],
+ },
+ {
+ value: '2980',
+ label: '铜川',
+ children: [
+ { value: '2981', label: '铜川市', isLeaf: true },
+ { value: '2982', label: '宜君县', isLeaf: true },
+ ],
+ },
+ ],
+ },
+ ],
+ };
- handleChange = (value, data, extra) => {
+ handleChange: CascaderSelectProps['onChange'] = (value, data, extra) => {
console.log(value, data, extra);
};
diff --git a/components/cascader-select/__docs__/demo/multiple/index.tsx b/components/cascader-select/__docs__/demo/multiple/index.tsx
index 1f869aae2d..f0d9aef090 100644
--- a/components/cascader-select/__docs__/demo/multiple/index.tsx
+++ b/components/cascader-select/__docs__/demo/multiple/index.tsx
@@ -2,17 +2,12 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { CascaderSelect } from '@alifd/next';
import 'whatwg-fetch';
+import type { CascaderSelectProps } from '@alifd/next/types/cascader-select';
class Demo extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {
- data: [],
- };
-
- this.handleChange = this.handleChange.bind(this);
- }
+ state = {
+ data: [],
+ };
componentDidMount() {
fetch('https://os.alipayobjects.com/rmsportal/ODDwqcDFTLAguOvWEolX.json')
@@ -23,9 +18,9 @@ class Demo extends React.Component {
.catch(e => console.log(e));
}
- handleChange(value, data, extra) {
+ handleChange: CascaderSelectProps['onChange'] = (value, data, extra) => {
console.log(value, data, extra);
- }
+ };
render() {
return (
diff --git a/components/cascader-select/__docs__/demo/only-leaf/index.tsx b/components/cascader-select/__docs__/demo/only-leaf/index.tsx
index ec8edf1fab..1ea22d0317 100644
--- a/components/cascader-select/__docs__/demo/only-leaf/index.tsx
+++ b/components/cascader-select/__docs__/demo/only-leaf/index.tsx
@@ -2,15 +2,12 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { CascaderSelect } from '@alifd/next';
import 'whatwg-fetch';
+import type { CascaderSelectProps } from '@alifd/next/types/cascader-select';
class Demo extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {
- data: [],
- };
- }
+ state = {
+ data: [],
+ };
componentDidMount() {
fetch('https://os.alipayobjects.com/rmsportal/ODDwqcDFTLAguOvWEolX.json')
@@ -19,7 +16,7 @@ class Demo extends React.Component {
.catch(e => console.log(e));
}
- handleChange = (value, data, extra) => {
+ handleChange: CascaderSelectProps['onChange'] = (value, data, extra) => {
console.log(value, data, extra);
};
diff --git a/components/cascader-select/__docs__/demo/search-async/index.tsx b/components/cascader-select/__docs__/demo/search-async/index.tsx
index cf43fd516a..4e2b69c922 100644
--- a/components/cascader-select/__docs__/demo/search-async/index.tsx
+++ b/components/cascader-select/__docs__/demo/search-async/index.tsx
@@ -1,9 +1,10 @@
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
-import { CascaderSelect, Icon } from '@alifd/next';
+import { CascaderSelect } from '@alifd/next';
+import type { CascaderSelectProps } from '@alifd/next/types/cascader-select';
function Demo() {
- const [data, setData] = useState([]);
+ const [data, setData] = useState>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
@@ -15,21 +16,21 @@ function Demo() {
.catch(e => console.log(e));
}, []);
- let timeId;
+ let timeId: number | undefined;
const duration = 1000;
- function handleSearch(searchVal) {
+ const handleSearch: CascaderSelectProps['onSearch'] = searchVal => {
setLoading(true);
if (timeId) {
clearTimeout(timeId);
}
- timeId = setTimeout(() => {
+ timeId = window.setTimeout(() => {
if (searchVal) {
- const item = { ...data[0].children[0].children[0] };
+ const item = { ...data[0].children![0].children![0] };
item.label = `${searchVal}_${item.label}`;
item.value = `${Date.now()}`;
- data[0].children[0].children[0] = item;
+ data[0].children![0].children![0] = item;
setData([...data]);
}
@@ -37,7 +38,7 @@ function Demo() {
timeId = undefined;
setLoading(false);
}, duration);
- }
+ };
return (
console.log(e));
}
- handleChange = (value, data, extra) => {
+ handleChange: CascaderSelectProps['onChange'] = (value, data, extra) => {
console.log(value, data, extra);
};
- filter(searchValue, path) {
- return (
+ filter: CascaderSelectProps['filter'] = (searchValue, path) => {
+ return !!(
searchValue === '' ||
path
.map(({ value, label }) => `${value}${label}`)
.join('')
.match(searchValue)
);
- }
+ };
render() {
return (
diff --git a/components/cascader-select/__docs__/index.en-us.md b/components/cascader-select/__docs__/index.en-us.md
index 358a980f83..2fdf285860 100644
--- a/components/cascader-select/__docs__/index.en-us.md
+++ b/components/cascader-select/__docs__/index.en-us.md
@@ -17,90 +17,81 @@ CascaderSelect consists of Select and Cascader. Cascader are hidden in a pop up
### CascaderSelect
-| Param | Description | Type | Default Value |
-| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ---------------------------- |
-| size | size of selector **options**: 'small', 'medium', 'large' | Enum | 'medium' |
-| placeholder | placeholder of selector | String | - |
-| disabled | whether is disabled | Boolean | false |
-| hasArrow | whether has arrow icon | Boolean | true |
-| hasBorder | whether selector has border | Boolean | true |
-| hasClear | whether has clear button | Boolean | false |
-| label | custom inline label | ReactNode | - |
-| readOnly | whether selector is read only, it can be expanded but cannot be selected under read only mode | Boolean | - |
-| dataSource | data source, structure can refer to the following document | Array<Object> | \[] |
-| defaultValue | (under uncontrol) default value | String/Array<String> | null |
-| value | (under control) current value | String/Array<String> | - |
-| onChange | callback triggered when value changes **signatures**: Function(value: String/Array, data: Object/Array, extra: Object) => void **params**: _value_: {String/Array} selected value, a single value is returned when single select, and an array is returned when multiple select _data_: {Object/Array} selected data, including value, label, returns a single value when single select, returns an array when multiple select, parent and child nodes are selected at the same time, only the parent node is returned _extra_: {Object} extra param _extra.selectedPath_: {Array} path of the selected data when single selecte _extra.checked_: {Boolean} whether is checked when multiple select _extra.currentData_: {Object} current operation data when multiple select _extra.checkedData_: {Array} all checked data when multiple select _extra.indeterminateData_: {Array} indeterminate data when multile selec | Function | - |
-| defaultExpandedValue | (under uncontrol) default expanded value, if not set, the component will be automatically set according to defaultValue/value | Array<String> | - |
-| expandedValue | (under control) current expanded value | Array<String> | - |
-| expandTriggerType | expand trigger type **options**: 'click', 'hover' | Enum | 'click' |
-| multiple | whether is multiple select | Boolean | false |
-| changeOnSelect | change immediately if selected, this property is only worked in single selection mode | Boolean | false |
-| canOnlyCheckLeaf | whether checkbox of leaf item can only be checked, this property is only worked in multiple selection mode | Boolean | false |
-| checkStrictly | whether selection of parent and child nodes are related | Boolean | false |
-| listStyle | style of list | Object | - |
-| listClassName | class name of list | String | - |
-| displayRender | custom rendering function to display results **signatures**: Function(label: Array) => ReactNode **params**: _label_: {Array} label array of the selected path **returns**: {ReactNode} display content | Function | single select: labelPath => labelPath.join(' / '); multiple select: labelPath => labelPath[labelPath.length - 1] |
-| showSearch | whether to show the search box | Boolean | false |
-| onSearch | Callback when the search box value changes **Signature**: Function(value: String) => void **Parameters**: _value_: {String } Data | Function | func.noop |
-| filter | custom search function **signatures**: Function(searchValue: String, path: Array) => Boolean **params**: _searchValue_: {String} search keyword _path_: {Array} item path **returns**: {Boolean} whether is matched | Function | fuzzy matching of label based on all nodes of the path |
-| resultRender | custom render function of search result **signatures**: Function(searchValue: String, path: Array) => ReactNode **params**: _searchValue_: {String} search keyword _path_: {Array} matched item path **returns**: {ReactNode} result content | Function | rendering by pattern of a / b / c |
-| resultAutoWidth | whether the search result list is equal to the selector | Boolean | true |
-| notFoundContent | content without data | ReactNode | 'Not Found' |
-| loadData | asynchronous data loading function **signatures**: Function(data: Object) => void **params**: _data_: {Object} data of the clicked item | Function | - |
-| header | custom dropdown header | ReactNode | - |
-| footer | custom dropdown footer | ReactNode | - |
-| defaultVisible | whether dropdown is default visible | Boolean | false |
-| visible | whether dropdown is current visible | Boolean | - |
-| onVisibleChange | callback triggered when open or close dropdown **signatures**: Function(visible: Boolean, type: String) => void **params**: _visible_: {Boolean} whether is visible _type_: {String} trigger type | Function | () => {} |
-| popupStyle | style of dropdown | Object | - |
-| popupClassName | className of dropdown | String | - |
-| popupContainer | container of dropdown | String/Function | - |
-| popupProps | properties of Popup | Object | {} |
-| followTrigger | follow Trigger or not | Boolean | - |
-| immutable | whether allow immutable dataSource | Boolean | false | 1.23 |
+Inherits partial props from Cascader, support passing props to Cascader: dataSource, useVirtual, multiple, canOnlyCheckLeaf,
+checkStrictly, resultRender, expandedValue, defaultExpandedValue, expandTriggerType, onExpand, listStyle, listClassName, loadData, i
+temRender, immutable. Support passing props to Select: other Select props except those listed above and those listed below.
+
+| Param | Description | Type | Default Value | Required | Supported Version |
+| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------- | ----------------- |
+| size | Size | 'small' \| 'medium' \| 'large' | 'medium' | | - |
+| disabled | Disabled | boolean | false | | - |
+| hasArrow | HasArrow | boolean | true | | - |
+| hasBorder | HasBorder | boolean | true | | - |
+| hasClear | HasClear | boolean | false | | - |
+| readOnly | ReadOnly, popup layer can be expanded but cannot be selected in read | boolean | - | | - |
+| defaultValue | Default value(not controlled) | string \| Array\ | - | | - |
+| value | Current value(controlled) | string \| Array\ | - | | - |
+| onChange | Callback when selected value changes | ( value: string \| Array\ \| null, data: CascaderDataItem \| Array\ \| null, extra?: Extra ) => void | - | | - |
+| changeOnSelect | Whether to call onChange as soon as selected, this property only works in single selection mode | boolean | false | | - |
+| displayRender | Custom render function of selected result | ( label: Array\, data: CascaderSelectDataItem ) => React.ReactNode | - | | - |
+| showSearch | Show search box | boolean | false | | - |
+| filter | Custom search function | (searchValue: string, path: CascaderSelectDataItem[]) => boolean | - | | - |
+| onSearch | Callback when search value changes | (value: string) => void | - | | 1.23 |
+| resultAutoWidth | Whether the search result list is the same width as the selection box | boolean | true | | - |
+| notFoundContent | Content when no data | React.ReactNode | - | | - |
+| header | Custom dropdown header | React.ReactNode | - | | - |
+| footer | Custom dropdown footer | React.ReactNode | - | | - |
+| defaultVisible | Visible by default | boolean | false | | - |
+| visible | Current visible | boolean | - | | - |
+| onVisibleChange | - | (visible: boolean, type?: CascaderSelectVisibleChangeType) => void | - | | - |
+| popupProps | Props object passed to Popup | React.ComponentPropsWithRef\ | - | | - |
+| isPreview | Whether it is in preview mode | boolean | false | | - |
+| renderPreview | Custom preview | ( value: CascaderSelectDataItem \| CascaderSelectDataItem[], props: CascaderSelectProps ) => React.ReactNode | - | | - |
+| menuProps | Props object passed to Cascader:The parameters focusedKey, onItemFocus, className, style, focusable, and isSelectIconRight are invalid. Additionally, onBlur is invalid when passed under the filter | Omit\ | - | | - |
+| autoClearSearchValue | Whether the current search will be cleared on selecting an item. Only applies when multiple is true | boolean | false | | - |
### Data structure of dataSource
```js
-const dataSource = [{
- value: '2974',
- label: '西安',
- children: [
- { value: '2975', label: '西安市', disabled: true },
- { value: '2976', label: '高陵县', checkboxDisabled: true },
- { value: '2977', label: '蓝田县' },
- { value: '2978', label: '户县' },
- { value: '2979', label: '周至县' },
- { value: '4208', label: '灞桥区' },
- { value: '4209', label: '长安区' },
- { value: '4210', label: '莲湖区' },
- { value: '4211', label: '临潼区' },
- { value: '4212', label: '未央区' },
- { value: '4213', label: '新城区' },
- { value: '4214', label: '阎良区' },
- { value: '4215', label: '雁塔区' },
- { value: '4388', label: '碑林区' },
- { value: '610127', label: '其它区' }
- ]
-}];
+const dataSource = [
+ {
+ value: '2974',
+ label: '西安',
+ children: [
+ { value: '2975', label: '西安市', disabled: true },
+ { value: '2976', label: '高陵县', checkboxDisabled: true },
+ { value: '2977', label: '蓝田县' },
+ { value: '2978', label: '户县' },
+ { value: '2979', label: '周至县' },
+ { value: '4208', label: '灞桥区' },
+ { value: '4209', label: '长安区' },
+ { value: '4210', label: '莲湖区' },
+ { value: '4211', label: '临潼区' },
+ { value: '4212', label: '未央区' },
+ { value: '4213', label: '新城区' },
+ { value: '4214', label: '阎良区' },
+ { value: '4215', label: '雁塔区' },
+ { value: '4388', label: '碑林区' },
+ { value: '610127', label: '其它区' },
+ ],
+ },
+];
```
The custom attribute of item in the array is also transparently passed to the data parameter of the onChange function.
-
## ARIA and KeyBoard
-| 按键 | 说明 |
-| :---------- | :------------------------------ |
-| Up Arrow | Get the previous item focus of the current item of same level |
-| Down Arrow | Get the next item focus of the current item of same level |
+| 按键 | 说明 |
+| :---------- | :--------------------------------------------------------------------------------------- |
+| Up Arrow | Get the previous item focus of the current item of same level |
+| Down Arrow | Get the next item focus of the current item of same level |
| Left Arrow | Enter the child element of the current item and get the first child element as the focus |
-| Right Arrow | Returns the parent of the current item and gets the focus |
-| Enter | Open the directory or select current item |
-| Esc | Close the directory |
-| SPACE | Select current item |
+| Right Arrow | Returns the parent of the current item and gets the focus |
+| Enter | Open the directory or select current item |
+| Esc | Close the directory |
+| SPACE | Select current item |
diff --git a/components/cascader-select/__docs__/index.md b/components/cascader-select/__docs__/index.md
index 7f9e01030e..960a60f7d9 100644
--- a/components/cascader-select/__docs__/index.md
+++ b/components/cascader-select/__docs__/index.md
@@ -17,79 +17,67 @@
### CascaderSelect
-| 参数 | 说明 | 类型 | 默认值 | 版本支持 |
-| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | --------------------------------------------------------------------------------------- | ---- |
-| size | 选择框大小 **可选值**: 'small', 'medium', 'large' | Enum | 'medium' | |
-| placeholder | 选择框占位符 | String | - | |
-| disabled | 是否禁用 | Boolean | false | |
-| hasArrow | 是否有下拉箭头 | Boolean | true | |
-| hasBorder | 是否有边框 | Boolean | true | |
-| hasClear | 是否有清除按钮 | Boolean | false | |
-| label | 自定义内联 label | ReactNode | - | |
-| readOnly | 是否只读,只读模式下可以展开弹层但不能选 | Boolean | - | |
-| dataSource | 数据源,结构可参考下方说明 | Array<Object> | \[] | |
-| defaultValue | (非受控)默认值 | String/Array<String> | null | |
-| value | (受控)当前值 | String/Array<String> | - | |
-| onChange | 选中值改变时触发的回调函数 **签名**: Function(value: String/Array, data: Object/Array, extra: Object) => void **参数**: _value_: {String/Array} 选中的值,单选时返回单个值,多选时返回数组 _data_: {Object/Array} 选中的数据,包括 value 和 label,单选时返回单个值,多选时返回数组,父子节点选中关联时,同时选中,只返回父节点 _extra_: {Object} 额外参数 _extra.selectedPath_: {Array} 单选时选中的数据的路径 _extra.checked_: {Boolean} 多选时当前的操作是选中还是取消选中 _extra.currentData_: {Object} 多选时当前操作的数据 _extra.checkedData_: {Array} 多选时所有被选中的数据 _extra.indeterminateData_: {Array} 多选时半选的数据 | Function | - | |
-| defaultExpandedValue | 默认展开值,如果不设置,组件内部会根据 defaultValue/value 进行自动设置 | Array<String> | - | |
-| expandedValue | (受控)当前展开值 | Array<String> | - | |
-| expandTriggerType | 展开触发的方式 **可选值**: 'click', 'hover' | Enum | 'click' | |
-| useVirtual | 是否开启虚拟滚动 | Boolean | false | |
-| multiple | 是否多选 | Boolean | false | |
-| changeOnSelect | 是否选中即发生改变, 该属性仅在单选模式下有效 | Boolean | false | |
-| canOnlyCheckLeaf | 是否只能勾选叶子项的checkbox,该属性仅在多选模式下有效 | Boolean | false | |
-| checkStrictly | 父子节点是否选中不关联 | Boolean | false | |
-| listStyle | 每列列表样式对象 | Object | - | |
-| listClassName | 每列列表类名 | String | - | |
-| displayRender | 选择框单选时展示结果的自定义渲染函数 **签名**: Function(label: Array) => ReactNode **参数**: _label_: {Array} 选中路径的文本数组 **返回值**: {ReactNode} 渲染在选择框中的内容 | Function | 单选时:labelPath => labelPath.join(' / ');多选时:labelPath => labelPath[labelPath.length - 1] | |
-| itemRender | 渲染 item 内容的方法 **签名**: Function(item: Object) => ReactNode **参数**: _item_: {Object} 渲染节点的item **返回值**: {ReactNode} item node | Function | - | |
-| showSearch | 是否显示搜索框 | Boolean | false | |
-| filter | 自定义搜索函数 **签名**: Function(searchValue: String, path: Array) => Boolean **参数**: _searchValue_: {String} 搜索的关键字 _path_: {Array} 节点路径 **返回值**: {Boolean} 是否匹配 | Function | 根据路径所有节点的文本值模糊匹配 | |
-| onSearch | 当搜索框值变化时回调 **签名**: Function(value: String) => void **参数**: _value_: {String} 数据 | Function | - | 1.23 |
-| resultRender | 搜索结果自定义渲染函数 **签名**: Function(searchValue: String, path: Array) => ReactNode **参数**: _searchValue_: {String} 搜索的关键字 _path_: {Array} 匹配到的节点路径 **返回值**: {ReactNode} 渲染的内容 | Function | 按照节点文本 a / b / c 的模式渲染 | |
-| resultAutoWidth | 搜索结果列表是否和选择框等宽 | Boolean | true | |
-| notFoundContent | 无数据时显示内容 | ReactNode | - | |
-| loadData | 异步加载数据函数 **签名**: Function(data: Object) => void **参数**: _data_: {Object} 当前点击异步加载的数据 | Function | - | |
-| header | 自定义下拉框头部 | ReactNode | - | |
-| footer | 自定义下拉框底部 | ReactNode | - | |
-| defaultVisible | 初始下拉框是否显示 | Boolean | false | |
-| visible | 当前下拉框是否显示 | Boolean | - | |
-| onVisibleChange | 下拉框显示或关闭时触发事件的回调函数 **签名**: Function(visible: Boolean, type: String) => void **参数**: _visible_: {Boolean} 是否显示 _type_: {String} 触发显示关闭的操作类型, fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发 | Function | () => {} | |
-| popupStyle | 下拉框自定义样式对象 | Object | - | |
-| popupClassName | 下拉框样式自定义类名 | String | - | |
-| popupContainer | 下拉框挂载的容器节点 | any | - | |
-| popupProps | 透传到 Popup 的属性对象 | Object | {} | |
-| followTrigger | 是否跟随滚动 | Boolean | - | |
-| isPreview | 是否为预览态 | Boolean | - | |
-| renderPreview | 预览态模式下渲染的内容 **签名**: Function(value: Array) => void **参数**: _value_: {Array} 选择值 { label: , value:} | Function | - | |
-| immutable | 是否是不可变数据 | Boolean | false | 1.23 |
+继承 Cascader, Select 的部分属性,支持透传给 Cascader 的属性有 dataSource, useVirtual, multiple, canOnlyCheckLeaf,
+checkStrictly, resultRender, expandedValue, defaultExpandedValue, expandTriggerType, onExpand, listStyle,
+listClassName, loadData, itemRender, immutable。支持透传给 Select 的包括除上面传给 Cascader 和下方单独列出的属性以外的其他全部属性。
+
+| 参数 | 说明 | 类型 | 默认值 | 是否必填 | 支持版本 |
+| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------- | -------- |
+| size | 选择框大小 | 'small' \| 'medium' \| 'large' | 'medium' | | - |
+| disabled | 是否禁用 | boolean | false | | - |
+| hasArrow | 是否有下拉箭头 | boolean | true | | - |
+| hasBorder | 是否有边框 | boolean | true | | - |
+| hasClear | 是否有清除按钮 | boolean | false | | - |
+| readOnly | 是否只读,只读模式下可以展开弹层但不能选 | boolean | - | | - |
+| defaultValue | (非受控)默认值 | string \| Array\ | - | | - |
+| value | (受控)当前值 | string \| Array\ | - | | - |
+| onChange | 选中值改变时触发的回调函数 | ( value: string \| Array\ \| null, data: CascaderDataItem \| Array\ \| null, extra?: Extra ) => void | - | | - |
+| changeOnSelect | 是否选中即发生改变,该属性仅在单选模式下有效 | boolean | false | | - |
+| displayRender | 选择框单选时展示结果的自定义渲染函数 | ( label: Array\, data: CascaderSelectDataItem ) => React.ReactNode | - | | - |
+| showSearch | 是否显示搜索框 | boolean | false | | - |
+| filter | 自定义搜索函数 | (searchValue: string, path: CascaderSelectDataItem[]) => boolean | - | | - |
+| onSearch | 当搜索框值变化时回调 | (value: string) => void | - | | 1.23 |
+| resultAutoWidth | 搜索结果列表是否和选择框等宽 | boolean | true | | - |
+| notFoundContent | 无数据时显示内容 | React.ReactNode | - | | - |
+| header | 自定义下拉框头部 | React.ReactNode | - | | - |
+| footer | 自定义下拉框底部 | React.ReactNode | - | | - |
+| defaultVisible | 初始下拉框是否显示 | boolean | false | | - |
+| visible | 当前下拉框是否显示 | boolean | - | | - |
+| onVisibleChange | 下拉框显示或关闭时触发事件的回调函数 | (visible: boolean, type?: CascaderSelectVisibleChangeType) => void | - | | - |
+| popupProps | 透传到 Popup 的属性对象 | React.ComponentPropsWithRef\ | - | | - |
+| isPreview | 是否为预览态 | boolean | false | | - |
+| renderPreview | 自定义预览态 | ( value: CascaderSelectDataItem \| CascaderSelectDataItem[], props: CascaderSelectProps ) => React.ReactNode | - | | - |
+| menuProps | 透传到 Cascader 的属性对象;focusedKey、onItemFocus、className、style、focusable、isSelectIconRight 传入无效,其中 onBlur 在 filter 下传入无效 | Omit\ | - | | - |
+| autoClearSearchValue | 是否在选中项后清空搜索框,只在 multiple 为 true 时有效 | boolean | false | | - |
### dataSource数据结构
```js
-const dataSource = [{
- value: '2974',
- label: '西安',
- children: [
- { value: '2975', label: '西安市', disabled: true },
- { value: '2976', label: '高陵县', checkboxDisabled: true },
- { value: '2977', label: '蓝田县' },
- { value: '2978', label: '户县' },
- { value: '2979', label: '周至县' },
- { value: '4208', label: '灞桥区' },
- { value: '4209', label: '长安区' },
- { value: '4210', label: '莲湖区' },
- { value: '4211', label: '临潼区' },
- { value: '4212', label: '未央区' },
- { value: '4213', label: '新城区' },
- { value: '4214', label: '阎良区' },
- { value: '4215', label: '雁塔区' },
- { value: '4388', label: '碑林区' },
- { value: '610127', label: '其它区' }
- ]
-}];
+const dataSource = [
+ {
+ value: '2974',
+ label: '西安',
+ children: [
+ { value: '2975', label: '西安市', disabled: true },
+ { value: '2976', label: '高陵县', checkboxDisabled: true },
+ { value: '2977', label: '蓝田县' },
+ { value: '2978', label: '户县' },
+ { value: '2979', label: '周至县' },
+ { value: '4208', label: '灞桥区' },
+ { value: '4209', label: '长安区' },
+ { value: '4210', label: '莲湖区' },
+ { value: '4211', label: '临潼区' },
+ { value: '4212', label: '未央区' },
+ { value: '4213', label: '新城区' },
+ { value: '4214', label: '阎良区' },
+ { value: '4215', label: '雁塔区' },
+ { value: '4388', label: '碑林区' },
+ { value: '610127', label: '其它区' },
+ ],
+ },
+];
```
数组中 Item 的自定义属性也会被透传到 onChange 函数的 data 参数中。
@@ -98,12 +86,12 @@ const dataSource = [{
## 无障碍键盘操作指南
-| 按键 | 说明 |
-| :---------- | :--------------------- |
-| Up Arrow | 获取同级当前项前一项焦点 |
-| Down Arrow | 获取同级当前项后一项焦点 |
+| 按键 | 说明 |
+| :---------- | :------------------------------------------- |
+| Up Arrow | 获取同级当前项前一项焦点 |
+| Down Arrow | 获取同级当前项后一项焦点 |
| Left Arrow | 进入当前项的子元素,并获取第一个子元素为焦点 |
-| Right Arrow | 返回当前项的父元素并获取焦点 |
-| Enter | 打开目录或选择当前项 |
-| Esc | 关闭目录 |
-| SPACE | 选择当前项 |
+| Right Arrow | 返回当前项的父元素并获取焦点 |
+| Enter | 打开目录或选择当前项 |
+| Esc | 关闭目录 |
+| SPACE | 选择当前项 |
diff --git a/components/cascader-select/__docs__/theme/index.jsx b/components/cascader-select/__docs__/theme/index.jsx
deleted file mode 100644
index 01451bc9ac..0000000000
--- a/components/cascader-select/__docs__/theme/index.jsx
+++ /dev/null
@@ -1,153 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-import '../../../demo-helper/style';
-import '../../style';
-import { Demo, DemoGroup, DemoHead, initDemo } from '../../../demo-helper';
-import CascaderSelect from '../../index';
-import ConfigProvider from '../../../config-provider';
-import zhCN from '../../../locale/zh-cn';
-import enUS from '../../../locale/en-us';
-
-const i18nMap = {
- 'en-us': {
- label: 'Label'
- },
- 'zh-cn': {
- label: '标签:'
- }
-};
-
-const createDataSource = () => {
- const dataSource = [];
-
- for (let i = 0; i < 10; i++) {
- const level1 = {
- label: `${i}`,
- value: `${i}`,
- children: []
- };
- dataSource.push(level1);
- for (let j = 0; j < 10; j++) {
- const level2 = {
- label: `${i}-${j}`,
- value: `${i}-${j}`,
- children: []
- };
- level1.children.push(level2);
- for (let k = 0; k < 10; k++) {
- const level3 = {
- label: `${i}-${j}-${k}`,
- value: `${i}-${j}-${k}`
- };
- level2.children.push(level3);
- }
- }
- }
-
- dataSource[1].disabled = true;
-
- return dataSource;
-};
-
-class FunctionDemo extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- demoFunction: {
- hasBorder: {
- label: '有无边框',
- value: 'true',
- enum: [{
- label: '有',
- value: 'true'
- }, {
- label: '无',
- value: 'false'
- }]
- },
- inlineLabel: {
- label: '是否内置标签',
- value: 'false',
- enum: [{
- label: '有',
- value: 'true'
- }, {
- label: '无',
- value: 'false'
- }]
- }
- }
- };
-
- this.onFunctionChange = this.onFunctionChange.bind(this);
- }
-
- onFunctionChange(demoFunction) {
- this.setState({
- demoFunction
- });
- }
-
- render() {
- const dataSource = createDataSource();
- // eslint-disable-next-line
- const { multiple, i18n } = this.props;
- const { demoFunction } = this.state;
- const hasBorder = demoFunction.hasBorder.value === 'true';
- const inlineLabel = demoFunction.inlineLabel.value === 'true';
- const cascaderSelectProps = {
- multiple,
- dataSource,
- hasBorder,
- style: { width: '300px' }
- };
- if (inlineLabel) {
- cascaderSelectProps.label = i18n.label;
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-}
-
-
-function render(lang = 'en-us') {
- const i18n = i18nMap[lang];
-
- ReactDOM.render((
-
-
-
-
-
-
- ), document.getElementById('container'));
-}
-
-window.renderDemo = function(lang) {
- render(lang);
-};
-
-window.renderDemo();
-
-initDemo('cascader-select');
diff --git a/components/cascader-select/__docs__/theme/index.tsx b/components/cascader-select/__docs__/theme/index.tsx
new file mode 100644
index 0000000000..9a750815b6
--- /dev/null
+++ b/components/cascader-select/__docs__/theme/index.tsx
@@ -0,0 +1,197 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import '../../../demo-helper/style';
+import '../../style';
+import { Demo, DemoGroup, DemoHead, type DemoProps, initDemo } from '../../../demo-helper';
+import CascaderSelect, { type CascaderSelectProps } from '../../index';
+import ConfigProvider from '../../../config-provider';
+import zhCN from '../../../locale/zh-cn';
+import enUS from '../../../locale/en-us';
+import { type CascaderDataItem } from '../../../cascader';
+
+const i18nMap = {
+ 'en-us': {
+ label: 'Label',
+ },
+ 'zh-cn': {
+ label: '标签:',
+ },
+} as const;
+
+const createDataSource = () => {
+ const dataSource: CascaderSelectProps['dataSource'] = [];
+
+ for (let i = 0; i < 10; i++) {
+ const level1: CascaderDataItem = {
+ label: `${i}`,
+ value: `${i}`,
+ children: [],
+ };
+ dataSource.push(level1);
+ for (let j = 0; j < 10; j++) {
+ const level2: CascaderDataItem = {
+ label: `${i}-${j}`,
+ value: `${i}-${j}`,
+ children: [],
+ };
+ level1.children!.push(level2);
+ for (let k = 0; k < 10; k++) {
+ const level3 = {
+ label: `${i}-${j}-${k}`,
+ value: `${i}-${j}-${k}`,
+ };
+ level2.children!.push(level3);
+ }
+ }
+ }
+
+ dataSource[1].disabled = true;
+
+ return dataSource;
+};
+
+class FunctionDemo extends React.Component<{
+ multiple?: CascaderSelectProps['multiple'];
+ i18n: (typeof i18nMap)[keyof typeof i18nMap];
+}> {
+ state = {
+ demoFunction: {
+ hasBorder: {
+ label: '有无边框',
+ value: 'true',
+ enum: [
+ {
+ label: '有',
+ value: 'true',
+ },
+ {
+ label: '无',
+ value: 'false',
+ },
+ ],
+ },
+ inlineLabel: {
+ label: '是否内置标签',
+ value: 'false',
+ enum: [
+ {
+ label: '有',
+ value: 'true',
+ },
+ {
+ label: '无',
+ value: 'false',
+ },
+ ],
+ },
+ },
+ };
+
+ onFunctionChange: DemoProps['onFunctionChange'] = demoFunction => {
+ this.setState({
+ demoFunction,
+ });
+ };
+
+ render() {
+ const dataSource = createDataSource();
+ const { multiple, i18n } = this.props;
+ const { demoFunction } = this.state;
+ const hasBorder = demoFunction.hasBorder.value === 'true';
+ const inlineLabel = demoFunction.inlineLabel.value === 'true';
+ const cascaderSelectProps: CascaderSelectProps = {
+ multiple,
+ dataSource,
+ hasBorder,
+ style: { width: '300px' },
+ };
+ if (inlineLabel) {
+ cascaderSelectProps.label = i18n.label;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+function render(lang = 'en-us') {
+ const i18n = i18nMap[lang as keyof typeof i18nMap];
+
+ ReactDOM.render(
+
+
+
+
+
+ ,
+ document.getElementById('container')
+ );
+}
+
+window.renderDemo = function (lang) {
+ render(lang);
+};
+
+window.renderDemo();
+
+initDemo('cascader-select');
diff --git a/components/cascader-select/__tests__/a11y-spec.js b/components/cascader-select/__tests__/a11y-spec.js
deleted file mode 100644
index 97b85fdc9e..0000000000
--- a/components/cascader-select/__tests__/a11y-spec.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import React from 'react';
-import Enzyme from 'enzyme';
-import Adapter from 'enzyme-adapter-react-16';
-import CascaderSelect from '../index';
-import '../style';
-import { unmount, testReact } from '../../util/__tests__/legacy/a11y/validate';
-
-Enzyme.configure({
- adapter: new Adapter(),
-});
-
-const ChinaArea = [
- {
- value: '2973',
- label: '陕西',
- children: [
- {
- value: '2974',
- label: '西安',
- children: [{ value: '2975', label: '西安市' }, { value: '2976', label: '高陵县' }],
- },
- {
- value: '2980',
- label: '铜川',
- children: [{ value: '2981', label: '铜川市' }, { value: '2982', label: '宜君县' }],
- },
- ],
- },
- {
- value: '3078',
- label: '四川',
- },
-];
-
-/* eslint-disable no-undef, react/jsx-filename-extension */
-describe('CascaderSelect A11y', () => {
- let wrapper;
-
- afterEach(() => {
- if (wrapper) {
- wrapper.unmount();
- wrapper = null;
- }
- unmount();
- });
-
- // TODO Select support a11y
- it.skip('should not have any violations when empty', async () => {
- wrapper = await testReact( );
- return wrapper;
- });
-});
diff --git a/components/cascader-select/__tests__/a11y-spec.tsx b/components/cascader-select/__tests__/a11y-spec.tsx
new file mode 100644
index 0000000000..47599c97a9
--- /dev/null
+++ b/components/cascader-select/__tests__/a11y-spec.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import CascaderSelect from '../index';
+import '../style';
+import { testReact } from '../../util/__tests__/a11y/validate';
+
+const ChinaArea = [
+ {
+ value: '2973',
+ label: '陕西',
+ children: [
+ {
+ value: '2974',
+ label: '西安',
+ children: [
+ { value: '2975', label: '西安市' },
+ { value: '2976', label: '高陵县' },
+ ],
+ },
+ {
+ value: '2980',
+ label: '铜川',
+ children: [
+ { value: '2981', label: '铜川市' },
+ { value: '2982', label: '宜君县' },
+ ],
+ },
+ ],
+ },
+ {
+ value: '3078',
+ label: '四川',
+ },
+];
+
+describe('CascaderSelect A11y', () => {
+ it('should not have any violations', async () => {
+ await testReact( );
+ });
+});
diff --git a/components/cascader-select/__tests__/index-spec.js b/components/cascader-select/__tests__/index-spec.js
deleted file mode 100644
index 979d80070c..0000000000
--- a/components/cascader-select/__tests__/index-spec.js
+++ /dev/null
@@ -1,613 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-import ReactTestUtils, { act } from 'react-dom/test-utils';
-import Enzyme, { mount } from 'enzyme';
-import Adapter from 'enzyme-adapter-react-16';
-import assert from 'power-assert';
-import { KEYCODE } from '../../util';
-import CascaderSelect from '../index';
-import '../style';
-
-/* eslint-disable react/jsx-filename-extension */
-/* global describe it afterEach */
-
-Enzyme.configure({ adapter: new Adapter() });
-
-function freeze(dataSource) {
- return dataSource.map(item => {
- const { children } = item;
- children && freeze(children);
- return Object.freeze({ ...item });
- });
-}
-
-const delay = time => new Promise(resolve => setTimeout(resolve, time));
-
-const ChinaArea = [
- {
- value: '2973',
- label: '陕西',
- children: [
- {
- value: '2974',
- label: '西安',
- children: [{ value: '2975', label: '西安市' }, { value: '2976', label: '高陵县' }],
- },
- {
- value: '2980',
- label: '铜川',
- children: [{ value: '2981', label: '铜川市' }, { value: '2982', label: '宜君县' }],
- },
- ],
- },
- {
- value: '3078',
- label: '四川',
- },
-];
-
-describe('CascaderSelect', () => {
- let wrapper;
-
- afterEach(() => {
- if (wrapper) {
- wrapper.unmount();
- wrapper = null;
- }
- });
-
- it('should show dropdown when set defaultVisible to true', () => {
- wrapper = mount( );
- assert(document.querySelector('.next-cascader-select-dropdown'));
- });
-
- it('should show dropdown when click select box', done => {
- wrapper = mount( );
- assert(!document.querySelector('.next-cascader-select-dropdown'));
- wrapper.find('.next-select').simulate('click');
- setTimeout(() => {
- assert(document.querySelector('.next-cascader-select-dropdown'));
- done();
- }, 500);
- });
-
- it('should render single cascader select', () => {
- let changeCalled = false;
- const handleChange = () => {
- changeCalled = true;
- };
-
- wrapper = mount(
-
- );
- assert(
- wrapper
- .find('span.next-select-inner em')
- .text()
- .trim() === '陕西 / 西安 / 西安市'
- );
-
- const item21 = findItem(2, 1);
- ReactTestUtils.Simulate.click(item21);
- assert(changeCalled);
- assert(
- wrapper
- .find('span.next-select-inner em')
- .text()
- .trim() === '陕西 / 西安 / 高陵县'
- );
- wrapper.setProps({ displayRender: label => label.join('-') });
- assert(
- wrapper
- .find('span.next-select-inner em')
- .text()
- .trim() === '陕西-西安-高陵县'
- );
- });
-
- it('should render single cascader under control', () => {
- let changeCalled = false;
- const handleChange = value => {
- changeCalled = true;
- wrapper.setProps({ value });
- };
-
- wrapper = mount(
-
- );
- assert(
- wrapper
- .find('span.next-select-inner em')
- .text()
- .trim() === '陕西 / 西安 / 西安市'
- );
-
- const item21 = findItem(2, 1);
- ReactTestUtils.Simulate.click(item21);
- assert(changeCalled);
- assert(
- wrapper
- .find('span.next-select-inner em')
- .text()
- .trim() === '陕西 / 西安 / 高陵县'
- );
- });
-
- it('should change select box display when expand item if set changeOnSelect to true', () => {
- wrapper = mount( );
-
- const item00 = findItem(0, 0);
- ReactTestUtils.Simulate.click(item00);
- wrapper.update();
- assert(
- wrapper
- .find('span.next-select-inner em')
- .text()
- .trim() === '陕西'
- );
-
- const item10 = findItem(1, 0);
- ReactTestUtils.Simulate.click(item10);
- wrapper.update();
- assert(
- wrapper
- .find('span.next-select-inner em')
- .text()
- .trim() === '陕西 / 西安'
- );
-
- const item20 = findItem(2, 0);
- ReactTestUtils.Simulate.click(item20);
- wrapper.update();
- assert(
- wrapper
- .find('span.next-select-inner em')
- .text()
- .trim() === '陕西 / 西安 / 西安市'
- );
- });
-
- it('should render multiple cascader', () => {
- const dataSource = [
- {
- value: '2973',
- label: '陕西',
- children: [
- {
- value: '2974',
- label: '西安',
- children: [
- {
- value: '2975',
- label: '西安市',
- },
- {
- value: '2976',
- label: '高陵县',
- },
- ],
- },
- {
- value: '2980',
- label: '铜川',
- },
- ],
- },
- ];
- let changeCalled = false;
- const handleChange = (v, d, e) => {
- assert.deepEqual(v, ['2980']);
- assert.deepEqual(d, [
- {
- value: '2980',
- label: '铜川',
- pos: '0-0-1',
- },
- ]);
- delete e.indeterminateData[0].children;
- assert.deepEqual(e, {
- checked: false,
- currentData: { value: '2975', label: '西安市', pos: '0-0-0-0' },
- checkedData: [{ value: '2980', label: '铜川', pos: '0-0-1' }],
- indeterminateData: [{ value: '2973', label: '陕西', pos: '0-0' }],
- });
- changeCalled = true;
- };
-
- wrapper = mount(
-
- );
- assert.deepEqual(getLabels(wrapper), ['铜川', '西安市']);
-
- const removeLink = wrapper.find('span.next-tag-close-btn').at(1);
- removeLink.simulate('click');
- assert.deepEqual(getLabels(wrapper), ['铜川']);
- assert(changeCalled);
-
- wrapper.setProps({
- displayRender: labelPath => labelPath.join(' / '),
- });
- assert.deepEqual(getLabels(wrapper), ['陕西 / 铜川']);
- });
-
- it('should render multiple cascader when set checkStrictly to true', () => {
- const dataSource = [
- {
- value: '2973',
- label: '陕西',
- children: [
- {
- value: '2974',
- label: '西安',
- children: [
- {
- value: '2975',
- label: '西安市',
- },
- {
- value: '2976',
- label: '高陵县',
- },
- ],
- },
- {
- value: '2980',
- label: '铜川',
- },
- ],
- },
- ];
- let changeCalled = false;
- const handleChange = (v, d, e) => {
- assert.deepEqual(v, ['2980']);
- assert.deepEqual(d, [
- {
- value: '2980',
- label: '铜川',
- pos: '0-0-1',
- },
- ]);
- assert.deepEqual(e, {
- checked: false,
- currentData: { value: '2975', label: '西安市', pos: '0-0-0-0' },
- checkedData: [{ value: '2980', label: '铜川', pos: '0-0-1' }],
- });
- changeCalled = true;
- };
- const wrapper = mount(
-
- );
- assert.deepEqual(getLabels(wrapper), ['西安市', '铜川']);
-
- const removeLink = wrapper.find('span.next-tag-close-btn').at(0);
- removeLink.simulate('click');
- assert.deepEqual(getLabels(wrapper), ['铜川']);
- assert(changeCalled);
- });
-
- it('should support searching when it is a single cascader select', () => {
- wrapper = mount( );
- wrapper.find('.next-select-trigger-search input').simulate('change', { target: { value: '哈哈' } });
- wrapper.update();
- assert(document.querySelector('.next-cascader-select-not-found').textContent.trim() === '无选项');
-
- wrapper.find('.next-select-trigger-search input').simulate('change', { target: { value: '高陵' } });
- wrapper.update();
- assert(document.querySelector('.next-cascader-filtered-list').textContent.trim() === '陕西 / 西安 / 高陵县');
- assert(document.querySelector('.next-cascader-filtered-list em').textContent.trim() === '高陵');
-
- ReactTestUtils.Simulate.click(document.querySelector('.next-cascader-filtered-item'));
- wrapper.update();
- assert(
- wrapper
- .find('span.next-select-inner em')
- .text()
- .trim() === '陕西 / 西安 / 高陵县'
- );
- });
-
- it('should support searching when it is a multiple cascader select', () => {
- wrapper = mount(
-
- );
- wrapper.find('.next-select-trigger-search input').simulate('change', { target: { value: '哈哈' } });
- wrapper.update();
- assert(document.querySelector('.next-cascader-select-not-found').textContent.trim() === '无选项');
-
- wrapper.find('.next-select-trigger-search input').simulate('change', { target: { value: '高陵' } });
- wrapper.update();
- assert(document.querySelector('.next-cascader-filtered-list').textContent.trim() === '陕西 / 西安 / 高陵县');
- assert(document.querySelector('.next-cascader-filtered-list em').textContent.trim() === '高陵');
- });
-
- it('should ignore case when searching', () => {
- const SpecialChars = '-[.+*?^$()[]{}|\\';
- const dataSource = [
- {
- value: 'Aa',
- label: 'Aa',
- children: [
- {
- value: 'Bb',
- label: 'Bb',
- },
- {
- value: SpecialChars,
- label: SpecialChars,
- },
- ],
- },
- ];
- wrapper = mount( );
-
- const specialCharCases = SpecialChars.split('').map(c => [c, c]);
-
- [['aa', 'Aa'], ['BB', 'Bb'], ...specialCharCases].forEach(([iptVal, excepted]) => {
- wrapper.find('.next-select-trigger-search input').simulate('change', { target: { value: iptVal } });
- wrapper.update();
- assert(document.querySelector('.next-cascader-filtered-list em').textContent.trim() === excepted);
- });
- });
-
- it('should support keyborad', done => {
- wrapper = mount( );
- wrapper.find('.next-select').simulate('click');
- setTimeout(() => {
- let cascader = document.querySelectorAll('.next-cascader');
- cascader = cascader[cascader.length - 1];
- assert(cascader);
- wrapper.find('.next-select-trigger-search input').simulate('keydown', { keyCode: KEYCODE.DOWN });
- assert(document.activeElement === findRealItem(cascader, 0, 0));
- done();
- }, 2000);
- });
-
- it('should support signle value not in dataSource', () => {
- const VALUE = '222333';
- let called = false;
- const valueRender = item => {
- assert(!item.label);
- assert(item.value === VALUE);
- called = true;
- };
- wrapper = mount( );
- assert(called);
- });
-
- it('should support multiple value not in dataSource', () => {
- const VALUE = '222333';
- let called = false;
- const valueRender = item => {
- assert(!item.label);
- assert(item.value === VALUE);
- called = true;
- };
- wrapper = mount(
- item.label || ''}
- dataSource={ChinaArea}
- valueRender={valueRender}
- />
- );
- wrapper.setProps({
- value: VALUE,
- });
- assert(called);
- wrapper.setProps({
- valueRender: item => item.label,
- onChange: value => {
- assert.deepEqual(value, [VALUE, '2973']);
- },
- });
- const item00 = findItem(0, 0);
- ReactTestUtils.Simulate.click(item00);
- });
-
- it('should support preview mode render', () => {
- const dataSource = [
- {
- value: '2973',
- label: '陕西',
- children: [
- {
- value: '2974',
- label: '西安',
- children: [
- {
- value: '2975',
- label: '西安市',
- },
- {
- value: '2976',
- label: '高陵县',
- },
- ],
- },
- {
- value: '2980',
- label: '铜川',
- },
- ],
- },
- ];
-
- wrapper = mount( );
- assert(wrapper.find('.next-form-preview').length > 0);
- assert(wrapper.find('.next-form-preview').text() === '陕西 / 西安 / 西安市');
- wrapper.setProps({
- renderPreview: items => {
- assert(items.length === 1);
- assert(items[0].label === '陕西 / 西安 / 西安市');
- return 'Hello World';
- },
- });
- assert(wrapper.find('.next-form-preview').text() === 'Hello World');
- });
-
- it('should support setting resultAutoWidth to false', done => {
- const width = '120px';
- const container = document.createElement('div');
-
- document.body.appendChild(container);
-
- act(() => {
- ReactDOM.render(
- ,
- container
- );
- });
-
- const iptElem = document.querySelector('.cs-auto-width input');
-
- ReactTestUtils.Simulate.input(iptElem);
- iptElem.value = '杭州';
- ReactTestUtils.Simulate.change(iptElem);
-
- setTimeout(() => {
- const popEl = document.querySelector('.result-auto-width-popup');
-
- assert(popEl.style.width === '');
-
- popEl.remove();
- container.remove();
- done();
- }, 50);
- });
-
- it('should support expandedValue', () => {
- wrapper = mount(
-
- );
- assert(findRealItem(document.querySelector('.myCascaderSelect'), 2, 1));
- });
-
- it('should support immutable data', () => {
- wrapper = mount(
-
- );
- assert(findRealItem(document.querySelector('.myCascaderSelect'), 2, 1));
- });
-
- it('should support onSearch', () => {
- wrapper = mount(
- assert(v === 'searchValue')}
- defaultVisible
- />
- );
-
- wrapper.find('input').simulate('change', { target: { value: 'searchValue' } });
- });
-
- it('keep value && label after dataSource updated', () => {
- const newDataSource = [
- {
- value: '3478',
- label: '浙江',
- children: [
- {
- value: '3479',
- label: '杭州',
- children: [{ value: '3480', label: '杭州市' }, { value: '3481', label: '建德市' }],
- },
- ],
- },
- ];
-
- // 多选 multiple=true
- wrapper = mount( );
-
- wrapper.setProps({
- dataSource: newDataSource,
- });
- assert.deepEqual(getLabels(wrapper), ['西安市']);
-
- wrapper
- .find('.next-checkbox-input')
- .at(0)
- .simulate('change', { target: { checked: true } });
-
- assert.deepEqual(getLabels(wrapper), ['西安市', '浙江']);
-
- wrapper
- .find('.next-tag-close-btn')
- .at(0)
- .simulate('click');
-
- assert.deepEqual(getLabels(wrapper), ['浙江']);
- wrapper.unmount();
-
- // 单选 multiple=false
- wrapper = mount( );
-
- assert(wrapper.find('.next-input-text-field em').text() === '陕西 / 西安 / 西安市');
- wrapper.setProps({
- dataSource: newDataSource,
- });
- assert(wrapper.find('.next-input-text-field em').text() === '陕西 / 西安 / 西安市');
- });
-
- it('should support popup v2', async () => {
- wrapper = mount( );
- wrapper.find('.next-select').simulate('click');
- await delay(300);
- assert(document.querySelector('.next-cascader-select-dropdown'));
- });
-});
-
-function findItem(menuIndex, itemIndex) {
- return document.querySelectorAll('.next-cascader-menu')[menuIndex].children[itemIndex];
-}
-
-function getLabels(wrapper) {
- return wrapper.find('span.next-tag-body').map(node => node.text().trim());
-}
-
-function findRealItem(cascader, listIndex, itemIndex) {
- return cascader.querySelectorAll('.next-cascader-menu')[listIndex].querySelectorAll('.next-cascader-menu-item')[
- itemIndex
- ];
-}
diff --git a/components/cascader-select/__tests__/index-spec.tsx b/components/cascader-select/__tests__/index-spec.tsx
new file mode 100644
index 0000000000..0ace606759
--- /dev/null
+++ b/components/cascader-select/__tests__/index-spec.tsx
@@ -0,0 +1,569 @@
+import React, { useState } from 'react';
+import CascaderSelect, { type CascaderSelectDataItem, type CascaderSelectProps } from '../index';
+import '../style';
+
+function freeze(dataSource: NonNullable) {
+ return dataSource.map(item => {
+ const { children } = item;
+ children && freeze(children);
+ return Object.freeze({ ...item });
+ });
+}
+
+function findItem(menuIndex: number, itemIndex: number) {
+ return cy.get('.next-cascader-menu').eq(menuIndex).children().eq(itemIndex);
+}
+
+function labelsShouldBe(expected: string[]) {
+ cy.get('span.next-tag-body').should('have.text', expected.join(''));
+}
+
+function findRealItem(
+ cascader: Cypress.Chainable>,
+ listIndex: number,
+ itemIndex: number
+) {
+ return cascader
+ .find('.next-cascader-menu')
+ .eq(listIndex)
+ .find('.next-cascader-menu-item')
+ .eq(itemIndex);
+}
+
+const ChinaArea = [
+ {
+ value: '2973',
+ label: '陕西',
+ children: [
+ {
+ value: '2974',
+ label: '西安',
+ children: [
+ { value: '2975', label: '西安市' },
+ { value: '2976', label: '高陵县' },
+ ],
+ },
+ {
+ value: '2980',
+ label: '铜川',
+ children: [
+ { value: '2981', label: '铜川市' },
+ { value: '2982', label: '宜君县' },
+ ],
+ },
+ ],
+ },
+ {
+ value: '3078',
+ label: '四川',
+ },
+];
+
+describe('CascaderSelect', () => {
+ it('should show dropdown when set defaultVisible to true', () => {
+ cy.mount( );
+ cy.get('.next-cascader-select-dropdown').should('exist');
+ });
+
+ it('should show dropdown when click select box', () => {
+ cy.mount( );
+ cy.get('.next-cascader-select-dropdown').should('not.exist');
+ cy.get('.next-select').click();
+ cy.get('.next-cascader-select-dropdown').should('exist');
+ });
+
+ it('should render single cascader select', () => {
+ const handleChange = cy.spy();
+
+ cy.mount(
+
+ ).as('Demo');
+ cy.get('.next-select-inner em').should('have.text', '陕西 / 西安 / 西安市');
+
+ findItem(2, 1).click();
+ cy.wrap(handleChange).should('be.called');
+ cy.get('.next-select-inner em').should('have.text', '陕西 / 西安 / 高陵县');
+ cy.rerender('Demo', { displayRender: (label: string[]) => label.join('-') });
+ cy.get('.next-select-inner em').should('have.text', '陕西-西安-高陵县');
+ });
+
+ it('should render single cascader under control', () => {
+ const changedSpy = cy.spy();
+ const Demo = () => {
+ const [value, setValue] = useState('2975');
+ const handleChange: CascaderSelectProps['onChange'] = (value: string) => {
+ changedSpy(value);
+ setValue(value);
+ };
+ return (
+
+ );
+ };
+
+ cy.mount( );
+ cy.get('.next-select-inner em').should('have.text', '陕西 / 西安 / 西安市');
+ findItem(2, 1).click();
+ cy.wrap(changedSpy).should('be.called');
+ cy.get('.next-select-inner em').should('have.text', '陕西 / 西安 / 高陵县');
+ });
+
+ it('should change select box display when expand item if set changeOnSelect to true', () => {
+ cy.mount( );
+ findItem(0, 0).click();
+ cy.get('.next-select-inner em').should('have.text', '陕西');
+ findItem(1, 0).click();
+ cy.get('.next-select-inner em').should('have.text', '陕西 / 西安');
+ findItem(2, 0).click();
+ cy.get('.next-select-inner em').should('have.text', '陕西 / 西安 / 西安市');
+ });
+
+ it('should render multiple cascader', () => {
+ const dataSource = [
+ {
+ value: '2973',
+ label: '陕西',
+ children: [
+ {
+ value: '2974',
+ label: '西安',
+ children: [
+ {
+ value: '2975',
+ label: '西安市',
+ },
+ {
+ value: '2976',
+ label: '高陵县',
+ },
+ ],
+ },
+ {
+ value: '2980',
+ label: '铜川',
+ },
+ ],
+ },
+ ];
+ const spyChange = cy.spy().as('handleChange');
+ const handleChange: CascaderSelectProps['onChange'] = (v, d, e) => {
+ spyChange();
+ expect(v).to.deep.equal(['2980']);
+ expect(d).to.deep.equal([
+ {
+ value: '2980',
+ label: '铜川',
+ pos: '0-0-1',
+ },
+ ]);
+ delete e!.indeterminateData![0].children;
+ expect(e).deep.equal({
+ checked: false,
+ currentData: { value: '2975', label: '西安市', pos: '0-0-0-0' },
+ checkedData: [{ value: '2980', label: '铜川', pos: '0-0-1' }],
+ indeterminateData: [{ value: '2973', label: '陕西', pos: '0-0' }],
+ });
+ };
+
+ cy.mount(
+
+ );
+ labelsShouldBe(['铜川', '西安市']);
+ cy.get('span.next-tag-close-btn').eq(1).click();
+ labelsShouldBe(['铜川']);
+ cy.get('@handleChange').should('be.called');
+ });
+
+ it('should render multiple cascader when set checkStrictly to true', () => {
+ const dataSource = [
+ {
+ value: '2973',
+ label: '陕西',
+ children: [
+ {
+ value: '2974',
+ label: '西安',
+ children: [
+ {
+ value: '2975',
+ label: '西安市',
+ },
+ {
+ value: '2976',
+ label: '高陵县',
+ },
+ ],
+ },
+ {
+ value: '2980',
+ label: '铜川',
+ },
+ ],
+ },
+ ];
+ const spyChange = cy.spy().as('handleChange');
+ const handleChange: CascaderSelectProps['onChange'] = (v, d, e) => {
+ spyChange();
+ expect(v).to.deep.equal(['2980']);
+ expect(d).to.deep.equal([
+ {
+ value: '2980',
+ label: '铜川',
+ pos: '0-0-1',
+ },
+ ]);
+ expect(e).to.deep.equal({
+ checked: false,
+ currentData: { value: '2975', label: '西安市', pos: '0-0-0-0' },
+ checkedData: [{ value: '2980', label: '铜川', pos: '0-0-1' }],
+ });
+ };
+ cy.mount(
+
+ );
+ labelsShouldBe(['西安市', '铜川']);
+ cy.get('span.next-tag-close-btn').eq(0).click();
+ labelsShouldBe(['铜川']);
+ cy.get('@handleChange').should('be.called');
+ });
+
+ it('should support searching when it is a single cascader select', () => {
+ cy.mount(
+
+ );
+ cy.get('.next-select-trigger-search input').type('哈哈');
+ cy.get('.next-cascader-select-not-found').should('have.text', '无选项');
+ cy.get('.next-select-trigger-search input').clear();
+ cy.get('.next-select-trigger-search input').type('高陵');
+ cy.get('.next-cascader-filtered-list').should('have.text', '陕西 / 西安 / 高陵县');
+ cy.get('.next-cascader-filtered-list em').should('have.text', '高陵');
+ cy.get('.next-cascader-filtered-item').click();
+ cy.get('.next-select-inner em').should('have.text', '陕西 / 西安 / 高陵县');
+ });
+
+ it('should support searching when it is a multiple cascader select', () => {
+ cy.mount(
+
+ );
+ cy.get('.next-select-trigger-search input').type('哈哈');
+ cy.get('.next-cascader-select-not-found').should('have.text', '无选项');
+ cy.get('.next-select-trigger-search input').clear();
+ cy.get('.next-select-trigger-search input').type('高陵');
+ cy.get('.next-cascader-filtered-list').should('have.text', '陕西 / 西安 / 高陵县');
+ cy.get('.next-cascader-filtered-list em').should('have.text', '高陵');
+ });
+
+ it('should ignore case when searching', () => {
+ const SpecialChars = '-[.+*?^$()[]{}|\\';
+ const dataSource = [
+ {
+ value: 'Aa',
+ label: 'Aa',
+ children: [
+ {
+ value: 'Bb',
+ label: 'Bb',
+ },
+ {
+ value: SpecialChars,
+ label: SpecialChars,
+ },
+ ],
+ },
+ ];
+ cy.mount(
+
+ );
+
+ const specialCharCases = SpecialChars.split('').map(c => [c, c]);
+
+ [['aa', 'Aa'], ['BB', 'Bb'], ...specialCharCases].forEach(([iptVal, excepted]) => {
+ cy.get('.next-select-trigger-search input').type(iptVal);
+ cy.get('.next-cascader-filtered-list em').eq(0).should('have.text', excepted);
+ cy.get('.next-select-trigger-search input').clear();
+ });
+ });
+
+ it('should support keyboard', () => {
+ cy.mount( );
+ cy.get('.next-select').click();
+ cy.get('.next-cascader').should('exist');
+ cy.get('.next-select-trigger-search input').type('{downArrow}', { force: true });
+ findRealItem(cy.get('.next-cascader'), 0, 0).then($el => {
+ expect($el.get(0)).to.equal(document.activeElement);
+ });
+ });
+
+ it('should support signle value not in dataSource', () => {
+ const VALUE = '222333';
+ const handleValueRender = cy.spy().as('handleValueRender');
+ const valueRender: CascaderSelectProps['valueRender'] = item => {
+ handleValueRender(!item.label, item.value);
+ };
+ cy.mount(
+
+ );
+ cy.get('@handleValueRender').should('be.calledWith', true, VALUE);
+ });
+
+ it('should support multiple value not in dataSource', () => {
+ const VALUE = '222333';
+ const handleValueRender = cy.spy().as('handleValueRender');
+ const valueRender: CascaderSelectProps['valueRender'] = item => {
+ handleValueRender(!item.label, item.value);
+ };
+ cy.mount(
+ item.label || ''}
+ dataSource={ChinaArea}
+ valueRender={valueRender}
+ defaultVisible
+ />
+ ).as('Demo');
+ cy.rerender('Demo', { value: VALUE });
+ cy.get('@handleValueRender').should('be.calledWith', true, VALUE);
+ const handleChange = cy.spy();
+ cy.rerender('Demo', {
+ valueRender: item => item.label,
+ onChange: handleChange,
+ });
+ findItem(0, 0).find('input').check();
+ cy.wrap(handleChange).should('be.calledWith', [VALUE, '2973']);
+ });
+
+ it('should support preview mode render', () => {
+ const dataSource = [
+ {
+ value: '2973',
+ label: '陕西',
+ children: [
+ {
+ value: '2974',
+ label: '西安',
+ children: [
+ {
+ value: '2975',
+ label: '西安市',
+ },
+ {
+ value: '2976',
+ label: '高陵县',
+ },
+ ],
+ },
+ {
+ value: '2980',
+ label: '铜川',
+ },
+ ],
+ },
+ ];
+
+ cy.mount( ).as(
+ 'Demo'
+ );
+ cy.get('.next-form-preview').should('exist');
+ cy.get('.next-form-preview').should('have.text', '陕西 / 西安 / 西安市');
+ cy.rerender('Demo', {
+ renderPreview: (items: CascaderSelectDataItem[]) => {
+ expect(items.length).to.equal(1);
+ expect(items[0].label).to.equal('陕西 / 西安 / 西安市');
+ return 'Hello World';
+ },
+ });
+ cy.get('.next-form-preview').should('have.text', 'Hello World');
+ });
+
+ it('should support setting resultAutoWidth to false', () => {
+ const width = '120px';
+ cy.mount(
+
+ );
+ cy.get('.cs-auto-width input').type('杭州');
+ cy.get('.result-auto-width-popup').then($el => {
+ expect($el.get(0).style.width).to.equal('');
+ });
+ });
+
+ it('should support expandedValue', () => {
+ cy.mount(
+
+ );
+ findRealItem(cy.get('.myCascaderSelect'), 2, 1).should('exist');
+ });
+
+ it('should support immutable data', () => {
+ cy.mount(
+
+ );
+ findRealItem(cy.get('.myCascaderSelect'), 2, 1).should('exist');
+ });
+
+ it('should support onSearch', () => {
+ const handleSearch = cy.spy();
+ cy.mount(
+
+ );
+ cy.get('input').type('searchValue');
+ cy.wrap(handleSearch).should('be.calledWith', 'searchValue');
+ });
+
+ it('keep value && label after dataSource updated', () => {
+ const newDataSource = [
+ {
+ value: '3478',
+ label: '浙江',
+ children: [
+ {
+ value: '3479',
+ label: '杭州',
+ children: [
+ { value: '3480', label: '杭州市' },
+ { value: '3481', label: '建德市' },
+ ],
+ },
+ ],
+ },
+ ];
+
+ // 多选 multiple=true
+ cy.mount(
+
+ ).as('Demo');
+
+ cy.rerender('Demo', { dataSource: newDataSource });
+
+ labelsShouldBe(['西安市']);
+
+ cy.get('.next-checkbox-input').eq(0).check();
+ labelsShouldBe(['西安市', '浙江']);
+
+ cy.get('.next-tag-close-btn').eq(0).click();
+ labelsShouldBe(['浙江']);
+
+ // // 单选 multiple=false
+ cy.mount( ).as('Demo1');
+
+ cy.get('.next-input-text-field em').should('have.text', '陕西 / 西安 / 西安市');
+
+ cy.rerender('Demo1', { dataSource: newDataSource });
+
+ cy.get('.next-input-text-field em').should('have.text', '陕西 / 西安 / 西安市');
+ });
+
+ it('should support popup v2', () => {
+ cy.mount( );
+ cy.get('.next-select').click();
+ cy.get('.next-cascader-select-dropdown').should('exist');
+ });
+
+ it('should support focus api', () => {
+ let cs: InstanceType | null = null;
+ cy.mount(
+ {
+ cs = c;
+ }}
+ dataSource={ChinaArea}
+ />
+ ).as('Demo');
+ cy.then(() => {
+ cs?.getInstance().focus();
+ expect(document.activeElement!.id).to.equal('cascader-focus');
+ });
+ });
+
+ it('should support visible by keyboard', () => {
+ cy.mount( );
+ cy.get('input').type('{upArrow}', { force: true });
+ cy.get('.next-cascader-select-dropdown').should('exist');
+ });
+
+ it('should support empty search value after selection , close #3008', () => {
+ const handleChange = cy.spy();
+ cy.mount(
+
+ );
+ cy.get('.next-select-trigger-search input').type('西安');
+ cy.get('.next-cascader-filtered-list').should('have.length', 1);
+ cy.get('.next-menu-item').first().click();
+ cy.get('.next-cascader-filtered-list').should('have.length', 0);
+ cy.get('.next-cascader > .next-cascader-inner').should('not.be.empty');
+ cy.get('.next-tag').invoke('text').should('eq', '西安');
+ cy.get('.next-select-trigger-search input').should('have.text', '');
+ });
+
+ it('should support The value of the menuProps attribute is passed by props, close #3852', () => {
+ cy.mount(
+
+ );
+ cy.get('.next-select').click();
+ cy.get('.next-menu-item').first().should('have.class', 'test-list-cls');
+ });
+});
diff --git a/components/cascader-select/__tests__/issue-spec.js b/components/cascader-select/__tests__/issue-spec.js
deleted file mode 100644
index 7f0c2ba6cc..0000000000
--- a/components/cascader-select/__tests__/issue-spec.js
+++ /dev/null
@@ -1,125 +0,0 @@
-import React, { createRef, useState } from 'react';
-import Enzyme, { mount } from 'enzyme';
-import Adapter from 'enzyme-adapter-react-16';
-import assert from 'power-assert';
-import CascaderSelect from '../index';
-import '../style';
-
-/* eslint-disable react/jsx-filename-extension */
-/* global describe it beforeEach */
-/* global describe it afterEach */
-
-Enzyme.configure({ adapter: new Adapter() });
-
-function delay(duration) {
- return new Promise(resolve => setTimeout(resolve, duration));
-}
-
-const ChinaAreaData = [
- {
- value: '2973',
- label: '陕西',
- children: [
- {
- value: '2974',
- label: '西安',
- children: [{ value: '2975', label: '西安市' }, { value: '2976', label: '高陵县' }],
- },
- {
- value: '2980',
- label: '铜川',
- children: [{ value: '2981', label: '铜川市' }, { value: '2982', label: '宜君县' }],
- },
- ],
- },
- {
- value: '3078',
- label: '四川',
- },
- {
- children: [
- {
- value: '3372',
- label: '乌鲁木齐',
- children: [
- {
- value: '3373',
- label: '乌鲁木齐市',
- },
- {
- value: '3374',
- label: '乌鲁木齐县',
- },
- ],
- },
- ],
- value: '3371',
- label: '新疆',
- },
-];
-
-describe('CascaderSelect issues', function() {
- this.timeout(1000000);
- let wrapper, root;
- beforeEach(() => {
- root = document.createElement('div');
- document.body.appendChild(root);
- });
- afterEach(() => {
- if (wrapper) {
- wrapper.unmount();
- wrapper = null;
- }
- });
-
- it('should sync expandedValue when visible=false and props.value changed ', async () => {
- const ref = createRef();
-
- function Demo() {
- const [value, setValue] = useState('2975');
- const [visible, setVisible] = useState(false);
- ref.current = { setValue, setVisible };
- return (
- setValue(v)}
- />
- );
- }
- wrapper = mount( , {
- attachTo: root,
- });
- assert(root.querySelector('span.next-select-inner em').textContent.trim() === '陕西 / 西安 / 西安市');
- ref.current.setVisible(true);
- await delay(100);
- assert(isExpanded('陕西', 0, 0, root));
- assert(isExpanded('西安', 1, 0, root));
- assert(isSelected('西安市', 2, 0, root));
- ref.current.setVisible(false);
- await delay(500);
- ref.current.setValue('3373');
- assert(root.querySelector('span.next-select-inner em').textContent.trim() === '新疆 / 乌鲁木齐 / 乌鲁木齐市');
- ref.current.setVisible(true);
- await delay(100);
- assert(isExpanded('新疆', 0, 2, root));
- assert(isExpanded('乌鲁木齐', 1, 0, root));
- assert(isSelected('乌鲁木齐市', 2, 0, root));
- });
-});
-
-function findItem(menuIndex, itemIndex, root = document) {
- return root.querySelectorAll('.next-cascader-menu')[menuIndex].children[itemIndex];
-}
-
-function isExpanded(text, menuIndex, itemIndex, root = document) {
- const item = findItem(menuIndex, itemIndex, root);
- return !!item && item.textContent.trim() === text && item.classList.contains('next-expanded');
-}
-
-function isSelected(text, menuIndex, itemIndex, root = document) {
- const item = findItem(menuIndex, itemIndex, root);
- return !!item && item.textContent.trim() === text && item.classList.contains('next-selected');
-}
diff --git a/components/cascader-select/__tests__/issue-spec.tsx b/components/cascader-select/__tests__/issue-spec.tsx
new file mode 100644
index 0000000000..65b1428489
--- /dev/null
+++ b/components/cascader-select/__tests__/issue-spec.tsx
@@ -0,0 +1,122 @@
+import React, { forwardRef, useEffect, useState } from 'react';
+import CascaderSelect from '../index';
+import '../style';
+
+const ChinaAreaData = [
+ {
+ value: '2973',
+ label: '陕西',
+ children: [
+ {
+ value: '2974',
+ label: '西安',
+ children: [
+ { value: '2975', label: '西安市' },
+ { value: '2976', label: '高陵县' },
+ ],
+ },
+ {
+ value: '2980',
+ label: '铜川',
+ children: [
+ { value: '2981', label: '铜川市' },
+ { value: '2982', label: '宜君县' },
+ ],
+ },
+ ],
+ },
+ {
+ value: '3078',
+ label: '四川',
+ },
+ {
+ children: [
+ {
+ value: '3372',
+ label: '乌鲁木齐',
+ children: [
+ {
+ value: '3373',
+ label: '乌鲁木齐市',
+ },
+ {
+ value: '3374',
+ label: '乌鲁木齐县',
+ },
+ ],
+ },
+ ],
+ value: '3371',
+ label: '新疆',
+ },
+];
+
+function findItem(menuIndex: number, itemIndex: number) {
+ return cy.get('.next-cascader-menu').eq(menuIndex).children().eq(itemIndex);
+}
+
+function shouldExpanded(text: string, menuIndex: number, itemIndex: number) {
+ const item = findItem(menuIndex, itemIndex);
+ item.should('have.text', text);
+ item.should('have.class', 'next-expanded');
+}
+
+function shouldSelected(text: string, menuIndex: number, itemIndex: number) {
+ const item = findItem(menuIndex, itemIndex);
+ item.should('exist');
+ item.should('have.text', text);
+ item.should('have.class', 'next-selected');
+}
+
+describe('CascaderSelect issues', function () {
+ it('should sync expandedValue when visible=false and props.value changed ', () => {
+ const Demo = forwardRef(props => {
+ const { value: propsValue = '2975', visible = false } = props;
+ const [value, setValue] = useState(propsValue);
+ useEffect(() => {
+ setValue(propsValue);
+ }, [propsValue]);
+ return (
+ setValue(v)}
+ />
+ );
+ });
+ cy.mount( ).as('Demo');
+ cy.get('span.next-select-inner em').should('have.text', '陕西 / 西安 / 西安市');
+ cy.rerender('Demo', { visible: true });
+ shouldExpanded('陕西', 0, 0);
+ shouldExpanded('西安', 1, 0);
+ shouldSelected('西安市', 2, 0);
+ cy.rerender('Demo', { value: '3373', visible: false }).as('Demo1');
+ cy.get('span.next-select-inner em').should('have.text', '新疆 / 乌鲁木齐 / 乌鲁木齐市');
+ cy.get('.next-cascader-menu').should('not.exist');
+ cy.rerender('Demo', { value: '3373', visible: true });
+ shouldExpanded('新疆', 0, 2);
+ shouldExpanded('乌鲁木齐', 1, 0);
+ shouldSelected('乌鲁木齐市', 2, 0);
+ });
+
+ // Fix https://github.com/alibaba-fusion/next/issues/3704
+ it('When using virtual scrolling, the background color should be white', () => {
+ cy.mount(
+
+
+
+ );
+ cy.get('.next-cascader-menu-wrapper').should(
+ 'have.css',
+ 'background-color',
+ 'rgb(255, 255, 255)'
+ );
+ });
+});
diff --git a/components/cascader-select/cascader-select.jsx b/components/cascader-select/cascader-select.jsx
deleted file mode 100644
index 1e97f238e5..0000000000
--- a/components/cascader-select/cascader-select.jsx
+++ /dev/null
@@ -1,1000 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import { polyfill } from 'react-lifecycles-compat';
-import classNames from 'classnames';
-import Select from '../select';
-import Cascader from '../cascader';
-import Menu from '../menu';
-import { func, obj, dom, KEYCODE } from '../util';
-import zhCN from '../locale/zh-cn';
-
-const { bindCtx } = func;
-const { pickOthers } = obj;
-const { getStyle } = dom;
-
-const normalizeValue = value => {
- if (value) {
- if (Array.isArray(value)) {
- return value;
- }
-
- return [value];
- }
-
- return [];
-};
-
-/**
- * CascaderSelect
- */
-class CascaderSelect extends Component {
- static propTypes = {
- prefix: PropTypes.string,
- pure: PropTypes.bool,
- className: PropTypes.string,
- /**
- * 选择框大小
- */
- size: PropTypes.oneOf(['small', 'medium', 'large']),
- /**
- * 选择框占位符
- */
- placeholder: PropTypes.string,
- /**
- * 是否禁用
- */
- disabled: PropTypes.bool,
- /**
- * 是否有下拉箭头
- */
- hasArrow: PropTypes.bool,
- /**
- * 是否有边框
- */
- hasBorder: PropTypes.bool,
- /**
- * 是否有清除按钮
- */
- hasClear: PropTypes.bool,
- /**
- * 自定义内联 label
- */
- label: PropTypes.node,
- /**
- * 是否只读,只读模式下可以展开弹层但不能选
- */
- readOnly: PropTypes.bool,
- /**
- * 数据源,结构可参考下方说明
- */
- dataSource: PropTypes.arrayOf(PropTypes.object),
- /**
- * (非受控)默认值
- */
- defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
- /**
- * (受控)当前值
- */
- value: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
- /**
- * 选中值改变时触发的回调函数
- * @param {String|Array} value 选中的值,单选时返回单个值,多选时返回数组
- * @param {Object|Array} data 选中的数据,包括 value 和 label,单选时返回单个值,多选时返回数组,父子节点选中关联时,同时选中,只返回父节点
- * @param {Object} extra 额外参数
- * @param {Array} extra.selectedPath 单选时选中的数据的路径
- * @param {Boolean} extra.checked 多选时当前的操作是选中还是取消选中
- * @param {Object} extra.currentData 多选时当前操作的数据
- * @param {Array} extra.checkedData 多选时所有被选中的数据
- * @param {Array} extra.indeterminateData 多选时半选的数据
- */
- onChange: PropTypes.func,
- /**
- * 默认展开值,如果不设置,组件内部会根据 defaultValue/value 进行自动设置
- */
- defaultExpandedValue: PropTypes.arrayOf(PropTypes.string),
- /**
- * (受控)当前展开值
- */
- expandedValue: PropTypes.arrayOf(PropTypes.string),
- /**
- * 展开触发的方式
- */
- expandTriggerType: PropTypes.oneOf(['click', 'hover']),
- onExpand: PropTypes.func,
- /**
- * 是否开启虚拟滚动
- */
- useVirtual: PropTypes.bool,
- /**
- * 是否多选
- */
- multiple: PropTypes.bool,
- /**
- * 是否选中即发生改变, 该属性仅在单选模式下有效
- */
- changeOnSelect: PropTypes.bool,
- /**
- * 是否只能勾选叶子项的checkbox,该属性仅在多选模式下有效
- */
- canOnlyCheckLeaf: PropTypes.bool,
- /**
- * 父子节点是否选中不关联
- */
- checkStrictly: PropTypes.bool,
- /**
- * 每列列表样式对象
- */
- listStyle: PropTypes.object,
- /**
- * 每列列表类名
- */
- listClassName: PropTypes.string,
- /**
- * 选择框单选时展示结果的自定义渲染函数
- * @param {Array} label 选中路径的文本数组
- * @return {ReactNode} 渲染在选择框中的内容
- * @default 单选时:labelPath => labelPath.join(' / ');多选时:labelPath => labelPath[labelPath.length - 1]
- */
- displayRender: PropTypes.func,
- /**
- * 渲染 item 内容的方法
- * @param {Object} item 渲染节点的item
- * @return {ReactNode} item node
- */
- itemRender: PropTypes.func,
- /**
- * 是否显示搜索框
- */
- showSearch: PropTypes.bool,
- /**
- * 自定义搜索函数
- * @param {String} searchValue 搜索的关键字
- * @param {Array} path 节点路径
- * @return {Boolean} 是否匹配
- * @default 根据路径所有节点的文本值模糊匹配
- */
- filter: PropTypes.func,
- /**
- * 当搜索框值变化时回调
- * @param {String} value 数据
- * @version 1.23
- */
- onSearch: PropTypes.func,
- /**
- * 搜索结果自定义渲染函数
- * @param {String} searchValue 搜索的关键字
- * @param {Array} path 匹配到的节点路径
- * @return {ReactNode} 渲染的内容
- * @default 按照节点文本 a / b / c 的模式渲染
- */
- resultRender: PropTypes.func,
- /**
- * 搜索结果列表是否和选择框等宽
- */
- resultAutoWidth: PropTypes.bool,
- /**
- * 无数据时显示内容
- */
- notFoundContent: PropTypes.node,
- /**
- * 国际化
- */
- locale: PropTypes.object,
- /**
- * 异步加载数据函数
- * @param {Object} data 当前点击异步加载的数据
- */
- loadData: PropTypes.func,
- /**
- * 自定义下拉框头部
- */
- header: PropTypes.node,
- /**
- * 自定义下拉框底部
- */
- footer: PropTypes.node,
- /**
- * 初始下拉框是否显示
- */
- defaultVisible: PropTypes.bool,
- /**
- * 当前下拉框是否显示
- */
- visible: PropTypes.bool,
- /**
- * 下拉框显示或关闭时触发事件的回调函数
- * @param {Boolean} visible 是否显示
- * @param {String} type 触发显示关闭的操作类型, fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发
- */
- onVisibleChange: PropTypes.func,
- /**
- * 下拉框自定义样式对象
- */
- popupStyle: PropTypes.object,
- /**
- * 下拉框样式自定义类名
- */
- popupClassName: PropTypes.string,
- /**
- * 下拉框挂载的容器节点
- */
- popupContainer: PropTypes.any,
- /**
- * 透传到 Popup 的属性对象
- */
- popupProps: PropTypes.object,
- /**
- * 是否跟随滚动
- */
- followTrigger: PropTypes.bool,
- /**
- * 是否为预览态
- */
- isPreview: PropTypes.bool,
- /**
- * 预览态模式下渲染的内容
- * @param {Array} value 选择值 { label: , value:}
- */
- renderPreview: PropTypes.func,
- /**
- * 是否是不可变数据
- * @version 1.23
- */
- immutable: PropTypes.bool,
- };
-
- static defaultProps = {
- prefix: 'next-',
- pure: false,
- size: 'medium',
- disabled: false,
- hasArrow: true,
- hasBorder: true,
- hasClear: false,
- dataSource: [],
- defaultValue: null,
- expandTriggerType: 'click',
- onExpand: () => {},
- useVirtual: false,
- multiple: false,
- changeOnSelect: false,
- canOnlyCheckLeaf: false,
- checkStrictly: false,
- showSearch: false,
- filter: (searchValue, path) => {
- return path.some(
- item =>
- String(item.label)
- .toLowerCase()
- .indexOf(String(searchValue).toLowerCase()) > -1
- );
- },
- resultRender: (searchValue, path) => {
- const parts = [];
- path.forEach((item, i) => {
- const reExp = searchValue.replace(/[-.+*?^$()[\]{}|\\]/g, v => `\\${v}`);
-
- const re = new RegExp(reExp, 'gi');
- const others = item.label.split(re);
- const matches = item.label.match(re);
-
- others.forEach((other, j) => {
- if (other) {
- parts.push(other);
- }
- if (j < others.length - 1) {
- parts.push({matches[j]} );
- }
- });
- if (i < path.length - 1) {
- parts.push(' / ');
- }
- });
- return {parts} ;
- },
- resultAutoWidth: true,
- defaultVisible: false,
- onVisibleChange: () => {},
- popupProps: {},
- immutable: false,
- locale: zhCN.Select,
- };
-
- constructor(props) {
- super(props);
-
- this.state = {
- value: normalizeValue('value' in props ? props.value : props.defaultValue),
- searchValue: '',
- visible: typeof props.visible === 'undefined' ? props.defaultVisible : props.visible,
- };
-
- // 缓存选中值数据
- this._valueDataCache = {};
-
- bindCtx(this, [
- 'handleVisibleChange',
- 'handleAfterOpen',
- 'handleSelect',
- 'handleChange',
- 'handleClear',
- 'handleRemove',
- 'handleSearch',
- 'getPopup',
- 'saveSelectRef',
- 'saveCascaderRef',
- 'handleKeyDown',
- ]);
- }
-
- static getDerivedStateFromProps(props) {
- const st = {};
-
- if ('value' in props) {
- st.value = normalizeValue(props.value);
- }
- if ('visible' in props) {
- st.visible = props.visible;
- }
-
- return st;
- }
-
- updateCache(dataSource) {
- this._v2n = {};
- this._p2n = {};
- const loop = (data, prefix = '0') =>
- data.forEach((item, index) => {
- const { value, children } = item;
- const pos = `${prefix}-${index}`;
- this._v2n[value] = this._p2n[pos] = { ...item, pos };
-
- if (children && children.length) {
- loop(children, pos);
- }
- });
-
- loop(dataSource);
- }
-
- flatValue(value) {
- const getDepth = v => {
- const pos = this.getPos(v);
- if (!pos) {
- return 0;
- }
- return pos.split('-').length;
- };
- const newValue = value.slice(0).sort((prev, next) => {
- return getDepth(prev) - getDepth(next);
- });
-
- for (let i = 0; i < newValue.length; i++) {
- for (let j = 0; j < newValue.length; j++) {
- if (i !== j && this.isDescendantOrSelf(this.getPos(newValue[i]), this.getPos(newValue[j]))) {
- newValue.splice(j, 1);
- j--;
- }
- }
- }
-
- return newValue;
- }
-
- isDescendantOrSelf(currentPos, targetPos) {
- if (!currentPos || !targetPos) {
- return false;
- }
-
- const currentNums = currentPos.split('-');
- const targetNums = targetPos.split('-');
-
- return (
- currentNums.length <= targetNums.length &&
- currentNums.every((num, index) => {
- return num === targetNums[index];
- })
- );
- }
-
- getValue(pos) {
- return this._p2n[pos] ? this._p2n[pos].value : null;
- }
-
- getPos(value) {
- return this._v2n[value] ? this._v2n[value].pos : null;
- }
-
- getData(value) {
- return value.map(v => this._v2n[v] || this._valueDataCache[v]);
- }
-
- getLabelPath(data) {
- const nums = data.pos.split('-');
- return nums.slice(1).reduce((ret, num, index) => {
- const p = nums.slice(0, index + 2).join('-');
- ret.push(this._p2n[p].label);
- return ret;
- }, []);
- }
-
- getSingleData(value) {
- if (!value.length) {
- return null;
- }
-
- if (Array.isArray(value)) value = value[0];
-
- let data = this._v2n[value];
-
- if (data) {
- const labelPath = this.getLabelPath(data);
- const displayRender = this.props.displayRender || (labels => labels.join(' / '));
-
- data = {
- ...data,
- label: displayRender(labelPath, data),
- };
-
- this._valueDataCache[value] = data;
- this.refreshValueDataCache(value);
- } else {
- data = this._valueDataCache[value];
- }
-
- return (
- data || {
- value,
- }
- );
- }
-
- getMultipleData(value) {
- if (!value.length) {
- return null;
- }
-
- const { checkStrictly, canOnlyCheckLeaf, displayRender } = this.props;
- const flatValue = checkStrictly || canOnlyCheckLeaf ? value : this.flatValue(value);
- let data = flatValue.map(v => {
- let item = this._v2n[v];
-
- if (item) {
- this._valueDataCache[v] = item;
- } else {
- item = this._valueDataCache[v];
- }
-
- return item || { value: v };
- });
-
- if (displayRender) {
- data = data.map(item => {
- if (!item.pos || !(item.value in this._v2n)) {
- return item;
- }
-
- const labelPath = this.getLabelPath(item);
- const newItem = {
- ...item,
- label: displayRender(labelPath, item),
- };
-
- this._valueDataCache[item.value] = newItem;
-
- return newItem;
- });
- }
-
- return data;
- }
-
- getIndeterminate(value) {
- const indeterminate = [];
-
- const positions = value.map(this.getPos.bind(this));
- positions.forEach(pos => {
- if (!pos) {
- return false;
- }
- const nums = pos.split('-');
- for (let i = nums.length; i > 2; i--) {
- const parentPos = nums.slice(0, i - 1).join('-');
- const parentValue = this.getValue(parentPos);
- if (indeterminate.indexOf(parentValue) === -1) {
- indeterminate.push(parentValue);
- }
- }
- });
-
- return indeterminate;
- }
-
- saveSelectRef(ref) {
- this.select = ref;
- }
-
- saveCascaderRef(ref) {
- this.cascader = ref;
- }
-
- completeValue(value) {
- const newValue = [];
-
- const flatValue = this.flatValue(value).reverse();
- const ps = Object.keys(this._p2n);
- for (let i = 0; i < ps.length; i++) {
- for (let j = 0; j < flatValue.length; j++) {
- const v = flatValue[j];
- if (this.isDescendantOrSelf(this.getPos(v), ps[i])) {
- newValue.push(this.getValue(ps[i]));
- ps.splice(i, 1);
- i--;
- break;
- }
- }
- }
-
- return newValue;
- }
-
- isLeaf(data) {
- return !((data.children && data.children.length) || (!!this.props.loadData && !data.isLeaf));
- }
-
- handleVisibleChange(visible, type) {
- const { searchValue } = this.state;
- if (!('visible' in this.props)) {
- this.setState({
- visible,
- });
- }
-
- if (!visible && searchValue) {
- this.setState({
- searchValue: '',
- });
- }
-
- if (['fromCascader', 'keyboard'].indexOf(type) !== -1 && !visible) {
- // 这里需要延迟下,showSearch 的情况下通过手动设置 menuProps={{focusable: true}} 回车 focus 会有延迟
- setTimeout(() => this.select.focusInput(), 0);
- }
-
- this.props.onVisibleChange(visible, type);
- }
-
- handleKeyDown(e) {
- const { onKeyDown } = this.props;
- const { visible } = this.state;
-
- if (onKeyDown) {
- onKeyDown(e);
- }
-
- if (!visible) {
- return;
- }
-
- switch (e.keyCode) {
- case KEYCODE.UP:
- case KEYCODE.DOWN:
- this.cascader.setFocusValue();
- e.preventDefault();
- break;
- default:
- break;
- }
- }
-
- getPopup(ref) {
- this.popup = ref;
- if (typeof this.props.popupProps.ref === 'function') {
- this.props.popupProps.ref(ref);
- }
- }
-
- handleAfterOpen() {
- if (!this.popup) {
- return;
- }
-
- const { prefix, popupProps } = this.props;
- const { v2 = false } = popupProps;
- if (!v2) {
- const dropDownNode = this.popup
- .getInstance()
- .overlay.getInstance()
- .getContentNode();
- const cascaderNode = dropDownNode.querySelector(`.${prefix}cascader`);
- if (cascaderNode) {
- this.cascaderHeight = getStyle(cascaderNode, 'height');
- }
- }
-
- if (typeof popupProps.afterOpen === 'function') {
- popupProps.afterOpen();
- }
- }
-
- handleSelect(value, data) {
- const { multiple, changeOnSelect } = this.props;
- const { visible, searchValue } = this.state;
-
- if (!multiple && (!changeOnSelect || this.isLeaf(data) || !!searchValue)) {
- this.handleVisibleChange(!visible, 'fromCascader');
- }
- }
-
- /**
- * 刷新值数据缓存,删除无效值
- * @param {Arrary | String} curValue 当前值
- */
- refreshValueDataCache = curValue => {
- if (curValue) {
- const valueArr = Array.isArray(curValue) ? curValue : [curValue];
-
- valueArr.length &&
- Object.keys(this._valueDataCache).forEach(v => {
- if (!valueArr.includes(v)) {
- delete this._valueDataCache[v];
- }
- });
- } else {
- this._valueDataCache = {};
- }
- };
-
- handleChange(value, data, extra) {
- const { multiple, onChange } = this.props;
- const { searchValue, value: stateValue } = this.state;
-
- const st = {};
-
- if (multiple && stateValue && Array.isArray(stateValue)) {
- const noExistedValues = stateValue.filter(v => !this._v2n[v]);
-
- if (noExistedValues.length > 0) {
- value = value.filter(v => {
- return !(noExistedValues.indexOf(v) >= 0);
- });
- }
-
- value = [...noExistedValues, ...value];
- // onChange 中的 data 参数也应该保留不存在的 value 的数据
- // 在 dataSource 异步加载的情况下,会出现value重复的现象,需要去重
- data = [...noExistedValues.map(v => this._valueDataCache[v]).filter(v => v), ...data].filter(
- (current, index, arr) => {
- return index === arr.indexOf(current);
- }
- );
- // 更新缓存
- this.refreshValueDataCache(value);
- }
-
- if (!('value' in this.props)) {
- st.value = value;
- }
- if (!multiple && searchValue) {
- st.searchValue = '';
- }
- if (Object.keys(st).length) {
- this.setState(st);
- }
-
- if (onChange) {
- onChange(value, data, extra);
- }
-
- if (searchValue && this.select) {
- this.select.handleSearchClear();
- }
- }
-
- handleClear() {
- // 单选时点击清空按钮
- const { hasClear, multiple, treeCheckable } = this.props;
- if (hasClear && (!multiple || !treeCheckable)) {
- if (!('value' in this.props)) {
- this.setState({
- value: [],
- });
- }
-
- this.props.onChange(null, null);
- }
- }
-
- handleRemove(currentData) {
- const { value: currentValue } = currentData;
- let value;
-
- const { multiple, checkStrictly, onChange } = this.props;
- if (multiple) {
- value = [...this.state.value];
- value.splice(value.indexOf(currentValue), 1);
-
- if (this.props.onChange) {
- const data = this.getData(value);
- const checked = false;
-
- if (checkStrictly) {
- this.props.onChange(value, data, {
- checked,
- currentData,
- checkedData: data,
- });
- } else {
- const checkedValue = this.completeValue(value);
- const checkedData = this.getData(checkedValue);
- const indeterminateValue = this.getIndeterminate(value);
- const indeterminateData = this.getData(indeterminateValue);
- this.props.onChange(value, data, {
- checked,
- currentData,
- checkedData,
- indeterminateData,
- });
- }
- }
- } else {
- value = [];
- onChange(null, null);
- }
-
- if (!('value' in this.props)) {
- this.setState({
- value,
- });
- }
-
- this.refreshValueDataCache(value);
- }
-
- handleSearch(searchValue) {
- this.setState({
- searchValue,
- });
-
- this.props.onSearch && this.props.onSearch(searchValue);
- }
-
- getPath(pos) {
- const items = [];
-
- const nums = pos.split('-');
- if (nums === 2) {
- items.push(this._p2n[pos]);
- } else {
- for (let i = 1; i < nums.length; i++) {
- const p = nums.slice(0, i + 1).join('-');
- items.push(this._p2n[p]);
- }
- }
-
- return items;
- }
-
- filterItems() {
- const { multiple, changeOnSelect, canOnlyCheckLeaf, filter } = this.props;
- const { searchValue } = this.state;
- let items = Object.keys(this._p2n).map(p => this._p2n[p]);
- if ((!multiple && !changeOnSelect) || (multiple && canOnlyCheckLeaf)) {
- items = items.filter(item => !item.children || !item.children.length);
- }
-
- return items.map(item => this.getPath(item.pos)).filter(path => filter(searchValue, path));
- }
-
- renderNotFound() {
- const { prefix, notFoundContent, locale } = this.props;
- return (
-
- {notFoundContent || locale.notFoundContent}
-
- );
- }
-
- renderCascader() {
- const { dataSource } = this.props;
- if (dataSource.length === 0) {
- return this.renderNotFound();
- }
-
- const { searchValue } = this.state;
- let filteredPaths = [];
-
- if (searchValue) {
- filteredPaths = this.filterItems();
- if (filteredPaths.length === 0) {
- return this.renderNotFound();
- }
- }
-
- const {
- multiple,
- useVirtual,
- changeOnSelect,
- checkStrictly,
- canOnlyCheckLeaf,
- defaultExpandedValue,
- expandTriggerType,
- onExpand,
- listStyle,
- listClassName,
- loadData,
- showSearch,
- resultRender,
- readOnly,
- itemRender,
- immutable,
- menuProps = {},
- } = this.props;
- const { value } = this.state;
-
- const props = {
- dataSource,
- value,
- multiple,
- useVirtual,
- canOnlySelectLeaf: !changeOnSelect,
- checkStrictly,
- canOnlyCheckLeaf,
- defaultExpandedValue,
- expandTriggerType,
- ref: this.saveCascaderRef,
- onExpand,
- listStyle,
- listClassName,
- loadData,
- itemRender,
- immutable,
- };
-
- if ('expandedValue' in this.props) {
- props.expandedValue = this.props.expandedValue;
- }
-
- if (!readOnly) {
- props.onChange = this.handleChange;
- props.onSelect = this.handleSelect;
- }
- if (showSearch) {
- props.searchValue = searchValue;
- props.filteredPaths = filteredPaths;
- props.resultRender = resultRender;
- props.filteredListStyle = { height: this.cascaderHeight };
- }
-
- return ;
- }
-
- renderPopupContent() {
- const { prefix, header, footer } = this.props;
- return (
-
- {header}
- {this.renderCascader()}
- {footer}
-
- );
- }
-
- renderPreview(others) {
- const { prefix, multiple, className, renderPreview } = this.props;
- const { value } = this.state;
- const previewCls = classNames(className, `${prefix}form-preview`);
- let items = (multiple ? this.getMultipleData(value) : this.getSingleData(value)) || [];
-
- if (!Array.isArray(items)) {
- items = [items];
- }
-
- if (typeof renderPreview === 'function') {
- return (
-
- {renderPreview(items, this.props)}
-
- );
- }
-
- return (
-
- {items.map(({ label }) => label).join(', ')}
-
- );
- }
-
- render() {
- const {
- prefix,
- size,
- hasArrow,
- hasBorder,
- hasClear,
- label,
- readOnly,
- placeholder,
- dataSource,
- disabled,
- multiple,
- className,
- showSearch,
- popupStyle,
- popupClassName,
- popupContainer,
- popupProps,
- followTrigger,
- isPreview,
- resultAutoWidth,
- } = this.props;
- const { value, searchValue, visible } = this.state;
- const others = pickOthers(Object.keys(CascaderSelect.propTypes), this.props);
- // mode应与multiple api保持一致
- if (multiple && 'mode' in others && others.mode !== 'multiple') {
- delete others.mode;
- }
-
- this.updateCache(dataSource);
-
- if (isPreview) {
- return this.renderPreview(others);
- }
-
- const popupContent = this.renderPopupContent();
-
- const props = {
- prefix,
- className,
- size,
- placeholder,
- disabled,
- hasArrow,
- hasBorder,
- hasClear,
- label,
- readOnly,
- ref: this.saveSelectRef,
- autoWidth: false,
- mode: multiple ? 'multiple' : 'single',
- value: multiple ? this.getMultipleData(value) : this.getSingleData(value),
- onChange: this.handleClear,
- onRemove: this.handleRemove,
- visible,
- onVisibleChange: this.handleVisibleChange,
- showSearch,
- onSearch: this.handleSearch,
- onKeyDown: this.handleKeyDown,
- popupContent,
- popupStyle,
- popupClassName,
- popupContainer,
- popupProps,
- followTrigger,
- };
-
- if (!multiple) {
- // 单选模式 select 会强制cache=true,会导致菜单展开状态的初始化不执行
- // 若用户没有手动设置cache true,这里重置为false
- if (!popupProps || !popupProps.cache) {
- props.popupProps = {
- ...popupProps,
- cache: false,
- };
- }
- }
-
- if (showSearch) {
- props.popupProps = {
- ...popupProps,
- ref: this.getPopup,
- afterOpen: this.handleAfterOpen,
- };
- props.autoWidth = resultAutoWidth && !!searchValue;
- }
-
- return ;
- }
-}
-
-export default polyfill(CascaderSelect);
diff --git a/components/cascader-select/cascader-select.tsx b/components/cascader-select/cascader-select.tsx
new file mode 100644
index 0000000000..6ca571014b
--- /dev/null
+++ b/components/cascader-select/cascader-select.tsx
@@ -0,0 +1,904 @@
+import React, {
+ Component,
+ type ReactNode,
+ type KeyboardEvent,
+ type DetailedHTMLProps,
+ type HTMLAttributes,
+ type ComponentPropsWithRef,
+} from 'react';
+import PropTypes from 'prop-types';
+import { polyfill } from 'react-lifecycles-compat';
+import classNames from 'classnames';
+import Select from '../select';
+import Cascader, { type CascaderDataItem, type Extra } from '../cascader';
+import Menu from '../menu';
+import { func, obj, dom, KEYCODE, type ClassPropsWithDefault } from '../util';
+import zhCN from '../locale/zh-cn';
+import type {
+ CascaderSelectDataItem,
+ CascaderSelectProps,
+ CascaderSelectState,
+ CascaderSelectVisibleChangeType,
+} from './types';
+import Overlay from '../overlay';
+
+const { Popup } = Overlay;
+const { bindCtx } = func;
+const { pickOthers } = obj;
+const { getStyle } = dom;
+
+type normalizeValueResult = T extends NonNullable
+ ? T extends unknown[]
+ ? NonNullable
+ : [NonNullable]
+ : [];
+
+const normalizeValue = (value: T): normalizeValueResult => {
+ if (value) {
+ if (Array.isArray(value)) {
+ return value as normalizeValueResult;
+ }
+
+ return [value] as normalizeValueResult;
+ }
+
+ return [] as normalizeValueResult;
+};
+
+export type CascaderSelectPropsWithDefault = ClassPropsWithDefault<
+ CascaderSelectProps,
+ typeof CascaderSelect.defaultProps
+>;
+
+/**
+ * CascaderSelect
+ */
+class CascaderSelect extends Component {
+ static displayName = 'CascaderSelect';
+ static propTypes = {
+ prefix: PropTypes.string,
+ pure: PropTypes.bool,
+ className: PropTypes.string,
+ size: PropTypes.oneOf(['small', 'medium', 'large']),
+ placeholder: PropTypes.string,
+ disabled: PropTypes.bool,
+ hasArrow: PropTypes.bool,
+ hasBorder: PropTypes.bool,
+ hasClear: PropTypes.bool,
+ label: PropTypes.node,
+ readOnly: PropTypes.bool,
+ dataSource: PropTypes.arrayOf(PropTypes.object),
+ defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
+ onChange: PropTypes.func,
+ defaultExpandedValue: PropTypes.arrayOf(PropTypes.string),
+ expandedValue: PropTypes.arrayOf(PropTypes.string),
+ expandTriggerType: PropTypes.oneOf(['click', 'hover']),
+ onExpand: PropTypes.func,
+ useVirtual: PropTypes.bool,
+ multiple: PropTypes.bool,
+ changeOnSelect: PropTypes.bool,
+ canOnlyCheckLeaf: PropTypes.bool,
+ checkStrictly: PropTypes.bool,
+ listStyle: PropTypes.object,
+ listClassName: PropTypes.string,
+ displayRender: PropTypes.func,
+ itemRender: PropTypes.func,
+ showSearch: PropTypes.bool,
+ filter: PropTypes.func,
+ onSearch: PropTypes.func,
+ resultRender: PropTypes.func,
+ resultAutoWidth: PropTypes.bool,
+ notFoundContent: PropTypes.node,
+ locale: PropTypes.object,
+ loadData: PropTypes.func,
+ header: PropTypes.node,
+ footer: PropTypes.node,
+ defaultVisible: PropTypes.bool,
+ visible: PropTypes.bool,
+ onVisibleChange: PropTypes.func,
+ popupStyle: PropTypes.object,
+ popupClassName: PropTypes.string,
+ popupContainer: PropTypes.any,
+ popupProps: PropTypes.object,
+ followTrigger: PropTypes.bool,
+ isPreview: PropTypes.bool,
+ renderPreview: PropTypes.func,
+ immutable: PropTypes.bool,
+ /**
+ * 查询选中后清除查询条件
+ */
+ autoClearSearchValue: PropTypes.bool,
+ };
+
+ static defaultProps = {
+ prefix: 'next-',
+ pure: false,
+ size: 'medium',
+ disabled: false,
+ hasArrow: true,
+ hasBorder: true,
+ hasClear: false,
+ dataSource: [],
+ defaultValue: null,
+ expandTriggerType: 'click',
+ onExpand: () => {},
+ useVirtual: false,
+ multiple: false,
+ changeOnSelect: false,
+ canOnlyCheckLeaf: false,
+ checkStrictly: false,
+ showSearch: false,
+ filter: (searchValue: string, path: Array<{ label: string; value: string }>) => {
+ return path.some(
+ item =>
+ String(item.label).toLowerCase().indexOf(String(searchValue).toLowerCase()) > -1
+ );
+ },
+ resultRender: (searchValue: string, path: Array<{ label: string; value: string }>) => {
+ const parts: ReactNode[] = [];
+ path.forEach((item, i) => {
+ const reExp = searchValue.replace(/[-.+*?^$()[\]{}|\\]/g, v => `\\${v}`);
+
+ const re = new RegExp(reExp, 'gi');
+ const others = item.label.split(re);
+ const matches = item.label.match(re);
+
+ others.forEach((other, j) => {
+ if (other) {
+ parts.push(other);
+ }
+ if (j < others.length - 1) {
+ parts.push({matches![j]} );
+ }
+ });
+ if (i < path.length - 1) {
+ parts.push(' / ');
+ }
+ });
+ return {parts} ;
+ },
+ resultAutoWidth: true,
+ defaultVisible: false,
+ onVisibleChange: () => {},
+ popupProps: {},
+ immutable: false,
+ locale: zhCN.Select,
+ autoClearSearchValue: false,
+ };
+
+ readonly props: CascaderSelectPropsWithDefault;
+ _valueDataCache: Record;
+ _v2n: Record;
+ _p2n: Record;
+ select: InstanceType;
+ cascader: InstanceType;
+ popup: InstanceType;
+ cascaderHeight: string | number;
+
+ constructor(props: CascaderSelectProps) {
+ super(props);
+
+ this.state = {
+ value: normalizeValue('value' in props ? props.value : props.defaultValue),
+ searchValue: '',
+ visible: typeof props.visible === 'undefined' ? props.defaultVisible! : props.visible,
+ };
+
+ // 缓存选中值数据
+ this._valueDataCache = {};
+
+ bindCtx(this, [
+ 'handleVisibleChange',
+ 'handleAfterOpen',
+ 'handleSelect',
+ 'handleChange',
+ 'handleClear',
+ 'handleRemove',
+ 'handleSearch',
+ 'getPopup',
+ 'saveSelectRef',
+ 'saveCascaderRef',
+ 'handleKeyDown',
+ ]);
+ }
+
+ static getDerivedStateFromProps(props: CascaderSelectPropsWithDefault) {
+ const st: Partial = {};
+
+ if ('value' in props) {
+ st.value = normalizeValue(props.value);
+ }
+ if ('visible' in props) {
+ st.visible = props.visible;
+ }
+
+ return st;
+ }
+
+ /**
+ * 使组件获得焦点
+ * @public
+ */
+ focus() {
+ this.select && this.select.focusInput();
+ }
+
+ updateCache(dataSource: CascaderDataItem[]) {
+ this._v2n = {};
+ this._p2n = {};
+ const loop = (data: CascaderDataItem[], prefix = '0') =>
+ data.forEach((item, index) => {
+ const { value, children } = item;
+ const pos = `${prefix}-${index}`;
+ this._v2n[value] = this._p2n[pos] = { ...item, pos };
+
+ if (children && children.length) {
+ loop(children, pos);
+ }
+ });
+
+ loop(dataSource);
+ }
+
+ flatValue(value: string[]) {
+ const getDepth = (v: string) => {
+ const pos = this.getPos(v);
+ if (!pos) {
+ return 0;
+ }
+ return pos.split('-').length;
+ };
+ const newValue = value.slice(0).sort((prev, next) => {
+ return getDepth(prev) - getDepth(next);
+ });
+
+ for (let i = 0; i < newValue.length; i++) {
+ for (let j = 0; j < newValue.length; j++) {
+ if (
+ i !== j &&
+ this.isDescendantOrSelf(this.getPos(newValue[i]), this.getPos(newValue[j]))
+ ) {
+ newValue.splice(j, 1);
+ j--;
+ }
+ }
+ }
+
+ return newValue;
+ }
+
+ isDescendantOrSelf(
+ currentPos: string | undefined | null,
+ targetPos: string | undefined | null
+ ) {
+ if (!currentPos || !targetPos) {
+ return false;
+ }
+
+ const currentNums = currentPos.split('-');
+ const targetNums = targetPos.split('-');
+
+ return (
+ currentNums.length <= targetNums.length &&
+ currentNums.every((num, index) => {
+ return num === targetNums[index];
+ })
+ );
+ }
+
+ getValue(pos: string) {
+ return this._p2n[pos] ? this._p2n[pos].value : null;
+ }
+
+ getPos(value: string) {
+ return this._v2n[value] ? this._v2n[value].pos : null;
+ }
+
+ getData(value: string[]) {
+ return value.map(v => this._v2n[v] || this._valueDataCache[v]);
+ }
+
+ getLabelPath(data: CascaderSelectDataItem) {
+ const nums = data.pos.split('-');
+ return nums.slice(1).reduce(
+ (ret, num, index) => {
+ const p = nums.slice(0, index + 2).join('-');
+ ret.push(this._p2n[p].label);
+ return ret;
+ },
+ [] as CascaderSelectDataItem['label'][]
+ );
+ }
+
+ getSingleData(value: string | string[]) {
+ if (!value.length) {
+ return null;
+ }
+
+ if (Array.isArray(value)) value = value[0];
+
+ let data = this._v2n[value];
+
+ if (data) {
+ const labelPath = this.getLabelPath(data);
+ const displayRender = this.props.displayRender || (labels => labels.join(' / '));
+
+ data = {
+ ...data,
+ label: displayRender(labelPath, data),
+ };
+
+ this._valueDataCache[value] = data;
+ this.refreshValueDataCache(value);
+ } else {
+ data = this._valueDataCache[value];
+ }
+
+ return (
+ data || {
+ value,
+ }
+ );
+ }
+
+ getMultipleData(value: string[]) {
+ if (!value.length) {
+ return null;
+ }
+
+ const { checkStrictly, canOnlyCheckLeaf, displayRender } = this.props;
+ const flatValue = checkStrictly || canOnlyCheckLeaf ? value : this.flatValue(value);
+ let data = flatValue.map(v => {
+ let item = this._v2n[v];
+
+ if (item) {
+ this._valueDataCache[v] = item;
+ } else {
+ item = this._valueDataCache[v];
+ }
+
+ return item || { value: v };
+ });
+
+ if (displayRender) {
+ data = data.map(item => {
+ if (!item.pos || !(item.value in this._v2n)) {
+ return item;
+ }
+
+ const labelPath = this.getLabelPath(item);
+ const newItem = {
+ ...item,
+ label: displayRender(labelPath, item),
+ };
+
+ this._valueDataCache[item.value] = newItem;
+
+ return newItem;
+ });
+ }
+
+ return data;
+ }
+
+ getIndeterminate(value: string[]) {
+ const indeterminate: Array = [];
+
+ const positions: string[] = value.map(this.getPos.bind(this));
+ positions.forEach(pos => {
+ if (!pos) {
+ return false;
+ }
+ const nums = pos.split('-');
+ for (let i = nums.length; i > 2; i--) {
+ const parentPos = nums.slice(0, i - 1).join('-');
+ const parentValue = this.getValue(parentPos) as string;
+ if (indeterminate.indexOf(parentValue) === -1) {
+ indeterminate.push(parentValue);
+ }
+ }
+ });
+
+ return indeterminate;
+ }
+
+ saveSelectRef(ref: InstanceType) {
+ this.select = ref;
+ }
+
+ saveCascaderRef(ref: InstanceType) {
+ this.cascader = ref;
+ }
+
+ completeValue(value: string[]) {
+ const newValue = [];
+
+ const flatValue = this.flatValue(value).reverse();
+ const ps = Object.keys(this._p2n);
+ for (let i = 0; i < ps.length; i++) {
+ for (let j = 0; j < flatValue.length; j++) {
+ const v = flatValue[j];
+ if (this.isDescendantOrSelf(this.getPos(v), ps[i])) {
+ newValue.push(this.getValue(ps[i]) as string);
+ ps.splice(i, 1);
+ i--;
+ break;
+ }
+ }
+ }
+
+ return newValue;
+ }
+
+ isLeaf(data: CascaderSelectDataItem) {
+ return !(
+ (data.children && data.children.length) ||
+ (!!this.props.loadData && !data.isLeaf)
+ );
+ }
+
+ handleVisibleChange(visible: boolean, type?: CascaderSelectVisibleChangeType) {
+ const { searchValue } = this.state;
+ if (!('visible' in this.props)) {
+ this.setState({
+ visible,
+ });
+ }
+
+ if (!visible && searchValue) {
+ this.setState({
+ searchValue: '',
+ });
+ }
+
+ if (['fromCascader', 'keyboard'].indexOf(type!) !== -1 && !visible) {
+ // 这里需要延迟下,showSearch 的情况下通过手动设置 menuProps={{focusable: true}} 回车 focus 会有延迟
+ setTimeout(() => this.select.focusInput(), 0);
+ }
+
+ this.props.onVisibleChange(visible, type);
+ }
+
+ handleKeyDown(e: KeyboardEvent) {
+ const { onKeyDown } = this.props;
+ const { visible } = this.state;
+
+ if (onKeyDown) {
+ onKeyDown(e);
+ }
+
+ if (!visible) {
+ switch (e.keyCode) {
+ case KEYCODE.UP:
+ case KEYCODE.DOWN: {
+ e.preventDefault();
+ this.handleVisibleChange(true, 'keyboard');
+ break;
+ }
+ // no default
+ }
+ return;
+ }
+
+ switch (e.keyCode) {
+ case KEYCODE.UP:
+ case KEYCODE.DOWN:
+ this.cascader.setFocusValue();
+ e.preventDefault();
+ break;
+ default:
+ break;
+ }
+ }
+
+ getPopup(ref: InstanceType) {
+ this.popup = ref;
+ if (typeof this.props.popupProps.ref === 'function') {
+ this.props.popupProps.ref(ref);
+ }
+ }
+
+ handleAfterOpen() {
+ if (!this.popup) {
+ return;
+ }
+
+ const { prefix, popupProps } = this.props;
+ const { v2 = false } = popupProps;
+ if (!v2) {
+ const dropDownNode = this.popup.getInstance().overlay!.getInstance().getContentNode();
+ const cascaderNode = dropDownNode!.querySelector(`.${prefix}cascader`) as HTMLElement;
+ if (cascaderNode) {
+ this.cascaderHeight = getStyle(cascaderNode, 'height');
+ }
+ }
+
+ if (typeof popupProps.afterOpen === 'function') {
+ popupProps.afterOpen();
+ }
+ }
+
+ handleSelect(value: unknown, data: CascaderSelectDataItem) {
+ const { multiple, changeOnSelect } = this.props;
+ const { visible, searchValue } = this.state;
+
+ if (!multiple && (!changeOnSelect || this.isLeaf(data) || !!searchValue)) {
+ this.handleVisibleChange(!visible, 'fromCascader');
+ }
+ }
+
+ /**
+ * 刷新值数据缓存,删除无效值
+ * @param curValue - 当前值
+ */
+ refreshValueDataCache = (curValue: string | string[]) => {
+ if (curValue) {
+ const valueArr = Array.isArray(curValue) ? curValue : [curValue];
+
+ valueArr.length &&
+ Object.keys(this._valueDataCache).forEach(v => {
+ if (!valueArr.includes(v)) {
+ delete this._valueDataCache[v];
+ }
+ });
+ } else {
+ this._valueDataCache = {};
+ }
+ };
+
+ handleChange(value: string[], data: CascaderSelectDataItem[], extra: Extra) {
+ const { multiple, onChange, autoClearSearchValue } = this.props;
+ const { searchValue, value: stateValue } = this.state;
+
+ const st = {} as CascaderSelectState;
+
+ if (multiple && stateValue && Array.isArray(stateValue)) {
+ const noExistedValues = stateValue.filter(v => !this._v2n[v]);
+
+ if (noExistedValues.length > 0) {
+ value = value.filter(v => {
+ return !(noExistedValues.indexOf(v) >= 0);
+ });
+ }
+
+ value = [...noExistedValues, ...value];
+ // onChange 中的 data 参数也应该保留不存在的 value 的数据
+ // 在 dataSource 异步加载的情况下,会出现 value 重复的现象,需要去重
+ data = [
+ ...noExistedValues.map(v => this._valueDataCache[v]).filter(v => v),
+ ...data,
+ ].filter((current, index, arr) => {
+ return index === arr.indexOf(current);
+ });
+ // 更新缓存
+ this.refreshValueDataCache(value);
+ }
+
+ if (!('value' in this.props)) {
+ st.value = value;
+ }
+ if (searchValue && ((multiple && autoClearSearchValue) || !multiple)) {
+ st.searchValue = '';
+ }
+ if (Object.keys(st).length) {
+ this.setState(st);
+ }
+
+ if (onChange) {
+ onChange(value, data, extra);
+ }
+
+ if (searchValue && this.select) {
+ this.select.handleSearchClear();
+ }
+ }
+
+ handleClear() {
+ // 单选时点击清空按钮
+ const { hasClear, multiple, treeCheckable } = this.props;
+ if (hasClear && (!multiple || !treeCheckable)) {
+ if (!('value' in this.props)) {
+ this.setState({
+ value: [],
+ });
+ }
+
+ this.props.onChange!(null, null);
+ }
+ }
+
+ handleRemove(currentData: CascaderSelectDataItem) {
+ const { value: currentValue } = currentData;
+ let value: string[];
+
+ const { multiple, checkStrictly, onChange } = this.props;
+ if (multiple) {
+ value = [...this.state.value];
+ value.splice(value.indexOf(currentValue), 1);
+
+ if (this.props.onChange) {
+ const data = this.getData(value);
+ const checked = false;
+
+ if (checkStrictly) {
+ this.props.onChange(value, data, {
+ checked,
+ currentData,
+ checkedData: data,
+ });
+ } else {
+ const checkedValue = this.completeValue(value);
+ const checkedData = this.getData(checkedValue);
+ const indeterminateValue = this.getIndeterminate(value);
+ const indeterminateData = this.getData(indeterminateValue);
+ this.props.onChange(value, data, {
+ checked,
+ currentData,
+ checkedData,
+ indeterminateData,
+ });
+ }
+ }
+ } else {
+ value = [];
+ onChange!(null, null);
+ }
+
+ if (!('value' in this.props)) {
+ this.setState({
+ value,
+ });
+ }
+
+ this.refreshValueDataCache(value);
+ }
+
+ handleSearch(searchValue: string) {
+ this.setState({
+ searchValue,
+ });
+
+ this.props.onSearch && this.props.onSearch(searchValue);
+ }
+
+ getPath(pos: string) {
+ const items = [];
+
+ const nums = pos.split('-');
+ // @ts-expect-error nums 应该是一个数组,这里可能是想表达 nums 的长度为 2?
+ if (nums === 2) {
+ items.push(this._p2n[pos]);
+ } else {
+ for (let i = 1; i < nums.length; i++) {
+ const p = nums.slice(0, i + 1).join('-');
+ items.push(this._p2n[p]);
+ }
+ }
+
+ return items;
+ }
+
+ filterItems() {
+ const { multiple, changeOnSelect, canOnlyCheckLeaf, filter } = this.props;
+ const { searchValue } = this.state;
+ let items = Object.keys(this._p2n).map(p => this._p2n[p]);
+ if ((!multiple && !changeOnSelect) || (multiple && canOnlyCheckLeaf)) {
+ items = items.filter(item => !item.children || !item.children.length);
+ }
+
+ return items.map(item => this.getPath(item.pos)).filter(path => filter(searchValue, path));
+ }
+
+ renderNotFound() {
+ const { prefix, notFoundContent, locale } = this.props;
+ return (
+
+ {notFoundContent || locale.notFoundContent}
+
+ );
+ }
+
+ renderCascader() {
+ const { dataSource } = this.props;
+ if (dataSource.length === 0) {
+ return this.renderNotFound();
+ }
+
+ const { searchValue } = this.state;
+ let filteredPaths: CascaderSelectDataItem[][] = [];
+
+ if (searchValue) {
+ filteredPaths = this.filterItems();
+ if (filteredPaths.length === 0) {
+ return this.renderNotFound();
+ }
+ }
+
+ const {
+ multiple,
+ useVirtual,
+ changeOnSelect,
+ checkStrictly,
+ canOnlyCheckLeaf,
+ defaultExpandedValue,
+ expandTriggerType,
+ onExpand,
+ listStyle,
+ listClassName,
+ loadData,
+ showSearch,
+ resultRender,
+ readOnly,
+ itemRender,
+ immutable,
+ menuProps = {},
+ } = this.props;
+ const { value } = this.state;
+
+ const props: ComponentPropsWithRef = {
+ dataSource,
+ value,
+ multiple,
+ useVirtual,
+ canOnlySelectLeaf: !changeOnSelect,
+ checkStrictly,
+ canOnlyCheckLeaf,
+ defaultExpandedValue,
+ expandTriggerType,
+ ref: this.saveCascaderRef,
+ onExpand,
+ listStyle,
+ listClassName,
+ loadData,
+ itemRender,
+ immutable,
+ };
+
+ if ('expandedValue' in this.props) {
+ props.expandedValue = this.props.expandedValue;
+ }
+
+ if (!readOnly) {
+ props.onChange = this.handleChange;
+ props.onSelect = this.handleSelect;
+ }
+ if (showSearch) {
+ props.searchValue = searchValue;
+ props.filteredPaths = filteredPaths;
+ props.resultRender = resultRender;
+ props.filteredListStyle = { height: this.cascaderHeight };
+ }
+
+ return ;
+ }
+
+ renderPopupContent() {
+ const { prefix, header, footer } = this.props;
+ return (
+
+ {header}
+ {this.renderCascader()}
+ {footer}
+
+ );
+ }
+
+ renderPreview(others: DetailedHTMLProps, HTMLDivElement>) {
+ const { prefix, multiple, className, renderPreview } = this.props;
+ const { value } = this.state;
+ const previewCls = classNames(className, `${prefix}form-preview`);
+ let items = (multiple ? this.getMultipleData(value) : this.getSingleData(value)) || [];
+
+ if (!Array.isArray(items)) {
+ items = [items];
+ }
+
+ if (typeof renderPreview === 'function') {
+ return (
+
+ {renderPreview(items, this.props)}
+
+ );
+ }
+
+ return (
+
+ {items.map(({ label }) => label).join(', ')}
+
+ );
+ }
+
+ render() {
+ const {
+ prefix,
+ size,
+ hasArrow,
+ hasBorder,
+ hasClear,
+ label,
+ readOnly,
+ placeholder,
+ dataSource,
+ disabled,
+ multiple,
+ className,
+ showSearch,
+ popupStyle,
+ popupClassName,
+ popupContainer,
+ popupProps,
+ followTrigger,
+ isPreview,
+ resultAutoWidth,
+ } = this.props;
+ const { value, searchValue, visible } = this.state;
+ const others = pickOthers(CascaderSelect.propTypes, this.props);
+ // mode 应与 multiple api 保持一致
+ if (multiple && 'mode' in others && others.mode !== 'multiple') {
+ delete others.mode;
+ }
+
+ this.updateCache(dataSource);
+
+ if (isPreview) {
+ return this.renderPreview(others);
+ }
+
+ const popupContent = this.renderPopupContent();
+
+ const props: ComponentPropsWithRef = {
+ prefix,
+ className,
+ size,
+ placeholder,
+ disabled,
+ hasArrow,
+ hasBorder,
+ hasClear,
+ label,
+ readOnly,
+ ref: this.saveSelectRef,
+ autoWidth: false,
+ mode: multiple ? 'multiple' : 'single',
+ value: multiple ? this.getMultipleData(value) : this.getSingleData(value),
+ onChange: this.handleClear,
+ onRemove: this.handleRemove,
+ visible,
+ onVisibleChange: this.handleVisibleChange,
+ showSearch,
+ onSearch: this.handleSearch,
+ onKeyDown: this.handleKeyDown,
+ popupContent,
+ popupStyle,
+ popupClassName,
+ popupContainer,
+ popupProps,
+ followTrigger,
+ };
+
+ if (!multiple) {
+ // 单选模式 select 会强制 cache=true,会导致菜单展开状态的初始化不执行
+ // 若用户没有手动设置 cache true,这里重置为 false
+ if (!popupProps || !popupProps.cache) {
+ props.popupProps = {
+ ...popupProps,
+ cache: false,
+ };
+ }
+ }
+
+ if (showSearch) {
+ props.popupProps = {
+ ...popupProps,
+ ref: this.getPopup,
+ afterOpen: this.handleAfterOpen,
+ };
+ props.autoWidth = resultAutoWidth && !!searchValue;
+ }
+
+ return ;
+ }
+}
+
+export default polyfill(CascaderSelect);
diff --git a/components/cascader-select/index.d.ts b/components/cascader-select/index.d.ts
deleted file mode 100644
index a183b17edb..0000000000
--- a/components/cascader-select/index.d.ts
+++ /dev/null
@@ -1,230 +0,0 @@
-///
-
-import React from 'react';
-import { CascaderProps, data, extra } from '../cascader';
-import { CommonProps } from '../util';
-import { PopupProps } from '../overlay';
-
-interface HTMLAttributesWeak extends React.HTMLAttributes {
- defaultValue?: any;
- onChange?: any;
-}
-
-export interface CascaderSelectProps extends CascaderProps, HTMLAttributesWeak, CommonProps {
- /**
- * 选择框大小
- */
- size?: 'small' | 'medium' | 'large';
- name?: string;
-
- /**
- * 选择框占位符
- */
- placeholder?: string;
-
- /**
- * 是否禁用
- */
- disabled?: boolean;
-
- /**
- * 是否有下拉箭头
- */
- hasArrow?: boolean;
-
- /**
- * 是否有边框
- */
- hasBorder?: boolean;
-
- /**
- * 是否有清除按钮
- */
- hasClear?: boolean;
-
- /**
- * 自定义内联 label
- */
- label?: React.ReactNode;
-
- /**
- * 是否只读,只读模式下可以展开弹层但不能选
- */
- readOnly?: boolean;
-
- /**
- * 数据源,结构可参考下方说明
- */
- dataSource?: Array;
-
- /**
- * (非受控)默认值
- */
- defaultValue?: string | Array