From ff8fb7736b45093b388b0df510810c102bf1d0b9 Mon Sep 17 00:00:00 2001 From: FairyYang <269291280@qq.com> Date: Mon, 21 Oct 2024 11:09:54 +0800 Subject: [PATCH] refactor(TimePicker2): convert to TypeScript, impove docs and tests, close #4616 --- .../__docs__/demo/disabled/index.tsx | 8 +- .../__docs__/demo/field/index.tsx | 6 +- .../__docs__/demo/preset/index.tsx | 5 +- .../__docs__/demo/render-menu/index.tsx | 5 +- .../time-picker2/__docs__/demo/step/index.tsx | 3 +- .../__docs__/demo/value/index.tsx | 22 +- .../time-picker2/__docs__/index.en-us.md | 133 ++-- components/time-picker2/__docs__/index.md | 145 ++-- .../time-picker2/__tests__/index-spec.js | 422 ------------ .../time-picker2/__tests__/index-spec.tsx | 378 ++++++++++ .../time-picker2/{constant.js => constant.ts} | 0 components/time-picker2/index.d.ts | 206 ------ components/time-picker2/index.jsx | 10 - components/time-picker2/index.tsx | 21 + .../mobile/{index.jsx => index.tsx} | 0 .../module/{date-input.jsx => date-input.tsx} | 72 +- .../module/{time-menu.jsx => time-menu.tsx} | 43 +- .../time-picker2/{panel.jsx => panel.tsx} | 147 ++-- .../{prop-types.js => prop-types.ts} | 22 +- .../time-picker2/{style.js => style.ts} | 0 .../{time-picker.jsx => time-picker.tsx} | 328 ++++----- components/time-picker2/types.ts | 650 ++++++++++++++++++ .../time-picker2/utils/{index.js => index.ts} | 51 +- components/util/func.ts | 4 +- 24 files changed, 1565 insertions(+), 1116 deletions(-) delete mode 100644 components/time-picker2/__tests__/index-spec.js create mode 100644 components/time-picker2/__tests__/index-spec.tsx rename components/time-picker2/{constant.js => constant.ts} (100%) delete mode 100644 components/time-picker2/index.d.ts delete mode 100644 components/time-picker2/index.jsx create mode 100644 components/time-picker2/index.tsx rename components/time-picker2/mobile/{index.jsx => index.tsx} (100%) rename components/time-picker2/module/{date-input.jsx => date-input.tsx} (70%) rename components/time-picker2/module/{time-menu.jsx => time-menu.tsx} (72%) rename components/time-picker2/{panel.jsx => panel.tsx} (63%) rename components/time-picker2/{prop-types.js => prop-types.ts} (65%) rename components/time-picker2/{style.js => style.ts} (100%) rename components/time-picker2/{time-picker.jsx => time-picker.tsx} (68%) create mode 100644 components/time-picker2/types.ts rename components/time-picker2/utils/{index.js => index.ts} (53%) diff --git a/components/time-picker2/__docs__/demo/disabled/index.tsx b/components/time-picker2/__docs__/demo/disabled/index.tsx index dffc109e33..277bc6cc80 100644 --- a/components/time-picker2/__docs__/demo/disabled/index.tsx +++ b/components/time-picker2/__docs__/demo/disabled/index.tsx @@ -6,7 +6,7 @@ const disabledHours = [1, 2, 3, 4, 5]; const disabledMinutes = [10, 20, 30, 40, 50]; const disabledSeconds = [10, 20, 30, 40, 50]; -const disabledItems = list => index => { +const disabledItems = (list: number[]) => (index: number) => { return list.indexOf(index) >= 0; }; @@ -20,6 +20,12 @@ ReactDOM.render( disabledMinutes={disabledItems(disabledMinutes)} disabledSeconds={disabledItems(disabledSeconds)} /> +

RangePicker Disable Hours/Minutes/Seconds

+ disabledHours} + disabledMinutes={() => disabledMinutes} + disabledSeconds={() => disabledSeconds} + /> , mountNode ); diff --git a/components/time-picker2/__docs__/demo/field/index.tsx b/components/time-picker2/__docs__/demo/field/index.tsx index 918ba9bd49..0b16bc9758 100644 --- a/components/time-picker2/__docs__/demo/field/index.tsx +++ b/components/time-picker2/__docs__/demo/field/index.tsx @@ -1,14 +1,14 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { TimePicker2, Field, Button } from '@alifd/next'; -import dayjs from 'dayjs'; +import dayjs, { type Dayjs } from 'dayjs'; class Demo extends React.Component { field = new Field(this); onClick = () => { - const value = this.field.getValue('time-picker'); - console.log(value.format('HH:mm:ss')); + const value: Dayjs | undefined = this.field.getValue('time-picker'); + console.log(value!.format('HH:mm:ss')); }; render() { diff --git a/components/time-picker2/__docs__/demo/preset/index.tsx b/components/time-picker2/__docs__/demo/preset/index.tsx index 2172ec87f3..37d0fcb72b 100644 --- a/components/time-picker2/__docs__/demo/preset/index.tsx +++ b/components/time-picker2/__docs__/demo/preset/index.tsx @@ -3,12 +3,13 @@ import ReactDOM from 'react-dom'; import React, { useState } from 'react'; import dayjs from 'dayjs'; import { TimePicker2 } from '@alifd/next'; +import type { TimePickerProps, ValueType } from '@alifd/next/types/time-picker2'; const nowTime = dayjs(new Date()); const currentHour = dayjs().hour(nowTime.hour()).minute(0).second(0); const nextHour = currentHour.hour(currentHour.hour() + 1); -const preset = [ +const preset: TimePickerProps['preset'] = [ { label: '此刻', value: () => nowTime, @@ -23,7 +24,7 @@ const presetRange = [ ]; function Picker() { - const [value, onChange] = useState(dayjs('12:00:00', 'HH:mm:ss', true)); + const [value, onChange] = useState(dayjs('12:00:00', 'HH:mm:ss', true)); return (
diff --git a/components/time-picker2/__docs__/demo/render-menu/index.tsx b/components/time-picker2/__docs__/demo/render-menu/index.tsx index 835e6a0e01..01275cb349 100644 --- a/components/time-picker2/__docs__/demo/render-menu/index.tsx +++ b/components/time-picker2/__docs__/demo/render-menu/index.tsx @@ -1,9 +1,10 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { TimePicker2 } from '@alifd/next'; +import type { TimePickerProps } from '@alifd/next/types/time-picker2'; -const renderTimeMenuItems = list => { - return list.map(({ label, value }) => { +const renderTimeMenuItems: TimePickerProps['renderTimeMenuItems'] = list => { + return list.map(({ value }) => { return { value, label: value > 9 ? String(value) : `0${value}`, diff --git a/components/time-picker2/__docs__/demo/step/index.tsx b/components/time-picker2/__docs__/demo/step/index.tsx index 6007824432..fb89ebebe3 100644 --- a/components/time-picker2/__docs__/demo/step/index.tsx +++ b/components/time-picker2/__docs__/demo/step/index.tsx @@ -1,10 +1,11 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { TimePicker2 } from '@alifd/next'; +import type { Dayjs } from 'dayjs'; ReactDOM.render( console.log(val.format('HH:mm:ss'))} + onChange={(val: Dayjs) => console.log(val.format('HH:mm:ss'))} hourStep={2} minuteStep={5} secondStep={5} diff --git a/components/time-picker2/__docs__/demo/value/index.tsx b/components/time-picker2/__docs__/demo/value/index.tsx index e36a4223a9..c0f22ae4a1 100644 --- a/components/time-picker2/__docs__/demo/value/index.tsx +++ b/components/time-picker2/__docs__/demo/value/index.tsx @@ -1,23 +1,33 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { TimePicker2 } from '@alifd/next'; -import dayjs from 'dayjs'; +import dayjs, { type Dayjs } from 'dayjs'; +import type { TimePickerProps, ValueType } from '@alifd/next/types/time-picker2'; -class ControlledTimePicker2 extends React.Component { - constructor(props, context) { - super(props, context); +interface ControlledTimePicker2Props { + onChange: (value: ValueType) => void; +} +class ControlledTimePicker2 extends React.Component< + ControlledTimePicker2Props, + { + value: Dayjs | null; + rangeValue: (Dayjs | null)[] | null; + } +> { + constructor(props: ControlledTimePicker2Props) { + super(props); this.state = { value: dayjs('12:00:00', 'HH:mm:ss', true), rangeValue: [dayjs('14:00:00', 'HH:mm:ss'), dayjs('16:00:00', 'HH:mm:ss')], }; } - onSelect = value => { + onSelect: TimePickerProps['onChange'] = (value: Dayjs | null) => { this.setState({ value }); this.props.onChange(value); }; - onRangeSelect = rangeValue => { + onRangeSelect: TimePickerProps['onChange'] = (rangeValue: (Dayjs | null)[] | null) => { this.setState({ rangeValue }); this.props.onChange(rangeValue); }; diff --git a/components/time-picker2/__docs__/index.en-us.md b/components/time-picker2/__docs__/index.en-us.md index 02d73c3e73..71f20f3151 100644 --- a/components/time-picker2/__docs__/index.en-us.md +++ b/components/time-picker2/__docs__/index.en-us.md @@ -13,11 +13,11 @@ A TimePicker is used to input a time by displaying an interface the user can interact with. The TimePicker panel only support 24h clock. Setting `format` with: -| Format | Example | Description | -| ------ | ------- | -------- | -| `H HH` | `0..23` | Hour,24h | -| `m mm` | `0..59` | Minute | -| `s ss` | `0..59` | Second | +| Format | Example | Description | +| ------ | ------- | ----------- | +| `H HH` | `0..23` | Hour,24h | +| `m mm` | `0..59` | Minute | +| `s ss` | `0..59` | Second | By default, TimePicker using dayjs instance as input value, which is the suggestion way. In addition, input value as string is also supported, e.g. "12:00:00". The type of the first parameter in the callback of `onChange` is based on the input value. @@ -25,47 +25,88 @@ By default, TimePicker using dayjs instance as input value, which is the suggest ### TimePicker -| Param | Descripiton | Type | Default Value | -| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ---------- | -| label | Inset label of input | ReactNode | - | -| size | Size of input

**option**:
'small', 'medium', 'large' | Enum | 'medium' | -| state | State of input

**option**:
'error', 'success' | Enum | - | -| placeholder | Placeholder of input | String | - | -| value | Time value | custom | - | -| defaultValue | Defualt time value | custom | - | -| hasClear | Has clear icon | Boolean | true | -| format | time format
| String | 'HH:mm:ss' | -| hourStep | Step of hour | Number | - | -| minuteStep | Step of minute | Number | - | -| secondStep | Step of second | Number | - | -| disabledHours | Function to disable hours

**signature**:
Function(index: Number) => Boolean
**paramter**:
_index_: {Number} hour 0 - 23
**return**:
{Boolean} if disabled
| Function | - | -| disabledMinutes | Function to disable minutes

**signature**:
Function(index: Number) => Boolean
**paramter**:
_index_: {Number} minute 0 - 59
**return**:
{Boolean} if disabled
| Function | - | -| disabledSeconds | Function to disable seconds

**signature**:
Function(index: Number) => Boolean
**paramter**:
_index_: {Number} second 0 - 59
**return**:
{Boolean} if disabled
| Function | - | -| renderTimeMenuItems | Render time menu
[{
label: '01',
value: 1
}]

**签名**:
Function(list: Array, mode: String, value: dayjs) => Array
**参数**:
_list_: {Array} default time menu list
_mode_: {String} menu type: hour, minute, second
_value_: {dayjs} value
**返回值**:
{Array}
-| visible | Visible state of popup | Boolean | - | -| defaultVisible | Default visible state of popup | Boolean | - | -| popupContainer | Container of popup

**signature**:
Function(target: Object) => ReactNode
**paramter**:
_target_: {Object} target container
**return**:
{ReactNode} container element
| Function | - | -| popupAlign | Align of popup, @see Overylay doc for detail | String | 'tl tl' | -| popupTriggerType | Trigger type of popup

**option**:
'click', 'hover' | Enum | 'click' | -| onVisibleChange | Callback when visible changes

**signature**:
Function(visible: Boolean, reason: String) => void
**paramter**:
_visible_: {Boolean} visible of popup
_reason_: {String} reason to change visible | Function | func.noop | -| popupStyle | Custom style of popup | Object | - | -| popupClassName | Custom className of popup | String | - | -| popupProps | Props of popup | Object | - | -| followTrigger | follow Trigger or not | Boolean | - | -| disabled | Disable the picker | Boolean | false | -| hasBorder | Whether the input has border | Boolean | true | -| preset | Rreset values, shown below the time panel.
Can be object or array of object, with the following properties.
**properties**:
label: {String} shown text
name: {String} key of React element, can be empty, and index will become key instead
value: {String/dayjs instance} date value | Object/Array | - | -| onChange | Callback when date changes

**signature**:
Function(value: Object/String) => void
**paramter**:
_value_: {Object/String} date value | Function | func.noop | +| Param | Description | Type | Default Value | Required | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------- | +| label | Button text | ReactNode | - | | +| size | Size of time picker | 'small' \| 'medium' \| 'large' | 'medium' | | +| state | Input state | 'error' \| 'success' | - | | +| hasClear | Whether to allow clearing time | boolean | true | | +| format | Time format | string | 'HH:mm:ss' | | +| hourStep | Hour option step | number | - | | +| minuteStep | Minute option step | number | - | | +| secondStep | Second option step | number | - | | +| renderTimeMenuItems | Render the selectable time list

**signature**:
**params**:
_list_: list
_mode_: mode
_value_: value | (
list: Array\,
mode: TimeMenuProps['mode'],
value: TimeMenuProps['value']
) => Array\ | - | | +| visible | Popup layer display status (controlled) | boolean | - | | +| defaultVisible | Popup layer default display status (uncontrolled) | boolean | - | | +| popupContainer | Popup layer container | string \| HTMLElement \| ((target: HTMLElement) => HTMLElement) | - | | +| popupAlign | Popup layer alignment, see Overlay documentation | string | 'tl bl' | | +| popupTriggerType | Popup layer trigger type | 'click' \| 'hover' | 'click' | | +| onVisibleChange | Callback when the popup layer display status changes | (visible: boolean, reason?: string) => void | - | | +| popupStyle | Popup layer custom style | CSSProperties | - | | +| popupClassName | Popup layer custom style class | string | - | | +| popupProps | Popup layer property | PopupProps | - | | +| followTrigger | Follow trigger element | boolean | - | | +| hasBorder | Whether the input has border | boolean | true | | +| isPreview | Is preview | boolean | - | | +| renderPreview | Content of preview mode | (value: ValueType, props: TimePickerProps) => ReactNode | - | | +| inputProps | Custom input property | InputProps | - | | +| placeholder | Input hint | string | - | | +| value | Time value (Dayjs object or time string, controlled state use) | string \| Dayjs \| null \| (Dayjs \| null \| string)[] | - | | +| defaultValue | Time init value (Dayjs object or time string, uncontrolled state use) | string \| Dayjs \| (Dayjs \| null)[] | - | | +| disabledHours | For the disabled hours function, if it's a TimePicker.RangePicker, the function should return a number[] and it shouldn't have an index parameter. If it's not a TimePicker.RangePicker, the function should return a boolean and it should have an index parameter. | (index?: number) => boolean \| number[] | - | | +| disabledMinutes | For the disabled minutes function, if it's a TimePicker.RangePicker, the function should return a number[] and it shouldn't have an index parameter. If it's not a TimePicker.RangePicker, the function should return a boolean and it should have an index parameter. | (index?: number) => boolean \| number[] | - | | +| disabledSeconds | For the disabled seconds function, if it's a TimePicker.RangePicker, the function should return a number[] and it shouldn't have an index parameter. If it's not a TimePicker.RangePicker, the function should return a boolean and it should have an index parameter. | (index?: number) => boolean \| number[] | - | | +| onChange | Callback when the time value changes | (value: ValueType) => void | - | | +| preset | Rreset values, shown below the time panel.
Can be object or array of object, with the following properties.
properties:
label: shown text
name: key of React element, can be empty, and index will become key instead
value: date value | PresetType \| PresetType[] | - | | +| disabled | Disable | boolean \| boolean[] | false | | + +### TimePicker.RangePicker + +| Param | Description | Type | Default Value | Required | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------- | +| label | Button text | ReactNode | - | | +| size | Size of time picker | 'small' \| 'medium' \| 'large' | 'medium' | | +| state | Input state | 'error' \| 'success' | - | | +| hasClear | Whether to allow clearing time | boolean | true | | +| format | Time format | string | 'HH:mm:ss' | | +| hourStep | Hour option step | number | - | | +| minuteStep | Minute option step | number | - | | +| secondStep | Second option step | number | - | | +| renderTimeMenuItems | Render the selectable time list

**signature**:
**params**:
_list_: list
_mode_: mode
_value_: value | (
list: Array\,
mode: TimeMenuProps['mode'],
value: TimeMenuProps['value']
) => Array\ | - | | +| visible | Popup layer display status (controlled) | boolean | - | | +| defaultVisible | Popup layer default display status (uncontrolled) | boolean | - | | +| popupContainer | Popup layer container | string \| HTMLElement \| ((target: HTMLElement) => HTMLElement) | - | | +| popupAlign | Popup layer alignment, see Overlay documentation | string | 'tl bl' | | +| popupTriggerType | Popup layer trigger type | 'click' \| 'hover' | 'click' | | +| onVisibleChange | Callback when the popup layer display status changes | (visible: boolean, reason?: string) => void | - | | +| popupStyle | Popup layer custom style | CSSProperties | - | | +| popupClassName | Popup layer custom style class | string | - | | +| popupProps | Popup layer property | PopupProps | - | | +| followTrigger | Follow trigger element | boolean | - | | +| hasBorder | Whether the input has border | boolean | true | | +| isPreview | Is preview | boolean | - | | +| renderPreview | Content of preview mode | (value: ValueType, props: TimePickerProps) => ReactNode | - | | +| inputProps | Custom input property | InputProps | - | | +| disabledHours | For the disabled hours function, if it's a TimePicker.RangePicker, the function should return a number[] and it shouldn't have an index parameter. If it's not a TimePicker.RangePicker, the function should return a boolean and it should have an index parameter. | (index?: number) => boolean \| number[] | - | | +| disabledMinutes | For the disabled minutes function, if it's a TimePicker.RangePicker, the function should return a number[] and it shouldn't have an index parameter. If it's not a TimePicker.RangePicker, the function should return a boolean and it should have an index parameter. | (index?: number) => boolean \| number[] | - | | +| disabledSeconds | For the disabled seconds function, if it's a TimePicker.RangePicker, the function should return a number[] and it shouldn't have an index parameter. If it's not a TimePicker.RangePicker, the function should return a boolean and it should have an index parameter. | (index?: number) => boolean \| number[] | - | | +| disabled | Disable | boolean \| boolean[] | false | | +| placeholder | Input hint | string \| string[] | - | | +| value | Time value (Dayjs object or time string, controlled state use) | Array\ | - | | +| defaultValue | Time init value (Dayjs object or time string, uncontrolled state use) | Array\ | - | | +| onChange | Callback when the time value changes | (value: Array\) => void | - | | +| onOk | Callback when the ok button is clicked | (value: Array\) => void | - | | +| preset | Rreset values, shown below the time panel.
Can be object or array of object, with the following properties.
properties:
label: shown text
name: key of React element, can be empty, and index will become key instead
value: date value | PresetType[] | - | | ## ARIA and KeyBoard -| 按键 | 说明 | -| :---- | :--------------- | -| Enter | Open time select popup | -| Esc | Close time select popup | -| Up | Input previous seconds (if `disabledMinutes={true}` is previous minutes or previous hours) | -| Down | Input next seconds (if `disabledMinutes={true}` is next minutes or next hours) | -| Page Up | Input previous minutes | -| Page Down | Input next minutes | -| Alt + Page Up | Input previous hours | -| Alt + Page Down | Input next hours | +| 按键 | 说明 | +| :-------------- | :------------------------------------------------------------------------------------------- | +| Enter | Open time select popup | +| Esc | Close time select popup | +| Up | Input previous seconds (if `disabledMinutes={true}` is previous minutes or previous hours) | +| Down | Input next seconds (if `disabledMinutes={true}` is next minutes or next hours) | +| Page Up | Input previous minutes | +| Page Down | Input next minutes | +| Alt + Page Up | Input previous hours | +| Alt + Page Down | Input next hours | diff --git a/components/time-picker2/__docs__/index.md b/components/time-picker2/__docs__/index.md index f7c7f10331..f8f05a33cb 100644 --- a/components/time-picker2/__docs__/index.md +++ b/components/time-picker2/__docs__/index.md @@ -28,79 +28,100 @@ API变化: 当用户需要输入一个时间,可以点击输入框,在弹出的时间选择面板上操作。时间选择面板仅支持 24 小时制。`format` 支持的时间格式如下: -| 格式 | 例子 | 说明 | -| ------ | ------- | -------- | +| 格式 | 例子 | 说明 | +| ------ | ------- | ------------- | | `H HH` | `0..23` | 时,24 小时制 | -| `m mm` | `0..59` | 分 | -| `s ss` | `0..59` | 秒 | +| `m mm` | `0..59` | 分 | +| `s ss` | `0..59` | 秒 | 组件默认使用 dayjs 实例作为输入输出值,推荐使用结合 dayjs 的方式使用组件。此外,组件也支持传入时间字符串的方式,例如直接传入 "12:00:00"。用户传入什么类型的 value/defaultValue 值,就会在 onChange 中返回相应的类型。 ## API -### 公共API - -| 参数 | 说明 | 类型 | 默认值 | -| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ---------- | -| label | 按钮的文案 | ReactNode | - | -| size | 时间选择框的尺寸

**可选值**:
'small', 'medium', 'large' | Enum | 'medium' | -| state | 输入框状态

**可选值**:
'error', 'success' | Enum | - | -| hasClear | 是否允许清空时间 | Boolean | true | -| format | 时间的格式
| String | 'HH:mm:ss' | -| hourStep | 小时选项步长 | Number | - | -| minuteStep | 分钟选项步长 | Number | - | -| secondStep | 秒钟选项步长 | Number | - | -| disabledHours | 禁用小时函数

**签名**:
Function(index: Number) => Boolean
**参数**:
_index_: {Number} 时 0 - 23
**返回值**:
{Boolean} 是否禁用
| Function | - | -| disabledMinutes | 禁用分钟函数

**签名**:
Function(index: Number) => Boolean
**参数**:
_index_: {Number} 分 0 - 59
**返回值**:
{Boolean} 是否禁用
| Function | - | -| disabledSeconds | 禁用秒钟函数

**签名**:
Function(index: Number) => Boolean
**参数**:
_index_: {Number} 秒 0 - 59
**返回值**:
{Boolean} 是否禁用
| Function | - | -| renderTimeMenuItems | 渲染的可选择时间列表
[{
label: '01',
value: 1
}]

**签名**:
Function(list: Array, mode: String, value: dayjs) => Array
**参数**:
_list_: {Array} 默认渲染的列表
_mode_: {String} 渲染的菜单 hour, minute, second
_value_: {dayjs} 当前时间,可能为 null
**返回值**:
{Array} 返回需要渲染的数据
| Function | - | -| visible | 弹层是否显示(受控) | Boolean | - | -| defaultVisible | 弹层默认是否显示(非受控) | Boolean | - | -| popupContainer | 弹层容器 | any | - | -| popupAlign | 弹层对齐方式, 详情见Overlay 文档 | String | 'tl bl' | -| popupTriggerType | 弹层触发方式

**可选值**:
'click', 'hover' | Enum | 'click' | -| onVisibleChange | 弹层展示状态变化时的回调

**签名**:
Function(visible: Boolean, type: String) => void
**参数**:
_visible_: {Boolean} 弹层是否隐藏和显示
_type_: {String} 触发弹层显示和隐藏的来源 fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发 | Function | func.noop | -| popupStyle | 弹层自定义样式 | Object | - | -| popupClassName | 弹层自定义样式类 | String | - | -| popupProps | 弹层属性 | Object | - | -| followTrigger | 是否跟随滚动 | Boolean | - | -| disabled | 是否禁用 | Boolean | false | -| hasBorder | 输入框是否有边框 | Boolean | true | -| isPreview | 是否为预览态 | Boolean | - | -| renderPreview | 预览态模式下渲染的内容

**签名**:
Function(value: DayjsObject/DayjsObject[]) => void
**参数**:
_value_: {DayjsObject/DayjsObject[]} 时间 | Function | - | - -### TimePicker2 - -| 参数 | 说明 | 类型 | 默认值 | -| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ---------- | -| placeholder | 输入框提示 | String | - | -| value | 时间值,dayjs格式或者2020-01-01字符串格式,受控状态使用 | custom | - | -| defaultValue | 时间初值,dayjs格式或者2020-01-01字符串格式,非受控状态使用 | custom | - | -| onChange | 时间值改变时的回调

**签名**:
Function(dateString: Object/String, date: DayjsObject) => void
**参数**:
_dateString_: {Object/String} 时间对象或时间字符串
_date_: {DayjsObject} dayjs时间对象 | Function | func.noop | -| onChange | 时间值改变时的回调

**签名**:
Function(date: DayjsObject, dateString: Object/String) => void
**参数**:
_date_: {DayjsObject} dayjs时间对象
_dateString_: {Object/String} 时间对象或时间字符串 | Function | func.noop | -| preset | 预设值,会显示在时间面板下面 | Array<custom>/custom | - | -| disabled | 是否禁用 | Boolean[] | [false, false] | +### TimePicker + +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | -------- | +| label | 按钮的文案 | ReactNode | - | | +| size | 时间选择框的尺寸 | 'small' \| 'medium' \| 'large' | 'medium' | | +| state | 输入框状态 | 'error' \| 'success' | - | | +| hasClear | 是否允许清空时间 | boolean | true | | +| format | 时间的格式 | string | 'HH:mm:ss' | | +| hourStep | 小时选项步长 | number | - | | +| minuteStep | 分钟选项步长 | number | - | | +| secondStep | 秒钟选项步长 | number | - | | +| renderTimeMenuItems | 渲染的可选择时间列表 [\{ label: '01', value: 1 \}]

**签名**:
**参数**:
_list_: 默认渲染的列表
_mode_: 渲染的菜单 hour, minute, second
_value_: 当前时间,可能为 null
**返回值**:
返回需要渲染的数据 | (
list: Array\,
mode: TimeMenuProps['mode'],
value: TimeMenuProps['value']
) => Array\ | - | | +| visible | 弹层是否显示(受控) | boolean | - | | +| defaultVisible | 弹层默认是否显示(非受控) | boolean | - | | +| popupContainer | 弹层容器 | string \| HTMLElement \| ((target: HTMLElement) => HTMLElement) | - | | +| popupAlign | 弹层对齐方式,详情见 Overlay 文档 | string | 'tl bl' | | +| popupTriggerType | 弹层触发方式 | 'click' \| 'hover' | 'click' | | +| onVisibleChange | 弹层展示状态变化时的回调 | (visible: boolean, reason?: string) => void | - | | +| popupStyle | 弹层自定义样式 | CSSProperties | - | | +| popupClassName | 弹层自定义样式类 | string | - | | +| popupProps | 弹层属性 | PopupProps | - | | +| followTrigger | 跟随触发元素 | boolean | - | | +| hasBorder | 是否有边框 | boolean | true | | +| isPreview | 是否为预览态 | boolean | - | | +| renderPreview | 预览态模式下渲染的内容 | (value: ValueType, props: TimePickerProps) => ReactNode | - | | +| inputProps | 自定义输入框属性 | InputProps | - | | +| placeholder | 输入框提示 | string | - | | +| value | 时间值(Dayjs 对象或时间字符串,受控状态使用) | string \| Dayjs \| null \| (Dayjs \| null \| string)[] | - | | +| defaultValue | 时间初值(Dayjs 对象或时间字符串,非受控状态使用) | string \| Dayjs \| (Dayjs \| null)[] | - | | +| disabledHours | 禁用小时的函数,TimePicker.RangePicker 时,函数需要返回 number[],且函数中没有 index 入参,非 TimePicker.RangePicker 时,函数需要返回 boolean,函数中有 index 入参 | (index?: number) => boolean \| number[] | - | | +| disabledMinutes | 禁用分钟函数,TimePicker.RangePicker 时,函数需要返回 number[],且函数中没有 index 入参,非 TimePicker.RangePicker 时,函数需要返回 boolean,函数中有 index 入参 | (index?: number) => boolean \| number[] | - | | +| disabledSeconds | 禁用秒钟函数,TimePicker.RangePicker 时,函数需要返回 number[],且函数中没有 index 入参,非 TimePicker.RangePicker 时,函数需要返回 boolean,函数中有 index 入参 | (index?: number) => boolean \| number[] | - | | +| onChange | 时间值改变时的回调 | (value: ValueType) => void | - | | +| preset | 预设值,会显示在时间面板下面 | PresetType \| PresetType[] | - | | +| disabled | 禁用 | boolean \| boolean[] | false | | ### TimePicker.RangePicker -| 参数 | 说明 | 类型 | 默认值 -| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------- | -| placeholder | 输入提示,例如 ['开始时间', '结束时间'] | String | [String, String] | - | -| value | 日期值(受控)例如 ['11:00:00', '11:59:59'] | [Dayjs, Dayjs] | - | -| defaultValue | 初始日期值 例如 ['11:00:00', '11:59:59'] | [Dayjs, Dayjs] | - | -| onChange | 日期值改变时的回调

**签名**:
Function(value) => void
**参数**:
_value_: {[Dayjs, Dayjs]} 日期范围 | Function | func.noop | -| onOk | 点击确认按钮时的回调

**签名**:
Function(value) => void
**参数**:
_value_: {[Dayjs, Dayjs]} 日期范围
| Function | func.noop -| preset | 预设值,会显示在时间面板下面 | Array<custom>/custom | - | +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | -------- | +| label | 按钮的文案 | ReactNode | - | | +| size | 时间选择框的尺寸 | 'small' \| 'medium' \| 'large' | 'medium' | | +| state | 输入框状态 | 'error' \| 'success' | - | | +| hasClear | 是否允许清空时间 | boolean | true | | +| format | 时间的格式 | string | 'HH:mm:ss' | | +| hourStep | 小时选项步长 | number | - | | +| minuteStep | 分钟选项步长 | number | - | | +| secondStep | 秒钟选项步长 | number | - | | +| renderTimeMenuItems | 渲染的可选择时间列表 [\{ label: '01', value: 1 \}]

**签名**:
**参数**:
_list_: 默认渲染的列表
_mode_: 渲染的菜单 hour, minute, second
_value_: 当前时间,可能为 null
**返回值**:
返回需要渲染的数据 | (
list: Array\,
mode: TimeMenuProps['mode'],
value: TimeMenuProps['value']
) => Array\ | - | | +| visible | 弹层是否显示(受控) | boolean | - | | +| defaultVisible | 弹层默认是否显示(非受控) | boolean | - | | +| popupContainer | 弹层容器 | string \| HTMLElement \| ((target: HTMLElement) => HTMLElement) | - | | +| popupAlign | 弹层对齐方式,详情见 Overlay 文档 | string | 'tl bl' | | +| popupTriggerType | 弹层触发方式 | 'click' \| 'hover' | 'click' | | +| onVisibleChange | 弹层展示状态变化时的回调 | (visible: boolean, reason?: string) => void | - | | +| popupStyle | 弹层自定义样式 | CSSProperties | - | | +| popupClassName | 弹层自定义样式类 | string | - | | +| popupProps | 弹层属性 | PopupProps | - | | +| followTrigger | 跟随触发元素 | boolean | - | | +| hasBorder | 是否有边框 | boolean | true | | +| isPreview | 是否为预览态 | boolean | - | | +| renderPreview | 预览态模式下渲染的内容 | (value: ValueType, props: TimePickerProps) => ReactNode | - | | +| inputProps | 自定义输入框属性 | InputProps | - | | +| disabledHours | 禁用小时的函数,TimePicker.RangePicker 时,函数需要返回 number[],且函数中没有 index 入参,非 TimePicker.RangePicker 时,函数需要返回 boolean,函数中有 index 入参 | (index?: number) => boolean \| number[] | - | | +| disabledMinutes | 禁用分钟函数,TimePicker.RangePicker 时,函数需要返回 number[],且函数中没有 index 入参,非 TimePicker.RangePicker 时,函数需要返回 boolean,函数中有 index 入参 | (index?: number) => boolean \| number[] | - | | +| disabledSeconds | 禁用秒钟函数,TimePicker.RangePicker 时,函数需要返回 number[],且函数中没有 index 入参,非 TimePicker.RangePicker 时,函数需要返回 boolean,函数中有 index 入参 | (index?: number) => boolean \| number[] | - | | +| disabled | 禁用 | boolean \| boolean[] | false | | +| placeholder | 输入框提示 | string \| string[] | - | | +| value | 时间值(Dayjs 对象或时间字符串,受控状态使用) | Array\ | - | | +| defaultValue | 时间初值(Dayjs 对象或时间字符串,非受控状态使用) | Array\ | - | | +| onChange | 时间值改变时的回调 | (value: Array\) => void | - | | +| onOk | 确定按钮点击时的回调 | (value: Array\) => void | - | | +| preset | 预设值,会显示在时间面板下面 | PresetType[] | - | | ## ARIA and KeyBoard -| 按键 | 说明 | -| :-------------- | :---------------------------------------------------- | -| Enter | 打开时间选择框 | -| Esc | 关闭时间选择框 | +| 按键 | 说明 | +| :-------------- | :------------------------------------------------------------------------------ | +| Enter | 打开时间选择框 | +| Esc | 关闭时间选择框 | | Up | 输入上一秒的时间 (如果 `disabledMinutes={true}` 则可能是上一分钟或者上一小时) | | Down | 输入下一秒的时间 (如果 `disabledMinutes={true}` 则可能是下一分钟或者下一小时) | -| Page Up | 输入上一分钟的时间 | -| Page Down | 输入下一分钟的时间 | -| Alt + Page Up | 输入上一小时的时间 | -| Alt + Page Down | 输入下一小时的时间 | +| Page Up | 输入上一分钟的时间 | +| Page Down | 输入下一分钟的时间 | +| Alt + Page Up | 输入上一小时的时间 | +| Alt + Page Down | 输入下一小时的时间 | diff --git a/components/time-picker2/__tests__/index-spec.js b/components/time-picker2/__tests__/index-spec.js deleted file mode 100644 index f260aef5d8..0000000000 --- a/components/time-picker2/__tests__/index-spec.js +++ /dev/null @@ -1,422 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import Enzyme, { mount } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import assert from 'power-assert'; -import dayjs from 'dayjs'; -import TimePicker2 from '../index'; -import '../../time-picker/style'; -import { KEYCODE, dom } from '../../util'; - -const { hasClass } = dom; -Enzyme.configure({ adapter: new Adapter() }); -const defaultValue = dayjs('11:12:13', 'HH:mm:ss', true); - -const TimeRangePicker = TimePicker2.RangePicker; - -/* eslint-disable */ -describe('TimePicker2', () => { - describe('render', () => { - let wrapper; - - afterEach(() => { - wrapper.unmount(); - wrapper = null; - }); - - it('should render time-picker', () => { - wrapper = mount(); - assert(wrapper.find('.next-time-picker2').length === 1); - }); - - it('should render with defaultValue', () => { - wrapper = mount(); - assert(wrapper.find('.next-time-picker2-input input').instance().value === '11:12:13'); - }); - - it('should render with defaultValue as string', () => { - wrapper = mount(); - assert(wrapper.find('.next-time-picker2-input input').instance().value === '11:11:11'); - }); - - it('should render with format', () => { - wrapper = mount(); - assert(wrapper.find('.next-time-picker2-input input').instance().value === '10:1:1'); - }); - - it('should render with defaultVisible', () => { - wrapper = mount(); - assert( - wrapper - .find('.next-time-picker2-menu-hour .next-time-picker2-menu-item.next-selected') - .instance().title === '11' - ); - assert( - wrapper - .find( - '.next-time-picker2-menu-minute .next-time-picker2-menu-item.next-selected' - ) - .instance().title === '12' - ); - assert( - wrapper - .find( - '.next-time-picker2-menu-second .next-time-picker2-menu-item.next-selected' - ) - .instance().title === '13' - ); - }); - - it('should render with value controlled', () => { - wrapper = mount(); - const newValue = dayjs('12:22:22', 'HH:mm:ss', true); - wrapper.setProps({ value: newValue }); - assert(wrapper.find('.next-time-picker2-input input').instance().value === '12:22:22'); - }); - - it('should render with visisble controlled', () => { - wrapper = mount(); - assert(wrapper.find('.next-time-picker2-body').length === 0); - wrapper.setProps({ visible: true }); - assert(wrapper.find('.next-time-picker2-body').length === 1); - }); - - it('should render with step', () => { - wrapper = mount(); - assert( - wrapper.find('.next-time-picker2-menu-second .next-time-picker2-menu-item') - .length === 12 - ); - }); - - it('should render menu items', () => { - const renderTimeMenuItems = list => { - return list.map(({ label, value }) => { - return { - value, - label: value > 9 ? String(value) : `0${value}`, - }; - }); - }; - wrapper = mount( - - ); - - assert( - wrapper - .find('.next-time-picker2-menu-second .next-time-picker2-menu-item') - .at(0) - .text() === '00' - ); - - assert( - wrapper - .find('.next-time-picker2-menu-second .next-time-picker2-menu-item') - .at(9) - .text() === '09' - ); - - assert( - wrapper - .find('.next-time-picker2-menu-second .next-time-picker2-menu-item') - .at(10) - .text() === '10' - ); - }); - it('should support preview mode render', () => { - wrapper = mount(); - assert(wrapper.find('.next-form-preview').length > 0); - assert(wrapper.find('.next-form-preview').text() === '12:00:00'); - wrapper.setProps({ - renderPreview: value => { - assert(value.format('HH:mm:ss') === '12:00:00'); - return 'Hello World'; - }, - }); - assert(wrapper.find('.next-form-preview').text() === 'Hello World'); - }); - - it('should support string value', () => { - wrapper = mount(); - assert.equal(wrapper.find('.next-form-preview').text(), '12:00:00'); - }); - it('should support preview mode render when no value set', () => { - wrapper = mount(); - assert(wrapper.find('.next-form-preview').length > 0); - }); - it('should support preview mode & setValue', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - wrapper = mount(, { attachTo: container }); - assert(wrapper.find('.next-form-preview').length > 0); - assert.equal(wrapper.find('.next-form-preview').text(), ''); - const value = dayjs('12:22:22', 'HH:mm:ss', true); - wrapper.setProps({ value: value }); - assert(wrapper.find('.next-form-preview').length > 0); - assert.equal(wrapper.find('.next-form-preview').text(), '12:22:22'); - }); - it('should support preview mode on type is range', () => { - wrapper = mount(); - assert(wrapper.find('.next-form-preview').length > 0); - const startValue = dayjs('12:22:22', 'HH:mm:ss', true); - const endValue = dayjs('17:22:22', 'HH:mm:ss', true); - wrapper.setProps({ value: [startValue, endValue] }); - assert.equal(wrapper.find('.next-form-preview').text(), '12:22:22-17:22:22'); - wrapper.setProps({ value: [startValue] }); - assert.equal(wrapper.find('.next-form-preview').text(), '12:22:22-'); - wrapper.setProps({ value: [null, endValue] }); - assert.equal(wrapper.find('.next-form-preview').text(), '-17:22:22'); - }); - }); - - describe('action', () => { - let wrapper; - let ret; - - afterEach(() => { - wrapper.unmount(); - wrapper = null; - ret = null; - }); - - it('should reset value', () => { - ret = 'hello'; - wrapper = mount( - { - ret = val; - }} - /> - ); - wrapper.find('.next-icon-delete-filling').simulate('click'); - assert(ret === null); - }); - - it('should format value(hide hours)', () => { - wrapper = mount(); - wrapper.find('.next-time-picker2-input input').simulate('click'); - assert(wrapper.find('.next-time-picker2-menu-hour').length === 0); - }); - - it('should format value(hide seconds)', () => { - wrapper = mount(); - wrapper.find('.next-time-picker2-input input').simulate('click'); - assert(wrapper.find('.next-time-picker2-menu-second').length === 0); - }); - - it('should input value in picker', () => { - wrapper = mount( - { - ret = val.format('HH:mm:ss'); - }} - /> - ); - wrapper - .find('.next-time-picker2-input input') - .simulate('change', { target: { value: '20:00:00' } }); - wrapper - .find('.next-time-picker2-input input') - .simulate('keydown', { keyCode: KEYCODE.ENTER }); - assert(wrapper.find('.next-time-picker2-input input').instance().value === '20:00:00'); - assert(ret === '20:00:00'); - }); - - it('should render presets & change value on clicking presets', () => { - ret = null; - wrapper = mount( - { - ret = val.format('HH:mm:ss'); - }} - preset={[ - { - label: 'now', - name: 'preset-key', - value: () => { - return dayjs('13:12:11', 'HH:mm:ss', true); - }, - }, - ]} - /> - ); - - wrapper.find('.next-time-picker2-input input').simulate('click'); - - wrapper.find('.next-time-picker2-footer button').simulate('click'); - - assert(ret === '13:12:11'); - }); - - it('should select time-picker2 panel', () => { - wrapper = mount( - { - ret = val.format('HH:mm:ss'); - }} - /> - ); - wrapper.find('.next-time-picker2-input input').simulate('click'); - wrapper - .find('.next-time-picker2-menu-hour .next-time-picker2-menu-item') - .at(2) - .simulate('click'); - assert(ret === '02:00:00'); - wrapper - .find('.next-time-picker2-menu-minute .next-time-picker2-menu-item') - .at(2) - .simulate('click'); - assert(ret === '02:02:00'); - wrapper - .find('.next-time-picker2-menu-second .next-time-picker2-menu-item') - .at(2) - .simulate('click'); - assert(ret === '02:02:02'); - }); - - it('should keyboard date time input', () => { - wrapper = mount(); - - const timeInput = wrapper.find('.next-time-picker2-input input'); - const instance = wrapper.instance().getInstance(); - timeInput.simulate('keydown', { keyCode: KEYCODE.DOWN }); - assert(instance.state.inputStr === '00:00:00'); - timeInput.simulate('keydown', { keyCode: KEYCODE.LEFT }); - timeInput.simulate('keydown', { keyCode: KEYCODE.DOWN, altKey: true }); - timeInput.simulate('keydown', { keyCode: KEYCODE.DOWN, shiftKey: true }); - timeInput.simulate('keydown', { keyCode: KEYCODE.DOWN, controlKey: true }); - assert(instance.state.inputStr === '00:00:00'); - timeInput.simulate('keydown', { keyCode: KEYCODE.DOWN }); - assert(instance.state.inputStr === '00:00:01'); - timeInput.simulate('keydown', { keyCode: KEYCODE.UP }); - assert(instance.state.inputStr === '00:00:00'); - timeInput.simulate('keydown', { keyCode: KEYCODE.PAGE_DOWN }); - assert(instance.state.inputStr === '00:01:00'); - timeInput.simulate('keydown', { keyCode: KEYCODE.PAGE_UP }); - assert(instance.state.inputStr === '00:00:00'); - timeInput.simulate('keydown', { keyCode: KEYCODE.PAGE_DOWN, altKey: true }); - assert(instance.state.inputStr === '01:00:00'); - timeInput.simulate('keydown', { keyCode: KEYCODE.PAGE_UP, altKey: true }); - assert(instance.state.inputStr === '00:00:00'); - }); - }); - - describe('range', () => { - let wrapper; - let ret; - - afterEach(() => { - wrapper.unmount(); - wrapper = null; - ret = null; - }); - - it('should support default value', () => { - wrapper = mount( - - ); - - assert.deepEqual(getStrValue(wrapper), ['11:12:13', '12:12:13']); - }); - - it('should select value', async () => { - wrapper = mount(); - - findInput(wrapper, 0).simulate('click'); - assert(findTime(wrapper, 12, 'hour').length === 2); - findTime(wrapper, 12, 'hour').at(1).simulate('click'); - clickOk(wrapper); - - assert.deepEqual(getStrValue(wrapper), ['11:12:13', '12:00:00']); - }); - it('should render with value controlled', () => { - wrapper = mount( - - ); - - assert.deepEqual(getStrValue(wrapper), ['11:12:13', '12:12:13']); - - findInput(wrapper, 0).simulate('click'); - assert(findTime(wrapper, 12, 'hour').length === 2); - findTime(wrapper, 13, 'hour').at(1).simulate('click'); - clickOk(wrapper); - - assert.deepEqual(getStrValue(wrapper), ['11:12:13', '12:12:13']); - - const first = dayjs('11:13:00', 'HH:mm:ss', true); - const second = dayjs('12:22:22', 'HH:mm:ss', true); - wrapper.setProps({ value: [first, second] }); - - assert.deepEqual(getStrValue(wrapper), ['11:13:00', '12:22:22']); - }); - }); - describe('issues', () => { - it('should has border when single time input is focusing', done => { - const container = document.createElement('div'); - document.body.appendChild(container); - ReactDOM.render(, container); - const inputNode = document.querySelector('.next-time-picker2-input'); - inputNode.querySelector('input').click(); - assert(hasClass(inputNode, 'next-time-picker2-input-focus')); - document.body.click(); - setTimeout(() => { - assert(!hasClass(inputNode, 'next-time-picker2-input-focus')); - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - done(); - }, 1000); - }); - - it('should has border when time-range input is focusing', done => { - const container = document.createElement('div'); - document.body.appendChild(container); - ReactDOM.render(, container); - const inputNode = document.querySelector('.next-time-picker2-input'); - inputNode.querySelectorAll('input')[0].click(); - assert(hasClass(inputNode, 'next-time-picker2-input-focus')); - inputNode.querySelectorAll('input')[1].click(); - assert(hasClass(inputNode, 'next-time-picker2-input-focus')); - document.body.click(); - setTimeout(() => { - assert(!hasClass(inputNode, 'next-time-picker2-input-focus')); - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - done(); - }, 1000); - }); - - it('should support custom formatting , close #3651', () => { - const div = document.createElement('div'); - document.body.appendChild(div); - const wrapper = mount(, { attachTo: div }); - wrapper - .find('.next-time-picker2-input input') - .simulate('change', { target: { value: '12' } }); - wrapper.update(); - assert( - document - .querySelector('li[title="12"][role="option"]') - .classList.contains('next-selected') - ); - }); - }); -}); - -function getStrValue(wrapper) { - const inputEl = wrapper.find('.next-time-picker2-input input'); - return inputEl.length === 1 ? inputEl.instance().value : inputEl.map(el => el.instance().value); -} - -function findInput(wrapper, idx) { - const input = wrapper.find('.next-input > input'); - return idx !== undefined ? input.at(idx) : input; -} - -function findTime(wrapper, strVal, mode = 'hour') { - return wrapper.find(`.next-time-picker2-menu-${mode}>li[title=${strVal}]`); -} - -function clickOk(wrapper) { - wrapper.find('button.next-date-picker2-footer-ok').simulate('click'); -} diff --git a/components/time-picker2/__tests__/index-spec.tsx b/components/time-picker2/__tests__/index-spec.tsx new file mode 100644 index 0000000000..7bcb82251b --- /dev/null +++ b/components/time-picker2/__tests__/index-spec.tsx @@ -0,0 +1,378 @@ +import React from 'react'; +import dayjs, { type Dayjs } from 'dayjs'; +import TimePicker2, { type TimePickerProps } from '../index'; +import '../../time-picker/style'; + +const defaultValue = dayjs('11:12:13', 'HH:mm:ss', true); + +const TimeRangePicker = TimePicker2.RangePicker; + +function checkInputValues(compareValue?: string[] | string) { + cy.get('.next-time-picker2-input input').then($inputEl => { + if ($inputEl.length === 1) { + const value = $inputEl.val() as string | undefined; + expect(value).to.equal(compareValue); + } else { + const values: (string | number)[] = []; + $inputEl.each((index, el) => { + values.push((el as HTMLInputElement).value); + }); + expect(values).to.deep.equal(compareValue); + } + }); +} + +function findInput(idx: number) { + if (idx !== undefined) { + return cy.get('.next-input > input').eq(idx); + } + return cy.get('.next-input > input'); +} + +function findTime(strVal: string | number, mode = 'hour') { + return cy.get(`.next-time-picker2-menu-${mode}>li[title=${strVal}]`); +} + +function clickOk() { + cy.get('button.next-date-picker2-footer-ok').click(); +} + +describe('TimePicker2', () => { + describe('render', () => { + it('should render time-picker', () => { + cy.mount(); + cy.get('.next-time-picker2').should('exist'); + }); + + it('should render with defaultValue', () => { + cy.mount(); + cy.get('.next-time-picker2-input input').should('have.value', '11:12:13'); + }); + + it('should render with defaultValue as string', () => { + cy.mount(); + cy.get('.next-time-picker2-input input').should('have.value', '11:11:11'); + }); + + it('should render with format', () => { + cy.mount(); + cy.get('.next-time-picker2-input input').should('have.value', '10:1:1'); + }); + + it('should render with defaultVisible', () => { + cy.mount(); + cy.get( + '.next-time-picker2-menu-hour .next-time-picker2-menu-item.next-selected' + ).should('have.attr', 'title', '11'); + cy.get( + '.next-time-picker2-menu-minute .next-time-picker2-menu-item.next-selected' + ).should('have.attr', 'title', '12'); + cy.get( + '.next-time-picker2-menu-second .next-time-picker2-menu-item.next-selected' + ).should('have.attr', 'title', '13'); + }); + + it('should render with value controlled', () => { + cy.mount().as('Demo'); + const newValue = dayjs('12:22:22', 'HH:mm:ss', true); + cy.rerender('Demo', { value: newValue }); + cy.get('.next-time-picker2-input input').should('have.value', '12:22:22'); + }); + + it('should render with visisble controlled', () => { + cy.mount().as('Demo'); + cy.get('.next-time-picker2-body').should('not.exist'); + cy.rerender('Demo', { visible: true }); + cy.get('.next-time-picker2-body').should('exist'); + }); + + it('should render with step', () => { + cy.mount(); + cy.get('.next-time-picker2-menu-second .next-time-picker2-menu-item').should( + 'have.length', + 12 + ); + }); + + it('should render menu items', () => { + const renderTimeMenuItems: TimePickerProps['renderTimeMenuItems'] = list => { + return list.map(({ value }) => { + return { + value, + label: value > 9 ? String(value) : `0${value}`, + }; + }); + }; + cy.mount(); + cy.get('.next-time-picker2-menu-second .next-time-picker2-menu-item') + .eq(0) + .should('have.text', '00'); + cy.get('.next-time-picker2-menu-second .next-time-picker2-menu-item') + .eq(9) + .should('have.text', '09'); + cy.get('.next-time-picker2-menu-second .next-time-picker2-menu-item') + .eq(10) + .should('have.text', '10'); + }); + it('should support preview mode render', () => { + cy.mount().as('Demo'); + cy.get('.next-form-preview').should('exist'); + cy.get('.next-form-preview').should('have.text', '12:00:00'); + const handleRenderPreview = cy.spy(); + cy.rerender('Demo', { + renderPreview: (value: Dayjs) => { + handleRenderPreview(value.format('HH:mm:ss')); + return 'Hello World'; + }, + }); + cy.wrap(handleRenderPreview).should('be.calledWith', '12:00:00'); + cy.get('.next-form-preview').should('have.text', 'Hello World'); + }); + + it('should support string value', () => { + cy.mount(); + cy.get('.next-form-preview').should('have.text', '12:00:00'); + }); + it('should support preview mode render when no value set', () => { + cy.mount(); + cy.get('.next-form-preview').should('exist'); + }); + it('should support preview mode & setValue', () => { + cy.mount().as('Demo'); + cy.get('.next-form-preview').should('exist'); + cy.get('.next-form-preview').should('have.text', ''); + const value = dayjs('12:22:22', 'HH:mm:ss', true); + cy.rerender('Demo', { value }); + cy.get('.next-form-preview').should('exist'); + cy.get('.next-form-preview').should('have.text', '12:22:22'); + }); + it('should support preview mode on type is range', () => { + cy.mount().as('Demo'); + cy.get('.next-form-preview').should('exist'); + + const startValue = dayjs('12:22:22', 'HH:mm:ss', true); + const endValue = dayjs('17:22:22', 'HH:mm:ss', true); + cy.rerender('Demo', { value: [startValue, endValue] }); + cy.get('.next-form-preview').should('have.text', '12:22:22-17:22:22'); + + cy.rerender('Demo', { value: [startValue] }); + cy.get('.next-form-preview').should('have.text', '12:22:22-'); + + cy.rerender('Demo', { value: [null, endValue] }); + cy.get('.next-form-preview').should('have.text', '-17:22:22'); + }); + }); + + describe('action', () => { + it('should reset value', () => { + const handleChange = cy.spy(); + cy.mount(); + cy.get('.next-icon-delete-filling').click(); + cy.wrap(handleChange).should('be.calledWith', null); + }); + + it('should format value(hide hours)', () => { + cy.mount(); + cy.get('.next-time-picker2-input input').click(); + cy.get('.next-time-picker2-menu-hour').should('not.exist'); + }); + + it('should format value(hide seconds)', () => { + cy.mount(); + cy.get('.next-time-picker2-input input').click(); + cy.get('.next-time-picker2-menu-second').should('not.exist'); + }); + + it('should input value in picker', () => { + const handleChange = cy.spy(); + cy.mount( + { + handleChange((val as Dayjs).format('HH:mm:ss')); + }} + /> + ); + cy.get('.next-time-picker2-input input').type('20:00:00'); + cy.get('.next-time-picker2-input input').trigger('keydown', { keyCode: 13 }); + cy.get('.next-time-picker2-input input').should('have.value', '20:00:00'); + cy.wrap(handleChange).should('be.calledWith', '20:00:00'); + }); + + it('should render presets & change value on clicking presets', () => { + const handleChange = cy.spy(); + cy.mount( + { + handleChange((val as Dayjs).format('HH:mm:ss')); + }} + preset={[ + { + label: 'now', + name: 'preset-key', + value: () => { + return dayjs('13:12:11', 'HH:mm:ss', true); + }, + }, + ]} + /> + ); + cy.get('.next-time-picker2-input input').click(); + + cy.get('.next-time-picker2-footer button').click(); + cy.wrap(handleChange).should('be.calledWith', '13:12:11'); + }); + + it('should select time-picker2 panel', () => { + const handleChange = cy.spy(); + cy.mount( + { + if (dayjs.isDayjs(val)) { + handleChange(val.format('HH:mm:ss')); + } else { + handleChange(val); + } + }} + /> + ); + cy.get('.next-time-picker2-input input').click(); + cy.get('.next-time-picker2-menu-hour .next-time-picker2-menu-item').eq(2).click(); + cy.wrap(handleChange).should('be.calledWith', '02:00:00'); + cy.get('.next-time-picker2-menu-minute .next-time-picker2-menu-item').eq(2).click(); + cy.wrap(handleChange).should('be.calledWith', '02:02:00'); + cy.get('.next-time-picker2-menu-second .next-time-picker2-menu-item').eq(2).click(); + cy.wrap(handleChange).should('be.calledWith', '02:02:02'); + }); + + it('should keyboard date time input', () => { + cy.mount(); + const timeInput = '.next-time-picker2-input input'; + + cy.get(timeInput).type('{downarrow}'); + cy.get(timeInput).should('have.value', '00:00:00'); + + cy.get(timeInput).type('{leftarrow}', { force: true }); + cy.get(timeInput).type('{alt+downarrow}', { force: true }); + cy.get(timeInput).type('{shift+downarrow}', { force: true }); + + cy.get(timeInput).should('have.value', '00:00:00'); + + cy.get(timeInput).type('{downarrow}'); + cy.get(timeInput).should('have.value', '00:00:01'); + + cy.get(timeInput).type('{uparrow}'); + cy.get(timeInput).should('have.value', '00:00:00'); + + cy.get(timeInput).type('{pagedown}'); + cy.get(timeInput).should('have.value', '00:01:00'); + + cy.get(timeInput).type('{pageup}'); + cy.get(timeInput).should('have.value', '00:00:00'); + + cy.get(timeInput).type('{alt+pageDown}'); + cy.get(timeInput).should('have.value', '01:00:00'); + + cy.get(timeInput).type('{alt+pageUp}'); + cy.get(timeInput).should('have.value', '00:00:00'); + }); + + it('should keyboard date time input', () => { + cy.mount(); + const timeInput = '.next-time-picker2-input input'; + + cy.get(timeInput).type('{ctrl+downarrow}', { force: true }); + cy.get(timeInput).should('have.value', '00:00:00'); + }); + }); + + describe('range', () => { + it('should support default value', () => { + cy.mount( + + ); + + checkInputValues(['11:12:13', '12:12:13']); + }); + + it('should select value', () => { + cy.mount(); + + findInput(0).click(); + findTime(12, 'hour').should('have.length', 2); + findTime(12, 'hour').eq(1).click(); + clickOk(); + + checkInputValues(['11:12:13', '12:00:00']); + }); + + it('should render with value controlled', () => { + cy.mount().as( + 'Demo' + ); + + checkInputValues(['11:12:13', '12:12:13']); + findInput(0).click(); + findTime(12, 'hour').should('have.length', 2); + findTime(13, 'hour').eq(1).click(); + clickOk(); + + checkInputValues(['11:12:13', '12:12:13']); + + const first = dayjs('11:13:00', 'HH:mm:ss', true); + const second = dayjs('12:22:22', 'HH:mm:ss', true); + cy.rerender('Demo', { value: [first, second] }); + checkInputValues(['11:13:00', '12:22:22']); + }); + }); + describe('issues', () => { + it('should has border when single time input is focusing', () => { + cy.mount( +
+ +
+ ); + cy.get('.next-time-picker2-input input').click(); + cy.get('.next-time-picker2-input').should( + 'have.class', + 'next-time-picker2-input-focus' + ); + cy.get('body').click(); + + cy.get('.next-time-picker2-input').should( + 'not.have.class', + 'next-time-picker2-input-focus' + ); + }); + + it('should has border when time-range input is focusing', () => { + cy.mount( +
+ +
+ ); + cy.get('.next-time-picker2-input input').eq(0).click(); + + cy.get('.next-time-picker2-input').should( + 'have.class', + 'next-time-picker2-input-focus' + ); + cy.get('.next-time-picker2-input input').eq(1).click(); + cy.get('.next-time-picker2-input').should( + 'have.class', + 'next-time-picker2-input-focus' + ); + cy.get('body').click(); + cy.get('.next-time-picker2-input').should( + 'not.have.class', + 'next-time-picker2-input-focus' + ); + }); + + it('should support custom formatting , close #3651', () => { + cy.mount(); + cy.get('.next-time-picker2-input input').type('12'); + + cy.get('li[title="12"][role="option"]').should('have.class', 'next-selected'); + }); + }); +}); diff --git a/components/time-picker2/constant.js b/components/time-picker2/constant.ts similarity index 100% rename from components/time-picker2/constant.js rename to components/time-picker2/constant.ts diff --git a/components/time-picker2/index.d.ts b/components/time-picker2/index.d.ts deleted file mode 100644 index b7466c1de4..0000000000 --- a/components/time-picker2/index.d.ts +++ /dev/null @@ -1,206 +0,0 @@ -/// - -import React from 'react'; -import { CommonProps } from '../util'; -import { PopupProps } from '../overlay'; -import { InputProps } from '../input'; -import { ButtonProps } from '../button'; -import { Dayjs, ConfigType } from 'dayjs'; - -interface IPresetType { - label?: string; - value?: ConfigType | (() => ConfigType); -} -interface HTMLAttributesWeak extends React.HTMLAttributes { - defaultValue?: any; - onChange?: any; -} - -export interface DatePreset extends ButtonProps { - label: string; - // 时间值(dayjs 对象或时间字符串)或者返回时间值的函数 - value: any; -} - -export interface RangePreset { - [propName: string]: Dayjs[]; -} - -export type StateValue = Dayjs | null; - -export interface TimePickerProps extends HTMLAttributesWeak, CommonProps { - /** - * 按钮的文案 - */ - label?: React.ReactNode; - name?: string; - - /** - * 输入框状态 - */ - state?: 'error' | 'success'; - - /** - * 输入框提示 - */ - placeholder?: string; - - /** - * 时间值(dayjs 对象或时间字符串,受控状态使用) - */ - value?: ConfigType; - - /** - * 时间初值(dayjs 对象或时间字符串,非受控状态使用) - */ - defaultValue?: ConfigType; - - /** - * 时间选择框的尺寸 - */ - size?: 'small' | 'medium' | 'large'; - - /** - * 是否允许清空时间 - */ - hasClear?: boolean; - - /** - * 时间的格式 - * https://dayjs.gitee.io/docs/zh-CN/display/format - */ - format?: string; - - /** - * 小时选项步长 - */ - hourStep?: number; - - /** - * 分钟选项步长 - */ - minuteStep?: number; - - /** - * 秒钟选项步长 - */ - secondStep?: number; - - /** - * 禁用小时函数 - */ - disabledHours?: (index: number) => boolean; - - /** - * 禁用分钟函数 - */ - disabledMinutes?: (index: number) => boolean; - - /** - * 禁用秒钟函数 - */ - disabledSeconds?: (index: number) => boolean; - - /** - * 弹层是否显示(受控) - */ - visible?: boolean; - - /** - * 弹层默认是否显示(非受控) - */ - defaultVisible?: boolean; - - /** - * 弹层容器 - */ - popupContainer?: string | HTMLElement | ((target: HTMLElement) => HTMLElement); - - /** - * 弹层对齐方式, 详情见Overlay 文档 - */ - popupAlign?: string; - - /** - * 弹层触发方式 - */ - popupTriggerType?: 'click' | 'hover'; - - /** - * 弹层展示状态变化时的回调 - */ - onVisibleChange?: (visible: boolean, reason: string) => void; - - /** - * 弹层自定义样式 - */ - popupStyle?: React.CSSProperties; - - /** - * 弹层自定义样式类 - */ - popupClassName?: string; - - /** - * 弹层属性 - */ - popupProps?: PopupProps; - - /** - * 是否禁用 - */ - disabled?: boolean; - - /** - * 输入框是否有边框 - */ - hasBorder?: boolean; - - /** - * 透传给 Input 的属性 - */ - inputProps?: InputProps; - - /** - * 是否为预览态 - */ - isPreview?: boolean; - - /** - * 预览态模式下渲染的内容 - * @param value 时间 - */ - renderPreview?: (value: StateValue | [StateValue, StateValue]) => React.ReactNode; - - /** - * 预设值,会显示在时间面板下面 - */ - ranges?: RangePreset | DatePreset[]; - - /** - * 时间值改变时的回调 - */ - onChange?: (date: Dayjs, dateString: string) => void; -} - -export interface RangePickerProps - extends Omit< - TimePickerProps, - 'value' | 'placeholder' | 'defaultValue' | 'onOk' | 'disabled' | 'onChange' | 'preset' - > { - value?: Array; - defaultValue?: Array; - onOk?: (value: Array, strVal: Array) => void; - onChange?: (value: Array, strVal: Array) => void; - placeholder?: string | Array; - disabled?: boolean | boolean[]; - preset?: IPresetType | IPresetType[]; -} - -export class RangePicker extends React.Component { - type: 'range'; -} - -export default class TimePicker extends React.Component { - static RangePicker: typeof RangePicker; -} diff --git a/components/time-picker2/index.jsx b/components/time-picker2/index.jsx deleted file mode 100644 index e2323d83bd..0000000000 --- a/components/time-picker2/index.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import ConfigProvider from '../config-provider'; -import TimePicker from './time-picker'; - -const ConfigTimePicker = ConfigProvider.config(TimePicker); - -ConfigTimePicker.RangePicker = React.forwardRef((props, ref) => ); -ConfigTimePicker.RangePicker.displayName = 'RangePicker'; - -export default ConfigTimePicker; diff --git a/components/time-picker2/index.tsx b/components/time-picker2/index.tsx new file mode 100644 index 0000000000..d5f8d2f939 --- /dev/null +++ b/components/time-picker2/index.tsx @@ -0,0 +1,21 @@ +import React, { type ComponentRef, type LegacyRef } from 'react'; + +import { assignSubComponent } from '../util/component'; +import ConfigProvider from '../config-provider'; +import TimePicker from './time-picker'; +import type { TimePickerProps, ValueType, RangePickerProps, PresetType } from './types'; + +const ConfigTimePicker = ConfigProvider.config(TimePicker); + +const TimePickerWithSub = assignSubComponent(TimePicker, { + RangePicker: React.forwardRef( + (props: TimePickerProps, ref: LegacyRef>) => ( + + ) + ), +}); +TimePickerWithSub.RangePicker.displayName = 'RangePicker'; + +export default TimePickerWithSub; + +export type { TimePickerProps, ValueType, RangePickerProps, PresetType }; diff --git a/components/time-picker2/mobile/index.jsx b/components/time-picker2/mobile/index.tsx similarity index 100% rename from components/time-picker2/mobile/index.jsx rename to components/time-picker2/mobile/index.tsx diff --git a/components/time-picker2/module/date-input.jsx b/components/time-picker2/module/date-input.tsx similarity index 70% rename from components/time-picker2/module/date-input.jsx rename to components/time-picker2/module/date-input.tsx index 1605dd7383..04bf518391 100644 --- a/components/time-picker2/module/date-input.jsx +++ b/components/time-picker2/module/date-input.tsx @@ -2,15 +2,17 @@ import React from 'react'; import { polyfill } from 'react-lifecycles-compat'; import PT from 'prop-types'; import classnames from 'classnames'; +import { type Dayjs } from 'dayjs'; + import SharedPT from '../prop-types'; import { TIME_INPUT_TYPE } from '../constant'; import { func, datejs, obj } from '../../util'; import { fmtValue } from '../../date-picker2/util'; - -import Input from '../../input'; +import Input, { type InputProps } from '../../input'; import Icon from '../../icon'; +import type { DateInputProps } from '../types'; -class DateInput extends React.Component { +class DateInput extends React.Component { static propTypes = { prefix: PT.string, rtl: PT.bool, @@ -44,45 +46,50 @@ class DateInput extends React.Component { size: 'medium', }; - constructor(props) { + prefixCls: string; + input?: InstanceType | InstanceType[]; + + constructor(props: DateInputProps) { super(props); this.prefixCls = `${props.prefix}time-picker2-input`; } - setInputRef = (el, index) => { + setInputRef = (el: InstanceType, index?: number) => { if (this.props.isRange) { if (!this.input) { this.input = []; } - this.input[index] = el; + (this.input as (InstanceType | undefined)[])[index!] = el; } else { this.input = el; } }; - setValue = v => { + setValue = (v: string | number | null) => { const { isRange, inputType, value } = this.props; - let newVal = v; + let newVal: string | number | null | Array = v; if (isRange) { - newVal = [...value]; - newVal[inputType] = v; + newVal = [...value!]; + newVal[inputType!] = v; } return newVal; }; - formatter = v => { + formatter = (v: Dayjs) => { const { format } = this.props; return typeof format === 'function' ? format(v) : v.format(format); }; - onInput = (v, e, eventType) => { + onInput: InputProps['onChange'] = (v, e, eventType) => { + // @ts-expect-error v 的类型是 string | number,this.setValue(v) 返回类型是 string | number | null,此处不应该重新赋值,应该定一个新的变量处理 v = this.setValue(v); if (eventType === 'clear') { + // @ts-expect-error v 的类型是 string | number,this.setValue(v) 返回类型是 string | number | null,此处不应该重新赋值,应该定一个新的变量处理 v = null; e.stopPropagation(); } @@ -90,7 +97,7 @@ class DateInput extends React.Component { func.invoke(this.props, 'onInput', [v, eventType]); }; - handleTypeChange = inputType => { + handleTypeChange = (inputType: number) => { if (inputType !== this.props.inputType) { func.invoke(this.props, 'onInputTypeChange', [inputType]); } @@ -98,7 +105,7 @@ class DateInput extends React.Component { getPlaceholder = () => { const { isRange } = this.props; - let holder = this.props.placeholder; + let holder: string | string[] | undefined = this.props.placeholder; if (isRange && !Array.isArray(holder)) { holder = Array(2).fill(holder); @@ -116,7 +123,7 @@ class DateInput extends React.Component { let size = 0; if (isRange) { - const fmtStr = fmtValue([value, value].map(datejs), format); + const fmtStr = fmtValue([value, value].map(datejs), format) as string[]; size = Math.max(...fmtStr.map(s => (s && s.length) || 0)); } else { const fmtStr = fmtValue(datejs(value), format); @@ -153,7 +160,9 @@ class DateInput extends React.Component { const placeholder = this.getPlaceholder(); const htmlSize = this.getHtmlSize(); - const sharedProps = { + // @ts-expect-error 下面 pickProps 使用错误,导致报错 + const sharedProps: InputProps = { + // @ts-expect-error 正确写法应该是 obj.pickProps(Input.propTypes, restProps) ...obj.pickProps(restProps, Input), ...inputProps, size, @@ -167,7 +176,7 @@ class DateInput extends React.Component { onKeyDown: onKeyDown, }; - let rangeProps; + let rangeProps: InputProps[]; if (isRange) { rangeProps = [TIME_INPUT_TYPE.BEGIN, TIME_INPUT_TYPE.END].map(idx => { const _disabled = Array.isArray(disabled) ? disabled[idx] : disabled; @@ -175,10 +184,10 @@ class DateInput extends React.Component { return { ...sharedProps, autoFocus, - placeholder: placeholder[idx], - value: value[idx] || '', + placeholder: placeholder![idx], + value: value![idx] || '', disabled: _disabled, - ref: ref => setInputRef(ref, idx), + ref: (ref: InstanceType) => setInputRef(ref, idx), onFocus: _disabled ? undefined : () => handleTypeChange(idx), className: classnames({ [`${prefixCls}-active`]: inputType === idx, @@ -192,29 +201,32 @@ class DateInput extends React.Component { { [`${prefixCls}-focus`]: focus, [`${prefixCls}-noborder`]: !hasBorder, - [`${prefixCls}-disabled`]: isRange && Array.isArray(disabled) ? disabled.every(v => v) : disabled, + [`${prefixCls}-disabled`]: + isRange && Array.isArray(disabled) ? disabled.every(v => v) : disabled, [`${prefixCls}-error`]: state === 'error', } ); - const calendarIcon = ; + const calendarIcon = ( + + ); return (
{isRange ? (
{separator}
@@ -223,12 +235,12 @@ class DateInput extends React.Component { {...sharedProps} label={label} state={state} - disabled={disabled} - hasClear={!state && hasClear} - placeholder={placeholder} + disabled={disabled as boolean} + hasClear={!state && hasClear!} + placeholder={placeholder as string} autoFocus={autoFocus} // eslint-disable-line jsx-a11y/no-autofocus ref={setInputRef} - value={value || ''} + value={(value as string) || ''} hint={state ? null : calendarIcon} /> )} diff --git a/components/time-picker2/module/time-menu.jsx b/components/time-picker2/module/time-menu.tsx similarity index 72% rename from components/time-picker2/module/time-menu.jsx rename to components/time-picker2/module/time-menu.tsx index 2823be07df..de6dd016a5 100644 --- a/components/time-picker2/module/time-menu.jsx +++ b/components/time-picker2/module/time-menu.tsx @@ -2,11 +2,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { checkDayjsObj } from '../utils'; +import type { TimeMenuListItem, TimeMenuProps } from '../types'; -function scrollTo(element, to, duration) { +function scrollTo(element: HTMLElement, to: number, duration: number) { const requestAnimationFrame = window.requestAnimationFrame || - function requestAnimationFrameTimeout(...params) { + function requestAnimationFrameTimeout(...params: [() => void]) { return setTimeout(params[0], 10); }; @@ -31,7 +32,7 @@ function scrollTo(element, to, duration) { const noop = () => {}; -class TimeMenu extends React.Component { +class TimeMenu extends React.Component { static propTypes = { prefix: PropTypes.string, title: PropTypes.node, @@ -48,40 +49,41 @@ class TimeMenu extends React.Component { static defaultProps = { step: 1, disabledItems: () => false, - renderTimeMenuItems: list => list, + renderTimeMenuItems: (list: unknown) => list, onSelect: () => {}, disabled: false, }; + menu: HTMLUListElement | null; + menuWrapper: HTMLDivElement | null; + prefixCls = `${this.props.prefix}time-picker2`; componentDidMount() { this.scrollToSelected(0); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: TimeMenuProps) { if (prevProps.activeIndex !== this.props.activeIndex) { this.scrollToSelected(120); } } - prefixCls = `${this.props.prefix}time-picker2`; - scrollToSelected(duration = 0) { const { activeIndex, step } = this.props; - const targetIndex = Math.floor((activeIndex || 0) / step); - const firstItem = this.menu.children[targetIndex]; + const targetIndex = Math.floor((activeIndex || 0) / step!); + const firstItem = this.menu!.children[targetIndex] as HTMLElement; const offsetTo = firstItem.offsetTop; - scrollTo(this.menu, offsetTo, duration); + scrollTo(this.menu!, offsetTo, duration); } - _menuRefHandler = ref => { + _menuRefHandler = (ref: HTMLUListElement | null) => { this.menu = ref; }; - _menuWrapperRefHandler = ref => { + _menuWrapperRefHandler = (ref: HTMLDivElement | null) => { this.menuWrapper = ref; }; - createMenuItems = list => { + createMenuItems = (list: Array) => { const { prefix, mode, @@ -92,21 +94,22 @@ class TimeMenu extends React.Component { renderTimeMenuItems, value: timeValue, } = this.props; - list = renderTimeMenuItems(list, mode, timeValue) || list; + list = renderTimeMenuItems!(list, mode, timeValue) || list; return list.map(({ label, value }) => { - const isDisabled = disabled || disabledItems(value); + const isDisabled = disabled || disabledItems!(value); const itemCls = classnames({ [`${this.prefixCls}-menu-item`]: true, [`${prefix}disabled`]: isDisabled, [`${prefix}selected`]: value === activeIndex, }); - const onClick = isDisabled ? noop : () => onSelect(value, mode); + const onClick = isDisabled ? noop : () => onSelect!(value, mode); return (
  • {/* {menuTitle} */} -
      +
        {this.createMenuItems(list)}
  • diff --git a/components/time-picker2/panel.jsx b/components/time-picker2/panel.tsx similarity index 63% rename from components/time-picker2/panel.jsx rename to components/time-picker2/panel.tsx index 5396aef9f4..2119743a82 100644 --- a/components/time-picker2/panel.jsx +++ b/components/time-picker2/panel.tsx @@ -1,78 +1,30 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; +import type { Dayjs } from 'dayjs'; + import nextLocale from '../locale/zh-cn'; import { func, datejs, pickAttrs } from '../util'; import TimeMenu from './module/time-menu'; import SharedPT from './prop-types'; +import type { PannelType, PanelProps, DisabledItems } from './types'; const { noop } = func; -class TimePickerPanel extends Component { +class TimePickerPanel extends Component { static propTypes = { prefix: PropTypes.string, - /** - * 时间值(dayjs 对象) - */ value: SharedPT.value, - /** - * 是否显示小时 - */ showHour: PropTypes.bool, - /** - * 是否显示分钟 - */ showMinute: PropTypes.bool, - /** - * 是否显示秒 - */ showSecond: PropTypes.bool, - /** - * 小时选项步长 - */ hourStep: PropTypes.number, - /** - * 分钟选项步长 - */ minuteStep: PropTypes.number, - /** - * 秒钟选项步长 - */ secondStep: PropTypes.number, - /** - * 禁用小时函数 - * @param {Number} index 时 0 - 23 - * @return {Boolean} 是否禁用 - */ disabledHours: PropTypes.func, - /** - * 禁用分钟函数 - * @param {Number} index 分 0 - 59 - * @return {Boolean} 是否禁用 - */ disabledMinutes: PropTypes.func, - /** - * 禁用秒函数 - * @param {Number} index 秒 0 - 59 - * @return {Boolean} 是否禁用 - */ disabledSeconds: PropTypes.func, - /** - * 渲染的可选择时间列表 - * [{ - * label: '01', - * value: 1 - * }] - * @param {Array} list 默认渲染的列表 - * @param {String} mode 渲染的菜单 hour, minute, second - * @param {dayjs} value 当前时间,可能为 null - * @return {Array} 返回需要渲染的数据 - */ renderTimeMenuItems: PropTypes.func, - /** - * 选择某个日期值时的回调 - * @param {Object} 选中后的日期值 - */ onSelect: PropTypes.func, isRange: PropTypes.bool, locale: PropTypes.object, @@ -98,11 +50,15 @@ class TimePickerPanel extends Component { /** * - * @param {enum} panelType 'start' | 'end' | 'panel' - * @param {*} index - * @param {*} type 'hour' | 'minute' | 'second' + * @param panelType - 'start' | 'end' | 'panel' + * @param index - number + * @param type - 'hour' | 'minute' | 'second' */ - onSelectMenuItem = (panelType, index, type) => { + onSelectMenuItem = ( + panelType: PannelType, + index: number, + type: 'hour' | 'minute' | 'second' + ) => { const { value, isRange } = this.props; const valueArr = Array.isArray(value) ? value : [value]; const val = panelType === 'end' ? valueArr[1] : valueArr[0]; @@ -125,9 +81,9 @@ class TimePickerPanel extends Component { const nextValueArray = []; if (panelType === 'start') { nextValueArray[0] = newValue; - nextValueArray[1] = value[1]; + nextValueArray[1] = (value as Dayjs[])[1]; } else if (panelType === 'end') { - nextValueArray[0] = value[0]; + nextValueArray[0] = (value as Dayjs[])[0]; nextValueArray[1] = newValue; } @@ -137,7 +93,7 @@ class TimePickerPanel extends Component { } }; - getDisabledItems = () => { + getDisabledItems: () => DisabledItems = () => { const { disabledHours, disabledMinutes, disabledSeconds, value, isRange } = this.props; const disableds = { @@ -146,28 +102,36 @@ class TimePickerPanel extends Component { newDisabledSeconds: [disabledSeconds], }; if (!isRange) { - return disableds; + return disableds as DisabledItems; } - const dHours = disabledHours() || []; - const dMinutes = disabledMinutes() || []; - const dSeconds = disabledSeconds() || []; + const dHours = (disabledHours!() || []) as number[]; + const dMinutes = (disabledMinutes!() || []) as number[]; + const dSeconds = (disabledSeconds!() || []) as number[]; - const v0 = value[0]; - const v1 = value[1]; + // fixme: value 可以换为 valueArr + const v0 = (value as Dayjs[])[0]; + const v1 = (value as Dayjs[])[1]; const hoursEqual = () => v0 && v1 && v0.hour() === v1.hour(); - const minutesEqual = () => v0 && v1 && v0.hour() === v1.hour() && v0.minute() === v1.minute(); + const minutesEqual = () => + v0 && v1 && v0.hour() === v1.hour() && v0.minute() === v1.minute(); - disableds.newDisabledHours[0] = h => (v1 && h > v1.hour()) || dHours.indexOf(h) > -1; - disableds.newDisabledMinutes[0] = m => (v1 && (hoursEqual() && m > v1.minute())) || dMinutes.indexOf(m) > -1; - disableds.newDisabledSeconds[0] = s => (v1 && (minutesEqual() && s > v1.second())) || dSeconds.indexOf(s) > -1; + disableds.newDisabledHours[0] = (h: number) => + (v1 && h > v1.hour()) || dHours.indexOf(h) > -1; + disableds.newDisabledMinutes[0] = (m: number) => + (v1 && hoursEqual() && m > v1.minute()) || dMinutes.indexOf(m) > -1; + disableds.newDisabledSeconds[0] = (s: number) => + (v1 && minutesEqual() && s > v1.second()) || dSeconds.indexOf(s) > -1; - disableds.newDisabledHours[1] = h => (v0 && h < v0.hour()) || dHours.indexOf(h) > -1; - disableds.newDisabledMinutes[1] = m => (v0 && m < (hoursEqual() && v0.minute())) || dMinutes.indexOf(m) > -1; - disableds.newDisabledSeconds[1] = s => (v0 && (minutesEqual() && s < v0.second())) || dSeconds.indexOf(s) > -1; + disableds.newDisabledHours[1] = (h: number) => + (v0 && h < v0.hour()) || dHours.indexOf(h) > -1; + disableds.newDisabledMinutes[1] = (m: number) => + (v0 && m < ((hoursEqual() && v0.minute()) as number)) || dMinutes.indexOf(m) > -1; + disableds.newDisabledSeconds[1] = (s: number) => + (v0 && minutesEqual() && s < v0.second()) || dSeconds.indexOf(s) > -1; - return disableds; + return disableds as DisabledItems; }; render() { @@ -198,9 +162,9 @@ class TimePickerPanel extends Component { className ); - const activeHour = []; - const activeMinute = []; - const activeSecond = []; + const activeHour: number[] = []; + const activeMinute: number[] = []; + const activeSecond: number[] = []; const valueArr = Array.isArray(value) ? value : [value]; valueArr.forEach((val, i) => { @@ -217,19 +181,23 @@ class TimePickerPanel extends Component { renderTimeMenuItems, }; - const { newDisabledHours, newDisabledMinutes, newDisabledSeconds } = this.getDisabledItems(); + const { newDisabledHours, newDisabledMinutes, newDisabledSeconds } = + this.getDisabledItems(); - const generatePanel = index => ( + const generatePanel = (index: number) => ( {showHour ? ( ) : null} @@ -238,10 +206,13 @@ class TimePickerPanel extends Component { {...commonProps} value={valueArr[index]} activeIndex={activeMinute[index]} - title={locale.minute} + title={locale!.minute} mode="minute" step={minuteStep} - onSelect={this.onSelectMenuItem.bind(this, `${index === 0 ? 'start' : 'end'}`)} + onSelect={this.onSelectMenuItem.bind( + this, + `${index === 0 ? 'start' : 'end'}` + )} disabledItems={newDisabledMinutes[index]} /> ) : null} @@ -250,10 +221,13 @@ class TimePickerPanel extends Component { {...commonProps} value={valueArr[index]} activeIndex={activeSecond[index]} - title={locale.second} + title={locale!.second} step={secondStep} mode="second" - onSelect={this.onSelectMenuItem.bind(this, `${index === 0 ? 'start' : 'end'}`)} + onSelect={this.onSelectMenuItem.bind( + this, + `${index === 0 ? 'start' : 'end'}` + )} disabledItems={newDisabledSeconds[index]} /> ) : null} @@ -262,7 +236,10 @@ class TimePickerPanel extends Component { const singlePanel = generatePanel(0); - const panelClassNames = classnames(`${this.prefixCls}-panel-col-${colLen}`, `${this.prefixCls}-panel-list`); + const panelClassNames = classnames( + `${this.prefixCls}-panel-col-${colLen}`, + `${this.prefixCls}-panel-list` + ); const doublePanel = ( diff --git a/components/time-picker2/prop-types.js b/components/time-picker2/prop-types.ts similarity index 65% rename from components/time-picker2/prop-types.js rename to components/time-picker2/prop-types.ts index 9f16e729da..a63f9d9dd2 100644 --- a/components/time-picker2/prop-types.js +++ b/components/time-picker2/prop-types.ts @@ -1,12 +1,14 @@ import PT from 'prop-types'; +import type { ConfigType } from 'dayjs'; + import { TIME_PICKER_TYPE, TIME_INPUT_TYPE } from './constant'; import { datejs } from '../util'; -export const error = (propName, ComponentName) => +export const error = (propName: string, ComponentName: string) => new Error(`Invalid prop ${propName} supplied to ${ComponentName}. Validation failed.`); -function checkType(type) { - return (props, propName, componentName) => { +function checkType(type: string | string[]) { + return (props: Record, propName: string, componentName: string) => { let value = props[propName]; if (value) { if (!Array.isArray(value)) { @@ -17,7 +19,7 @@ function checkType(type) { type = [type]; } - if (!value.every(v => type.includes(typeof v))) { + if (!(value as unknown[]).every(v => type.includes(typeof v))) { throw error(propName, componentName); } } @@ -25,12 +27,12 @@ function checkType(type) { } const SharedPT = { - date(props, propName, componentName) { - if (propName in props && !datejs(props.propName).isValid()) { + date(props: Record, propName: string, componentName: string) { + if (propName in props && !datejs(props.propName as ConfigType).isValid()) { throw error(propName, componentName); } }, - value(props, propName, componentName) { + value(props: Record, propName: string, componentName: string) { if (props[propName]) { let value = props[propName]; @@ -40,7 +42,11 @@ const SharedPT = { value = [value]; } - if (!value.every(v => !v || datejs(v).isValid() || typeof v === 'string')) { + if ( + !(value as unknown[]).every( + v => !v || datejs(v as ConfigType).isValid() || typeof v === 'string' + ) + ) { throw error(propName, componentName); } } diff --git a/components/time-picker2/style.js b/components/time-picker2/style.ts similarity index 100% rename from components/time-picker2/style.js rename to components/time-picker2/style.ts diff --git a/components/time-picker2/time-picker.jsx b/components/time-picker2/time-picker.tsx similarity index 68% rename from components/time-picker2/time-picker.jsx rename to components/time-picker2/time-picker.tsx index 882631ae13..04ebed5587 100644 --- a/components/time-picker2/time-picker.jsx +++ b/components/time-picker2/time-picker.tsx @@ -1,7 +1,9 @@ -import React, { Component } from 'react'; +import React, { Component, type KeyboardEvent } from 'react'; import PropTypes from 'prop-types'; import { polyfill } from 'react-lifecycles-compat'; import classnames from 'classnames'; +import type { Dayjs } from 'dayjs'; + import ConfigProvider from '../config-provider'; import Input from '../input'; import Button from '../button'; @@ -15,6 +17,13 @@ import { switchInputType, fmtValue, isValueChanged } from '../date-picker2/util' import FooterPanel from '../date-picker2/panels/footer-panel'; import DateInput from './module/date-input'; import { TIME_PICKER_TYPE, TIME_INPUT_TYPE } from './constant'; +import type { + DateInputProps, + TimePickerProps, + TimePickerState, + ValueType, + InputType, +} from './types'; const { Popup } = Overlay; const { noop, checkDate, checkRangeDate } = func; @@ -26,160 +35,48 @@ const presetPropType = PropTypes.shape({ ...Button.propTypes, }); -class TimePicker2 extends Component { +type SharedInputProps = DateInputProps & { + ref: (el: InstanceType) => void; +}; +class TimePicker2 extends Component { static propTypes = { ...ConfigProvider.propTypes, prefix: PropTypes.string, rtl: PropTypes.bool, - /** - * 按钮的文案 - */ label: PropTypes.node, - /** - * 输入框状态 - */ state: PropTypes.oneOf(['error', 'success']), - /** - * 输入框提示 - */ placeholder: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.string), PropTypes.string]), - /** - * 时间值,dayjs格式或者2020-01-01字符串格式,受控状态使用 - */ value: SharedPT.value, - /** - * 时间初值,dayjs格式或者2020-01-01字符串格式,非受控状态使用 - */ defaultValue: SharedPT.value, - /** - * 时间选择框的尺寸 - */ size: PropTypes.oneOf(['small', 'medium', 'large']), - /** - * 是否允许清空时间 - */ hasClear: PropTypes.bool, - /** - * 时间的格式 - * https://dayjs.gitee.io/docs/zh-CN/display/format - */ format: PropTypes.string, - /** - * 小时选项步长 - */ hourStep: PropTypes.number, - /** - * 分钟选项步长 - */ minuteStep: PropTypes.number, - /** - * 秒钟选项步长 - */ secondStep: PropTypes.number, - /** - * 禁用小时函数 - * @param {Number} index 时 0 - 23 - * @return {Boolean} 是否禁用 - */ disabledHours: PropTypes.func, - /** - * 禁用分钟函数 - * @param {Number} index 分 0 - 59 - * @return {Boolean} 是否禁用 - */ disabledMinutes: PropTypes.func, - /** - * 禁用秒钟函数 - * @param {Number} index 秒 0 - 59 - * @return {Boolean} 是否禁用 - */ disabledSeconds: PropTypes.func, - /** - * 渲染的可选择时间列表 - * [{ - * label: '01', - * value: 1 - * }] - * @param {Array} list 默认渲染的列表 - * @param {String} mode 渲染的菜单 hour, minute, second - * @param {dayjs} value 当前时间,可能为 null - * @return {Array} 返回需要渲染的数据 - */ renderTimeMenuItems: PropTypes.func, - /** - * 弹层是否显示(受控) - */ visible: PropTypes.bool, - /** - * 弹层默认是否显示(非受控) - */ defaultVisible: PropTypes.bool, - /** - * 弹层容器 - * @param {Object} target 目标节点 - * @return {ReactNode} 容器节点 - */ popupContainer: PropTypes.any, - /** - * 弹层对齐方式, 详情见Overlay 文档 - */ popupAlign: PropTypes.string, - /** - * 弹层触发方式 - */ popupTriggerType: PropTypes.oneOf(['click', 'hover']), - /** - * 弹层展示状态变化时的回调 - * @param {Boolean} visible 弹层是否隐藏和显示 - * @param {String} type 触发弹层显示和隐藏的来源 fromTrigger 表示由trigger的点击触发; docClick 表示由document的点击触发 - */ onVisibleChange: PropTypes.func, - /** - * 弹层自定义样式 - */ popupStyle: PropTypes.object, - /** - * 弹层自定义样式类 - */ popupClassName: PropTypes.string, - /** - * 弹层属性 - */ popupProps: PropTypes.object, - /** - * 是否跟随滚动 - */ followTrigger: PropTypes.bool, - /** - * 是否禁用 - */ disabled: PropTypes.bool, - /** - * 输入框是否有边框 - */ hasBorder: PropTypes.bool, - /** - * 是否为预览态 - */ isPreview: PropTypes.bool, - /** - * 预览态模式下渲染的内容 - * @param {DayjsObject|DayjsObject[]} value 时间 - */ renderPreview: PropTypes.func, - /** - * 时间值改变时的回调 - * @param {DayjsObject} date dayjs时间对象 - * @param {Object|String} dateString 时间对象或时间字符串 - */ onChange: PropTypes.func, className: PropTypes.string, name: PropTypes.string, - /** - * 预设值,会显示在时间面板下面 - */ preset: PropTypes.oneOfType([PropTypes.arrayOf(presetPropType), presetPropType]), - inputProps: PropTypes.shape(Input.propTypes), + inputProps: PropTypes.shape(Input.propTypes!), popupComponent: PropTypes.elementType, type: PropTypes.oneOf(['time', 'range']), }; @@ -200,8 +97,12 @@ class TimePicker2 extends Component { onVisibleChange: noop, }; - constructor(props, context) { - super(props, context); + prefixCls: string; + dateInput: InstanceType; + clearTimeoutId: number; + + constructor(props: TimePickerProps) { + super(props); const isRange = props.type === TIME_PICKER_TYPE.RANGE; this.state = {}; @@ -221,10 +122,10 @@ class TimePicker2 extends Component { this.state = { ...this.state, isRange, - inputStr: '', // 输入框的输入值, string类型 - value, // 确定值 dayjs类型 - curValue: value, // 临时值 dayjs类型 - preValue: value, // 上个值 dayjs类型 + inputStr: '', // 输入框的输入值,string 类型 + value, // 确定值 dayjs 类型 + curValue: value, // 临时值 dayjs 类型 + preValue: value, // 上个值 dayjs 类型 inputValue: fmtValue(value, format), inputing: false, visible: 'visible' in this.props ? visible : defaultVisible, @@ -232,22 +133,23 @@ class TimePicker2 extends Component { this.prefixCls = `${prefix}time-picker2`; } - static getDerivedStateFromProps(props, prevState) { + static getDerivedStateFromProps(props: TimePickerProps, prevState: TimePickerState) { const { disabled, type, format, value: propsValue } = props; const isRange = type === TIME_PICKER_TYPE.RANGE; - let state = { + let state: TimePickerState = { isRange, }; if ('value' in props) { // checkDate function doesn't support hh:mm:ss format, convert to valid dayjs value in advance - const formatter = v => (typeof v === 'string' ? datejs(v, format) : v); - const formattedValue = Array.isArray(propsValue) + const formatter = (v: string | Dayjs | null) => + typeof v === 'string' ? datejs(v, format) : v; + const formattedValue: ValueType = Array.isArray(propsValue) ? propsValue.map(v => formatter(v)) - : formatter(propsValue); + : formatter(propsValue!); const value = isRange - ? checkRangeDate(formattedValue, state.inputType, disabled) - : checkDate(formattedValue); + ? checkRangeDate(formattedValue, state.inputType!, disabled) + : checkDate(formattedValue as Dayjs | null); if (isValueChanged(value, state.preValue)) { state = { ...state, @@ -275,38 +177,47 @@ class TimePicker2 extends Component { const { props } = this; const { type, value, defaultValue } = props; - let val = type === TIME_PICKER_TYPE.RANGE ? [null, null] : null; + let val: TimePickerProps['value'] = type === TIME_PICKER_TYPE.RANGE ? [null, null] : null; - val = 'value' in props ? value : 'defaultValue' in props ? defaultValue : val; + val = 'value' in props ? value! : 'defaultValue' in props ? defaultValue! : val; return this.checkValue(val); }; /** * 获取 RangePicker 输入框初始输入状态 - * @returns {Object} inputState - * @returns {boolean} inputState.justBeginInput 是否初始输入 - * @returns {number} inputState.inputType 当前输入框 + * @returns inputState - Object + * @returns inputState.justBeginInput 是否初始输入 - boolean + * @returns inputState.inputType 当前输入框 - number */ getInitRangeInputState = () => { return { justBeginInput: this.isEnabled(), - inputType: this.isEnabled(0) ? TIME_INPUT_TYPE.BEGIN : TIME_INPUT_TYPE.END, + inputType: (this.isEnabled(0) + ? TIME_INPUT_TYPE.BEGIN + : TIME_INPUT_TYPE.END) as InputType, }; }; - onKeyDown = e => { - if (e.keyCode === KEYCODE.ENTER) { + onKeyDown = (e: KeyboardEvent) => { + if (e!.keyCode === KEYCODE.ENTER) { const { inputValue } = this.state; - this.handleChange(inputValue, 'KEYDOWN_ENTER'); + this.handleChange(inputValue!, 'KEYDOWN_ENTER'); this.onClick(); return; } const { value, inputStr, inputType, isRange } = this.state; - const { format, hourStep = 1, minuteStep = 1, secondStep = 1, disabledMinutes, disabledSeconds } = this.props; + const { + format, + hourStep = 1, + minuteStep = 1, + secondStep = 1, + disabledMinutes, + disabledSeconds, + } = this.props; - let unit = 'second'; + let unit: 'hour' | 'minute' | 'second' = 'second'; if (disabledSeconds) { unit = disabledMinutes ? 'hour' : 'minute'; @@ -315,35 +226,36 @@ class TimePicker2 extends Component { const timeStr = onTimeKeydown( e, { - format, - timeInputStr: isRange ? inputStr[inputType] : inputStr, + format: format!, + timeInputStr: isRange ? inputStr![inputType!] : (inputStr as string), steps: { hour: hourStep, minute: minuteStep, second: secondStep, }, + // @ts-expect-error 此处的 value 没有考虑到数组的情况 value, }, unit ); if (!timeStr) return; - let newInputStr = timeStr; + let newInputStr: string | string[] | undefined = timeStr; if (isRange) { - newInputStr = inputStr; - newInputStr[inputType] = timeStr; + newInputStr = inputStr as string[]; + newInputStr[inputType!] = timeStr; } - this.handleChange(newInputStr, 'KEYDOWN_CODE'); + this.handleChange(newInputStr!, 'KEYDOWN_CODE'); }; - onVisibleChange = (visible, type) => { + onVisibleChange = (visible: boolean, type?: string) => { if (!('visible' in this.props)) { this.setState({ visible, }); } - this.props.onVisibleChange(visible, type); + this.props.onVisibleChange!(visible, type); }; onClick = () => { @@ -351,37 +263,37 @@ class TimePicker2 extends Component { if (!visible) { this.onVisibleChange(true); - this.handleInputFocus(inputType); + this.handleInputFocus(inputType!); } }; /** * 处理点击文档区域导致的弹层收起逻辑 - * @param {boolean} visible 是否可见 - * @param {string} type 事件类型 + * @param visible - 是否可见 + * @param type - 事件类型 */ - handleVisibleChange = (visible, targetType) => { + handleVisibleChange = (visible: boolean, targetType: string) => { if (targetType === 'docClick') { // 弹层收起 触发 Change 逻辑 if (!visible) { - this.handleChange(this.state.curValue, 'VISIBLE_CHANGE'); + this.handleChange(this.state.curValue!, 'VISIBLE_CHANGE'); } this.onVisibleChange(visible); } }; - handleInputFocus = inputType => { + handleInputFocus = (inputType: number) => { let inputEl = this.dateInput && this.dateInput.input; if (this.state.isRange) { - inputEl = inputEl && inputEl[inputType]; + inputEl = inputEl && (inputEl as InstanceType[])[inputType]; } - inputEl && inputEl.focus(); + inputEl && (inputEl as InstanceType).focus(); }; onOk = () => { const { curValue } = this.state; - const checkedValue = this.checkValue(curValue); + const checkedValue = this.checkValue(curValue!); func.invoke(this.props, 'onOk', this.getOutputArgs(checkedValue)); @@ -389,7 +301,7 @@ class TimePicker2 extends Component { this.handleChange(checkedValue, 'CLICK_OK'); }; - onInputTypeChange = idx => { + onInputTypeChange = (idx: InputType) => { const { inputType, visible } = this.state; if (idx !== inputType) { @@ -400,34 +312,40 @@ class TimePicker2 extends Component { } }; - checkValue = (value, strictly) => { + checkValue: (value: TimePickerProps['value'], strictly?: boolean) => ValueType = ( + value, + strictly + ) => { const { inputType } = this.state; const { format, type, disabled } = this.props; - const formatter = v => (typeof v === 'string' ? datejs(v, format) : v); - const formattedValue = Array.isArray(value) ? value.map(v => formatter(v)) : formatter(value); + const formatter = (v: Dayjs | null | string) => + typeof v === 'string' ? datejs(v, format) : v; + const formattedValue: ValueType = Array.isArray(value) + ? value.map(v => formatter(v)) + : formatter(value!); return type === TIME_PICKER_TYPE.RANGE - ? checkRangeDate(formattedValue, inputType, disabled, strictly) - : checkDate(formattedValue); + ? checkRangeDate(formattedValue, inputType!, disabled, strictly) + : checkDate(formattedValue as Dayjs | string | null); }; /** * 获取 `onChange` 和 `onOk` 方法的输出参数 - * @param {Dayjs} value + * @param value - Dayjs * @returns 默认返回 `Dayjs` 实例和 `format` 格式化的值 * 如果传了了 `outputFormat` 属性则返回 `outputFormat` 格式化的值 */ - getOutputArgs = value => { + getOutputArgs = (value: ValueType) => { const { format } = this.props; return [value, fmtValue(value, format)]; }; - onChange = v => { + onChange = (v: ValueType) => { const { state, props } = this; const { format } = props; const nextValue = v === undefined ? state.value : v; - const value = this.checkValue(nextValue); + const value = this.checkValue(nextValue!); this.setState({ curValue: value, @@ -436,15 +354,15 @@ class TimePicker2 extends Component { inputValue: fmtValue(value, format), }); - func.invoke(this.props, 'onChange', this.getOutputArgs(nextValue)); + func.invoke(this.props, 'onChange', this.getOutputArgs(nextValue!)); }; - shouldSwitchInput = value => { + shouldSwitchInput = (value: (Dayjs | null)[]) => { const { inputType, justBeginInput } = this.state; const idx = justBeginInput ? switchInputType(inputType) : value.indexOf(null); if (idx !== -1 && this.isEnabled(idx)) { - this.onInputTypeChange(idx); + this.onInputTypeChange(idx as InputType); this.handleInputFocus(idx); return true; } @@ -452,19 +370,25 @@ class TimePicker2 extends Component { return false; }; - handleChange = (v, eventType) => { + handleChange = (v: ValueType | string | null | string[], eventType?: string) => { const { format } = this.props; const { isRange, value, preValue } = this.state; - const forceEvents = ['KEYDOWN_ENTER', 'CLICK_PRESET', 'CLICK_OK', 'INPUT_CLEAR', 'VISIBLE_CHANGE']; - const isTemporary = isRange && !forceEvents.includes(eventType); + const forceEvents = [ + 'KEYDOWN_ENTER', + 'CLICK_PRESET', + 'CLICK_OK', + 'INPUT_CLEAR', + 'VISIBLE_CHANGE', + ]; + const isTemporary = isRange && !forceEvents.includes(eventType!); // 面板收起时候,将值设置为确认值 - v = eventType === 'VISIBLE_CHANGE' ? value : this.checkValue(v, !isTemporary); + v = eventType === 'VISIBLE_CHANGE' ? value! : this.checkValue(v, !isTemporary); const stringV = fmtValue(v, format); this.setState({ - curValue: v, + curValue: v as ValueType | null, inputStr: stringV, inputValue: stringV, inputing: false, @@ -482,16 +406,20 @@ class TimePicker2 extends Component { // 2. 非 选择预设时间、面板收起、清空输入 操作 // 3. 不需要切换输入框 const shouldHidePanel = - ['CLICK_PRESET', 'VISIBLE_CHANGE', 'KEYDOWN_ENTER', 'INPUT_CLEAR', 'CLICK_OK'].includes( - eventType - ) || - (isRange && !this.shouldSwitchInput(v)); + [ + 'CLICK_PRESET', + 'VISIBLE_CHANGE', + 'KEYDOWN_ENTER', + 'INPUT_CLEAR', + 'CLICK_OK', + ].includes(eventType!) || + (isRange && !this.shouldSwitchInput(v as (Dayjs | null)[])); if (shouldHidePanel) { this.onVisibleChange(false); } if (isValueChanged(v, preValue)) { - this.onChange(v); + this.onChange(v as ValueType); } } ); @@ -500,10 +428,8 @@ class TimePicker2 extends Component { /** * 获取输入框的禁用状态 - * @param {Number} idx - * @returns {Boolean} */ - isEnabled = idx => { + isEnabled = (idx?: number) => { const { disabled } = this.props; return Array.isArray(disabled) @@ -518,17 +444,17 @@ class TimePicker2 extends Component { * 清空输入之后 input 组件内部会让第二个输入框获得焦点 * 所以这里需要设置 setTimeout 才能让第一个 input 获得焦点 */ - this.clearTimeoutId = setTimeout(() => { + this.clearTimeoutId = window.setTimeout(() => { this.handleInputFocus(0); }); this.setState({ - inputType: TIME_INPUT_TYPE.BEGIN, + inputType: TIME_INPUT_TYPE.BEGIN as InputType, justBeginInput: this.isEnabled(), }); }; - handleInput = (v, eventType) => { + handleInput = (v: string, eventType?: string) => { const { isRange } = this.state; if (eventType === 'clear') { this.handleChange(v, 'INPUT_CLEAR'); @@ -547,7 +473,7 @@ class TimePicker2 extends Component { } }; - renderPreview(others) { + renderPreview(others: Omit) { const { prefix, format, className, renderPreview } = this.props; const { value } = this.state; const previewCls = classnames(className, `${prefix}form-preview`); @@ -560,7 +486,7 @@ class TimePicker2 extends Component { if (typeof renderPreview === 'function') { return (
    - {renderPreview(value, this.props)} + {renderPreview(value!, this.props)}
    ); } @@ -607,7 +533,8 @@ class TimePicker2 extends Component { ...others } = this.props; - const { value, inputStr, inputValue, curValue, inputing, visible, isRange, inputType } = this.state; + const { value, inputStr, inputValue, curValue, inputing, visible, isRange, inputType } = + this.state; const triggerCls = classnames({ [`${this.prefixCls}-trigger`]: true, }); @@ -617,10 +544,11 @@ class TimePicker2 extends Component { } if (isPreview) { + // @ts-expect-error TimePicker2 上不存在 PropTypes 属性,应该是 propTypes return this.renderPreview(obj.pickOthers(others, TimePicker2.PropTypes)); } - const sharedInputProps = { + const sharedInputProps: SharedInputProps = { prefix, locale, label, @@ -638,7 +566,7 @@ class TimePicker2 extends Component { onInputTypeChange: this.onInputTypeChange, onInput: this.handleInput, onKeyDown: this.onKeyDown, - ref: el => (this.dateInput = el), + ref: (el: InstanceType) => (this.dateInput = el), }; const triggerInput = ( @@ -649,7 +577,7 @@ class TimePicker2 extends Component { state={state} onClick={this.onClick} hasBorder={hasBorder} - placeholder={placeholder || locale.placeholder} + placeholder={placeholder || locale!.placeholder} className={classnames(`${this.prefixCls}-input`)} />
    @@ -658,13 +586,13 @@ class TimePicker2 extends Component { const panelProps = { prefix, locale, - value: inputing ? this.checkValue(inputStr) : curValue, + value: inputing ? this.checkValue(inputStr!) : curValue, // value: curValue, isRange, disabled, - showHour: format.indexOf('H') > -1, - showSecond: format.indexOf('s') > -1, - showMinute: format.indexOf('m') > -1, + showHour: format!.indexOf('H') > -1, + showSecond: format!.indexOf('s') > -1, + showMinute: format!.indexOf('m') > -1, hourStep, minuteStep, secondStep, @@ -685,7 +613,7 @@ class TimePicker2 extends Component { ); const PopupComponent = popupComponent ? popupComponent : Popup; - const oKable = !!(isRange ? inputValue && inputValue[inputType] : inputValue); + const oKable = !!(isRange ? inputValue && inputValue[inputType!] : inputValue); return (
    @@ -697,6 +625,7 @@ class TimePicker2 extends Component { onVisibleChange={this.handleVisibleChange} trigger={triggerInput} container={popupContainer} + // @ts-expect-error disabled 为 boolean | boolean[],此处没有考虑到 boolean[] 这种情况 disabled={disabled} triggerType={popupTriggerType} style={popupStyle} @@ -704,6 +633,7 @@ class TimePicker2 extends Component { >
    + {/* @ts-expect-error disabled 为 boolean | boolean[],TimePickerPanel 没有考虑到 boolean[] 这种情况 */} {preset || isRange ? ( , + Omit { + prefix?: string; + rtl?: boolean; + locale?: Locale['Input']; + value?: string | string[]; + inputType?: InputType; + format?: string | ((v: Dayjs) => Dayjs); + isRange?: boolean; + hasClear?: boolean | null; + onInput?: (v: string | number | null | (string | number | null)[], evetType?: string) => void; + onInputTypeChange?: (v: number) => void; + autoFocus?: boolean; + readOnly?: boolean; + placeholder?: string; + size?: 'small' | 'medium' | 'large'; + focus?: boolean; + hasBorder?: boolean; + onKeyDown?: InputProps['onKeyDown']; + onClick?: InputProps['onClick']; + separator?: ReactNode; + disabled?: boolean | boolean[]; + inputProps?: InputProps; + label?: ReactNode; + state?: InputProps['state']; + defaultValue?: string | number | null | undefined; + onBlur?: InputProps['onBlur']; + className?: string; +} +export type InputType = 0 | 1; +export interface PresetType extends Omit { + label?: string; + value?: ConfigType | ConfigType[] | (() => ConfigType | ConfigType[]); +} +interface InputCommonHTMLAttributes + extends Omit< + React.InputHTMLAttributes, + 'defaultValue' | 'onChange' | 'onKeyDown' | 'size' | 'maxLength' | 'value' + > { + [key: `data-${string}`]: string; +} + +interface HTMLAttributesWeak + extends Omit, 'onChange' | 'defaultValue'> {} + +interface TimeMenuPropsCommonHTMLAttributes + extends Omit< + InputHTMLAttributes, + 'value' | 'onKeyDown' | 'size' | 'onInput' | 'disabled' | 'defaultValue' + > {} + +export interface TimeMenuProps + extends CommonProps, + Omit { + prefix?: string; + title?: ReactNode; + mode?: 'hour' | 'minute' | 'second'; + step?: number; + activeIndex: number; + value?: Dayjs | null; + disabledItems?: (index: number) => boolean; + renderTimeMenuItems?: ( + list: Array, + mode: TimeMenuProps['mode'], + value: TimeMenuProps['value'] + ) => Array; + onSelect?: (value: TimeMenuListItem['value'], mode: TimeMenuProps['mode']) => void; + disabled?: boolean; +} +export interface TimeMenuListItem { + label: ReactNode; + value: number; +} + +export interface PanelProps + extends Omit, + Pick< + TimePickerProps, + | 'prefix' + | 'locale' + | 'hourStep' + | 'minuteStep' + | 'secondStep' + | 'disabledHours' + | 'disabledMinutes' + | 'disabledSeconds' + | 'renderTimeMenuItems' + | 'locale' + > { + /** + * 是否显示小时 + */ + showHour: boolean; + + /** + * 是否显示分钟 + */ + showSecond: boolean; + + /** + * 是否显示秒 + */ + showMinute: boolean; + value?: ValueType; + + /** + * 选择某个日期值时的回调 + */ + onSelect: (value: Dayjs | Dayjs[], type: PannelType) => void; + className?: string; + isRange: boolean; + disabled: boolean; +} + +export type PannelType = 'start' | 'end' | 'panel'; + +/** + * @api TimePicker + */ +export interface TimePickerProps extends HTMLAttributesWeak, CommonProps { + /** + * 按钮的文案 + * @en Button text + */ + label?: ReactNode; + + /** + * @deprecated use Form.Item name instead + * @skip + */ + name?: string; + + /** + * 时间选择框的尺寸 + * @en size of time picker + * @defaultValue 'medium' + */ + size?: 'small' | 'medium' | 'large'; + + /** + * 输入框状态 + * @en input state + */ + state?: 'error' | 'success'; + + /** + * 是否允许清空时间 + * @en whether to allow clearing time + * @defaultValue true + */ + hasClear?: boolean; + + /** + * 时间的格式 + * @en time format + * @remarks see https://Dayjsjs.com/docs/#/parsing/string-format/ + * @defaultValue 'HH:mm:ss' + */ + format?: string; + + /** + * 小时选项步长 + * @en hour option step + */ + hourStep?: number; + + /** + * 分钟选项步长 + * @en minute option step + */ + minuteStep?: number; + + /** + * 秒钟选项步长 + * @en second option step + */ + secondStep?: number; + + /** + * 渲染的可选择时间列表 [\{ label: '01', value: 1 \}] + * @en render the selectable time list + * @param list - 默认渲染的列表 + * @param mode - 渲染的菜单 hour, minute, second + * @param value - 当前时间,可能为 null + * @returns 返回需要渲染的数据 + */ + renderTimeMenuItems?: ( + list: Array, + mode: TimeMenuProps['mode'], + value: TimeMenuProps['value'] + ) => Array; + + /** + * 弹层是否显示(受控) + * @en popup layer display status (controlled) + */ + visible?: boolean; + + /** + * 弹层默认是否显示(非受控) + * @en popup layer default display status (uncontrolled) + */ + defaultVisible?: boolean; + + /** + * 弹层容器 + * @en popup layer container + */ + popupContainer?: string | HTMLElement | ((target: HTMLElement) => HTMLElement); + + /** + * 弹层对齐方式,详情见 Overlay 文档 + * @en popup layer alignment, see Overlay documentation + * @defaultValue 'tl bl' + */ + popupAlign?: string; + + /** + * 弹层触发方式 + * @en popup layer trigger type + * @defaultValue 'click' + */ + popupTriggerType?: 'click' | 'hover'; + + /** + * 弹层展示状态变化时的回调 + * @en callback when the popup layer display status changes + */ + onVisibleChange?: (visible: boolean, reason?: string) => void; + + /** + * 弹层自定义样式 + * @en popup layer custom style + */ + popupStyle?: CSSProperties; + + /** + * 弹层自定义样式类 + * @en popup layer custom style class + */ + popupClassName?: string; + + /** + * 弹层属性 + * @en popup layer property + */ + popupProps?: PopupProps; + + /** + * 跟随触发元素 + * @en follow trigger element + */ + followTrigger?: boolean; + + /** + * 是否有边框 + * @en Whether the input has border + * @defaultValue true + */ + hasBorder?: boolean; + + /** + * 是否为预览态 + * @en is preview + */ + isPreview?: boolean; + + /** + * 预览态模式下渲染的内容 + * @en content of preview mode + */ + renderPreview?: (value: ValueType, props: TimePickerProps) => ReactNode; + + /** + * 自定义输入框属性 + * @en custom input property + */ + inputProps?: InputProps; + + /** + * 国际化 + * @en internationalization + * @skip + */ + locale?: Locale['TimePicker']; + + /** + * 弹层组件 + * @en popup component + * @skip + */ + popupComponent?: JSXElementConstructor; + + /** + * 输入框提示 + * @en input hint + */ + placeholder?: string; + + /** + * 时间值(Dayjs 对象或时间字符串,受控状态使用) + * @en time value (Dayjs object or time string, controlled state use) + */ + value?: string | Dayjs | null | (Dayjs | null | string)[]; + + /** + * 时间初值(Dayjs 对象或时间字符串,非受控状态使用) + * @en time init value (Dayjs object or time string, uncontrolled state use) + */ + defaultValue?: string | Dayjs | (Dayjs | null)[]; + + /** + * 禁用小时的函数,TimePicker.RangePicker 时,函数需要返回 number[],且函数中没有 index 入参,非 TimePicker.RangePicker 时,函数需要返回 boolean,函数中有 index 入参 + * @en For the disabled hours function, if it's a TimePicker.RangePicker, the function should return a number[] and it shouldn't have an index parameter. If it's not a TimePicker.RangePicker, the function should return a boolean and it should have an index parameter. + */ + disabledHours?: (index?: number) => boolean | number[]; + + /** + * 禁用分钟函数,TimePicker.RangePicker 时,函数需要返回 number[],且函数中没有 index 入参,非 TimePicker.RangePicker 时,函数需要返回 boolean,函数中有 index 入参 + * @en For the disabled minutes function, if it's a TimePicker.RangePicker, the function should return a number[] and it shouldn't have an index parameter. If it's not a TimePicker.RangePicker, the function should return a boolean and it should have an index parameter. + */ + disabledMinutes?: (index?: number) => boolean | number[]; + + /** + * 禁用秒钟函数,TimePicker.RangePicker 时,函数需要返回 number[],且函数中没有 index 入参,非 TimePicker.RangePicker 时,函数需要返回 boolean,函数中有 index 入参 + * @en For the disabled seconds function, if it's a TimePicker.RangePicker, the function should return a number[] and it shouldn't have an index parameter. If it's not a TimePicker.RangePicker, the function should return a boolean and it should have an index parameter. + */ + disabledSeconds?: (index?: number) => boolean | number[]; + + /** + * 时间值改变时的回调 + * @en callback when the time value changes + */ + onChange?: (value: ValueType) => void; + + /** + * 时间类型 + * @en time type + * @skip + * @defaultValue 'time' + */ + type?: 'time' | 'range'; + + /** + * 预设值,会显示在时间面板下面 + * @en Rreset values, shown below the time panel. + * Can be object or array of object, with the following properties. + * properties: + * label: shown text + * name: key of React element, can be empty, and index will become key instead + * value: date value + */ + preset?: PresetType | PresetType[]; + + /** + * 禁用 + * @en disable + * @defaultValue false + */ + disabled?: boolean | boolean[]; +} + +export interface TimePickerState { + value?: ValueType; + visible?: boolean | undefined; + inputStr?: string | string[]; + inputing?: boolean; + isRange?: boolean; + inputValue?: string | string[]; + curValue?: ValueType; + preValue?: ValueType; + selecting?: boolean; + inputType?: InputType; + justBeginInput?: boolean; +} + +export type ValueType = Dayjs | (Dayjs | null)[] | null; + +/** + * @api TimePicker.RangePicker + */ +export interface RangePickerProps extends Omit, CommonProps { + /** + * 按钮的文案 + * @en Button text + */ + label?: ReactNode; + + /** + * @deprecated use Form.Item name instead + * @skip + */ + name?: string; + + /** + * 时间选择框的尺寸 + * @en size of time picker + * @defaultValue 'medium' + */ + size?: 'small' | 'medium' | 'large'; + + /** + * 输入框状态 + * @en input state + */ + state?: 'error' | 'success'; + + /** + * 是否允许清空时间 + * @en whether to allow clearing time + * @defaultValue true + */ + hasClear?: boolean; + + /** + * 时间的格式 + * @en time format + * @remarks see https://Dayjsjs.com/docs/#/parsing/string-format/ + * @defaultValue 'HH:mm:ss' + */ + format?: string; + + /** + * 小时选项步长 + * @en hour option step + */ + hourStep?: number; + + /** + * 分钟选项步长 + * @en minute option step + */ + minuteStep?: number; + + /** + * 秒钟选项步长 + * @en second option step + */ + secondStep?: number; + + /** + * 渲染的可选择时间列表 [\{ label: '01', value: 1 \}] + * @en render the selectable time list + * @param list - 默认渲染的列表 + * @param mode - 渲染的菜单 hour, minute, second + * @param value - 当前时间,可能为 null + * @returns 返回需要渲染的数据 + */ + renderTimeMenuItems?: ( + list: Array, + mode: TimeMenuProps['mode'], + value: TimeMenuProps['value'] + ) => Array; + + /** + * 弹层是否显示(受控) + * @en popup layer display status (controlled) + */ + visible?: boolean; + + /** + * 弹层默认是否显示(非受控) + * @en popup layer default display status (uncontrolled) + */ + defaultVisible?: boolean; + + /** + * 弹层容器 + * @en popup layer container + */ + popupContainer?: string | HTMLElement | ((target: HTMLElement) => HTMLElement); + + /** + * 弹层对齐方式,详情见 Overlay 文档 + * @en popup layer alignment, see Overlay documentation + * @defaultValue 'tl bl' + */ + popupAlign?: string; + + /** + * 弹层触发方式 + * @en popup layer trigger type + * @defaultValue 'click' + */ + popupTriggerType?: 'click' | 'hover'; + + /** + * 弹层展示状态变化时的回调 + * @en callback when the popup layer display status changes + */ + onVisibleChange?: (visible: boolean, reason?: string) => void; + + /** + * 弹层自定义样式 + * @en popup layer custom style + */ + popupStyle?: CSSProperties; + + /** + * 弹层自定义样式类 + * @en popup layer custom style class + */ + popupClassName?: string; + + /** + * 弹层属性 + * @en popup layer property + */ + popupProps?: PopupProps; + + /** + * 跟随触发元素 + * @en follow trigger element + */ + followTrigger?: boolean; + + /** + * 是否有边框 + * @en Whether the input has border + * @defaultValue true + */ + hasBorder?: boolean; + + /** + * 是否为预览态 + * @en is preview + */ + isPreview?: boolean; + + /** + * 预览态模式下渲染的内容 + * @en content of preview mode + */ + renderPreview?: (value: ValueType, props: TimePickerProps) => ReactNode; + + /** + * 自定义输入框属性 + * @en custom input property + */ + inputProps?: InputProps; + + /** + * 国际化 + * @en internationalization + * @skip + */ + locale?: Locale['TimePicker']; + + /** + * 弹层组件 + * @en popup component + * @skip + */ + popupComponent?: JSXElementConstructor; + + /** + * 禁用小时的函数,TimePicker.RangePicker 时,函数需要返回 number[],且函数中没有 index 入参,非 TimePicker.RangePicker 时,函数需要返回 boolean,函数中有 index 入参 + * @en For the disabled hours function, if it's a TimePicker.RangePicker, the function should return a number[] and it shouldn't have an index parameter. If it's not a TimePicker.RangePicker, the function should return a boolean and it should have an index parameter. + */ + disabledHours?: (index?: number) => boolean | number[]; + + /** + * 禁用分钟函数,TimePicker.RangePicker 时,函数需要返回 number[],且函数中没有 index 入参,非 TimePicker.RangePicker 时,函数需要返回 boolean,函数中有 index 入参 + * @en For the disabled minutes function, if it's a TimePicker.RangePicker, the function should return a number[] and it shouldn't have an index parameter. If it's not a TimePicker.RangePicker, the function should return a boolean and it should have an index parameter. + */ + disabledMinutes?: (index?: number) => boolean | number[]; + + /** + * 禁用秒钟函数,TimePicker.RangePicker 时,函数需要返回 number[],且函数中没有 index 入参,非 TimePicker.RangePicker 时,函数需要返回 boolean,函数中有 index 入参 + * @en For the disabled seconds function, if it's a TimePicker.RangePicker, the function should return a number[] and it shouldn't have an index parameter. If it's not a TimePicker.RangePicker, the function should return a boolean and it should have an index parameter. + */ + disabledSeconds?: (index?: number) => boolean | number[]; + + /** + * 时间类型 + * @en time type + * @skip + * @defaultValue 'time' + */ + type?: 'time' | 'range'; + + /** + * 禁用 + * @en disable + * @defaultValue false + */ + disabled?: boolean | boolean[]; + /** + * 输入框提示 + * @en input hint + */ + placeholder?: string | string[]; + + /** + * 时间值(Dayjs 对象或时间字符串,受控状态使用) + * @en time value (Dayjs object or time string, controlled state use) + */ + value?: Array; + + /** + * 时间初值(Dayjs 对象或时间字符串,非受控状态使用) + * @en time init value (Dayjs object or time string, uncontrolled state use) + */ + defaultValue?: Array; + + /** + * 时间值改变时的回调 + * @en callback when the time value changes + */ + onChange?: (value: Array) => void; + + /** + * 确定按钮点击时的回调 + * @en callback when the ok button is clicked + */ + onOk?: (value: Array) => void; + + /** + * 预设值,会显示在时间面板下面 + * @en Rreset values, shown below the time panel. + * Can be object or array of object, with the following properties. + * properties: + * label: shown text + * name: key of React element, can be empty, and index will become key instead + * value: date value + */ + preset?: PresetType[]; +} + +export interface DisabledItems { + newDisabledHours: ((index: number) => boolean)[]; + newDisabledMinutes: ((index: number) => boolean)[]; + newDisabledSeconds: ((index: number) => boolean)[]; +} diff --git a/components/time-picker2/utils/index.js b/components/time-picker2/utils/index.ts similarity index 53% rename from components/time-picker2/utils/index.js rename to components/time-picker2/utils/index.ts index 0493c46eec..c2945804ea 100644 --- a/components/time-picker2/utils/index.js +++ b/components/time-picker2/utils/index.ts @@ -1,30 +1,58 @@ +import type { KeyboardEvent } from 'react'; +import type { Dayjs } from 'dayjs'; import { datejs, KEYCODE } from '../../util'; // 检查传入值是否为 dayjs 对象 -export function checkDayjsObj(props, propName, componentName) { +export function checkDayjsObj( + props: Record, + propName: string, + componentName: string +) { if (props[propName] && !datejs.isSelf(props[propName])) { - return new Error(`Invalid prop ${propName} supplied to ${componentName}. Required a dayjs object.`); + return new Error( + `Invalid prop ${propName} supplied to ${componentName}. Required a dayjs object.` + ); } } // 检查传入值是否为 dayjs 对象 -export function checkDateValue(props, propName, componentName) { +export function checkDateValue( + props: Record, + propName: string, + componentName: string +) { if (props[propName] && !datejs.isSelf(props[propName]) && typeof props[propName] !== 'string') { return new Error( `Invalid prop ${propName} supplied to ${componentName}. Required a dayjs object or format date string.` ); } + return null; } /** * 监听键盘事件,操作时间 - * @param {KeyboardEvent} e - * @param {Object} param1 - * @param {String} type second hour minute + * @param e - 键盘事件 + * @param param1 - Object + * @param type - second hour minute */ -export function onTimeKeydown(e, { format, timeInputStr, steps, value }, type) { - if ([KEYCODE.UP, KEYCODE.DOWN, KEYCODE.PAGE_UP, KEYCODE.PAGE_DOWN].indexOf(e.keyCode) === -1) return; - if ((e.altKey && [KEYCODE.PAGE_UP, KEYCODE.PAGE_DOWN].indexOf(e.keyCode) === -1) || e.controlKey || e.shiftKey) +export function onTimeKeydown( + e: KeyboardEvent, + { + format, + timeInputStr, + steps, + value, + }: { format: string; timeInputStr: string; steps: Record; value: Dayjs }, + type: 'second' | 'hour' | 'minute' +) { + if ([KEYCODE.UP, KEYCODE.DOWN, KEYCODE.PAGE_UP, KEYCODE.PAGE_DOWN].indexOf(e.keyCode) === -1) + return; + if ( + (e.altKey && [KEYCODE.PAGE_UP, KEYCODE.PAGE_DOWN].indexOf(e.keyCode) === -1) || + // @ts-expect-error e.controlKey 是旧标准的用法,新标准使用 e.ctrlKey 来代表 Control 键是否被按下 + e.controlKey || + e.shiftKey + ) return; let time = datejs(timeInputStr, format, true); @@ -49,10 +77,7 @@ export function onTimeKeydown(e, { format, timeInputStr, steps, value }, type) { time = value.clone(); } else { time = datejs(); - time = time - .hour(0) - .minute(0) - .second(0); + time = time.hour(0).minute(0).second(0); } e.preventDefault(); diff --git a/components/util/func.ts b/components/util/func.ts index d020a6ee54..467ec8c96e 100644 --- a/components/util/func.ts +++ b/components/util/func.ts @@ -168,9 +168,9 @@ export function checkDate(value: ConfigType, format?: OptionType): Dayjs | null * @param strictly - 是否严格校验:严格模式下不允许开始时间大于结束时间,在显示确认按键的,用户输入过程可不严格校验 */ export function checkRangeDate( - value: ConfigType, + value: ConfigType | ConfigType[], inputType: number, - disabled?: boolean, + disabled?: boolean | boolean[], strictly: boolean = true, format?: OptionType ): [Dayjs | null, Dayjs | null] {