diff --git a/README.md b/README.md index 2cd8c9fbd..c3a271adc 100644 --- a/README.md +++ b/README.md @@ -53,23 +53,22 @@ render(, mountNode); | popupStyle | React.CSSProperties | | customize popup style | | transitionName | String | '' | css class for animation | | locale | Object | import from 'rc-picker/lib/locale/en_US' | rc-picker locale | -| inputReadOnly | Boolean | false | set input to read only | -| allowClear | Boolean | false | whether show clear button | -| autoFocus | Boolean | false | whether auto focus | -| showTime | Boolean \| Object | [showTime options](#showTime-options) | to provide an additional time selection | +| inputReadOnly | boolean | false | set input to read only | +| allowClear | boolean \| { clearIcon?: ReactNode } | false | whether show clear button or customize clear button | +| autoFocus | boolean | false | whether auto focus | +| showTime | boolean \| Object | [showTime options](#showTime-options) | to provide an additional time selection | | picker | time \| date \| week \| month \| year | | control which kind of panel should be shown | | format | String \| String[] | depends on whether you set timePicker and your locale | use to format/parse date(without time) value to/from input. When an array is provided, all values are used for parsing and first value for display | -| use12Hours | Boolean | false | 12 hours display mode | +| use12Hours | boolean | false | 12 hours display mode | | value | moment | | current value like input's value | | defaultValue | moment | | defaultValue like input's defaultValue | -| open | Boolean | false | current open state of picker. controlled prop | +| open | boolean | false | current open state of picker. controlled prop | | suffixIcon | ReactNode | | The custom suffix icon | -| clearIcon | ReactNode | | The custom clear icon | | prevIcon | ReactNode | | The custom prev icon | | nextIcon | ReactNode | | The custom next icon | | superPrevIcon | ReactNode | | The custom super prev icon | | superNextIcon | ReactNode | | The custom super next icon | -| disabled | Boolean | false | whether the picker is disabled | +| disabled | boolean | false | whether the picker is disabled | | placeholder | String | | picker input's placeholder | | getPopupContainer | function(trigger) | | to set the container of the floating layer, while the default is to create a div element in body | | onChange | Function(date: moment, dateString: string) | | a callback function, can be executed when the selected time is changing | @@ -93,9 +92,9 @@ render(, mountNode); | mode | time \| datetime \| date \| week \| month \| year \| decade | | control which kind of panel | | picker | time \| date \| week \| month \| year | | control which kind of panel | | tabIndex | Number | 0 | view [tabIndex](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) | -| showTime | Boolean \| Object | [showTime options](#showTime-options) | to provide an additional time selection | -| showToday | Boolean | false | whether to show today button | -| disabledDate | Function(date:moment) => Boolean | | whether to disable select of current date | +| showTime | boolean \| Object | [showTime options](#showTime-options) | to provide an additional time selection | +| showToday | boolean | false | whether to show today button | +| disabledDate | Function(date:moment) => boolean | | whether to disable select of current date | | dateRender | Function(currentDate:moment, today:moment) => React.Node | | custom rendering function for date cells | | monthCellRender | Function(currentDate:moment, locale:Locale) => React.Node | | Custom month cell render method | | renderExtraFooter | (mode) => React.Node | | extra footer | @@ -118,33 +117,33 @@ render(, mountNode); | separator | String | '~' | set separator between inputs | | picker | time \| date \| week \| month \| year | | control which kind of panel | | placeholder | [String, String] | | placeholder of date input | -| showTime | Boolean \| Object | [showTime options](#showTime-options) | to provide an additional time selection | +| showTime | boolean \| Object | [showTime options](#showTime-options) | to provide an additional time selection | | showTime.defaultValue | [moment, moment] | | to set default time of selected date | -| use12Hours | Boolean | false | 12 hours display mode | +| use12Hours | boolean | false | 12 hours display mode | | disabledTime | Function(date: moment, type:'start'\|'end'):Object | | | to specify the time that cannot be selected | | ranges | { String \| [range: string]: moment[] } \| { [range: string]: () => moment[] } | | preseted ranges for quick selection | | format | String \| String[] | depends on whether you set timePicker and your locale | use to format/parse date(without time) value to/from input. When an array is provided, all values are used for parsing and first value for display | -| allowEmpty | [Boolean, Boolean] | | allow range picker clearing text | -| selectable | [Boolean, Boolean] | | whether to selected picker | -| disabled | Boolean | false | whether the range picker is disabled | +| allowEmpty | [boolean, boolean] | | allow range picker clearing text | +| selectable | [boolean, boolean] | | whether to selected picker | +| disabled | boolean | false | whether the range picker is disabled | | onChange | Function(value:[moment], formatString: [string, string]) | | a callback function, can be executed when the selected time is changing | | onCalendarChange | Function(value:[moment], formatString: [string, string], info: { range:'start'\|'end' }) | | a callback function, can be executed when the start time or the end time of the range is changing | | direction | String: ltr or rtl | | Layout direction of picker component, it supports RTL direction too. | -| order | Boolean | true | (TimeRangePicker only) `false` to disable auto order | +| order | boolean | true | (TimeRangePicker only) `false` to disable auto order | ### showTime-options | Property | Type | Default | Description | | ------------------- | ------- | ------- | ---------------------------------- | | format | String | | moment format | -| showHour | Boolean | true | whether show hour | -| showMinute | Boolean | true | whether show minute | -| showSecond | Boolean | true | whether show second | -| use12Hours | Boolean | false | 12 hours display mode | +| showHour | boolean | true | whether show hour | +| showMinute | boolean | true | whether show minute | +| showSecond | boolean | true | whether show second | +| use12Hours | boolean | false | 12 hours display mode | | hourStep | Number | 1 | interval between hours in picker | | minuteStep | Number | 1 | interval between minutes in picker | | secondStep | Number | 1 | interval between seconds in picker | -| hideDisabledOptions | Boolean | false | whether hide disabled options | +| hideDisabledOptions | boolean | false | whether hide disabled options | | defaultValue | moment | null | default initial value | ## Development diff --git a/src/Picker.tsx b/src/Picker.tsx index af70305da..85e7b2525 100644 --- a/src/Picker.tsx +++ b/src/Picker.tsx @@ -37,6 +37,7 @@ import { formatValue, isEqual, parseValue } from './utils/dateUtil'; import { toArray } from './utils/miscUtil'; import { elementsContains, getDefaultFormat, getInputSize } from './utils/uiUtil'; import { legacyPropsWarning } from './utils/warnUtil'; +import { getClearIcon } from './utils/getClearIcon'; export type PickerRefConfig = { focus: () => void; @@ -49,7 +50,7 @@ export type PickerSharedProps = { popupStyle?: React.CSSProperties; transitionName?: string; placeholder?: string; - allowClear?: boolean; + allowClear?: boolean | { clearIcon?: React.ReactNode }; autoFocus?: boolean; disabled?: boolean; tabIndex?: number; @@ -66,6 +67,10 @@ export type PickerSharedProps = { // Render suffixIcon?: React.ReactNode; + /** + * Clear all icon + * @deprecated Please use `allowClear` instead + **/ clearIcon?: React.ReactNode; prevIcon?: React.ReactNode; nextIcon?: React.ReactNode; @@ -477,28 +482,41 @@ function InnerPicker(props: PickerProps) { ); } - let clearNode: React.ReactNode; - if (allowClear && mergedValue && !disabled) { - clearNode = ( - { - e.preventDefault(); - e.stopPropagation(); - }} - onMouseUp={(e) => { - e.preventDefault(); - e.stopPropagation(); - triggerChange(null); - triggerOpen(false); - }} - className={`${prefixCls}-clear`} - role="button" - > - {clearIcon || } - + // ============================ Clear ============================ + if (process.env.NODE_ENV !== 'production') { + warning( + !props.clearIcon, + '`clearIcon` will be removed in future. Please use `allowClear` instead.', ); } + const mergedClearIcon: React.ReactNode = getClearIcon( + prefixCls, + allowClear, + clearIcon, + ); + + const clearNode: React.ReactNode = ( + { + e.preventDefault(); + e.stopPropagation(); + }} + onMouseUp={(e) => { + e.preventDefault(); + e.stopPropagation(); + triggerChange(null); + triggerOpen(false); + }} + className={`${prefixCls}-clear`} + role="button" + > + {mergedClearIcon} + + ); + + const mergedAllowClear = !!allowClear && mergedValue && !disabled; + const mergedInputProps: React.InputHTMLAttributes & { ref: React.MutableRefObject } = { id, tabIndex, @@ -515,7 +533,7 @@ function InnerPicker(props: PickerProps) { ...inputProps, size: getInputSize(picker, formatList[0], generateConfig), name, - ...pickAttrs(props, { aria: true, data: true}), + ...pickAttrs(props, { aria: true, data: true }), autoComplete, }; @@ -590,7 +608,7 @@ function InnerPicker(props: PickerProps) { > {inputNode} {suffixNode} - {clearNode} + {mergedAllowClear && clearNode} diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index 38f9b8db3..9421e8585 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -48,6 +48,7 @@ import getRanges from './utils/getRanges'; import { getValue, toArray, updateValues } from './utils/miscUtil'; import { elementsContains, getDefaultFormat, getInputSize } from './utils/uiUtil'; import { legacyPropsWarning } from './utils/warnUtil'; +import { getClearIcon } from './utils/getClearIcon'; function reorderValues( values: RangeValue, @@ -689,18 +690,18 @@ function InnerRangePicker(props: RangePickerProps) { const startStr = mergedValue && mergedValue[0] ? formatValue(mergedValue[0], { - locale, - format: 'YYYYMMDDHHmmss', - generateConfig, - }) + locale, + format: 'YYYYMMDDHHmmss', + generateConfig, + }) : ''; const endStr = mergedValue && mergedValue[1] ? formatValue(mergedValue[1], { - locale, - format: 'YYYYMMDDHHmmss', - generateConfig, - }) + locale, + format: 'YYYYMMDDHHmmss', + generateConfig, + }) : ''; useEffect(() => { @@ -752,6 +753,10 @@ function InnerRangePicker(props: RangePickerProps) { } warning(!dateRender, `'dateRender' is deprecated. Please use 'cellRender' instead.`); warning(!monthCellRender, `'monthCellRender' is deprecated. Please use 'cellRender' instead.`); + warning( + !clearIcon, + '`clearIcon` will be removed in future. Please use `allowClear` instead.', + ); } // ============================ Private ============================ @@ -859,7 +864,7 @@ function InnerRangePicker(props: RangePickerProps) { defaultValue={ mergedActivePickerIndex === 0 ? getValue(selectedValue, 1) : getValue(selectedValue, 0) } - // defaultPickerValue={undefined} + // defaultPickerValue={undefined} /> ); @@ -1038,39 +1043,44 @@ function InnerRangePicker(props: RangePickerProps) { ); } - let clearNode: React.ReactNode; - if ( - allowClear && - ((getValue(mergedValue, 0) && !mergedDisabled[0]) || - (getValue(mergedValue, 1) && !mergedDisabled[1])) - ) { - clearNode = ( - { - e.preventDefault(); - e.stopPropagation(); - }} - onMouseUp={(e) => { - e.preventDefault(); - e.stopPropagation(); - let values = mergedValue; + const mergedClearIcon: React.ReactNode = getClearIcon( + prefixCls, + allowClear, + clearIcon, + ); - if (!mergedDisabled[0]) { - values = updateValues(values, null, 0); - } - if (!mergedDisabled[1]) { - values = updateValues(values, null, 1); - } + const clearNode: React.ReactNode = ( + { + e.preventDefault(); + e.stopPropagation(); + }} + onMouseUp={(e) => { + e.preventDefault(); + e.stopPropagation(); + let values = mergedValue; - triggerChange(values, null); - triggerOpen(false, mergedActivePickerIndex, 'clear'); - }} - className={`${prefixCls}-clear`} - > - {clearIcon || } - - ); - } + if (!mergedDisabled[0]) { + values = updateValues(values, null, 0); + } + if (!mergedDisabled[1]) { + values = updateValues(values, null, 1); + } + + triggerChange(values, null); + triggerOpen(false, mergedActivePickerIndex, 'clear'); + }} + className={`${prefixCls}-clear`} + role="button" + > + {mergedClearIcon} + + ); + + const mergedAllowClear = allowClear && ( + (getValue(mergedValue as RangeValue, 0) && !mergedDisabled[0]) || + (getValue(mergedValue as RangeValue, 1) && !mergedDisabled[1]) + ); const inputSharedProps = { size: getInputSize(picker, formatList[0], generateConfig), @@ -1209,7 +1219,7 @@ function InnerRangePicker(props: RangePickerProps) { }} /> {suffixNode} - {clearNode} + {mergedAllowClear && clearNode} diff --git a/src/utils/getClearIcon.tsx b/src/utils/getClearIcon.tsx new file mode 100644 index 000000000..af0ced5a4 --- /dev/null +++ b/src/utils/getClearIcon.tsx @@ -0,0 +1,15 @@ +import type { ReactNode } from "react"; +import React from "react"; + +export function getClearIcon( + prefixCls, + allowClear?: boolean | { clearIcon?: ReactNode }, + clearIcon?: ReactNode, +) { + + const mergedClearIcon = typeof allowClear === "object" ? allowClear.clearIcon : clearIcon; + + return ( + mergedClearIcon || + ); +} \ No newline at end of file diff --git a/tests/__snapshots__/range.spec.tsx.snap b/tests/__snapshots__/range.spec.tsx.snap index 820a81c6f..cc4931eab 100644 --- a/tests/__snapshots__/range.spec.tsx.snap +++ b/tests/__snapshots__/range.spec.tsx.snap @@ -45,6 +45,7 @@ exports[`Picker.Range icon 1`] = ` { + let errorSpy; beforeEach(() => { jest.useFakeTimers().setSystemTime(fakeTime); }); + beforeAll(() => { + errorSpy = jest.spyOn(console, 'error').mockImplementation(() => null); + }); + + beforeEach(() => { + errorSpy.mockReset(); + resetWarned(); + }); afterAll(() => { jest.clearAllTimers(); jest.useRealTimers(); @@ -535,6 +544,7 @@ describe('Picker.Basic', () => { }); it('icon', () => { + expect(errorSpy).not.toHaveBeenCalled(); render( { allowClear />, ); - expect(document.querySelector('.rc-picker-input')).toMatchSnapshot(); + expect(errorSpy).toHaveBeenCalledWith( + 'Warning: `clearIcon` will be removed in future. Please use `allowClear` instead.' + ); }); it('inputRender', () => { @@ -611,34 +623,28 @@ describe('Picker.Basic', () => { }); it('should show warning when hour step is invalid', () => { - const spy = jest.spyOn(console, 'error').mockImplementation(() => {}); - expect(spy).not.toBeCalled(); + expect(errorSpy).not.toBeCalled(); const { container } = render(); openPicker(container); - expect(spy).toBeCalledWith('Warning: `hourStep` 9 is invalid. It should be a factor of 24.'); - spy.mockRestore(); + expect(errorSpy).toBeCalledWith('Warning: `hourStep` 9 is invalid. It should be a factor of 24.'); }); it('should show warning when minute step is invalid', () => { - const spy = jest.spyOn(console, 'error').mockImplementation(() => {}); - expect(spy).not.toBeCalled(); + expect(errorSpy).not.toBeCalled(); const { container } = render(); openPicker(container); - expect(spy).toBeCalledWith( + expect(errorSpy).toBeCalledWith( 'Warning: `minuteStep` 9 is invalid. It should be a factor of 60.', ); - spy.mockRestore(); }); it('should show warning when second step is invalid', () => { - const spy = jest.spyOn(console, 'error').mockImplementation(() => {}); - expect(spy).not.toBeCalled(); + expect(errorSpy).not.toBeCalled(); const { container } = render(); openPicker(container); - expect(spy).toBeCalledWith( + expect(errorSpy).toBeCalledWith( 'Warning: `secondStep` 9 is invalid. It should be a factor of 60.', ); - spy.mockRestore(); }); // https://github.com/ant-design/ant-design/issues/40914 @@ -650,7 +656,7 @@ describe('Picker.Basic', () => { const { container } = render(); openPicker(container); - const column = document.querySelector(`.rc-picker-time-panel-column:nth-child(${index+1})`); + const column = document.querySelector(`.rc-picker-time-panel-column:nth-child(${index + 1})`); expect(column).toBeTruthy(); const cells = column.querySelectorAll('.rc-picker-time-panel-cell-inner'); @@ -742,7 +748,7 @@ describe('Picker.Basic', () => { it('defaultOpenValue in timePicker', () => { resetWarned(); const onChange = jest.fn(); - const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const errSpy = jest.spyOn(console, 'error').mockImplementation(() => { }); const { container } = render( { expect(document.querySelector('input').value).toEqual(''); }); + it('custom clear icon', () => { + render( + clear }} + defaultValue={getMoment('2020-09-17')} + />, + ); + + // clear + expect(document.querySelector('.custom-clear')).toBeTruthy(); + clearValue(); + expect(document.querySelector('input').value).toEqual(''); + }); + it('panelRender', () => { render(

Light

} />); expect(document.querySelector('.rc-picker')).toMatchSnapshot(); diff --git a/tests/range.spec.tsx b/tests/range.spec.tsx index 11ca11940..5dae9aef9 100644 --- a/tests/range.spec.tsx +++ b/tests/range.spec.tsx @@ -20,8 +20,15 @@ import { selectCell, } from './util/commonUtil'; import type { RangePickerProps } from '../src/RangePicker'; +import { resetWarned } from 'rc-util/lib/warning'; describe('Picker.Range', () => { + let errorSpy; + + beforeAll(() => { + errorSpy = jest.spyOn(console, 'error').mockImplementation(() => null); + }); + function matchValues(container: HTMLElement, value1: string, value2: string) { expect(container.querySelectorAll('input')[0].value).toEqual(value1); expect(container.querySelectorAll('input')[1].value).toEqual(value2); @@ -33,6 +40,8 @@ describe('Picker.Range', () => { } beforeEach(() => { + errorSpy.mockReset(); + resetWarned(); global.scrollCalled = false; jest.useFakeTimers().setSystemTime(getMoment('1990-09-03 00:00:00').valueOf()); }); @@ -319,13 +328,11 @@ describe('Picker.Range', () => { }); it('null value with disabled', () => { - const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); render(); - expect(errSpy).toHaveBeenCalledWith( + expect(errorSpy).toHaveBeenCalledWith( 'Warning: `disabled` should not set with empty `value`. You should set `allowEmpty` or `value` instead.', ); - errSpy.mockReset(); }); it('clear should trigger change', () => { @@ -747,6 +754,9 @@ describe('Picker.Range', () => { ); expect(container).toMatchSnapshot(); + expect(errorSpy).toHaveBeenCalledWith( + 'Warning: `clearIcon` will be removed in future. Please use `allowClear` instead.' + ); }); it('block native mouseDown in panel to prevent focus changed', () => { @@ -1101,19 +1111,19 @@ describe('Picker.Range', () => { targetCell: string; match: string[]; }[] = [ - { - picker: 'week', - defaultValue: ['2020-06-13'], - targetCell: '9', - match: ['2020-24th'], - }, - { - picker: 'quarter', - defaultValue: ['2020-03-30', '2020-05-20'], - targetCell: 'Q1', - match: ['2020-Q1'], - }, - ]; + { + picker: 'week', + defaultValue: ['2020-06-13'], + targetCell: '9', + match: ['2020-24th'], + }, + { + picker: 'quarter', + defaultValue: ['2020-03-30', '2020-05-20'], + targetCell: 'Q1', + match: ['2020-Q1'], + }, + ]; list.forEach(({ picker, defaultValue, match, targetCell }) => { it(picker, () => { @@ -1310,7 +1320,7 @@ describe('Picker.Range', () => { openPicker(container, 1); inputValue('1989-01-07', 1); - console.log('close!'); + keyDown(container, 1, KeyCode.ENTER); expect(isOpen()).toBeTruthy(); @@ -1595,8 +1605,6 @@ describe('Picker.Range', () => { fireEvent.focus(document.querySelectorAll('input')[0]); inputValue('', 0); - console.log(container.querySelector('.rc-picker').innerHTML); - // reselect date selectCell(9, 0); expect(document.querySelectorAll('input')[0].value).toBe('1990-09-09'); @@ -1898,4 +1906,19 @@ describe('Picker.Range', () => { expect(document.querySelector('.rc-picker-cell-disabled')).toBeFalsy(); }); + + it('custom clear icon', () => { + render( + clear
}} + defaultValue={[getMoment('2000-09-03'), getMoment('2000-09-03')]} + />, + ); + + // clear + expect(document.querySelector('.custom-clear')).toBeTruthy(); + clearValue(); + expect(document.querySelector('input').value).toEqual(''); + }); + }); diff --git a/tests/util/commonUtil.tsx b/tests/util/commonUtil.tsx index 8e91df9a8..e3ccb7c15 100644 --- a/tests/util/commonUtil.tsx +++ b/tests/util/commonUtil.tsx @@ -175,7 +175,7 @@ export function confirmOK() { } export function clearValue() { - const clearBtn = document.querySelector('.rc-picker-clear-btn'); + const clearBtn = document.querySelector('.rc-picker-clear'); fireEvent.mouseDown(clearBtn); fireEvent.mouseUp(clearBtn); }