Merge branch 'feature' into next-merge-feature

This commit is contained in:
MadCcc 2022-10-19 17:46:21 +08:00
commit 5c72d04b10
64 changed files with 17611 additions and 346 deletions

View File

@ -7,7 +7,7 @@ version: 2.1
jobs:
test-argos-ci:
docker:
- image: circleci/node:16-browsers
- image: cimg/node:lts-browsers
steps:
- checkout
- run:
@ -16,9 +16,6 @@ jobs:
- run:
name: Install argos cli
command: npm i fast-glob lodash @argos-ci/core
- run:
name: Install puppeteer
command: node node_modules/puppeteer/install.js
- run:
name: Build dist file
command: npm run dist
@ -28,7 +25,8 @@ jobs:
- run:
name: Upload screenshots to Argos CI
command: npm run argos
# The resource_class feature allows configuring CPU and RAM resources for each job. Different resource classes are available for different executors. https://circleci.com/docs/2.0/configuration-reference/#resourceclass
resource_class: large
# Invoke jobs via workflows
# See: https://circleci.com/docs/2.0/configuration-reference/#workflows

View File

@ -31,7 +31,7 @@ module.exports = {
'@typescript-eslint/no-unused-vars': [2, { args: 'none' }],
'no-unused-expressions': 'off',
'@typescript-eslint/no-unused-expressions': 2,
'@typescript-eslint/consistent-type-imports': 2,
'@typescript-eslint/consistent-type-imports': [2, { disallowTypeAnnotations: false }],
},
},
{

View File

@ -3,7 +3,6 @@ import { easeInOutCubic } from '../easings';
describe('Test easings', () => {
it('easeInOutCubic return value', () => {
const nums: number[] = [];
// eslint-disable-next-line no-plusplus
for (let index = 0; index < 5; index++) {
nums.push(easeInOutCubic(index, 1, 5, 4));
}

View File

@ -2,17 +2,14 @@ import { waitFakeTimer } from '../../../tests/utils';
import scrollTo from '../scrollTo';
describe('Test ScrollTo function', () => {
let dateNowMock: jest.SpyInstance;
const dateNowMock = jest.spyOn(Date, 'now');
beforeAll(() => {
jest.useFakeTimers();
});
beforeEach(() => {
dateNowMock = jest
.spyOn(Date, 'now')
.mockImplementationOnce(() => 0)
.mockImplementationOnce(() => 1000);
dateNowMock.mockReturnValueOnce(0).mockReturnValueOnce(1000);
});
afterAll(() => {
@ -21,7 +18,7 @@ describe('Test ScrollTo function', () => {
afterEach(() => {
jest.clearAllTimers();
dateNowMock.mockRestore();
dateNowMock.mockClear();
});
it('test scrollTo', async () => {

View File

@ -4,7 +4,7 @@ import { render, fireEvent } from '../../../tests/utils';
describe('Table', () => {
it('useSyncState', () => {
const Test: React.FC = () => {
const Test = () => {
const [getVal, setVal] = useSyncState('light');
return <span onClick={() => setVal('bamboo')}>{getVal()}</span>;
};

View File

@ -294,15 +294,19 @@ describe('Wave component', () => {
fakeDoc.appendChild(document.createElement('span'));
expect(fakeDoc.childNodes).toHaveLength(2);
(container.querySelector('.bamboo') as any).getRootNode = () => fakeDoc;
const elem = container.querySelector('.bamboo');
// Click should not throw
fireEvent.click(container.querySelector('.bamboo')!);
act(() => {
jest.runAllTimers();
});
if (elem) {
elem.getRootNode = () => fakeDoc;
expect(fakeDoc.querySelector('style')).toBeTruthy();
// Click should not throw
fireEvent.click(elem);
act(() => {
jest.runAllTimers();
});
expect(fakeDoc.querySelector('style')).toBeTruthy();
}
jest.useRealTimers();
});

View File

@ -29,17 +29,17 @@ const Content = () => {
};
it('Delay loading timer in Button component', () => {
const otherTimer: any = 9528;
jest.spyOn(window, 'setTimeout').mockReturnValue(otherTimer);
const otherTimer = 9528;
jest.spyOn<Window, 'setTimeout'>(window, 'setTimeout').mockReturnValue(otherTimer);
jest.restoreAllMocks();
const wrapper = render(<Content />);
const btnTimer: any = 9527;
jest.spyOn(window, 'setTimeout').mockReturnValue(btnTimer);
jest.spyOn(window, 'clearTimeout');
const setTimeoutMock = window.setTimeout as any as jest.Mock;
const clearTimeoutMock = window.clearTimeout as any as jest.Mock;
const btnTimer = 9527;
const setTimeoutMock = jest
.spyOn<Window, 'setTimeout'>(window, 'setTimeout')
.mockReturnValue(btnTimer);
const clearTimeoutMock = jest.spyOn<Window, 'clearTimeout'>(window, 'clearTimeout');
// other component may call setTimeout or clearTimeout
const setTimeoutCount = () => {
@ -58,7 +58,11 @@ it('Delay loading timer in Button component', () => {
// trigger timer handler
act(() => {
setTimeoutMock.mock.calls[0][0]();
const timerHandler = setTimeoutMock.mock.calls[0][0];
if (typeof timerHandler === 'function') {
timerHandler();
}
});
expect(setTimeoutCount()).toBe(1);
expect(clearTimeoutCount()).toBe(0);

View File

@ -7,7 +7,6 @@ import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, render, sleep } from '../../../tests/utils';
import ConfigProvider from '../../config-provider';
import type { SizeType } from '../../config-provider/SizeContext';
describe('Button', () => {
mountTest(Button);
@ -38,7 +37,8 @@ describe('Button', () => {
it('warns if size is wrong', () => {
resetWarned();
const mockWarn = jest.spyOn(console, 'error').mockImplementation(() => {});
const size = 'who am I' as any as SizeType;
const size = 'who am I';
// @ts-expect-error: Type '"who am I"' is not assignable to type 'SizeType'.ts(2322)
render(<Button.Group size={size} />);
expect(mockWarn).toHaveBeenCalledWith('Warning: [antd: Button.Group] Invalid prop `size`.');

View File

@ -1,18 +1,19 @@
import React from 'react';
import Button from '..';
import { fireEvent, render, sleep } from '../../../tests/utils';
import { fireEvent, render, sleep, assertsExist } from '../../../tests/utils';
// Mock Wave ref
let waveInstanceMock: any;
let waveInstanceMock: InstanceType<typeof import('../../_util/wave').default> | null;
jest.mock('../../_util/wave', () => {
const Wave = jest.requireActual('../../_util/wave');
const Wave: typeof import('../../_util/wave') = jest.requireActual('../../_util/wave');
const WaveComponent = Wave.default;
return {
...Wave,
__esModule: true,
default: (props: any) => (
default: (props: import('../../_util/wave').WaveProps) => (
<WaveComponent
ref={(node: any) => {
ref={node => {
waveInstanceMock = node;
}}
{...props}
@ -77,12 +78,14 @@ describe('click wave effect', () => {
it('should run resetEffect in transitionstart', async () => {
const wrapper = render(<Button type="primary">button</Button>);
assertsExist(waveInstanceMock);
const resetEffect = jest.spyOn(waveInstanceMock, 'resetEffect');
await clickButton(wrapper);
expect(resetEffect).toHaveBeenCalledTimes(1);
fireEvent.click(wrapper.container.querySelector('.ant-btn')!);
await sleep(10);
expect(resetEffect).toHaveBeenCalledTimes(2);
// @ts-expect-error: Property 'animationStart' is private and only accessible within class 'Wave'.ts(2341)
waveInstanceMock.animationStart = false;
fireEvent(wrapper.container.querySelector('.ant-btn')!, new Event('transitionstart'));
expect(resetEffect).toHaveBeenCalledTimes(3);
@ -91,6 +94,7 @@ describe('click wave effect', () => {
it('should handle transitionend', async () => {
const wrapper = render(<Button type="primary">button</Button>);
assertsExist(waveInstanceMock);
const resetEffect = jest.spyOn(waveInstanceMock, 'resetEffect');
await clickButton(wrapper);
expect(resetEffect).toHaveBeenCalledTimes(1);

View File

@ -7,6 +7,7 @@ import { ConfigContext } from '../config-provider';
import DisabledContext from '../config-provider/DisabledContext';
import type { SizeType } from '../config-provider/SizeContext';
import SizeContext from '../config-provider/SizeContext';
import { useCompactItemContext } from '../space/Compact';
import { cloneElement } from '../_util/reactNode';
import { tuple } from '../_util/type';
import warning from '../_util/warning';
@ -178,7 +179,6 @@ const InternalButton: React.ForwardRefRenderFunction<unknown, ButtonProps> = (pr
const [innerLoading, setLoading] = React.useState<Loading>(!!loading);
const [hasTwoCNChar, setHasTwoCNChar] = React.useState(false);
const buttonRef = (ref as any) || React.createRef<HTMLElement>();
const isNeedInserted = () =>
React.Children.count(children) === 1 && !icon && !isUnBorderedButtonType(type);
@ -247,9 +247,10 @@ const InternalButton: React.ForwardRefRenderFunction<unknown, ButtonProps> = (pr
);
const autoInsertSpace = autoInsertSpaceInButton !== false;
const { compactSize, compactItemClassnames } = useCompactItemContext(prefixCls, direction);
const sizeClassNameMap = { large: 'lg', small: 'sm', middle: undefined };
const sizeFullname = groupSize || customizeSize || size;
const sizeFullname = compactSize || groupSize || customizeSize || size;
const sizeCls = sizeFullname ? sizeClassNameMap[sizeFullname] || '' : '';
const iconType = innerLoading ? 'loading' : icon;
@ -272,6 +273,7 @@ const InternalButton: React.ForwardRefRenderFunction<unknown, ButtonProps> = (pr
[`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-disabled`]: linkButtonRestProps.href !== undefined && mergedDisabled,
},
compactItemClassnames,
className,
);

View File

@ -0,0 +1,88 @@
@import '../../style/mixins/index';
@btn-prefix-cls: ~'@{ant-prefix}-btn';
// Button in Space.Compact
.@{btn-prefix-cls} {
.compact-item(@btn-prefix-cls);
// make `btn-icon-only` not too narrow
&-icon-only&-compact-item {
flex: none;
}
// Special styles for Primary Button
&-compact-item.@{btn-prefix-cls}-primary {
&:not([disabled]) + &:not([disabled]) {
position: relative;
&::after {
position: absolute;
top: -@border-width-base;
left: -@border-width-base;
display: inline-block;
width: @border-width-base;
height: calc(100% + @border-width-base * 2);
background-color: @btn-group-border;
content: ' ';
}
}
}
// ----------RTL----------
&-compact-item-rtl {
&.@{btn-prefix-cls}-compact-first-item&:not(.@{btn-prefix-cls}-compact-last-item) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
&.@{btn-prefix-cls}-compact-last-item&:not(.@{btn-prefix-cls}-compact-first-item) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
&.@{btn-prefix-cls}-sm {
&.@{btn-prefix-cls}-compact-first-item&:not(.@{btn-prefix-cls}-compact-last-item) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
&.@{btn-prefix-cls}-compact-last-item&:not(.@{btn-prefix-cls}-compact-first-item) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
// ----------RTL Special styles for Primary Button----------
&.@{btn-prefix-cls}-primary {
&:not([disabled]) + &:not([disabled]) {
&::after {
right: -@border-width-base;
}
}
}
}
// Button in Space.Compact when direction=vertical
.compact-item-vertical(@btn-prefix-cls);
// Special styles for Primary Button
&-compact-vertical-item {
&.@{btn-prefix-cls}-primary {
&:not([disabled]) + &:not([disabled]) {
position: relative;
&::after {
position: absolute;
top: -@border-width-base;
left: -@border-width-base;
display: inline-block;
width: calc(100% + @border-width-base * 2);
height: @border-width-base;
background-color: @btn-group-border;
content: ' ';
}
}
}
}
}

View File

@ -14,19 +14,16 @@ import Button from '../../radio/radioButton';
import Select from '../../select';
import Header, { type CalendarHeaderProps } from '../Header';
function calendarProps(): PickerPanelProps<any> {
return (global as any).calendarProps;
}
function calendarHeaderProps(): CalendarHeaderProps<any> {
return (global as any).calendarHeaderProps;
}
const ref: {
calendarProps?: PickerPanelProps<unknown>;
calendarHeaderProps?: CalendarHeaderProps<unknown>;
} = {};
jest.mock('../Header', () => {
const HeaderModule = jest.requireActual('../Header');
const HeaderComponent = HeaderModule.default;
return (props: CalendarHeaderProps<any>) => {
(global as any).calendarHeaderProps = props;
ref.calendarHeaderProps = props;
return <HeaderComponent {...props} />;
};
});
@ -36,8 +33,8 @@ jest.mock('rc-picker', () => {
const PickerPanelComponent = RcPicker.PickerPanel;
return {
...RcPicker,
PickerPanel: (props: PickerPanelProps<any>) => {
(global as any).calendarProps = props;
PickerPanel: (props: PickerPanelProps<unknown>) => {
ref.calendarProps = props;
return <PickerPanelComponent {...props} />;
},
};
@ -151,8 +148,8 @@ describe('Calendar', () => {
it('getDateRange should returns a disabledDate function', () => {
const validRange: [Dayjs.Dayjs, Dayjs.Dayjs] = [Dayjs('2018-02-02'), Dayjs('2018-05-18')];
render(<Calendar validRange={validRange} defaultValue={Dayjs('2018-02-02')} />);
expect(calendarProps().disabledDate?.(Dayjs('2018-06-02'))).toBe(true);
expect(calendarProps().disabledDate?.(Dayjs('2018-04-02'))).toBe(false);
expect(ref.calendarProps?.disabledDate?.(Dayjs('2018-06-02'))).toBe(true);
expect(ref.calendarProps?.disabledDate?.(Dayjs('2018-04-02'))).toBe(false);
});
it('validRange should work with disabledDate function', () => {
@ -161,11 +158,11 @@ describe('Calendar', () => {
<Calendar validRange={validRange} disabledDate={data => data.isSame(Dayjs('2018-02-03'))} />,
);
expect(calendarProps().disabledDate?.(Dayjs('2018-02-01'))).toBe(true);
expect(calendarProps().disabledDate?.(Dayjs('2018-02-02'))).toBe(false);
expect(calendarProps().disabledDate?.(Dayjs('2018-02-03'))).toBe(true);
expect(calendarProps().disabledDate?.(Dayjs('2018-02-04'))).toBe(false);
expect(calendarProps().disabledDate?.(Dayjs('2018-06-01'))).toBe(true);
expect(ref.calendarProps?.disabledDate?.(Dayjs('2018-02-01'))).toBe(true);
expect(ref.calendarProps?.disabledDate?.(Dayjs('2018-02-02'))).toBe(false);
expect(ref.calendarProps?.disabledDate?.(Dayjs('2018-02-03'))).toBe(true);
expect(ref.calendarProps?.disabledDate?.(Dayjs('2018-02-04'))).toBe(false);
expect(ref.calendarProps?.disabledDate?.(Dayjs('2018-06-01'))).toBe(true);
});
it('Calendar MonthSelect should display correct label', () => {
@ -178,9 +175,9 @@ describe('Calendar', () => {
const monthMode = 'month';
const yearMode = 'year';
const wrapper = render(<Calendar />);
expect(calendarHeaderProps().mode).toEqual(monthMode);
expect(ref.calendarHeaderProps?.mode).toEqual(monthMode);
wrapper.rerender(<Calendar mode={yearMode} />);
expect(calendarHeaderProps().mode).toEqual(yearMode);
expect(ref.calendarHeaderProps?.mode).toEqual(yearMode);
});
it('Calendar should switch mode', () => {
@ -188,9 +185,9 @@ describe('Calendar', () => {
const yearMode = 'year';
const onPanelChangeStub = jest.fn();
const wrapper = render(<Calendar mode={yearMode} onPanelChange={onPanelChangeStub} />);
expect(calendarHeaderProps().mode).toEqual(yearMode);
expect(ref.calendarHeaderProps?.mode).toEqual(yearMode);
wrapper.rerender(<Calendar mode={monthMode} onPanelChange={onPanelChangeStub} />);
expect(calendarHeaderProps().mode).toEqual(monthMode);
expect(ref.calendarHeaderProps?.mode).toEqual(monthMode);
expect(onPanelChangeStub).toHaveBeenCalledTimes(0);
});
@ -231,7 +228,7 @@ describe('Calendar', () => {
const date = Dayjs(new Date(Date.UTC(2017, 7, 9, 8)));
const wrapper = render(<Calendar onPanelChange={onPanelChange} value={date} />);
expect(calendarHeaderProps().mode).toBe('month');
expect(ref.calendarHeaderProps?.mode).toBe('month');
expect(wrapper.container.querySelectorAll('.ant-picker-date-panel').length).toBe(1);
expect(wrapper.container.querySelectorAll('.ant-picker-month-panel').length).toBe(0);
fireEvent.click(wrapper.container.querySelector('.ant-radio-button-input[value="year"]')!);

View File

@ -262,7 +262,7 @@ exports[`renders ./components/carousel/demo/basic.md extend context correctly 1`
tabindex="-1"
>
<h3
style="height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
style="margin:0;height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
>
4
</h3>
@ -282,7 +282,7 @@ exports[`renders ./components/carousel/demo/basic.md extend context correctly 1`
tabindex="-1"
>
<h3
style="height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
style="margin:0;height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
>
1
</h3>
@ -302,7 +302,7 @@ exports[`renders ./components/carousel/demo/basic.md extend context correctly 1`
tabindex="-1"
>
<h3
style="height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
style="margin:0;height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
>
2
</h3>
@ -322,7 +322,7 @@ exports[`renders ./components/carousel/demo/basic.md extend context correctly 1`
tabindex="-1"
>
<h3
style="height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
style="margin:0;height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
>
3
</h3>
@ -342,7 +342,7 @@ exports[`renders ./components/carousel/demo/basic.md extend context correctly 1`
tabindex="-1"
>
<h3
style="height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
style="margin:0;height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
>
4
</h3>
@ -362,7 +362,7 @@ exports[`renders ./components/carousel/demo/basic.md extend context correctly 1`
tabindex="-1"
>
<h3
style="height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
style="margin:0;height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
>
1
</h3>
@ -382,7 +382,7 @@ exports[`renders ./components/carousel/demo/basic.md extend context correctly 1`
tabindex="-1"
>
<h3
style="height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
style="margin:0;height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
>
2
</h3>
@ -402,7 +402,7 @@ exports[`renders ./components/carousel/demo/basic.md extend context correctly 1`
tabindex="-1"
>
<h3
style="height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
style="margin:0;height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
>
3
</h3>
@ -422,7 +422,7 @@ exports[`renders ./components/carousel/demo/basic.md extend context correctly 1`
tabindex="-1"
>
<h3
style="height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
style="margin:0;height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
>
4
</h3>

View File

@ -262,7 +262,7 @@ exports[`renders ./components/carousel/demo/basic.md correctly 1`] = `
tabindex="-1"
>
<h3
style="height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
style="margin:0;height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
>
4
</h3>
@ -282,7 +282,7 @@ exports[`renders ./components/carousel/demo/basic.md correctly 1`] = `
tabindex="-1"
>
<h3
style="height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
style="margin:0;height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
>
1
</h3>
@ -302,7 +302,7 @@ exports[`renders ./components/carousel/demo/basic.md correctly 1`] = `
tabindex="-1"
>
<h3
style="height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
style="margin:0;height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
>
2
</h3>
@ -322,7 +322,7 @@ exports[`renders ./components/carousel/demo/basic.md correctly 1`] = `
tabindex="-1"
>
<h3
style="height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
style="margin:0;height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
>
3
</h3>
@ -342,7 +342,7 @@ exports[`renders ./components/carousel/demo/basic.md correctly 1`] = `
tabindex="-1"
>
<h3
style="height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
style="margin:0;height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
>
4
</h3>
@ -362,7 +362,7 @@ exports[`renders ./components/carousel/demo/basic.md correctly 1`] = `
tabindex="-1"
>
<h3
style="height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
style="margin:0;height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
>
1
</h3>
@ -382,7 +382,7 @@ exports[`renders ./components/carousel/demo/basic.md correctly 1`] = `
tabindex="-1"
>
<h3
style="height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
style="margin:0;height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
>
2
</h3>
@ -402,7 +402,7 @@ exports[`renders ./components/carousel/demo/basic.md correctly 1`] = `
tabindex="-1"
>
<h3
style="height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
style="margin:0;height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
>
3
</h3>
@ -422,7 +422,7 @@ exports[`renders ./components/carousel/demo/basic.md correctly 1`] = `
tabindex="-1"
>
<h3
style="height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
style="margin:0;height:160px;color:#fff;line-height:160px;text-align:center;background:#364d79"
>
4
</h3>

View File

@ -18,6 +18,7 @@ import { Carousel } from 'antd';
import React from 'react';
const contentStyle: React.CSSProperties = {
margin: 0,
height: '160px',
color: '#fff',
lineHeight: '160px',

View File

@ -19,6 +19,8 @@ import defaultRenderEmpty from '../config-provider/defaultRenderEmpty';
import DisabledContext from '../config-provider/DisabledContext';
import type { SizeType } from '../config-provider/SizeContext';
import SizeContext from '../config-provider/SizeContext';
import { useCompactItemContext } from '../space/Compact';
import { FormItemInputContext } from '../form/context';
import getIcons from '../select/utils/iconUtil';
import type { SelectCommonPlacement } from '../_util/motion';
@ -196,6 +198,7 @@ const Cascader = React.forwardRef((props: CascaderProps<any>, ref: React.Ref<Cas
const [wrapSelectSSR, hashId] = useSelectStyle(prefixCls);
const [wrapCascaderSSR] = useStyle(cascaderPrefixCls);
const { compactSize, compactItemClassnames } = useCompactItemContext(prefixCls, direction);
// =================== Dropdown ====================
const mergedDropdownClassName = classNames(
popupClassName || dropdownClassName,
@ -228,7 +231,7 @@ const Cascader = React.forwardRef((props: CascaderProps<any>, ref: React.Ref<Cas
// ===================== Size ======================
const size = React.useContext(SizeContext);
const mergedSize = customizeSize || size;
const mergedSize = compactSize || customizeSize || size;
// ===================== Disabled =====================
const disabled = React.useContext(DisabledContext);
@ -287,6 +290,7 @@ const Cascader = React.forwardRef((props: CascaderProps<any>, ref: React.Ref<Cas
[`${prefixCls}-in-form-item`]: isFormItemInput,
},
getStatusClassNames(prefixCls, mergedStatus, hasFeedback),
compactItemClassnames,
className,
hashId,
)}

View File

@ -242,10 +242,10 @@ describe('CheckboxGroup', () => {
const onChange = jest.fn();
const Demo: React.FC = () => {
const [v, setV] = useState<string>('');
const [v, setV] = useState('');
React.useEffect(() => {
setTimeout(setV('1') as unknown as TimerHandler, 1000);
setV('1');
}, []);
return (

View File

@ -13,6 +13,7 @@ import { ConfigContext } from '../../config-provider';
import DisabledContext from '../../config-provider/DisabledContext';
import SizeContext from '../../config-provider/SizeContext';
import { FormItemInputContext } from '../../form/context';
import { useCompactItemContext } from '../../space/Compact';
import LocaleReceiver from '../../locale-provider/LocaleReceiver';
import { getMergedStatus, getStatusClassNames } from '../../_util/statusUtils';
import enUS from '../locale/en_US';
@ -53,6 +54,7 @@ export default function generateRangePicker<DateType>(
const innerRef = React.useRef<RCRangePicker<DateType>>(null);
const { getPrefixCls, direction, getPopupContainer } = useContext(ConfigContext);
const prefixCls = getPrefixCls('picker', customizePrefixCls);
const { compactSize, compactItemClassnames } = useCompactItemContext(prefixCls, direction);
const { format, showTime, picker } = props as any;
const rootPrefixCls = getPrefixCls();
@ -76,7 +78,7 @@ export default function generateRangePicker<DateType>(
// ===================== Size =====================
const size = React.useContext(SizeContext);
const mergedSize = customizeSize || size;
const mergedSize = compactSize || customizeSize || size;
// ===================== Disabled =====================
const disabled = React.useContext(DisabledContext);
@ -135,6 +137,7 @@ export default function generateRangePicker<DateType>(
hasFeedback,
),
hashId,
compactItemClassnames,
className,
)}
locale={locale!.lang}

View File

@ -7,6 +7,7 @@ import type { GenerateConfig } from 'rc-picker/lib/generate/index';
import type { PickerMode } from 'rc-picker/lib/interface';
import * as React from 'react';
import { forwardRef, useContext, useImperativeHandle } from 'react';
import { useCompactItemContext } from '../../space/Compact';
import type { PickerDateProps, PickerProps, PickerTimeProps } from '.';
import { Components, getTimeProps } from '.';
import { ConfigContext } from '../../config-provider';
@ -54,6 +55,7 @@ export default function generatePicker<DateType>(generateConfig: GenerateConfig<
const { getPrefixCls, direction, getPopupContainer } = useContext(ConfigContext);
const prefixCls = getPrefixCls('picker', customizePrefixCls);
const { compactSize, compactItemClassnames } = useCompactItemContext(prefixCls, direction);
const innerRef = React.useRef<RCPicker<DateType>>(null);
const { format, showTime } = props as any;
@ -100,7 +102,7 @@ export default function generatePicker<DateType>(generateConfig: GenerateConfig<
// ===================== Size =====================
const size = React.useContext(SizeContext);
const mergedSize = customizeSize || size;
const mergedSize = compactSize || customizeSize || size;
// ===================== Disabled =====================
const disabled = React.useContext(DisabledContext);
@ -150,6 +152,7 @@ export default function generatePicker<DateType>(generateConfig: GenerateConfig<
hasFeedback,
),
hashId,
compactItemClassnames,
className,
)}
prefixCls={prefixCls}

View File

@ -1,7 +1,6 @@
import type { ChangeEventHandler } from 'react';
import React, { useState } from 'react';
import scrollIntoView from 'scroll-into-view-if-needed';
import userEvent from '@testing-library/user-event';
import classNames from 'classnames';
import type { ColProps } from 'antd/es/grid';
import type { FormInstance } from '..';
@ -20,15 +19,7 @@ import Switch from '../../switch';
import TreeSelect from '../../tree-select';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import {
fireEvent,
render,
sleep,
act,
screen,
pureRender,
waitFakeTimer,
} from '../../../tests/utils';
import { fireEvent, render, screen, pureRender, waitFakeTimer } from '../../../tests/utils';
import ConfigProvider from '../../config-provider';
import Drawer from '../../drawer';
import zhCN from '../../locale/zh_CN';
@ -51,27 +42,52 @@ describe('Form', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const change = async (
container: ReturnType<typeof render>['container'],
index: number,
value: string,
executeMockTimer: boolean,
) => {
fireEvent.change(container.querySelectorAll('input')?.[index], { target: { value } });
await sleep(200);
// const change = async (
// container: ReturnType<typeof render>['container'],
// index: number,
// value: string,
// executeMockTimer: boolean,
// ) => {
// fireEvent.change(container.querySelectorAll('input')?.[index], { target: { value } });
// await sleep(200);
if (executeMockTimer) {
for (let i = 0; i < 10; i += 1) {
act(() => {
jest.runAllTimers();
});
}
await sleep(1);
// if (executeMockTimer) {
// for (let i = 0; i < 10; i += 1) {
// act(() => {
// jest.runAllTimers();
// });
// }
// await sleep(1);
// }
// };
const changeValue = async (
input: HTMLElement | null | number,
value: string,
advTimer = 1000,
) => {
let element: HTMLElement;
if (typeof input === 'number') {
element = document.querySelectorAll('input')[input];
}
expect(element!).toBeTruthy();
fireEvent.change(element!, {
target: {
value,
},
});
if (advTimer) {
await waitFakeTimer(advTimer / 20, 20);
}
};
beforeEach(() => {
jest.useRealTimers();
document.body.innerHTML = '';
jest.useFakeTimers();
(scrollIntoView as any).mockReset();
});
@ -80,6 +96,8 @@ describe('Form', () => {
});
afterAll(() => {
jest.clearAllTimers();
jest.useRealTimers();
errorSpy.mockRestore();
warnSpy.mockRestore();
(scrollIntoView as any).mockRestore();
@ -100,34 +118,38 @@ describe('Form', () => {
);
// user type something and clear
await userEvent.type(screen.getByLabelText('test'), 'test');
await userEvent.clear(screen.getByLabelText('test'));
await changeValue(0, 'test');
await changeValue(0, '');
// should show alert with correct message and show correct styles
await expect(screen.findByRole('alert')).resolves.toHaveTextContent("'test' is required");
expect(screen.getByLabelText('test')).toHaveClass('ant-input-status-error');
expect(container.querySelectorAll('.ant-form-item-has-error').length).toBeTruthy();
expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent(
"'test' is required",
);
expect(container.querySelector('.ant-input-status-error')).toBeTruthy();
expect(container.querySelector('.ant-form-item-has-error')).toBeTruthy();
expect(onChange).toHaveBeenCalled();
});
it('should clean up', async () => {
jest.useFakeTimers();
const Demo: React.FC = () => {
const [form] = Form.useForm();
const onChange = async () => {
// Wait a while and then some logic to validate
await waitFakeTimer();
try {
await form.validateFields();
} catch (err) {
// do nothing
}
};
return (
<Form form={form} initialValues={{ aaa: '2' }}>
<Form.Item name="aaa">
<Input
onChange={async () => {
await sleep(0);
try {
await form.validateFields();
} catch {
// do nothing
}
}}
/>
<Input onChange={onChange} />
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => {
@ -155,14 +177,18 @@ describe('Form', () => {
};
const { container } = render(<Demo />);
await change(container, 0, '1', true);
expect(screen.getByRole('alert')).toHaveTextContent('aaa');
await change(container, 0, '2', true);
expect(screen.getByRole('alert')).toHaveTextContent('ccc');
await change(container, 0, '1', true);
expect(screen.getByRole('alert')).toHaveTextContent('aaa');
jest.useRealTimers();
await changeValue(0, '1');
await waitFakeTimer();
expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent('aaa');
await changeValue(0, '2');
await waitFakeTimer();
expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent('ccc');
await changeValue(0, '1');
await waitFakeTimer();
expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent('aaa');
});
});
@ -176,6 +202,7 @@ describe('Form', () => {
'Warning: [antd: Form.Item] `children` of render props only work with `shouldUpdate` or `dependencies`.',
);
});
it("`shouldUpdate` shouldn't work with `dependencies`", () => {
render(
<Form>
@ -255,7 +282,6 @@ describe('Form', () => {
});
it('input element should have the prop aria-describedby pointing to the help id when there are errors', async () => {
jest.useFakeTimers();
const { container } = pureRender(
<Form>
<Form.Item name="test" rules={[{ len: 3 }, { type: 'number' }]}>
@ -263,15 +289,11 @@ describe('Form', () => {
</Form.Item>
</Form>,
);
fireEvent.change(container.querySelector('input')!, { target: { value: 'Invalid number' } });
await waitFakeTimer();
await changeValue(0, 'Invalid number');
expect(container.querySelector('input')?.getAttribute('aria-describedby')).toBe('test_help');
expect(container.querySelector('.ant-form-item-explain')?.id).toBe('test_help');
jest.clearAllTimers();
jest.useRealTimers();
});
it('input element should have the prop aria-invalid when there are errors', async () => {
@ -283,8 +305,7 @@ describe('Form', () => {
</Form>,
);
fireEvent.change(container.querySelector('input')!, { target: { value: 'Invalid number' } });
await sleep(800);
await changeValue(0, 'Invalid number');
expect(container.querySelector('input')?.getAttribute('aria-invalid')).toBe('true');
});
@ -407,7 +428,7 @@ describe('Form', () => {
it('scrollToFirstError', async () => {
const onFinishFailed = jest.fn();
render(
const { container } = render(
<Form scrollToFirstError={{ block: 'center' }} onFinishFailed={onFinishFailed}>
<Form.Item name="test" rules={[{ required: true }]}>
<input />
@ -419,7 +440,9 @@ describe('Form', () => {
);
expect(scrollIntoView).not.toHaveBeenCalled();
await userEvent.click(screen.getByRole('button', { name: /submit/i }));
fireEvent.submit(container.querySelector('form')!);
await waitFakeTimer();
const inputNode = document.getElementById('test');
expect(scrollIntoView).toHaveBeenCalledWith(inputNode, {
block: 'center',
@ -452,7 +475,7 @@ describe('Form', () => {
});
it('dynamic change required', async () => {
render(
const { container } = render(
<Form>
<Form.Item label="light" name="light" valuePropName="checked">
<input type="checkbox" />
@ -469,45 +492,24 @@ describe('Form', () => {
);
// should not show alert by default
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
expect(container.querySelector('.ant-form-item-explain')).toBeFalsy();
// click to change the light field value to true
await userEvent.click(screen.getByLabelText('light'));
fireEvent.click(container.querySelector('input')!);
await waitFakeTimer();
// user input something and clear
await userEvent.type(screen.getByLabelText('bamboo'), '1');
await userEvent.clear(screen.getByLabelText('bamboo'));
await changeValue(1, '1');
await changeValue(1, '');
// should show alert says that the field is required
await expect(screen.findByRole('alert')).resolves.toHaveTextContent("'bamboo' is required");
});
it('should show alert with string when help is non-empty string', async () => {
render(
<Form>
<Form.Item help="good">
<input />
</Form.Item>
</Form>,
expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent(
"'bamboo' is required",
);
await expect(screen.findByRole('alert')).resolves.toHaveTextContent('good');
});
it('should show alert with empty string when help is empty string', async () => {
render(
<Form>
<Form.Item help="">
<input />
</Form.Item>
</Form>,
);
await expect(screen.findByRole('alert')).resolves.toHaveTextContent('');
});
describe('should show related className when customize help', () => {
it('normal', () => {
it('normal', async () => {
const { container } = render(
<Form>
<Form.Item help="good">
@ -515,10 +517,14 @@ describe('Form', () => {
</Form.Item>
</Form>,
);
await waitFakeTimer();
expect(container.querySelector('.ant-form-item-explain')).toHaveTextContent('good');
expect(container.querySelector('.ant-form-item-with-help')).toBeTruthy();
});
it('empty string', () => {
it('empty string', async () => {
const { container } = render(
<Form>
<Form.Item help="">
@ -526,6 +532,10 @@ describe('Form', () => {
</Form.Item>
</Form>,
);
await waitFakeTimer();
expect(container.querySelector('.ant-form-item-explain')).toHaveTextContent('');
expect(container.querySelector('.ant-form-item-with-help')).toBeTruthy();
});
});
@ -539,8 +549,6 @@ describe('Form', () => {
// https://github.com/ant-design/ant-design/issues/20706
it('Error change should work', async () => {
jest.useFakeTimers();
const { container } = render(
<Form>
<Form.Item
@ -565,17 +573,16 @@ describe('Form', () => {
/* eslint-disable no-await-in-loop */
for (let i = 0; i < 3; i += 1) {
await change(container, 0, 'bamboo', true);
await change(container, 0, '', true);
await changeValue(0, 'bamboo');
await changeValue(0, '');
expect(container.querySelector('.ant-form-item-explain')?.textContent).toEqual(
"'name' is required",
);
await change(container, 0, 'p', true);
await sleep(100);
await changeValue(0, 'p');
expect(container.querySelector('.ant-form-item-explain')?.textContent).toEqual('not a p');
}
/* eslint-enable */
jest.useRealTimers();
});
// https://github.com/ant-design/ant-design/issues/20813
@ -592,15 +599,17 @@ describe('Form', () => {
);
};
render(<App />);
const { container } = render(<App />);
// should show initial text
await expect(screen.findByRole('alert')).resolves.toHaveTextContent('');
await waitFakeTimer();
expect(container.querySelector('.ant-form-item-explain')).toHaveTextContent('');
await userEvent.click(screen.getByRole('button'));
fireEvent.click(container.querySelector('button')!);
// should show bamboo alert without opacity and hide first alert with opacity: 0
await expect(screen.findByRole('alert')).resolves.toHaveTextContent('bamboo');
await waitFakeTimer();
expect(container.querySelector('.ant-form-item-explain')).toHaveTextContent('bamboo');
});
it('warning when use `dependencies` but `name` is empty & children is not a render props', () => {
@ -616,8 +625,6 @@ describe('Form', () => {
// https://github.com/ant-design/ant-design/issues/20948
it('not repeat render when Form.Item is not a real Field', async () => {
jest.useFakeTimers();
const shouldNotRender = jest.fn();
const StaticInput: React.FC<React.InputHTMLAttributes<HTMLInputElement>> = ({
id,
@ -663,9 +670,6 @@ describe('Form', () => {
expect(container.querySelector<HTMLInputElement>('#changed')!.value).toEqual('bamboo');
expect(shouldNotRender).toHaveBeenCalledTimes(1);
expect(shouldRender).toHaveBeenCalledTimes(2);
jest.clearAllTimers();
jest.useRealTimers();
});
it('empty help should also render', () => {
@ -692,14 +696,12 @@ describe('Form', () => {
</Form>,
);
await change(container, 0, '', true);
await changeValue(0, '');
expect(container.querySelector('.ant-form-item')).toHaveClass('ant-form-item-has-error');
expect(container.querySelector('.ant-form-item-explain')!.textContent).toEqual('help');
jest.useRealTimers();
});
it('clear validation message when', async () => {
jest.useFakeTimers();
const { container } = render(
<Form>
<Form.Item name="test" label="test" rules={[{ required: true, message: 'message' }]}>
@ -708,27 +710,26 @@ describe('Form', () => {
</Form>,
);
await change(container, 0, '1', true);
await changeValue(0, '1');
expect(container.querySelectorAll('.ant-form-item-explain').length).toBeFalsy();
await change(container, 0, '', true);
await changeValue(0, '');
expect(container.querySelectorAll('.ant-form-item-explain').length).toBeTruthy();
await change(container, 0, '123', true);
await sleep(800);
await changeValue(0, '123');
expect(container.querySelectorAll('.ant-form-item-explain').length).toBeFalsy();
jest.useRealTimers();
});
// https://github.com/ant-design/ant-design/issues/21167
it('`require` without `name`', () => {
render(
const { container } = render(
<Form.Item label="test" name="test" required>
<input />
</Form.Item>,
);
expect(screen.getByTitle('test')).toHaveClass('ant-form-item-required');
// expect(screen.getByTitle('test')).toHaveClass('ant-form-item-required');
expect(container.querySelector('.ant-form-item-required')).toBeTruthy();
});
it('0 is a validate Field', () => {
@ -757,7 +758,7 @@ describe('Form', () => {
});
// https://github.com/ant-design/ant-design/issues/21415
it('should not throw error when Component.props.onChange is null', () => {
it('should not throw error when Component.props.onChange is null', async () => {
const CustomComponent: React.FC = () => (
<input onChange={null as unknown as ChangeEventHandler<HTMLInputElement>} />
);
@ -768,10 +769,8 @@ describe('Form', () => {
</Form.Item>
</Form>,
);
const handle = async () => {
await userEvent.type(screen.getByRole('textbox'), 'aaa');
};
expect(handle).not.toThrow();
await changeValue(0, 'aaa');
});
it('change `help` should not warning', async () => {
@ -797,91 +796,91 @@ describe('Form', () => {
);
};
render(<Demo />);
await userEvent.click(screen.getByRole('button'));
const { container } = render(<Demo />);
fireEvent.click(container.querySelector('button')!);
expect(errorSpy).not.toHaveBeenCalled();
});
it('`label` support template', async () => {
render(
const { container } = render(
// eslint-disable-next-line no-template-curly-in-string
<Form validateMessages={{ required: '${label} is good!' }}>
<Form.Item name="test" label="Bamboo" rules={[{ required: true }]}>
<input />
</Form.Item>
<Form.Item>
<Button htmlType="submit">Submit</Button>
</Form.Item>
</Form>,
);
await userEvent.click(screen.getByRole('button'));
fireEvent.submit(container.querySelector('form')!);
await waitFakeTimer();
await expect(screen.findByRole('alert')).resolves.toHaveTextContent('Bamboo is good!');
expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent(
'Bamboo is good!',
);
});
// https://github.com/ant-design/ant-design/issues/33691
it('should keep upper locale in nested ConfigProvider', async () => {
render(
const { container } = render(
<ConfigProvider locale={zhCN}>
<ConfigProvider>
<Form>
<Form.Item name="test" label="Bamboo" rules={[{ required: true }]}>
<input />
</Form.Item>
<Form.Item>
<Button htmlType="submit">Submit</Button>
</Form.Item>
</Form>
</ConfigProvider>
</ConfigProvider>,
);
await userEvent.click(screen.getByRole('button'));
fireEvent.submit(container.querySelector('form')!);
await waitFakeTimer();
await expect(screen.findByRole('alert')).resolves.toHaveTextContent('请输入Bamboo');
expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent(
'请输入Bamboo',
);
});
it('`name` support template when label is not provided', async () => {
render(
const { container } = render(
// eslint-disable-next-line no-template-curly-in-string
<Form validateMessages={{ required: '${label} is good!' }}>
<Form.Item name="Bamboo" rules={[{ required: true }]}>
<input />
</Form.Item>
<Form.Item>
<Button htmlType="submit">Submit</Button>
</Form.Item>
</Form>,
);
await userEvent.click(screen.getByRole('button'));
fireEvent.submit(container.querySelector('form')!);
await waitFakeTimer();
await expect(screen.findByRole('alert')).resolves.toHaveTextContent('Bamboo is good!');
expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent(
'Bamboo is good!',
);
});
it('`messageVariables` support validate', async () => {
render(
const { container } = render(
// eslint-disable-next-line no-template-curly-in-string
<Form validateMessages={{ required: '${label} is good!' }}>
<Form.Item name="test" messageVariables={{ label: 'Bamboo' }} rules={[{ required: true }]}>
<input />
</Form.Item>
<Form.Item>
<Button htmlType="submit">Submit</Button>
</Form.Item>
</Form>,
);
await userEvent.click(screen.getByRole('button'));
fireEvent.submit(container.querySelector('form')!);
await waitFakeTimer();
await expect(screen.findByRole('alert')).resolves.toHaveTextContent('Bamboo is good!');
expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent(
'Bamboo is good!',
);
});
it('validation message should has alert role', async () => {
// https://github.com/ant-design/ant-design/issues/25711
render(
const { container } = render(
// eslint-disable-next-line no-template-curly-in-string
<Form validateMessages={{ required: 'name is good!' }}>
<Form.Item name="test" rules={[{ required: true }]}>
@ -893,9 +892,12 @@ describe('Form', () => {
</Form>,
);
await userEvent.click(screen.getByRole('button'));
fireEvent.submit(container.querySelector('form')!);
await waitFakeTimer();
await expect(screen.findByRole('alert')).resolves.toHaveTextContent('name is good!');
expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent(
'name is good!',
);
});
it('return same form instance', async () => {
@ -917,11 +919,12 @@ describe('Form', () => {
);
};
pureRender(<App />);
const { container } = pureRender(<App />);
for (let i = 0; i < 5; i += 1) {
fireEvent.click(container.querySelector('button')!);
// eslint-disable-next-line no-await-in-loop
await userEvent.click(screen.getByRole('button'));
await waitFakeTimer();
}
expect(instances.size).toBe(1);
@ -942,12 +945,13 @@ describe('Form', () => {
</Form.Item>
</Form>
);
pureRender(<Demo />);
const { container } = pureRender(<Demo />);
renderTimes = 0;
jest.clearAllMocks();
fireEvent.change(screen.getByLabelText('username'), { target: { value: 'a' } });
await changeValue(0, 'a');
expect(renderTimes).toEqual(1);
expect(screen.getByLabelText('username')).toHaveValue('a');
expect(container.querySelector('input')).toHaveValue('a');
});
it('should warning with `defaultValue`', () => {
@ -979,13 +983,15 @@ describe('Form', () => {
</Form>
);
const { rerender } = render(<Demo showA />);
const { container, rerender } = render(<Demo showA />);
await expect(screen.findByRole('alert')).resolves.toBeInTheDocument();
await waitFakeTimer();
expect(container.querySelector('.ant-form-item-explain')).toBeTruthy();
rerender(<Demo showA={false} />);
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
await waitFakeTimer();
expect(container.querySelector('.ant-form-item-explain')).toBeFalsy();
});
it('no warning of initialValue & getValueProps & preserve', () => {
@ -1000,7 +1006,7 @@ describe('Form', () => {
});
it('should customize id when pass with id', () => {
render(
const { container } = render(
<Form>
<Form.Item name="light">
<Input id="bamboo" />
@ -1008,11 +1014,11 @@ describe('Form', () => {
</Form>,
);
expect(screen.getByRole('textbox')).toHaveAttribute('id', 'bamboo');
expect(container.querySelector('input')!.id).toEqual('bamboo');
});
it('should trigger validate when onBlur when pass validateTrigger onBlur', async () => {
render(
const { container } = render(
<Form validateTrigger="onBlur">
<Form.Item name="light" label="light" rules={[{ len: 3 }]}>
<Input />
@ -1021,14 +1027,14 @@ describe('Form', () => {
);
// type a invalidate value, not trigger validation
await userEvent.type(screen.getByRole('textbox'), '7777');
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
await changeValue(0, '7777');
expect(container.querySelector('.ant-form-item-explain')).toBeFalsy();
// tab(onBlur) the input field, trigger and see the alert
fireEvent.blur(screen.getByRole('textbox'));
fireEvent.blur(container.querySelector('input')!);
await waitFakeTimer();
await expect(screen.findByRole('alert')).resolves.toBeInTheDocument();
expect(container.querySelector('.ant-form-item-explain')).toBeTruthy();
});
describe('Form item hidden', () => {
@ -1056,7 +1062,7 @@ describe('Form', () => {
});
it('legacy hideRequiredMark', () => {
render(
const { container } = render(
<Form hideRequiredMark role="form">
<Form.Item name="light" label="light" required>
<Input />
@ -1064,7 +1070,7 @@ describe('Form', () => {
</Form>,
);
expect(screen.getByRole('form')).toHaveClass('ant-form-hide-required-mark');
expect(container.querySelector('form')!).toHaveClass('ant-form-hide-required-mark');
});
it('form should support disabled', () => {
@ -1137,7 +1143,7 @@ describe('Form', () => {
});
it('_internalItemRender api test', () => {
render(
const { container } = render(
<Form>
<Form.Item
name="light"
@ -1159,7 +1165,7 @@ describe('Form', () => {
</Form>,
);
expect(screen.getByRole('heading')).toHaveTextContent(/warning title/i);
expect(container.querySelector('h1')!).toHaveTextContent(/warning title/i);
});
it('Form Item element id will auto add form_item prefix if form name is empty and item name is in the black list', async () => {
@ -1198,16 +1204,17 @@ describe('Form', () => {
);
};
const { rerender } = render(<Demo />);
const { container, rerender } = render(<Demo />);
expect(mockFn).toHaveBeenCalled();
expect((Util.getFieldId as () => string)()).toBe(itemName);
// make sure input id is parentNode
expect(screen.getByLabelText(itemName)).toHaveAccessibleName(itemName);
await userEvent.click(screen.getByRole('button'));
fireEvent.click(container.querySelector('button')!);
await waitFakeTimer();
expect(screen.getByRole('button')).toHaveTextContent('show');
expect(container.querySelector('button')!).toHaveTextContent('show');
mockFn.mockRestore();
@ -1217,7 +1224,7 @@ describe('Form', () => {
describe('tooltip', () => {
it('ReactNode', async () => {
render(
const { container } = render(
<Form>
<Form.Item label="light" tooltip={<span>Bamboo</span>}>
<Input />
@ -1225,21 +1232,14 @@ describe('Form', () => {
</Form>,
);
await userEvent.hover(screen.getByRole('img', { name: 'question-circle' }));
await expect(screen.findByRole('tooltip')).resolves.toMatchInlineSnapshot(`
<div
class="ant-tooltip-inner"
role="tooltip"
>
<span>
Bamboo
</span>
</div>
`);
fireEvent.mouseEnter(container.querySelector('.anticon-question-circle')!);
await waitFakeTimer();
expect(container.querySelector('.ant-tooltip-inner')).toHaveTextContent('Bamboo');
});
it('config tooltip should show when hover on icon', async () => {
render(
const { container } = render(
<Form>
<Form.Item label="light" tooltip={{ title: 'Bamboo' }}>
<Input />
@ -1247,9 +1247,10 @@ describe('Form', () => {
</Form>,
);
await userEvent.hover(screen.getByRole('img', { name: 'question-circle' }));
fireEvent.mouseEnter(container.querySelector('.anticon-question-circle')!);
await waitFakeTimer();
await expect(screen.findByRole('tooltip')).resolves.toHaveTextContent('Bamboo');
expect(container.querySelector('.ant-tooltip-inner')).toHaveTextContent('Bamboo');
});
});
@ -1269,20 +1270,17 @@ describe('Form', () => {
</Form>,
);
await userEvent.type(screen.getByLabelText('test'), 'test');
await userEvent.clear(screen.getByLabelText('test'));
await changeValue(0, 'test');
await changeValue(0, '');
await sleep(1000);
expect(container.querySelectorAll('.ant-form-item-with-help').length).toBeTruthy();
expect(container.querySelectorAll('.ant-form-item-has-warning').length).toBeTruthy();
expect(container.querySelector('.ant-form-item-with-help')).toBeTruthy();
expect(container.querySelector('.ant-form-item-has-warning')).toBeTruthy();
});
it('not warning when remove on validate', async () => {
jest.useFakeTimers();
let rejectFn: (reason?: any) => void = jest.fn();
const { container, unmount } = render(
const { unmount } = render(
<Form>
<Form.Item>
<Form.Item
@ -1304,7 +1302,7 @@ describe('Form', () => {
</Form>,
);
await change(container, 0, '', true);
await changeValue(0, '');
unmount();
@ -1312,8 +1310,6 @@ describe('Form', () => {
rejectFn(new Error('delay failed'));
expect(errorSpy).not.toHaveBeenCalled();
jest.useRealTimers();
});
describe('form colon', () => {
@ -1437,8 +1433,6 @@ describe('Form', () => {
});
it('Form.Item.useStatus should work', async () => {
jest.useFakeTimers();
const {
Item: { useStatus },
} = Form;
@ -1495,9 +1489,6 @@ describe('Form', () => {
expect(container.querySelector('.custom-input-required')?.classList).toContain(
'custom-input-status-error',
);
jest.clearAllTimers();
jest.useRealTimers();
});
it('item customize margin', async () => {
@ -1513,9 +1504,8 @@ describe('Form', () => {
</Form>,
);
fireEvent.change(container.querySelector('input')!, { target: { value: '' } });
await changeValue(0, '');
await sleep(0);
computeSpy.mockRestore();
expect(container.querySelector('.ant-form-item-margin-offset')).toHaveStyle({

View File

@ -143,4 +143,44 @@ describe('Grid', () => {
xxl: false,
});
});
it('should align by responsive align prop', () => {
const matchMediaSpy = jest.spyOn(window, 'matchMedia');
matchMediaSpy.mockImplementation(
query =>
({
addListener: (cb: (e: { matches: boolean }) => void) => {
cb({ matches: query === '(max-width: 575px)' });
},
removeListener: jest.fn(),
matches: query === '(max-width: 575px)',
} as any),
);
const { container } = render(<Row align="middle" />);
expect(container.innerHTML).toContain('ant-row-middle');
const { container: container2 } = render(<Row align={{ xs: 'middle' }} />);
expect(container2.innerHTML).toContain('ant-row-middle');
const { container: container3 } = render(<Row align={{ lg: 'middle' }} />);
expect(container3.innerHTML).not.toContain('ant-row-middle');
});
it('should justify by responsive justify prop', () => {
const matchMediaSpy = jest.spyOn(window, 'matchMedia');
matchMediaSpy.mockImplementation(
query =>
({
addListener: (cb: (e: { matches: boolean }) => void) => {
cb({ matches: query === '(max-width: 575px)' });
},
removeListener: jest.fn(),
matches: query === '(max-width: 575px)',
} as any),
);
const { container } = render(<Row justify="center" />);
expect(container.innerHTML).toContain('ant-row-center');
const { container: container2 } = render(<Row justify={{ xs: 'center' }} />);
expect(container2.innerHTML).toContain('ant-row-center');
const { container: container3 } = render(<Row justify={{ lg: 'center' }} />);
expect(container3.innerHTML).not.toContain('ant-row-center');
});
});

View File

@ -44,9 +44,9 @@ If the Ant Design grid layout component does not meet your needs, you can use th
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| align | Vertical alignment | `top` \| `middle` \| `bottom` | `top` | |
| align | Vertical alignment | `top` \| `middle` \| `bottom` \| `stretch` \| `{[key in 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'xxl']: 'top' \| 'middle' \| 'bottom' \| 'stretch'}` | `top` | object: 4.24.0 |
| gutter | Spacing between grids, could be a number or a object like { xs: 8, sm: 16, md: 24}. Or you can use array to make horizontal and vertical spacing work at the same time `[horizontal, vertical]` | number \| object \| array | 0 | |
| justify | Horizontal arrangement | `start` \| `end` \| `center` \| `space-around` \| `space-between` \| `space-evenly` | `start` | |
| justify | Horizontal arrangement | `start` \| `end` \| `center` \| `space-around` \| `space-between` \| `space-evenly` \| `{[key in 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'xxl']: 'start' \| 'end' \| 'center' \| 'space-around' \| 'space-between' \| 'space-evenly'}` | `start` | object: 4.24.0 |
| wrap | Auto wrap line | boolean | true | 4.8.0 |
### Col

View File

@ -43,9 +43,9 @@ Ant Design 的布局组件若不能满足你的需求,你也可以直接使用
| 成员 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| align | 垂直对齐方式 | `top` \| `middle` \| `bottom` | `top` | |
| align | 垂直对齐方式 | `top` \| `middle` \| `bottom` \| `stretch` \| `{[key in 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'xxl']: 'top' \| 'middle' \| 'bottom' \| 'stretch'}` | `top` | object: 4.24.0 |
| gutter | 栅格间隔,可以写成像素值或支持响应式的对象写法来设置水平间隔 { xs: 8, sm: 16, md: 24}。或者使用数组形式同时设置 `[水平间距, 垂直间距]` | number \| object \| array | 0 | |
| justify | 水平排列方式 | `start` \| `end` \| `center` \| `space-around` \| `space-between` \| `space-evenly` | `start` | |
| justify | 水平排列方式 | `start` \| `end` \| `center` \| `space-around` \| `space-between` \| `space-evenly` \| `{[key in 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'xxl']: 'start' \| 'end' \| 'center' \| 'space-around' \| 'space-between' \| 'space-evenly'}` | `start` | object: 4.24.0 |
| wrap | 是否自动换行 | boolean | true | 4.8.0 |
### Col

View File

@ -11,16 +11,50 @@ import { useRowStyle } from './style';
const RowAligns = tuple('top', 'middle', 'bottom', 'stretch');
const RowJustify = tuple('start', 'end', 'center', 'space-around', 'space-between', 'space-evenly');
type Responsive = 'xxl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs';
type ResponsiveLike<T> = {
[key in Responsive]?: T;
};
type Gap = number | undefined;
export type Gutter = number | undefined | Partial<Record<Breakpoint, number>>;
type ResponsiveAligns = ResponsiveLike<typeof RowAligns[number]>;
type ResponsiveJustify = ResponsiveLike<typeof RowJustify[number]>;
export interface RowProps extends React.HTMLAttributes<HTMLDivElement> {
gutter?: Gutter | [Gutter, Gutter];
align?: typeof RowAligns[number];
justify?: typeof RowJustify[number];
align?: typeof RowAligns[number] | ResponsiveAligns;
justify?: typeof RowJustify[number] | ResponsiveJustify;
prefixCls?: string;
wrap?: boolean;
}
function useMergePropByScreen(oriProp: RowProps['align'] | RowProps['justify'], screen: ScreenMap) {
const [prop, setProp] = React.useState(typeof oriProp === 'string' ? oriProp : '');
const clacMergeAlignOrJustify = () => {
if (typeof oriProp !== 'object') {
return;
}
for (let i = 0; i < responsiveArray.length; i++) {
const breakpoint: Breakpoint = responsiveArray[i];
// if do not match, do nothing
if (!screen[breakpoint]) continue;
const curVal = oriProp[breakpoint];
if (curVal !== undefined) {
setProp(curVal);
return;
}
}
};
React.useEffect(() => {
clacMergeAlignOrJustify();
}, [JSON.stringify(oriProp), screen]);
return prop;
}
const Row = React.forwardRef<HTMLDivElement, RowProps>((props, ref) => {
const {
prefixCls: customizePrefixCls,
@ -44,6 +78,20 @@ const Row = React.forwardRef<HTMLDivElement, RowProps>((props, ref) => {
xl: true,
xxl: true,
});
// to save screens info when responsiveObserve callback had been call
const [curScreens, setCurScreens] = React.useState<ScreenMap>({
xs: false,
sm: false,
md: false,
lg: false,
xl: false,
xxl: false,
});
// ================================== calc reponsive data ==================================
const mergeAlign = useMergePropByScreen(align, curScreens);
const mergeJustify = useMergePropByScreen(justify, curScreens);
const supportFlexGap = useFlexGapSupport();
@ -52,6 +100,7 @@ const Row = React.forwardRef<HTMLDivElement, RowProps>((props, ref) => {
// ================================== Effect ==================================
React.useEffect(() => {
const token = ResponsiveObserve.subscribe(screen => {
setCurScreens(screen);
const currentGutter = gutterRef.current || 0;
if (
(!Array.isArray(currentGutter) && typeof currentGutter === 'object') ||
@ -91,8 +140,8 @@ const Row = React.forwardRef<HTMLDivElement, RowProps>((props, ref) => {
prefixCls,
{
[`${prefixCls}-no-wrap`]: wrap === false,
[`${prefixCls}-${justify}`]: justify,
[`${prefixCls}-${align}`]: align,
[`${prefixCls}-${mergeJustify}`]: mergeJustify,
[`${prefixCls}-${mergeAlign}`]: mergeAlign,
[`${prefixCls}-rtl`]: direction === 'rtl',
},
className,

View File

@ -11,6 +11,7 @@ import DisabledContext from '../config-provider/DisabledContext';
import type { SizeType } from '../config-provider/SizeContext';
import SizeContext from '../config-provider/SizeContext';
import { FormItemInputContext, NoFormStyle } from '../form/context';
import { useCompactItemContext } from '../space/Compact';
import { cloneElement } from '../_util/reactNode';
import type { InputStatus } from '../_util/statusUtils';
import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils';
@ -57,6 +58,7 @@ const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>((props,
// Style
const [wrapSSR, hashId] = useStyle(prefixCls);
const { compactSize, compactItemClassnames } = useCompactItemContext(prefixCls, direction);
let upIcon = <UpOutlined className={`${prefixCls}-handler-up-inner`} />;
let downIcon = <DownOutlined className={`${prefixCls}-handler-down-inner`} />;
const controlsTemp = typeof controls === 'boolean' ? controls : undefined;
@ -84,7 +86,7 @@ const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>((props,
} = useContext(FormItemInputContext);
const mergedStatus = getMergedStatus(contextStatus, customStatus);
const mergeSize = customizeSize || size;
const mergeSize = compactSize || customizeSize || size;
// ===================== Disabled =====================
const disabled = React.useContext(DisabledContext);
const mergedDisabled = customDisabled ?? disabled;
@ -98,6 +100,7 @@ const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>((props,
[`${prefixCls}-in-form-item`]: isFormItemInput,
},
getStatusClassNames(prefixCls, mergedStatus),
compactItemClassnames,
hashId,
className,
);

View File

@ -10,6 +10,7 @@ import DisabledContext from '../config-provider/DisabledContext';
import type { SizeType } from '../config-provider/SizeContext';
import SizeContext from '../config-provider/SizeContext';
import { FormItemInputContext, NoFormStyle } from '../form/context';
import { NoCompactStyle, useCompactItemContext } from '../space/Compact';
import type { InputStatus } from '../_util/statusUtils';
import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils';
import warning from '../_util/warning';
@ -140,6 +141,7 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
allowClear,
addonAfter,
addonBefore,
className,
onChange,
...rest
} = props;
@ -151,9 +153,12 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
// Style
const [wrapSSR, hashId] = useStyle(prefixCls);
// ===================== Compact Item =====================
const { compactSize, compactItemClassnames } = useCompactItemContext(prefixCls, direction);
// ===================== Size =====================
const size = React.useContext(SizeContext);
const mergedSize = customSize || size;
const mergedSize = compactSize || customSize || size;
// ===================== Disabled =====================
const disabled = React.useContext(DisabledContext);
@ -221,19 +226,24 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
onFocus={handleFocus}
suffix={suffixNode}
allowClear={mergedAllowClear}
className={classNames(className, compactItemClassnames)}
onChange={handleChange}
addonAfter={
addonAfter && (
<NoFormStyle override status>
{addonAfter}
</NoFormStyle>
<NoCompactStyle>
<NoFormStyle override status>
{addonAfter}
</NoFormStyle>
</NoCompactStyle>
)
}
addonBefore={
addonBefore && (
<NoFormStyle override status>
{addonBefore}
</NoFormStyle>
<NoCompactStyle>
<NoFormStyle override status>
{addonBefore}
</NoFormStyle>
</NoCompactStyle>
)
}
inputClassName={classNames(

View File

@ -5,6 +5,7 @@ import * as React from 'react';
import Button from '../button';
import { ConfigContext } from '../config-provider';
import SizeContext from '../config-provider/SizeContext';
import { useCompactItemContext } from '../space/Compact';
import { cloneElement } from '../_util/reactNode';
import type { InputProps, InputRef } from './Input';
import Input from './Input';
@ -44,7 +45,11 @@ const Search = React.forwardRef<InputRef, SearchProps>((props, ref) => {
const contextSize = React.useContext(SizeContext);
const composedRef = React.useRef<boolean>(false);
const size = customizeSize || contextSize;
const prefixCls = getPrefixCls('input-search', customizePrefixCls);
const inputPrefixCls = getPrefixCls('input', customizeInputPrefixCls);
const { compactSize } = useCompactItemContext(prefixCls, direction);
const size = compactSize || customizeSize || contextSize;
const inputRef = React.useRef<InputRef>(null);
@ -76,9 +81,6 @@ const Search = React.forwardRef<InputRef, SearchProps>((props, ref) => {
onSearch(e);
};
const prefixCls = getPrefixCls('input-search', customizePrefixCls);
const inputPrefixCls = getPrefixCls('input', customizeInputPrefixCls);
const searchIcon = typeof enterButton === 'boolean' ? <SearchOutlined /> : null;
const btnClassName = `${prefixCls}-button`;

View File

@ -5081,6 +5081,170 @@ exports[`renders ./components/input/demo/borderless-debug.md extend context corr
RMB
</span>
</span>
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
style="border:2px solid #000"
>
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-clear-icon-hidden ant-input-clear-icon"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</div>
`;
exports[`renders ./components/input/demo/debug-addon.md extend context correctly 1`] = `
<div
class="ant-space ant-space-vertical"
>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
Input addon Button:
</div>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
<span
class="ant-input-group-wrapper"
>
<span
class="ant-input-wrapper ant-input-group"
>
<input
class="ant-input"
type="text"
value="mysite"
/>
<span
class="ant-input-group-addon"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Submit
</span>
</button>
</span>
</span>
</span>
</div>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
<span
class="ant-input-group-wrapper"
>
<span
class="ant-input-wrapper ant-input-group"
>
<input
class="ant-input"
type="text"
value="mysite"
/>
<span
class="ant-input-group-addon"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Submit
</span>
</button>
</span>
</span>
</span>
</div>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
<br />
</div>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
<br />
</div>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
Input addon Button icon:
</div>
<div
class="ant-space-item"
>
<span
class="ant-input-group-wrapper"
>
<span
class="ant-input-wrapper ant-input-group"
>
<input
class="ant-input"
type="text"
value="mysite"
/>
<span
class="ant-input-group-addon"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span
aria-label="setting"
class="anticon anticon-setting"
role="img"
>
<svg
aria-hidden="true"
data-icon="setting"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z"
/>
</svg>
</span>
</button>
</span>
</span>
</span>
</div>
</div>
`;

View File

@ -1300,6 +1300,170 @@ exports[`renders ./components/input/demo/borderless-debug.md correctly 1`] = `
RMB
</span>
</span>
<span
class="ant-input-affix-wrapper ant-input-affix-wrapper-textarea-with-clear-btn"
style="border:2px solid #000"
>
<textarea
class="ant-input"
/>
<span
aria-label="close-circle"
class="anticon anticon-close-circle ant-input-clear-icon-hidden ant-input-clear-icon"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</div>
`;
exports[`renders ./components/input/demo/debug-addon.md correctly 1`] = `
<div
class="ant-space ant-space-vertical"
>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
Input addon Button:
</div>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
<span
class="ant-input-group-wrapper"
>
<span
class="ant-input-wrapper ant-input-group"
>
<input
class="ant-input"
type="text"
value="mysite"
/>
<span
class="ant-input-group-addon"
>
<button
class="ant-btn ant-btn-primary"
type="button"
>
<span>
Submit
</span>
</button>
</span>
</span>
</span>
</div>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
<span
class="ant-input-group-wrapper"
>
<span
class="ant-input-wrapper ant-input-group"
>
<input
class="ant-input"
type="text"
value="mysite"
/>
<span
class="ant-input-group-addon"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span>
Submit
</span>
</button>
</span>
</span>
</span>
</div>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
<br />
</div>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
<br />
</div>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
Input addon Button icon:
</div>
<div
class="ant-space-item"
>
<span
class="ant-input-group-wrapper"
>
<span
class="ant-input-wrapper ant-input-group"
>
<input
class="ant-input"
type="text"
value="mysite"
/>
<span
class="ant-input-group-addon"
>
<button
class="ant-btn ant-btn-default"
type="button"
>
<span
aria-label="setting"
class="anticon anticon-setting"
role="img"
>
<svg
aria-hidden="true"
data-icon="setting"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z"
/>
</svg>
</span>
</button>
</span>
</span>
</span>
</div>
</div>
`;

View File

@ -1,14 +1,14 @@
---
order: 98
title:
zh-CN: Borderless Debug
en-US: Borderless Debug
zh-CN: Style Debug
en-US: Style Debug
debug: true
---
## zh-CN
Buggy!
Buggy! 测试一些踩过的样式坑。
## en-US
@ -29,6 +29,7 @@ const App: React.FC = () => (
<Input placeholder="Unbordered" bordered={false} allowClear />
<Input prefix="¥" suffix="RMB" bordered={false} />
<Input prefix="¥" suffix="RMB" disabled bordered={false} />
<TextArea allowClear style={{ border: '2px solid #000' }} />
</div>
);

View File

@ -0,0 +1,42 @@
---
order: 100
title:
zh-CN: debug 前置/后置标签
en-US: debug Pre / Post tab
debug: true
---
## zh-CN
一些特殊的前置后置标签。
## en-US
Some special pre & post tabs example.
```tsx
import { SettingOutlined } from '@ant-design/icons';
import { Input, Space, Button } from 'antd';
import React from 'react';
const App: React.FC = () => (
<Space direction="vertical">
Input addon Button:
<Input addonAfter={<Button type="primary">Submit</Button>} defaultValue="mysite" />
<Input addonAfter={<Button>Submit</Button>} defaultValue="mysite" />
<br />
<br />
Input addon Button icon:
<Input
addonAfter={
<Button>
<SettingOutlined />
</Button>
}
defaultValue="mysite"
/>
</Space>
);
export default App;
```

View File

@ -1,9 +1,12 @@
/* eslint-disable no-template-curly-in-string */
import Pagination from 'rc-pagination/lib/locale/it_IT';
import Calendar from '../calendar/locale/it_IT';
import DatePicker from '../date-picker/locale/it_IT';
import type { Locale } from '../locale-provider';
import TimePicker from '../time-picker/locale/it_IT';
const typeTemplate = ' ${label} non è un ${type} valido';
const localeValues: Locale = {
locale: 'it',
Pagination,
@ -17,11 +20,17 @@ const localeValues: Locale = {
filterTitle: 'Menù Filtro',
filterConfirm: 'OK',
filterReset: 'Reset',
selectNone: 'Deseleziona tutto',
selectionAll: 'Seleziona tutto',
filterEmptyText: 'Senza filtri',
filterCheckall: 'Seleziona tutti',
filterSearchPlaceholder: 'Cerca nei filtri',
emptyText: 'Senza dati',
selectAll: 'Seleziona pagina corrente',
selectInvert: 'Inverti selezione nella pagina corrente',
selectNone: 'Deseleziona tutto',
selectionAll: 'Seleziona tutto',
sortTitle: 'Ordina',
expand: 'Espandi riga',
collapse: 'Comprimi riga ',
triggerDesc: 'Clicca per ordinare in modo discendente',
triggerAsc: 'Clicca per ordinare in modo ascendente',
cancelSort: "Clicca per eliminare l'ordinamento",
@ -36,16 +45,23 @@ const localeValues: Locale = {
cancelText: 'Annulla',
},
Transfer: {
titles: ['', ''],
searchPlaceholder: 'Cerca qui',
itemUnit: 'elemento',
itemsUnit: 'elementi',
remove: 'Elimina',
selectCurrent: 'Seleziona la pagina corrente',
removeCurrent: 'Rimuovi la pagina corrente',
selectAll: 'Seleziona tutti i dati',
removeAll: 'Rimuovi tutti i dati',
selectInvert: 'Inverti la pagina corrente',
},
Upload: {
uploading: 'Caricamento...',
removeFile: 'Rimuovi il file',
uploadError: 'Errore di caricamento',
previewFile: 'Anteprima file',
downloadFile: 'Download file',
downloadFile: 'Scarica file',
},
Empty: {
description: 'Nessun dato',
@ -59,6 +75,62 @@ const localeValues: Locale = {
copied: 'copia effettuata',
expand: 'espandi',
},
PageHeader: {
back: 'Torna',
},
Form: {
optional: '(opzionale)',
defaultValidateMessages: {
default: 'Errore di convalida del campo ${label}',
required: 'Si prega di inserire ${label}',
enum: '${label} deve essere uno di [${enum}]',
whitespace: '${label} non può essere un carattere vuoto',
date: {
format: 'Il formato della data ${label} non è valido',
parse: '${label} non può essere convertito in una data',
invalid: '${label} non è una data valida',
},
types: {
string: typeTemplate,
method: typeTemplate,
array: typeTemplate,
object: typeTemplate,
number: typeTemplate,
date: typeTemplate,
boolean: typeTemplate,
integer: typeTemplate,
float: typeTemplate,
regexp: typeTemplate,
email: typeTemplate,
url: typeTemplate,
hex: typeTemplate,
},
string: {
len: '${label} deve avere ${len} caratteri',
min: '${label} deve contenere almeno ${min} caratteri',
max: '${label} deve contenere fino a ${max} caratteri',
range: '${label} deve contenere tra ${min}-${max} caratteri',
},
number: {
len: '${label} deve essere uguale a ${len}',
min: '${label} valore minimo è ${min}',
max: '${label} valor e massimo è ${max}',
range: '${label} deve essere compreso tra ${min}-${max}',
},
array: {
len: 'Deve essere ${len} ${label}',
min: 'Almeno ${min} ${label}',
max: 'Massimo ${max} ${label}',
range: 'Il totale di ${label} deve essere compreso tra ${min}-${max}',
},
pattern: {
mismatch: '${label} non corrisponde al modello ${pattern}',
},
},
},
Image: {
preview: 'Anteprima',
},
};
export default localeValues;

View File

@ -1,6 +1,9 @@
import classNames from 'classnames';
import RcMentions from 'rc-mentions';
import type { MentionsProps as RcMentionsProps } from 'rc-mentions/lib/Mentions';
import type {
MentionsProps as RcMentionsProps,
MentionsRef as RcMentionsRef,
} from 'rc-mentions/lib/Mentions';
import { composeRef } from 'rc-util/lib/ref';
// eslint-disable-next-line import/no-named-as-default
import * as React from 'react';
@ -34,6 +37,8 @@ export interface MentionProps extends RcMentionsProps {
popupClassName?: string;
}
export interface MentionsRef extends RcMentionsRef {}
export interface MentionState {
focused: boolean;
}
@ -49,13 +54,13 @@ interface MentionsEntity {
}
interface CompoundedComponent
extends React.ForwardRefExoticComponent<MentionProps & React.RefAttributes<HTMLElement>> {
extends React.ForwardRefExoticComponent<MentionProps & React.RefAttributes<MentionsRef>> {
Option: typeof Option;
_InternalPanelDoNotUseOrYouWillBeFired: typeof PurePanel;
getMentions: (value: string, config?: MentionsConfig) => MentionsEntity[];
}
const InternalMentions: React.ForwardRefRenderFunction<unknown, MentionProps> = (
const InternalMentions: React.ForwardRefRenderFunction<MentionsRef, MentionProps> = (
{
prefixCls: customizePrefixCls,
className,
@ -71,7 +76,7 @@ const InternalMentions: React.ForwardRefRenderFunction<unknown, MentionProps> =
ref,
) => {
const [focused, setFocused] = React.useState(false);
const innerRef = React.useRef<HTMLElement>();
const innerRef = React.useRef<MentionsRef>();
const mergedRef = composeRef(ref, innerRef);
const { getPrefixCls, renderEmpty, direction } = React.useContext(ConfigContext);
const {
@ -176,7 +181,9 @@ const InternalMentions: React.ForwardRefRenderFunction<unknown, MentionProps> =
return wrapSSR(mentions);
};
const Mentions = React.forwardRef<unknown, MentionProps>(InternalMentions) as CompoundedComponent;
const Mentions = React.forwardRef<MentionsRef, MentionProps>(
InternalMentions,
) as CompoundedComponent;
if (process.env.NODE_ENV !== 'production') {
Mentions.displayName = 'Mentions';
}

View File

@ -23,6 +23,7 @@ import getIcons from './utils/iconUtil';
import useStyle from './style';
import genPurePanel from '../_util/PurePanel';
import warning from '../_util/warning';
import { useCompactItemContext } from '../space/Compact';
type RawValue = string | number;
@ -96,6 +97,7 @@ const InternalSelect = <OptionType extends BaseOptionType | DefaultOptionType =
const prefixCls = getPrefixCls('select', customizePrefixCls);
const rootPrefixCls = getPrefixCls();
const { compactSize, compactItemClassnames } = useCompactItemContext(prefixCls, direction);
const [wrapSSR, hashId] = useStyle(prefixCls);
@ -156,7 +158,7 @@ const InternalSelect = <OptionType extends BaseOptionType | DefaultOptionType =
hashId,
);
const mergedSize = customizeSize || size;
const mergedSize = compactSize || customizeSize || size;
// ===================== Disabled =====================
const disabled = React.useContext(DisabledContext);
@ -171,6 +173,7 @@ const InternalSelect = <OptionType extends BaseOptionType | DefaultOptionType =
[`${prefixCls}-in-form-item`]: isFormItemInput,
},
getStatusClassNames(prefixCls, mergedStatus, hasFeedback),
compactItemClassnames,
className,
hashId,
);

View File

@ -0,0 +1,123 @@
import classNames from 'classnames';
import toArray from 'rc-util/lib/Children/toArray';
import * as React from 'react';
import type { DirectionType } from '../config-provider';
import { ConfigContext } from '../config-provider';
import type { SizeType } from '../config-provider/SizeContext';
export interface SpaceCompactItemContextType {
compactSize?: SizeType;
compactDirection?: 'horizontal' | 'vertical';
isFirstItem?: boolean;
isLastItem?: boolean;
}
export const SpaceCompactItemContext = React.createContext<SpaceCompactItemContextType | null>(
null,
);
export const useCompactItemContext = (prefixCls: string, direction: DirectionType) => {
const compactItemContext = React.useContext(SpaceCompactItemContext);
const compactItemClassnames = React.useMemo(() => {
if (!compactItemContext) return '';
const { compactDirection, isFirstItem, isLastItem } = compactItemContext;
const separator = compactDirection === 'vertical' ? '-vertical-' : '-';
return classNames({
[`${prefixCls}-compact${separator}item`]: true,
[`${prefixCls}-compact${separator}first-item`]: isFirstItem,
[`${prefixCls}-compact${separator}last-item`]: isLastItem,
[`${prefixCls}-compact${separator}item-rtl`]: direction === 'rtl',
});
}, [prefixCls, direction, compactItemContext]);
return {
compactSize: compactItemContext?.compactSize,
compactDirection: compactItemContext?.compactDirection,
compactItemClassnames,
};
};
export const NoCompactStyle: React.FC<React.PropsWithChildren<{}>> = ({ children }) => (
<SpaceCompactItemContext.Provider value={null}>{children}</SpaceCompactItemContext.Provider>
);
export interface SpaceCompactProps extends React.HTMLAttributes<HTMLDivElement> {
prefixCls?: string;
size?: SizeType;
direction?: 'horizontal' | 'vertical';
block?: boolean;
}
const CompactItem: React.FC<React.PropsWithChildren<SpaceCompactItemContextType>> = ({
children,
...otherProps
}) => (
<SpaceCompactItemContext.Provider value={otherProps}>{children}</SpaceCompactItemContext.Provider>
);
const Compact: React.FC<SpaceCompactProps> = props => {
const { getPrefixCls, direction: directionConfig } = React.useContext(ConfigContext);
const {
size = 'middle',
direction,
block,
prefixCls: customizePrefixCls,
className,
children,
...restProps
} = props;
const prefixCls = getPrefixCls('space-compact', customizePrefixCls);
const clx = classNames(
prefixCls,
{
[`${prefixCls}-rtl`]: directionConfig === 'rtl',
[`${prefixCls}-block`]: block,
[`${prefixCls}-vertical`]: direction === 'vertical',
},
className,
);
const compactItemContext = React.useContext(SpaceCompactItemContext);
const childNodes = toArray(children);
const nodes = React.useMemo(
() =>
childNodes.map((child, i) => {
const key = (child && child.key) || `${prefixCls}-item-${i}`;
return (
<CompactItem
key={key}
compactSize={size}
compactDirection={direction}
isFirstItem={i === 0 && (!compactItemContext || compactItemContext?.isFirstItem)}
isLastItem={
i === childNodes.length - 1 && (!compactItemContext || compactItemContext?.isLastItem)
}
>
{child}
</CompactItem>
);
}),
[size, childNodes, compactItemContext],
);
// =========================== Render ===========================
if (childNodes.length === 0) {
return null;
}
return (
<div className={clx} {...restProps}>
{nodes}
</div>
);
};
export default Compact;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Space.Compact rtl render component should be rendered correctly in RTL direction 1`] = `null`;
exports[`Space.Compact rtl render component should be rendered correctly in RTL direction 2`] = `
<div
class="ant-space-compact ant-space-compact-rtl"
>
<button
class="ant-btn ant-btn-primary ant-btn-rtl ant-btn-compact-item ant-btn-compact-first-item ant-btn-compact-last-item ant-btn-compact-item-rtl"
type="button"
>
<span>
Submit
</span>
</button>
</div>
`;

View File

@ -0,0 +1,185 @@
/* eslint-disable no-console */
import React from 'react';
import Space from '..';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { render } from '../../../tests/utils';
import Input from '../../input';
import Button from '../../button';
import AutoComplete from '../../auto-complete';
import Cascader from '../../cascader';
import DatePicker from '../../date-picker';
import Select from '../../select';
import TimePicker from '../../time-picker';
import TreeSelect from '../../tree-select';
describe('Space.Compact', () => {
mountTest(Space.Compact);
mountTest(() => (
<Space.Compact>
<Button type="primary">Submit</Button>
</Space.Compact>
));
rtlTest(Space.Compact);
rtlTest(() => (
<Space.Compact>
<Button type="primary">Submit</Button>
</Space.Compact>
));
it('should render width empty children', () => {
const { container } = render(<Space.Compact />);
expect(container.children.length).toBe(0);
});
it('block className', () => {
const { container } = render(
<Space.Compact block>
<Input defaultValue="https://ant.design" />
<Button type="primary">Submit</Button>
</Space.Compact>,
);
expect(
container.querySelector('.ant-space-compact')?.classList.contains('ant-space-compact-block'),
).toBe(true);
});
it('compact-item className', () => {
const { container } = render(
<Space.Compact>
<Input defaultValue="https://ant.design" />
<Input.Search />
<Button type="primary">Submit</Button>
</Space.Compact>,
);
expect(
container.querySelector('.ant-input')?.classList.contains('ant-input-compact-item'),
).toBe(true);
expect(
container.querySelector('.ant-input-search')?.classList.contains('ant-input-compact-item'),
).toBe(true);
expect(
container.querySelector('.ant-input')?.classList.contains('ant-input-compact-first-item'),
).toBe(true);
expect(
container
.querySelector('.ant-btn-compact-item')
?.classList.contains('ant-btn-compact-last-item'),
).toBe(true);
});
[
{
name: 'Button',
component: Button,
targetCls: 'ant-btn',
expectClsPrefix: 'ant-btn',
},
{
name: 'AutoComplete',
component: AutoComplete,
targetCls: 'ant-select',
expectClsPrefix: 'ant-select',
},
{
name: 'Cascader',
component: Cascader,
targetCls: 'ant-cascader',
expectClsPrefix: 'ant-select',
},
{
name: 'DatePicker',
component: DatePicker,
targetCls: 'ant-picker',
expectClsPrefix: 'ant-picker',
},
{
name: 'Input',
component: Input,
targetCls: 'ant-input',
expectClsPrefix: 'ant-input',
},
{
name: 'Input.Search',
component: Input.Search,
targetCls: 'ant-input-search',
expectClsPrefix: 'ant-input',
},
{
name: 'Select',
component: Select,
targetCls: 'ant-select',
expectClsPrefix: 'ant-select',
},
{
name: 'TimePicker',
component: TimePicker,
targetCls: 'ant-picker',
expectClsPrefix: 'ant-picker',
},
{
name: 'TreeSelect',
component: TreeSelect,
targetCls: 'ant-select',
expectClsPrefix: 'ant-select',
},
].forEach(({ component, name, targetCls, expectClsPrefix }) => {
it(`compact-item for ${name}`, () => {
const { container } = render(
<Space.Compact>{React.createElement(component as any)}</Space.Compact>,
);
expect(container.querySelectorAll(`.${targetCls}`).length).toBe(1);
['compact-item', 'compact-first-item', 'compact-last-item'].forEach(suffix => {
expect(
container
.querySelector(`.${targetCls}`)
?.classList.contains([expectClsPrefix, suffix].join('-')),
).toBe(true);
});
});
});
it('size', () => {
const { container } = render(
<Space.Compact size="small">
<Input defaultValue="https://ant.design" />
<Button type="primary">Submit</Button>
</Space.Compact>,
);
expect(container.querySelector('.ant-input')?.classList.contains('ant-input-sm')).toBe(true);
expect(container.querySelector('.ant-btn')?.classList.contains('ant-btn-sm')).toBe(true);
});
it('direction=vertical', () => {
const { container } = render(
<Space.Compact size="small" direction="vertical">
<Button type="primary">Button 1</Button>
<Button type="primary">Button 2</Button>
<Button type="primary">Button 3</Button>
<Button type="primary">Button 4</Button>
</Space.Compact>,
);
expect(
container
.querySelector('.ant-space-compact')
?.classList.contains('ant-space-compact-vertical'),
).toBe(true);
expect(
container.querySelector('.ant-btn')?.classList.contains('ant-btn-compact-vertical-item'),
).toBe(true);
expect(
container
.querySelectorAll('.ant-btn')[0]
?.classList.contains('ant-btn-compact-vertical-first-item'),
).toBe(true);
expect(
container
.querySelectorAll('.ant-btn')[3]
?.classList.contains('ant-btn-compact-vertical-last-item'),
).toBe(true);
});
});

View File

@ -0,0 +1,42 @@
---
order: 10
version: 4.24.0
title:
zh-CN: 垂直方向紧凑布局
en-US: Vertical Compact Mode
---
## zh-CN
垂直方向的紧凑布局,目前仅支持 Button 组合。
## en-US
Vertical Mode for Space.Compact, support Button only.
```tsx
import { Button, Space } from 'antd';
import React from 'react';
const App: React.FC = () => (
<Space>
<Space.Compact direction="vertical">
<Button>Button 1</Button>
<Button>Button 2</Button>
<Button>Button 3</Button>
</Space.Compact>
<Space.Compact direction="vertical">
<Button type="dashed">Button 1</Button>
<Button type="dashed">Button 2</Button>
<Button type="dashed">Button 3</Button>
</Space.Compact>
<Space.Compact direction="vertical">
<Button type="primary">Button 1</Button>
<Button type="primary">Button 2</Button>
<Button type="primary">Button 3</Button>
</Space.Compact>
</Space>
);
export default App;
```

View File

@ -0,0 +1,136 @@
---
order: 9
version: 4.24.0
title:
zh-CN: Button 紧凑布局
en-US: Button Compact Mode
---
## zh-CN
Button 组件紧凑排列的示例。
## en-US
Button component compact example.
```tsx
import {
DownloadOutlined,
EllipsisOutlined,
HeartOutlined,
LikeOutlined,
CommentOutlined,
StarOutlined,
ShareAltOutlined,
WarningOutlined,
MailOutlined,
MobileOutlined,
} from '@ant-design/icons';
import { Button, Menu, Dropdown, Space, Tooltip } from 'antd';
import React from 'react';
const App: React.FC = () => (
<div>
<Space.Compact block>
<Tooltip title="Like">
<Button icon={<LikeOutlined />} />
</Tooltip>
<Tooltip title="Comment">
<Button icon={<CommentOutlined />} />
</Tooltip>
<Tooltip title="Star">
<Button icon={<StarOutlined />} />
</Tooltip>
<Tooltip title="Heart">
<Button icon={<HeartOutlined />} />
</Tooltip>
<Tooltip title="Share">
<Button icon={<ShareAltOutlined />} />
</Tooltip>
<Tooltip title="Download">
<Button icon={<DownloadOutlined />} />
</Tooltip>
<Dropdown
placement="bottomRight"
overlay={
<Menu
items={[
{
key: '1',
label: 'Report',
icon: <WarningOutlined />,
},
{
key: '2',
label: 'Mail',
icon: <MailOutlined />,
},
{
key: '3',
label: 'Mobile',
icon: <MobileOutlined />,
},
]}
/>
}
trigger={['click']}
>
<Button icon={<EllipsisOutlined />} />
</Dropdown>
</Space.Compact>
<br />
<Space.Compact block>
<Button type="primary">Button 1</Button>
<Button type="primary">Button 2</Button>
<Button type="primary">Button 3</Button>
<Button type="primary">Button 4</Button>
<Tooltip title="Tooltip">
<Button type="primary" icon={<DownloadOutlined />} disabled />
</Tooltip>
<Tooltip title="Tooltip">
<Button type="primary" icon={<DownloadOutlined />} />
</Tooltip>
</Space.Compact>
<br />
<Space.Compact block>
<Button>Button 1</Button>
<Button>Button 2</Button>
<Button>Button 3</Button>
<Tooltip title="Tooltip">
<Button icon={<DownloadOutlined />} disabled />
</Tooltip>
<Tooltip title="Tooltip">
<Button icon={<DownloadOutlined />} />
</Tooltip>
<Button type="primary">Button 4</Button>
<Dropdown
placement="bottomRight"
overlay={
<Menu
items={[
{
key: '1',
label: '1st item',
},
{
key: '2',
label: '2nd item',
},
{
key: '3',
label: '3rd item',
},
]}
/>
}
trigger={['click']}
>
<Button type="primary" icon={<EllipsisOutlined />} />
</Dropdown>
</Space.Compact>
</div>
);
export default App;
```

View File

@ -0,0 +1,107 @@
---
order: 99
version: 4.24.0
title:
zh-CN: 调试 Input 前置/后置标签
en-US: Input addon debug
debug: true
---
## zh-CN
调试 Input 前置/后置标签。
## en-US
Input addon debug.
```tsx
import { SettingOutlined, CopyOutlined, DownloadOutlined } from '@ant-design/icons';
import { Cascader, Input, Select, Space, Button, Tooltip } from 'antd';
import React from 'react';
const { Option } = Select;
const selectBefore = (
<Select defaultValue="http://" className="select-before">
<Option value="http://">http://</Option>
<Option value="https://">https://</Option>
</Select>
);
const selectAfter = (
<Select defaultValue=".com" className="select-after">
<Option value=".com">.com</Option>
<Option value=".jp">.jp</Option>
<Option value=".cn">.cn</Option>
<Option value=".org">.org</Option>
</Select>
);
const App: React.FC = () => (
<Space direction="vertical">
<Space.Compact block>
<Button>default Button</Button>
<Button danger>danger Button</Button>
<Button type="dashed">dashed Button</Button>
<Button type="text">text Button</Button>
<Button type="link">Link Button</Button>
<Tooltip title="Tooltip">
<Button icon={<DownloadOutlined />} disabled />
</Tooltip>
</Space.Compact>
<br />
<Space.Compact>
<Button>Prefix</Button>
<Input addonBefore="http://" addonAfter=".com" defaultValue="mysite" />
<Button type="primary">Submit</Button>
</Space.Compact>
<Space.Compact>
<Input placeholder="prefix" />
<Input addonBefore={selectBefore} addonAfter={selectAfter} defaultValue="mysite" />
<Button icon={<CopyOutlined />} />
</Space.Compact>
<Space.Compact>
<Input.Search />
<Input.Search />
<Button icon={<CopyOutlined />} />
</Space.Compact>
<Space.Compact>
<Input addonAfter={<SettingOutlined />} defaultValue="mysite" />
<Button type="primary">Submit</Button>
<Input placeholder="suffix" addonAfter={<SettingOutlined />} />
</Space.Compact>
<Space.Compact>
<Input addonBefore="http://" suffix=".com" defaultValue="mysite" />
<Button type="primary">Submit</Button>
</Space.Compact>
<Space.Compact>
<Button>Prefix</Button>
<Input
addonBefore={<Cascader placeholder="cascader" style={{ width: 150 }} />}
defaultValue="mysite"
/>
<Button type="primary">Submit</Button>
</Space.Compact>
</Space>
);
export default App;
```
```css
.select-before {
width: 90px;
}
.select-after {
width: 80px;
}
[data-theme='compact'] .select-before {
width: 71px;
}
[data-theme='compact'] .select-after {
width: 65px;
}
```

View File

@ -0,0 +1,116 @@
---
order: 99
version: 4.24.0
title:
zh-CN: 紧凑布局嵌套
en-US: Nested Space Compact
debug: true
---
## zh-CN
嵌套使用的紧凑布局
## en-US
Nested `Space.Compact`
```tsx
import { CopyOutlined, SearchOutlined } from '@ant-design/icons';
import { Button, Cascader, Input, InputNumber, Space, Select, TimePicker } from 'antd';
import React from 'react';
const { Option } = Select;
const App: React.FC = () => (
<>
<Space.Compact block>
<Space.Compact>
<Space.Compact>
<Input style={{ width: 90 }} placeholder="Typing..." />
<Button icon={<SearchOutlined />} />
</Space.Compact>
<Space.Compact>
<InputNumber defaultValue={12} />
<Select defaultValue="Option1">
<Option value="Option1">Opt1</Option>
<Option value="Option2">Opt2</Option>
</Select>
</Space.Compact>
</Space.Compact>
<Button type="primary">Separator</Button>
<Space.Compact>
<Space.Compact>
<Input.Search style={{ width: 110 }} placeholder="Search" />
<Button type="primary">Submit</Button>
</Space.Compact>
<Space.Compact>
<Input defaultValue="mysite" />
<Button icon={<CopyOutlined />} />
</Space.Compact>
</Space.Compact>
</Space.Compact>
<>
<br />
<Space.Compact block>
<Space.Compact>
<TimePicker />
<Button type="primary">Submit</Button>
</Space.Compact>
<Space.Compact>
<Cascader
options={[
{
value: 'zhejiang',
label: 'Zhejiang',
children: [
{
value: 'hangzhou',
label: 'Hangzhou',
children: [
{
value: 'xihu',
label: 'West Lake',
},
],
},
],
},
{
value: 'jiangsu',
label: 'Jiangsu',
children: [
{
value: 'nanjing',
label: 'Nanjing',
children: [
{
value: 'zhonghuamen',
label: 'Zhong Hua Men',
},
],
},
],
},
]}
placeholder="Select Address"
/>
<Button type="primary">Submit</Button>
</Space.Compact>
</Space.Compact>
</>
</>
);
export default App;
```
```css
[data-theme='compact'] .select-before {
width: 71px;
}
[data-theme='compact'] .select-after {
width: 65px;
}
```

View File

@ -0,0 +1,256 @@
---
order: 8
version: 4.24.0
title:
zh-CN: 紧凑布局组合
en-US: Compact Mode for form component
---
## zh-CN
使用 Space.Compact 让表单组件之间紧凑连接且合并边框。
## en-US
Compact Mode for form component.
```tsx
import { CopyOutlined } from '@ant-design/icons';
import {
AutoComplete,
Button,
Cascader,
DatePicker,
Input,
InputNumber,
Select,
Space,
TimePicker,
Tooltip,
TreeSelect,
} from 'antd';
import React from 'react';
const { Option } = Select;
const { TreeNode } = TreeSelect;
const App: React.FC = () => (
<div className="site-space-compact-wrapper">
<Space.Compact block>
<Input style={{ width: '20%' }} defaultValue="0571" />
<Input style={{ width: '30%' }} defaultValue="26888888" />
</Space.Compact>
<br />
<Space.Compact block size="small">
<Input style={{ width: 'calc(100% - 200px)' }} defaultValue="https://ant.design" />
<Button type="primary">Submit</Button>
</Space.Compact>
<br />
<Space.Compact block>
<Input style={{ width: 'calc(100% - 200px)' }} defaultValue="https://ant.design" />
<Button type="primary">Submit</Button>
</Space.Compact>
<br />
<Space.Compact block>
<Input
style={{ width: 'calc(100% - 200px)' }}
defaultValue="git@github.com:ant-design/ant-design.git"
/>
<Tooltip title="copy git url">
<Button icon={<CopyOutlined />} />
</Tooltip>
</Space.Compact>
<br />
<Space.Compact block>
<Select defaultValue="Zhejiang">
<Option value="Zhejiang">Zhejiang</Option>
<Option value="Jiangsu">Jiangsu</Option>
</Select>
<Input style={{ width: '50%' }} defaultValue="Xihu District, Hangzhou" />
</Space.Compact>
<br />
<Space.Compact block>
<Select mode="multiple" defaultValue="Zhejianggggg" style={{ width: '50%' }}>
<Option value="Zhejianggggg">Zhejianggggg</Option>
<Option value="Jiangsu">Jiangsu</Option>
</Select>
<Input style={{ width: '50%' }} defaultValue="Xihu District, Hangzhou" />
</Space.Compact>
<br />
<Space.Compact block>
<Input.Search style={{ width: '30%' }} defaultValue="0571" />
<Input.Search allowClear style={{ width: '50%' }} defaultValue="26888888" />
<Input.Search style={{ width: '20%' }} defaultValue="+1" />
</Space.Compact>
<br />
<Space.Compact block>
<Select defaultValue="Option1">
<Option value="Option1">Option1</Option>
<Option value="Option2">Option2</Option>
</Select>
<Input style={{ width: '50%' }} defaultValue="input content" />
<InputNumber defaultValue={12} />
</Space.Compact>
<br />
<Space.Compact block>
<Input style={{ width: '50%' }} defaultValue="input content" />
<DatePicker style={{ width: '50%' }} />
</Space.Compact>
<br />
<Space.Compact block>
<DatePicker.RangePicker style={{ width: '70%' }} />
<Input style={{ width: '30%' }} defaultValue="input content" />
<Button type="primary">查询</Button>
</Space.Compact>
<br />
<Space.Compact block>
<Input style={{ width: '30%' }} defaultValue="input content" />
<DatePicker.RangePicker style={{ width: '70%' }} />
</Space.Compact>
<br />
<Space.Compact block>
<Select defaultValue="Option1-1">
<Option value="Option1-1">Option1-1</Option>
<Option value="Option1-2">Option1-2</Option>
</Select>
<Select defaultValue="Option2-2">
<Option value="Option2-1">Option2-1</Option>
<Option value="Option2-2">Option2-2</Option>
</Select>
</Space.Compact>
<br />
<Space.Compact block>
<Select defaultValue="1">
<Option value="1">Between</Option>
<Option value="2">Except</Option>
</Select>
<Input style={{ width: 100, textAlign: 'center' }} placeholder="Minimum" />
<Input
className="site-input-split"
style={{
width: 30,
borderLeft: 0,
borderRight: 0,
pointerEvents: 'none',
}}
placeholder="~"
disabled
/>
<Input
className="site-input-right"
style={{
width: 100,
textAlign: 'center',
}}
placeholder="Maximum"
/>
</Space.Compact>
<br />
<Space.Compact block>
<Select defaultValue="Sign Up" style={{ width: '30%' }}>
<Option value="Sign Up">Sign Up</Option>
<Option value="Sign In">Sign In</Option>
</Select>
<AutoComplete
style={{ width: '70%' }}
placeholder="Email"
options={[{ value: 'text 1' }, { value: 'text 2' }]}
/>
</Space.Compact>
<br />
<Space.Compact block>
<TimePicker style={{ width: '70%' }} />
<Cascader
style={{ width: '70%' }}
options={[
{
value: 'zhejiang',
label: 'Zhejiang',
children: [
{
value: 'hangzhou',
label: 'Hangzhou',
children: [
{
value: 'xihu',
label: 'West Lake',
},
],
},
],
},
{
value: 'jiangsu',
label: 'Jiangsu',
children: [
{
value: 'nanjing',
label: 'Nanjing',
children: [
{
value: 'zhonghuamen',
label: 'Zhong Hua Men',
},
],
},
],
},
]}
placeholder="Select Address"
/>
</Space.Compact>
<br />
<Space.Compact block>
<TimePicker.RangePicker />
<TreeSelect
showSearch
style={{ width: '60%' }}
value="leaf1"
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
placeholder="Please select"
allowClear
treeDefaultExpandAll
onChange={() => {}}
>
<TreeNode value="parent 1" title="parent 1">
<TreeNode value="parent 1-0" title="parent 1-0">
<TreeNode value="leaf1" title="leaf1" />
<TreeNode value="leaf2" title="leaf2" />
</TreeNode>
<TreeNode value="parent 1-1" title="parent 1-1">
<TreeNode value="leaf3" title={<b style={{ color: '#08c' }}>leaf3</b>} />
</TreeNode>
</TreeNode>
</TreeSelect>
<Button type="primary">Submit</Button>
</Space.Compact>
<br />
</div>
);
export default App;
```
```css
.site-space-compact-wrapper .site-input-split {
background-color: #fff;
}
.site-space-compact-wrapper .site-input-right:not(.ant-input-rtl) {
border-left-width: 0;
}
.site-space-compact-wrapper .site-input-right:not(.ant-input-rtl):hover,
.site-space-compact-wrapper .site-input-right:not(.ant-input-rtl):focus {
border-left-width: 1px;
}
.site-space-compact-wrapper .site-input-right.ant-input-rtl {
border-right-width: 0;
}
.site-space-compact-wrapper .site-input-right.ant-input-rtl:hover,
.site-space-compact-wrapper .site-input-right.ant-input-rtl:focus {
border-right-width: 1px;
}
```

View File

@ -1,5 +1,5 @@
---
order: 3
order: 5
title:
zh-CN: 自定义尺寸
en-US: Customize Size

View File

@ -1,5 +1,5 @@
---
order: 99
order: 7
title:
zh-CN: 分隔符
en-US: Split

View File

@ -1,5 +1,5 @@
---
order: 98
order: 6
title:
zh-CN: 自动换行
en-US: Wrap

View File

@ -10,7 +10,8 @@ Set components spacing.
## When To Use
Avoid components clinging together and set a unified space.
- Avoid components clinging together and set a unified space.
- Use Space.Compact when child form components are compactly connected and the border is collapsed.
## API
@ -25,3 +26,22 @@ Avoid components clinging together and set a unified space.
### Size
`'small' | 'middle' | 'large' | number`
### Space.Compact
Use Space.Compact when child form components are compactly connected and the border is collapsed. The supported components are
- Button
- AutoComplete
- Cascader
- DatePicker
- Input/Input.Search
- Select
- TimePicker
- TreeSelect
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| block | Option to fit width to its parent\'s width | boolean | false | 4.24.0 |
| direction | Set direction of layout | `vertical` \| `horizontal` | `horizontal` | 4.24.0 |
| size | Set child component size | `large` \| `middle` \| `small` | `middle` | 4.24.0 |

View File

@ -5,6 +5,7 @@ import { ConfigContext } from '../config-provider';
import type { SizeType } from '../config-provider/SizeContext';
import useFlexGapSupport from '../_util/hooks/useFlexGapSupport';
import Item from './Item';
import Compact from './Compact';
import useStyle from './style';
@ -150,4 +151,11 @@ const Space: React.FC<SpaceProps> = props => {
);
};
export default Space;
interface CompoundedComponent extends React.FC<SpaceProps> {
Compact: typeof Compact;
}
const CompoundedSpace = Space as CompoundedComponent;
CompoundedSpace.Compact = Compact;
export default CompoundedSpace;

View File

@ -15,9 +15,12 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/wc6%263gJ0Y8/Space.svg
- 适合行内元素的水平间距。
- 可以设置各种水平对齐方式。
- 需要表单组件之间紧凑连接且合并边框时,使用 Space.Compact自 antd@4.24.0 版本开始提供该组件。)。
## API
### Space
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| align | 对齐方式 | `start` \| `end` \|`center` \|`baseline` | - | 4.2.0 |
@ -29,3 +32,24 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/wc6%263gJ0Y8/Space.svg
### Size
`'small' | 'middle' | 'large' | number`
### Space.Compact
> 自 antd@4.24.0 版本开始提供该组件。
需要表单组件之间紧凑连接且合并边框时,使用 Space.Compact。支持的组件有
- Button
- AutoComplete
- Cascader
- DatePicker
- Input/Input.Search
- Select
- TimePicker
- TreeSelect
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| block | 将宽度调整为父元素宽度的选项 | boolean | false | 4.24.0 |
| direction | 指定排列方向 | `vertical` \| `horizontal` | `horizontal` | 4.24.0 |
| size | 子组件大小 | `large` \| `middle` \| `small` | `middle` | 4.24.0 |

View File

@ -0,0 +1,17 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@space-compact-prefix-cls: ~'@{ant-prefix}-space-compact';
.@{space-compact-prefix-cls} {
display: inline-flex;
&-block {
display: flex;
width: 100%;
}
&-vertical {
flex-direction: column;
}
}

View File

@ -0,0 +1,51 @@
.compact-item-vertical-border-radius(@prefix-cls) {
&-item:not(&-first-item):not(&-last-item) {
border-radius: 0;
}
&-item&-first-item {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
.@{prefix-cls}-sm & {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
}
&-item&-last-item {
border-top-left-radius: 0;
border-top-right-radius: 0;
.@{prefix-cls}-sm & {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
}
}
.compact-item-vertical-border(@prefix-cls) {
// border collapse
&-item:not(&-last-item) {
margin-bottom: -@border-width-base;
}
&-item {
&:hover,
&:focus,
&:active {
z-index: 2;
}
&[disabled] {
z-index: 0;
}
}
}
.compact-item-vertical(@prefix-cls) {
&-compact-vertical {
.compact-item-vertical-border(@prefix-cls);
.compact-item-vertical-border-radius(@prefix-cls);
}
}

View File

@ -0,0 +1,170 @@
.compact-item-border-radius(@prefix-cls, @bordered-item-cls: null) {
& when (@bordered-item-cls = null) {
// border-radius
&-item:not(&-first-item):not(&-last-item).@{prefix-cls} {
border-radius: 0;
}
&-item.@{prefix-cls}&-first-item:not(&-item-rtl) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
.@{prefix-cls}-sm & {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
&-item.@{prefix-cls}&-last-item:not(&-item-rtl) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
.@{prefix-cls}-sm & {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
// ----------rtl for first item----------
&-item.@{prefix-cls}&-item-rtl&-first-item {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
.@{prefix-cls}-sm & {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
// ----------rtl for last item----------
&-item.@{prefix-cls}&-item-rtl&-last-item {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
.@{prefix-cls}-sm & {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
}
& when (not (@bordered-item-cls = null)) {
// border-radius
&-item:not(&-first-item):not(&-last-item).@{prefix-cls} > .@{bordered-item-cls} {
border-radius: 0;
}
&-item&-first-item.@{prefix-cls}:not(&-item-rtl) > .@{bordered-item-cls} {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
.@{prefix-cls}-sm & {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
&-item&-last-item.@{prefix-cls}:not(&-item-rtl) > .@{bordered-item-cls} {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
.@{prefix-cls}-sm & {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
// ----------rtl for first item----------
&-item.@{prefix-cls}&-first-item&-item-rtl > .@{bordered-item-cls} {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
.@{prefix-cls}-sm & {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
// ----------rtl for last item----------
&-item.@{prefix-cls}&-last-item&-item-rtl > .@{bordered-item-cls} {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
.@{prefix-cls}-sm & {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
}
}
.compact-item-border(@prefix-cls, @bordered-item-cls: null, @special-open-cls) {
& when (@bordered-item-cls = null) {
// border collapse
&-item:not(&-last-item):not(&-item-rtl) {
margin-right: -@border-width-base;
}
// rtl border collapse
&-item:not(&-last-item)&-item-rtl {
margin-left: -@border-width-base;
}
&-item {
&:hover,
&:focus,
&:active {
z-index: 2;
}
// Select has an extra focus className
& when (not (@special-item-cls = null)) {
&.@{special-item-cls} {
z-index: 2;
}
}
&[disabled] {
z-index: 0;
}
}
}
& when (not (@bordered-item-cls = null)) {
// border collapse
&-item:not(&-last-item) {
margin-right: -@border-width-base;
&.@{prefix-cls}-compact-item-rtl {
margin-right: 0;
margin-left: -@border-width-base;
}
}
&-item {
&:hover,
&:focus,
&:active {
> * {
z-index: 2;
}
}
// Select has an special focus-item
& when (not (@special-item-cls = null)) {
&.@{special-item-cls} > * {
z-index: 2;
}
}
&[disabled] > * {
z-index: 0;
}
}
}
}
.compact-item(@prefix-cls, @bordered-item-cls: null, @special-item-cls: null) {
&-compact {
.compact-item-border(@prefix-cls, @bordered-item-cls, @special-item-cls);
.compact-item-border-radius(@prefix-cls, @bordered-item-cls);
}
}

View File

@ -22,6 +22,7 @@ import type { SelectCommonPlacement } from '../_util/motion';
import { getTransitionDirection, getTransitionName } from '../_util/motion';
import type { InputStatus } from '../_util/statusUtils';
import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils';
import { useCompactItemContext } from '../space/Compact';
import warning from '../_util/warning';
import useStyle from './style';
@ -119,6 +120,7 @@ const InternalTreeSelect = <OptionType extends BaseOptionType | DefaultOptionTyp
const prefixCls = getPrefixCls('select', customizePrefixCls);
const treePrefixCls = getPrefixCls('select-tree', customizePrefixCls);
const treeSelectPrefixCls = getPrefixCls('tree-select', customizePrefixCls);
const { compactSize, compactItemClassnames } = useCompactItemContext(prefixCls, direction);
const [wrapSelectSSR, hashId] = useSelectStyle(prefixCls);
const [wrapTreeSelectSSR] = useStyle(treeSelectPrefixCls, treePrefixCls);
@ -181,7 +183,7 @@ const InternalTreeSelect = <OptionType extends BaseOptionType | DefaultOptionTyp
: ('bottomLeft' as SelectCommonPlacement);
};
const mergedSize = customizeSize || size;
const mergedSize = compactSize || customizeSize || size;
// ===================== Disabled =====================
const disabled = React.useContext(DisabledContext);
const mergedDisabled = customDisabled ?? disabled;
@ -196,6 +198,7 @@ const InternalTreeSelect = <OptionType extends BaseOptionType | DefaultOptionTyp
[`${prefixCls}-in-form-item`]: isFormItemInput,
},
getStatusClassNames(prefixCls, mergedStatus, hasFeedback),
compactItemClassnames,
className,
hashId,
);

View File

@ -36,6 +36,7 @@ interface CopyConfig {
}
interface EditConfig {
text?: string;
editing?: boolean;
icon?: React.ReactNode;
tooltip?: boolean | React.ReactNode;
@ -339,11 +340,11 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
// ========================== Tooltip ===========================
let tooltipProps: TooltipProps = {};
if (ellipsisConfig.tooltip === true) {
tooltipProps = { title: children };
tooltipProps = { title: editConfig.text ?? children };
} else if (React.isValidElement(ellipsisConfig.tooltip)) {
tooltipProps = { title: ellipsisConfig.tooltip };
} else if (typeof ellipsisConfig.tooltip === 'object') {
tooltipProps = { title: children, ...ellipsisConfig.tooltip };
tooltipProps = { title: editConfig.text ?? children, ...ellipsisConfig.tooltip };
} else {
tooltipProps = { title: ellipsisConfig.tooltip };
}
@ -354,6 +355,10 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
return undefined;
}
if (isValid(editConfig.text)) {
return editConfig.text;
}
if (isValid(children)) {
return children;
}
@ -374,7 +379,7 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
if (editing) {
return (
<Editable
value={typeof children === 'string' ? children : ''}
value={editConfig.text ?? (typeof children === 'string' ? children : '')}
onSave={onEditChange}
onCancel={onEditCancel}
onEnd={editConfig.onEnd}

View File

@ -1175,6 +1175,133 @@ Array [
</div>
</div>
</div>,
<div
aria-label="This is a loooooooooooooooooooooooooooooooong editable text with suffix."
class="ant-typography ant-typography-ellipsis ant-typography-single-line"
>
This is a loooooooooooooooooooooooooooooooong editable text
<!-- -->
with suffix.
<div
aria-label="Edit"
class="ant-typography-edit"
role="button"
style="border:0;background:transparent;padding:0;line-height:inherit;display:inline-block"
tabindex="0"
>
<span
aria-label="edit"
class="anticon anticon-edit"
role="button"
>
<svg
aria-hidden="true"
data-icon="edit"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z"
/>
</svg>
</span>
</div>
<div>
<div
class="ant-tooltip"
style="opacity:0"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
>
Edit
</div>
</div>
</div>
</div>
<span
aria-hidden="true"
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;font-size:0;word-break:keep-all;white-space:nowrap"
>
lg
</span>
<span
aria-hidden="true"
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;font-size:0;width:0;white-space:normal;margin:0;padding:0"
>
<span
aria-hidden="true"
>
...
</span>
with suffix.
<div
aria-label="Edit"
class="ant-typography-edit"
role="button"
style="border:0;background:transparent;padding:0;line-height:inherit;display:inline-block"
tabindex="0"
>
<span
aria-label="edit"
class="anticon anticon-edit"
role="button"
>
<svg
aria-hidden="true"
data-icon="edit"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z"
/>
</svg>
</span>
</div>
<div>
<div
class="ant-tooltip"
style="opacity:0"
>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-arrow"
>
<span
class="ant-tooltip-arrow-content"
/>
</div>
<div
class="ant-tooltip-inner"
role="tooltip"
>
Edit
</div>
</div>
</div>
</div>
</span>
</div>,
<div
class="ant-typography"
>
@ -1231,9 +1358,7 @@ Array [
</div>
</div>
</div>,
Trigger edit with:,
<!-- -->,
,
Trigger edit with: ,
<div
class="ant-radio-group ant-radio-group-outline"
>

View File

@ -887,6 +887,85 @@ Array [
</span>
</div>
</div>,
<div
aria-label="This is a loooooooooooooooooooooooooooooooong editable text with suffix."
class="ant-typography ant-typography-ellipsis ant-typography-single-line"
>
This is a loooooooooooooooooooooooooooooooong editable text
<!-- -->
with suffix.
<div
aria-label="Edit"
class="ant-typography-edit"
role="button"
style="border:0;background:transparent;padding:0;line-height:inherit;display:inline-block"
tabindex="0"
>
<span
aria-label="edit"
class="anticon anticon-edit"
role="button"
>
<svg
aria-hidden="true"
data-icon="edit"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z"
/>
</svg>
</span>
</div>
<span
aria-hidden="true"
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;font-size:0;word-break:keep-all;white-space:nowrap"
>
lg
</span>
<span
aria-hidden="true"
style="position:fixed;display:block;left:0;top:0;z-index:-9999;visibility:hidden;pointer-events:none;font-size:0;width:0;white-space:normal;margin:0;padding:0"
>
<span
aria-hidden="true"
>
...
</span>
with suffix.
<div
aria-label="Edit"
class="ant-typography-edit"
role="button"
style="border:0;background:transparent;padding:0;line-height:inherit;display:inline-block"
tabindex="0"
>
<span
aria-label="edit"
class="anticon anticon-edit"
role="button"
>
<svg
aria-hidden="true"
data-icon="edit"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z"
/>
</svg>
</span>
</div>
</span>
</div>,
<div
class="ant-typography"
>
@ -919,9 +998,7 @@ Array [
</span>
</div>
</div>,
Trigger edit with:,
<!-- -->,
,
Trigger edit with: ,
<div
class="ant-radio-group ant-radio-group-outline"
>

View File

@ -0,0 +1,81 @@
import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
import React from 'react';
import { fireEvent, render } from '../../../tests/utils';
import Base from '../Base';
// eslint-disable-next-line no-unused-vars
jest.mock('copy-to-clipboard');
jest.mock('../../_util/styleChecker', () => ({
isStyleSupport: () => true,
}));
describe('Typography.Editable', () => {
const LINE_STR_COUNT = 20;
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
let mockRectSpy: ReturnType<typeof spyElementPrototypes>;
beforeAll(() => {
mockRectSpy = spyElementPrototypes(HTMLElement, {
offsetHeight: {
get() {
let html = this.innerHTML;
html = html.replace(/<[^>]*>/g, '');
const lines = Math.ceil(html.length / LINE_STR_COUNT);
return lines * 16;
},
},
getBoundingClientRect() {
let html = this.innerHTML;
html = html.replace(/<[^>]*>/g, '');
const lines = Math.ceil(html.length / LINE_STR_COUNT);
return { height: lines * 16 };
},
});
});
afterAll(() => {
errorSpy.mockRestore();
mockRectSpy.mockRestore();
});
const fullStr =
'Bamboo is Little Light Bamboo is Little Light Bamboo is Little Light Bamboo is Little Light Bamboo is Little Light';
it('should use editConfig.text over children in editing mode ', async () => {
const suffix = '--The information is very important';
const ref = React.createRef<any>();
const { container: wrapper, unmount } = render(
<Base
ellipsis={{ rows: 1, suffix }}
component="p"
editable={{ text: fullStr + suffix }}
ref={ref}
>
{fullStr}
</Base>,
);
fireEvent.click(wrapper.querySelector('.ant-typography-edit')!);
expect(wrapper.querySelector('textarea')?.textContent).toEqual(fullStr + suffix);
unmount();
});
it('should use children as the fallback of editConfig.text in editing mode', async () => {
const suffix = '--The information is very important';
const ref = React.createRef<any>();
const { container: wrapper, unmount } = render(
<Base ellipsis={{ rows: 1, suffix }} component="p" ref={ref} editable>
{fullStr}
</Base>,
);
fireEvent.click(wrapper.querySelector('.ant-typography-edit')!);
expect(wrapper.querySelector('textarea')?.textContent).toEqual(fullStr);
unmount();
});
});

View File

@ -396,6 +396,15 @@ describe('Typography', () => {
expect(onEnd).toHaveBeenCalledTimes(1);
});
it('should trigger onStart when type Start', () => {
const onStart = jest.fn();
const { container: wrapper } = render(<Paragraph editable={{ onStart }}>Bamboo</Paragraph>);
fireEvent.click(wrapper.querySelectorAll('.ant-typography-edit')[0]);
fireEvent.keyDown(wrapper.querySelector('textarea')!, { keyCode: KeyCode.A });
fireEvent.keyUp(wrapper.querySelector('textarea')!, { keyCode: KeyCode.A });
expect(onStart).toHaveBeenCalledTimes(1);
});
it('should trigger onCancel when type ESC', () => {
const onCancel = jest.fn();
const { container: wrapper } = render(

View File

@ -16,12 +16,19 @@ Provide additional interactive capacity of editable and copyable.
```tsx
import { CheckOutlined, HighlightOutlined, SmileFilled, SmileOutlined } from '@ant-design/icons';
import { Divider, Radio, Typography } from 'antd';
import React, { useState } from 'react';
import React, { useState, useMemo } from 'react';
const { Paragraph } = Typography;
const App: React.FC = () => {
const [editableStr, setEditableStr] = useState('This is an editable text.');
const [editableStrWithSuffix, setEditableStrWithSuffix] = useState(
'This is a loooooooooooooooooooooooooooooooong editable text with suffix.',
);
const [editableStrWithSuffixStartPart, editableStrWithSuffixSuffixPart] = useMemo(
() => [editableStrWithSuffix.slice(0, -12), editableStrWithSuffix.slice(-12)],
[editableStrWithSuffix],
);
const [customIconStr, setCustomIconStr] = useState('Custom Edit icon and replace tooltip text.');
const [clickTriggerStr, setClickTriggerStr] = useState(
'Text or icon as trigger - click to start editing.',
@ -60,6 +67,17 @@ const App: React.FC = () => {
return (
<>
<Paragraph editable={{ onChange: setEditableStr }}>{editableStr}</Paragraph>
<Paragraph
editable={{
onChange: setEditableStrWithSuffix,
text: editableStrWithSuffix,
}}
ellipsis={{
suffix: editableStrWithSuffixSuffixPart,
}}
>
{editableStrWithSuffixStartPart}
</Paragraph>
<Paragraph
editable={{
icon: <HighlightOutlined />,
@ -69,8 +87,7 @@ const App: React.FC = () => {
>
{customIconStr}
</Paragraph>
Trigger edit with:{' '}
<Radio.Group
Trigger edit with: <Radio.Group
onChange={e => setChooseTrigger(radioToState(e.target.value))}
value={stateToRadio()}
>

View File

@ -93,9 +93,10 @@ Basic text writing, including headings, body text, lists, and more.
editing: boolean,
maxLength: number,
autoSize: boolean | { minRows: number, maxRows: number },
onStart: function,
text: string,
onChange: function(string),
onCancel: function,
onStart: function,
onEnd: function,
triggerType: ('icon' | 'text')[],
enterIcon: ReactNode,
@ -108,9 +109,10 @@ Basic text writing, including headings, body text, lists, and more.
| icon | Custom editable icon | ReactNode | &lt;EditOutlined /> | 4.6.0 |
| maxLength | `maxLength` attribute of textarea | number | - | 4.4.0 |
| tooltip | Custom tooltip text, hide when it is false | boolean \| ReactNode | `Edit` | 4.6.0 |
| onStart | Called when enter editable state | function | - | |
| text | Edit text, specify the editing content instead of using the children implicitly | string | - | 4.24.0 |
| onChange | Called when input at textarea | function(value: string) | - | |
| onCancel | Called when type ESC to exit editable state | function | - | |
| onStart | Called when enter editable state | function | - | |
| onEnd | Called when type ENTER to exit editable state | function | - | 4.14.0 |
| triggerType | Edit mode trigger - icon, text or both (not specifying icon as trigger hides it) | Array&lt;`icon`\|`text`> | \[`icon`] | |
| enterIcon | Custom "enter" icon in the edit field (passing `null` removes the icon) | ReactNode | `<EnterOutlined />` | 4.17.0 |

View File

@ -94,9 +94,10 @@ cover: https://gw.alipayobjects.com/zos/alicdn/GOM1KQ24O/Typography.svg
editing: boolean,
maxLength: number,
autoSize: boolean | { minRows: number, maxRows: number },
onStart: function,
text: string,
onChange: function(string),
onCancel: function,
onStart: function,
onEnd: function,
triggerType: ('icon' | 'text')[],
enterIcon: ReactNode,
@ -109,10 +110,11 @@ cover: https://gw.alipayobjects.com/zos/alicdn/GOM1KQ24O/Typography.svg
| icon | 自定义编辑图标 | ReactNode | &lt;EditOutlined /> | 4.6.0 |
| maxLength | 编辑中文本域最大长度 | number | - | 4.4.0 |
| tooltip | 自定义提示文本,为 false 时关闭 | boolean \| ReactNode | `编辑` | 4.6.0 |
| onCancel | 按 ESC 退出编辑状态时触发 | function | - | |
| text | 显式地指定编辑文案,为空时将隐式地使用 children | string | - | 4.24.0 |
| onChange | 文本域编辑时触发 | function(value: string) | - | |
| onEnd | 按 ENTER 结束编辑状态时触发 | function | - | 4.14.0 |
| onCancel | 按 ESC 退出编辑状态时触发 | function | - | |
| onStart | 进入编辑中状态时触发 | function | - | |
| onEnd | 按 ENTER 结束编辑状态时触发 | function | - | 4.14.0 |
| triggerType | 编辑模式触发器类型,图标、文本或者两者都设置(不设置图标作为触发器时它会隐藏) | Array&lt;`icon`\|`text`> | \[`icon`] | |
| enterIcon | 在编辑段中自定义“enter”图标传递“null”将删除图标 | ReactNode | `<EnterOutlined />` | 4.17.0 |

View File

@ -91,7 +91,7 @@
"test-node": "npm run version && jest --config .jest.node.js --cache=false",
"tsc": "tsc --noEmit",
"site:test": "jest --config .jest.site.js --cache=false --force-exit",
"test-image": "npm run dist && jest --config .jest.image.js --no-cache -i -u",
"test-image": "npm run dist && jest --config .jest.image.js -i -u",
"argos": "node ./scripts/argos-upload.js",
"version": "node ./scripts/generate-version",
"install-react-16": "npm i --no-save --legacy-peer-deps react@16 react-dom@16",

View File

@ -6,6 +6,11 @@ import { render, act } from '@testing-library/react';
import { _rs as onLibResize } from 'rc-resize-observer/lib/utils/observerUtil';
import { _rs as onEsResize } from 'rc-resize-observer/es/utils/observerUtil';
export function assertsExist<T>(item: T | null | undefined): asserts item is T {
expect(item).not.toBeUndefined();
expect(item).not.toBeNull();
}
export function setMockDate(dateString = '2017-09-18T03:30:07.795') {
MockDate.set(dateString);
}