Skip to content

fix: presets和maxDate的对比 #930

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
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
52 changes: 34 additions & 18 deletions src/PickerInput/Popup/PresetPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import * as React from 'react';
import type { ValueDate } from '../../interface';
import moment from 'moment';

export interface PresetPanelProps<ValueType = any> {
export interface PresetPanelProps<ValueType = any, DateType extends object = any> {
prefixCls: string;
presets: ValueDate<ValueType>[];
onClick: (value: ValueType) => void;
onHover: (value: ValueType) => void;
maxDate?: DateType;
}

function executeValue<ValueType extends object>(value: ValueDate<ValueType>['value']): ValueType {
Expand All @@ -15,7 +17,7 @@ function executeValue<ValueType extends object>(value: ValueDate<ValueType>['val
export default function PresetPanel<DateType extends object = any>(
props: PresetPanelProps<DateType>,
) {
const { prefixCls, presets, onClick, onHover } = props;
const { prefixCls, presets, onClick, onHover, maxDate } = props;
Comment on lines 17 to +20
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

需要 generateConfig 进行日期比较

为了与项目其他部分保持一致并支持不同的日期库,建议通过 props 传入 generateConfig

 export interface PresetPanelProps<ValueType = any> {
   prefixCls: string;
   presets: ValueDate<ValueType>[];
   onClick: (value: ValueType) => void;
   onHover: (value: ValueType) => void;
-  maxDate?: DateType;
+  maxDate?: ValueType;
+  generateConfig: GenerateConfig<ValueType>;
 }

 export default function PresetPanel<DateType extends object = any>(
   props: PresetPanelProps<DateType>,
 ) {
-  const { prefixCls, presets, onClick, onHover, maxDate } = props;
+  const { prefixCls, presets, onClick, onHover, maxDate, generateConfig } = props;

还需要在调用此组件的地方传入 generateConfig


🏁 Script executed:

#!/bin/bash
# 检查 PresetPanel 的使用位置,确认是否可以访问 generateConfig
echo "=== 查找 PresetPanel 的使用位置 ==="
rg "PresetPanel" --type ts --type tsx -A 5 -B 5

echo -e "\n=== 检查 generateConfig 的使用模式 ==="
ast-grep --pattern 'generateConfig'

Length of output: 33299


🏁 Script executed:

#!/bin/bash
echo "=== Inspect PresetPanel.tsx ==="
sed -n '1,200p' src/PickerInput/Popup/PresetPanel.tsx || true

echo -e "\n=== Search for maxDate usage ==="
grep -R "maxDate" -n src/PickerInput/Popup/PresetPanel.tsx || true

Length of output: 1930


需要为 PresetPanel 添加 generateConfig 支持,移除对 moment 的直接依赖

为了与项目其他地方保持一致并支持多种日期库,建议对 src/PickerInput/Popup/PresetPanel.tsx 做以下修改:

• 在 Props 接口中:

  • 修正错误的 maxDate?: dateTy;maxDate?: ValueType;
  • 新增 generateConfig: GenerateConfig<ValueType>;

• 在组件签名和解构 props 时引入 generateConfig

• 将

const isDisabled = moment(value).isAfter(maxDate) || moment(value).isSame(maxDate);

替换为

const rawValue = executeValue(value);
const isDisabled =
  generateConfig.isAfter(rawValue, maxDate) ||
  generateConfig.isSame(rawValue, maxDate);

• 在调用此组件的地方(如 PopupPanel.tsx)传入 generateConfig

示例 diff:

--- a/src/PickerInput/Popup/PresetPanel.tsx
+++ b/src/PickerInput/Popup/PresetPanel.tsx
@@
-export interface PresetPanelProps<ValueType = any> {
+export interface PresetPanelProps<ValueType = any> {
   prefixCls: string;
   presets: ValueDate<ValueType>[];
   onClick: (value: ValueType) => void;
   onHover: (value: ValueType) => void;
-  maxDate?: dateTy;
+  maxDate?: ValueType;
+  generateConfig: GenerateConfig<ValueType>;
 }

-export default function PresetPanel<DateType extends object = any>(
-  props: PresetPanelProps<DateType>,
-) {
-  const { prefixCls, presets, onClick, onHover, maxDate } = props;
+export default function PresetPanel<DateType extends object = any>(
+  props: PresetPanelProps<DateType>,
+) {
+  const { prefixCls, presets, onClick, onHover, maxDate, generateConfig } = props;

@@
-        const isDisabled = moment(value).isAfter(maxDate) || moment(value).isSame(maxDate);
+        const raw = executeValue(value);
+        const isDisabled =
+          generateConfig.isAfter(raw, maxDate) ||
+          generateConfig.isSame(raw, maxDate);

请同时在使用 <PresetPanel> 的所有调用处(如 PopupPanel)传入当前的 generateConfig

🤖 Prompt for AI Agents
In src/PickerInput/Popup/PresetPanel.tsx lines 17 to 20, the component currently
does not accept generateConfig as a prop and directly uses moment for date
comparison, which reduces flexibility and consistency. To fix this, add
generateConfig: GenerateConfig<ValueType> to the component's props interface,
destructure it from props, and replace the moment-based date comparison with
generateConfig.isAfter and generateConfig.isSame calls using the computed
rawValue. Also, ensure that all usages of PresetPanel pass in the generateConfig
prop to maintain proper functionality.


if (!presets.length) {
return null;
Expand All @@ -24,22 +26,36 @@ export default function PresetPanel<DateType extends object = any>(
return (
<div className={`${prefixCls}-presets`}>
<ul>
{presets.map(({ label, value }, index) => (
<li
key={index}
onClick={() => {
onClick(executeValue(value));
}}
onMouseEnter={() => {
onHover(executeValue(value));
}}
onMouseLeave={() => {
onHover(null);
}}
>
{label}
</li>
))}
{presets.map(({ label, value }, index) => {
// const isDisabled =
// maxDate && moment.isMoment(maxDate)
// ? moment(typeof value === 'function' ? value() : value).isAfter(maxDate)
// : false;
const isDisabled = maxDate
? moment(typeof value === 'function' ? value() : value).isAfter(maxDate)
: false;

return (
<li
key={index}
onClick={() => {
if (!isDisabled) {
onClick(executeValue(value));
}
}}
onMouseEnter={() => {
if (!isDisabled) {
onHover(executeValue(value));
}
}}
onMouseLeave={() => {
onHover(null);
}}
>
{label}
</li>
);
})}
</ul>
</div>
);
Expand Down
5 changes: 4 additions & 1 deletion src/PickerInput/Popup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ export interface PopupProps<DateType extends object = any, PresetValue = DateTyp
needConfirm: boolean;
isInvalid: (date: DateType | DateType[]) => boolean;
onOk: VoidFunction;

onPanelMouseDown?: React.MouseEventHandler<HTMLDivElement>;
maxDate?: DateType;
}

export default function Popup<DateType extends object = any>(props: PopupProps<DateType>) {
Expand Down Expand Up @@ -79,6 +79,8 @@ export default function Popup<DateType extends object = any>(props: PopupProps<D
defaultOpenValue,
onOk,
onSubmit,

maxDate,
} = props;

const { prefixCls } = React.useContext(PickerContext);
Expand Down Expand Up @@ -181,6 +183,7 @@ export default function Popup<DateType extends object = any>(props: PopupProps<D
<div className={`${prefixCls}-panel-layout`}>
{/* `any` here since PresetPanel is reused for both Single & Range Picker which means return type is not stable */}
<PresetPanel<any>
maxDate={maxDate}
prefixCls={prefixCls}
presets={presets}
onClick={onPresetSubmit}
Expand Down
1 change: 1 addition & 0 deletions src/PickerInput/RangePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,7 @@ function RangePicker<DateType extends object = any>(
onNow={onNow}
// Render
cellRender={onInternalCellRender}
maxDate={maxDate}
/>
);

Expand Down
1 change: 1 addition & 0 deletions src/PickerInput/SinglePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@ function Picker<DateType extends object = any>(
onNow={onNow}
// Render
cellRender={onInternalCellRender}
maxDate={maxDate}
/>
);

Expand Down
151 changes: 149 additions & 2 deletions tests/picker.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-loop-func */
import { act, createEvent, fireEvent, render } from '@testing-library/react';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import dayjs, { type Dayjs } from 'dayjs';
import moment from 'moment';
import 'moment/locale/zh-cn';
import KeyCode from '@rc-component/util/lib/KeyCode';
Expand Down Expand Up @@ -1197,6 +1196,154 @@ describe('Picker.Basic', () => {
expect(onChange.mock.calls[0][0].format('YYYY-MM-DD')).toEqual('1990-09-04');
});

it('presets - trigger', () => {
const onChange = jest.fn();
const mockHover = jest.fn();

const yesterday = dayjs().subtract(1, 'day');
render(
<DayPicker
{...({
onChange,
onHover: mockHover,
open: true,
presets: [{ label: 'Tomorrow', value: yesterday }],
maxDate: dayjs(),
} as React.ComponentProps<typeof DayPicker> & { onHover?: (date: Dayjs | null) => void })}
/>,
);

const presetEle = document.querySelector('.rc-picker-presets li');
if (!presetEle) throw new Error('Preset element not found');

fireEvent.click(presetEle);
expect(onChange).toHaveBeenCalled();

fireEvent.mouseEnter(presetEle);
// expect(mockHover).toHaveBeenCalled();
});

it('should not trigger onClick when preset date is after maxDate', () => {
const onChange = jest.fn();
const mockHover = jest.fn();
const futureDate = dayjs().add(1, 'day'); // 使用 dayjs 替代 moment
const maxDate = dayjs(); //
const onHover = mockHover as jest.MockedFunction<(date: Dayjs | null) => void>;
render(
<DayPicker
{...{
onChange,
onHover,
open: true,
presets: [{ label: 'Tomorrow', value: futureDate }],
maxDate,
}}
/>,
);

const presetEle = document.querySelector('.rc-picker-presets li');
if (!presetEle) throw new Error('Preset element not found');

fireEvent.click(presetEle);
expect(onChange).not.toHaveBeenCalled();

fireEvent.mouseEnter(presetEle);
expect(mockHover).not.toHaveBeenCalled();
});

it('should not render presets when presets is empty', () => {
const mockHover = jest.fn();
const mockChange = jest.fn();
const onHover = mockHover as jest.MockedFunction<(date: Dayjs | null) => void>;

render(
<DayPicker
{...{
onChange: mockChange,
onHover,
open: true,
presets: [{ label: 'Tomorrow', value: dayjs().add(1, 'day') }],
}}
/>,
);

// 用 DOM 判断是否渲染了 presets
const presetEle = document.querySelector('.rc-picker-presets li');

if (!presetEle) throw new Error('Preset element not found');

fireEvent.click(presetEle);
expect(mockChange).toHaveBeenCalled();
});

it('should not render presets when presets is function', () => {
const mockHover = jest.fn();
const mockChange = jest.fn();
const onHover = mockHover as jest.MockedFunction<(date: Dayjs | null) => void>;
render(
<DayPicker
{...{
onChange: mockChange,
onHover,
open: true,
maxDate: dayjs(),
presets: [{ label: 'Tomorrow', value: () => dayjs().subtract(1, 'day') }],
}}
/>,
);

// 用 DOM 判断是否渲染了 presets
const presetEle = document.querySelector('.rc-picker-presets li');

if (!presetEle) throw new Error('Preset element not found');

fireEvent.click(presetEle);
expect(mockChange).toHaveBeenCalled();
});

it('should allow click when maxDate is not a moment object', () => {
const onChange = jest.fn();

render(
<DayPicker
{...({
onChange,
open: true,
presets: [{ label: 'Valid', value: dayjs().subtract(1, 'day') }],
maxDate: dayjs(), // 👈 不是 moment 对象
} as React.ComponentProps<typeof DayPicker>)}
/>,
);

const presetEle = document.querySelector('.rc-picker-presets li');
if (!presetEle) throw new Error('Preset element not found');

fireEvent.click(presetEle);
expect(onChange).toHaveBeenCalled(); // ✅ 应该能调用
});

it('should allow click when maxDate is not provided', () => {
const onChange = jest.fn();

render(
<DayPicker
{...({
onChange,
open: true,
maxDate: undefined,
presets: [{ label: 'Valid', value: dayjs().add(1, 'day') }],
// 👇 不传 maxDate
} as React.ComponentProps<typeof DayPicker>)}
/>,
);

const presetEle = document.querySelector('.rc-picker-presets li');
if (!presetEle) throw new Error('Preset element not found');

fireEvent.click(presetEle);
expect(onChange).toHaveBeenCalled(); // ✅ 应该能调用
});

it('presets support callback', () => {
const onChange = jest.fn();
const mockPresetValue = jest.fn().mockImplementationOnce(() => getDay('2000-09-03'));
Expand Down