diff --git a/components/_util/motion.tsx b/components/_util/motion.tsx
index 1c69a9f05a..edac9ccda1 100644
--- a/components/_util/motion.tsx
+++ b/components/_util/motion.tsx
@@ -1,5 +1,6 @@
import { CSSMotionProps, MotionEventHandler, MotionEndEventHandler } from 'rc-motion';
import { MotionEvent } from 'rc-motion/lib/interface';
+import { tuple } from './type';
// ================== Collapse Motion ==================
const getCollapsedHeight: MotionEventHandler = () => ({ height: 0, opacity: 0 });
@@ -25,11 +26,21 @@ const collapseMotion: CSSMotionProps = {
motionDeadline: 500,
};
+const SelectPlacements = tuple('bottomLeft', 'bottomRight', 'topLeft', 'topRight');
+export type SelectCommonPlacement = typeof SelectPlacements[number];
+
+const getTransitionDirection = (placement: SelectCommonPlacement | undefined) => {
+ if (placement !== undefined && (placement === 'topLeft' || placement === 'topRight')) {
+ return `slide-down`;
+ }
+ return `slide-up`;
+};
+
const getTransitionName = (rootPrefixCls: string, motion: string, transitionName?: string) => {
if (transitionName !== undefined) {
return transitionName;
}
return `${rootPrefixCls}-${motion}`;
};
-export { getTransitionName };
+export { getTransitionName, getTransitionDirection };
export default collapseMotion;
diff --git a/components/tooltip/placements.tsx b/components/_util/placements.tsx
similarity index 97%
rename from components/tooltip/placements.tsx
rename to components/_util/placements.tsx
index 3fc9152268..eb4dead8a8 100644
--- a/components/tooltip/placements.tsx
+++ b/components/_util/placements.tsx
@@ -42,6 +42,7 @@ export default function getPlacements(config: PlacementsConfig) {
horizontalArrowShift = 16,
verticalArrowShift = 8,
autoAdjustOverflow,
+ arrowPointAtCenter,
} = config;
const placementMap: BuildInPlacements = {
left: {
@@ -94,7 +95,7 @@ export default function getPlacements(config: PlacementsConfig) {
},
};
Object.keys(placementMap).forEach(key => {
- placementMap[key] = config.arrowPointAtCenter
+ placementMap[key] = arrowPointAtCenter
? {
...placementMap[key],
overflow: getOverflowOptions(autoAdjustOverflow),
diff --git a/components/_util/statusUtils.tsx b/components/_util/statusUtils.tsx
new file mode 100644
index 0000000000..25153ba93f
--- /dev/null
+++ b/components/_util/statusUtils.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
+import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled';
+import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
+import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
+import classNames from 'classnames';
+import { ValidateStatus } from '../form/FormItem';
+import { tuple } from './type';
+
+const InputStatuses = tuple('warning', 'error', '');
+export type InputStatus = typeof InputStatuses[number];
+
+const iconMap = {
+ success: CheckCircleFilled,
+ warning: ExclamationCircleFilled,
+ error: CloseCircleFilled,
+ validating: LoadingOutlined,
+};
+
+export const getFeedbackIcon = (prefixCls: string, status?: ValidateStatus) => {
+ const IconNode = status && iconMap[status];
+ return IconNode ? (
+
+
+
+ ) : null;
+};
+
+export function getStatusClassNames(
+ prefixCls: string,
+ status?: ValidateStatus,
+ hasFeedback?: boolean,
+) {
+ return classNames({
+ [`${prefixCls}-status-success`]: status === 'success',
+ [`${prefixCls}-status-warning`]: status === 'warning',
+ [`${prefixCls}-status-error`]: status === 'error',
+ [`${prefixCls}-status-validating`]: status === 'validating',
+ [`${prefixCls}-has-feedback`]: hasFeedback,
+ });
+}
+
+export const getMergedStatus = (contextStatus?: ValidateStatus, customStatus?: InputStatus) =>
+ customStatus || contextStatus;
diff --git a/components/auto-complete/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/auto-complete/__tests__/__snapshots__/demo-extend.test.ts.snap
index 6bedc29b73..08fcb1420c 100644
--- a/components/auto-complete/__tests__/__snapshots__/demo-extend.test.ts.snap
+++ b/components/auto-complete/__tests__/__snapshots__/demo-extend.test.ts.snap
@@ -107,7 +107,7 @@ exports[`renders ./components/auto-complete/demo/certain-category.md extend cont
class="ant-select-selection-search"
>
`;
+exports[`renders ./components/auto-complete/demo/status.md extend context correctly 1`] = `
+
+`;
+
exports[`renders ./components/auto-complete/demo/uncertain-category.md extend context correctly 1`] = `
-
`;
exports[`ConfigProvider components Slider configProvider componentSize middle 1`] = `
@@ -22058,22 +22052,19 @@ exports[`ConfigProvider components Slider configProvider componentSize middle 1`
-
`;
exports[`ConfigProvider components Slider configProvider virtual and dropdownMatchSelectWidth 1`] = `
@@ -22112,22 +22103,19 @@ exports[`ConfigProvider components Slider configProvider virtual and dropdownMat
-
`;
exports[`ConfigProvider components Slider normal 1`] = `
@@ -22166,22 +22154,19 @@ exports[`ConfigProvider components Slider normal 1`] = `
-
`;
exports[`ConfigProvider components Slider prefixCls 1`] = `
@@ -22220,9 +22205,6 @@ exports[`ConfigProvider components Slider prefixCls 1`] = `
-
`;
diff --git a/components/date-picker/__tests__/DatePicker.test.js b/components/date-picker/__tests__/DatePicker.test.js
index 51cc73d956..7da679cdf7 100644
--- a/components/date-picker/__tests__/DatePicker.test.js
+++ b/components/date-picker/__tests__/DatePicker.test.js
@@ -199,4 +199,36 @@ describe('DatePicker', () => {
expect(year).toBe(startDate.format('YYYY'));
expect(wrapper.find('.ant-picker-time-panel').length).toBe(1);
});
+
+ it('placement api work correctly ', () => {
+ const popupAlignDefault = (points = ['tl', 'bl'], offset = [0, 4]) => ({
+ points,
+ offset,
+ overflow: {
+ adjustX: 1,
+ adjustY: 1,
+ },
+ });
+
+ const wrapper = mount(
+ ,
+ );
+ expect(wrapper.find('Trigger').prop('popupAlign')).toEqual(popupAlignDefault(['tl', 'bl']));
+ wrapper.setProps({
+ placement: 'bottomRight',
+ });
+ expect(wrapper.find('Trigger').prop('popupAlign')).toEqual(popupAlignDefault(['tr', 'br']));
+ wrapper.setProps({
+ placement: 'topLeft',
+ });
+ expect(wrapper.find('Trigger').prop('popupAlign')).toEqual(
+ popupAlignDefault(['bl', 'tl'], [0, -4]),
+ );
+ wrapper.setProps({
+ placement: 'topRight',
+ });
+ expect(wrapper.find('Trigger').prop('popupAlign')).toEqual(
+ popupAlignDefault(['br', 'tr'], [0, -4]),
+ );
+ });
});
diff --git a/components/date-picker/__tests__/RangePicker.test.js b/components/date-picker/__tests__/RangePicker.test.js
index 26a347abef..de0249f790 100644
--- a/components/date-picker/__tests__/RangePicker.test.js
+++ b/components/date-picker/__tests__/RangePicker.test.js
@@ -5,6 +5,7 @@ import DatePicker from '..';
import { setMockDate, resetMockDate } from '../../../tests/utils';
import { openPicker, selectCell, closePicker } from './utils';
import focusTest from '../../../tests/shared/focusTest';
+import enUS from '../locale/en_US';
const { RangePicker } = DatePicker;
@@ -96,4 +97,10 @@ describe('RangePicker', () => {
expect(wrapper.find('input').first().props().placeholder).toEqual('Start date');
expect(wrapper.find('input').last().props().placeholder).toEqual('End date');
});
+
+ it('RangePicker picker quarter placeholder', () => {
+ const wrapper = mount( );
+ expect(wrapper.find('input').at(0).props().placeholder).toEqual('Start quarter');
+ expect(wrapper.find('input').at(1).props().placeholder).toEqual('End quarter');
+ });
});
diff --git a/components/date-picker/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/date-picker/__tests__/__snapshots__/demo-extend.test.ts.snap
index 47a53a4e9f..b690f01b3e 100644
--- a/components/date-picker/__tests__/__snapshots__/demo-extend.test.ts.snap
+++ b/components/date-picker/__tests__/__snapshots__/demo-extend.test.ts.snap
@@ -28598,6 +28598,1867 @@ exports[`renders ./components/date-picker/demo/mode.md extend context correctly
`;
+exports[`renders ./components/date-picker/demo/placement.md extend context correctly 1`] = `
+Array [
+
+
+
+
+
+
+
+ topLeft
+
+
+
+
+
+
+
+
+ topRight
+
+
+
+
+
+
+
+
+ bottomLeft
+
+
+
+
+
+
+
+
+ bottomRight
+
+
+
,
+ ,
+ ,
+ ,
+
+
+
+
+
+
+
+
+
+
+
+ Su
+
+
+ Mo
+
+
+ Tu
+
+
+ We
+
+
+ Th
+
+
+ Fr
+
+
+ Sa
+
+
+
+
+
+
+
+ 30
+
+
+
+
+ 31
+
+
+
+
+ 1
+
+
+
+
+ 2
+
+
+
+
+ 3
+
+
+
+
+ 4
+
+
+
+
+ 5
+
+
+
+
+
+
+ 6
+
+
+
+
+ 7
+
+
+
+
+ 8
+
+
+
+
+ 9
+
+
+
+
+ 10
+
+
+
+
+ 11
+
+
+
+
+ 12
+
+
+
+
+
+
+ 13
+
+
+
+
+ 14
+
+
+
+
+ 15
+
+
+
+
+ 16
+
+
+
+
+ 17
+
+
+
+
+ 18
+
+
+
+
+ 19
+
+
+
+
+
+
+ 20
+
+
+
+
+ 21
+
+
+
+
+ 22
+
+
+
+
+ 23
+
+
+
+
+ 24
+
+
+
+
+ 25
+
+
+
+
+ 26
+
+
+
+
+
+
+ 27
+
+
+
+
+ 28
+
+
+
+
+ 29
+
+
+
+
+ 30
+
+
+
+
+ 1
+
+
+
+
+ 2
+
+
+
+
+ 3
+
+
+
+
+
+
+ 4
+
+
+
+
+ 5
+
+
+
+
+ 6
+
+
+
+
+ 7
+
+
+
+
+ 8
+
+
+
+
+ 9
+
+
+
+
+ 10
+
+
+
+
+
+
+
+
+
+
+
+
,
+ ,
+ ,
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
,
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Su
+
+
+ Mo
+
+
+ Tu
+
+
+ We
+
+
+ Th
+
+
+ Fr
+
+
+ Sa
+
+
+
+
+
+
+
+ 30
+
+
+
+
+ 31
+
+
+
+
+ 1
+
+
+
+
+ 2
+
+
+
+
+ 3
+
+
+
+
+ 4
+
+
+
+
+ 5
+
+
+
+
+
+
+ 6
+
+
+
+
+ 7
+
+
+
+
+ 8
+
+
+
+
+ 9
+
+
+
+
+ 10
+
+
+
+
+ 11
+
+
+
+
+ 12
+
+
+
+
+
+
+ 13
+
+
+
+
+ 14
+
+
+
+
+ 15
+
+
+
+
+ 16
+
+
+
+
+ 17
+
+
+
+
+ 18
+
+
+
+
+ 19
+
+
+
+
+
+
+ 20
+
+
+
+
+ 21
+
+
+
+
+ 22
+
+
+
+
+ 23
+
+
+
+
+ 24
+
+
+
+
+ 25
+
+
+
+
+ 26
+
+
+
+
+
+
+ 27
+
+
+
+
+ 28
+
+
+
+
+ 29
+
+
+
+
+ 30
+
+
+
+
+ 1
+
+
+
+
+ 2
+
+
+
+
+ 3
+
+
+
+
+
+
+ 4
+
+
+
+
+ 5
+
+
+
+
+ 6
+
+
+
+
+ 7
+
+
+
+
+ 8
+
+
+
+
+ 9
+
+
+
+
+ 10
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Su
+
+
+ Mo
+
+
+ Tu
+
+
+ We
+
+
+ Th
+
+
+ Fr
+
+
+ Sa
+
+
+
+
+
+
+
+ 27
+
+
+
+
+ 28
+
+
+
+
+ 29
+
+
+
+
+ 30
+
+
+
+
+ 1
+
+
+
+
+ 2
+
+
+
+
+ 3
+
+
+
+
+
+
+ 4
+
+
+
+
+ 5
+
+
+
+
+ 6
+
+
+
+
+ 7
+
+
+
+
+ 8
+
+
+
+
+ 9
+
+
+
+
+ 10
+
+
+
+
+
+
+ 11
+
+
+
+
+ 12
+
+
+
+
+ 13
+
+
+
+
+ 14
+
+
+
+
+ 15
+
+
+
+
+ 16
+
+
+
+
+ 17
+
+
+
+
+
+
+ 18
+
+
+
+
+ 19
+
+
+
+
+ 20
+
+
+
+
+ 21
+
+
+
+
+ 22
+
+
+
+
+ 23
+
+
+
+
+ 24
+
+
+
+
+
+
+ 25
+
+
+
+
+ 26
+
+
+
+
+ 27
+
+
+
+
+ 28
+
+
+
+
+ 29
+
+
+
+
+ 30
+
+
+
+
+ 31
+
+
+
+
+
+
+ 1
+
+
+
+
+ 2
+
+
+
+
+ 3
+
+
+
+
+ 4
+
+
+
+
+ 5
+
+
+
+
+ 6
+
+
+
+
+ 7
+
+
+
+
+
+
+
+
+
+
+
+
+
,
+]
+`;
+
exports[`renders ./components/date-picker/demo/presetted-ranges.md extend context correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Q1
+
+
+
+
+ Q2
+
+
+
+
+ Q3
+
+
+
+
+ Q4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Q1
+
+
+
+
+ Q2
+
+
+
+
+ Q3
+
+
+
+
+ Q4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -45044,6 +47204,1211 @@ exports[`renders ./components/date-picker/demo/start-end.md extend context corre
`;
+exports[`renders ./components/date-picker/demo/status.md extend context correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Su
+
+
+ Mo
+
+
+ Tu
+
+
+ We
+
+
+ Th
+
+
+ Fr
+
+
+ Sa
+
+
+
+
+
+
+
+ 30
+
+
+
+
+ 31
+
+
+
+
+ 1
+
+
+
+
+ 2
+
+
+
+
+ 3
+
+
+
+
+ 4
+
+
+
+
+ 5
+
+
+
+
+
+
+ 6
+
+
+
+
+ 7
+
+
+
+
+ 8
+
+
+
+
+ 9
+
+
+
+
+ 10
+
+
+
+
+ 11
+
+
+
+
+ 12
+
+
+
+
+
+
+ 13
+
+
+
+
+ 14
+
+
+
+
+ 15
+
+
+
+
+ 16
+
+
+
+
+ 17
+
+
+
+
+ 18
+
+
+
+
+ 19
+
+
+
+
+
+
+ 20
+
+
+
+
+ 21
+
+
+
+
+ 22
+
+
+
+
+ 23
+
+
+
+
+ 24
+
+
+
+
+ 25
+
+
+
+
+ 26
+
+
+
+
+
+
+ 27
+
+
+
+
+ 28
+
+
+
+
+ 29
+
+
+
+
+ 30
+
+
+
+
+ 1
+
+
+
+
+ 2
+
+
+
+
+ 3
+
+
+
+
+
+
+ 4
+
+
+
+
+ 5
+
+
+
+
+ 6
+
+
+
+
+ 7
+
+
+
+
+ 8
+
+
+
+
+ 9
+
+
+
+
+ 10
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Su
+
+
+ Mo
+
+
+ Tu
+
+
+ We
+
+
+ Th
+
+
+ Fr
+
+
+ Sa
+
+
+
+
+
+
+
+ 30
+
+
+
+
+ 31
+
+
+
+
+ 1
+
+
+
+
+ 2
+
+
+
+
+ 3
+
+
+
+
+ 4
+
+
+
+
+ 5
+
+
+
+
+
+
+ 6
+
+
+
+
+ 7
+
+
+
+
+ 8
+
+
+
+
+ 9
+
+
+
+
+ 10
+
+
+
+
+ 11
+
+
+
+
+ 12
+
+
+
+
+
+
+ 13
+
+
+
+
+ 14
+
+
+
+
+ 15
+
+
+
+
+ 16
+
+
+
+
+ 17
+
+
+
+
+ 18
+
+
+
+
+ 19
+
+
+
+
+
+
+ 20
+
+
+
+
+ 21
+
+
+
+
+ 22
+
+
+
+
+ 23
+
+
+
+
+ 24
+
+
+
+
+ 25
+
+
+
+
+ 26
+
+
+
+
+
+
+ 27
+
+
+
+
+ 28
+
+
+
+
+ 29
+
+
+
+
+ 30
+
+
+
+
+ 1
+
+
+
+
+ 2
+
+
+
+
+ 3
+
+
+
+
+
+
+ 4
+
+
+
+
+ 5
+
+
+
+
+ 6
+
+
+
+
+ 7
+
+
+
+
+ 8
+
+
+
+
+ 9
+
+
+
+
+ 10
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
exports[`renders ./components/date-picker/demo/suffix.md extend context correctly 1`] = `
`;
+exports[`renders ./components/date-picker/demo/placement.md correctly 1`] = `
+Array [
+
+
+
+
+
+
+
+ topLeft
+
+
+
+
+
+
+
+
+ topRight
+
+
+
+
+
+
+
+
+ bottomLeft
+
+
+
+
+
+
+
+
+ bottomRight
+
+
+
,
+
,
+
,
+
,
+
,
+
,
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
,
+]
+`;
+
exports[`renders ./components/date-picker/demo/presetted-ranges.md correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -3432,6 +3728,103 @@ exports[`renders ./components/date-picker/demo/start-end.md correctly 1`] = `
`;
+exports[`renders ./components/date-picker/demo/status.md correctly 1`] = `
+
+`;
+
exports[`renders ./components/date-picker/demo/suffix.md correctly 1`] = `
{
+ const [placement, SetPlacement] = React.useState('topLeft');
+
+ const placementChange = e => {
+ SetPlacement(e.target.value);
+ };
+
+ return (
+ <>
+
+ topLeft
+ topRight
+ bottomLeft
+ bottomRight
+
+
+
+
+
+
+
+ >
+ );
+};
+
+ReactDOM.render(
, mountNode);
+```
diff --git a/components/date-picker/demo/range-picker.md b/components/date-picker/demo/range-picker.md
index 2fcb48c006..10d8f3ee7d 100644
--- a/components/date-picker/demo/range-picker.md
+++ b/components/date-picker/demo/range-picker.md
@@ -24,6 +24,7 @@ ReactDOM.render(
+
,
mountNode,
diff --git a/components/date-picker/demo/status.md b/components/date-picker/demo/status.md
new file mode 100644
index 0000000000..cb8ebe7db3
--- /dev/null
+++ b/components/date-picker/demo/status.md
@@ -0,0 +1,28 @@
+---
+order: 19
+version: 4.19.0
+title:
+ zh-CN: 自定义状态
+ en-US: Status
+---
+
+## zh-CN
+
+使用 `status` 为 DatePicker 添加状态,可选 `error` 或者 `warning`。
+
+## en-US
+
+Add status to DatePicker with `status`, which could be `error` or `warning`.
+
+```tsx
+import { DatePicker, Space } from 'antd';
+
+const Status: React.FC = () => (
+
+
+
+
+);
+
+ReactDOM.render(
, mountNode);
+```
diff --git a/components/date-picker/generatePicker/generateRangePicker.tsx b/components/date-picker/generatePicker/generateRangePicker.tsx
index 6444353153..12d9154006 100644
--- a/components/date-picker/generatePicker/generateRangePicker.tsx
+++ b/components/date-picker/generatePicker/generateRangePicker.tsx
@@ -10,7 +10,7 @@ import enUS from '../locale/en_US';
import { ConfigContext, ConfigConsumerProps } from '../../config-provider';
import SizeContext from '../../config-provider/SizeContext';
import LocaleReceiver from '../../locale-provider/LocaleReceiver';
-import { getRangePlaceholder } from '../util';
+import { getRangePlaceholder, transPlacement2DropdownAlign } from '../util';
import { RangePickerProps, PickerLocale, getTimeProps, Components } from '.';
import { PickerComponentClass } from './interface';
@@ -43,6 +43,7 @@ export default function generateRangePicker
(
prefixCls: customizePrefixCls,
getPopupContainer: customGetPopupContainer,
className,
+ placement,
size: customizeSize,
bordered = true,
placeholder,
@@ -73,6 +74,7 @@ export default function generateRangePicker(
}
ref={this.pickerRef}
+ dropdownAlign={transPlacement2DropdownAlign(direction, placement)}
placeholder={getRangePlaceholder(picker, locale, placeholder)}
suffixIcon={picker === 'time' ? : }
clearIcon={ }
diff --git a/components/date-picker/generatePicker/generateSinglePicker.tsx b/components/date-picker/generatePicker/generateSinglePicker.tsx
index d061682316..852e62145a 100644
--- a/components/date-picker/generatePicker/generateSinglePicker.tsx
+++ b/components/date-picker/generatePicker/generateSinglePicker.tsx
@@ -7,7 +7,7 @@ import RCPicker from 'rc-picker';
import { PickerMode } from 'rc-picker/lib/interface';
import { GenerateConfig } from 'rc-picker/lib/generate/index';
import enUS from '../locale/en_US';
-import { getPlaceholder } from '../util';
+import { getPlaceholder, transPlacement2DropdownAlign } from '../util';
import devWarning from '../../_util/devWarning';
import { ConfigContext, ConfigConsumerProps } from '../../config-provider';
import LocaleReceiver from '../../locale-provider/LocaleReceiver';
@@ -21,9 +21,18 @@ import {
Components,
} from '.';
import { PickerComponentClass } from './interface';
+import { FormItemStatusContext } from '../../form/context';
+import {
+ getFeedbackIcon,
+ getMergedStatus,
+ getStatusClassNames,
+ InputStatus,
+} from '../../_util/statusUtils';
export default function generatePicker(generateConfig: GenerateConfig) {
- type DatePickerProps = PickerProps;
+ type DatePickerProps = PickerProps & {
+ status?: InputStatus;
+ };
function getPicker(
picker?: PickerMode,
@@ -59,6 +68,23 @@ export default function generatePicker(generateConfig: GenerateConfig<
}
};
+ renderFeedback = (prefixCls: string) => (
+
+ {({ hasFeedback, status: contextStatus }) => {
+ const { status: customStatus } = this.props;
+ const status = getMergedStatus(contextStatus, customStatus);
+ return hasFeedback && getFeedbackIcon(prefixCls, status);
+ }}
+
+ );
+
+ renderSuffix = (prefixCls: string, mergedPicker?: PickerMode) => (
+ <>
+ {mergedPicker === 'time' ? : }
+ {this.renderFeedback(prefixCls)}
+ >
+ );
+
renderPicker = (contextLocale: PickerLocale) => {
const locale = { ...contextLocale, ...this.props.locale };
const { getPrefixCls, direction, getPopupContainer } = this.context;
@@ -68,7 +94,9 @@ export default function generatePicker(generateConfig: GenerateConfig<
className,
size: customizeSize,
bordered = true,
+ placement,
placeholder,
+ status: customStatus,
...restProps
} = this.props;
const { format, showTime } = this.props as any;
@@ -99,36 +127,44 @@ export default function generatePicker(generateConfig: GenerateConfig<
const mergedSize = customizeSize || size;
return (
-
- ref={this.pickerRef}
- placeholder={getPlaceholder(mergedPicker, locale, placeholder)}
- suffixIcon={
- mergedPicker === 'time' ? :
- }
- clearIcon={ }
- prevIcon={ }
- nextIcon={ }
- superPrevIcon={ }
- superNextIcon={ }
- allowClear
- transitionName={`${rootPrefixCls}-slide-up`}
- {...additionalProps}
- {...restProps}
- {...additionalOverrideProps}
- locale={locale!.lang}
- className={classNames(
- {
- [`${prefixCls}-${mergedSize}`]: mergedSize,
- [`${prefixCls}-borderless`]: !bordered,
- },
- className,
+
+ {({ hasFeedback, status: contextStatus }) => (
+
+ ref={this.pickerRef}
+ placeholder={getPlaceholder(mergedPicker, locale, placeholder)}
+ suffixIcon={this.renderSuffix(prefixCls, mergedPicker)}
+ dropdownAlign={transPlacement2DropdownAlign(direction, placement)}
+ clearIcon={ }
+ prevIcon={ }
+ nextIcon={ }
+ superPrevIcon={ }
+ superNextIcon={ }
+ allowClear
+ transitionName={`${rootPrefixCls}-slide-up`}
+ {...additionalProps}
+ {...restProps}
+ {...additionalOverrideProps}
+ locale={locale!.lang}
+ className={classNames(
+ {
+ [`${prefixCls}-${mergedSize}`]: mergedSize,
+ [`${prefixCls}-borderless`]: !bordered,
+ },
+ getStatusClassNames(
+ prefixCls,
+ getMergedStatus(contextStatus, customStatus),
+ hasFeedback,
+ ),
+ className,
+ )}
+ prefixCls={prefixCls}
+ getPopupContainer={customizeGetPopupContainer || getPopupContainer}
+ generateConfig={generateConfig}
+ components={Components}
+ direction={direction}
+ />
)}
- prefixCls={prefixCls}
- getPopupContainer={customizeGetPopupContainer || getPopupContainer}
- generateConfig={generateConfig}
- components={Components}
- direction={direction}
- />
+
);
}}
diff --git a/components/date-picker/generatePicker/index.tsx b/components/date-picker/generatePicker/index.tsx
index 26e14c4bc8..643a848f61 100644
--- a/components/date-picker/generatePicker/index.tsx
+++ b/components/date-picker/generatePicker/index.tsx
@@ -17,6 +17,7 @@ import PickerTag from '../PickerTag';
import { TimePickerLocale } from '../../time-picker';
import generateSinglePicker from './generateSinglePicker';
import generateRangePicker from './generateRangePicker';
+import { tuple } from '../../_util/type';
export const Components = { button: PickerButton, rangeItem: PickerTag };
@@ -27,13 +28,18 @@ function toArray(list: T | T[]): T[] {
return Array.isArray(list) ? list : [list];
}
-export function getTimeProps(
- props: { format?: string; picker?: PickerMode } & SharedTimeProps,
+export function getTimeProps(
+ props: { format?: string; picker?: PickerMode } & Omit<
+ SharedTimeProps,
+ 'disabledTime'
+ > & {
+ disabledTime?: DisabledTime;
+ },
) {
const { format, picker, showHour, showMinute, showSecond, use12Hours } = props;
const firstFormat = toArray(format)[0];
- const showTimeObj: SharedTimeProps = { ...props };
+ const showTimeObj = { ...props };
if (firstFormat && typeof firstFormat === 'string') {
if (!firstFormat.includes('s') && showSecond === undefined) {
@@ -64,16 +70,16 @@ export function getTimeProps(
showTime: showTimeObj,
};
}
+const DataPickerPlacements = tuple('bottomLeft', 'bottomRight', 'topLeft', 'topRight');
+type DataPickerPlacement = typeof DataPickerPlacements[number];
type InjectDefaultProps = Omit<
Props,
- | 'locale'
- | 'generateConfig'
- | 'hideHeader'
- | 'components'
+ 'locale' | 'generateConfig' | 'hideHeader' | 'components'
> & {
locale?: PickerLocale;
size?: SizeType;
+ placement?: DataPickerPlacement;
bordered?: boolean;
};
@@ -96,6 +102,7 @@ export type AdditionalPickerLocaleLangProps = {
monthPlaceholder?: string;
weekPlaceholder?: string;
rangeYearPlaceholder?: [string, string];
+ rangeQuarterPlaceholder?: [string, string];
rangeMonthPlaceholder?: [string, string];
rangeWeekPlaceholder?: [string, string];
rangePlaceholder?: [string, string];
diff --git a/components/date-picker/index.en-US.md b/components/date-picker/index.en-US.md
index aaaac1ea19..48e299d6b9 100644
--- a/components/date-picker/index.en-US.md
+++ b/components/date-picker/index.en-US.md
@@ -69,9 +69,11 @@ The following APIs are shared by DatePicker, RangePicker.
| panelRender | Customize panel render | (panelNode) => ReactNode | - | 4.5.0 |
| picker | Set picker type | `date` \| `week` \| `month` \| `quarter` \| `year` | `date` | `quarter`: 4.1.0 |
| placeholder | The placeholder of date input | string \| \[string,string] | - | |
+| placement | The position where the selection box pops up | `bottomLeft` `bottomRight` `topLeft` `topRight` | bottomLeft | |
| popupStyle | To customize the style of the popup calendar | CSSProperties | {} | |
| prevIcon | The custom prev icon | ReactNode | - | 4.17.0 |
| size | To determine the size of the input box, the height of `large` and `small`, are 40px and 24px respectively, while default size is 32px | `large` \| `middle` \| `small` | - | |
+| status | Set validation status | 'error' \| 'warning' | - | 4.19.0 |
| style | To customize the style of the input box | CSSProperties | {} | |
| suffixIcon | The custom suffix icon | ReactNode | - | |
| superNextIcon | The custom super next icon | ReactNode | - | 4.17.0 |
diff --git a/components/date-picker/index.zh-CN.md b/components/date-picker/index.zh-CN.md
index 43c9f44432..3e718c202c 100644
--- a/components/date-picker/index.zh-CN.md
+++ b/components/date-picker/index.zh-CN.md
@@ -70,9 +70,11 @@ import locale from 'antd/lib/locale/zh_CN';
| panelRender | 自定义渲染面板 | (panelNode) => ReactNode | - | 4.5.0 |
| picker | 设置选择器类型 | `date` \| `week` \| `month` \| `quarter` \| `year` | `date` | `quarter`: 4.1.0 |
| placeholder | 输入框提示文字 | string \| \[string, string] | - | |
+| placement | 选择框弹出的位置 | `bottomLeft` `bottomRight` `topLeft` `topRight` | bottomLeft | |
| popupStyle | 额外的弹出日历样式 | CSSProperties | {} | |
| prevIcon | 自定义上一个图标 | ReactNode | - | 4.17.0 |
| size | 输入框大小,`large` 高度为 40px,`small` 为 24px,默认是 32px | `large` \| `middle` \| `small` | - | |
+| status | 设置校验状态 | 'error' \| 'warning' | - | 4.19.0 |
| style | 自定义输入框样式 | CSSProperties | {} | |
| suffixIcon | 自定义的选择框后缀图标 | ReactNode | - | |
| superNextIcon | 自定义 `<<` 切换图标 | ReactNode | - | 4.17.0 |
diff --git a/components/date-picker/locale/en_GB.tsx b/components/date-picker/locale/en_GB.tsx
index 15cb6161b2..5e278f58c0 100644
--- a/components/date-picker/locale/en_GB.tsx
+++ b/components/date-picker/locale/en_GB.tsx
@@ -12,6 +12,7 @@ const locale: PickerLocale = {
weekPlaceholder: 'Select week',
rangePlaceholder: ['Start date', 'End date'],
rangeYearPlaceholder: ['Start year', 'End year'],
+ rangeQuarterPlaceholder: ['Start quarter', 'End quarter'],
rangeMonthPlaceholder: ['Start month', 'End month'],
rangeWeekPlaceholder: ['Start week', 'End week'],
...CalendarLocale,
diff --git a/components/date-picker/locale/en_US.tsx b/components/date-picker/locale/en_US.tsx
index 47b57b9eeb..358216edd7 100644
--- a/components/date-picker/locale/en_US.tsx
+++ b/components/date-picker/locale/en_US.tsx
@@ -12,6 +12,7 @@ const locale: PickerLocale = {
weekPlaceholder: 'Select week',
rangePlaceholder: ['Start date', 'End date'],
rangeYearPlaceholder: ['Start year', 'End year'],
+ rangeQuarterPlaceholder: ['Start quarter', 'End quarter'],
rangeMonthPlaceholder: ['Start month', 'End month'],
rangeWeekPlaceholder: ['Start week', 'End week'],
...CalendarLocale,
diff --git a/components/date-picker/locale/zh_CN.tsx b/components/date-picker/locale/zh_CN.tsx
index bb3181123d..41df329de6 100644
--- a/components/date-picker/locale/zh_CN.tsx
+++ b/components/date-picker/locale/zh_CN.tsx
@@ -13,6 +13,7 @@ const locale: PickerLocale = {
rangePlaceholder: ['开始日期', '结束日期'],
rangeYearPlaceholder: ['开始年份', '结束年份'],
rangeMonthPlaceholder: ['开始月份', '结束月份'],
+ rangeQuarterPlaceholder: ['开始季度', '结束季度'],
rangeWeekPlaceholder: ['开始周', '结束周'],
...CalendarLocale,
},
diff --git a/components/date-picker/locale/zh_TW.tsx b/components/date-picker/locale/zh_TW.tsx
index 663e943802..f0bcf20c1e 100644
--- a/components/date-picker/locale/zh_TW.tsx
+++ b/components/date-picker/locale/zh_TW.tsx
@@ -13,6 +13,7 @@ const locale: PickerLocale = {
rangePlaceholder: ['開始日期', '結束日期'],
rangeYearPlaceholder: ['開始年份', '結束年份'],
rangeMonthPlaceholder: ['開始月份', '結束月份'],
+ rangeQuarterPlaceholder: ['開始季度', '結束季度'],
rangeWeekPlaceholder: ['開始周', '結束周'],
...CalendarLocale,
},
diff --git a/components/date-picker/style/index.less b/components/date-picker/style/index.less
index 4fc64c1976..3b6d3a08e4 100644
--- a/components/date-picker/style/index.less
+++ b/components/date-picker/style/index.less
@@ -1,6 +1,7 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@import '../../input/style/mixin';
+@import './status';
@picker-prefix-cls: ~'@{ant-prefix}-picker';
@@ -13,7 +14,7 @@
}
.@{picker-prefix-cls} {
- @arrow-size: 10px;
+ @arrow-size: @popover-arrow-width;
.reset-component();
.picker-padding(@input-height-base, @font-size-base, @input-padding-horizontal-base);
@@ -106,6 +107,8 @@
}
&-suffix {
+ display: flex;
+ flex: none;
align-self: center;
margin-left: (@padding-xs / 2);
color: @disabled-color;
@@ -114,6 +117,10 @@
> * {
vertical-align: top;
+
+ &:not(:last-child) {
+ margin-right: 8px;
+ }
}
}
@@ -221,17 +228,17 @@
&-placement-bottomLeft {
.@{picker-prefix-cls}-range-arrow {
- top: (@arrow-size / 2) - (@arrow-size / 3);
+ top: (@arrow-size / 2) - (@arrow-size / 3) + 0.7px;
display: block;
- transform: rotate(-45deg);
+ transform: rotate(-135deg) translateY(1px);
}
}
&-placement-topLeft {
.@{picker-prefix-cls}-range-arrow {
- bottom: (@arrow-size / 2) - (@arrow-size / 3);
+ bottom: (@arrow-size / 2) - (@arrow-size / 3) + 0.7px;
display: block;
- transform: rotate(135deg);
+ transform: rotate(45deg);
}
}
@@ -311,19 +318,14 @@
width: @arrow-size;
height: @arrow-size;
margin-left: @input-padding-horizontal-base * 1.5;
- box-shadow: 2px -2px 6px fade(@black, 6%);
+ background: linear-gradient(
+ 135deg,
+ transparent 40%,
+ @calendar-bg 40%
+ ); // Use linear-gradient to prevent arrow from covering text
+ box-shadow: 2px 2px 6px -2px fade(@black, 10%); // use spread radius to hide shadow over popover
transition: left @animation-duration-slow ease-out;
-
- &::after {
- position: absolute;
- top: @border-width-base;
- right: @border-width-base;
- width: @arrow-size;
- height: @arrow-size;
- border: (@arrow-size / 2) solid @border-color-split;
- border-color: @calendar-bg @calendar-bg transparent transparent;
- content: '';
- }
+ .roundedArrow(@arrow-size, 5px, @calendar-bg);
}
&-panel-container {
diff --git a/components/date-picker/style/index.tsx b/components/date-picker/style/index.tsx
index cc4424295a..18447e360f 100644
--- a/components/date-picker/style/index.tsx
+++ b/components/date-picker/style/index.tsx
@@ -3,3 +3,5 @@ import './index.less';
// style dependencies
import '../../tag/style';
import '../../button/style';
+
+// deps-lint-skip: form
diff --git a/components/date-picker/style/status.less b/components/date-picker/style/status.less
new file mode 100644
index 0000000000..be3adba330
--- /dev/null
+++ b/components/date-picker/style/status.less
@@ -0,0 +1,52 @@
+@import '../../input/style/mixin';
+
+@picker-prefix-cls: ~'@{ant-prefix}-picker';
+
+.picker-status-color(
+ @text-color: @input-color;
+ @border-color: @input-border-color;
+ @background-color: @input-bg;
+ @hoverBorderColor: @primary-color-hover;
+ @outlineColor: @primary-color-outline;
+) {
+ &.@{picker-prefix-cls} {
+ &,
+ &:not([disabled]):hover {
+ background-color: @background-color;
+ border-color: @border-color;
+ }
+
+ &-focused,
+ &:focus {
+ .active(@text-color, @hoverBorderColor, @outlineColor);
+ }
+ }
+
+ .@{picker-prefix-cls}-feedback-icon {
+ color: @text-color;
+ }
+}
+
+.@{picker-prefix-cls} {
+ &-status-error {
+ .picker-status-color(@error-color, @error-color, @input-bg, @error-color-hover, @error-color-outline);
+ }
+
+ &-status-warning {
+ .picker-status-color(@warning-color, @warning-color, @input-bg, @warning-color-hover, @warning-color-outline);
+ }
+
+ &-status-validating {
+ .@{picker-prefix-cls}-feedback-icon {
+ display: inline-block;
+ color: @primary-color;
+ }
+ }
+
+ &-status-success {
+ .@{picker-prefix-cls}-feedback-icon {
+ color: @success-color;
+ animation-name: diffZoomIn1 !important;
+ }
+ }
+}
diff --git a/components/date-picker/util.ts b/components/date-picker/util.ts
index 0643a0842a..c2dbc6c501 100644
--- a/components/date-picker/util.ts
+++ b/components/date-picker/util.ts
@@ -1,4 +1,6 @@
import { PickerMode } from 'rc-picker/lib/interface';
+import { DirectionType } from '../config-provider';
+import { SelectCommonPlacement } from '../_util/motion';
import { PickerLocale } from './generatePicker';
export function getPlaceholder(
@@ -40,6 +42,9 @@ export function getRangePlaceholder(
if (picker === 'year' && locale.lang.yearPlaceholder) {
return locale.lang.rangeYearPlaceholder;
}
+ if (picker === 'quarter' && locale.lang.quarterPlaceholder) {
+ return locale.lang.rangeQuarterPlaceholder;
+ }
if (picker === 'month' && locale.lang.monthPlaceholder) {
return locale.lang.rangeMonthPlaceholder;
}
@@ -51,3 +56,56 @@ export function getRangePlaceholder(
}
return locale.lang.rangePlaceholder;
}
+
+export function transPlacement2DropdownAlign(
+ direction: DirectionType,
+ placement?: SelectCommonPlacement,
+) {
+ const overflow = {
+ adjustX: 1,
+ adjustY: 1,
+ };
+ switch (placement) {
+ case 'bottomLeft': {
+ return {
+ points: ['tl', 'bl'],
+ offset: [0, 4],
+ overflow,
+ };
+ }
+ case 'bottomRight': {
+ return {
+ points: ['tr', 'br'],
+ offset: [0, 4],
+ overflow,
+ };
+ }
+ case 'topLeft': {
+ return {
+ points: ['bl', 'tl'],
+ offset: [0, -4],
+ overflow,
+ };
+ }
+ case 'topRight': {
+ return {
+ points: ['br', 'tr'],
+ offset: [0, -4],
+ overflow,
+ };
+ }
+ default: {
+ return direction === 'rtl'
+ ? {
+ points: ['tr', 'br'],
+ offset: [0, 4],
+ overflow,
+ }
+ : {
+ points: ['tl', 'bl'],
+ offset: [0, 4],
+ overflow,
+ };
+ }
+ }
+}
diff --git a/components/dropdown/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/dropdown/__tests__/__snapshots__/demo-extend.test.ts.snap
index ff41a07fd6..0496324905 100644
--- a/components/dropdown/__tests__/__snapshots__/demo-extend.test.ts.snap
+++ b/components/dropdown/__tests__/__snapshots__/demo-extend.test.ts.snap
@@ -220,7 +220,7 @@ Array [
type="button"
>
- bottomCenter
+ bottom
,
@@ -860,7 +860,1291 @@ Array [
type="button"
>
- topCenter
+ top
+
+ ,
+
,
+
+
+ topRight
+
+ ,
+
,
+]
+`;
+
+exports[`renders ./components/dropdown/demo/arrow-center.md extend context correctly 1`] = `
+Array [
+
+
+ bottomLeft
+
+ ,
+
,
+
+
+ bottom
+
+ ,
+
,
+
+
+ bottomRight
+
+ ,
+
,
+
,
+
+
+ topLeft
+
+ ,
+
,
+
+
+ top
,
@@ -5754,7 +7038,7 @@ exports[`renders ./components/dropdown/demo/placement.md extend context correctl
type="button"
>
- bottomCenter
+ bottom
@@ -6408,7 +7692,7 @@ exports[`renders ./components/dropdown/demo/placement.md extend context correctl
type="button"
>
- topCenter
+ top
diff --git a/components/dropdown/__tests__/__snapshots__/demo.test.js.snap b/components/dropdown/__tests__/__snapshots__/demo.test.js.snap
index ac10832018..c7fedda705 100644
--- a/components/dropdown/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/dropdown/__tests__/__snapshots__/demo.test.js.snap
@@ -15,7 +15,7 @@ Array [
type="button"
>
- bottomCenter
+ bottom
,
- topCenter
+ top
+
+ ,
+
+
+ topRight
+
+ ,
+]
+`;
+
+exports[`renders ./components/dropdown/demo/arrow-center.md correctly 1`] = `
+Array [
+
+
+ bottomLeft
+
+ ,
+
+
+ bottom
+
+ ,
+
+
+ bottomRight
+
+ ,
+ ,
+
+
+ topLeft
+
+ ,
+
+
+ top
,
- bottomCenter
+ bottom
@@ -706,7 +760,7 @@ exports[`renders ./components/dropdown/demo/placement.md correctly 1`] = `
type="button"
>
- topCenter
+ top
diff --git a/components/dropdown/__tests__/index.test.js b/components/dropdown/__tests__/index.test.js
index 06631ea92a..dc97a9bb8f 100644
--- a/components/dropdown/__tests__/index.test.js
+++ b/components/dropdown/__tests__/index.test.js
@@ -59,4 +59,24 @@ describe('Dropdown', () => {
await sleep(500);
expect(wrapper.find(Dropdown).find('#customExpandIcon').length).toBe(1);
});
+
+ it('should warn if use topCenter or bottomCenter', () => {
+ const error = jest.spyOn(console, 'error');
+ mount(
+
+
+ bottomCenter
+
+
+ topCenter
+
+
,
+ );
+ expect(error).toHaveBeenCalledWith(
+ expect.stringContaining("[antd: Dropdown] You are using 'bottomCenter'"),
+ );
+ expect(error).toHaveBeenCalledWith(
+ expect.stringContaining("[antd: Dropdown] You are using 'topCenter'"),
+ );
+ });
});
diff --git a/components/dropdown/demo/arrow-center.md b/components/dropdown/demo/arrow-center.md
new file mode 100644
index 0000000000..fd4834fbb7
--- /dev/null
+++ b/components/dropdown/demo/arrow-center.md
@@ -0,0 +1,75 @@
+---
+order: 3
+title:
+ zh-CN: 箭头指向
+ en-US: Arrow pointing at the center
+---
+
+## zh-CN
+
+设置 `arrow` 为 `{ pointAtCenter: true }` 后,箭头将指向目标元素的中心。
+
+## en-US
+
+By specifying `arrow` prop with `{ pointAtCenter: true }`, the arrow will point to the center of the target element.
+
+```jsx
+import { Menu, Dropdown, Button } from 'antd';
+
+const menu = (
+
+
+
+ 1st menu item
+
+
+
+
+ 2nd menu item
+
+
+
+
+ 3rd menu item
+
+
+
+);
+
+ReactDOM.render(
+ <>
+
+ bottomLeft
+
+
+ bottom
+
+
+ bottomRight
+
+
+
+ topLeft
+
+
+ top
+
+
+ topRight
+
+ >,
+ mountNode,
+);
+```
+
+```css
+#components-dropdown-demo-arrow-center .ant-btn {
+ margin-right: 8px;
+ margin-bottom: 8px;
+}
+.ant-row-rtl #components-dropdown-demo-arrow-center .ant-btn {
+ margin-right: 0;
+ margin-bottom: 8px;
+ margin-left: 8px;
+}
+```
diff --git a/components/dropdown/demo/arrow.md b/components/dropdown/demo/arrow.md
index 7a6af177b3..b4c3880e7f 100644
--- a/components/dropdown/demo/arrow.md
+++ b/components/dropdown/demo/arrow.md
@@ -41,8 +41,8 @@ ReactDOM.render(
bottomLeft
-
- bottomCenter
+
+ bottom
bottomRight
@@ -51,8 +51,8 @@ ReactDOM.render(
topLeft
-
- topCenter
+
+ top
topRight
diff --git a/components/dropdown/demo/dropdown-button.md b/components/dropdown/demo/dropdown-button.md
index 956fc58847..21e406aa78 100644
--- a/components/dropdown/demo/dropdown-button.md
+++ b/components/dropdown/demo/dropdown-button.md
@@ -46,7 +46,7 @@ ReactDOM.render(
Dropdown
- }>
+ }>
Dropdown
diff --git a/components/dropdown/demo/placement.md b/components/dropdown/demo/placement.md
index d46c3a564f..196bffc5ef 100644
--- a/components/dropdown/demo/placement.md
+++ b/components/dropdown/demo/placement.md
@@ -42,8 +42,8 @@ ReactDOM.render(
bottomLeft
-
- bottomCenter
+
+ bottom
bottomRight
@@ -53,8 +53,8 @@ ReactDOM.render(
topLeft
-
- topCenter
+
+ top
topRight
diff --git a/components/dropdown/dropdown.tsx b/components/dropdown/dropdown.tsx
index 8a8a3a06bc..fd4e096893 100644
--- a/components/dropdown/dropdown.tsx
+++ b/components/dropdown/dropdown.tsx
@@ -7,6 +7,7 @@ import { ConfigContext } from '../config-provider';
import devWarning from '../_util/devWarning';
import { tuple } from '../_util/type';
import { cloneElement } from '../_util/reactNode';
+import getPlacements from '../_util/placements';
const Placements = tuple(
'topLeft',
@@ -15,6 +16,8 @@ const Placements = tuple(
'bottomLeft',
'bottomCenter',
'bottomRight',
+ 'top',
+ 'bottom',
);
type Placement = typeof Placements[number];
@@ -34,8 +37,12 @@ type Align = {
useCssTransform?: boolean;
};
+export type DropdownArrowOptions = {
+ pointAtCenter?: boolean;
+};
+
export interface DropDownProps {
- arrow?: boolean;
+ arrow?: boolean | DropdownArrowOptions;
trigger?: ('click' | 'hover' | 'contextMenu')[];
overlay: React.ReactElement | OverlayFunc;
onVisibleChange?: (visible: boolean) => void;
@@ -129,10 +136,21 @@ const Dropdown: DropdownInterface = props => {
const getPlacement = () => {
const { placement } = props;
- if (placement !== undefined) {
- return placement;
+ if (!placement) {
+ return direction === 'rtl' ? ('bottomRight' as Placement) : ('bottomLeft' as Placement);
}
- return direction === 'rtl' ? ('bottomRight' as Placement) : ('bottomLeft' as Placement);
+
+ if (placement.includes('Center')) {
+ const newPlacement = placement.slice(0, placement.indexOf('Center'));
+ devWarning(
+ !placement.includes('Center'),
+ 'Dropdown',
+ `You are using '${placement}' placement in Dropdown, which is deprecated. Try to use '${newPlacement}' instead.`,
+ );
+ return newPlacement;
+ }
+
+ return placement;
};
const {
@@ -169,11 +187,16 @@ const Dropdown: DropdownInterface = props => {
alignPoint = true;
}
+ const builtinPlacements = getPlacements({
+ arrowPointAtCenter: typeof arrow === 'object' && arrow.pointAtCenter,
+ });
+
return (
HTMLElement | () => document.body | |
| overlay | The dropdown menu | [Menu](/components/menu) \| () => Menu | - | |
| overlayClassName | The class name of the dropdown root element | string | - | |
| overlayStyle | The style of the dropdown root element | CSSProperties | - | |
-| placement | Placement of popup menu: `bottomLeft`, `bottomCenter`, `bottomRight`, `topLeft`, `topCenter` or `topRight` | string | `bottomLeft` | |
+| placement | Placement of popup menu: `bottom` `bottomLeft` `bottomRight` `top` `topLeft` `topRight` | string | `bottomLeft` | |
| trigger | The trigger mode which executes the dropdown action. Note that hover can't be used on touchscreens | Array<`click`\|`hover`\|`contextMenu`> | \[`hover`] | |
| visible | Whether the dropdown menu is currently visible | boolean | - | |
| onVisibleChange | Called when the visible state is changed. Not trigger when hidden by click item | (visible: boolean) => void | - | |
@@ -44,7 +44,7 @@ You should use [Menu](/components/menu/) as `overlay`. The menu items and divide
| disabled | Whether the dropdown menu is disabled | boolean | - | |
| icon | Icon (appears on the right) | ReactNode | - | |
| overlay | The dropdown menu | [Menu](/components/menu) | - | |
-| placement | Placement of popup menu: `bottomLeft` `bottomCenter` `bottomRight` `topLeft` `topCenter` `topRight` | string | `bottomLeft` | |
+| placement | Placement of popup menu: `bottom` `bottomLeft` `bottomRight` `top` `topLeft` `topRight` | string | `bottomLeft` | |
| size | Size of the button, the same as [Button](/components/button/#API) | string | `default` | |
| trigger | The trigger mode which executes the dropdown action | Array<`click`\|`hover`\|`contextMenu`> | \[`hover`] | |
| type | Type of the button, the same as [Button](/components/button/#API) | string | `default` | |
diff --git a/components/dropdown/index.zh-CN.md b/components/dropdown/index.zh-CN.md
index 15eccfea50..3ff8a060bd 100644
--- a/components/dropdown/index.zh-CN.md
+++ b/components/dropdown/index.zh-CN.md
@@ -21,14 +21,14 @@ cover: https://gw.alipayobjects.com/zos/alicdn/eedWN59yJ/Dropdown.svg
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
-| arrow | 下拉框箭头是否显示 | boolean | false | |
+| arrow | 下拉框箭头是否显示 | boolean \| { pointAtCenter: boolean } | false | |
| disabled | 菜单是否禁用 | boolean | - | |
| destroyPopupOnHide | 关闭后是否销毁 Dropdown | boolean | false | |
| getPopupContainer | 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。[示例](https://codepen.io/afc163/pen/zEjNOy?editors=0010) | (triggerNode: HTMLElement) => HTMLElement | () => document.body | |
| overlay | 菜单 | [Menu](/components/menu) \| () => Menu | - | |
| overlayClassName | 下拉根元素的类名称 | string | - | |
| overlayStyle | 下拉根元素的样式 | CSSProperties | - | |
-| placement | 菜单弹出位置:`bottomLeft` `bottomCenter` `bottomRight` `topLeft` `topCenter` `topRight` | string | `bottomLeft` | |
+| placement | 菜单弹出位置:`bottom` `bottomLeft` `bottomRight` `top` `topLeft` `topRight` | string | `bottomLeft` | |
| trigger | 触发下拉的行为, 移动端不支持 hover | Array<`click`\|`hover`\|`contextMenu`> | \[`hover`] | |
| visible | 菜单是否显示 | boolean | - | |
| onVisibleChange | 菜单显示状态改变时调用,参数为 `visible`。点击菜单按钮导致的消失不会触发 | (visible: boolean) => void | - | |
@@ -48,7 +48,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/eedWN59yJ/Dropdown.svg
| disabled | 菜单是否禁用 | boolean | - | |
| icon | 右侧的 icon | ReactNode | - | |
| overlay | 菜单 | [Menu](/components/menu/) | - | |
-| placement | 菜单弹出位置:`bottomLeft` `bottomCenter` `bottomRight` `topLeft` `topCenter` `topRight` | string | `bottomLeft` | |
+| placement | 菜单弹出位置:`bottom` `bottomLeft` `bottomRight` `top` `topLeft` `topRight` | string | `bottomLeft` | |
| size | 按钮大小,和 [Button](/components/button/#API) 一致 | string | `default` | |
| trigger | 触发下拉的行为 | Array<`click`\|`hover`\|`contextMenu`> | \[`hover`] | |
| type | 按钮类型,和 [Button](/components/button/#API) 一致 | string | `default` | |
diff --git a/components/dropdown/style/index.less b/components/dropdown/style/index.less
index b9a6769aa1..3a516ef9f7 100644
--- a/components/dropdown/style/index.less
+++ b/components/dropdown/style/index.less
@@ -49,14 +49,14 @@
}
// Offset the popover to account for the dropdown arrow
- &-show-arrow&-placement-topCenter,
&-show-arrow&-placement-topLeft,
+ &-show-arrow&-placement-top,
&-show-arrow&-placement-topRight {
padding-bottom: @popover-distance;
}
- &-show-arrow&-placement-bottomCenter,
&-show-arrow&-placement-bottomLeft,
+ &-show-arrow&-placement-bottom,
&-show-arrow&-placement-bottomRight {
padding-top: @popover-distance;
}
@@ -68,23 +68,25 @@
position: absolute;
z-index: 1; // lift it up so the menu wouldn't cask shadow on it
display: block;
- width: sqrt(@popover-arrow-width * @popover-arrow-width * 2);
- height: sqrt(@popover-arrow-width * @popover-arrow-width * 2);
- background: transparent;
- border-style: solid;
- border-width: (sqrt(@popover-arrow-width * @popover-arrow-width * 2) / 2);
+ width: @popover-arrow-width;
+ height: @popover-arrow-width;
+ background: linear-gradient(
+ 135deg,
+ transparent 40%,
+ @popover-bg 40%
+ ); // Use linear-gradient to prevent arrow from covering text
+ .roundedArrow(@popover-arrow-width, 5px, @popover-bg);
+ }
+
+ &-placement-top > &-arrow,
+ &-placement-topLeft > &-arrow,
+ &-placement-topRight > &-arrow {
+ bottom: @popover-arrow-width * sqrt((1 / 2)) + 2px;
+ box-shadow: 3px 3px 7px -3px fade(@black, 10%);
transform: rotate(45deg);
}
- &-placement-topCenter > &-arrow,
- &-placement-topLeft > &-arrow,
- &-placement-topRight > &-arrow {
- bottom: @popover-distance - @popover-arrow-width + 2.2px;
- border-color: transparent @popover-bg @popover-bg transparent;
- box-shadow: 3px 3px 7px fade(@black, 7%);
- }
-
- &-placement-topCenter > &-arrow {
+ &-placement-top > &-arrow {
left: 50%;
transform: translateX(-50%) rotate(45deg);
}
@@ -97,17 +99,17 @@
right: 16px;
}
- &-placement-bottomCenter > &-arrow,
+ &-placement-bottom > &-arrow,
&-placement-bottomLeft > &-arrow,
&-placement-bottomRight > &-arrow {
- top: @popover-distance - @popover-arrow-width + 2px;
- border-color: @popover-bg transparent transparent @popover-bg;
- box-shadow: -2px -2px 5px fade(@black, 6%);
+ top: (@popover-arrow-width + 2px) * sqrt((1 / 2));
+ box-shadow: 2px 2px 5px -2px fade(@black, 10%);
+ transform: rotate(-135deg) translateY(-0.5px);
}
- &-placement-bottomCenter > &-arrow {
+ &-placement-bottom > &-arrow {
left: 50%;
- transform: translateX(-50%) rotate(45deg);
+ transform: translateX(-50%) rotate(-135deg) translateY(-0.5px);
}
&-placement-bottomLeft > &-arrow {
@@ -299,8 +301,8 @@
&.@{ant-prefix}-slide-down-enter.@{ant-prefix}-slide-down-enter-active&-placement-bottomLeft,
&.@{ant-prefix}-slide-down-appear.@{ant-prefix}-slide-down-appear-active&-placement-bottomLeft,
- &.@{ant-prefix}-slide-down-enter.@{ant-prefix}-slide-down-enter-active&-placement-bottomCenter,
- &.@{ant-prefix}-slide-down-appear.@{ant-prefix}-slide-down-appear-active&-placement-bottomCenter,
+ &.@{ant-prefix}-slide-down-enter.@{ant-prefix}-slide-down-enter-active&-placement-bottom,
+ &.@{ant-prefix}-slide-down-appear.@{ant-prefix}-slide-down-appear-active&-placement-bottom,
&.@{ant-prefix}-slide-down-enter.@{ant-prefix}-slide-down-enter-active&-placement-bottomRight,
&.@{ant-prefix}-slide-down-appear.@{ant-prefix}-slide-down-appear-active&-placement-bottomRight {
animation-name: antSlideUpIn;
@@ -308,21 +310,21 @@
&.@{ant-prefix}-slide-up-enter.@{ant-prefix}-slide-up-enter-active&-placement-topLeft,
&.@{ant-prefix}-slide-up-appear.@{ant-prefix}-slide-up-appear-active&-placement-topLeft,
- &.@{ant-prefix}-slide-up-enter.@{ant-prefix}-slide-up-enter-active&-placement-topCenter,
- &.@{ant-prefix}-slide-up-appear.@{ant-prefix}-slide-up-appear-active&-placement-topCenter,
+ &.@{ant-prefix}-slide-up-enter.@{ant-prefix}-slide-up-enter-active&-placement-top,
+ &.@{ant-prefix}-slide-up-appear.@{ant-prefix}-slide-up-appear-active&-placement-top,
&.@{ant-prefix}-slide-up-enter.@{ant-prefix}-slide-up-enter-active&-placement-topRight,
&.@{ant-prefix}-slide-up-appear.@{ant-prefix}-slide-up-appear-active&-placement-topRight {
animation-name: antSlideDownIn;
}
&.@{ant-prefix}-slide-down-leave.@{ant-prefix}-slide-down-leave-active&-placement-bottomLeft,
- &.@{ant-prefix}-slide-down-leave.@{ant-prefix}-slide-down-leave-active&-placement-bottomCenter,
+ &.@{ant-prefix}-slide-down-leave.@{ant-prefix}-slide-down-leave-active&-placement-bottom,
&.@{ant-prefix}-slide-down-leave.@{ant-prefix}-slide-down-leave-active&-placement-bottomRight {
animation-name: antSlideUpOut;
}
&.@{ant-prefix}-slide-up-leave.@{ant-prefix}-slide-up-leave-active&-placement-topLeft,
- &.@{ant-prefix}-slide-up-leave.@{ant-prefix}-slide-up-leave-active&-placement-topCenter,
+ &.@{ant-prefix}-slide-up-leave.@{ant-prefix}-slide-up-leave-active&-placement-top,
&.@{ant-prefix}-slide-up-leave.@{ant-prefix}-slide-up-leave-active&-placement-topRight {
animation-name: antSlideDownOut;
}
diff --git a/components/form/FormItem.tsx b/components/form/FormItem.tsx
index d04a33850f..d879c2cab0 100644
--- a/components/form/FormItem.tsx
+++ b/components/form/FormItem.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { useContext } from 'react';
+import { useContext, useMemo } from 'react';
import classNames from 'classnames';
import { Field, FormInstance, FieldContext, ListContext } from 'rc-field-form';
import { FieldProps } from 'rc-field-form/lib/Field';
@@ -12,7 +12,12 @@ import { tuple } from '../_util/type';
import devWarning from '../_util/devWarning';
import FormItemLabel, { FormItemLabelProps, LabelTooltipType } from './FormItemLabel';
import FormItemInput, { FormItemInputProps } from './FormItemInput';
-import { FormContext, NoStyleItemContext } from './context';
+import {
+ FormContext,
+ FormItemStatusContext,
+ NoStyleItemContext,
+ FormItemStatusContextProps,
+} from './context';
import { toArray, getFieldId } from './util';
import { cloneElement, isValidElement } from '../_util/reactNode';
import useFrameState from './hooks/useFrameState';
@@ -199,6 +204,28 @@ function FormItem(props: FormItemProps): React.ReactElemen
// ===================== Children Ref =====================
const getItemRef = useItemRef();
+ // ======================== Status ========================
+ let mergedValidateStatus: ValidateStatus = '';
+ if (validateStatus !== undefined) {
+ mergedValidateStatus = validateStatus;
+ } else if (meta?.validating) {
+ mergedValidateStatus = 'validating';
+ } else if (debounceErrors.length) {
+ mergedValidateStatus = 'error';
+ } else if (debounceWarnings.length) {
+ mergedValidateStatus = 'warning';
+ } else if (meta?.touched) {
+ mergedValidateStatus = 'success';
+ }
+
+ const formItemStatusContext = useMemo(
+ () => ({
+ status: mergedValidateStatus,
+ hasFeedback,
+ }),
+ [mergedValidateStatus, hasFeedback],
+ );
+
// ======================== Render ========================
function renderLayout(
baseChildren: React.ReactNode,
@@ -208,19 +235,6 @@ function FormItem(props: FormItemProps): React.ReactElemen
if (noStyle && !hidden) {
return baseChildren;
}
- // ======================== Status ========================
- let mergedValidateStatus: ValidateStatus = '';
- if (validateStatus !== undefined) {
- mergedValidateStatus = validateStatus;
- } else if (meta?.validating) {
- mergedValidateStatus = 'validating';
- } else if (debounceErrors.length) {
- mergedValidateStatus = 'error';
- } else if (debounceWarnings.length) {
- mergedValidateStatus = 'warning';
- } else if (meta?.touched) {
- mergedValidateStatus = 'success';
- }
const itemClassName = {
[`${prefixCls}-item`]: true,
@@ -281,11 +295,12 @@ function FormItem(props: FormItemProps): React.ReactElemen
warnings={debounceWarnings}
prefixCls={prefixCls}
status={mergedValidateStatus}
- validateStatus={mergedValidateStatus}
help={help}
>
- {baseChildren}
+
+ {baseChildren}
+
diff --git a/components/form/FormItemInput.tsx b/components/form/FormItemInput.tsx
index e67a9f502f..4b4c2c8641 100644
--- a/components/form/FormItemInput.tsx
+++ b/components/form/FormItemInput.tsx
@@ -1,10 +1,5 @@
import * as React from 'react';
import classNames from 'classnames';
-import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
-import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
-import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';
-import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled';
-
import Col, { ColProps } from '../grid/col';
import { ValidateStatus } from './FormItem';
import { FormContext, FormItemPrefixContext } from './context';
@@ -15,8 +10,6 @@ interface FormItemInputMiscProps {
children: React.ReactNode;
errors: React.ReactNode[];
warnings: React.ReactNode[];
- hasFeedback?: boolean;
- validateStatus?: ValidateStatus;
/** @private Internal Usage, do not use in any of your production. */
_internalItemRender?: {
mark: string;
@@ -38,13 +31,6 @@ export interface FormItemInputProps {
help?: React.ReactNode;
}
-const iconMap: { [key: string]: any } = {
- success: CheckCircleFilled,
- warning: ExclamationCircleFilled,
- error: CloseCircleFilled,
- validating: LoadingOutlined,
-};
-
const FormItemInput: React.FC = props => {
const {
prefixCls,
@@ -53,9 +39,7 @@ const FormItemInput: React.FC = pro
children,
errors,
warnings,
- hasFeedback,
_internalItemRender: formItemRender,
- validateStatus,
extra,
help,
} = props;
@@ -67,15 +51,6 @@ const FormItemInput: React.FC = pro
const className = classNames(`${baseClassName}-control`, mergedWrapperCol.className);
- // Should provides additional icon if `hasFeedback`
- const IconNode = validateStatus && iconMap[validateStatus];
- const icon =
- hasFeedback && IconNode ? (
-
-
-
- ) : null;
-
// Pass to sub FormItem should not with col info
const subFormContext = React.useMemo(() => ({ ...formContext }), [formContext]);
delete subFormContext.labelCol;
@@ -84,7 +59,6 @@ const FormItemInput: React.FC = pro
const inputDom = (
);
const formItemContext = React.useMemo(() => ({ prefixCls, status }), [prefixCls, status]);
diff --git a/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap
index 07e2d782c3..716c00ce29 100644
--- a/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap
+++ b/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap
@@ -1747,7 +1747,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md extend context c
class="ant-form-item-control-input-content"
>
@@ -16497,38 +16513,46 @@ exports[`renders ./components/form/demo/validate-static.md extend context correc
>
-
-
-
-
+
-
-
+
+
+
+
+
+
+
+
-
+
@@ -16553,38 +16577,46 @@ exports[`renders ./components/form/demo/validate-static.md extend context correc
>
-
-
-
-
+
-
-
+
+
+
+
+
+
+
+
-
+
@@ -17215,29 +17270,6 @@ exports[`renders ./components/form/demo/validate-static.md extend context correc
-
-
-
-
-
-
-
@@ -17264,7 +17296,7 @@ exports[`renders ./components/form/demo/validate-static.md extend context correc
class="ant-form-item-control-input-content"
>
@@ -18672,29 +18727,6 @@ exports[`renders ./components/form/demo/validate-static.md extend context correc
-
-
-
-
-
-
-
@@ -18721,7 +18753,7 @@ exports[`renders ./components/form/demo/validate-static.md extend context correc
class="ant-form-item-control-input-content"
>
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
@@ -20455,7 +20692,7 @@ exports[`renders ./components/form/demo/validate-static.md extend context correc
class="ant-form-item-control-input-content"
>
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
@@ -20538,7 +20780,7 @@ exports[`renders ./components/form/demo/validate-static.md extend context correc
class="ant-form-item-control-input-content"
>
+
+
+
+
+
+
+
-
-
-
-
-
-
-
@@ -20625,7 +20867,7 @@ exports[`renders ./components/form/demo/validate-static.md extend context correc
class="ant-form-item-control-input-content"
>
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
`;
diff --git a/components/form/__tests__/__snapshots__/demo.test.js.snap b/components/form/__tests__/__snapshots__/demo.test.js.snap
index 07041fb0b3..7f0aeb06b7 100644
--- a/components/form/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/form/__tests__/__snapshots__/demo.test.js.snap
@@ -1303,7 +1303,7 @@ exports[`renders ./components/form/demo/disabled-input-debug.md correctly 1`] =
class="ant-form-item-control-input-content"
>
@@ -7558,38 +7574,46 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
>
-
-
-
-
+
-
-
+
+
+
+
+
+
+
+
-
+
@@ -7614,38 +7638,46 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
>
-
-
-
-
+
-
-
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
@@ -7771,7 +7803,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
class="ant-form-item-control-input-content"
>
-
-
-
-
-
-
-
@@ -7863,7 +7895,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
class="ant-form-item-control-input-content"
>
+ >
+ I'm Select
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
@@ -8359,7 +8519,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
class="ant-form-item-control-input-content"
>
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
@@ -8442,7 +8607,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
class="ant-form-item-control-input-content"
>
+
+
+
+
+
+
+
-
-
-
-
-
-
-
@@ -8529,7 +8694,7 @@ exports[`renders ./components/form/demo/validate-static.md correctly 1`] = `
class="ant-form-item-control-input-content"
>
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
`;
diff --git a/components/form/context.tsx b/components/form/context.tsx
index 73801a8b1b..8ba6d2ac6b 100644
--- a/components/form/context.tsx
+++ b/components/form/context.tsx
@@ -50,3 +50,10 @@ export interface FormItemPrefixContextProps {
export const FormItemPrefixContext = React.createContext({
prefixCls: '',
});
+
+export interface FormItemStatusContextProps {
+ status?: ValidateStatus;
+ hasFeedback?: boolean;
+}
+
+export const FormItemStatusContext = React.createContext({});
diff --git a/components/form/demo/validate-static.md b/components/form/demo/validate-static.md
index 4d82dbadf5..286960f9eb 100644
--- a/components/form/demo/validate-static.md
+++ b/components/form/demo/validate-static.md
@@ -23,7 +23,17 @@ We provide properties like `validateStatus` `help` `hasFeedback` to customize yo
```tsx
import { SmileOutlined } from '@ant-design/icons';
-import { Form, Input, DatePicker, TimePicker, Select, Cascader, InputNumber, Mentions } from 'antd';
+import {
+ Form,
+ Input,
+ DatePicker,
+ TimePicker,
+ Select,
+ Cascader,
+ InputNumber,
+ Mentions,
+ TreeSelect,
+} from 'antd';
const { Option } = Select;
@@ -87,7 +97,7 @@ ReactDOM.render(
-
+
Option 1
Option 2
Option 3
@@ -97,10 +107,18 @@ ReactDOM.render(
-
+
+
+
+
+
@@ -137,9 +155,13 @@ ReactDOM.render(
-
+
+
+
+
+
,
mountNode,
);
diff --git a/components/form/demo/without-form-create.md b/components/form/demo/without-form-create.md
index 758c0ee78f..33efcfe52e 100644
--- a/components/form/demo/without-form-create.md
+++ b/components/form/demo/without-form-create.md
@@ -19,9 +19,10 @@ import { Form, InputNumber } from 'antd';
type ValidateStatus = Parameters[0]['validateStatus'];
-function validatePrimeNumber(
- number: number,
-): { validateStatus: ValidateStatus; errorMsg: string | null } {
+function validatePrimeNumber(number: number): {
+ validateStatus: ValidateStatus;
+ errorMsg: string | null;
+} {
if (number === 11) {
return {
validateStatus: 'success',
diff --git a/components/form/style/mixin.less b/components/form/style/mixin.less
index 603212a275..60ab5646f5 100644
--- a/components/form/style/mixin.less
+++ b/components/form/style/mixin.less
@@ -10,40 +10,10 @@
.@{ant-prefix}-form-item-split {
color: @text-color;
}
- // 输入框的不同校验状态
- :not(.@{ant-prefix}-input-disabled):not(.@{ant-prefix}-input-borderless).@{ant-prefix}-input,
- :not(.@{ant-prefix}-input-affix-wrapper-disabled):not(.@{ant-prefix}-input-affix-wrapper-borderless).@{ant-prefix}-input-affix-wrapper,
- :not(.@{ant-prefix}-input-number-affix-wrapper-disabled):not(.@{ant-prefix}-input-number-affix-wrapper-borderless).@{ant-prefix}-input-number-affix-wrapper {
- &,
- &:hover {
- background-color: @background-color;
- border-color: @border-color;
- }
-
- &:focus,
- &-focused {
- .active(@border-color, @hoverBorderColor, @outlineColor);
- }
- }
.@{ant-prefix}-calendar-picker-open .@{ant-prefix}-calendar-picker-input {
.active(@border-color, @hoverBorderColor, @outlineColor);
}
-
- .@{ant-prefix}-input-prefix,
- .@{ant-prefix}-input-number-prefix {
- color: @text-color;
- }
-
- .@{ant-prefix}-input-group-addon,
- .@{ant-prefix}-input-number-group-addon {
- color: @text-color;
- border-color: @border-color;
- }
-
- .has-feedback {
- color: @text-color;
- }
}
// Reset form styles
diff --git a/components/form/style/status.less b/components/form/style/status.less
index e16cf5753b..1a53d97a96 100644
--- a/components/form/style/status.less
+++ b/components/form/style/status.less
@@ -24,287 +24,19 @@
}
&-has-feedback {
- // ========================= Input =========================
- .@{ant-prefix}-input {
- padding-right: 24px;
- }
- // https://github.com/ant-design/ant-design/issues/19884
- .@{ant-prefix}-input-affix-wrapper {
- .@{ant-prefix}-input-suffix {
- padding-right: 18px;
- }
- }
-
- // Fix issue: https://github.com/ant-design/ant-design/issues/7854
- .@{ant-prefix}-input-search:not(.@{ant-prefix}-input-search-enter-button) {
- .@{ant-prefix}-input-suffix {
- right: 28px;
- }
- }
-
// ======================== Switch =========================
.@{ant-prefix}-switch {
margin: 2px 0 4px;
}
-
- // ======================== Select =========================
- // Fix overlapping between feedback icon and 's arrow.
- // https://github.com/ant-design/ant-design/issues/4431
- > .@{ant-prefix}-select .@{ant-prefix}-select-arrow,
- > .@{ant-prefix}-select .@{ant-prefix}-select-clear,
- :not(.@{ant-prefix}-input-group-addon) > .@{ant-prefix}-select .@{ant-prefix}-select-arrow,
- :not(.@{ant-prefix}-input-group-addon) > .@{ant-prefix}-select .@{ant-prefix}-select-clear,
- :not(.@{ant-prefix}-input-number-group-addon)
- > .@{ant-prefix}-select
- .@{ant-prefix}-select-arrow,
- :not(.@{ant-prefix}-input-number-group-addon)
- > .@{ant-prefix}-select
- .@{ant-prefix}-select-clear {
- right: 32px;
- }
- > .@{ant-prefix}-select .@{ant-prefix}-select-selection-selected-value,
- :not(.@{ant-prefix}-input-group-addon)
- > .@{ant-prefix}-select
- .@{ant-prefix}-select-selection-selected-value,
- :not(.@{ant-prefix}-input-number-group-addon)
- > .@{ant-prefix}-select
- .@{ant-prefix}-select-selection-selected-value {
- padding-right: 42px;
- }
-
- // ======================= Cascader ========================
- .@{ant-prefix}-cascader-picker {
- &-arrow {
- margin-right: 19px;
- }
-
- &-clear {
- right: 32px;
- }
- }
-
- // ======================== Picker =========================
- // Fix issue: https://github.com/ant-design/ant-design/issues/4783
- .@{ant-prefix}-picker {
- padding-right: @input-padding-horizontal-base + @font-size-base * 1.3;
-
- &-large {
- padding-right: @input-padding-horizontal-lg + @font-size-base * 1.3;
- }
-
- &-small {
- padding-right: @input-padding-horizontal-sm + @font-size-base * 1.3;
- }
- }
-
- // ===================== Status Group ======================
- &.@{form-item-prefix-cls} {
- &-has-success,
- &-has-warning,
- &-has-error,
- &-is-validating {
- // ====================== Icon ======================
- .@{form-item-prefix-cls}-children-icon {
- position: absolute;
- top: 50%;
- right: 0;
- z-index: 1;
- width: @input-height-base;
- height: 20px;
- margin-top: -10px;
- font-size: @font-size-base;
- line-height: 20px;
- text-align: center;
- visibility: visible;
- animation: zoomIn 0.3s @ease-out-back;
- pointer-events: none;
- }
- }
- }
- }
-
- // ======================== Success ========================
- &-has-success {
- &.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {
- color: @success-color;
- animation-name: diffZoomIn1 !important;
- }
}
// ======================== Warning ========================
&-has-warning {
.form-control-validation(@warning-color; @warning-color; @form-warning-input-bg; @warning-color-hover; @warning-color-outline);
-
- &.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {
- color: @warning-color;
- animation-name: diffZoomIn3 !important;
- }
-
- // Select
- .@{ant-prefix}-select:not(.@{ant-prefix}-select-disabled):not(.@{ant-prefix}-select-customize-input) {
- .@{ant-prefix}-select-selector {
- background-color: @form-warning-input-bg;
- border-color: @warning-color !important;
- }
- &.@{ant-prefix}-select-open .@{ant-prefix}-select-selector,
- &.@{ant-prefix}-select-focused .@{ant-prefix}-select-selector {
- .active(@warning-color, @warning-color-hover, @warning-color-outline);
- }
- }
-
- // InputNumber, TimePicker
- .@{ant-prefix}-input-number,
- .@{ant-prefix}-picker {
- background-color: @form-warning-input-bg;
- border-color: @warning-color;
-
- &-focused,
- &:focus {
- .active(@warning-color, @warning-color-hover, @warning-color-outline);
- }
-
- &:not([disabled]):hover {
- background-color: @form-warning-input-bg;
- border-color: @warning-color;
- }
- }
-
- .@{ant-prefix}-cascader-picker:focus .@{ant-prefix}-cascader-input {
- .active(@warning-color, @warning-color-hover, @warning-color-outline);
- }
}
// ========================= Error =========================
&-has-error {
.form-control-validation(@error-color; @error-color; @form-error-input-bg; @error-color-hover; @error-color-outline);
-
- &.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {
- color: @error-color;
- animation-name: diffZoomIn2 !important;
- }
-
- // Select
- .@{ant-prefix}-select:not(.@{ant-prefix}-select-disabled):not(.@{ant-prefix}-select-customize-input) {
- .@{ant-prefix}-select-selector {
- background-color: @form-error-input-bg;
- border-color: @error-color !important;
- }
- &.@{ant-prefix}-select-open .@{ant-prefix}-select-selector,
- &.@{ant-prefix}-select-focused .@{ant-prefix}-select-selector {
- .active(@error-color, @error-color-hover, @error-color-outline);
- }
- }
-
- // fixes https://github.com/ant-design/ant-design/issues/20482
- .@{ant-prefix}-input-group-addon,
- .@{ant-prefix}-input-number-group-addon {
- .@{ant-prefix}-select {
- &.@{ant-prefix}-select-single:not(.@{ant-prefix}-select-customize-input)
- .@{ant-prefix}-select-selector {
- background-color: inherit;
- border: 0;
- box-shadow: none;
- }
- }
- }
-
- .@{ant-prefix}-select.@{ant-prefix}-select-auto-complete {
- .@{ant-prefix}-input:focus {
- border-color: @error-color;
- }
- }
-
- // InputNumber, TimePicker
- .@{ant-prefix}-input-number,
- .@{ant-prefix}-picker {
- background-color: @form-error-input-bg;
- border-color: @error-color;
-
- &-focused,
- &:focus {
- .active(@error-color, @error-color-hover, @error-color-outline);
- }
-
- &:not([disabled]):hover {
- background-color: @form-error-input-bg;
- border-color: @error-color;
- }
- }
-
- .@{ant-prefix}-mention-wrapper {
- .@{ant-prefix}-mention-editor {
- &,
- &:not([disabled]):hover {
- background-color: @form-error-input-bg;
- border-color: @error-color;
- }
- }
- &.@{ant-prefix}-mention-active:not([disabled]) .@{ant-prefix}-mention-editor,
- .@{ant-prefix}-mention-editor:not([disabled]):focus {
- .active(@error-color, @error-color-hover, @error-color-outline);
- }
- }
-
- // Cascader
- .@{ant-prefix}-cascader-picker {
- &:hover
- .@{ant-prefix}-cascader-picker-label:hover
- + .@{ant-prefix}-cascader-input.@{ant-prefix}-input {
- border-color: @error-color;
- }
-
- &:focus .@{ant-prefix}-cascader-input {
- background-color: @form-error-input-bg;
- .active(@error-color, @error-color-hover, @error-color-outline);
- }
- }
-
- // Transfer
- .@{ant-prefix}-transfer {
- &-list {
- border-color: @error-color;
-
- &-search:not([disabled]) {
- border-color: @input-border-color;
-
- &:hover {
- .hover();
- }
-
- &:focus {
- .active();
- }
- }
- }
- }
-
- // Radio.Group
- .@{ant-prefix}-radio-button-wrapper {
- border-color: @error-color !important;
-
- &:not(:first-child) {
- &::before {
- background-color: @error-color;
- }
- }
- }
-
- // Mentions
- .@{ant-prefix}-mentions {
- border-color: @error-color !important;
-
- &-focused,
- &:focus {
- .active(@error-color, @error-color-hover, @error-color-outline);
- }
- }
- }
-
- // ====================== Validating =======================
- &-is-validating {
- &.@{form-item-prefix-cls}-has-feedback .@{form-item-prefix-cls}-children-icon {
- display: inline-block;
- color: @primary-color;
- }
}
}
diff --git a/components/grid/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/grid/__tests__/__snapshots__/demo-extend.test.ts.snap
index 6a70a5aa85..902f65cfd5 100644
--- a/components/grid/__tests__/__snapshots__/demo-extend.test.ts.snap
+++ b/components/grid/__tests__/__snapshots__/demo-extend.test.ts.snap
@@ -838,41 +838,41 @@ Array [
style="width:50%"
>
@@ -914,37 +914,37 @@ Array [
>
8
16
24
32
40
48
@@ -958,41 +958,41 @@ Array [
style="width:50%"
>
@@ -1034,37 +1034,37 @@ Array [
>
8
16
24
32
40
48
@@ -1078,41 +1078,41 @@ Array [
style="width:50%;margin-bottom:48px"
>
@@ -1154,37 +1154,37 @@ Array [
>
2
3
4
6
8
12
diff --git a/components/grid/__tests__/__snapshots__/demo.test.js.snap b/components/grid/__tests__/__snapshots__/demo.test.js.snap
index e003cb99ce..4e9428a144 100644
--- a/components/grid/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/grid/__tests__/__snapshots__/demo.test.js.snap
@@ -838,41 +838,41 @@ Array [
style="width:50%"
>
8
16
24
32
40
48
@@ -934,41 +934,41 @@ Array [
style="width:50%"
>
8
16
24
32
40
48
@@ -1030,41 +1030,41 @@ Array [
style="width:50%;margin-bottom:48px"
>
2
3
4
6
8
12
diff --git a/components/index.tsx b/components/index.tsx
index 053e09c9ba..88b07ad7ae 100644
--- a/components/index.tsx
+++ b/components/index.tsx
@@ -92,7 +92,7 @@ export { default as Form } from './form';
export { default as Grid } from './grid';
-export type { InputProps } from './input';
+export type { InputProps, InputRef } from './input';
export { default as Input } from './input';
export type { ImageProps } from './image';
diff --git a/components/input-number/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/input-number/__tests__/__snapshots__/demo-extend.test.ts.snap
index 83bab7495c..3091a683e1 100644
--- a/components/input-number/__tests__/__snapshots__/demo-extend.test.ts.snap
+++ b/components/input-number/__tests__/__snapshots__/demo-extend.test.ts.snap
@@ -1008,6 +1008,90 @@ exports[`renders ./components/input-number/demo/borderless.md extend context cor
`;
+exports[`renders ./components/input-number/demo/controls.md extend context correctly 1`] = `
+
+`;
+
exports[`renders ./components/input-number/demo/digit.md extend context correctly 1`] = `
`;
+
+exports[`renders ./components/input-number/demo/status.md extend context correctly 1`] = `
+
+`;
diff --git a/components/input-number/__tests__/__snapshots__/demo.test.js.snap b/components/input-number/__tests__/__snapshots__/demo.test.js.snap
index 1a00d1d82b..8ae06246ea 100644
--- a/components/input-number/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/input-number/__tests__/__snapshots__/demo.test.js.snap
@@ -735,6 +735,90 @@ exports[`renders ./components/input-number/demo/borderless.md correctly 1`] = `
`;
+exports[`renders ./components/input-number/demo/controls.md correctly 1`] = `
+
+`;
+
exports[`renders ./components/input-number/demo/digit.md correctly 1`] = `
`;
+
+exports[`renders ./components/input-number/demo/status.md correctly 1`] = `
+
+`;
diff --git a/components/input-number/__tests__/__snapshots__/index.test.js.snap b/components/input-number/__tests__/__snapshots__/index.test.js.snap
index a3dcd16a6b..6675131674 100644
--- a/components/input-number/__tests__/__snapshots__/index.test.js.snap
+++ b/components/input-number/__tests__/__snapshots__/index.test.js.snap
@@ -1,5 +1,183 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`InputNumber renders correctly when controls has custom upIcon and downIcon 1`] = `
+
+`;
+
+exports[`InputNumber renders correctly when controls is {} 1`] = `
+
+`;
+
+exports[`InputNumber renders correctly when controls is boolean 1`] = `
+
+`;
+
exports[`InputNumber rtl render component should be rendered correctly in RTL direction 1`] = `
{
expect(onStep).toBeCalledTimes(2);
expect(onStep).toHaveBeenLastCalledWith(1, { offset: 1, type: 'down' });
});
+
+ it('renders correctly when controls is boolean', () => {
+ expect(mount(
).render()).toMatchSnapshot();
+ });
+
+ it('renders correctly when controls is {}', () => {
+ expect(mount(
).render()).toMatchSnapshot();
+ });
+
+ it('renders correctly when controls has custom upIcon and downIcon', () => {
+ const wrapper = mount(
+
,
+ downIcon:
,
+ }}
+ />,
+ );
+ expect(wrapper.render()).toMatchSnapshot();
+ });
+
+ it('should support className', () => {
+ const wrapper = mount(
+
,
+ downIcon:
,
+ }}
+ />,
+ );
+ expect(wrapper.find('.anticon-arrow-up').getDOMNode().className.includes('my-class-name')).toBe(
+ true,
+ );
+ expect(
+ wrapper.find('.anticon-arrow-down').getDOMNode().className.includes('my-class-name'),
+ ).toBe(true);
+ });
});
diff --git a/components/input-number/demo/controls.md b/components/input-number/demo/controls.md
new file mode 100644
index 0000000000..e693a0a9e5
--- /dev/null
+++ b/components/input-number/demo/controls.md
@@ -0,0 +1,25 @@
+---
+order: 99
+debug: true
+title:
+ zh-CN: 图标按钮
+ en-US: Icon
+---
+
+## zh-CN
+
+可以扩展 `controls` 属性用以设置自定义图标。
+
+## en-US
+
+When you need to use a custom `Icon`, you can set the `Icon` component as the property value of `upIcon` and `downIcon`.
+
+```jsx
+import { InputNumber } from 'antd';
+import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
+
+ReactDOM.render(
+
, downIcon:
}} />,
+ mountNode,
+);
+```
diff --git a/components/input-number/demo/status.md b/components/input-number/demo/status.md
new file mode 100644
index 0000000000..602cc98f59
--- /dev/null
+++ b/components/input-number/demo/status.md
@@ -0,0 +1,31 @@
+---
+order: 19
+version: 4.19.0
+title:
+ zh-CN: 自定义状态
+ en-US: Status
+---
+
+## zh-CN
+
+使用 `status` 为 InputNumber 添加状态,可选 `error` 或者 `warning`。
+
+## en-US
+
+Add status to InputNumber with `status`, which could be `error` or `warning`.
+
+```tsx
+import { InputNumber, Space } from 'antd';
+import ClockCircleOutlined from '@ant-design/icons/ClockCircleOutlined';
+
+const ValidateInputs: React.FC = () => (
+
+
+
+ } />
+ } />
+
+);
+
+ReactDOM.render(
, mountNode);
+```
diff --git a/components/input-number/index.en-US.md b/components/input-number/index.en-US.md
index 6b56421621..ccf0444692 100644
--- a/components/input-number/index.en-US.md
+++ b/components/input-number/index.en-US.md
@@ -19,7 +19,7 @@ When a numeric value needs to be provided.
| addonBefore | The label text displayed before (on the left side of) the input field | ReactNode | - | |
| autoFocus | If get focus when component mounted | boolean | false | - |
| bordered | Whether has border style | boolean | true | 4.12.0 |
-| controls | Whether to show `+-` controls | boolean | true | 4.17.0 |
+| controls | Whether to show `+-` controls, or set custom arrows icon | boolean \| { upIcon?: React.ReactNode; downIcon?: React.ReactNode; } | - | 4.19.0 |
| decimalSeparator | Decimal separator | string | - | - |
| defaultValue | The initial value | number | - | - |
| disabled | If disable the input | boolean | false | - |
@@ -30,6 +30,7 @@ When a numeric value needs to be provided.
| parser | Specifies the value extracted from formatter | function(string): number | - | - |
| precision | The precision of input value. Will use `formatter` when config of `formatter` | number | - | - |
| readOnly | If readonly the input | boolean | false | - |
+| status | Set validation status | 'error' \| 'warning' | - | 4.19.0 |
| prefix | The prefix icon for the Input | ReactNode | - | 4.17.0 |
| size | The height of input box | `large` \| `middle` \| `small` | - | - |
| step | The number to which the current value is increased or decreased. It can be an integer or decimal | number \| string | 1 | - |
diff --git a/components/input-number/index.tsx b/components/input-number/index.tsx
index 60bee2fb91..64086960e6 100644
--- a/components/input-number/index.tsx
+++ b/components/input-number/index.tsx
@@ -1,23 +1,32 @@
-import * as React from 'react';
+import DownOutlined from '@ant-design/icons/DownOutlined';
+import UpOutlined from '@ant-design/icons/UpOutlined';
import classNames from 'classnames';
import RcInputNumber, { InputNumberProps as RcInputNumberProps } from 'rc-input-number';
-import UpOutlined from '@ant-design/icons/UpOutlined';
-import DownOutlined from '@ant-design/icons/DownOutlined';
-
+import * as React from 'react';
+import { useContext } from 'react';
import { ConfigContext } from '../config-provider';
import SizeContext, { SizeType } from '../config-provider/SizeContext';
+import { FormItemStatusContext } from '../form/context';
import { cloneElement } from '../_util/reactNode';
+import {
+ getFeedbackIcon,
+ getStatusClassNames,
+ InputStatus,
+ getMergedStatus,
+} from '../_util/statusUtils';
type ValueType = string | number;
export interface InputNumberProps
- extends Omit, 'prefix' | 'size'> {
+ extends Omit, 'prefix' | 'size' | 'controls'> {
prefixCls?: string;
addonBefore?: React.ReactNode;
addonAfter?: React.ReactNode;
prefix?: React.ReactNode;
size?: SizeType;
bordered?: boolean;
+ status?: InputStatus;
+ controls?: boolean | { upIcon?: React.ReactNode; downIcon?: React.ReactNode };
}
const InputNumber = React.forwardRef((props, ref) => {
@@ -37,12 +46,33 @@ const InputNumber = React.forwardRef((props,
prefix,
bordered = true,
readOnly,
+ status: customStatus,
+ controls,
...others
} = props;
const prefixCls = getPrefixCls('input-number', customizePrefixCls);
- const upIcon = ;
- const downIcon = ;
+ let upIcon = ;
+ let downIcon = ;
+ const controlsTemp = typeof controls === 'boolean' ? controls : undefined;
+
+ if (typeof controls === 'object') {
+ upIcon =
+ typeof controls.upIcon === 'undefined' ? (
+ upIcon
+ ) : (
+ {controls.upIcon}
+ );
+ downIcon =
+ typeof controls.downIcon === 'undefined' ? (
+ downIcon
+ ) : (
+ {controls.downIcon}
+ );
+ }
+
+ const { hasFeedback, status: contextStatus } = useContext(FormItemStatusContext);
+ const mergedStatus = getMergedStatus(contextStatus, customStatus);
const mergeSize = customizeSize || size;
const inputNumberClass = classNames(
@@ -53,6 +83,7 @@ const InputNumber = React.forwardRef((props,
[`${prefixCls}-readonly`]: readOnly,
[`${prefixCls}-borderless`]: !bordered,
},
+ getStatusClassNames(prefixCls, mergedStatus),
className,
);
@@ -64,29 +95,35 @@ const InputNumber = React.forwardRef((props,
downHandler={downIcon}
prefixCls={prefixCls}
readOnly={readOnly}
+ controls={controlsTemp}
{...others}
/>
);
- if (prefix != null) {
- const affixWrapperCls = classNames(`${prefixCls}-affix-wrapper`, {
- [`${prefixCls}-affix-wrapper-focused`]: focused,
- [`${prefixCls}-affix-wrapper-disabled`]: props.disabled,
- [`${prefixCls}-affix-wrapper-sm`]: size === 'small',
- [`${prefixCls}-affix-wrapper-lg`]: size === 'large',
- [`${prefixCls}-affix-wrapper-rtl`]: direction === 'rtl',
- [`${prefixCls}-affix-wrapper-readonly`]: readOnly,
- [`${prefixCls}-affix-wrapper-borderless`]: !bordered,
- // className will go to addon wrapper
- [`${className}`]: !(addonBefore || addonAfter) && className,
- });
+ if (prefix != null || hasFeedback) {
+ const affixWrapperCls = classNames(
+ `${prefixCls}-affix-wrapper`,
+ getStatusClassNames(`${prefixCls}-affix-wrapper`, mergedStatus, hasFeedback),
+ {
+ [`${prefixCls}-affix-wrapper-focused`]: focused,
+ [`${prefixCls}-affix-wrapper-disabled`]: props.disabled,
+ [`${prefixCls}-affix-wrapper-sm`]: size === 'small',
+ [`${prefixCls}-affix-wrapper-lg`]: size === 'large',
+ [`${prefixCls}-affix-wrapper-rtl`]: direction === 'rtl',
+ [`${prefixCls}-affix-wrapper-readonly`]: readOnly,
+ [`${prefixCls}-affix-wrapper-borderless`]: !bordered,
+ // className will go to addon wrapper
+ [`${className}`]: !(addonBefore || addonAfter) && className,
+ },
+ );
+
element = (
inputRef.current!.focus()}
>
- {prefix}
+ {prefix && {prefix} }
{cloneElement(element, {
style: null,
value: props.value,
@@ -99,6 +136,9 @@ const InputNumber = React.forwardRef((props,
props.onBlur?.(event);
},
})}
+ {hasFeedback && (
+ {getFeedbackIcon(prefixCls, mergedStatus)}
+ )}
);
}
@@ -122,6 +162,7 @@ const InputNumber = React.forwardRef((props,
[`${prefixCls}-group-wrapper-lg`]: size === 'large',
[`${prefixCls}-group-wrapper-rtl`]: direction === 'rtl',
},
+ getStatusClassNames(`${prefixCls}-group-wrapper`, mergedStatus, hasFeedback),
className,
);
element = (
diff --git a/components/input-number/index.zh-CN.md b/components/input-number/index.zh-CN.md
index 1ddfc5bbf3..aaa83c0854 100644
--- a/components/input-number/index.zh-CN.md
+++ b/components/input-number/index.zh-CN.md
@@ -22,7 +22,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/XOS8qZ0kU/InputNumber.svg
| addonBefore | 带标签的 input,设置前置标签 | ReactNode | - | 4.17.0 |
| autoFocus | 自动获取焦点 | boolean | false | - |
| bordered | 是否有边框 | boolean | true | 4.12.0 |
-| controls | 是否显示增减按钮 | boolean | true | 4.17.0 |
+| controls | 是否显示增减按钮,也可设置自定义箭头图标 | boolean \| { upIcon?: React.ReactNode; downIcon?: React.ReactNode; } | - | 4.19.0 |
| decimalSeparator | 小数点 | string | - | - |
| defaultValue | 初始值 | number | - | - |
| disabled | 禁用 | boolean | false | - |
@@ -33,6 +33,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/XOS8qZ0kU/InputNumber.svg
| parser | 指定从 `formatter` 里转换回数字的方式,和 `formatter` 搭配使用 | function(string): number | - | - |
| precision | 数值精度,配置 `formatter` 时会以 `formatter` 为准 | number | - | - |
| readOnly | 只读 | boolean | false | - |
+| status | 设置校验状态 | 'error' \| 'warning' | - | 4.19.0 |
| prefix | 带有前缀图标的 input | ReactNode | - | 4.17.0 |
| size | 输入框大小 | `large` \| `middle` \| `small` | - | - |
| step | 每次改变步数,可以为小数 | number \| string | 1 | - |
diff --git a/components/input-number/style/affix.less b/components/input-number/style/affix.less
index 3724907288..357ab94d30 100644
--- a/components/input-number/style/affix.less
+++ b/components/input-number/style/affix.less
@@ -8,7 +8,7 @@
&-affix-wrapper {
.input();
// or number handler will cover form status
- position: static;
+ position: relative;
display: inline-flex;
width: 90px;
padding: 0;
@@ -49,14 +49,33 @@
visibility: hidden;
content: '\a0';
}
+
+ .@{ant-prefix}-input-number-handler-wrap {
+ z-index: 2;
+ }
}
- &-prefix {
+ &-prefix,
+ &-suffix {
display: flex;
flex: none;
align-items: center;
+ pointer-events: none;
+ }
+
+ &-prefix {
margin-inline-end: @input-affix-margin;
}
+
+ &-suffix {
+ position: absolute;
+ top: 0;
+ right: 0;
+ z-index: 1;
+ height: 100%;
+ margin-right: @input-padding-horizontal-base;
+ margin-left: @input-affix-margin;
+ }
}
.@{ant-prefix}-input-number-group-wrapper .@{ant-prefix}-input-number-affix-wrapper {
diff --git a/components/input-number/style/index.less b/components/input-number/style/index.less
index fd17e7fda4..97662c0c8a 100644
--- a/components/input-number/style/index.less
+++ b/components/input-number/style/index.less
@@ -2,6 +2,7 @@
@import '../../style/mixins/index';
@import '../../input/style/mixin';
@import './affix';
+@import './status';
@input-number-prefix-cls: ~'@{ant-prefix}-input-number';
@form-item-prefix-cls: ~'@{ant-prefix}-form-item';
diff --git a/components/input-number/style/index.tsx b/components/input-number/style/index.tsx
index 3a3ab0de59..ff215acbf9 100644
--- a/components/input-number/style/index.tsx
+++ b/components/input-number/style/index.tsx
@@ -1,2 +1,4 @@
import '../../style/index.less';
import './index.less';
+
+// deps-lint-skip: form
diff --git a/components/input-number/style/status.less b/components/input-number/style/status.less
new file mode 100644
index 0000000000..df6255880c
--- /dev/null
+++ b/components/input-number/style/status.less
@@ -0,0 +1,45 @@
+@import '../../input/style/mixin';
+
+@input-number-prefix-cls: ~'@{ant-prefix}-input-number';
+
+@input-number-wrapper-cls: @input-number-prefix-cls, ~'@{input-number-prefix-cls}-affix-wrapper';
+
+each(@input-number-wrapper-cls, {
+ .@{value} {
+ &-status-error {
+ .status-color(@value, @error-color, @error-color, @input-bg, @error-color-hover, @error-color-outline);
+ .status-color-common(@input-number-prefix-cls, @error-color, @error-color, @input-bg, @error-color-hover, @error-color-outline)
+ }
+
+ &-status-warning {
+ .status-color(@value, @warning-color, @warning-color, @input-bg, @warning-color-hover, @warning-color-outline);
+ .status-color-common(@input-number-prefix-cls, @warning-color, @warning-color, @input-bg, @warning-color-hover, @warning-color-outline)
+ }
+ }
+});
+
+.@{input-number-prefix-cls}-affix-wrapper {
+ &-status-validating {
+ .@{input-number-prefix-cls}-feedback-icon {
+ display: inline-block;
+ color: @primary-color;
+ }
+ }
+
+ &-status-success {
+ .@{input-number-prefix-cls}-feedback-icon {
+ color: @success-color;
+ animation-name: diffZoomIn1 !important;
+ }
+ }
+}
+
+.@{input-number-prefix-cls}-group-wrapper {
+ &-status-error {
+ .group-status-color(@input-number-prefix-cls, @error-color, @error-color);
+ }
+
+ &-status-warning {
+ .group-status-color(@input-number-prefix-cls, @warning-color, @warning-color);
+ }
+}
diff --git a/components/input/ClearableLabeledInput.tsx b/components/input/ClearableLabeledInput.tsx
index 54272c724d..d325223925 100644
--- a/components/input/ClearableLabeledInput.tsx
+++ b/components/input/ClearableLabeledInput.tsx
@@ -1,12 +1,13 @@
-import * as React from 'react';
-import classNames from 'classnames';
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
-import { tuple } from '../_util/type';
-import type { InputProps } from './Input';
+import classNames from 'classnames';
+import * as React from 'react';
import { DirectionType } from '../config-provider';
import { SizeType } from '../config-provider/SizeContext';
+import { FormItemStatusContext, FormItemStatusContextProps } from '../form/context';
import { cloneElement } from '../_util/reactNode';
-import { getInputClassName, hasPrefixSuffix } from './utils';
+import { getMergedStatus, getStatusClassNames, InputStatus } from '../_util/statusUtils';
+import { tuple } from '../_util/type';
+import type { InputProps } from './Input';
const ClearableInputType = tuple('text', 'input');
@@ -40,24 +41,12 @@ export interface ClearableInputProps extends BasicProps {
addonBefore?: React.ReactNode;
addonAfter?: React.ReactNode;
triggerFocus?: () => void;
+ status?: InputStatus;
}
class ClearableLabeledInput extends React.Component {
- /** @private Do Not use out of this class. We do not promise this is always keep. */
- private containerRef = React.createRef();
-
- onInputMouseUp: React.MouseEventHandler = e => {
- if (this.containerRef.current?.contains(e.target as Element)) {
- const { triggerFocus } = this.props;
- triggerFocus?.();
- }
- };
-
renderClearIcon(prefixCls: string) {
- const { allowClear, value, disabled, readOnly, handleReset, suffix } = this.props;
- if (!allowClear) {
- return null;
- }
+ const { value, disabled, readOnly, handleReset, suffix } = this.props;
const needClear = !disabled && !readOnly && value;
const className = `${prefixCls}-clear-icon`;
return (
@@ -78,118 +67,24 @@ class ClearableLabeledInput extends React.Component {
);
}
- renderSuffix(prefixCls: string) {
- const { suffix, allowClear } = this.props;
- if (suffix || allowClear) {
- return (
-
- {this.renderClearIcon(prefixCls)}
- {suffix}
-
- );
- }
- return null;
- }
-
- renderLabeledIcon(prefixCls: string, element: React.ReactElement) {
+ renderTextAreaWithClearIcon(
+ prefixCls: string,
+ element: React.ReactElement,
+ statusContext: FormItemStatusContextProps,
+ ) {
const {
- focused,
value,
- prefix,
- className,
- size,
- suffix,
- disabled,
allowClear,
- direction,
+ className,
style,
- readOnly,
+ direction,
bordered,
hidden,
+ status: customStatus,
} = this.props;
- if (!hasPrefixSuffix(this.props)) {
- return cloneElement(element, {
- value,
- });
- }
- const suffixNode = this.renderSuffix(prefixCls);
- const prefixNode = prefix ? {prefix} : null;
+ const { status: contextStatus, hasFeedback } = statusContext;
- const affixWrapperCls = classNames(`${prefixCls}-affix-wrapper`, {
- [`${prefixCls}-affix-wrapper-focused`]: focused,
- [`${prefixCls}-affix-wrapper-disabled`]: disabled,
- [`${prefixCls}-affix-wrapper-sm`]: size === 'small',
- [`${prefixCls}-affix-wrapper-lg`]: size === 'large',
- [`${prefixCls}-affix-wrapper-input-with-clear-btn`]: suffix && allowClear && value,
- [`${prefixCls}-affix-wrapper-rtl`]: direction === 'rtl',
- [`${prefixCls}-affix-wrapper-readonly`]: readOnly,
- [`${prefixCls}-affix-wrapper-borderless`]: !bordered,
- // className will go to addon wrapper
- [`${className}`]: !hasAddon(this.props) && className,
- });
- return (
-
- {prefixNode}
- {cloneElement(element, {
- style: null,
- value,
- className: getInputClassName(prefixCls, bordered, size, disabled),
- })}
- {suffixNode}
-
- );
- }
-
- renderInputWithLabel(prefixCls: string, labeledElement: React.ReactElement) {
- const { addonBefore, addonAfter, style, size, className, direction, hidden } = this.props;
- // Not wrap when there is not addons
- if (!hasAddon(this.props)) {
- return labeledElement;
- }
-
- const wrapperClassName = `${prefixCls}-group`;
- const addonClassName = `${wrapperClassName}-addon`;
- const addonBeforeNode = addonBefore ? (
- {addonBefore}
- ) : null;
- const addonAfterNode = addonAfter ? {addonAfter} : null;
-
- const mergedWrapperClassName = classNames(`${prefixCls}-wrapper`, wrapperClassName, {
- [`${wrapperClassName}-rtl`]: direction === 'rtl',
- });
-
- const mergedGroupClassName = classNames(
- `${prefixCls}-group-wrapper`,
- {
- [`${prefixCls}-group-wrapper-sm`]: size === 'small',
- [`${prefixCls}-group-wrapper-lg`]: size === 'large',
- [`${prefixCls}-group-wrapper-rtl`]: direction === 'rtl',
- },
- className,
- );
-
- // Need another wrapper for changing display:table to display:inline-block
- // and put style prop in wrapper
- return (
-
-
- {addonBeforeNode}
- {cloneElement(labeledElement, { style: null })}
- {addonAfterNode}
-
-
- );
- }
-
- renderTextAreaWithClearIcon(prefixCls: string, element: React.ReactElement) {
- const { value, allowClear, className, style, direction, bordered, hidden } = this.props;
if (!allowClear) {
return cloneElement(element, {
value,
@@ -198,6 +93,11 @@ class ClearableLabeledInput extends React.Component {
const affixWrapperCls = classNames(
`${prefixCls}-affix-wrapper`,
`${prefixCls}-affix-wrapper-textarea-with-clear-btn`,
+ getStatusClassNames(
+ `${prefixCls}-affix-wrapper`,
+ getMergedStatus(contextStatus, customStatus),
+ hasFeedback,
+ ),
{
[`${prefixCls}-affix-wrapper-rtl`]: direction === 'rtl',
[`${prefixCls}-affix-wrapper-borderless`]: !bordered,
@@ -217,11 +117,16 @@ class ClearableLabeledInput extends React.Component {
}
render() {
- const { prefixCls, inputType, element } = this.props;
- if (inputType === ClearableInputType[0]) {
- return this.renderTextAreaWithClearIcon(prefixCls, element);
- }
- return this.renderInputWithLabel(prefixCls, this.renderLabeledIcon(prefixCls, element));
+ return (
+
+ {statusContext => {
+ const { prefixCls, inputType, element } = this.props;
+ if (inputType === ClearableInputType[0]) {
+ return this.renderTextAreaWithClearIcon(prefixCls, element, statusContext);
+ }
+ }}
+
+ );
}
}
diff --git a/components/input/Input.tsx b/components/input/Input.tsx
index 02d0b192cd..065ce1f593 100644
--- a/components/input/Input.tsx
+++ b/components/input/Input.tsx
@@ -1,65 +1,25 @@
-import * as React from 'react';
+import React, { forwardRef, useContext, useEffect, useRef } from 'react';
+import RcInput, { InputProps as RcInputProps, InputRef } from 'rc-input';
+import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
import classNames from 'classnames';
-import omit from 'rc-util/lib/omit';
-import type Group from './Group';
-import type Search from './Search';
-import type TextArea from './TextArea';
-import type Password from './Password';
-import { LiteralUnion } from '../_util/type';
-import ClearableLabeledInput from './ClearableLabeledInput';
-import { ConfigConsumer, ConfigConsumerProps, DirectionType } from '../config-provider';
+import { composeRef } from 'rc-util/lib/ref';
import SizeContext, { SizeType } from '../config-provider/SizeContext';
+import {
+ getFeedbackIcon,
+ getMergedStatus,
+ getStatusClassNames,
+ InputStatus,
+} from '../_util/statusUtils';
+import { ConfigContext } from '../config-provider';
+import { FormItemStatusContext } from '../form/context';
+import { hasPrefixSuffix } from './utils';
import devWarning from '../_util/devWarning';
-import { getInputClassName, hasPrefixSuffix } from './utils';
export interface InputFocusOptions extends FocusOptions {
cursor?: 'start' | 'end' | 'all';
}
-export interface ShowCountProps {
- formatter: (args: { count: number; maxLength?: number }) => React.ReactNode;
-}
-
-export interface InputProps
- extends Omit, 'size' | 'prefix' | 'type'> {
- prefixCls?: string;
- size?: SizeType;
- // ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#%3Cinput%3E_types
- type?: LiteralUnion<
- | 'button'
- | 'checkbox'
- | 'color'
- | 'date'
- | 'datetime-local'
- | 'email'
- | 'file'
- | 'hidden'
- | 'image'
- | 'month'
- | 'number'
- | 'password'
- | 'radio'
- | 'range'
- | 'reset'
- | 'search'
- | 'submit'
- | 'tel'
- | 'text'
- | 'time'
- | 'url'
- | 'week',
- string
- >;
- onPressEnter?: React.KeyboardEventHandler;
- addonBefore?: React.ReactNode;
- addonAfter?: React.ReactNode;
- prefix?: React.ReactNode;
- suffix?: React.ReactNode;
- allowClear?: boolean;
- showCount?: boolean | ShowCountProps;
- bordered?: boolean;
- htmlSize?: number;
-}
+export type { InputRef };
export function fixControlledValue(value: T) {
if (typeof value === 'undefined' || value === null) {
@@ -150,276 +110,133 @@ export function triggerFocus(
}
}
-export interface InputState {
- value: any;
- focused: boolean;
- /** `value` from prev props */
- prevValue: any;
+export interface InputProps
+ extends Omit<
+ RcInputProps,
+ 'wrapperClassName' | 'groupClassName' | 'inputClassName' | 'affixWrapperClassName' | 'clearIcon'
+ > {
+ size?: SizeType;
+ status?: InputStatus;
+ bordered?: boolean;
}
-class Input extends React.Component {
- static Group: typeof Group;
+const Input = forwardRef((props, ref) => {
+ const {
+ prefixCls: customizePrefixCls,
+ bordered = true,
+ status: customStatus,
+ size: customSize,
+ onBlur,
+ onFocus,
+ suffix,
+ ...rest
+ } = props;
+ const { getPrefixCls, direction, input } = React.useContext(ConfigContext);
- static Search: typeof Search;
+ const prefixCls = getPrefixCls('input', customizePrefixCls);
+ const inputRef = useRef(null);
- static TextArea: typeof TextArea;
+ // ===================== Status =====================
+ const size = React.useContext(SizeContext);
+ const mergedSize = customSize || size;
- static Password: typeof Password;
+ // ===================== Status =====================
+ const { status: contextStatus, hasFeedback } = useContext(FormItemStatusContext);
+ const mergedStatus = getMergedStatus(contextStatus, customStatus);
- static defaultProps = {
- type: 'text',
- };
-
- input!: HTMLInputElement;
-
- clearableInput!: ClearableLabeledInput;
-
- removePasswordTimeout: any;
-
- direction: DirectionType = 'ltr';
-
- constructor(props: InputProps) {
- super(props);
- const value = typeof props.value === 'undefined' ? props.defaultValue : props.value;
- this.state = {
- value,
- focused: false,
- // eslint-disable-next-line react/no-unused-state
- prevValue: props.value,
- };
- }
-
- static getDerivedStateFromProps(nextProps: InputProps, { prevValue }: InputState) {
- const newState: Partial = { prevValue: nextProps.value };
- if (nextProps.value !== undefined || prevValue !== nextProps.value) {
- newState.value = nextProps.value;
- }
- if (nextProps.disabled) {
- newState.focused = false;
- }
- return newState;
- }
-
- componentDidMount() {
- this.clearPasswordValueAttribute();
- }
-
- // Since polyfill `getSnapshotBeforeUpdate` need work with `componentDidUpdate`.
- // We keep an empty function here.
- componentDidUpdate() {}
-
- getSnapshotBeforeUpdate(prevProps: InputProps) {
- if (hasPrefixSuffix(prevProps) !== hasPrefixSuffix(this.props)) {
+ // ===================== Focus warning =====================
+ const inputHasPrefixSuffix = hasPrefixSuffix(props);
+ const prevHasPrefixSuffix = useRef(inputHasPrefixSuffix);
+ useEffect(() => {
+ if (inputHasPrefixSuffix && !prevHasPrefixSuffix.current) {
devWarning(
- this.input !== document.activeElement,
+ document.activeElement === inputRef.current?.input,
'Input',
`When Input is focused, dynamic add or remove prefix / suffix will make it lose focus caused by dom structure change. Read more: https://ant.design/components/input/#FAQ`,
);
}
- return null;
- }
+ prevHasPrefixSuffix.current = inputHasPrefixSuffix;
+ }, [inputHasPrefixSuffix]);
- componentWillUnmount() {
- if (this.removePasswordTimeout) {
- clearTimeout(this.removePasswordTimeout);
- }
- }
-
- focus = (option?: InputFocusOptions) => {
- triggerFocus(this.input, option);
+ // ===================== Remove Password value =====================
+ const removePasswordTimeoutRef = useRef([]);
+ const removePasswordTimeout = () => {
+ removePasswordTimeoutRef.current.push(
+ window.setTimeout(() => {
+ if (
+ inputRef.current?.input &&
+ inputRef.current?.input.getAttribute('type') === 'password' &&
+ inputRef.current?.input.hasAttribute('value')
+ ) {
+ inputRef.current?.input.removeAttribute('value');
+ }
+ }),
+ );
};
- blur() {
- this.input.blur();
- }
+ useEffect(() => {
+ removePasswordTimeout();
+ return () => removePasswordTimeoutRef.current.forEach(item => window.clearTimeout(item));
+ }, []);
- setSelectionRange(start: number, end: number, direction?: 'forward' | 'backward' | 'none') {
- this.input.setSelectionRange(start, end, direction);
- }
-
- select() {
- this.input.select();
- }
-
- saveClearableInput = (input: ClearableLabeledInput) => {
- this.clearableInput = input;
- };
-
- saveInput = (input: HTMLInputElement) => {
- this.input = input;
- };
-
- onFocus: React.FocusEventHandler = e => {
- const { onFocus } = this.props;
- this.setState({ focused: true }, this.clearPasswordValueAttribute);
- onFocus?.(e);
- };
-
- onBlur: React.FocusEventHandler = e => {
- const { onBlur } = this.props;
- this.setState({ focused: false }, this.clearPasswordValueAttribute);
+ const handleBlur = (e: React.FocusEvent) => {
+ removePasswordTimeout();
onBlur?.(e);
};
- setValue(value: string, callback?: () => void) {
- if (this.props.value === undefined) {
- this.setState({ value }, callback);
- } else {
- callback?.();
- }
- }
-
- handleReset = (e: React.MouseEvent) => {
- this.setValue('', () => {
- this.focus();
- });
- resolveOnChange(this.input, e, this.props.onChange);
+ const handleFocus = (e: React.FocusEvent) => {
+ removePasswordTimeout();
+ onFocus?.(e);
};
- renderInput = (
- prefixCls: string,
- size: SizeType | undefined,
- bordered: boolean,
- input: ConfigConsumerProps['input'] = {},
- ) => {
- const {
- className,
- addonBefore,
- addonAfter,
- size: customizeSize,
- disabled,
- htmlSize,
- } = this.props;
- // Fix https://fb.me/react-unknown-prop
- const otherProps = omit(this.props as InputProps & { inputType: any }, [
- 'prefixCls',
- 'onPressEnter',
- 'addonBefore',
- 'addonAfter',
- 'prefix',
- 'suffix',
- 'allowClear',
- // Input elements must be either controlled or uncontrolled,
- // specify either the value prop, or the defaultValue prop, but not both.
- 'defaultValue',
- 'size',
- 'inputType',
- 'bordered',
- 'htmlSize',
- 'showCount',
- ]);
- return (
-
- );
- };
+ const suffixNode = (hasFeedback || suffix) && (
+ <>
+ {suffix}
+ {hasFeedback && getFeedbackIcon(prefixCls, mergedStatus)}
+ >
+ );
- clearPasswordValueAttribute = () => {
- // https://github.com/ant-design/ant-design/issues/20541
- this.removePasswordTimeout = setTimeout(() => {
- if (
- this.input &&
- this.input.getAttribute('type') === 'password' &&
- this.input.hasAttribute('value')
- ) {
- this.input.removeAttribute('value');
- }
- });
- };
-
- handleChange = (e: React.ChangeEvent) => {
- this.setValue(e.target.value, this.clearPasswordValueAttribute);
- resolveOnChange(this.input, e, this.props.onChange);
- };
-
- handleKeyDown = (e: React.KeyboardEvent) => {
- const { onPressEnter, onKeyDown } = this.props;
- if (onPressEnter && e.keyCode === 13) {
- onPressEnter(e);
- }
- onKeyDown?.(e);
- };
-
- renderShowCountSuffix = (prefixCls: string) => {
- const { value } = this.state;
- const { maxLength, suffix, showCount } = this.props;
- // Max length value
- const hasMaxLength = Number(maxLength) > 0;
-
- if (suffix || showCount) {
- const valueLength = [...fixControlledValue(value)].length;
- let dataCount = null;
- if (typeof showCount === 'object') {
- dataCount = showCount.formatter({ count: valueLength, maxLength });
- } else {
- dataCount = `${valueLength}${hasMaxLength ? ` / ${maxLength}` : ''}`;
- }
- return (
- <>
- {!!showCount && (
-
- {dataCount}
-
- )}
- {suffix}
- >
- );
- }
- return null;
- };
-
- renderComponent = ({ getPrefixCls, direction, input }: ConfigConsumerProps) => {
- const { value, focused } = this.state;
- const { prefixCls: customizePrefixCls, bordered = true } = this.props;
- const prefixCls = getPrefixCls('input', customizePrefixCls);
- this.direction = direction;
-
- const showCountSuffix = this.renderShowCountSuffix(prefixCls);
-
- return (
-
- {size => (
-
- )}
-
- );
- };
-
- render() {
- return {this.renderComponent} ;
- }
-}
+ return (
+ }
+ inputClassName={classNames(
+ {
+ [`${prefixCls}-sm`]: mergedSize === 'small',
+ [`${prefixCls}-lg`]: mergedSize === 'large',
+ [`${prefixCls}-rtl`]: direction === 'rtl',
+ [`${prefixCls}-borderless`]: !bordered,
+ },
+ getStatusClassNames(prefixCls, mergedStatus),
+ )}
+ affixWrapperClassName={classNames(
+ {
+ [`${prefixCls}-affix-wrapper-sm`]: mergedSize === 'small',
+ [`${prefixCls}-affix-wrapper-lg`]: mergedSize === 'large',
+ [`${prefixCls}-affix-wrapper-rtl`]: direction === 'rtl',
+ [`${prefixCls}-affix-wrapper-borderless`]: !bordered,
+ },
+ getStatusClassNames(`${prefixCls}-affix-wrapper`, mergedStatus, hasFeedback),
+ )}
+ wrapperClassName={classNames({
+ [`${prefixCls}-group-rtl`]: direction === 'rtl',
+ })}
+ groupClassName={classNames(
+ {
+ [`${prefixCls}-group-wrapper-sm`]: mergedSize === 'small',
+ [`${prefixCls}-group-wrapper-lg`]: mergedSize === 'large',
+ [`${prefixCls}-group-wrapper-rtl`]: direction === 'rtl',
+ },
+ getStatusClassNames(`${prefixCls}-group-wrapper`, mergedStatus, hasFeedback),
+ )}
+ />
+ );
+});
export default Input;
diff --git a/components/input/Password.tsx b/components/input/Password.tsx
index efcf854746..1d2155a091 100644
--- a/components/input/Password.tsx
+++ b/components/input/Password.tsx
@@ -5,8 +5,8 @@ import EyeOutlined from '@ant-design/icons/EyeOutlined';
import EyeInvisibleOutlined from '@ant-design/icons/EyeInvisibleOutlined';
import { useState } from 'react';
+import Input, { InputRef, InputProps } from './Input';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
-import Input, { InputProps } from './Input';
export interface PasswordProps extends InputProps {
readonly inputPrefixCls?: string;
@@ -20,7 +20,7 @@ const ActionMap: Record = {
hover: 'onMouseOver',
};
-const Password = React.forwardRef((props, ref) => {
+const Password = React.forwardRef((props, ref) => {
const [visible, setVisible] = useState(false);
const onVisibleChange = () => {
diff --git a/components/input/Search.tsx b/components/input/Search.tsx
index 5fc2631dc2..2a4ac4cd7e 100644
--- a/components/input/Search.tsx
+++ b/components/input/Search.tsx
@@ -2,7 +2,7 @@ import * as React from 'react';
import classNames from 'classnames';
import { composeRef } from 'rc-util/lib/ref';
import SearchOutlined from '@ant-design/icons/SearchOutlined';
-import Input, { InputProps } from './Input';
+import Input, { InputProps, InputRef } from './Input';
import Button from '../button';
import SizeContext from '../config-provider/SizeContext';
import { ConfigContext } from '../config-provider';
@@ -21,7 +21,7 @@ export interface SearchProps extends InputProps {
loading?: boolean;
}
-const Search = React.forwardRef ((props, ref) => {
+const Search = React.forwardRef((props, ref) => {
const {
prefixCls: customizePrefixCls,
inputPrefixCls: customizeInputPrefixCls,
@@ -42,7 +42,7 @@ const Search = React.forwardRef ((props, ref) => {
const size = customizeSize || contextSize;
- const inputRef = React.useRef (null);
+ const inputRef = React.useRef(null);
const onChange = (e: React.ChangeEvent) => {
if (e && e.target && e.type === 'click' && customOnSearch) {
@@ -61,7 +61,7 @@ const Search = React.forwardRef ((props, ref) => {
const onSearch = (e: React.MouseEvent | React.KeyboardEvent) => {
if (customOnSearch) {
- customOnSearch(inputRef.current?.input.value!, e);
+ customOnSearch(inputRef.current?.input?.value!, e);
}
};
@@ -129,7 +129,7 @@ const Search = React.forwardRef ((props, ref) => {
return (
(inputRef, ref)}
+ ref={composeRef(inputRef, ref)}
onPressEnter={onSearch}
{...restProps}
size={size}
diff --git a/components/input/TextArea.tsx b/components/input/TextArea.tsx
index 933f52eafb..88001c7365 100644
--- a/components/input/TextArea.tsx
+++ b/components/input/TextArea.tsx
@@ -1,13 +1,20 @@
-import * as React from 'react';
+import classNames from 'classnames';
import RcTextArea, { TextAreaProps as RcTextAreaProps } from 'rc-textarea';
import ResizableTextArea from 'rc-textarea/lib/ResizableTextArea';
-import omit from 'rc-util/lib/omit';
-import classNames from 'classnames';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
-import ClearableLabeledInput from './ClearableLabeledInput';
+import omit from 'rc-util/lib/omit';
+import * as React from 'react';
import { ConfigContext } from '../config-provider';
-import { fixControlledValue, resolveOnChange, triggerFocus, InputFocusOptions } from './Input';
import SizeContext, { SizeType } from '../config-provider/SizeContext';
+import { FormItemStatusContext } from '../form/context';
+import {
+ getFeedbackIcon,
+ getStatusClassNames,
+ InputStatus,
+ getMergedStatus,
+} from '../_util/statusUtils';
+import ClearableLabeledInput from './ClearableLabeledInput';
+import { fixControlledValue, InputFocusOptions, resolveOnChange, triggerFocus } from './Input';
interface ShowCountProps {
formatter: (args: { count: number; maxLength?: number }) => string;
@@ -42,6 +49,7 @@ export interface TextAreaProps extends RcTextAreaProps {
bordered?: boolean;
showCount?: boolean | ShowCountProps;
size?: SizeType;
+ status?: InputStatus;
}
export interface TextAreaRef {
@@ -63,6 +71,7 @@ const TextArea = React.forwardRef(
onCompositionStart,
onCompositionEnd,
onChange,
+ status: customStatus,
...props
},
ref,
@@ -70,6 +79,9 @@ const TextArea = React.forwardRef(
const { getPrefixCls, direction } = React.useContext(ConfigContext);
const size = React.useContext(SizeContext);
+ const { status: contextStatus, hasFeedback } = React.useContext(FormItemStatusContext);
+ const mergedStatus = getMergedStatus(contextStatus, customStatus);
+
const innerRef = React.useRef(null);
const clearableInputRef = React.useRef(null);
@@ -161,12 +173,15 @@ const TextArea = React.forwardRef(
const textArea = (
(
handleReset={handleReset}
ref={clearableInputRef}
bordered={bordered}
+ status={customStatus}
style={showCount ? undefined : style}
/>
);
// Only show text area wrapper when needed
- if (showCount) {
+ if (showCount || hasFeedback) {
const valueLength = [...val].length;
let dataCount = '';
@@ -217,14 +233,16 @@ const TextArea = React.forwardRef(
`${prefixCls}-textarea`,
{
[`${prefixCls}-textarea-rtl`]: direction === 'rtl',
+ [`${prefixCls}-textarea-show-count`]: showCount,
},
- `${prefixCls}-textarea-show-count`,
+ getStatusClassNames(`${prefixCls}-textarea`, mergedStatus, hasFeedback),
className,
)}
style={style}
data-count={dataCount}
>
{textareaNode}
+ {hasFeedback && getFeedbackIcon(prefixCls, mergedStatus)}
);
}
diff --git a/components/input/__tests__/Password.test.js b/components/input/__tests__/Password.test.js
index 734c1a8a20..0eac90ea2f 100644
--- a/components/input/__tests__/Password.test.js
+++ b/components/input/__tests__/Password.test.js
@@ -25,7 +25,7 @@ describe('Input.Password', () => {
it('should support size', () => {
const wrapper = mount(
);
- expect(wrapper.find('input').hasClass('ant-input-lg')).toBe(true);
+ expect(wrapper.find('.ant-input-affix-wrapper-lg')).toBeTruthy();
expect(wrapper.render()).toMatchSnapshot();
});
diff --git a/components/input/__tests__/__snapshots__/Password.test.js.snap b/components/input/__tests__/__snapshots__/Password.test.js.snap
index 2753b0914a..c1774e867a 100644
--- a/components/input/__tests__/__snapshots__/Password.test.js.snap
+++ b/components/input/__tests__/__snapshots__/Password.test.js.snap
@@ -2,7 +2,7 @@
exports[`Input.Password rtl render component should be rendered correctly in RTL direction 1`] = `
diff --git a/components/input/__tests__/__snapshots__/Search.test.js.snap b/components/input/__tests__/__snapshots__/Search.test.js.snap
index 29de245a3a..bebab65545 100644
--- a/components/input/__tests__/__snapshots__/Search.test.js.snap
+++ b/components/input/__tests__/__snapshots__/Search.test.js.snap
@@ -2,7 +2,7 @@
exports[`Input.Search rtl render component should be rendered correctly in RTL direction 1`] = `
-
-
-
+
+
+
+
,
@@ -5004,7 +5009,7 @@ exports[`renders ./components/input/demo/borderless-debug.md extend context corr
class="ant-input-affix-wrapper ant-input-affix-wrapper-borderless"
>
-
-
-
+
+
+
+
@@ -5043,7 +5053,7 @@ exports[`renders ./components/input/demo/borderless-debug.md extend context corr
¥
@@ -5062,7 +5072,7 @@ exports[`renders ./components/input/demo/borderless-debug.md extend context corr
¥
-
-
-
+
+
+
+
@@ -5555,24 +5570,29 @@ exports[`renders ./components/input/demo/group.md extend context correctly 1`] =
class="ant-input-suffix"
>
-
-
-
+
+
+
+
@@ -8937,24 +8957,29 @@ exports[`renders ./components/input/demo/search-input.md extend context correctl
class="ant-input-suffix"
>
-
-
-
+
+
+
+
@@ -9018,24 +9043,29 @@ exports[`renders ./components/input/demo/search-input.md extend context correctl
class="ant-input-suffix"
>
-
-
-
+
+
+
+
@@ -9122,7 +9152,7 @@ exports[`renders ./components/input/demo/search-input.md extend context correctl
style="margin-bottom:8px"
>
-
-
-
+
+
+
+
@@ -9180,7 +9215,7 @@ exports[`renders ./components/input/demo/search-input.md extend context correctl
class="ant-space-item"
>
,
,
+
+
+
+
+
+
+
+
+
+`;
+
exports[`renders ./components/input/demo/textarea.md extend context correctly 1`] = `
Array [
+`;
+
exports[`renders ./components/input/demo/textarea.md correctly 1`] = `
Array [
,
]
`;
+
+exports[`renders ./components/mentions/demo/status.md extend context correctly 1`] = `
+
+`;
diff --git a/components/mentions/__tests__/__snapshots__/demo.test.js.snap b/components/mentions/__tests__/__snapshots__/demo.test.js.snap
index 8440ca56a2..2e2c58f457 100644
--- a/components/mentions/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/mentions/__tests__/__snapshots__/demo.test.js.snap
@@ -206,3 +206,39 @@ Array [
,
]
`;
+
+exports[`renders ./components/mentions/demo/status.md correctly 1`] = `
+
+`;
diff --git a/components/mentions/demo/status.md b/components/mentions/demo/status.md
new file mode 100644
index 0000000000..5d64166744
--- /dev/null
+++ b/components/mentions/demo/status.md
@@ -0,0 +1,51 @@
+---
+order: 8
+title:
+ zh-CN: 自定义状态
+ en-US: Status
+---
+
+## zh-CN
+
+使用 `status` 为 Mentions 添加状态。可选 `error` 或者 `warning`。
+
+## en-US
+
+Add status to Mentions with `status`, which could be `error` or `warning`。
+
+```jsx
+import { Mentions, Space } from 'antd';
+
+const { Option } = Mentions;
+
+function onChange(value) {
+ console.log('Change:', value);
+}
+
+function onSelect(option) {
+ console.log('select', option);
+}
+
+const MentionsStatuses = () => {
+ const options = (
+ <>
+
afc163
+
zombieJ
+
yesmeck
+ >
+ );
+
+ return (
+
+
+ {options}
+
+
+ {options}
+
+
+ );
+};
+
+ReactDOM.render(
, mountNode);
+```
diff --git a/components/mentions/index.en-US.md b/components/mentions/index.en-US.md
index bc3f4295c4..ccdfc7ca61 100644
--- a/components/mentions/index.en-US.md
+++ b/components/mentions/index.en-US.md
@@ -21,32 +21,33 @@ When you need to mention someone or something.
### Mention
-| Property | Description | Type | Default |
-| --- | --- | --- | --- |
-| autoFocus | Auto get focus when component mounted | boolean | false |
-| autoSize | Textarea height autosize feature, can be set to true \| false or an object { minRows: 2, maxRows: 6 } | boolean \| object | false |
-| defaultValue | Default value | string | - |
-| filterOption | Customize filter option logic | false \| (input: string, option: OptionProps) => boolean | - |
-| getPopupContainer | Set the mount HTML node for suggestions | () => HTMLElement | - |
-| notFoundContent | Set mentions content when not match | ReactNode | `Not Found` |
-| placement | Set popup placement | `top` \| `bottom` | `bottom` |
-| prefix | Set trigger prefix keyword | string \| string\[] | `@` |
-| split | Set split string before and after selected mention | string | ` ` |
-| validateSearch | Customize trigger search logic | (text: string, props: MentionsProps) => void | - |
-| value | Set value of mentions | string | - |
-| onBlur | Trigger when mentions lose focus | () => void | - |
-| onChange | Trigger when value changed | (text: string) => void | - |
-| onFocus | Trigger when mentions get focus | () => void | - |
-| onResize | The callback function that is triggered when textarea resize | function({ width, height }) | - |
-| onSearch | Trigger when prefix hit | (text: string, prefix: string) => void | - |
-| onSelect | Trigger when user select the option | (option: OptionProps, prefix: string) => void | - |
+| Property | Description | Type | Default | Version |
+| --- | --- | --- | --- | --- |
+| autoFocus | Auto get focus when component mounted | boolean | false | |
+| autoSize | Textarea height autosize feature, can be set to true \| false or an object { minRows: 2, maxRows: 6 } | boolean \| object | false | |
+| defaultValue | Default value | string | - | |
+| filterOption | Customize filter option logic | false \| (input: string, option: OptionProps) => boolean | - | |
+| getPopupContainer | Set the mount HTML node for suggestions | () => HTMLElement | - | |
+| notFoundContent | Set mentions content when not match | ReactNode | `Not Found` | |
+| placement | Set popup placement | `top` \| `bottom` | `bottom` | |
+| prefix | Set trigger prefix keyword | string \| string\[] | `@` | |
+| split | Set split string before and after selected mention | string | ` ` | |
+| status | Set validation status | 'error' \| 'warning' \| 'success' \| 'validating' | - | 4.19.0 |
+| validateSearch | Customize trigger search logic | (text: string, props: MentionsProps) => void | - | |
+| value | Set value of mentions | string | - | |
+| onBlur | Trigger when mentions lose focus | () => void | - | |
+| onChange | Trigger when value changed | (text: string) => void | - | |
+| onFocus | Trigger when mentions get focus | () => void | - | |
+| onResize | The callback function that is triggered when textarea resize | function({ width, height }) | - | |
+| onSearch | Trigger when prefix hit | (text: string, prefix: string) => void | - | |
+| onSelect | Trigger when user select the option | (option: OptionProps, prefix: string) => void | - | |
### Mention methods
-| Name | Description |
-| --- | --- |
-| blur() | Remove focus |
-| focus() | Get focus |
+| Name | Description |
+| ------- | ------------ |
+| blur() | Remove focus |
+| focus() | Get focus |
### Option
diff --git a/components/mentions/index.tsx b/components/mentions/index.tsx
index aa2e3d97b3..cff2ba6317 100644
--- a/components/mentions/index.tsx
+++ b/components/mentions/index.tsx
@@ -5,6 +5,13 @@ import { MentionsProps as RcMentionsProps } from 'rc-mentions/lib/Mentions';
import { composeRef } from 'rc-util/lib/ref';
import Spin from '../spin';
import { ConfigContext } from '../config-provider';
+import { FormItemStatusContext } from '../form/context';
+import {
+ getFeedbackIcon,
+ getMergedStatus,
+ getStatusClassNames,
+ InputStatus,
+} from '../_util/statusUtils';
export const { Option } = RcMentions;
@@ -22,6 +29,7 @@ export interface OptionProps {
export interface MentionProps extends RcMentionsProps {
loading?: boolean;
+ status?: InputStatus;
}
export interface MentionState {
@@ -53,6 +61,7 @@ const InternalMentions: React.ForwardRefRenderFunction
=
filterOption,
children,
notFoundContent,
+ status: customStatus,
...restProps
},
ref,
@@ -61,6 +70,8 @@ const InternalMentions: React.ForwardRefRenderFunction =
const innerRef = React.useRef();
const mergedRef = composeRef(ref, innerRef);
const { getPrefixCls, renderEmpty, direction } = React.useContext(ConfigContext);
+ const { status: contextStatus, hasFeedback } = React.useContext(FormItemStatusContext);
+ const mergedStatus = getMergedStatus(contextStatus, customStatus);
const onFocus: React.FocusEventHandler = (...args) => {
if (restProps.onFocus) {
@@ -112,10 +123,11 @@ const InternalMentions: React.ForwardRefRenderFunction =
[`${prefixCls}-focused`]: focused,
[`${prefixCls}-rtl`]: direction === 'rtl',
},
- className,
+ getStatusClassNames(prefixCls, mergedStatus),
+ !hasFeedback && className,
);
- return (
+ const mentions = (
=
{getOptions()}
);
+
+ if (hasFeedback) {
+ return (
+
+ {mentions}
+ {getFeedbackIcon(prefixCls, mergedStatus)}
+
+ );
+ }
+
+ return mentions;
};
const Mentions = React.forwardRef(InternalMentions) as CompoundedComponent;
diff --git a/components/mentions/index.zh-CN.md b/components/mentions/index.zh-CN.md
index 7876603712..d303d5b9e3 100644
--- a/components/mentions/index.zh-CN.md
+++ b/components/mentions/index.zh-CN.md
@@ -22,36 +22,37 @@ cover: https://gw.alipayobjects.com/zos/alicdn/jPE-itMFM/Mentions.svg
### Mentions
-| 参数 | 说明 | 类型 | 默认值 |
-| --- | --- | --- | --- |
-| autoFocus | 自动获得焦点 | boolean | false |
-| autoSize | 自适应内容高度,可设置为 true \| false 或对象:{ minRows: 2, maxRows: 6 } | boolean \| object | false |
-| defaultValue | 默认值 | string | - |
-| filterOption | 自定义过滤逻辑 | false \| (input: string, option: OptionProps) => boolean | - |
-| getPopupContainer | 指定建议框挂载的 HTML 节点 | () => HTMLElement | - |
-| notFoundContent | 当下拉列表为空时显示的内容 | ReactNode | `Not Found` |
-| placement | 弹出层展示位置 | `top` \| `bottom` | `bottom` |
-| prefix | 设置触发关键字 | string \| string\[] | `@` |
-| split | 设置选中项前后分隔符 | string | ` ` |
-| validateSearch | 自定义触发验证逻辑 | (text: string, props: MentionsProps) => void | - |
-| value | 设置值 | string | - |
-| onBlur | 失去焦点时触发 | () => void | - |
-| onChange | 值改变时触发 | (text: string) => void | - |
-| onFocus | 获得焦点时触发 | () => void | - |
-| onResize | resize 回调 | function({ width, height }) | - |
-| onSearch | 搜索时触发 | (text: string, prefix: string) => void | - |
-| onSelect | 选择选项时触发 | (option: OptionProps, prefix: string) => void | - |
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+| --- | --- | --- | --- | --- |
+| autoFocus | 自动获得焦点 | boolean | false | |
+| autoSize | 自适应内容高度,可设置为 true \| false 或对象:{ minRows: 2, maxRows: 6 } | boolean \| object | false | |
+| defaultValue | 默认值 | string | - | |
+| filterOption | 自定义过滤逻辑 | false \| (input: string, option: OptionProps) => boolean | - | |
+| getPopupContainer | 指定建议框挂载的 HTML 节点 | () => HTMLElement | - | |
+| notFoundContent | 当下拉列表为空时显示的内容 | ReactNode | `Not Found` | |
+| placement | 弹出层展示位置 | `top` \| `bottom` | `bottom` | |
+| prefix | 设置触发关键字 | string \| string\[] | `@` | |
+| split | 设置选中项前后分隔符 | string | ` ` | |
+| status | 设置校验状态 | 'error' \| 'warning' | - | 4.19.0 |
+| validateSearch | 自定义触发验证逻辑 | (text: string, props: MentionsProps) => void | - | |
+| value | 设置值 | string | - | |
+| onBlur | 失去焦点时触发 | () => void | - | |
+| onChange | 值改变时触发 | (text: string) => void | - | |
+| onFocus | 获得焦点时触发 | () => void | - | |
+| onResize | resize 回调 | function({ width, height }) | - | |
+| onSearch | 搜索时触发 | (text: string, prefix: string) => void | - | |
+| onSelect | 选择选项时触发 | (option: OptionProps, prefix: string) => void | - | |
### Mentions 方法
-| 名称 | 描述 |
-| --- | --- |
-| blur() | 移除焦点 |
+| 名称 | 描述 |
+| ------- | -------- |
+| blur() | 移除焦点 |
| focus() | 获取焦点 |
### Option
-| 参数 | 说明 | 类型 | 默认值 |
-| --- | --- | --- | --- |
-| children | 选项内容 | ReactNode | - |
-| value | 选择时填充的值 | string | - |
+| 参数 | 说明 | 类型 | 默认值 |
+| -------- | -------------- | --------- | ------ |
+| children | 选项内容 | ReactNode | - |
+| value | 选择时填充的值 | string | - |
diff --git a/components/mentions/style/index.less b/components/mentions/style/index.less
index c18683fca6..489a7ad40f 100644
--- a/components/mentions/style/index.less
+++ b/components/mentions/style/index.less
@@ -1,6 +1,7 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@import '../../input/style/mixin';
+@import './status';
@mention-prefix-cls: ~'@{ant-prefix}-mentions';
diff --git a/components/mentions/style/index.tsx b/components/mentions/style/index.tsx
index 5fa72cf4f6..b39860748d 100644
--- a/components/mentions/style/index.tsx
+++ b/components/mentions/style/index.tsx
@@ -3,3 +3,5 @@ import './index.less';
// style dependencies
import '../../empty/style';
import '../../spin/style';
+
+// deps-lint-skip: form
diff --git a/components/mentions/style/status.less b/components/mentions/style/status.less
new file mode 100644
index 0000000000..83cbb3ab8e
--- /dev/null
+++ b/components/mentions/style/status.less
@@ -0,0 +1,43 @@
+@import '../../input/style/mixin';
+
+@mention-prefix-cls: ~'@{ant-prefix}-mentions';
+@input-prefix-cls: ~'@{ant-prefix}-input';
+
+.@{mention-prefix-cls} {
+ &-status-error {
+ .status-color(@mention-prefix-cls, @error-color, @error-color, @input-bg, @error-color-hover, @error-color-outline);
+ .status-color-common(@input-prefix-cls, @error-color, @error-color, @input-bg, @error-color-hover, @error-color-outline);
+ }
+
+ &-status-warning {
+ .status-color(@mention-prefix-cls, @warning-color, @warning-color, @input-bg, @warning-color-hover, @warning-color-outline);
+ .status-color-common(@input-prefix-cls, @warning-color, @warning-color, @input-bg, @warning-color-hover, @warning-color-outline);
+ }
+
+ &-affix-wrapper {
+ position: relative;
+
+ .@{mention-prefix-cls}-feedback-icon {
+ position: absolute;
+ top: 0;
+ right: @input-padding-horizontal-base;
+ bottom: 0;
+ z-index: 1;
+ display: inline-flex;
+ align-items: center;
+ margin: auto;
+ }
+
+ &-status-error {
+ .@{mention-prefix-cls}-feedback-icon {
+ color: @error-color;
+ }
+ }
+
+ &-has-warning {
+ .@{mention-prefix-cls}-feedback-icon {
+ color: @warning-color;
+ }
+ }
+ }
+}
diff --git a/components/menu/SubMenu.tsx b/components/menu/SubMenu.tsx
index 6268b5cd79..97d49ed7fb 100644
--- a/components/menu/SubMenu.tsx
+++ b/components/menu/SubMenu.tsx
@@ -2,7 +2,7 @@ import * as React from 'react';
import { SubMenu as RcSubMenu, useFullPath } from 'rc-menu';
import classNames from 'classnames';
import omit from 'rc-util/lib/omit';
-import MenuContext from './MenuContext';
+import MenuContext, { MenuTheme } from './MenuContext';
import { isValidElement, cloneElement } from '../_util/reactNode';
interface TitleEventEntity {
@@ -23,10 +23,11 @@ export interface SubMenuProps {
popupOffset?: [number, number];
popupClassName?: string;
children?: React.ReactNode;
+ theme?: MenuTheme;
}
function SubMenu(props: SubMenuProps) {
- const { popupClassName, icon, title } = props;
+ const { popupClassName, icon, title, theme } = props;
const context = React.useContext(MenuContext);
const { prefixCls, inlineCollapsed, antdMenuTheme } = context;
@@ -71,7 +72,11 @@ function SubMenu(props: SubMenuProps) {
);
diff --git a/components/menu/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/menu/__tests__/__snapshots__/demo-extend.test.ts.snap
index c5f320fc10..96a13c9541 100644
--- a/components/menu/__tests__/__snapshots__/demo-extend.test.ts.snap
+++ b/components/menu/__tests__/__snapshots__/demo-extend.test.ts.snap
@@ -3801,6 +3801,369 @@ Array [
]
`;
+exports[`renders ./components/menu/demo/submenu-theme.md extend context correctly 1`] = `
+Array [
+
+
+
+ Light
+
+ ,
+ ,
+ ,
+ ,
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
,
+]
+`;
+
exports[`renders ./components/menu/demo/switch-mode.md extend context correctly 1`] = `
Array [
+
+
+ Light
+
+ ,
+ ,
+ ,
+ ,
+
,
+]
+`;
+
exports[`renders ./components/menu/demo/switch-mode.md correctly 1`] = `
Array [
{
);
});
+ describe('allows the overriding of theme at the popup submenu level', () => {
+ const menuModesWithPopupSubMenu = ['horizontal', 'vertical'];
+
+ menuModesWithPopupSubMenu.forEach(menuMode => {
+ it(`when menu is mode ${menuMode}`, () => {
+ const wrapper = mount(
+
+
+ Option 1
+ Option 2
+
+ menu2
+ ,
+ );
+
+ act(() => {
+ jest.runAllTimers();
+ wrapper.update();
+ });
+
+ expect(wrapper.find('ul.ant-menu-root').hasClass('ant-menu-dark')).toBeTruthy();
+ expect(wrapper.find('div.ant-menu-submenu-popup').hasClass('ant-menu-light')).toBeTruthy();
+ });
+ });
+ });
+
// https://github.com/ant-design/ant-design/pulls/4677
// https://github.com/ant-design/ant-design/issues/4692
// TypeError: Cannot read property 'indexOf' of undefined
diff --git a/components/menu/demo/submenu-theme.md b/components/menu/demo/submenu-theme.md
new file mode 100755
index 0000000000..e44fb4f7e7
--- /dev/null
+++ b/components/menu/demo/submenu-theme.md
@@ -0,0 +1,65 @@
+---
+order: 5
+title:
+ zh-CN: 子菜单主题
+ en-US: Sub-menu theme
+---
+
+## zh-CN
+
+内建了两套主题 `light` 和 `dark`,默认 `light`。
+
+## en-US
+
+The Sub-menu will inherit the theme of `Menu`, but you can override this at the `SubMenu` level via the `theme` prop.
+
+```jsx
+import { Menu, Switch } from 'antd';
+import { MailOutlined } from '@ant-design/icons';
+
+const { SubMenu } = Menu;
+
+const SubMenuTheme = () => {
+ const [theme, setTheme] = React.useState('light');
+ const [current, setCurrent] = React.useState('1');
+
+ const changeTheme = value => {
+ setTheme(value ? 'dark' : 'light');
+ };
+
+ const handleClick = e => {
+ setCurrent(e.key);
+ };
+
+ return (
+ <>
+
+
+
+
+ } title="Navigation One" theme={theme}>
+ Option 1
+ Option 2
+ Option 3
+
+ Option 5
+ Option 6
+
+ >
+ );
+};
+
+ReactDOM.render( , mountNode);
+```
diff --git a/components/menu/demo/switch-mode.md b/components/menu/demo/switch-mode.md
index 1601bb7930..09ad107654 100755
--- a/components/menu/demo/switch-mode.md
+++ b/components/menu/demo/switch-mode.md
@@ -1,5 +1,5 @@
---
-order: 5
+order: 6
title:
zh-CN: 切换菜单类型
en-US: Switch the menu type
diff --git a/components/menu/index.en-US.md b/components/menu/index.en-US.md
index 8ef44e08fb..e40c8f658d 100644
--- a/components/menu/index.en-US.md
+++ b/components/menu/index.en-US.md
@@ -90,7 +90,7 @@ More layouts with navigation: [Layout](/components/layout).
### Menu.SubMenu
| Param | Description | Type | Default value | Version |
-| --- | --- | --- | --- | --- |
+| --- | --- | --- | --- | --- | --- |
| children | Sub-menus or sub-menu items | Array<MenuItem \| SubMenu> | - | |
| disabled | Whether sub-menu is disabled | boolean | false | |
| icon | Icon of sub menu | ReactNode | - | 4.2.0 |
@@ -98,6 +98,7 @@ More layouts with navigation: [Layout](/components/layout).
| popupClassName | Sub-menu class name, not working when `mode="inline"` | string | - | |
| popupOffset | Sub-menu offset, not working when `mode="inline"` | \[number, number] | - | |
| title | Title of sub menu | ReactNode | - | |
+| theme | Color theme of the SubMenu (inherits from Menu by default) | | `light` \| `dark` | - | 4.19.0 |
| onTitleClick | Callback executed when the sub-menu title is clicked | function({ key, domEvent }) | - | |
### Menu.ItemGroup
diff --git a/components/menu/index.zh-CN.md b/components/menu/index.zh-CN.md
index 267a7b2d8f..b85a613d8f 100644
--- a/components/menu/index.zh-CN.md
+++ b/components/menu/index.zh-CN.md
@@ -91,7 +91,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/3XZcjGpvK/Menu.svg
### Menu.SubMenu
| 参数 | 说明 | 类型 | 默认值 | 版本 |
-| --- | --- | --- | --- | --- |
+| --- | --- | --- | --- | --- | --- |
| children | 子菜单的菜单项 | Array<MenuItem \| SubMenu> | - | |
| disabled | 是否禁用 | boolean | false | |
| icon | 菜单图标 | ReactNode | - | 4.2.0 |
@@ -100,6 +100,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/3XZcjGpvK/Menu.svg
| popupOffset | 子菜单偏移量,`mode="inline"` 时无效 | \[number, number] | - | |
| title | 子菜单项值 | ReactNode | - | |
| onTitleClick | 点击子菜单标题 | function({ key, domEvent }) | - | |
+| theme | 设置子菜单的主题,默认从 Menu 上继承 | | `light` \| `dark` | - | 4.19.0 |
### Menu.ItemGroup
diff --git a/components/notification/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/notification/__tests__/__snapshots__/demo-extend.test.ts.snap
index 55b6e2fd12..586575b02d 100644
--- a/components/notification/__tests__/__snapshots__/demo-extend.test.ts.snap
+++ b/components/notification/__tests__/__snapshots__/demo-extend.test.ts.snap
@@ -189,6 +189,77 @@ Array [
exports[`renders ./components/notification/demo/placement.md extend context correctly 1`] = `
+
+
+
+
+
+
+
+
+
+ top
+
+
+
+
+
+
+
+
+
+
+
+ bottom
+
+
+
+
+
diff --git a/components/notification/__tests__/__snapshots__/demo.test.js.snap b/components/notification/__tests__/__snapshots__/demo.test.js.snap
index 7717828a77..419ebc8e60 100644
--- a/components/notification/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/notification/__tests__/__snapshots__/demo.test.js.snap
@@ -189,6 +189,77 @@ Array [
exports[`renders ./components/notification/demo/placement.md correctly 1`] = `
+
+
+
+
+
+
+
+
+
+ top
+
+
+
+
+
+
+
+
+
+
+
+ bottom
+
+
+
+
+
diff --git a/components/notification/__tests__/placement.test.js b/components/notification/__tests__/placement.test.js
index 8d59eddb49..e7fed3a720 100644
--- a/components/notification/__tests__/placement.test.js
+++ b/components/notification/__tests__/placement.test.js
@@ -34,6 +34,22 @@ describe('Notification.placement', () => {
const defaultBottom = '24px';
let style;
+ // top
+ open({
+ placement: 'top',
+ top: 50,
+ });
+ style = getStyle($$('.ant-notification-top')[0]);
+ expect(style.top).toBe('50px');
+ expect(style.left).toBe('0px');
+ expect(style.right).toBe('0px');
+ expect(style.bottom).toBe('');
+
+ open({
+ placement: 'top',
+ });
+ expect($$('.ant-notification-top').length).toBe(1);
+
// topLeft
open({
placement: 'topLeft',
@@ -63,6 +79,22 @@ describe('Notification.placement', () => {
});
expect($$('.ant-notification-topRight').length).toBe(1);
+ // bottom
+ open({
+ placement: 'bottom',
+ bottom: 100,
+ });
+ style = getStyle($$('.ant-notification-bottom')[0]);
+ expect(style.top).toBe('');
+ expect(style.left).toBe('0px');
+ expect(style.right).toBe('0px');
+ expect(style.bottom).toBe('100px');
+
+ open({
+ placement: 'bottom',
+ });
+ expect($$('.ant-notification-bottom').length).toBe(1);
+
// bottomRight
open({
placement: 'bottomRight',
diff --git a/components/notification/demo/placement.md b/components/notification/demo/placement.md
index 7a6b385c8c..73531c20ab 100755
--- a/components/notification/demo/placement.md
+++ b/components/notification/demo/placement.md
@@ -20,6 +20,8 @@ import {
RadiusUprightOutlined,
RadiusBottomleftOutlined,
RadiusBottomrightOutlined,
+ BorderTopOutlined,
+ BorderBottomOutlined,
} from '@ant-design/icons';
const openNotification = placement => {
@@ -33,6 +35,17 @@ const openNotification = placement => {
ReactDOM.render(
+
+ openNotification('top')}>
+
+ top
+
+ openNotification('bottom')}>
+
+ bottom
+
+
+
openNotification('topLeft')}>
diff --git a/components/notification/index.tsx b/components/notification/index.tsx
index 0749758d4f..620a192405 100755
--- a/components/notification/index.tsx
+++ b/components/notification/index.tsx
@@ -10,7 +10,13 @@ import InfoCircleOutlined from '@ant-design/icons/InfoCircleOutlined';
import createUseNotification from './hooks/useNotification';
import ConfigProvider, { globalConfig } from '../config-provider';
-export type NotificationPlacement = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
+export type NotificationPlacement =
+ | 'top'
+ | 'topLeft'
+ | 'topRight'
+ | 'bottom'
+ | 'bottomLeft'
+ | 'bottomRight';
export type IconType = 'success' | 'info' | 'error' | 'warning';
@@ -79,6 +85,14 @@ function getPlacementStyle(
) {
let style;
switch (placement) {
+ case 'top':
+ style = {
+ left: 0,
+ right: 0,
+ top,
+ bottom: 'auto',
+ };
+ break;
case 'topLeft':
style = {
left: 0,
@@ -93,6 +107,14 @@ function getPlacementStyle(
bottom: 'auto',
};
break;
+ case 'bottom':
+ style = {
+ left: 0,
+ right: 0,
+ top: 'auto',
+ bottom,
+ };
+ break;
case 'bottomLeft':
style = {
left: 0,
diff --git a/components/notification/index.zh-CN.md b/components/notification/index.zh-CN.md
index f69e48bf7f..e387d8b2b4 100644
--- a/components/notification/index.zh-CN.md
+++ b/components/notification/index.zh-CN.md
@@ -71,7 +71,7 @@ notification.config({
| closeIcon | 自定义关闭图标 | ReactNode | - | |
| duration | 默认自动关闭延时,单位秒 | number | 4.5 | |
| getContainer | 配置渲染节点的输出位置 | () => HTMLNode | () => document.body | |
-| placement | 弹出位置,可选 `topLeft` `topRight` `bottomLeft` `bottomRight` | string | `topRight` | |
+| placement | 弹出位置,可选 `top` `topLeft` `topRight` `bottom` `bottomLeft` `bottomRight` | string | `topRight` | |
| rtl | 是否开启 RTL 模式 | boolean | false | |
| top | 消息从顶部弹出时,距离顶部的位置,单位像素 | number | 24 | |
| maxCount | 最大显示数, 超过限制时,最早的消息会被自动关闭 | number | - | 4.17.0 |
diff --git a/components/notification/style/index.less b/components/notification/style/index.less
index b85b73f55f..fd457e392a 100644
--- a/components/notification/style/index.less
+++ b/components/notification/style/index.less
@@ -16,6 +16,8 @@
z-index: @zindex-notification;
margin-right: @notification-margin-edge;
+ &-top,
+ &-bottom,
&-topLeft,
&-bottomLeft {
margin-right: 0;
@@ -27,6 +29,12 @@
}
}
+ &-top,
+ &-bottom {
+ margin-right: auto;
+ margin-left: auto;
+ }
+
&-close-icon {
font-size: @font-size-base;
cursor: pointer;
@@ -50,6 +58,12 @@
border-radius: @border-radius-base;
box-shadow: @shadow-2;
+ .@{notification-prefix-cls}-top &,
+ .@{notification-prefix-cls}-bottom & {
+ margin-right: auto;
+ margin-left: auto;
+ }
+
.@{notification-prefix-cls}-topLeft &,
.@{notification-prefix-cls}-bottomLeft & {
margin-right: auto;
diff --git a/components/popover/style/index.less b/components/popover/style/index.less
index 16c71fa266..6cc7c294be 100644
--- a/components/popover/style/index.less
+++ b/components/popover/style/index.less
@@ -137,6 +137,7 @@
background-color: @popover-bg;
content: '';
pointer-events: auto;
+ .roundedArrow(@popover-arrow-width, 5px, @popover-bg);
}
}
@@ -170,8 +171,8 @@
left: @popover-distance - @popover-arrow-rotate-width;
&-content {
- box-shadow: -3px 3px 7px fade(@black, 7%);
- transform: translateX((@popover-arrow-rotate-width / 2)) rotate(45deg);
+ box-shadow: 3px 3px 7px fade(@black, 7%);
+ transform: translateX((@popover-arrow-rotate-width / 2)) rotate(135deg);
}
}
@@ -194,8 +195,8 @@
top: @popover-distance - @popover-arrow-rotate-width;
&-content {
- box-shadow: -2px -2px 5px fade(@black, 6%);
- transform: translateY((@popover-arrow-rotate-width / 2)) rotate(45deg);
+ box-shadow: 2px 2px 5px fade(@black, 6%);
+ transform: translateY((@popover-arrow-rotate-width / 2)) rotate(-135deg);
}
}
@@ -218,8 +219,8 @@
right: @popover-distance - @popover-arrow-rotate-width;
&-content {
- box-shadow: 3px -3px 7px fade(@black, 7%);
- transform: translateX((-@popover-arrow-rotate-width / 2)) rotate(45deg);
+ box-shadow: 3px 3px 7px fade(@black, 7%);
+ transform: translateX((-@popover-arrow-rotate-width / 2)) rotate(-45deg);
}
}
diff --git a/components/select/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/select/__tests__/__snapshots__/demo-extend.test.ts.snap
index b2dd668aef..466c0ddbb7 100644
--- a/components/select/__tests__/__snapshots__/demo-extend.test.ts.snap
+++ b/components/select/__tests__/__snapshots__/demo-extend.test.ts.snap
@@ -4154,6 +4154,253 @@ exports[`renders ./components/select/demo/option-label-prop.md extend context co
`;
+exports[`renders ./components/select/demo/placement.md extend context correctly 1`] = `
+Array [
+
+
+
+
+
+
+
+ topLeft
+
+
+
+
+
+
+
+
+ topRight
+
+
+
+
+
+
+
+
+ bottomLeft
+
+
+
+
+
+
+
+
+ bottomRight
+
+
+
,
+
,
+
,
+
+
+
+
+
+
+ HangZhou #310000
+
+
+
+
+
+
+
+ HangZhou
+
+
+ NingBo
+
+
+
+
+
+
+
+
+ HangZhou #310000
+
+
+
+
+
+
+ WenZhou #325000
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
,
+]
+`;
+
exports[`renders ./components/select/demo/responsive.md extend context correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
exports[`renders ./components/select/demo/suffix.md extend context correctly 1`] = `
Array [
`;
+exports[`renders ./components/select/demo/placement.md correctly 1`] = `
+Array [
+
+
+
+
+
+
+
+ topLeft
+
+
+
+
+
+
+
+
+ topRight
+
+
+
+
+
+
+
+
+ bottomLeft
+
+
+
+
+
+
+
+
+ bottomRight
+
+
+
,
+
,
+
,
+
+
+
+
+
+
+ HangZhou #310000
+
+
+
+
+
+
+
+
+
+
,
+]
+`;
+
exports[`renders ./components/select/demo/responsive.md correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
exports[`renders ./components/select/demo/suffix.md correctly 1`] = `
Array [
{
+ const [placement, SetPlacement] = React.useState('topLeft');
+
+ const placementChange = e => {
+ SetPlacement(e.target.value);
+ };
+
+ return (
+ <>
+
+ topLeft
+ topRight
+ bottomLeft
+ bottomRight
+
+
+
+
+ HangZhou #310000
+ NingBo #315000
+ WenZhou #325000
+
+ >
+ );
+};
+
+ReactDOM.render( , mountNode);
+```
diff --git a/components/select/demo/status.md b/components/select/demo/status.md
new file mode 100644
index 0000000000..cb3ba5e6d1
--- /dev/null
+++ b/components/select/demo/status.md
@@ -0,0 +1,34 @@
+---
+order: 25
+version: 4.19.0
+title:
+ zh-CN: 自定义状态
+ en-US: Status
+---
+
+## zh-CN
+
+使用 `status` 为 Select 添加状态,可选 `error` 或者 `warning`。
+
+## en-US
+
+Add status to Select with `status`, which could be `error` or `warning`.
+
+```tsx
+import { Select, Space } from 'antd';
+
+const Status: React.FC = () => (
+
+
+
+
+);
+
+ReactDOM.render( , mountNode);
+```
+
+```css
+#components-select-demo-status .ant-select {
+ margin: 0;
+}
+```
diff --git a/components/select/index.en-US.md b/components/select/index.en-US.md
index fc96445800..90c686ed49 100644
--- a/components/select/index.en-US.md
+++ b/components/select/index.en-US.md
@@ -37,7 +37,7 @@ Select component to select value from options.
| dropdownMatchSelectWidth | Determine whether the dropdown menu and the select input are the same width. Default set `min-width` same as input. Will ignore when value less than select width. `false` will disable virtual scroll | boolean \| number | true | |
| dropdownRender | Customize dropdown content | (originNode: ReactNode) => ReactNode | - | |
| dropdownStyle | The style of dropdown menu | CSSProperties | - | |
-| fieldNames | Customize node label, value, options field name | object | { label: `label`, value: `value`, options: `options` } | 4.17.0 |
+| fieldNames | Customize node title, key, options field name | object | { label: `label`, key: `key`, options: `options` } | 4.17.0 |
| filterOption | If true, filter options by input, if function, filter options against it. The function will receive two arguments, `inputValue` and `option`, if the function returns `true`, the option will be included in the filtered set; Otherwise, it will be excluded | boolean \| function(inputValue, option) | true | |
| filterSort | Sort function for search options sorting, see [Array.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)'s compareFunction | (optionA: Option, optionB: Option) => number | - | 4.9.0 |
| getPopupContainer | Parent Node which the selector should be rendered to. Default to `body`. When position issues happen, try to modify it into scrollable content and position it relative. [Example](https://codesandbox.io/s/4j168r7jw0) | function(triggerNode) | () => document.body | |
@@ -55,11 +55,13 @@ Select component to select value from options.
| optionLabelProp | Which prop value of option will render as content of select. [Example](https://codesandbox.io/s/antd-reproduction-template-tk678) | string | `children` | |
| options | Select options. Will get better perf than jsx definition | { label, value }\[] | - | |
| placeholder | Placeholder of select | ReactNode | - | |
+| placement | The position where the selection box pops up | `bottomLeft` `bottomRight` `topLeft` `topRight` | bottomLeft | |
| removeIcon | The custom remove icon | ReactNode | - | |
| searchValue | The current input "search" text | string | - | |
| showArrow | Whether to show the drop-down arrow | boolean | true(for single select), false(for multiple select) | |
| showSearch | Whether show search input in single mode | boolean | false | |
| size | Size of Select input | `large` \| `middle` \| `small` | `middle` | |
+| status | Set validation status | 'error' \| 'warning' | - | 4.19.0 |
| suffixIcon | The custom suffix icon | ReactNode | - | |
| tagRender | Customize tag render, only applies when `mode` is set to `multiple` or `tags` | (props) => ReactNode | - | |
| tokenSeparators | Separator used to tokenize on `tag` and `multiple` mode | string\[] | - | |
diff --git a/components/select/index.tsx b/components/select/index.tsx
index 0346795b1e..6c3dce851c 100755
--- a/components/select/index.tsx
+++ b/components/select/index.tsx
@@ -6,10 +6,13 @@ import classNames from 'classnames';
import RcSelect, { Option, OptGroup, SelectProps as RcSelectProps, BaseSelectRef } from 'rc-select';
import type { BaseOptionType, DefaultOptionType } from 'rc-select/lib/Select';
import { OptionProps } from 'rc-select/lib/Option';
+import { useContext } from 'react';
import { ConfigContext } from '../config-provider';
import getIcons from './utils/iconUtil';
import SizeContext, { SizeType } from '../config-provider/SizeContext';
-import { getTransitionName } from '../_util/motion';
+import { FormItemStatusContext } from '../form/context';
+import { getMergedStatus, getStatusClassNames, InputStatus } from '../_util/statusUtils';
+import { getTransitionName, getTransitionDirection, SelectCommonPlacement } from '../_util/motion';
type RawValue = string | number;
@@ -38,9 +41,11 @@ export interface SelectProps<
OptionType extends BaseOptionType | DefaultOptionType = DefaultOptionType,
> extends Omit<
InternalSelectProps,
- 'inputIcon' | 'mode' | 'getInputElement' | 'getRawInputElement' | 'backfill'
+ 'inputIcon' | 'mode' | 'getInputElement' | 'getRawInputElement' | 'backfill' | 'placement'
> {
+ placement?: SelectCommonPlacement;
mode?: 'multiple' | 'tags';
+ status?: InputStatus;
}
const SECRET_COMBOBOX_MODE_DO_NOT_USE = 'SECRET_COMBOBOX_MODE_DO_NOT_USE';
@@ -53,9 +58,12 @@ const InternalSelect = ,
ref: React.Ref,
@@ -88,6 +96,12 @@ const InternalSelect = {
+ if (placement !== undefined) {
+ return placement;
+ }
+ return direction === 'rtl'
+ ? ('bottomRight' as SelectCommonPlacement)
+ : ('bottomLeft' as SelectCommonPlacement);
+ };
+
return (
ref={ref as any}
virtual={virtual}
dropdownMatchSelectWidth={dropdownMatchSelectWidth}
{...selectProps}
- transitionName={getTransitionName(rootPrefixCls, 'slide-up', props.transitionName)}
+ transitionName={getTransitionName(
+ rootPrefixCls,
+ getTransitionDirection(placement),
+ props.transitionName,
+ )}
listHeight={listHeight}
listItemHeight={listItemHeight}
mode={mode as any}
prefixCls={prefixCls}
+ placement={getPlacement()}
direction={direction}
inputIcon={suffixIcon}
menuItemSelectedIcon={itemIcon}
@@ -143,6 +176,7 @@ const InternalSelect =
);
};
diff --git a/components/select/index.zh-CN.md b/components/select/index.zh-CN.md
index 49f0b3f62b..280c7c5fd6 100644
--- a/components/select/index.zh-CN.md
+++ b/components/select/index.zh-CN.md
@@ -38,7 +38,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/_0XzgOis7/Select.svg
| dropdownMatchSelectWidth | 下拉菜单和选择器同宽。默认将设置 `min-width`,当值小于选择框宽度时会被忽略。false 时会关闭虚拟滚动 | boolean \| number | true | |
| dropdownRender | 自定义下拉框内容 | (originNode: ReactNode) => ReactNode | - | |
| dropdownStyle | 下拉菜单的 style 属性 | CSSProperties | - | |
-| fieldNames | 自定义节点 label、value、options 的字段 | object | { label: `label`, value: `value`, options: `options` } | 4.17.0 |
+| fieldNames | 自定义节点 label、key、options 的字段 | object | { label: `label`, key: `key`, options: `options` } | 4.17.0 |
| filterOption | 是否根据输入项进行筛选。当其为一个函数时,会接收 `inputValue` `option` 两个参数,当 `option` 符合筛选条件时,应返回 true,反之则返回 false | boolean \| function(inputValue, option) | true | |
| filterSort | 搜索时对筛选结果项的排序函数, 类似[Array.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)里的 compareFunction | (optionA: Option, optionB: Option) => number | - | 4.9.0 |
| getPopupContainer | 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。[示例](https://codesandbox.io/s/4j168r7jw0) | function(triggerNode) | () => document.body | |
@@ -56,11 +56,13 @@ cover: https://gw.alipayobjects.com/zos/alicdn/_0XzgOis7/Select.svg
| optionLabelProp | 回填到选择框的 Option 的属性值,默认是 Option 的子元素。比如在子元素需要高亮效果时,此值可以设为 `value`。[示例](https://codesandbox.io/s/antd-reproduction-template-tk678) | string | `children` | |
| options | 数据化配置选项内容,相比 jsx 定义会获得更好的渲染性能 | { label, value }\[] | - | |
| placeholder | 选择框默认文本 | string | - | |
+| placement | 选择框弹出的位置 | `bottomLeft` `bottomRight` `topLeft` `topRight` | bottomLeft | |
| removeIcon | 自定义的多选框清除图标 | ReactNode | - | |
| searchValue | 控制搜索文本 | string | - | |
| showArrow | 是否显示下拉小箭头 | boolean | 单选为 true,多选为 false | |
| showSearch | 使单选模式可搜索 | boolean | false | |
| size | 选择框大小 | `large` \| `middle` \| `small` | `middle` | |
+| status | 设置校验状态 | 'error' \| 'warning' | - | 4.19.0 |
| suffixIcon | 自定义的选择框后缀图标 | ReactNode | - | |
| tagRender | 自定义 tag 内容 render,仅在 `mode` 为 `multiple` 或 `tags` 时生效 | (props) => ReactNode | - | |
| tokenSeparators | 在 `tags` 和 `multiple` 模式下自动分词的分隔符 | string\[] | - | |
diff --git a/components/select/style/index.less b/components/select/style/index.less
index c4007cdeff..a63f49ca05 100644
--- a/components/select/style/index.less
+++ b/components/select/style/index.less
@@ -3,6 +3,7 @@
@import '../../input/style/mixin';
@import './single';
@import './multiple';
+@import './status';
@select-prefix-cls: ~'@{ant-prefix}-select';
@select-height-without-border: @input-height-base - 2 * @border-width-base;
@@ -120,7 +121,8 @@
position: absolute;
top: 50%;
right: @control-padding-horizontal - 1px;
- width: @font-size-sm;
+ display: flex;
+ align-items: center;
height: @font-size-sm;
margin-top: (-@font-size-sm / 2);
color: @disabled-color;
diff --git a/components/select/style/index.tsx b/components/select/style/index.tsx
index a914d0b4bd..98037eeccf 100644
--- a/components/select/style/index.tsx
+++ b/components/select/style/index.tsx
@@ -3,3 +3,5 @@ import './index.less';
// style dependencies
import '../../empty/style';
+
+// deps-lint-skip: form
diff --git a/components/select/style/status.less b/components/select/style/status.less
new file mode 100644
index 0000000000..cf528508d6
--- /dev/null
+++ b/components/select/style/status.less
@@ -0,0 +1,80 @@
+@import '../../input/style/mixin';
+
+@select-prefix-cls: ~'@{ant-prefix}-select';
+
+.select-status-color(
+ @text-color;
+ @border-color;
+ @background-color;
+ @hoverBorderColor;
+ @outlineColor;
+) {
+ &.@{select-prefix-cls}:not(.@{select-prefix-cls}-disabled):not(.@{select-prefix-cls}-customize-input) {
+ .@{select-prefix-cls}-selector {
+ background-color: @background-color;
+ border-color: @border-color !important;
+ }
+ &.@{select-prefix-cls}-open .@{select-prefix-cls}-selector,
+ &.@{select-prefix-cls}-focused .@{select-prefix-cls}-selector {
+ .active(@border-color, @hoverBorderColor, @outlineColor);
+ }
+ }
+
+ .@{select-prefix-cls}-feedback-icon {
+ color: @text-color;
+ }
+}
+
+.select-status-base(@prefix-cls) {
+ .@{prefix-cls} {
+ &-status-error {
+ .select-status-color(@error-color, @error-color, @select-background, @error-color-hover, @error-color-outline);
+ }
+
+ &-status-warning {
+ .select-status-color(@warning-color, @warning-color, @input-bg, @warning-color-hover, @warning-color-outline);
+ }
+
+ &-status-success {
+ .@{prefix-cls}-feedback-icon {
+ color: @success-color;
+ }
+ }
+
+ &-status-validating {
+ .@{prefix-cls}-feedback-icon {
+ color: @primary-color;
+ }
+ }
+
+ &-status-error,
+ &-status-warning,
+ &-status-success,
+ &-status-validating {
+ &.@{prefix-cls}-has-feedback {
+ //.@{prefix-cls}-arrow,
+ .@{prefix-cls}-clear {
+ right: 32px;
+ }
+
+ .@{prefix-cls}-selection-selected-value {
+ padding-right: 42px;
+ }
+ }
+ }
+
+ &-feedback-icon {
+ font-size: @font-size-base;
+ text-align: center;
+ visibility: visible;
+ animation: zoomIn 0.3s @ease-out-back;
+ pointer-events: none;
+
+ &:not(:first-child) {
+ margin-left: 8px;
+ }
+ }
+ }
+}
+
+.select-status-base(@select-prefix-cls);
diff --git a/components/select/utils/iconUtil.tsx b/components/select/utils/iconUtil.tsx
index b0ca65fe85..e6e8a7c094 100644
--- a/components/select/utils/iconUtil.tsx
+++ b/components/select/utils/iconUtil.tsx
@@ -1,10 +1,13 @@
import * as React from 'react';
+import { ReactNode } from 'react';
import DownOutlined from '@ant-design/icons/DownOutlined';
import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
import CheckOutlined from '@ant-design/icons/CheckOutlined';
import CloseOutlined from '@ant-design/icons/CloseOutlined';
import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
import SearchOutlined from '@ant-design/icons/SearchOutlined';
+import { ValidateStatus } from '../../form/FormItem';
+import { getFeedbackIcon } from '../../_util/statusUtils';
export default function getIcons({
suffixIcon,
@@ -13,7 +16,10 @@ export default function getIcons({
removeIcon,
loading,
multiple,
+ hasFeedback,
+ status,
prefixCls,
+ showArrow,
}: {
suffixIcon?: React.ReactNode;
clearIcon?: React.ReactNode;
@@ -21,7 +27,10 @@ export default function getIcons({
removeIcon?: React.ReactNode;
loading?: boolean;
multiple?: boolean;
+ hasFeedback?: boolean;
+ status?: ValidateStatus;
prefixCls: string;
+ showArrow?: boolean;
}) {
// Clear Icon
let mergedClearIcon = clearIcon;
@@ -29,19 +38,27 @@ export default function getIcons({
mergedClearIcon = ;
}
+ // Validation Feedback Icon
+ const getSuffixIconNode = (arrowIcon?: ReactNode) => (
+ <>
+ {showArrow !== false && arrowIcon}
+ {hasFeedback && getFeedbackIcon(prefixCls, status)}
+ >
+ );
+
// Arrow item icon
let mergedSuffixIcon = null;
if (suffixIcon !== undefined) {
- mergedSuffixIcon = suffixIcon;
+ mergedSuffixIcon = getSuffixIconNode(suffixIcon);
} else if (loading) {
- mergedSuffixIcon = ;
+ mergedSuffixIcon = getSuffixIconNode( );
} else {
const iconCls = `${prefixCls}-suffix`;
mergedSuffixIcon = ({ open, showSearch }: { open: boolean; showSearch: boolean }) => {
if (open && showSearch) {
- return ;
+ return getSuffixIconNode( );
}
- return ;
+ return getSuffixIconNode( );
};
}
diff --git a/components/skeleton/Input.tsx b/components/skeleton/Input.tsx
index 4287615227..1cb5e31f85 100644
--- a/components/skeleton/Input.tsx
+++ b/components/skeleton/Input.tsx
@@ -6,11 +6,12 @@ import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
export interface SkeletonInputProps extends Omit {
size?: 'large' | 'small' | 'default';
+ block?: boolean;
}
const SkeletonInput = (props: SkeletonInputProps) => {
const renderSkeletonInput = ({ getPrefixCls }: ConfigConsumerProps) => {
- const { prefixCls: customizePrefixCls, className, active } = props;
+ const { prefixCls: customizePrefixCls, className, active, block } = props;
const prefixCls = getPrefixCls('skeleton', customizePrefixCls);
const otherProps = omit(props, ['prefixCls']);
const cls = classNames(
@@ -18,6 +19,7 @@ const SkeletonInput = (props: SkeletonInputProps) => {
`${prefixCls}-element`,
{
[`${prefixCls}-active`]: active,
+ [`${prefixCls}-block`]: block,
},
className,
);
diff --git a/components/skeleton/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/skeleton/__tests__/__snapshots__/demo-extend.test.ts.snap
index 259207bae4..5761cff88c 100644
--- a/components/skeleton/__tests__/__snapshots__/demo-extend.test.ts.snap
+++ b/components/skeleton/__tests__/__snapshots__/demo-extend.test.ts.snap
@@ -138,7 +138,6 @@ Array [
>
@@ -154,6 +153,15 @@ Array [
,
,
,
+
+
+
,
+
,
+
,
@@ -227,9 +235,9 @@ Array [
>
- Button Block
+ Button and Input Block
@@ -154,6 +153,15 @@ Array [
,
,
,
+
+
+
,
+
,
+
,
@@ -227,9 +235,9 @@ Array [
>
- Button Block
+ Button and Input Block
-
+
+
+
+
-
+
diff --git a/components/skeleton/style/index.less b/components/skeleton/style/index.less
index 226d84aadb..c4ca85b58f 100644
--- a/components/skeleton/style/index.less
+++ b/components/skeleton/style/index.less
@@ -109,13 +109,17 @@
}
}
- // Skeleton Block Button
+ // Skeleton Block Button, Input
&.@{skeleton-prefix-cls}-block {
width: 100%;
.@{skeleton-button-prefix-cls} {
width: 100%;
}
+
+ .@{skeleton-input-prefix-cls} {
+ width: 100%;
+ }
}
// Skeleton element
@@ -238,7 +242,8 @@
}
.skeleton-element-input-size(@size) {
- width: 100%;
+ width: @size * 5;
+ min-width: @size * 5;
.skeleton-element-common-size(@size);
}
diff --git a/components/slider/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/slider/__tests__/__snapshots__/demo-extend.test.ts.snap
index a2fb486332..43f642d410 100644
--- a/components/slider/__tests__/__snapshots__/demo-extend.test.ts.snap
+++ b/components/slider/__tests__/__snapshots__/demo-extend.test.ts.snap
@@ -3,14 +3,14 @@
exports[`renders ./components/slider/demo/basic.md extend context correctly 1`] = `
Array [
@@ -49,19 +49,16 @@ Array [
-
,
@@ -107,7 +104,7 @@ Array [
aria-valuenow="50"
class="ant-slider-handle ant-slider-handle-2"
role="slider"
- style="left:50%;right:auto;transform:translateX(-50%)"
+ style="left:50%;transform:translateX(-50%)"
tabindex="0"
/>
@@ -134,9 +131,6 @@ Array [
-
,
"Disabled: ",
@@ -210,7 +204,7 @@ exports[`renders ./components/slider/demo/dragableTrack.md extend context correc
aria-valuenow="50"
class="ant-slider-handle ant-slider-handle-2"
role="slider"
- style="left:50%;right:auto;transform:translateX(-50%)"
+ style="left:50%;transform:translateX(-50%)"
tabindex="0"
/>
@@ -237,23 +231,20 @@ exports[`renders ./components/slider/demo/dragableTrack.md extend context correc
-
`;
exports[`renders ./components/slider/demo/event.md extend context correctly 1`] = `
Array [
@@ -292,19 +283,16 @@ Array [
-
,
@@ -350,7 +338,7 @@ Array [
aria-valuenow="50"
class="ant-slider-handle ant-slider-handle-2"
role="slider"
- style="left:50%;right:auto;transform:translateX(-50%)"
+ style="left:50%;transform:translateX(-50%)"
tabindex="0"
/>
@@ -377,9 +365,6 @@ Array [
-
,
]
`;
@@ -408,14 +393,14 @@ exports[`renders ./components/slider/demo/icon-slider.md extend context correctl
@@ -454,9 +439,6 @@ exports[`renders ./components/slider/demo/icon-slider.md extend context correctl
-
@@ -535,9 +517,6 @@ exports[`renders ./components/slider/demo/input-number.md extend context correct
-
@@ -675,9 +654,6 @@ exports[`renders ./components/slider/demo/input-number.md extend context correct
-
,
@@ -839,25 +815,25 @@ Array [
>
0°C
26°C
37°C
100°C
@@ -866,33 +842,33 @@ Array [
,
@@ -936,7 +912,7 @@ Array [
aria-valuenow="37"
class="ant-slider-handle ant-slider-handle-2"
role="slider"
- style="left:37%;right:auto;transform:translateX(-50%)"
+ style="left:37%;transform:translateX(-50%)"
tabindex="0"
/>
@@ -968,25 +944,25 @@ Array [
>
0°C
26°C
37°C
100°C
@@ -998,7 +974,7 @@ Array [
included=false
,
-
+
@@ -1062,25 +1038,25 @@ Array [
>
0°C
26°C
37°C
100°C
@@ -1092,33 +1068,33 @@ Array [
marks & step
,
@@ -1160,25 +1136,25 @@ Array [
>
0°C
26°C
37°C
100°C
@@ -1190,33 +1166,33 @@ Array [
step=null
,
@@ -1258,25 +1234,25 @@ Array [
>
0°C
26°C
37°C
100°C
@@ -1290,14 +1266,14 @@ Array [
exports[`renders ./components/slider/demo/reverse.md extend context correctly 1`] = `
Array [
@@ -1336,19 +1312,16 @@ Array [
-
,
@@ -1394,7 +1367,7 @@ Array [
aria-valuenow="50"
class="ant-slider-handle ant-slider-handle-2"
role="slider"
- style="right:50%;left:auto;transform:translateX(+50%)"
+ style="right:50%;transform:translateX(50%)"
tabindex="0"
/>
@@ -1421,9 +1394,6 @@ Array [
-
,
"Reversed: ",
@@ -1490,23 +1460,20 @@ exports[`renders ./components/slider/demo/show-tooltip.md extend context correct
-
`;
exports[`renders ./components/slider/demo/tip-formatter.md extend context correctly 1`] = `
Array [
@@ -1545,19 +1512,16 @@ Array [
-
,
@@ -1594,9 +1558,6 @@ Array [
-
,
]
`;
@@ -1614,7 +1575,7 @@ Array [
/>
@@ -1653,9 +1614,6 @@ Array [
-
,
@@ -1715,7 +1673,7 @@ Array [
aria-valuenow="50"
class="ant-slider-handle ant-slider-handle-2"
role="slider"
- style="bottom:50%;top:auto;transform:translateY(+50%)"
+ style="bottom:50%;transform:translateY(50%)"
tabindex="0"
/>
@@ -1742,42 +1700,39 @@ Array [
-
,
@@ -1821,7 +1776,7 @@ Array [
aria-valuenow="37"
class="ant-slider-handle ant-slider-handle-2"
role="slider"
- style="bottom:37%;top:auto;transform:translateY(+50%)"
+ style="bottom:37%;transform:translateY(50%)"
tabindex="0"
/>
@@ -1853,25 +1808,25 @@ Array [
>
0°C
26°C
37°C
100°C
diff --git a/components/slider/__tests__/__snapshots__/demo.test.js.snap b/components/slider/__tests__/__snapshots__/demo.test.js.snap
index 549e10ee4f..bed03c73da 100644
--- a/components/slider/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/slider/__tests__/__snapshots__/demo.test.js.snap
@@ -3,14 +3,14 @@
exports[`renders ./components/slider/demo/basic.md correctly 1`] = `
Array [
,
,
"Disabled: ",
-
`;
exports[`renders ./components/slider/demo/event.md correctly 1`] = `
Array [
,
,
]
`;
@@ -216,14 +201,14 @@ exports[`renders ./components/slider/demo/icon-slider.md correctly 1`] = `
,
0°C
26°C
37°C
100°C
@@ -578,33 +554,33 @@ Array [
,
0°C
26°C
37°C
100°C
@@ -662,7 +638,7 @@ Array [
included=false
,
-
+
0°C
26°C
37°C
100°C
@@ -732,33 +708,33 @@ Array [
marks & step
,
0°C
26°C
37°C
100°C
@@ -806,33 +782,33 @@ Array [
step=null
,
0°C
26°C
37°C
100°C
@@ -882,14 +858,14 @@ Array [
exports[`renders ./components/slider/demo/reverse.md correctly 1`] = `
Array [
,
,
"Reversed: ",
-
,
,
]
`;
@@ -1088,7 +1052,7 @@ Array [
/>
-
,
-
,
0°C
26°C
37°C
100°C
diff --git a/components/slider/__tests__/__snapshots__/index.test.js.snap b/components/slider/__tests__/__snapshots__/index.test.js.snap
index c86a5e63ad..8b7c1eb1e7 100644
--- a/components/slider/__tests__/__snapshots__/index.test.js.snap
+++ b/components/slider/__tests__/__snapshots__/index.test.js.snap
@@ -2,7 +2,7 @@
exports[`Slider rtl render component should be rendered correctly in RTL direction 1`] = `
`;
exports[`Slider should render in RTL direction 1`] = `
@@ -78,9 +75,6 @@ exports[`Slider should render in RTL direction 1`] = `
-
`;
diff --git a/components/slider/__tests__/index.test.js b/components/slider/__tests__/index.test.js
index 4b965e4d38..050d006aec 100644
--- a/components/slider/__tests__/index.test.js
+++ b/components/slider/__tests__/index.test.js
@@ -11,7 +11,7 @@ import { sleep } from '../../../tests/utils';
describe('Slider', () => {
mountTest(Slider);
rtlTest(Slider);
- focusTest(Slider, { refFocus: true });
+ focusTest(Slider, { testLib: true });
it('should show tooltip when hovering slider handler', () => {
const wrapper = mount(
);
@@ -51,7 +51,7 @@ describe('Slider', () => {
const wrapper = mount(
,
);
- expect(wrapper.find('.ant-slider-handle').get(0).props).toHaveProperty('value', 48);
+ expect(wrapper.find('.ant-slider-handle').get(0).props).toHaveProperty('aria-valuenow', 48);
});
it('when step is not null, thumb can be slided to the multiples of step', () => {
@@ -62,7 +62,7 @@ describe('Slider', () => {
};
const wrapper = mount(
);
- expect(wrapper.find('.ant-slider-handle').get(0).props).toHaveProperty('value', 49);
+ expect(wrapper.find('.ant-slider-handle').get(0).props).toHaveProperty('aria-valuenow', 49);
});
it('when step is undefined, thumb can be slided to the multiples of step', () => {
@@ -75,7 +75,7 @@ describe('Slider', () => {
const wrapper = mount(
,
);
- expect(wrapper.find('.ant-slider-handle').get(0).props).toHaveProperty('value', 49);
+ expect(wrapper.find('.ant-slider-handle').get(0).props).toHaveProperty('aria-valuenow', 49);
});
it('should render in RTL direction', () => {
diff --git a/components/slider/index.tsx b/components/slider/index.tsx
index a2817e7f04..907354552d 100644
--- a/components/slider/index.tsx
+++ b/components/slider/index.tsx
@@ -1,18 +1,11 @@
import * as React from 'react';
-import RcSlider, { Range as RcRange, Handle as RcHandle } from 'rc-slider';
+import RcSlider, { SliderProps as RcSliderProps } from 'rc-slider';
import classNames from 'classnames';
import { TooltipPlacement } from '../tooltip';
import SliderTooltip from './SliderTooltip';
import { ConfigContext } from '../config-provider';
-export interface SliderMarks {
- [key: number]:
- | React.ReactNode
- | {
- style: React.CSSProperties;
- label: React.ReactNode;
- };
-}
+export type SliderMarks = RcSliderProps['marks'];
interface HandleGeneratorInfo {
value?: number;
@@ -93,43 +86,6 @@ const Slider = React.forwardRef
(
return direction === 'rtl' ? 'left' : 'right';
};
- const handleWithTooltip: HandleGeneratorFn = ({
- tooltipPrefixCls,
- prefixCls,
- info: { value, dragging, index, ...restProps },
- }) => {
- const {
- tipFormatter,
- tooltipVisible,
- tooltipPlacement,
- getTooltipPopupContainer,
- vertical,
- } = props;
- const isTipFormatter = tipFormatter ? visibles[index] || dragging : false;
- const visible = tooltipVisible || (tooltipVisible === undefined && isTipFormatter);
- const rootPrefixCls = getPrefixCls();
-
- return (
-
- toggleTooltipVisible(index, true)}
- onMouseLeave={() => toggleTooltipVisible(index, false)}
- />
-
- );
- };
-
const {
prefixCls: customizePrefixCls,
tooltipPrefixCls: customizeTooltipPrefixCls,
@@ -148,45 +104,57 @@ const Slider = React.forwardRef(
restProps.reverse = !restProps.reverse;
}
- // extrack draggableTrack from range={{ ... }}
- let draggableTrack: boolean | undefined;
- if (typeof range === 'object') {
- draggableTrack = range.draggableTrack;
- }
+ // Range config
+ const [mergedRange, draggableTrack] = React.useMemo(() => {
+ if (!range) {
+ return [false];
+ }
+
+ return typeof range === 'object' ? [true, range.draggableTrack] : [true, false];
+ }, [range]);
+
+ const handleRender: RcSliderProps['handleRender'] = (node, info) => {
+ const { index, dragging } = info;
+
+ const rootPrefixCls = getPrefixCls();
+ const { tipFormatter, tooltipVisible, tooltipPlacement, getTooltipPopupContainer, vertical } =
+ props;
+
+ const isTipFormatter = tipFormatter ? visibles[index] || dragging : false;
+ const visible = tooltipVisible || (tooltipVisible === undefined && isTipFormatter);
+
+ const passedProps = {
+ ...node.props,
+ onMouseEnter: () => toggleTooltipVisible(index, true),
+ onMouseLeave: () => toggleTooltipVisible(index, false),
+ };
- if (range) {
return (
-
- handleWithTooltip({
- tooltipPrefixCls,
- prefixCls,
- info,
- })
- }
- prefixCls={prefixCls}
- />
+
+ {React.cloneElement(node, passedProps)}
+
);
- }
+ };
+
return (
- handleWithTooltip({
- tooltipPrefixCls,
- prefixCls,
- info,
- })
- }
prefixCls={prefixCls}
+ handleRender={handleRender}
/>
);
},
diff --git a/components/slider/style/index.less b/components/slider/style/index.less
index d5967bb08f..98b7685e0d 100644
--- a/components/slider/style/index.less
+++ b/components/slider/style/index.less
@@ -49,9 +49,12 @@
transition: border-color 0.3s, box-shadow 0.6s,
transform 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28);
- &-dragging&-dragging&-dragging {
- border-color: @slider-handle-color-focus;
- box-shadow: 0 0 0 5px @slider-handle-color-focus-shadow;
+ // &-dragging&-dragging&-dragging {
+ // border-color: @slider-handle-color-focus;
+ // box-shadow: 0 0 0 5px @slider-handle-color-focus-shadow;
+ // }
+ &-dragging {
+ z-index: 1;
}
&:focus {
@@ -104,6 +107,7 @@
width: 100%;
height: 4px;
background: transparent;
+ pointer-events: none;
}
&-dot {
@@ -111,20 +115,11 @@
top: -2px;
width: 8px;
height: 8px;
- margin-left: -4px;
background-color: @component-background;
border: 2px solid @slider-dot-border-color;
border-radius: 50%;
cursor: pointer;
- &:first-child {
- margin-left: -4px;
- }
-
- &:last-child {
- margin-left: -4px;
- }
-
&-active {
border-color: @slider-dot-border-color-active;
}
@@ -196,8 +191,7 @@
.@{slider-prefix-cls}-dot {
top: auto;
- left: 2px;
- margin-bottom: -4px;
+ margin-left: -2px;
}
}
diff --git a/components/slider/style/rtl.less b/components/slider/style/rtl.less
index 7dde8a9087..d78a54a8e0 100644
--- a/components/slider/style/rtl.less
+++ b/components/slider/style/rtl.less
@@ -14,27 +14,6 @@
left: auto;
}
}
-
- &-dot {
- .@{slider-prefix-cls}-rtl & {
- margin-right: -4px;
- margin-left: 0;
- }
-
- &:first-child {
- .@{slider-prefix-cls}-rtl & {
- margin-right: -4px;
- margin-left: 0;
- }
- }
-
- &:last-child {
- .@{slider-prefix-cls}-rtl & {
- margin-right: -4px;
- margin-left: 0;
- }
- }
- }
}
.vertical() {
diff --git a/components/space/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/space/__tests__/__snapshots__/demo-extend.test.ts.snap
index e4907f8a9e..12f9fe8a3a 100644
--- a/components/space/__tests__/__snapshots__/demo-extend.test.ts.snap
+++ b/components/space/__tests__/__snapshots__/demo-extend.test.ts.snap
@@ -321,14 +321,14 @@ exports[`renders ./components/space/demo/base.md extend context correctly 1`] =
exports[`renders ./components/space/demo/customize.md extend context correctly 1`] = `
Array [
@@ -367,9 +367,6 @@ Array [
-
,
,
,
diff --git a/components/space/__tests__/__snapshots__/demo.test.js.snap b/components/space/__tests__/__snapshots__/demo.test.js.snap
index 9a26d2b2cb..aef6fb490b 100644
--- a/components/space/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/space/__tests__/__snapshots__/demo.test.js.snap
@@ -246,14 +246,14 @@ exports[`renders ./components/space/demo/base.md correctly 1`] = `
exports[`renders ./components/space/demo/customize.md correctly 1`] = `
Array [
,
,
,
diff --git a/components/space/index.tsx b/components/space/index.tsx
index 6255cb4410..9c0b7f5e4f 100644
--- a/components/space/index.tsx
+++ b/components/space/index.tsx
@@ -88,11 +88,12 @@ const Space: React.FC = props => {
latestIndex = i;
}
- /* eslint-disable react/no-array-index-key */
+ const keyOfChild = child && child.key;
+
return (
- = props => {
{child}
);
- /* eslint-enable */
});
const spaceContext = React.useMemo(
diff --git a/components/style/mixins/index.less b/components/style/mixins/index.less
index 921eb91470..1da375ec4c 100644
--- a/components/style/mixins/index.less
+++ b/components/style/mixins/index.less
@@ -11,3 +11,4 @@
@import 'motion';
@import 'reset';
@import 'operation-unit';
+@import 'rounded-arrow';
diff --git a/components/style/mixins/rounded-arrow.less b/components/style/mixins/rounded-arrow.less
new file mode 100644
index 0000000000..bba5e16459
--- /dev/null
+++ b/components/style/mixins/rounded-arrow.less
@@ -0,0 +1,37 @@
+.roundedArrow(@width, @outer-radius, @bg-color: var(--antd-arrow-background-color)) {
+ @corner-height: unit(((@outer-radius) * (1 - 1 / sqrt(2))));
+
+ @width-without-unit: unit(@width);
+ @outer-radius-without-unit: unit(@outer-radius);
+ @inner-radius-without-unit: unit(@border-radius-base);
+
+ @a-x: @width-without-unit - @corner-height;
+ @a-y: 2 * @width-without-unit + @corner-height;
+ @b-x: @a-x + @outer-radius-without-unit * (1 / sqrt(2));
+ @b-y: 2 * @width-without-unit;
+ @c-x: 2 * @width-without-unit - @inner-radius-without-unit;
+ @c-y: 2 * @width-without-unit;
+ @d-x: 2 * @width-without-unit;
+ @d-y: 2 * @width-without-unit - @inner-radius-without-unit;
+ @e-x: 2 * @width-without-unit;
+ @e-y: @f-y + @outer-radius-without-unit * (1 / sqrt(2));
+ @f-x: 2 * @width-without-unit + @corner-height;
+ @f-y: @width-without-unit - @corner-height;
+
+ border-radius: 0 0 @border-radius-base 0;
+ pointer-events: none;
+
+ &::before {
+ position: absolute;
+ top: -@width;
+ left: -@width;
+ width: @width * 3;
+ height: @width * 3;
+ background: linear-gradient(to left, @bg-color 50%, @bg-color 50%) no-repeat ceil(-@width + 1px)
+ ceil(-@width + 1px); // Hack firefox: https://github.com/ant-design/ant-design/pull/33710#issuecomment-1015287825
+ content: '';
+ clip-path: path(
+ 'M @{a-x} @{a-y} A @{outer-radius-without-unit} @{outer-radius-without-unit} 0 0 1 @{b-x} @{b-y} L @{c-x} @{c-y} A @{inner-radius-without-unit} @{inner-radius-without-unit} 0 0 0 @{d-x} @{d-y} L @{e-x} @{e-y} A @{outer-radius-without-unit} @{outer-radius-without-unit} 0 0 1 @{f-x} @{f-y} Z'
+ );
+ }
+}
diff --git a/components/style/themes/variable.less b/components/style/themes/variable.less
index 0779d6577b..6f0e6c3ad4 100644
--- a/components/style/themes/variable.less
+++ b/components/style/themes/variable.less
@@ -221,7 +221,7 @@ html {
// Border color
@border-color-base: hsv(0, 0, 85%); // base border outline a component
-@border-color-split: hsv(0, 0, 94%); // split border inside a component
+@border-color-split: rgba(0, 0, 0, 0.06); // split border inside a component
@border-color-inverse: @white;
@border-width-base: 1px; // width of the border for a component
@border-style-base: solid; // style of a components border
@@ -571,7 +571,7 @@ html {
// Tooltip background color
@tooltip-bg: rgba(0, 0, 0, 0.75);
// Tooltip arrow width
-@tooltip-arrow-width: 5px;
+@tooltip-arrow-width: 8px * sqrt(2);
// Tooltip distance with trigger
@tooltip-distance: @tooltip-arrow-width - 1px + 4px;
// Tooltip arrow color
@@ -587,7 +587,7 @@ html {
@popover-min-width: 177px;
@popover-min-height: 32px;
// Popover arrow width
-@popover-arrow-width: 6px;
+@popover-arrow-width: @tooltip-arrow-width;
// Popover arrow color
@popover-arrow-color: @popover-bg;
// Popover outer arrow width
@@ -626,7 +626,7 @@ html {
// Progress
// --
@progress-default-color: @processing-color;
-@progress-remaining-color: @background-color-base;
+@progress-remaining-color: rgba(0, 0, 0, 0.04);
@progress-info-text-color: @progress-text-color;
@progress-radius: 100px;
@progress-steps-item-bg: #f3f3f3;
diff --git a/components/table/__tests__/Table.filter.test.js b/components/table/__tests__/Table.filter.test.js
index 7f93412439..132f8ffc66 100644
--- a/components/table/__tests__/Table.filter.test.js
+++ b/components/table/__tests__/Table.filter.test.js
@@ -1863,6 +1863,48 @@ describe('Table.filter', () => {
expect(wrapper.find('li.ant-dropdown-menu-item').length).toBe(2);
});
+ it('should supports filterSearch has type of function', () => {
+ jest.useFakeTimers();
+ jest.spyOn(console, 'error').mockImplementation(() => undefined);
+ const wrapper = mount(
+ createTable({
+ columns: [
+ {
+ ...column,
+ filters: [
+ {
+ text: '123',
+ value: '123',
+ },
+ {
+ text: 123456,
+ value: '456',
+ },
+ {
+ text: 123 ,
+ value: '456',
+ },
+ ],
+ filterSearch: (input, record) => record.value.indexOf(input) > -1,
+ },
+ ],
+ }),
+ );
+ wrapper.find('span.ant-dropdown-trigger').simulate('click', nativeEvent);
+ act(() => {
+ jest.runAllTimers();
+ wrapper.update();
+ });
+ expect(wrapper.find(Menu).length).toBe(1);
+ expect(wrapper.find(Input).length).toBe(1);
+ expect(wrapper.find('li.ant-dropdown-menu-item').length).toBe(3);
+ wrapper
+ .find(Input)
+ .find('input')
+ .simulate('change', { target: { value: '456' } });
+ expect(wrapper.find('li.ant-dropdown-menu-item').length).toBe(2);
+ });
+
it('supports check all items', () => {
jest.useFakeTimers();
jest.spyOn(console, 'error').mockImplementation(() => undefined);
@@ -2052,4 +2094,71 @@ describe('Table.filter', () => {
wrapper.find('.ant-table-filter-dropdown-btns .ant-btn-primary').simulate('click');
expect(renderedNames(wrapper)).toEqual(['Jack']);
});
+
+ it('clearFilters should support params', () => {
+ const filterConfig = [
+ ['Jack', 'NoParams', {}, ['Jack'], true],
+ ['Lucy', 'Confirm', { confirm: true }, ['Jack', 'Lucy', 'Tom', 'Jerry'], true],
+ ['Tom', 'Close', { closeDropdown: true }, ['Tom'], false],
+ [
+ 'Jerry',
+ 'Params',
+ { closeDropdown: true, confirm: true },
+ ['Jack', 'Lucy', 'Tom', 'Jerry'],
+ false,
+ ],
+ ];
+ const filter = ({ prefixCls, setSelectedKeys, confirm, clearFilters }) => (
+
+ {filterConfig.map(([text, id, param]) => (
+ <>
+ {
+ setSelectedKeys([text]);
+ confirm();
+ }}
+ id={`set${id}`}
+ >
+ setSelectedKeys
+
+ clearFilters(param)} id={`reset${id}`}>
+ Reset
+
+ >
+ ))}
+
+ );
+
+ const wrapper = mount(
+ createTable({
+ columns: [
+ {
+ ...column,
+ filterDropdown: filter,
+ },
+ ],
+ }),
+ );
+
+ function getFilterMenu() {
+ return wrapper.find('FilterDropdown');
+ }
+
+ // check if renderer well
+ wrapper.find('span.ant-dropdown-trigger').simulate('click', nativeEvent);
+ expect(wrapper.find('#customFilter')).toMatchSnapshot();
+ expect(getFilterMenu().props().filterState.filteredKeys).toBeFalsy();
+
+ filterConfig.forEach(([text, id, , res1, res2]) => {
+ wrapper.find(`#set${id}`).simulate('click');
+ wrapper.update();
+ expect(renderedNames(wrapper)).toEqual([text]);
+
+ wrapper.find('span.ant-dropdown-trigger').simulate('click', nativeEvent);
+ wrapper.find(`#reset${id}`).simulate('click');
+ wrapper.update();
+ expect(renderedNames(wrapper)).toEqual(res1);
+ expect(wrapper.find('Dropdown').first().props().visible).toBe(res2);
+ });
+ });
});
diff --git a/components/table/__tests__/Table.sorter.test.js b/components/table/__tests__/Table.sorter.test.js
index a25b3cdf55..eaf742f0b2 100644
--- a/components/table/__tests__/Table.sorter.test.js
+++ b/components/table/__tests__/Table.sorter.test.js
@@ -68,19 +68,55 @@ describe('Table.sorter', () => {
),
);
+ const getNameColumn = () => wrapper.find('th').at(0);
+
expect(renderedNames(wrapper)).toEqual(['Tom', 'Lucy', 'Jack', 'Jerry']);
+ expect(getNameColumn().prop('aria-sort')).toEqual('descending');
+ });
+
+ it('should change aria-sort when default sort order is set to descend', () => {
+ const wrapper = mount(
+ createTable(
+ {
+ sortDirections: ['descend', 'ascend'],
+ },
+ {
+ defaultSortOrder: 'descend',
+ },
+ ),
+ );
+
+ const getNameColumn = () => wrapper.find('th').at(0);
+
+ // Test that it cycles through the order of sortDirections
+ expect(renderedNames(wrapper)).toEqual(['Tom', 'Lucy', 'Jack', 'Jerry']);
+ expect(getNameColumn().prop('aria-sort')).toEqual('descending');
+
+ wrapper.find('.ant-table-column-sorters').simulate('click');
+ expect(getNameColumn().prop('aria-sort')).toEqual('ascending');
+
+ wrapper.find('.ant-table-column-sorters').simulate('click');
+ expect(getNameColumn().prop('aria-sort')).toEqual(undefined);
});
it('sort records', () => {
const wrapper = mount(createTable());
+ const getNameColumn = () => wrapper.find('th').at(0);
+
+ // first assert default state
+ expect(renderedNames(wrapper)).toEqual(['Jack', 'Lucy', 'Tom', 'Jerry']);
+ expect(getNameColumn().prop('aria-sort')).toEqual(undefined);
+
// ascend
wrapper.find('.ant-table-column-sorters').simulate('click');
expect(renderedNames(wrapper)).toEqual(['Jack', 'Jerry', 'Lucy', 'Tom']);
+ expect(getNameColumn().prop('aria-sort')).toEqual('ascending');
// descend
wrapper.find('.ant-table-column-sorters').simulate('click');
expect(renderedNames(wrapper)).toEqual(['Tom', 'Lucy', 'Jack', 'Jerry']);
+ expect(getNameColumn().prop('aria-sort')).toEqual('descending');
});
describe('can be controlled by sortOrder', () => {
@@ -333,12 +369,16 @@ describe('Table.sorter', () => {
// sort name
getNameColumn().simulate('click');
expect(getNameIcon('up').hasClass('active')).toBeTruthy();
+ expect(getNameColumn().prop('aria-sort')).toEqual('ascending');
expect(getAgeIcon('up').hasClass('active')).toBeFalsy();
+ expect(getAgeColumn().prop('aria-sort')).toEqual(undefined);
// sort age
getAgeColumn().simulate('click');
expect(getNameIcon('up').hasClass('active')).toBeFalsy();
+ expect(getNameColumn().prop('aria-sort')).toEqual(undefined);
expect(getAgeIcon('up').hasClass('active')).toBeTruthy();
+ expect(getAgeColumn().prop('aria-sort')).toEqual('ascending');
});
// https://github.com/ant-design/ant-design/issues/12571
@@ -380,7 +420,7 @@ describe('Table.sorter', () => {
const wrapper = mount( );
- const getNameColumn = () => wrapper.find('.ant-table-column-has-sorters').at(0);
+ const getNameColumn = () => wrapper.find('th').at(0);
const getIcon = name => getNameColumn().find(`.ant-table-column-sorter-${name}`).first();
expect(getIcon('up').hasClass('active')).toBeFalsy();
@@ -390,16 +430,19 @@ describe('Table.sorter', () => {
getNameColumn().simulate('click');
expect(getIcon('up').hasClass('active')).toBeTruthy();
expect(getIcon('down').hasClass('active')).toBeFalsy();
+ expect(getNameColumn().prop('aria-sort')).toEqual('ascending');
// sort name
getNameColumn().simulate('click');
expect(getIcon('up').hasClass('active')).toBeFalsy();
expect(getIcon('down').hasClass('active')).toBeTruthy();
+ expect(getNameColumn().prop('aria-sort')).toEqual('descending');
// sort name
getNameColumn().simulate('click');
expect(getIcon('up').hasClass('active')).toBeFalsy();
expect(getIcon('down').hasClass('active')).toBeFalsy();
+ expect(getNameColumn().prop('aria-sort')).toEqual(undefined);
});
// https://github.com/ant-design/ant-design/issues/12737
@@ -444,7 +487,7 @@ describe('Table.sorter', () => {
const wrapper = mount( );
- const getNameColumn = () => wrapper.find('.ant-table-column-has-sorters').at(0);
+ const getNameColumn = () => wrapper.find('th').at(0);
const getIcon = name => getNameColumn().find(`.ant-table-column-sorter-${name}`).first();
expect(getIcon('up').hasClass('active')).toBeFalsy();
@@ -454,16 +497,19 @@ describe('Table.sorter', () => {
getNameColumn().simulate('click');
expect(getIcon('up').hasClass('active')).toBeTruthy();
expect(getIcon('down').hasClass('active')).toBeFalsy();
+ expect(getNameColumn().prop('aria-sort')).toEqual('ascending');
// sort name
getNameColumn().simulate('click');
expect(getIcon('up').hasClass('active')).toBeFalsy();
expect(getIcon('down').hasClass('active')).toBeTruthy();
+ expect(getNameColumn().prop('aria-sort')).toEqual('descending');
// sort name
getNameColumn().simulate('click');
expect(getIcon('up').hasClass('active')).toBeFalsy();
expect(getIcon('down').hasClass('active')).toBeFalsy();
+ expect(getNameColumn().prop('aria-sort')).toEqual(undefined);
});
// https://github.com/ant-design/ant-design/issues/12870
@@ -508,13 +554,14 @@ describe('Table.sorter', () => {
}
const wrapper = mount( );
- const getNameColumn = () => wrapper.find('.ant-table-column-has-sorters').at(0);
+ const getNameColumn = () => wrapper.find('th').at(0);
expect(
getNameColumn().find('.ant-table-column-sorter-up').at(0).hasClass('active'),
).toBeFalsy();
expect(
getNameColumn().find('.ant-table-column-sorter-down').at(0).hasClass('active'),
).toBeFalsy();
+ expect(getNameColumn().prop('aria-sort')).toEqual(undefined);
// sort name
getNameColumn().simulate('click');
@@ -524,6 +571,7 @@ describe('Table.sorter', () => {
expect(
getNameColumn().find('.ant-table-column-sorter-down').at(0).hasClass('active'),
).toBeFalsy();
+ expect(getNameColumn().prop('aria-sort')).toEqual('ascending');
// sort name
getNameColumn().simulate('click');
@@ -533,6 +581,7 @@ describe('Table.sorter', () => {
expect(
getNameColumn().find('.ant-table-column-sorter-down').at(0).hasClass('active'),
).toBeTruthy();
+ expect(getNameColumn().prop('aria-sort')).toEqual('descending');
// sort name
getNameColumn().simulate('click');
@@ -542,6 +591,7 @@ describe('Table.sorter', () => {
expect(
getNameColumn().find('.ant-table-column-sorter-down').at(0).hasClass('active'),
).toBeFalsy();
+ expect(getNameColumn().prop('aria-sort')).toEqual(undefined);
});
it('should first sort by descend, then ascend, then cancel sort', () => {
@@ -550,18 +600,22 @@ describe('Table.sorter', () => {
sortDirections: ['descend', 'ascend'],
}),
);
+ const getNameColumn = () => wrapper.find('th').at(0);
// descend
- wrapper.find('.ant-table-column-sorters').simulate('click');
+ getNameColumn().simulate('click');
expect(renderedNames(wrapper)).toEqual(['Tom', 'Lucy', 'Jack', 'Jerry']);
+ expect(getNameColumn().prop('aria-sort')).toEqual('descending');
// ascend
- wrapper.find('.ant-table-column-sorters').simulate('click');
+ getNameColumn().simulate('click');
expect(renderedNames(wrapper)).toEqual(['Jack', 'Jerry', 'Lucy', 'Tom']);
+ expect(getNameColumn().prop('aria-sort')).toEqual('ascending');
// cancel sort
- wrapper.find('.ant-table-column-sorters').simulate('click');
+ getNameColumn().simulate('click');
expect(renderedNames(wrapper)).toEqual(['Jack', 'Lucy', 'Tom', 'Jerry']);
+ expect(getNameColumn().prop('aria-sort')).toEqual(undefined);
});
it('should first sort by descend, then cancel sort', () => {
@@ -571,13 +625,20 @@ describe('Table.sorter', () => {
}),
);
+ const getNameColumn = () => wrapper.find('th').at(0);
+
+ // default
+ expect(getNameColumn().prop('aria-sort')).toEqual(undefined);
+
// descend
- wrapper.find('.ant-table-column-sorters').simulate('click');
+ getNameColumn().simulate('click');
expect(renderedNames(wrapper)).toEqual(['Tom', 'Lucy', 'Jack', 'Jerry']);
+ expect(getNameColumn().prop('aria-sort')).toEqual('descending');
// cancel sort
- wrapper.find('.ant-table-column-sorters').simulate('click');
+ getNameColumn().simulate('click');
expect(renderedNames(wrapper)).toEqual(['Jack', 'Lucy', 'Tom', 'Jerry']);
+ expect(getNameColumn().prop('aria-sort')).toEqual(undefined);
});
it('should first sort by descend, then cancel sort. (column prop)', () => {
@@ -590,13 +651,20 @@ describe('Table.sorter', () => {
),
);
+ const getNameColumn = () => wrapper.find('th').at(0);
+
+ // default
+ expect(getNameColumn().prop('aria-sort')).toEqual(undefined);
+
// descend
- wrapper.find('.ant-table-column-sorters').simulate('click');
+ getNameColumn().simulate('click');
expect(renderedNames(wrapper)).toEqual(['Tom', 'Lucy', 'Jack', 'Jerry']);
+ expect(getNameColumn().prop('aria-sort')).toEqual('descending');
// cancel sort
- wrapper.find('.ant-table-column-sorters').simulate('click');
+ getNameColumn().simulate('click');
expect(renderedNames(wrapper)).toEqual(['Jack', 'Lucy', 'Tom', 'Jerry']);
+ expect(getNameColumn().prop('aria-sort')).toEqual(undefined);
});
it('pagination back', () => {
@@ -614,9 +682,14 @@ describe('Table.sorter', () => {
}),
);
- wrapper.find('.ant-table-column-sorters').simulate('click');
+ const getNameColumn = () => wrapper.find('th').at(0);
+
+ expect(getNameColumn().prop('aria-sort')).toEqual(undefined);
+
+ getNameColumn().simulate('click');
expect(onChange.mock.calls[0][0].current).toBe(2);
expect(onPageChange).not.toHaveBeenCalled();
+ expect(getNameColumn().prop('aria-sort')).toEqual('ascending');
});
it('should support onHeaderCell in sort column', () => {
diff --git a/components/table/__tests__/__snapshots__/Table.filter.test.js.snap b/components/table/__tests__/__snapshots__/Table.filter.test.js.snap
index c01f5d3957..2b3bac0583 100644
--- a/components/table/__tests__/__snapshots__/Table.filter.test.js.snap
+++ b/components/table/__tests__/__snapshots__/Table.filter.test.js.snap
@@ -1,5 +1,61 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`Table.filter clearFilters should support params 1`] = `
+
+
+ setSelectedKeys
+
+
+ Reset
+
+
+ setSelectedKeys
+
+
+ Reset
+
+
+ setSelectedKeys
+
+
+ Reset
+
+
+ setSelectedKeys
+
+
+ Reset
+
+
+`;
+
exports[`Table.filter override custom filter correctly 1`] = `
`;
+exports[`renders ./components/table/demo/filter-search.md extend context correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Select all items
+
+
+
+
+
+
+
+ Reset
+
+
+
+
+ OK
+
+
+
+
+
+
+
+
+
+
+
+ Age
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Address
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reset
+
+
+
+
+ OK
+
+
+
+
+
+
+
+
+
+
+
+
+
+ John Brown
+
+
+ 32
+
+
+ New York No. 1 Lake Park
+
+
+
+
+ Jim Green
+
+
+ 42
+
+
+ London No. 1 Lake Park
+
+
+
+
+ Joe Black
+
+
+ 32
+
+
+ Sidney No. 1 Lake Park
+
+
+
+
+ Jim Red
+
+
+ 32
+
+
+ London No. 2 Lake Park
+
+
+
+
+
+
+
+
+
+
+
+`;
+
exports[`renders ./components/table/demo/fixed-columns.md extend context correctly 1`] = `
`;
+exports[`renders ./components/table/demo/filter-search.md correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Name
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Age
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Address
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ John Brown
+
+
+ 32
+
+
+ New York No. 1 Lake Park
+
+
+
+
+ Jim Green
+
+
+ 42
+
+
+ London No. 1 Lake Park
+
+
+
+
+ Joe Black
+
+
+ 32
+
+
+ Sidney No. 1 Lake Park
+
+
+
+
+ Jim Red
+
+
+ 32
+
+
+ London No. 2 Lake Park
+
+
+
+
+
+
+
+
+
+
+
+`;
+
exports[`renders ./components/table/demo/fixed-columns.md correctly 1`] = `
| null>(null);
@@ -61,7 +61,7 @@ const EditableCell: React.FC
= ({
...restProps
}) => {
const [editing, setEditing] = useState(false);
- const inputRef = useRef (null);
+ const inputRef = useRef(null);
const form = useContext(EditableContext)!;
useEffect(() => {
diff --git a/components/table/demo/filter-in-tree.md b/components/table/demo/filter-in-tree.md
index bfc03c36e8..351607e9db 100644
--- a/components/table/demo/filter-in-tree.md
+++ b/components/table/demo/filter-in-tree.md
@@ -10,8 +10,6 @@ title:
可以使用 `filterMode` 来修改筛选菜单的 UI,可选值有 `menu`(默认)和 `tree`。
-> `filterSearch` 用于开启筛选项的搜索。
-
## en-US
You can use `filterMode` to change default filter interface, options: `menu`(default) and `tree`.
diff --git a/components/table/demo/filter-search.md b/components/table/demo/filter-search.md
new file mode 100644
index 0000000000..e144c554c3
--- /dev/null
+++ b/components/table/demo/filter-search.md
@@ -0,0 +1,99 @@
+---
+order: 6.2
+version: 4.19.0
+title:
+ en-US: Filter search
+ zh-CN: 自定义筛选的搜索
+---
+
+## zh-CN
+
+`filterSearch` 用于开启筛选项的搜索,通过 `filterSearch:(input, record) => boolean` 设置自定义筛选方法
+
+## en-US
+
+`filterSearch` is used to enable search of filter items, and you can set a custom filter method through `filterSearch:(input, record) => boolean`.
+
+```jsx
+import { Table } from 'antd';
+
+const columns = [
+ {
+ title: 'Name',
+ dataIndex: 'name',
+ filters: [
+ {
+ text: 'Joe',
+ value: 'Joe',
+ },
+ {
+ text: 'Category 1',
+ value: 'Category 1',
+ },
+ {
+ text: 'Category 2',
+ value: 'Category 2',
+ },
+ ],
+ filterMode: 'tree',
+ filterSearch: true,
+ onFilter: (value, record) => record.address.startsWith(value),
+ width: '30%',
+ },
+ {
+ title: 'Age',
+ dataIndex: 'age',
+ sorter: (a, b) => a.age - b.age,
+ },
+ {
+ title: 'Address',
+ dataIndex: 'address',
+ filters: [
+ {
+ text: London ,
+ value: 'London',
+ },
+ {
+ text: New York ,
+ value: 'New York',
+ },
+ ],
+ onFilter: (value, record) => record.address.startsWith(value),
+ filterSearch:(input, record) => record.value.indexOf(input) > -1,
+ width: '40%',
+ },
+];
+
+const data = [
+ {
+ key: '1',
+ name: 'John Brown',
+ age: 32,
+ address: 'New York No. 1 Lake Park',
+ },
+ {
+ key: '2',
+ name: 'Jim Green',
+ age: 42,
+ address: 'London No. 1 Lake Park',
+ },
+ {
+ key: '3',
+ name: 'Joe Black',
+ age: 32,
+ address: 'Sidney No. 1 Lake Park',
+ },
+ {
+ key: '4',
+ name: 'Jim Red',
+ age: 32,
+ address: 'London No. 2 Lake Park',
+ },
+];
+
+function onChange(pagination, filters, sorter, extra) {
+ console.log('params', pagination, filters, sorter, extra);
+}
+
+ReactDOM.render(, mountNode);
+```
diff --git a/components/table/hooks/useFilter/FilterDropdown.tsx b/components/table/hooks/useFilter/FilterDropdown.tsx
index da8272d893..ac0b5393d1 100644
--- a/components/table/hooks/useFilter/FilterDropdown.tsx
+++ b/components/table/hooks/useFilter/FilterDropdown.tsx
@@ -11,13 +11,25 @@ import type { CheckboxChangeEvent } from '../../../checkbox';
import Radio from '../../../radio';
import Dropdown from '../../../dropdown';
import Empty from '../../../empty';
-import { ColumnType, ColumnFilterItem, Key, TableLocale, GetPopupContainer } from '../../interface';
+import {
+ ColumnType,
+ ColumnFilterItem,
+ Key,
+ TableLocale,
+ GetPopupContainer,
+ FilterSearchType,
+} from '../../interface';
import FilterDropdownMenuWrapper from './FilterWrapper';
import FilterSearch from './FilterSearch';
import { FilterState, flattenKeys } from '.';
import useSyncState from '../../../_util/hooks/useSyncState';
import { ConfigContext } from '../../../config-provider/context';
+interface FilterRestProps {
+ confirm?: Boolean;
+ closeDropdown?: Boolean;
+}
+
function hasSubMenu(filters: ColumnFilterItem[]) {
return filters.some(({ children }) => children);
}
@@ -35,12 +47,14 @@ function renderFilterItems({
filteredKeys,
filterMultiple,
searchValue,
+ filterSearch,
}: {
filters: ColumnFilterItem[];
prefixCls: string;
filteredKeys: Key[];
filterMultiple: boolean;
searchValue: string;
+ filterSearch: FilterSearchType;
}) {
return filters.map((filter, index) => {
const key = String(filter.value);
@@ -58,6 +72,7 @@ function renderFilterItems({
filteredKeys,
filterMultiple,
searchValue,
+ filterSearch,
})}
);
@@ -72,6 +87,9 @@ function renderFilterItems({
);
if (searchValue.trim()) {
+ if (typeof filterSearch === 'function') {
+ return filterSearch(searchValue, filter) ? item : undefined;
+ }
return searchValueMatched(searchValue, filter.text) ? item : undefined;
}
return item;
@@ -86,7 +104,7 @@ export interface FilterDropdownProps {
filterState?: FilterState;
filterMultiple: boolean;
filterMode?: 'menu' | 'tree';
- filterSearch?: boolean;
+ filterSearch?: FilterSearchType;
columnKey: Key;
children: React.ReactNode;
triggerFilter: (filterState: FilterState) => void;
@@ -203,7 +221,15 @@ function FilterDropdown(props: FilterDropdownProps) {
internalTriggerFilter(getFilteredKeysSync());
};
- const onReset = () => {
+ const onReset = (
+ { confirm, closeDropdown }: FilterRestProps = { confirm: false, closeDropdown: false },
+ ) => {
+ if (confirm) {
+ internalTriggerFilter([]);
+ }
+ if (closeDropdown) {
+ triggerVisible(false);
+ }
setSearchValue('');
setFilteredKeysSync([]);
};
@@ -353,6 +379,7 @@ function FilterDropdown(props: FilterDropdownProps) {
>
{renderFilterItems({
filters: column.filters || [],
+ filterSearch,
prefixCls,
filteredKeys: getFilteredKeysSync(),
filterMultiple,
@@ -367,7 +394,12 @@ function FilterDropdown(props: FilterDropdownProps) {
<>
{getFilterComponent()}
-
+ onReset()}
+ >
{locale.filterReset}
diff --git a/components/table/hooks/useFilter/FilterSearch.tsx b/components/table/hooks/useFilter/FilterSearch.tsx
index ebd39a0ccc..fb1dacdc4e 100644
--- a/components/table/hooks/useFilter/FilterSearch.tsx
+++ b/components/table/hooks/useFilter/FilterSearch.tsx
@@ -1,12 +1,12 @@
import * as React from 'react';
import SearchOutlined from '@ant-design/icons/SearchOutlined';
import Input from '../../../input';
-import { TableLocale } from '../../interface';
+import { TableLocale, FilterSearchType } from '../../interface';
interface FilterSearchProps {
value: string;
onChange: (e: React.ChangeEvent) => void;
- filterSearch: Boolean;
+ filterSearch: FilterSearchType;
tablePrefixCls: string;
locale: TableLocale;
}
diff --git a/components/table/hooks/useSorter.tsx b/components/table/hooks/useSorter.tsx
index c85e94af61..7758c530b6 100644
--- a/components/table/hooks/useSorter.tsx
+++ b/components/table/hooks/useSorter.tsx
@@ -192,6 +192,15 @@ function injectSorter(
}
};
+ // Inform the screen-reader so it can tell the visually impaired user which column is sorted
+ if (sorterOrder) {
+ if (sorterOrder === 'ascend') {
+ cell['aria-sort'] = 'ascending';
+ } else {
+ cell['aria-sort'] = 'descending';
+ }
+ }
+
cell.className = classNames(cell.className, `${prefixCls}-column-has-sorters`);
return cell;
diff --git a/components/table/index.en-US.md b/components/table/index.en-US.md
index 197004f72a..c19fce6262 100644
--- a/components/table/index.en-US.md
+++ b/components/table/index.en-US.md
@@ -130,7 +130,7 @@ One of the Table `columns` prop for describing the table's columns, Column has t
| filterIcon | Customized filter icon | ReactNode \| (filtered: boolean) => ReactNode | - | |
| filterMultiple | Whether multiple filters can be selected | boolean | true | |
| filterMode | To specify the filter interface | 'menu' \| 'tree' | 'menu' | 4.17.0 |
-| filterSearch | Whether to be searchable for filter menu | Boolean | false | 4.17.0 |
+| filterSearch | Whether to be searchable for filter menu | boolean \| function(input, record):boolean | false | boolean:4.17.0 function:4.19.0 |
| filters | Filter menu config | object\[] | - | |
| fixed | (IE not support) Set column to be fixed: `true`(same as left) `'left'` `'right'` | boolean \| string | false | |
| key | Unique key of this column, you can ignore this prop if you've set a unique `dataIndex` | string | - | |
diff --git a/components/table/index.zh-CN.md b/components/table/index.zh-CN.md
index cf7ee91007..6db807b71f 100644
--- a/components/table/index.zh-CN.md
+++ b/components/table/index.zh-CN.md
@@ -137,7 +137,7 @@ const columns = [
| filterIcon | 自定义 filter 图标。 | ReactNode \| (filtered: boolean) => ReactNode | false | |
| filterMultiple | 是否多选 | boolean | true | |
| filterMode | 指定筛选菜单的用户界面 | 'menu' \| 'tree' | 'menu' | 4.17.0 |
-| filterSearch | 筛选菜单项是否可搜索 | Boolean | false | 4.17.0 |
+| filterSearch | 筛选菜单项是否可搜索 | boolean \| function(input, record):boolean | false | boolean:4.17.0 function:4.19.0 |
| filters | 表头的筛选菜单项 | object\[] | - | |
| fixed | (IE 下无效)列是否固定,可选 true (等效于 left) `left` `right` | boolean \| string | false | |
| key | React 需要的 key,如果已经设置了唯一的 `dataIndex`,可以忽略这个属性 | string | - | |
diff --git a/components/table/interface.tsx b/components/table/interface.tsx
index a88f85a426..d6477a18f2 100644
--- a/components/table/interface.tsx
+++ b/components/table/interface.tsx
@@ -73,6 +73,7 @@ export type ColumnTitle =
export type FilterValue = (Key | boolean)[];
export type FilterKey = Key[] | null;
+export type FilterSearchType = boolean | ((input: string, record: {}) => boolean);
export interface FilterConfirmProps {
closeDropdown: boolean;
}
diff --git a/components/time-picker/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/time-picker/__tests__/__snapshots__/demo-extend.test.ts.snap
index f238708ee6..7f17ef1b79 100644
--- a/components/time-picker/__tests__/__snapshots__/demo-extend.test.ts.snap
+++ b/components/time-picker/__tests__/__snapshots__/demo-extend.test.ts.snap
@@ -19017,6 +19017,2836 @@ Array [
]
`;
+exports[`renders ./components/time-picker/demo/status.md extend context correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+ 00
+
+
+
+
+ 01
+
+
+
+
+ 02
+
+
+
+
+ 03
+
+
+
+
+ 04
+
+
+
+
+ 05
+
+
+
+
+ 06
+
+
+
+
+ 07
+
+
+
+
+ 08
+
+
+
+
+ 09
+
+
+
+
+ 10
+
+
+
+
+ 11
+
+
+
+
+ 12
+
+
+
+
+ 13
+
+
+
+
+ 14
+
+
+
+
+ 15
+
+
+
+
+ 16
+
+
+
+
+ 17
+
+
+
+
+ 18
+
+
+
+
+ 19
+
+
+
+
+ 20
+
+
+
+
+ 21
+
+
+
+
+ 22
+
+
+
+
+ 23
+
+
+
+
+
+
+ 00
+
+
+
+
+ 01
+
+
+
+
+ 02
+
+
+
+
+ 03
+
+
+
+
+ 04
+
+
+
+
+ 05
+
+
+
+
+ 06
+
+
+
+
+ 07
+
+
+
+
+ 08
+
+
+
+
+ 09
+
+
+
+
+ 10
+
+
+
+
+ 11
+
+
+
+
+ 12
+
+
+
+
+ 13
+
+
+
+
+ 14
+
+
+
+
+ 15
+
+
+
+
+ 16
+
+
+
+
+ 17
+
+
+
+
+ 18
+
+
+
+
+ 19
+
+
+
+
+ 20
+
+
+
+
+ 21
+
+
+
+
+ 22
+
+
+
+
+ 23
+
+
+
+
+ 24
+
+
+
+
+ 25
+
+
+
+
+ 26
+
+
+
+
+ 27
+
+
+
+
+ 28
+
+
+
+
+ 29
+
+
+
+
+ 30
+
+
+
+
+ 31
+
+
+
+
+ 32
+
+
+
+
+ 33
+
+
+
+
+ 34
+
+
+
+
+ 35
+
+
+
+
+ 36
+
+
+
+
+ 37
+
+
+
+
+ 38
+
+
+
+
+ 39
+
+
+
+
+ 40
+
+
+
+
+ 41
+
+
+
+
+ 42
+
+
+
+
+ 43
+
+
+
+
+ 44
+
+
+
+
+ 45
+
+
+
+
+ 46
+
+
+
+
+ 47
+
+
+
+
+ 48
+
+
+
+
+ 49
+
+
+
+
+ 50
+
+
+
+
+ 51
+
+
+
+
+ 52
+
+
+
+
+ 53
+
+
+
+
+ 54
+
+
+
+
+ 55
+
+
+
+
+ 56
+
+
+
+
+ 57
+
+
+
+
+ 58
+
+
+
+
+ 59
+
+
+
+
+
+
+ 00
+
+
+
+
+ 01
+
+
+
+
+ 02
+
+
+
+
+ 03
+
+
+
+
+ 04
+
+
+
+
+ 05
+
+
+
+
+ 06
+
+
+
+
+ 07
+
+
+
+
+ 08
+
+
+
+
+ 09
+
+
+
+
+ 10
+
+
+
+
+ 11
+
+
+
+
+ 12
+
+
+
+
+ 13
+
+
+
+
+ 14
+
+
+
+
+ 15
+
+
+
+
+ 16
+
+
+
+
+ 17
+
+
+
+
+ 18
+
+
+
+
+ 19
+
+
+
+
+ 20
+
+
+
+
+ 21
+
+
+
+
+ 22
+
+
+
+
+ 23
+
+
+
+
+ 24
+
+
+
+
+ 25
+
+
+
+
+ 26
+
+
+
+
+ 27
+
+
+
+
+ 28
+
+
+
+
+ 29
+
+
+
+
+ 30
+
+
+
+
+ 31
+
+
+
+
+ 32
+
+
+
+
+ 33
+
+
+
+
+ 34
+
+
+
+
+ 35
+
+
+
+
+ 36
+
+
+
+
+ 37
+
+
+
+
+ 38
+
+
+
+
+ 39
+
+
+
+
+ 40
+
+
+
+
+ 41
+
+
+
+
+ 42
+
+
+
+
+ 43
+
+
+
+
+ 44
+
+
+
+
+ 45
+
+
+
+
+ 46
+
+
+
+
+ 47
+
+
+
+
+ 48
+
+
+
+
+ 49
+
+
+
+
+ 50
+
+
+
+
+ 51
+
+
+
+
+ 52
+
+
+
+
+ 53
+
+
+
+
+ 54
+
+
+
+
+ 55
+
+
+
+
+ 56
+
+
+
+
+ 57
+
+
+
+
+ 58
+
+
+
+
+ 59
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 00
+
+
+
+
+ 01
+
+
+
+
+ 02
+
+
+
+
+ 03
+
+
+
+
+ 04
+
+
+
+
+ 05
+
+
+
+
+ 06
+
+
+
+
+ 07
+
+
+
+
+ 08
+
+
+
+
+ 09
+
+
+
+
+ 10
+
+
+
+
+ 11
+
+
+
+
+ 12
+
+
+
+
+ 13
+
+
+
+
+ 14
+
+
+
+
+ 15
+
+
+
+
+ 16
+
+
+
+
+ 17
+
+
+
+
+ 18
+
+
+
+
+ 19
+
+
+
+
+ 20
+
+
+
+
+ 21
+
+
+
+
+ 22
+
+
+
+
+ 23
+
+
+
+
+
+
+ 00
+
+
+
+
+ 01
+
+
+
+
+ 02
+
+
+
+
+ 03
+
+
+
+
+ 04
+
+
+
+
+ 05
+
+
+
+
+ 06
+
+
+
+
+ 07
+
+
+
+
+ 08
+
+
+
+
+ 09
+
+
+
+
+ 10
+
+
+
+
+ 11
+
+
+
+
+ 12
+
+
+
+
+ 13
+
+
+
+
+ 14
+
+
+
+
+ 15
+
+
+
+
+ 16
+
+
+
+
+ 17
+
+
+
+
+ 18
+
+
+
+
+ 19
+
+
+
+
+ 20
+
+
+
+
+ 21
+
+
+
+
+ 22
+
+
+
+
+ 23
+
+
+
+
+ 24
+
+
+
+
+ 25
+
+
+
+
+ 26
+
+
+
+
+ 27
+
+
+
+
+ 28
+
+
+
+
+ 29
+
+
+
+
+ 30
+
+
+
+
+ 31
+
+
+
+
+ 32
+
+
+
+
+ 33
+
+
+
+
+ 34
+
+
+
+
+ 35
+
+
+
+
+ 36
+
+
+
+
+ 37
+
+
+
+
+ 38
+
+
+
+
+ 39
+
+
+
+
+ 40
+
+
+
+
+ 41
+
+
+
+
+ 42
+
+
+
+
+ 43
+
+
+
+
+ 44
+
+
+
+
+ 45
+
+
+
+
+ 46
+
+
+
+
+ 47
+
+
+
+
+ 48
+
+
+
+
+ 49
+
+
+
+
+ 50
+
+
+
+
+ 51
+
+
+
+
+ 52
+
+
+
+
+ 53
+
+
+
+
+ 54
+
+
+
+
+ 55
+
+
+
+
+ 56
+
+
+
+
+ 57
+
+
+
+
+ 58
+
+
+
+
+ 59
+
+
+
+
+
+
+ 00
+
+
+
+
+ 01
+
+
+
+
+ 02
+
+
+
+
+ 03
+
+
+
+
+ 04
+
+
+
+
+ 05
+
+
+
+
+ 06
+
+
+
+
+ 07
+
+
+
+
+ 08
+
+
+
+
+ 09
+
+
+
+
+ 10
+
+
+
+
+ 11
+
+
+
+
+ 12
+
+
+
+
+ 13
+
+
+
+
+ 14
+
+
+
+
+ 15
+
+
+
+
+ 16
+
+
+
+
+ 17
+
+
+
+
+ 18
+
+
+
+
+ 19
+
+
+
+
+ 20
+
+
+
+
+ 21
+
+
+
+
+ 22
+
+
+
+
+ 23
+
+
+
+
+ 24
+
+
+
+
+ 25
+
+
+
+
+ 26
+
+
+
+
+ 27
+
+
+
+
+ 28
+
+
+
+
+ 29
+
+
+
+
+ 30
+
+
+
+
+ 31
+
+
+
+
+ 32
+
+
+
+
+ 33
+
+
+
+
+ 34
+
+
+
+
+ 35
+
+
+
+
+ 36
+
+
+
+
+ 37
+
+
+
+
+ 38
+
+
+
+
+ 39
+
+
+
+
+ 40
+
+
+
+
+ 41
+
+
+
+
+ 42
+
+
+
+
+ 43
+
+
+
+
+ 44
+
+
+
+
+ 45
+
+
+
+
+ 46
+
+
+
+
+ 47
+
+
+
+
+ 48
+
+
+
+
+ 49
+
+
+
+
+ 50
+
+
+
+
+ 51
+
+
+
+
+ 52
+
+
+
+
+ 53
+
+
+
+
+ 54
+
+
+
+
+ 55
+
+
+
+
+ 56
+
+
+
+
+ 57
+
+
+
+
+ 58
+
+
+
+
+ 59
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
exports[`renders ./components/time-picker/demo/suffix.md extend context correctly 1`] = `
Array [
+`;
+
exports[`renders ./components/time-picker/demo/suffix.md correctly 1`] = `
(
+
+
+
+
+);
+
+ReactDOM.render( , mountNode);
+```
diff --git a/components/time-picker/index.en-US.md b/components/time-picker/index.en-US.md
index c5190a6f71..a3b8742ed3 100644
--- a/components/time-picker/index.en-US.md
+++ b/components/time-picker/index.en-US.md
@@ -33,9 +33,7 @@ import moment from 'moment';
| clearText | The clear tooltip of icon | string | clear | |
| defaultValue | To set default time | [moment](http://momentjs.com/) | - | |
| disabled | Determine whether the TimePicker is disabled | boolean | false | |
-| disabledHours | To specify the hours that cannot be selected | function() | - | |
-| disabledMinutes | To specify the minutes that cannot be selected | function(selectedHour) | - | |
-| disabledSeconds | To specify the seconds that cannot be selected | function(selectedHour, selectedMinute) | - | |
+| disabledTime | To specify the time that cannot be selected | [DisabledTime](#DisabledTime) | - | 4.19.0 |
| format | To set the time format | string | `HH:mm:ss` | |
| getPopupContainer | To set the container of the floating layer, while the default is to create a div element in body | function(trigger) | - | |
| hideDisabledOptions | Whether hide the options that can not be selected | boolean | false | |
@@ -44,11 +42,13 @@ import moment from 'moment';
| minuteStep | Interval between minutes in picker | number | 1 | |
| open | Whether to popup panel | boolean | false | |
| placeholder | Display when there's no value | string \| \[string, string] | `Select a time` | |
+| placement | The position where the selection box pops up | `bottomLeft` `bottomRight` `topLeft` `topRight` | bottomLeft | |
| popupClassName | The className of panel | string | - | |
| popupStyle | The style of panel | CSSProperties | - | |
| renderExtraFooter | Called from time picker panel to render some addon to its bottom | () => ReactNode | - | |
| secondStep | Interval between seconds in picker | number | 1 | |
| showNow | Whether to show `Now` button on panel | boolean | - | 4.4.0 |
+| status | Set validation status | 'error' \| 'warning' \| 'success' \| 'validating' | - | 4.19.0 |
| suffixIcon | The custom suffix icon | ReactNode | - | |
| use12Hours | Display as 12 hours format, with default format `h:mm:ss a` | boolean | false | |
| value | To set time | [moment](http://momentjs.com/) | - | |
@@ -56,12 +56,22 @@ import moment from 'moment';
| onOpenChange | A callback function which will be called while panel opening/closing | (open: boolean) => void | - | |
| onSelect | A callback function, executes when a value is selected | function(time: moment): void | - | |
+#### DisabledTime
+
+```typescript
+type DisabledTime = (now: Moment) => {
+ disabledHours?: () => number[];
+ disabledMinutes?: (selectedHour: number) => number[];
+ disabledSeconds?: (selectedHour: number, selectedMinute: number) => number[];
+};
+```
+
## Methods
-| Name | Description | Version |
-| --- | --- | --- |
-| blur() | Remove focus | |
-| focus() | Get focus | |
+| Name | Description | Version |
+| ------- | ------------ | ------- |
+| blur() | Remove focus | |
+| focus() | Get focus | |
### RangePicker
@@ -69,8 +79,22 @@ Same props from [RangePicker](/components/date-picker/#RangePicker) of DatePicke
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
+| disabledTime | To specify the time that cannot be selected | [RangeDisabledTime](#RangeDisabledTime) | - | 4.19.0 |
| order | Order start and end time | boolean | true | 4.1.0 |
+### RangeDisabledTime
+
+```typescript
+type RangeDisabledTime = (
+ now: Moment,
+ type = 'start' | 'end',
+) => {
+ disabledHours?: () => number[];
+ disabledMinutes?: (selectedHour: number) => number[];
+ disabledSeconds?: (selectedHour: number, selectedMinute: number) => number[];
+};
+```
+