Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions packages/components/date-picker/DateRangePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>((origin
setIsFirstValueSelected,
cacheValue,
setCacheValue,
isSwitchTimeMode,
} = useRange(props);

const { format, timeFormat, valueType } = getDefaultFormat({
Expand Down Expand Up @@ -154,7 +155,7 @@ const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>((origin
const { year: defaultYear, month: defaultMonth } = initYearMonthTime({ value, mode, format, enableTimePicker });
setYear(defaultYear);
setMonth(defaultMonth);
} else if (value.length === 2 && !enableTimePicker) {
} else if (value.length === 2 && (!enableTimePicker || isSwitchTimeMode)) {
handleSyncPanelValue(value);
} else {
setYear(value.map((v: string) => parseToDayjs(v, format).year()));
Expand Down Expand Up @@ -200,8 +201,8 @@ const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>((origin
setCacheValue(nextValue);
setInputValue(nextValue);

// 有时间选择器走 confirm 逻辑
if (enableTimePicker) return;
// 有时间选择器且非 switch mode,走 confirm 逻辑
if (enableTimePicker && !isSwitchTimeMode) return;

// 确保两端都是有效值
const notValidIndex = nextValue.findIndex((v) => !v || !isValidDate(v, format));
Expand All @@ -222,8 +223,9 @@ const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>((origin
}

// 首次点击不关闭、确保两端都有有效值并且无时间选择器时点击后自动关闭
if (!isFirstValueSelected || !activeIndex) {
if (!isFirstValueSelected || !activeIndex || isSwitchTimeMode) {
let nextIndex = notValidIndex;
if (isSwitchTimeMode && activeIndex === 1 && isFirstValueSelected) return;
if (nextIndex === -1) nextIndex = activeIndex ? 0 : 1;
setActiveIndex(nextIndex);
setIsFirstValueSelected(!!nextValue[0]);
Expand Down Expand Up @@ -286,24 +288,24 @@ const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>((origin
}

// time-picker 点击
function onTimePickerChange(val: string) {
function onTimePickerChange(val: string, context?: { activeIndex: 0 | 1 }) {
const { hours, minutes, seconds, milliseconds, meridiem } = extractTimeObj(val);

const currentIndex = context.activeIndex ?? activeIndex;
const nextInputValue = [...inputValue];
const changedInputValue = inputValue[activeIndex];
const changedInputValue = inputValue[currentIndex];
const currentDate = !dayjs(changedInputValue, format).isValid()
? dayjs().year(year[activeIndex]).month(month[activeIndex])
? dayjs().year(year[currentIndex]).month(month[currentIndex])
: dayjs(changedInputValue, format);
// am pm 12小时制转化 24小时制
let nextHours = hours;
if (/am/i.test(meridiem) && nextHours === 12) nextHours -= 12;
if (/pm/i.test(meridiem) && nextHours < 12) nextHours += 12;

const nextDate = currentDate.hour(nextHours).minute(minutes).second(seconds).millisecond(milliseconds).toDate();
nextInputValue[activeIndex] = nextDate;
nextInputValue[currentIndex] = nextDate;

const nextTime = [...time];
nextTime[activeIndex] = val;
nextTime[currentIndex] = val;
setTime(nextTime);

setIsSelected(true);
Expand Down Expand Up @@ -333,6 +335,8 @@ const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>((origin
}
}

if (isSwitchTimeMode && nextValue.every(Boolean)) handlePopupInvisible();

// 首次点击不关闭、确保两端都有有效值并且无时间选择器时点击后自动关闭
if (!isFirstValueSelected || !activeIndex) {
let nextIndex = notValidIndex;
Expand Down Expand Up @@ -470,6 +474,7 @@ const DateRangePicker = forwardRef<HTMLDivElement, DateRangePickerProps>((origin
onMonthChange,
onTimePickerChange,
disableTime,
isSwitchTimeMode,
};

return (
Expand Down
20 changes: 11 additions & 9 deletions packages/components/date-picker/DateRangePickerPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const DateRangePickerPanel = forwardRef<HTMLDivElement, DateRangePickerPanelProp
setIsFirstValueSelected,
cacheValue,
setCacheValue,
isSwitchTimeMode,
} = useRangeValue(props);

const { format, timeFormat } = getDefaultFormat({
Expand Down Expand Up @@ -112,8 +113,8 @@ const DateRangePickerPanel = forwardRef<HTMLDivElement, DateRangePickerPanelProp
}
}

// 有时间选择器走 confirm 逻辑
if (enableTimePicker) return;
// 有时间选择器且非 switch mode,走 confirm 逻辑
if (enableTimePicker && !isSwitchTimeMode) return;

// 首次点击不关闭、确保两端都有有效值并且无时间选择器时点击后自动关闭
if (nextValue.length === 2 && isFirstValueSelected) {
Expand Down Expand Up @@ -179,32 +180,33 @@ const DateRangePickerPanel = forwardRef<HTMLDivElement, DateRangePickerPanelProp
}

// time-picker 点击
function onTimePickerChange(val: string) {
function onTimePickerChange(val: string, context?: { activeIndex: 0 | 1 }) {
const { hours, minutes, seconds, milliseconds, meridiem } = extractTimeObj(val);
const currentIndex = context.activeIndex ?? activeIndex;

const nextInputValue = [...cacheValue];
const changedInputValue = cacheValue[activeIndex];
const changedInputValue = cacheValue[currentIndex];
const currentDate = !dayjs(changedInputValue, format).isValid()
? dayjs().year(year[activeIndex]).month(month[activeIndex])
? dayjs().year(year[currentIndex]).month(month[currentIndex])
: dayjs(changedInputValue, format);
// am pm 12小时制转化 24小时制
let nextHours = hours;
if (/am/i.test(meridiem) && nextHours === 12) nextHours -= 12;
if (/pm/i.test(meridiem) && nextHours < 12) nextHours += 12;

const nextDate = currentDate.hour(nextHours).minute(minutes).second(seconds).millisecond(milliseconds).toDate();
nextInputValue[activeIndex] = nextDate;
nextInputValue[currentIndex] = nextDate;

const nextTime = [...time];
nextTime[activeIndex] = val;
nextTime[currentIndex] = val;
setTime(nextTime);

setIsSelected(true);
setCacheValue(formatDate(nextInputValue, { format }));

props.onTimeChange?.({
time: val,
partial: activeIndex ? 'end' : 'start',
partial: currentIndex ? 'end' : 'start',
date: value.map((v) => dayjs(v).toDate()),
trigger: 'time-hour' as DatePickerTimeChangeTrigger,
});
Expand Down Expand Up @@ -346,8 +348,8 @@ const DateRangePickerPanel = forwardRef<HTMLDivElement, DateRangePickerPanelProp
onMonthChange,
onTimePickerChange,
onPanelClick,
isSwitchTimeMode,
};

return <RangePanel ref={ref} className={className} style={style} {...panelProps} />;
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ export default function YearDatePicker() {
<Space direction="vertical">
<DateRangePicker value={range1} presets={presets} onChange={(val) => setRange1(val)} />

<DateRangePicker value={range2} presets={presets} onChange={(val) => setRange2(val)} enableTimePicker />
<DateRangePicker
value={range2}
presets={presets}
onChange={(val) => setRange2(val)}
enableTimePicker={{ mode: 'switch' }}
/>
</Space>
);
}
2 changes: 2 additions & 0 deletions packages/components/date-picker/_example/date-range.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export default function YearDatePicker() {
<Space direction="vertical">
<DateRangePicker onPick={onPick} allowInput clearable onChange={onChange} />
<DateRangePicker enableTimePicker allowInput clearable onPick={onPick} onChange={onChange} />
{/* 可以通过 enableTimePicker 的 mode 属性控制时间选择器的显示方式 */}
<DateRangePicker enableTimePicker={{ mode: 'switch' }} allowInput clearable onPick={onPick} onChange={onChange} />
</Space>
);
}
7 changes: 7 additions & 0 deletions packages/components/date-picker/_example/panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ export default function PanelExample() {
<div style={{ border: '1px solid var(--td-border-level-2-color)', width: 'fit-content', borderRadius: 3 }}>
<DateRangePickerPanel enableTimePicker onCellClick={handleRangeCellClick} onChange={handleRangeChange} />
</div>
<div style={{ border: '1px solid var(--td-border-level-2-color)', width: 'fit-content', borderRadius: 3 }}>
<DateRangePickerPanel
enableTimePicker={{ mode: 'switch' }}
onCellClick={handleRangeCellClick}
onChange={handleRangeChange}
/>
</div>
</Space>
);
}
26 changes: 19 additions & 7 deletions packages/components/date-picker/base/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import Button from '../../button';
import useConfig from '../../hooks/useConfig';
import { TdDatePickerProps, TdDateRangePickerProps, DateValue, DateMultipleValue } from '../type';

interface DatePickerFooterProps
extends Pick<TdDatePickerProps, 'enableTimePicker' | 'presetsPlacement' | 'needConfirm'> {
interface DatePickerFooterProps extends Pick<TdDatePickerProps, 'presetsPlacement' | 'needConfirm'> {
presets?: TdDatePickerProps['presets'] | TdDateRangePickerProps['presets'];
onPresetClick?: Function;
onConfirmClick?: Function;
selectedValue?: DateValue | DateMultipleValue;
onTimePanelChange?: () => void;
enableTimePicker?: TdDateRangePickerProps['enableTimePicker'] | TdDatePickerProps['enableTimePicker'];
isDateRangeContent?: boolean;
isSwitchTimeMode?: boolean;
}

const DatePickerFooter = (props: DatePickerFooterProps) => {
Expand All @@ -28,6 +31,8 @@ const DatePickerFooter = (props: DatePickerFooterProps) => {
onPresetClick,
selectedValue,
needConfirm,
onTimePanelChange,
isSwitchTimeMode,
} = props;

const footerClass = classNames(
Expand Down Expand Up @@ -58,11 +63,18 @@ const DatePickerFooter = (props: DatePickerFooterProps) => {
return (
<div className={footerClass}>
<div className={`${classPrefix}-date-picker__presets`}>{renderPresets()}</div>
{enableTimePicker && needConfirm && (
<Button disabled={!selectedValue} size="small" theme="primary" onClick={(e) => onConfirmClick({ e })}>
{confirmText}
</Button>
)}
<div>
{isSwitchTimeMode && (
<Button style={{ marginRight: 16 }} size="small" theme="primary" variant="text" onClick={onTimePanelChange}>
{props.isDateRangeContent ? t(local.selectTime) : t(local.selectDate)}
</Button>
)}
{enableTimePicker && needConfirm && (
<Button disabled={!selectedValue} size="small" theme="primary" onClick={(e) => onConfirmClick({ e })}>
{confirmText}
</Button>
)}
</div>
</div>
);
};
Expand Down
2 changes: 1 addition & 1 deletion packages/components/date-picker/date-picker.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ defaultTime | Array | ["00:00:00", "23:59:59"] | Time selector default value。T
disableDate | Object / Array / Function | - | Typescript: `DisableRangeDate` `type DisableRangeDate = Array<DateValue> \| DisableDateObj \| ((context: { date: DateRangeValue; partial: DateRangePickerPartial }) => boolean)` `interface DisableDateObj { from?: string; to?: string; before?: string; after?: string }` `type DateRangePickerPartial = 'start' \| 'end'`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/date-picker/type.ts) | N
disableTime | Function | - | disable time config function。Typescript: `(times: Array<Date \| null>, context: { partial: DateRangePickerPartial }) => Partial<{ hour: Array<number>, minute: Array<number>, second: Array<number> }>` | N
disabled | Boolean | - | \- | N
enableTimePicker | Boolean | false | \- | N
enableTimePicker | Boolean / Object | false | Typescript: `boolean \| TimePanelConfig` `interface TimePanelConfig { mode?: TimePanelMode }` `type TimePanelMode = 'parallel' \| 'switch' `。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/date-picker/type.ts) | N
firstDayOfWeek | Number | - | options: 1/2/3/4/5/6/7 | N
format | String | - | \- | N
label | TNode | - | Typescript: `string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N
Expand Down
2 changes: 1 addition & 1 deletion packages/components/date-picker/date-picker.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ defaultTime | Array | ["00:00:00", "23:59:59"] | 时间选择器默认值,当
disableDate | Object / Array / Function | - | 禁用日期,示例:['A', 'B'] 表示日期 A 和日期 B 会被禁用。{ from: 'A', to: 'B' } 表示在 A 到 B 之间的日期会被禁用(包含A和B)。{ before: 'A', after: 'B' } 表示在 A 之前和在 B 之后的日期都会被禁用。其中 A = '2021-01-01',B = '2021-02-01'。值类型为 Function 则表示返回值为 true 的日期会被禁用。TS 类型:`DisableRangeDate` `type DisableRangeDate = Array<DateValue> \| DisableDateObj \| ((context: { date: DateRangeValue; partial: DateRangePickerPartial }) => boolean)` `interface DisableDateObj { from?: string; to?: string; before?: string; after?: string }` `type DateRangePickerPartial = 'start' \| 'end'`[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/date-picker/type.ts) | N
disableTime | Function | - | 禁用时间项的配置函数,仅在日期区间选择器中开启时间展示时可用。TS 类型:`(times: Array<Date \| null>, context: { partial: DateRangePickerPartial }) => Partial<{ hour: Array<number>, minute: Array<number>, second: Array<number> }>` | N
disabled | Boolean | - | 是否禁用组件 | N
enableTimePicker | Boolean | false | 是否显示时间选择 | N
enableTimePicker | Boolean / Object | false | 是否显示时间选择, 默认不展示,设置为 true 时,默认模式为 parallel ,与日期面板并列展示,可以通过配置 mode 为 switch 调整展示方式。TS 类型:`boolean \| TimePanelConfig` `interface TimePanelConfig { mode?: TimePanelMode }` `type TimePanelMode = 'parallel' \| 'switch' `[详细类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/date-picker/type.ts) | N
firstDayOfWeek | Number | - | 第一天从星期几开始。可选项:1/2/3/4/5/6/7 | N
format | String | - | 用于格式化日期,[详细文档](https://day.js.org/docs/en/display/format) | N
label | TNode | - | 左侧文本。TS 类型:`string \| TNode`[通用类型定义](https://github.com/Tencent/tdesign-react/blob/develop/packages/components/common.ts) | N
Expand Down
16 changes: 14 additions & 2 deletions packages/components/date-picker/hooks/useRange.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { isObject } from 'lodash-es';
import { CalendarIcon as TdCalendarIcon } from 'tdesign-icons-react';
import classNames from 'classnames';

Expand All @@ -22,6 +23,11 @@ export default function useRange(props: TdDateRangePickerProps) {
const isMountedRef = useRef(false);
const inputRef = useRef<RangeInputRefInterface>(null);

const isSwitchTimeMode = useMemo(
() => isObject(props.enableTimePicker) && props.enableTimePicker.mode === 'switch',
[props.enableTimePicker],
);

const {
value,
onChange,
Expand Down Expand Up @@ -92,7 +98,12 @@ export default function useRange(props: TdDateRangePickerProps) {
onChange: (newVal: string[], { e, position }) => {
const index = position === 'first' ? 0 : 1;

props.onInput?.({ input: newVal[index], value, partial: PARTIAL_MAP[position], e: e as React.FormEvent<HTMLInputElement> });
props.onInput?.({
input: newVal[index],
value,
partial: PARTIAL_MAP[position],
e: e as React.FormEvent<HTMLInputElement>,
});
setInputValue(newVal);

// 跳过不符合格式化的输入框内容
Expand Down Expand Up @@ -197,5 +208,6 @@ export default function useRange(props: TdDateRangePickerProps) {
setIsFirstValueSelected,
cacheValue,
setCacheValue,
isSwitchTimeMode,
};
}
9 changes: 8 additions & 1 deletion packages/components/date-picker/hooks/useRangeValue.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useMemo } from 'react';
import { isObject } from 'lodash-es';
import {
isValidDate,
formatDate,
Expand Down Expand Up @@ -47,6 +48,11 @@ export default function useRange(props: TdDateRangePickerProps) {
const [year, setYear] = useState<Array<number>>(() => initYearMonthTime({ value, mode: props.mode, format }).year);
const [cacheValue, setCacheValue] = useState(() => formatDate(value, { format })); // 缓存选中值,panel 点击时更改

const isSwitchTimeMode = useMemo(
() => isObject(props.enableTimePicker) && props.enableTimePicker.mode === 'switch',
[props.enableTimePicker],
);

// 输入框响应 value 变化
useEffect(() => {
if (!value) {
Expand All @@ -73,5 +79,6 @@ export default function useRange(props: TdDateRangePickerProps) {
setIsFirstValueSelected,
cacheValue,
setCacheValue,
isSwitchTimeMode,
};
}
18 changes: 16 additions & 2 deletions packages/components/date-picker/panel/ExtraContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,22 @@ export interface ExtraContentProps
> {
selectedValue?: DateValue | DateMultipleValue;
presets?: TdDatePickerProps['presets'] | TdDateRangePickerProps['presets'];
onTimeModeChange?: () => void;
isDateRangeContent?: boolean;
isSwitchTimeMode?: boolean;
}

export default function ExtraContent(props: ExtraContentProps) {
const { presets, enableTimePicker, presetsPlacement, onPresetClick, onConfirmClick, selectedValue, needConfirm } =
props;
const {
presets,
enableTimePicker,
presetsPlacement,
onPresetClick,
onConfirmClick,
selectedValue,
needConfirm,
isSwitchTimeMode,
} = props;

const showPanelFooter = (enableTimePicker && needConfirm) || presets;

Expand All @@ -27,6 +38,9 @@ export default function ExtraContent(props: ExtraContentProps) {
presetsPlacement={presetsPlacement}
selectedValue={selectedValue}
needConfirm={needConfirm}
onTimePanelChange={props.onTimeModeChange}
isDateRangeContent={props.isDateRangeContent}
isSwitchTimeMode={isSwitchTimeMode}
/>
) : null;
}
5 changes: 3 additions & 2 deletions packages/components/date-picker/panel/PanelContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface PanelContentProps {
popupVisible?: boolean;
tableData: any[];
internalYear?: Array<number>;
isSwitchTimeMode?: boolean;
onMonthChange: SinglePanelProps['onMonthChange'] | RangePanelProps['onMonthChange'];
onYearChange: SinglePanelProps['onYearChange'] | RangePanelProps['onYearChange'];
onJumperClick: SinglePanelProps['onJumperClick'] | RangePanelProps['onJumperClick'];
Expand All @@ -45,7 +46,6 @@ export default function PanelContent(props: PanelContentProps) {
enableTimePicker,
timePickerProps,
firstDayOfWeek,

partial = 'start',
time,
tableData,
Expand All @@ -55,14 +55,15 @@ export default function PanelContent(props: PanelContentProps) {
onCellMouseEnter,
onCellMouseLeave,
onTimePickerChange,
isSwitchTimeMode,
} = props;

const onMonthChange = useEventCallback(props.onMonthChange);
const onYearChange = useEventCallback(props.onYearChange);

const { timeFormat } = getDefaultFormat({ mode, format, enableTimePicker });

const showTimePicker = enableTimePicker && mode === 'date';
const showTimePicker = enableTimePicker && !isSwitchTimeMode && mode === 'date';

const defaultTime = '00:00:00';

Expand Down
Loading
Loading