mirror of
https://github.com/ant-design/ant-design.git
synced 2025-01-22 08:53:29 +08:00
Merge pull request #27893 from ant-design/merge-feature
chore: mergin master into feature
This commit is contained in:
commit
e18c13940f
@ -114,17 +114,15 @@ import 'antd/dist/antd.css'; // or 'antd/dist/antd.less'
|
||||
## 🔗 链接
|
||||
|
||||
- [首页](https://ant.design/)
|
||||
- [组件库](https://ant.design/components/overview-cn)
|
||||
- [所有组件](https://ant.design/components/overview-cn)
|
||||
- [Ant Design Pro](http://pro.ant.design/)
|
||||
- [Ant Design Charts](https://charts.ant.design)
|
||||
- [更新日志](CHANGELOG.en-US.md)
|
||||
- [React 底层基础组件](http://react-component.github.io/)
|
||||
- [移动端组件](http://mobile.ant.design)
|
||||
- [页面级组件](https://procomponents.ant.design)
|
||||
- [Ant Design 图表](https://charts.ant.design)
|
||||
- [Ant Design 图标](https://github.com/ant-design/ant-design-icons)
|
||||
- [Ant Design 色彩](https://github.com/ant-design/ant-design-colors)
|
||||
- [Ant Design Pro 布局组件](https://github.com/ant-design/ant-design-pro-layout)
|
||||
- [Ant Design Pro 区块集](https://github.com/ant-design/pro-blocks)
|
||||
- [Dark Theme](https://github.com/ant-design/ant-design-dark-theme)
|
||||
- [首页模板集](https://landing.ant.design)
|
||||
- [动效](https://motion.ant.design)
|
||||
- [脚手架市场](http://scaffold.ant.design)
|
||||
@ -133,8 +131,8 @@ import 'antd/dist/antd.css'; // or 'antd/dist/antd.less'
|
||||
- [版本发布规则](https://github.com/ant-design/ant-design/wiki/%E8%BD%AE%E5%80%BC%E8%A7%84%E5%88%99%E5%92%8C%E7%89%88%E6%9C%AC%E5%8F%91%E5%B8%83%E6%B5%81%E7%A8%8B)
|
||||
- [常见问题](https://ant.design/docs/react/faq-cn)
|
||||
- [CodeSandbox 模板](https://u.ant.design/codesandbox-repro) for bug reports
|
||||
- [Awesome Ant Design](https://github.com/websemantics/awesome-ant-design)
|
||||
- [定制主题](https://ant.design/docs/react/customize-theme-cn)
|
||||
- [国际化](https://ant.design/docs//react/i18n-cn)
|
||||
- [成为社区协作成员](https://github.com/ant-design/ant-design/wiki/Collaborators#how-to-apply-for-being-a-collaborator)
|
||||
|
||||
## ⌨️ 本地开发
|
||||
|
10
README.md
10
README.md
@ -110,17 +110,16 @@ Dozens of languages supported in `antd`, see [i18n](https://ant.design/docs/reac
|
||||
## 🔗 Links
|
||||
|
||||
- [Home page](https://ant.design/)
|
||||
- [Components](https://ant.design/components/overview)
|
||||
- [Components Overview](https://ant.design/components/overview)
|
||||
- [Ant Design Pro](http://pro.ant.design/)
|
||||
- [Ant Design Charts](https://charts.ant.design)
|
||||
- [Change Log](CHANGELOG.en-US.md)
|
||||
- [rc-components](http://react-component.github.io/)
|
||||
- [Mobile UI](http://mobile.ant.design)
|
||||
- [Ant Design Pro Components](https://procomponents.ant.design)
|
||||
- [Ant Design Charts](https://charts.ant.design)
|
||||
- [Ant Design Icons](https://github.com/ant-design/ant-design-icons)
|
||||
- [Ant Design Colors](https://github.com/ant-design/ant-design-colors)
|
||||
- [Ant Design Pro Layout](https://github.com/ant-design/ant-design-pro-layout)
|
||||
- [Ant Design Pro Blocks](https://github.com/ant-design/pro-blocks)
|
||||
- [Dark Theme](https://github.com/ant-design/ant-design-dark-theme)
|
||||
- [Ant Design Charts](https://charts.ant.design)
|
||||
- [Landing Pages](https://landing.ant.design)
|
||||
- [Motion](https://motion.ant.design)
|
||||
- [Scaffold Market](http://scaffold.ant.design)
|
||||
@ -128,7 +127,6 @@ Dozens of languages supported in `antd`, see [i18n](https://ant.design/docs/reac
|
||||
- [Versioning Release Note](https://github.com/ant-design/ant-design/wiki/%E8%BD%AE%E5%80%BC%E8%A7%84%E5%88%99%E5%92%8C%E7%89%88%E6%9C%AC%E5%8F%91%E5%B8%83%E6%B5%81%E7%A8%8B)
|
||||
- [FAQ](https://ant.design/docs/react/faq)
|
||||
- [CodeSandbox Template](https://u.ant.design/codesandbox-repro) for bug reports
|
||||
- [Awesome Ant Design](https://github.com/websemantics/awesome-ant-design)
|
||||
- [Customize Theme](https://ant.design/docs/react/customize-theme)
|
||||
- [How to Apply for Being A Collaborator](https://github.com/ant-design/ant-design/wiki/Collaborators#how-to-apply-for-being-a-collaborator)
|
||||
|
||||
|
@ -12,11 +12,8 @@ import Wave from '../wave';
|
||||
import TransButton from '../transButton';
|
||||
import { isStyleSupport, isFlexSupported } from '../styleChecker';
|
||||
import { sleep } from '../../../tests/utils';
|
||||
import focusTest from '../../../tests/shared/focusTest';
|
||||
|
||||
describe('Test utils function', () => {
|
||||
focusTest(TransButton);
|
||||
|
||||
describe('throttle', () => {
|
||||
it('throttle function should work', async () => {
|
||||
const callback = jest.fn();
|
||||
@ -193,9 +190,10 @@ describe('Test utils function', () => {
|
||||
|
||||
describe('TransButton', () => {
|
||||
it('can be focus/blur', () => {
|
||||
const wrapper = mount(<TransButton>TransButton</TransButton>);
|
||||
expect(typeof wrapper.instance().focus).toBe('function');
|
||||
expect(typeof wrapper.instance().blur).toBe('function');
|
||||
const ref = React.createRef();
|
||||
mount(<TransButton ref={ref}>TransButton</TransButton>);
|
||||
expect(typeof ref.current.focus).toBe('function');
|
||||
expect(typeof ref.current.blur).toBe('function');
|
||||
});
|
||||
|
||||
it('should trigger onClick when press enter', () => {
|
||||
|
@ -2,16 +2,20 @@ import * as React from 'react';
|
||||
|
||||
export const { isValidElement } = React;
|
||||
|
||||
type AnyObject = Record<any, any>;
|
||||
|
||||
type RenderProps = undefined | AnyObject | ((originProps: AnyObject) => AnyObject | undefined);
|
||||
|
||||
export function replaceElement(
|
||||
element: React.ReactNode,
|
||||
replacement: React.ReactNode,
|
||||
props: any,
|
||||
props: RenderProps,
|
||||
): React.ReactNode {
|
||||
if (!isValidElement(element)) return replacement;
|
||||
|
||||
return React.cloneElement(element, typeof props === 'function' ? props() : props);
|
||||
return React.cloneElement(element, typeof props === 'function' ? props(element.props) : props);
|
||||
}
|
||||
|
||||
export function cloneElement(element: React.ReactNode, props?: any): React.ReactElement {
|
||||
export function cloneElement(element: React.ReactNode, props?: RenderProps): React.ReactElement {
|
||||
return replaceElement(element, element, props) as React.ReactElement;
|
||||
}
|
||||
|
@ -20,81 +20,52 @@ const inlineStyle: React.CSSProperties = {
|
||||
display: 'inline-block',
|
||||
};
|
||||
|
||||
class TransButton extends React.Component<TransButtonProps> {
|
||||
div?: HTMLDivElement;
|
||||
|
||||
lastKeyCode?: number;
|
||||
|
||||
componentDidMount() {
|
||||
const { autoFocus } = this.props;
|
||||
if (autoFocus) {
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
|
||||
onKeyDown: React.KeyboardEventHandler<HTMLDivElement> = event => {
|
||||
const TransButton = React.forwardRef<HTMLDivElement, TransButtonProps>((props, ref) => {
|
||||
const onKeyDown: React.KeyboardEventHandler<HTMLDivElement> = event => {
|
||||
const { keyCode } = event;
|
||||
if (keyCode === KeyCode.ENTER) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
onKeyUp: React.KeyboardEventHandler<HTMLDivElement> = event => {
|
||||
const onKeyUp: React.KeyboardEventHandler<HTMLDivElement> = event => {
|
||||
const { keyCode } = event;
|
||||
const { onClick } = this.props;
|
||||
const { onClick } = props;
|
||||
if (keyCode === KeyCode.ENTER && onClick) {
|
||||
onClick();
|
||||
}
|
||||
};
|
||||
|
||||
setRef = (btn: HTMLDivElement) => {
|
||||
this.div = btn;
|
||||
const { style, noStyle, disabled, ...restProps } = props;
|
||||
|
||||
let mergedStyle: React.CSSProperties = {};
|
||||
|
||||
if (!noStyle) {
|
||||
mergedStyle = {
|
||||
...inlineStyle,
|
||||
};
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
mergedStyle.pointerEvents = 'none';
|
||||
}
|
||||
|
||||
mergedStyle = {
|
||||
...mergedStyle,
|
||||
...style,
|
||||
};
|
||||
|
||||
focus() {
|
||||
if (this.div) {
|
||||
this.div.focus();
|
||||
}
|
||||
}
|
||||
|
||||
blur() {
|
||||
if (this.div) {
|
||||
this.div.blur();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { style, noStyle, disabled, ...restProps } = this.props;
|
||||
|
||||
let mergedStyle: React.CSSProperties = {};
|
||||
|
||||
if (!noStyle) {
|
||||
mergedStyle = {
|
||||
...inlineStyle,
|
||||
};
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
mergedStyle.pointerEvents = 'none';
|
||||
}
|
||||
|
||||
mergedStyle = {
|
||||
...mergedStyle,
|
||||
...style,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
ref={this.setRef}
|
||||
{...restProps}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={this.onKeyUp}
|
||||
style={mergedStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
ref={ref}
|
||||
{...restProps}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
style={mergedStyle}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default TransButton;
|
||||
|
@ -20,7 +20,7 @@ export interface DataSourceItemObject {
|
||||
value: string;
|
||||
text: string;
|
||||
}
|
||||
export type DataSourceItemType = string | DataSourceItemObject;
|
||||
export type DataSourceItemType = DataSourceItemObject | React.ReactNode;
|
||||
|
||||
export interface AutoCompleteProps
|
||||
extends Omit<
|
||||
|
@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import Animate from 'rc-animate';
|
||||
import CSSMotion from 'rc-motion';
|
||||
import addEventListener from 'rc-util/lib/Dom/addEventListener';
|
||||
import useMergedState from 'rc-util/lib/hooks/useMergedState';
|
||||
import classNames from 'classnames';
|
||||
import omit from 'omit.js';
|
||||
import VerticalAlignTopOutlined from '@ant-design/icons/VerticalAlignTopOutlined';
|
||||
@ -8,6 +9,7 @@ import { throttleByAnimationFrame } from '../_util/throttleByAnimationFrame';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
import getScroll from '../_util/getScroll';
|
||||
import scrollTo from '../_util/scrollTo';
|
||||
import { cloneElement } from '../_util/reactNode';
|
||||
|
||||
export interface BackTopProps {
|
||||
visibilityHeight?: number;
|
||||
@ -22,7 +24,9 @@ export interface BackTopProps {
|
||||
}
|
||||
|
||||
const BackTop: React.FC<BackTopProps> = props => {
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const [visible, setVisible] = useMergedState(false, {
|
||||
value: props.visible,
|
||||
});
|
||||
|
||||
const ref = React.createRef<HTMLDivElement>();
|
||||
const scrollEvent = React.useRef<any>();
|
||||
@ -61,13 +65,6 @@ const BackTop: React.FC<BackTopProps> = props => {
|
||||
};
|
||||
}, [props.target]);
|
||||
|
||||
const getVisible = () => {
|
||||
if ('visible' in props) {
|
||||
return props.visible;
|
||||
}
|
||||
return visible;
|
||||
};
|
||||
|
||||
const scrollToTop = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const { onClick, target, duration = 450 } = props;
|
||||
scrollTo(0, {
|
||||
@ -89,9 +86,18 @@ const BackTop: React.FC<BackTopProps> = props => {
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Animate component="" transitionName="fade">
|
||||
{getVisible() ? <div>{children || defaultElement}</div> : null}
|
||||
</Animate>
|
||||
<CSSMotion visible={visible} motionName="fade" removeOnLeave>
|
||||
{({ className: motionClassName }) => {
|
||||
const childNode = children || defaultElement;
|
||||
return (
|
||||
<div>
|
||||
{cloneElement(childNode, ({ className }) => ({
|
||||
className: classNames(motionClassName, className),
|
||||
}))}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</CSSMotion>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -38,11 +38,12 @@ export interface ScrollNumberProps {
|
||||
prefixCls?: string;
|
||||
className?: string;
|
||||
count?: string | number | null;
|
||||
displayComponent?: React.ReactElement<HTMLElement>;
|
||||
children?: React.ReactElement<HTMLElement>;
|
||||
component?: string;
|
||||
onAnimated?: Function;
|
||||
style?: React.CSSProperties;
|
||||
title?: string | number | null;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export interface ScrollNumberState {
|
||||
@ -56,8 +57,9 @@ const ScrollNumber: React.FC<ScrollNumberProps> = ({
|
||||
className,
|
||||
style,
|
||||
title,
|
||||
show,
|
||||
component = 'sup',
|
||||
displayComponent,
|
||||
children,
|
||||
onAnimated = () => {},
|
||||
...restProps
|
||||
}) => {
|
||||
@ -92,6 +94,7 @@ const ScrollNumber: React.FC<ScrollNumberProps> = ({
|
||||
};
|
||||
}, [animateStarted, customizeCount, onAnimated]);
|
||||
|
||||
// =========================== Function ===========================
|
||||
const getPositionByNum = (num: number, i: number) => {
|
||||
const currentCount = Math.abs(Number(count));
|
||||
const lstCount = Math.abs(Number(lastCount));
|
||||
@ -115,6 +118,15 @@ const ScrollNumber: React.FC<ScrollNumberProps> = ({
|
||||
return num;
|
||||
};
|
||||
|
||||
// ============================ Render ============================
|
||||
const newProps = {
|
||||
...restProps,
|
||||
'data-show': show,
|
||||
style,
|
||||
className: classNames(prefixCls, className),
|
||||
title: title as string,
|
||||
};
|
||||
|
||||
const renderCurrentNumber = (num: number | string, i: number) => {
|
||||
if (typeof num === 'number') {
|
||||
const position = getPositionByNum(num, i);
|
||||
@ -142,21 +154,15 @@ const ScrollNumber: React.FC<ScrollNumberProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderNumberElement = () => {
|
||||
if (count && Number(count) % 1 === 0) {
|
||||
return getNumberArray(count)
|
||||
.map((num, i) => renderCurrentNumber(num, i))
|
||||
.reverse();
|
||||
}
|
||||
return count;
|
||||
};
|
||||
|
||||
const newProps = {
|
||||
...restProps,
|
||||
style,
|
||||
className: classNames(prefixCls, className),
|
||||
title: title as string,
|
||||
};
|
||||
const numberNodeRef = React.useRef<React.ReactNode>(null);
|
||||
if (show) {
|
||||
numberNodeRef.current =
|
||||
count && Number(count) % 1 === 0
|
||||
? getNumberArray(count)
|
||||
.map((num, i) => renderCurrentNumber(num, i))
|
||||
.reverse()
|
||||
: count;
|
||||
}
|
||||
|
||||
// allow specify the border
|
||||
// mock border-color by box-shadow for compatible with old usage:
|
||||
@ -167,15 +173,12 @@ const ScrollNumber: React.FC<ScrollNumberProps> = ({
|
||||
boxShadow: `0 0 0 1px ${style.borderColor} inset`,
|
||||
};
|
||||
}
|
||||
if (displayComponent) {
|
||||
return cloneElement(displayComponent, {
|
||||
className: classNames(
|
||||
`${prefixCls}-custom-component`,
|
||||
displayComponent.props && displayComponent.props.className,
|
||||
),
|
||||
});
|
||||
if (children) {
|
||||
return cloneElement(children, oriProps => ({
|
||||
className: classNames(`${prefixCls}-custom-component`, oriProps?.className),
|
||||
}));
|
||||
}
|
||||
return React.createElement(component as any, newProps, renderNumberElement());
|
||||
return React.createElement(component as any, newProps, numberNodeRef.current);
|
||||
};
|
||||
|
||||
export default ScrollNumber;
|
||||
|
@ -1503,7 +1503,7 @@ exports[`renders ./components/badge/demo/offset.md correctly 1`] = `
|
||||
<sup
|
||||
class="ant-scroll-number ant-badge-count"
|
||||
data-show="true"
|
||||
style="right:-10px;margin-top:10px"
|
||||
style="margin-top:10px;right:-10px"
|
||||
title="5"
|
||||
>
|
||||
<span
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { mount, render } from 'enzyme';
|
||||
import { mount } from 'enzyme';
|
||||
import Badge from '../index';
|
||||
import Tooltip from '../../tooltip';
|
||||
import mountTest from '../../../tests/shared/mountTest';
|
||||
@ -30,10 +30,11 @@ describe('Badge', () => {
|
||||
});
|
||||
|
||||
it('badge should support float number', () => {
|
||||
let wrapper = render(<Badge count={3.5} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
let wrapper = mount(<Badge count={3.5} />);
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
|
||||
wrapper = mount(<Badge count="3.5" />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
expect(() => wrapper.unmount()).not.toThrow();
|
||||
});
|
||||
|
||||
@ -66,44 +67,44 @@ describe('Badge', () => {
|
||||
const wrapper = mount(<Badge count={9} />);
|
||||
wrapper.setProps({ count: 10 });
|
||||
jest.runAllTimers();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
wrapper.setProps({ count: 11 });
|
||||
jest.runAllTimers();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
wrapper.setProps({ count: 11 });
|
||||
jest.runAllTimers();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
wrapper.setProps({ count: 111 });
|
||||
jest.runAllTimers();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
wrapper.setProps({ count: 10 });
|
||||
jest.runAllTimers();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
jest.runAllTimers();
|
||||
wrapper.setProps({ count: 9 });
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should be compatible with borderColor style', () => {
|
||||
const wrapper = render(
|
||||
const wrapper = mount(
|
||||
<Badge
|
||||
count={4}
|
||||
style={{ backgroundColor: '#fff', color: '#999', borderColor: '#d9d9d9' }}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/13694
|
||||
it('should support offset when count is a ReactNode', () => {
|
||||
const wrapper = render(
|
||||
const wrapper = mount(
|
||||
<Badge count={<span className="custom" style={{ color: '#f5222d' }} />} offset={[10, 20]}>
|
||||
<a href="#" className="head-example">
|
||||
head
|
||||
</a>
|
||||
</Badge>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/15349
|
||||
@ -114,19 +115,19 @@ describe('Badge', () => {
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/15799
|
||||
it('render correct with negative number', () => {
|
||||
const wrapper = render(
|
||||
const wrapper = mount(
|
||||
<div>
|
||||
<Badge count="-10" />
|
||||
<Badge count={-10} />
|
||||
</div>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/21331
|
||||
it('render Badge status/color when contains children', () => {
|
||||
const wrapper = render(
|
||||
<>
|
||||
const wrapper = mount(
|
||||
<div>
|
||||
<Badge count={5} status="success">
|
||||
<a />
|
||||
</Badge>
|
||||
@ -136,9 +137,9 @@ describe('Badge', () => {
|
||||
<Badge count={5} color="#08c">
|
||||
<a />
|
||||
</Badge>
|
||||
</>,
|
||||
</div>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('Badge should work when status/color is empty string', () => {
|
||||
@ -153,17 +154,17 @@ describe('Badge', () => {
|
||||
});
|
||||
|
||||
it('render Badge size when contains children', () => {
|
||||
const wrapper = render(
|
||||
<>
|
||||
const wrapper = mount(
|
||||
<div>
|
||||
<Badge size="default" count={5}>
|
||||
<a />
|
||||
</Badge>
|
||||
<Badge size="small" count={5}>
|
||||
<a />
|
||||
</Badge>
|
||||
</>,
|
||||
</div>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import Animate from 'rc-animate';
|
||||
import CSSMotion from 'rc-motion';
|
||||
import classNames from 'classnames';
|
||||
import ScrollNumber from './ScrollNumber';
|
||||
import Ribbon from './Ribbon';
|
||||
@ -56,123 +56,77 @@ const Badge: CompoundedComponent = ({
|
||||
const { getPrefixCls, direction } = React.useContext(ConfigContext);
|
||||
const prefixCls = getPrefixCls('badge', customizePrefixCls);
|
||||
|
||||
const getNumberedDisplayCount = () => {
|
||||
const displayCount =
|
||||
(count as number) > (overflowCount as number) ? `${overflowCount}+` : count;
|
||||
return displayCount as string | number | null;
|
||||
};
|
||||
// ================================ Misc ================================
|
||||
const numberedDisplayCount = ((count as number) > (overflowCount as number)
|
||||
? `${overflowCount}+`
|
||||
: count) as string | number | null;
|
||||
|
||||
const hasStatus = (): boolean =>
|
||||
const hasStatus =
|
||||
(status !== null && status !== undefined) || (color !== null && color !== undefined);
|
||||
|
||||
const isZero = () => {
|
||||
const numberedDisplayCount = getNumberedDisplayCount();
|
||||
return numberedDisplayCount === '0' || numberedDisplayCount === 0;
|
||||
};
|
||||
const isZero = numberedDisplayCount === '0' || numberedDisplayCount === 0;
|
||||
|
||||
const isDot = () => {
|
||||
return (dot && !isZero()) || hasStatus();
|
||||
};
|
||||
const showAsDot = (dot && !isZero) || hasStatus;
|
||||
|
||||
const getDisplayCount = () => {
|
||||
// dot mode don't need count
|
||||
if (isDot()) {
|
||||
return '';
|
||||
}
|
||||
return getNumberedDisplayCount();
|
||||
};
|
||||
const displayCount = showAsDot ? '' : numberedDisplayCount;
|
||||
|
||||
const getScrollNumberTitle = () => {
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
return typeof count === 'string' || typeof count === 'number' ? count : undefined;
|
||||
};
|
||||
|
||||
const getStyleWithOffset = () => {
|
||||
if (direction === 'rtl') {
|
||||
return offset
|
||||
? {
|
||||
left: parseInt(offset[0] as string, 10),
|
||||
marginTop: offset[1],
|
||||
...style,
|
||||
}
|
||||
: style;
|
||||
}
|
||||
return offset
|
||||
? {
|
||||
right: -parseInt(offset[0] as string, 10),
|
||||
marginTop: offset[1],
|
||||
...style,
|
||||
}
|
||||
: style;
|
||||
};
|
||||
|
||||
const isHidden = () => {
|
||||
const displayCount = getDisplayCount();
|
||||
const isHidden = React.useMemo(() => {
|
||||
const isEmpty = displayCount === null || displayCount === undefined || displayCount === '';
|
||||
return (isEmpty || (isZero() && !showZero)) && !isDot();
|
||||
};
|
||||
return (isEmpty || (isZero && !showZero)) && !showAsDot;
|
||||
}, [displayCount, isZero, showZero, showAsDot]);
|
||||
|
||||
const renderStatusText = () => {
|
||||
const hidden = isHidden();
|
||||
return hidden || !text ? null : <span className={`${prefixCls}-status-text`}>{text}</span>;
|
||||
};
|
||||
// We will cache the dot status to avoid shaking on leaved motion
|
||||
const isDotRef = React.useRef(showAsDot);
|
||||
if (!isHidden) {
|
||||
isDotRef.current = showAsDot;
|
||||
}
|
||||
|
||||
const renderDisplayComponent = () => {
|
||||
const customNode = count as React.ReactElement<any>;
|
||||
if (!customNode || typeof customNode !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
return cloneElement(customNode, {
|
||||
style: {
|
||||
...getStyleWithOffset(),
|
||||
...(customNode.props && customNode.props.style),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const renderBadgeNumber = () => {
|
||||
const scrollNumberPrefixCls = getPrefixCls('scroll-number', customizeScrollNumberPrefixCls);
|
||||
const displayCount = getDisplayCount();
|
||||
const bDot = isDot();
|
||||
const hidden = isHidden();
|
||||
|
||||
const scrollNumberCls = classNames({
|
||||
[`${prefixCls}-dot`]: bDot,
|
||||
[`${prefixCls}-count`]: !bDot,
|
||||
[`${prefixCls}-count-sm`]: size === 'small',
|
||||
[`${prefixCls}-multiple-words`]:
|
||||
!bDot && count && count.toString && count.toString().length > 1,
|
||||
[`${prefixCls}-status-${status}`]: !!status,
|
||||
[`${prefixCls}-status-${color}`]: isPresetColor(color),
|
||||
});
|
||||
|
||||
let statusStyle: React.CSSProperties | undefined = getStyleWithOffset();
|
||||
if (color && !isPresetColor(color)) {
|
||||
statusStyle = statusStyle || {};
|
||||
statusStyle.background = color;
|
||||
// =============================== Styles ===============================
|
||||
const mergedStyle = React.useMemo<React.CSSProperties>(() => {
|
||||
if (!offset) {
|
||||
return { ...style };
|
||||
}
|
||||
|
||||
return hidden ? null : (
|
||||
<ScrollNumber
|
||||
prefixCls={scrollNumberPrefixCls}
|
||||
data-show={!hidden}
|
||||
className={scrollNumberCls}
|
||||
count={displayCount}
|
||||
displayComponent={renderDisplayComponent()} // <Badge status="success" count={<Icon type="xxx" />}></Badge>
|
||||
title={getScrollNumberTitle()}
|
||||
style={statusStyle}
|
||||
key="scrollNumber"
|
||||
/>
|
||||
);
|
||||
};
|
||||
const offsetStyle: React.CSSProperties = { marginTop: offset[1] };
|
||||
if (direction === 'rtl') {
|
||||
offsetStyle.left = parseInt(offset[0] as string, 10);
|
||||
} else {
|
||||
offsetStyle.right = -parseInt(offset[0] as string, 10);
|
||||
}
|
||||
|
||||
return {
|
||||
...offsetStyle,
|
||||
...style,
|
||||
};
|
||||
}, [direction, offset, style]);
|
||||
|
||||
// =============================== Render ===============================
|
||||
// >>> Title
|
||||
const titleNode =
|
||||
title ?? (typeof count === 'string' || typeof count === 'number' ? count : undefined);
|
||||
|
||||
// >>> Status Text
|
||||
const statusTextNode =
|
||||
isHidden || !text ? null : <span className={`${prefixCls}-status-text`}>{text}</span>;
|
||||
|
||||
// >>> Display Component
|
||||
const displayNode =
|
||||
!count || typeof count !== 'object'
|
||||
? undefined
|
||||
: cloneElement(count, oriProps => ({
|
||||
style: {
|
||||
...mergedStyle,
|
||||
...oriProps.style,
|
||||
},
|
||||
}));
|
||||
|
||||
// Shared styles
|
||||
const statusCls = classNames({
|
||||
[`${prefixCls}-status-dot`]: hasStatus(),
|
||||
[`${prefixCls}-status-dot`]: hasStatus,
|
||||
[`${prefixCls}-status-${status}`]: !!status,
|
||||
[`${prefixCls}-status-${color}`]: isPresetColor(color),
|
||||
});
|
||||
|
||||
const statusStyle: React.CSSProperties = {};
|
||||
if (color && !isPresetColor(color)) {
|
||||
statusStyle.background = color;
|
||||
@ -181,7 +135,7 @@ const Badge: CompoundedComponent = ({
|
||||
const badgeClassName = classNames(
|
||||
prefixCls,
|
||||
{
|
||||
[`${prefixCls}-status`]: hasStatus(),
|
||||
[`${prefixCls}-status`]: hasStatus,
|
||||
[`${prefixCls}-not-a-wrapper`]: !children,
|
||||
[`${prefixCls}-rtl`]: direction === 'rtl',
|
||||
},
|
||||
@ -189,11 +143,10 @@ const Badge: CompoundedComponent = ({
|
||||
);
|
||||
|
||||
// <Badge status="success" />
|
||||
if (!children && hasStatus()) {
|
||||
const styleWithOffset = getStyleWithOffset();
|
||||
const statusTextColor = styleWithOffset && styleWithOffset.color;
|
||||
if (!children && hasStatus) {
|
||||
const statusTextColor = mergedStyle.color;
|
||||
return (
|
||||
<span {...restProps} className={badgeClassName} style={styleWithOffset}>
|
||||
<span {...restProps} className={badgeClassName} style={mergedStyle}>
|
||||
<span className={statusCls} style={statusStyle} />
|
||||
<span style={{ color: statusTextColor }} className={`${prefixCls}-status-text`}>
|
||||
{text}
|
||||
@ -202,18 +155,51 @@ const Badge: CompoundedComponent = ({
|
||||
);
|
||||
}
|
||||
|
||||
// <Badge status="success" count={<Icon type="xxx" />}></Badge>
|
||||
return (
|
||||
<span {...restProps} className={badgeClassName}>
|
||||
{children}
|
||||
<Animate
|
||||
component=""
|
||||
showProp="data-show"
|
||||
transitionName={children ? `${prefixCls}-zoom` : ''}
|
||||
transitionAppear
|
||||
>
|
||||
{renderBadgeNumber()}
|
||||
</Animate>
|
||||
{renderStatusText()}
|
||||
<CSSMotion visible={!isHidden} motionName={`${prefixCls}-zoom`} motionAppear>
|
||||
{({ className: motionClassName }) => {
|
||||
const scrollNumberPrefixCls = getPrefixCls(
|
||||
'scroll-number',
|
||||
customizeScrollNumberPrefixCls,
|
||||
);
|
||||
|
||||
const isDot = isDotRef.current;
|
||||
|
||||
const scrollNumberCls = classNames({
|
||||
[`${prefixCls}-dot`]: isDot,
|
||||
[`${prefixCls}-count`]: !isDot,
|
||||
[`${prefixCls}-count-sm`]: size === 'small',
|
||||
[`${prefixCls}-multiple-words`]:
|
||||
!isDot && count && count.toString && count.toString().length > 1,
|
||||
[`${prefixCls}-status-${status}`]: !!status,
|
||||
[`${prefixCls}-status-${color}`]: isPresetColor(color),
|
||||
});
|
||||
|
||||
let scrollNumberStyle: React.CSSProperties = { ...mergedStyle };
|
||||
if (color && !isPresetColor(color)) {
|
||||
scrollNumberStyle = scrollNumberStyle || {};
|
||||
scrollNumberStyle.background = color;
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollNumber
|
||||
prefixCls={scrollNumberPrefixCls}
|
||||
show={!isHidden}
|
||||
className={classNames(motionClassName, scrollNumberCls)}
|
||||
count={displayCount}
|
||||
title={titleNode}
|
||||
style={scrollNumberStyle}
|
||||
key="scrollNumber"
|
||||
>
|
||||
{displayNode}
|
||||
</ScrollNumber>
|
||||
);
|
||||
}}
|
||||
</CSSMotion>
|
||||
{statusTextNode}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
@ -1,8 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import RcCheckbox from 'rc-checkbox';
|
||||
import CheckboxGroup, { GroupContext } from './Group';
|
||||
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
|
||||
import { GroupContext } from './Group';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
import devWarning from '../_util/devWarning';
|
||||
|
||||
export interface AbstractCheckboxProps<T> {
|
||||
@ -42,117 +42,84 @@ export interface CheckboxProps extends AbstractCheckboxProps<CheckboxChangeEvent
|
||||
indeterminate?: boolean;
|
||||
}
|
||||
|
||||
class Checkbox extends React.PureComponent<CheckboxProps, {}> {
|
||||
static Group: typeof CheckboxGroup;
|
||||
const InternalCheckbox: React.ForwardRefRenderFunction<HTMLInputElement, CheckboxProps> = (
|
||||
{
|
||||
prefixCls: customizePrefixCls,
|
||||
className,
|
||||
children,
|
||||
indeterminate = false,
|
||||
style,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
...restProps
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { getPrefixCls, direction } = React.useContext(ConfigContext);
|
||||
const checkboxGroup = React.useContext(GroupContext);
|
||||
|
||||
static __ANT_CHECKBOX = true;
|
||||
|
||||
static defaultProps = {
|
||||
indeterminate: false,
|
||||
};
|
||||
|
||||
static contextType = GroupContext;
|
||||
|
||||
context: any;
|
||||
|
||||
private rcCheckbox: any;
|
||||
|
||||
componentDidMount() {
|
||||
const { value } = this.props;
|
||||
this.context?.registerValue(value);
|
||||
const prevValue = React.useRef(restProps.value);
|
||||
|
||||
React.useEffect(() => {
|
||||
checkboxGroup?.registerValue(restProps.value);
|
||||
devWarning(
|
||||
'checked' in this.props || this.context || !('value' in this.props),
|
||||
'checked' in restProps || !!checkboxGroup || !('value' in restProps),
|
||||
'Checkbox',
|
||||
'`value` is not a valid prop, do you mean `checked`?',
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
componentDidUpdate({ value: prevValue }: CheckboxProps) {
|
||||
const { value } = this.props;
|
||||
if (value !== prevValue) {
|
||||
this.context?.cancelValue(prevValue);
|
||||
this.context?.registerValue(value);
|
||||
React.useEffect(() => {
|
||||
if (restProps.value !== prevValue.current) {
|
||||
checkboxGroup?.cancelValue(prevValue.current);
|
||||
checkboxGroup?.registerValue(restProps.value);
|
||||
}
|
||||
return () => checkboxGroup?.cancelValue(restProps.value);
|
||||
}, [restProps.value]);
|
||||
|
||||
const prefixCls = getPrefixCls('checkbox', customizePrefixCls);
|
||||
const checkboxProps: CheckboxProps = { ...restProps };
|
||||
if (checkboxGroup) {
|
||||
checkboxProps.onChange = (...args) => {
|
||||
if (restProps.onChange) {
|
||||
restProps.onChange(...args);
|
||||
}
|
||||
if (checkboxGroup.toggleOption) {
|
||||
checkboxGroup.toggleOption({ label: children, value: restProps.value });
|
||||
}
|
||||
};
|
||||
checkboxProps.name = checkboxGroup.name;
|
||||
checkboxProps.checked = checkboxGroup.value.indexOf(restProps.value) !== -1;
|
||||
checkboxProps.disabled = restProps.disabled || checkboxGroup.disabled;
|
||||
}
|
||||
const classString = classNames(
|
||||
{
|
||||
[`${prefixCls}-wrapper`]: true,
|
||||
[`${prefixCls}-rtl`]: direction === 'rtl',
|
||||
[`${prefixCls}-wrapper-checked`]: checkboxProps.checked,
|
||||
[`${prefixCls}-wrapper-disabled`]: checkboxProps.disabled,
|
||||
},
|
||||
className,
|
||||
);
|
||||
const checkboxClass = classNames({
|
||||
[`${prefixCls}-indeterminate`]: indeterminate,
|
||||
});
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/label-has-associated-control
|
||||
<label
|
||||
className={classString}
|
||||
style={style}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<RcCheckbox {...checkboxProps} prefixCls={prefixCls} className={checkboxClass} ref={ref} />
|
||||
{children !== undefined && <span>{children}</span>}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
const { value } = this.props;
|
||||
this.context?.cancelValue(value);
|
||||
}
|
||||
const Checkbox = React.forwardRef<unknown, CheckboxProps>(InternalCheckbox);
|
||||
|
||||
saveCheckbox = (node: any) => {
|
||||
this.rcCheckbox = node;
|
||||
};
|
||||
|
||||
focus() {
|
||||
this.rcCheckbox.focus();
|
||||
}
|
||||
|
||||
blur() {
|
||||
this.rcCheckbox.blur();
|
||||
}
|
||||
|
||||
renderCheckbox = ({ getPrefixCls, direction }: ConfigConsumerProps) => {
|
||||
const { props, context } = this;
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
className,
|
||||
children,
|
||||
indeterminate,
|
||||
style,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
...restProps
|
||||
} = props;
|
||||
const checkboxGroup = context;
|
||||
const prefixCls = getPrefixCls('checkbox', customizePrefixCls);
|
||||
const checkboxProps: CheckboxProps = { ...restProps };
|
||||
if (checkboxGroup) {
|
||||
checkboxProps.onChange = (...args) => {
|
||||
if (restProps.onChange) {
|
||||
restProps.onChange(...args);
|
||||
}
|
||||
checkboxGroup.toggleOption({ label: children, value: props.value });
|
||||
};
|
||||
checkboxProps.name = checkboxGroup.name;
|
||||
checkboxProps.checked = checkboxGroup.value.indexOf(props.value) !== -1;
|
||||
checkboxProps.disabled = props.disabled || checkboxGroup.disabled;
|
||||
}
|
||||
const classString = classNames(
|
||||
{
|
||||
[`${prefixCls}-wrapper`]: true,
|
||||
[`${prefixCls}-rtl`]: direction === 'rtl',
|
||||
[`${prefixCls}-wrapper-checked`]: checkboxProps.checked,
|
||||
[`${prefixCls}-wrapper-disabled`]: checkboxProps.disabled,
|
||||
},
|
||||
className,
|
||||
);
|
||||
const checkboxClass = classNames({
|
||||
[`${prefixCls}-indeterminate`]: indeterminate,
|
||||
});
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/label-has-associated-control
|
||||
<label
|
||||
className={classString}
|
||||
style={style}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<RcCheckbox
|
||||
{...checkboxProps}
|
||||
prefixCls={prefixCls}
|
||||
className={checkboxClass}
|
||||
ref={this.saveCheckbox}
|
||||
/>
|
||||
{children !== undefined && <span>{children}</span>}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return <ConfigConsumer>{this.renderCheckbox}</ConfigConsumer>;
|
||||
}
|
||||
}
|
||||
Checkbox.displayName = 'Checkbox';
|
||||
|
||||
export default Checkbox;
|
||||
|
@ -31,9 +31,12 @@ export interface CheckboxGroupProps extends AbstractCheckboxGroupProps {
|
||||
}
|
||||
|
||||
export interface CheckboxGroupContext {
|
||||
name?: string;
|
||||
toggleOption?: (option: CheckboxOptionType) => void;
|
||||
value?: any;
|
||||
disabled?: boolean;
|
||||
registerValue: (val: string) => void;
|
||||
cancelValue: (val: string) => void;
|
||||
}
|
||||
|
||||
export const GroupContext = React.createContext<CheckboxGroupContext | null>(null);
|
||||
|
@ -7,7 +7,7 @@ import mountTest from '../../../tests/shared/mountTest';
|
||||
import rtlTest from '../../../tests/shared/rtlTest';
|
||||
|
||||
describe('Checkbox', () => {
|
||||
focusTest(Checkbox);
|
||||
focusTest(Checkbox, { refFocus: true });
|
||||
mountTest(Checkbox);
|
||||
rtlTest(Checkbox);
|
||||
|
||||
|
@ -3,6 +3,13 @@ import Checkbox from '..';
|
||||
import Input from '../../input';
|
||||
|
||||
describe('Checkbox.typescript', () => {
|
||||
it('Checkbox', () => {
|
||||
const ref = React.createRef<HTMLInputElement>();
|
||||
const checkbox = <Checkbox value ref={ref} />;
|
||||
|
||||
expect(checkbox).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Checkbox.Group', () => {
|
||||
const group = (
|
||||
<Checkbox.Group>
|
||||
|
@ -1,8 +1,18 @@
|
||||
import Checkbox from './Checkbox';
|
||||
import InternalCheckbox, { CheckboxProps } from './Checkbox';
|
||||
import Group from './Group';
|
||||
|
||||
export { CheckboxProps, CheckboxChangeEvent } from './Checkbox';
|
||||
export { CheckboxGroupProps, CheckboxOptionType } from './Group';
|
||||
|
||||
interface CompoundedComponent
|
||||
extends React.ForwardRefExoticComponent<CheckboxProps & React.RefAttributes<HTMLInputElement>> {
|
||||
Group: typeof Group;
|
||||
__ANT_CHECKBOX: boolean;
|
||||
}
|
||||
|
||||
const Checkbox = InternalCheckbox as CompoundedComponent;
|
||||
|
||||
Checkbox.Group = Group;
|
||||
Checkbox.__ANT_CHECKBOX = true;
|
||||
|
||||
export default Checkbox;
|
||||
|
@ -34,8 +34,6 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
|
||||
const contextSize = React.useContext(SizeContext);
|
||||
const { getPrefixCls, direction, form: contextForm } = React.useContext(ConfigContext);
|
||||
|
||||
const { name } = props;
|
||||
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
className = '',
|
||||
@ -50,6 +48,7 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
|
||||
scrollToFirstError,
|
||||
requiredMark,
|
||||
onFinishFailed,
|
||||
name,
|
||||
...restFormProps
|
||||
} = props;
|
||||
|
||||
@ -118,6 +117,7 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
|
||||
<FieldForm
|
||||
id={name}
|
||||
{...restFormProps}
|
||||
name={name}
|
||||
onFinishFailed={onInternalFinishFailed}
|
||||
form={wrapForm}
|
||||
className={formClassName}
|
||||
|
@ -9,4 +9,6 @@ const Image: React.FC<ImageProps> = ({ prefixCls: customizePrefixCls, ...otherPr
|
||||
return <RcImage prefixCls={prefixCls} {...otherProps} />;
|
||||
};
|
||||
|
||||
export { ImageProps };
|
||||
|
||||
export default Image;
|
||||
|
@ -45,7 +45,7 @@ interface ClearableInputProps extends BasicProps {
|
||||
prefix?: React.ReactNode;
|
||||
addonBefore?: React.ReactNode;
|
||||
addonAfter?: React.ReactNode;
|
||||
triggerFocus: () => void;
|
||||
triggerFocus?: () => void;
|
||||
}
|
||||
|
||||
class ClearableLabeledInput extends React.Component<ClearableInputProps> {
|
||||
@ -55,7 +55,7 @@ class ClearableLabeledInput extends React.Component<ClearableInputProps> {
|
||||
onInputMouseUp: React.MouseEventHandler = e => {
|
||||
if (this.containerRef.current?.contains(e.target as Element)) {
|
||||
const { triggerFocus } = this.props;
|
||||
triggerFocus();
|
||||
triggerFocus?.();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import RcTextArea, { TextAreaProps as RcTextAreaProps, ResizableTextArea } from 'rc-textarea';
|
||||
import RcTextArea, { TextAreaProps as RcTextAreaProps } from 'rc-textarea';
|
||||
import omit from 'omit.js';
|
||||
import classNames from 'classnames';
|
||||
import useMergedState from 'rc-util/lib/hooks/useMergedState';
|
||||
import { composeRef } from 'rc-util/lib/ref';
|
||||
import ClearableLabeledInput from './ClearableLabeledInput';
|
||||
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
import { fixControlledValue, resolveOnChange } from './Input';
|
||||
import SizeContext, { SizeType } from '../config-provider/SizeContext';
|
||||
|
||||
@ -15,79 +17,68 @@ export interface TextAreaProps extends RcTextAreaProps {
|
||||
size?: SizeType;
|
||||
}
|
||||
|
||||
export interface TextAreaState {
|
||||
value: any;
|
||||
/** `value` from prev props */
|
||||
prevValue: any;
|
||||
export interface TextAreaRef extends HTMLTextAreaElement {
|
||||
resizableTextArea: any;
|
||||
}
|
||||
|
||||
class TextArea extends React.Component<TextAreaProps, TextAreaState> {
|
||||
resizableTextArea: ResizableTextArea;
|
||||
const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
|
||||
(
|
||||
{
|
||||
prefixCls: customizePrefixCls,
|
||||
bordered = true,
|
||||
showCount = false,
|
||||
maxLength,
|
||||
className,
|
||||
style,
|
||||
size: customizeSize,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { getPrefixCls, direction } = React.useContext(ConfigContext);
|
||||
const size = React.useContext(SizeContext);
|
||||
|
||||
clearableInput: ClearableLabeledInput;
|
||||
const innerRef = React.useRef<TextAreaRef>();
|
||||
const clearableInputRef = React.useRef<ClearableLabeledInput>(null);
|
||||
|
||||
constructor(props: TextAreaProps) {
|
||||
super(props);
|
||||
const value = typeof props.value === 'undefined' ? props.defaultValue : props.value;
|
||||
this.state = {
|
||||
value,
|
||||
// eslint-disable-next-line react/no-unused-state
|
||||
prevValue: props.value,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps: TextAreaProps, { prevValue }: TextAreaState) {
|
||||
const newState: Partial<TextAreaState> = { prevValue: nextProps.value };
|
||||
if (nextProps.value !== undefined || prevValue !== nextProps.value) {
|
||||
newState.value = nextProps.value;
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
|
||||
setValue(value: string, callback?: () => void) {
|
||||
if (this.props.value === undefined) {
|
||||
this.setState({ value }, callback);
|
||||
}
|
||||
}
|
||||
|
||||
focus = () => {
|
||||
this.resizableTextArea.textArea.focus();
|
||||
};
|
||||
|
||||
blur() {
|
||||
this.resizableTextArea.textArea.blur();
|
||||
}
|
||||
|
||||
setSelectionRange(start: number, end: number, direction?: 'forward' | 'backward' | 'none') {
|
||||
this.resizableTextArea.textArea.setSelectionRange(start, end, direction);
|
||||
}
|
||||
|
||||
saveTextArea = (textarea: RcTextArea) => {
|
||||
this.resizableTextArea = textarea?.resizableTextArea;
|
||||
};
|
||||
|
||||
saveClearableInput = (clearableInput: ClearableLabeledInput) => {
|
||||
this.clearableInput = clearableInput;
|
||||
};
|
||||
|
||||
handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
this.setValue(e.target.value);
|
||||
resolveOnChange(this.resizableTextArea.textArea, e, this.props.onChange);
|
||||
};
|
||||
|
||||
handleReset = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
this.setValue('', () => {
|
||||
this.focus();
|
||||
const [value, setValue] = useMergedState(props.defaultValue, {
|
||||
value: props.value,
|
||||
});
|
||||
resolveOnChange(this.resizableTextArea.textArea, e, this.props.onChange);
|
||||
};
|
||||
|
||||
renderTextArea = (prefixCls: string, bordered: boolean, size?: SizeType) => {
|
||||
const { showCount, className, style, size: customizeSize } = this.props;
|
||||
const prevValue = React.useRef(props.value);
|
||||
|
||||
return (
|
||||
React.useEffect(() => {
|
||||
if (props.value !== undefined || prevValue.current !== props.value) {
|
||||
setValue(props.value);
|
||||
prevValue.current = props.value;
|
||||
}
|
||||
}, [props.value, prevValue.current]);
|
||||
|
||||
const handleSetValue = (val: string, callback?: () => void) => {
|
||||
if (props.value === undefined) {
|
||||
setValue(val);
|
||||
callback?.();
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
handleSetValue(e.target.value);
|
||||
resolveOnChange(innerRef.current!, e, props.onChange);
|
||||
};
|
||||
|
||||
const handleReset = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
handleSetValue('', () => {
|
||||
innerRef.current?.focus();
|
||||
});
|
||||
resolveOnChange(innerRef.current!, e, props.onChange);
|
||||
};
|
||||
|
||||
const prefixCls = getPrefixCls('input', customizePrefixCls);
|
||||
|
||||
const textArea = (
|
||||
<RcTextArea
|
||||
{...omit(this.props, ['allowClear', 'bordered', 'showCount', 'size'])}
|
||||
{...omit(props, ['allowClear'])}
|
||||
maxLength={maxLength}
|
||||
className={classNames({
|
||||
[`${prefixCls}-borderless`]: !bordered,
|
||||
[className!]: className && !showCount,
|
||||
@ -96,79 +87,58 @@ class TextArea extends React.Component<TextAreaProps, TextAreaState> {
|
||||
})}
|
||||
style={showCount ? null : style}
|
||||
prefixCls={prefixCls}
|
||||
onChange={this.handleChange}
|
||||
ref={this.saveTextArea}
|
||||
onChange={handleChange}
|
||||
ref={composeRef(ref, innerRef)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderComponent = ({ getPrefixCls, direction }: ConfigConsumerProps) => {
|
||||
let value = fixControlledValue(this.state?.value);
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
bordered = true,
|
||||
showCount = false,
|
||||
maxLength,
|
||||
className,
|
||||
style,
|
||||
} = this.props;
|
||||
|
||||
const prefixCls = getPrefixCls('input', customizePrefixCls);
|
||||
let val = fixControlledValue(value) as string;
|
||||
|
||||
// Max length value
|
||||
const hasMaxLength = Number(maxLength) > 0;
|
||||
// fix #27612 将value转为数组进行截取,解决 '😂'.length === 2 等emoji表情导致的截取乱码的问题
|
||||
value = hasMaxLength ? [...value].slice(0, maxLength).join('') : value;
|
||||
val = hasMaxLength ? [...val].slice(0, maxLength).join('') : val;
|
||||
|
||||
// TextArea
|
||||
const textareaNode = (size?: SizeType) => (
|
||||
const textareaNode = (
|
||||
<ClearableLabeledInput
|
||||
{...this.props}
|
||||
{...props}
|
||||
prefixCls={prefixCls}
|
||||
direction={direction}
|
||||
inputType="text"
|
||||
value={value}
|
||||
element={this.renderTextArea(prefixCls, bordered, size)}
|
||||
handleReset={this.handleReset}
|
||||
ref={this.saveClearableInput}
|
||||
triggerFocus={this.focus}
|
||||
value={val}
|
||||
element={textArea}
|
||||
handleReset={handleReset}
|
||||
ref={clearableInputRef}
|
||||
bordered={bordered}
|
||||
/>
|
||||
);
|
||||
|
||||
// Only show text area wrapper when needed
|
||||
if (showCount) {
|
||||
const valueLength = [...value].length;
|
||||
const valueLength = [...val].length;
|
||||
const dataCount = `${valueLength}${hasMaxLength ? ` / ${maxLength}` : ''}`;
|
||||
|
||||
return (
|
||||
<SizeContext.Consumer>
|
||||
{(size?: SizeType) => (
|
||||
<div
|
||||
className={classNames(
|
||||
`${prefixCls}-textarea`,
|
||||
{
|
||||
[`${prefixCls}-textarea-rtl`]: direction === 'rtl',
|
||||
},
|
||||
`${prefixCls}-textarea-show-count`,
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
data-count={dataCount}
|
||||
>
|
||||
{textareaNode(size)}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
`${prefixCls}-textarea`,
|
||||
{
|
||||
[`${prefixCls}-textarea-rtl`]: direction === 'rtl',
|
||||
},
|
||||
`${prefixCls}-textarea-show-count`,
|
||||
className,
|
||||
)}
|
||||
</SizeContext.Consumer>
|
||||
style={style}
|
||||
data-count={dataCount}
|
||||
>
|
||||
{textareaNode}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <SizeContext.Consumer>{textareaNode}</SizeContext.Consumer>;
|
||||
};
|
||||
|
||||
render() {
|
||||
return <ConfigConsumer>{this.renderComponent}</ConfigConsumer>;
|
||||
}
|
||||
}
|
||||
return textareaNode;
|
||||
},
|
||||
);
|
||||
|
||||
export default TextArea;
|
||||
|
@ -7,7 +7,7 @@ import { sleep } from '../../../tests/utils';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
focusTest(TextArea);
|
||||
focusTest(TextArea, { refFocus: true });
|
||||
|
||||
describe('TextArea', () => {
|
||||
const originalGetComputedStyle = window.getComputedStyle;
|
||||
@ -33,10 +33,12 @@ describe('TextArea', () => {
|
||||
it('should auto calculate height according to content length', async () => {
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
const ref = React.createRef();
|
||||
|
||||
const wrapper = mount(
|
||||
<TextArea value="" readOnly autoSize={{ minRows: 2, maxRows: 6 }} wrap="off" />,
|
||||
<TextArea value="" readOnly autoSize={{ minRows: 2, maxRows: 6 }} wrap="off" ref={ref} />,
|
||||
);
|
||||
const mockFunc = jest.spyOn(wrapper.instance().resizableTextArea, 'resizeTextarea');
|
||||
const mockFunc = jest.spyOn(ref.current.resizableTextArea, 'resizeTextarea');
|
||||
wrapper.setProps({ value: '1111\n2222\n3333' });
|
||||
await sleep(0);
|
||||
expect(mockFunc).toHaveBeenCalledTimes(1);
|
||||
@ -78,8 +80,9 @@ describe('TextArea', () => {
|
||||
});
|
||||
|
||||
it('when prop value not in this.props, resizeTextarea should be called', async () => {
|
||||
const wrapper = mount(<TextArea aria-label="textarea" />);
|
||||
const resizeTextarea = jest.spyOn(wrapper.instance().resizableTextArea, 'resizeTextarea');
|
||||
const ref = React.createRef();
|
||||
const wrapper = mount(<TextArea aria-label="textarea" ref={ref} />);
|
||||
const resizeTextarea = jest.spyOn(ref.current.resizableTextArea, 'resizeTextarea');
|
||||
wrapper.find('textarea').simulate('change', {
|
||||
target: {
|
||||
value: 'test',
|
||||
@ -130,7 +133,7 @@ describe('TextArea', () => {
|
||||
const textarea = mount(<TextArea value="111" />);
|
||||
input.setProps({ value: undefined });
|
||||
textarea.setProps({ value: undefined });
|
||||
expect(textarea.find('textarea').prop('value')).toBe(input.getDOMNode().value);
|
||||
expect(textarea.find('textarea').at(0).getDOMNode().value).toBe(input.getDOMNode().value);
|
||||
});
|
||||
|
||||
describe('should support showCount', () => {
|
||||
@ -173,10 +176,11 @@ describe('TextArea', () => {
|
||||
it('set mouse cursor position', () => {
|
||||
const defaultValue = '11111';
|
||||
const valLength = defaultValue.length;
|
||||
const wrapper = mount(<TextArea autoFocus defaultValue={defaultValue} />);
|
||||
wrapper.instance().setSelectionRange(valLength, valLength);
|
||||
expect(wrapper.instance().resizableTextArea.textArea.selectionStart).toEqual(5);
|
||||
expect(wrapper.instance().resizableTextArea.textArea.selectionEnd).toEqual(5);
|
||||
const ref = React.createRef();
|
||||
mount(<TextArea autoFocus ref={ref} defaultValue={defaultValue} />);
|
||||
ref.current.resizableTextArea.textArea.setSelectionRange(valLength, valLength);
|
||||
expect(ref.current.resizableTextArea.textArea.selectionStart).toEqual(5);
|
||||
expect(ref.current.resizableTextArea.textArea.selectionEnd).toEqual(5);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -5,8 +5,8 @@ import BarsOutlined from '@ant-design/icons/BarsOutlined';
|
||||
import RightOutlined from '@ant-design/icons/RightOutlined';
|
||||
import LeftOutlined from '@ant-design/icons/LeftOutlined';
|
||||
|
||||
import { LayoutContext, LayoutContextProps } from './layout';
|
||||
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
|
||||
import { LayoutContext } from './layout';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
import isNumeric from '../_util/isNumeric';
|
||||
|
||||
const dimensionMaxMap = {
|
||||
@ -45,8 +45,6 @@ export interface SiderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
onBreakpoint?: (broken: boolean) => void;
|
||||
}
|
||||
|
||||
type InternalSideProps = SiderProps & LayoutContextProps;
|
||||
|
||||
export interface SiderState {
|
||||
collapsed?: boolean;
|
||||
below: boolean;
|
||||
@ -60,129 +58,94 @@ const generateId = (() => {
|
||||
};
|
||||
})();
|
||||
|
||||
class InternalSider extends React.Component<InternalSideProps, SiderState> {
|
||||
static defaultProps = {
|
||||
collapsible: false,
|
||||
defaultCollapsed: false,
|
||||
reverseArrow: false,
|
||||
width: 200,
|
||||
collapsedWidth: 80,
|
||||
style: {},
|
||||
theme: 'dark' as SiderTheme,
|
||||
};
|
||||
const Sider: React.FC<SiderProps> = ({
|
||||
prefixCls: customizePrefixCls,
|
||||
className,
|
||||
trigger,
|
||||
children,
|
||||
defaultCollapsed = false,
|
||||
theme = 'dark',
|
||||
style = {},
|
||||
collapsible = false,
|
||||
reverseArrow = false,
|
||||
width = 200,
|
||||
collapsedWidth = 80,
|
||||
zeroWidthTriggerStyle,
|
||||
breakpoint,
|
||||
onCollapse,
|
||||
onBreakpoint,
|
||||
...props
|
||||
}) => {
|
||||
const { siderHook } = React.useContext(LayoutContext);
|
||||
|
||||
static getDerivedStateFromProps(nextProps: InternalSideProps) {
|
||||
if ('collapsed' in nextProps) {
|
||||
return {
|
||||
collapsed: nextProps.collapsed,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const [collapsed, setCollapsed] = React.useState(
|
||||
'collapsed' in props ? props.collapsed : defaultCollapsed,
|
||||
);
|
||||
const [below, setBelow] = React.useState(false);
|
||||
|
||||
private mql: MediaQueryList;
|
||||
|
||||
private uniqueId: string;
|
||||
|
||||
constructor(props: InternalSideProps) {
|
||||
super(props);
|
||||
this.uniqueId = generateId('ant-sider-');
|
||||
let matchMedia;
|
||||
if (typeof window !== 'undefined') {
|
||||
matchMedia = window.matchMedia;
|
||||
}
|
||||
if (matchMedia && props.breakpoint && props.breakpoint in dimensionMaxMap) {
|
||||
this.mql = matchMedia(`(max-width: ${dimensionMaxMap[props.breakpoint]})`);
|
||||
}
|
||||
let collapsed;
|
||||
React.useEffect(() => {
|
||||
if ('collapsed' in props) {
|
||||
collapsed = props.collapsed;
|
||||
} else {
|
||||
collapsed = props.defaultCollapsed;
|
||||
setCollapsed(props.collapsed);
|
||||
}
|
||||
this.state = {
|
||||
collapsed,
|
||||
below: false,
|
||||
};
|
||||
}
|
||||
}, [props.collapsed]);
|
||||
|
||||
componentDidMount() {
|
||||
if (this.mql) {
|
||||
try {
|
||||
this.mql.addEventListener('change', this.responsiveHandler);
|
||||
} catch (error) {
|
||||
this.mql.addListener(this.responsiveHandler);
|
||||
}
|
||||
this.responsiveHandler(this.mql);
|
||||
const handleSetCollapsed = (value: boolean, type: CollapseType) => {
|
||||
if (!('collapsed' in props)) {
|
||||
setCollapsed(value);
|
||||
}
|
||||
|
||||
this.props?.siderHook.addSider(this.uniqueId);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
try {
|
||||
this.mql?.removeEventListener('change', this.responsiveHandler);
|
||||
} catch (error) {
|
||||
this.mql?.removeListener(this.responsiveHandler);
|
||||
}
|
||||
this.props?.siderHook.removeSider(this.uniqueId);
|
||||
}
|
||||
|
||||
responsiveHandler = (mql: MediaQueryListEvent | MediaQueryList) => {
|
||||
this.setState({ below: mql.matches });
|
||||
const { onBreakpoint } = this.props;
|
||||
const { collapsed } = this.state;
|
||||
if (onBreakpoint) {
|
||||
onBreakpoint(mql.matches);
|
||||
}
|
||||
if (collapsed !== mql.matches) {
|
||||
this.setCollapsed(mql.matches, 'responsive');
|
||||
}
|
||||
};
|
||||
|
||||
setCollapsed = (collapsed: boolean, type: CollapseType) => {
|
||||
if (!('collapsed' in this.props)) {
|
||||
this.setState({
|
||||
collapsed,
|
||||
});
|
||||
}
|
||||
const { onCollapse } = this.props;
|
||||
if (onCollapse) {
|
||||
onCollapse(collapsed, type);
|
||||
onCollapse(value, type);
|
||||
}
|
||||
};
|
||||
|
||||
toggle = () => {
|
||||
const collapsed = !this.state.collapsed;
|
||||
this.setCollapsed(collapsed, 'clickTrigger');
|
||||
React.useEffect(() => {
|
||||
const responsiveHandler = (mql: MediaQueryListEvent | MediaQueryList) => {
|
||||
setBelow(mql.matches);
|
||||
if (onBreakpoint) {
|
||||
onBreakpoint(mql.matches);
|
||||
}
|
||||
if (collapsed !== mql.matches) {
|
||||
handleSetCollapsed(mql.matches, 'responsive');
|
||||
}
|
||||
};
|
||||
|
||||
let mql: MediaQueryList;
|
||||
if (typeof window !== 'undefined') {
|
||||
const { matchMedia } = window;
|
||||
if (matchMedia && breakpoint && breakpoint in dimensionMaxMap) {
|
||||
mql = matchMedia(`(max-width: ${dimensionMaxMap[breakpoint]})`);
|
||||
try {
|
||||
mql.addEventListener('change', responsiveHandler);
|
||||
} catch (error) {
|
||||
mql.addListener(responsiveHandler);
|
||||
}
|
||||
responsiveHandler(mql);
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
try {
|
||||
mql?.removeEventListener('change', responsiveHandler);
|
||||
} catch (error) {
|
||||
mql?.removeListener(responsiveHandler);
|
||||
}
|
||||
};
|
||||
}, [onBreakpoint, collapsed]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const uniqueId = generateId('ant-sider-');
|
||||
siderHook.addSider(uniqueId);
|
||||
return () => siderHook.removeSider(uniqueId);
|
||||
}, []);
|
||||
|
||||
const toggle = () => {
|
||||
handleSetCollapsed(!collapsed, 'clickTrigger');
|
||||
};
|
||||
|
||||
renderSider = ({ getPrefixCls }: ConfigConsumerProps) => {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
className,
|
||||
theme,
|
||||
collapsible,
|
||||
reverseArrow,
|
||||
trigger,
|
||||
style,
|
||||
width,
|
||||
collapsedWidth,
|
||||
zeroWidthTriggerStyle,
|
||||
children,
|
||||
...others
|
||||
} = this.props;
|
||||
const { collapsed, below } = this.state;
|
||||
const { getPrefixCls } = React.useContext(ConfigContext);
|
||||
|
||||
const renderSider = () => {
|
||||
const prefixCls = getPrefixCls('layout-sider', customizePrefixCls);
|
||||
const divProps = omit(others, [
|
||||
'collapsed',
|
||||
'defaultCollapsed',
|
||||
'onCollapse',
|
||||
'breakpoint',
|
||||
'onBreakpoint',
|
||||
'siderHook',
|
||||
'zeroWidthTriggerStyle',
|
||||
]);
|
||||
const divProps = omit(props, ['collapsed']);
|
||||
const rawWidth = collapsed ? collapsedWidth : width;
|
||||
// use "px" as fallback unit for width
|
||||
const siderWidth = isNumeric(rawWidth) ? `${rawWidth}px` : String(rawWidth);
|
||||
@ -190,7 +153,7 @@ class InternalSider extends React.Component<InternalSideProps, SiderState> {
|
||||
const zeroWidthTrigger =
|
||||
parseFloat(String(collapsedWidth || 0)) === 0 ? (
|
||||
<span
|
||||
onClick={this.toggle}
|
||||
onClick={toggle}
|
||||
className={classNames(
|
||||
`${prefixCls}-zero-width-trigger`,
|
||||
`${prefixCls}-zero-width-trigger-${reverseArrow ? 'right' : 'left'}`,
|
||||
@ -209,11 +172,7 @@ class InternalSider extends React.Component<InternalSideProps, SiderState> {
|
||||
const triggerDom =
|
||||
trigger !== null
|
||||
? zeroWidthTrigger || (
|
||||
<div
|
||||
className={`${prefixCls}-trigger`}
|
||||
onClick={this.toggle}
|
||||
style={{ width: siderWidth }}
|
||||
>
|
||||
<div className={`${prefixCls}-trigger`} onClick={toggle} style={{ width: siderWidth }}>
|
||||
{trigger || defaultTrigger}
|
||||
</div>
|
||||
)
|
||||
@ -244,29 +203,16 @@ class InternalSider extends React.Component<InternalSideProps, SiderState> {
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { collapsed } = this.state;
|
||||
const { collapsedWidth } = this.props;
|
||||
return (
|
||||
<SiderContext.Provider
|
||||
value={{
|
||||
siderCollapsed: collapsed,
|
||||
collapsedWidth,
|
||||
}}
|
||||
>
|
||||
<ConfigConsumer>{this.renderSider}</ConfigConsumer>
|
||||
</SiderContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<SiderContext.Provider
|
||||
value={{
|
||||
siderCollapsed: collapsed,
|
||||
collapsedWidth,
|
||||
}}
|
||||
>
|
||||
{renderSider()}
|
||||
</SiderContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
export default class Sider extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<LayoutContext.Consumer>
|
||||
{(context: LayoutContextProps) => <InternalSider {...context} {...this.props} />}
|
||||
</LayoutContext.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default Sider;
|
||||
|
@ -35,3 +35,49 @@ exports[`Layout rtl render component should be rendered correctly in RTL directi
|
||||
/>
|
||||
</aside>
|
||||
`;
|
||||
|
||||
exports[`Layout should be controlled by collapsed 1`] = `
|
||||
<Sider>
|
||||
<aside
|
||||
className="ant-layout-sider ant-layout-sider-dark"
|
||||
style={
|
||||
Object {
|
||||
"flex": "0 0 200px",
|
||||
"maxWidth": "200px",
|
||||
"minWidth": "200px",
|
||||
"width": "200px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="ant-layout-sider-children"
|
||||
>
|
||||
Sider
|
||||
</div>
|
||||
</aside>
|
||||
</Sider>
|
||||
`;
|
||||
|
||||
exports[`Layout should be controlled by collapsed 2`] = `
|
||||
<Sider
|
||||
collapsed={true}
|
||||
>
|
||||
<aside
|
||||
className="ant-layout-sider ant-layout-sider-dark ant-layout-sider-collapsed"
|
||||
style={
|
||||
Object {
|
||||
"flex": "0 0 80px",
|
||||
"maxWidth": "80px",
|
||||
"minWidth": "80px",
|
||||
"width": "80px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="ant-layout-sider-children"
|
||||
>
|
||||
Sider
|
||||
</div>
|
||||
</aside>
|
||||
</Sider>
|
||||
`;
|
||||
|
@ -127,9 +127,10 @@ describe('Layout', () => {
|
||||
|
||||
it('should be controlled by collapsed', () => {
|
||||
const wrapper = mount(<Sider>Sider</Sider>);
|
||||
expect(wrapper.find('InternalSider').instance().state.collapsed).toBe(false);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
wrapper.setProps({ collapsed: true });
|
||||
expect(wrapper.find('InternalSider').instance().state.collapsed).toBe(true);
|
||||
wrapper.update();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should not add ant-layout-has-sider when `hasSider` is `false`', () => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import InternalLayout, { BasicProps, Content, Footer, Header } from './layout';
|
||||
import Sider, { SiderProps } from './Sider';
|
||||
import Sider from './Sider';
|
||||
|
||||
export { BasicProps as LayoutProps } from './layout';
|
||||
export { SiderProps } from './Sider';
|
||||
@ -8,7 +8,7 @@ interface LayoutType extends React.FC<BasicProps> {
|
||||
Header: typeof Header;
|
||||
Footer: typeof Footer;
|
||||
Content: typeof Content;
|
||||
Sider: React.ComponentClass<SiderProps>;
|
||||
Sider: typeof Sider;
|
||||
}
|
||||
|
||||
const Layout = InternalLayout as LayoutType;
|
||||
|
@ -54,17 +54,6 @@ const BasicLayout: React.FC<BasicPropsWithTagName> = props => {
|
||||
|
||||
const [siders, setSiders] = React.useState<string[]>([]);
|
||||
|
||||
const getSiderHook = () => {
|
||||
return {
|
||||
addSider: (id: string) => {
|
||||
setSiders([...siders, id]);
|
||||
},
|
||||
removeSider: (id: string) => {
|
||||
setSiders(siders.filter(currentId => currentId !== id));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const { prefixCls, className, children, hasSider, tagName: Tag, ...others } = props;
|
||||
const classString = classNames(
|
||||
prefixCls,
|
||||
@ -76,7 +65,18 @@ const BasicLayout: React.FC<BasicPropsWithTagName> = props => {
|
||||
);
|
||||
|
||||
return (
|
||||
<LayoutContext.Provider value={{ siderHook: getSiderHook() }}>
|
||||
<LayoutContext.Provider
|
||||
value={{
|
||||
siderHook: {
|
||||
addSider: (id: string) => {
|
||||
setSiders(prev => [...prev, id]);
|
||||
},
|
||||
removeSider: (id: string) => {
|
||||
setSiders(prev => prev.filter(currentId => currentId !== id));
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tag className={classString} {...others}>
|
||||
{children}
|
||||
</Tag>
|
||||
|
@ -8,6 +8,7 @@
|
||||
@skeleton-button-prefix-cls: ~'@{skeleton-prefix-cls}-button';
|
||||
@skeleton-input-prefix-cls: ~'@{skeleton-prefix-cls}-input';
|
||||
@skeleton-image-prefix-cls: ~'@{skeleton-prefix-cls}-image';
|
||||
@skeleton-block-radius: 4px;
|
||||
|
||||
.@{skeleton-prefix-cls} {
|
||||
display: table;
|
||||
@ -35,6 +36,7 @@
|
||||
height: @skeleton-title-height;
|
||||
margin-top: @margin-md;
|
||||
background: @skeleton-color;
|
||||
border-radius: @skeleton-block-radius;
|
||||
|
||||
+ .@{skeleton-paragraph-prefix-cls} {
|
||||
margin-top: @skeleton-title-paragraph-margin-top;
|
||||
@ -50,6 +52,7 @@
|
||||
height: @skeleton-paragraph-li-height;
|
||||
list-style: none;
|
||||
background: @skeleton-color;
|
||||
border-radius: @skeleton-block-radius;
|
||||
|
||||
&:last-child:not(:first-child):not(:nth-child(2)) {
|
||||
width: 61%;
|
||||
|
@ -42,4 +42,11 @@ describe('Spin', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
Spin.setDefaultIndicator(null);
|
||||
});
|
||||
|
||||
it('should render 0', () => {
|
||||
const wrapper = mount(
|
||||
<Spin>{0}</Spin>,
|
||||
);
|
||||
expect(wrapper.find('.ant-spin-container').at(0).text()).toBe('0');
|
||||
});
|
||||
});
|
||||
|
@ -127,7 +127,7 @@ class Spin extends React.Component<SpinProps, SpinState> {
|
||||
}
|
||||
|
||||
isNestedPattern() {
|
||||
return !!(this.props && this.props.children);
|
||||
return !!(this.props && typeof this.props.children !== 'undefined');
|
||||
}
|
||||
|
||||
renderSpin = ({ getPrefixCls, direction }: ConfigConsumerProps) => {
|
||||
|
@ -409,7 +409,6 @@
|
||||
|
||||
// Skeleton
|
||||
// ---
|
||||
@skeleton-color: #303030;
|
||||
@skeleton-to-color: fade(@white, 16%);
|
||||
|
||||
// Alert
|
||||
|
@ -275,11 +275,13 @@
|
||||
@radio-wrapper-margin-right: 8px;
|
||||
|
||||
// Media queries breakpoints
|
||||
// Extra small screen / phone
|
||||
// @screen-xs and @screen-xs-min is not used in Grid
|
||||
// smallest break point is @screen-md
|
||||
@screen-xs: 480px;
|
||||
@screen-xs-min: @screen-xs;
|
||||
// 👆 Extra small screen / phone
|
||||
|
||||
// Small screen / tablet
|
||||
// 👇 Small screen / tablet
|
||||
@screen-sm: 576px;
|
||||
@screen-sm-min: @screen-sm;
|
||||
|
||||
@ -858,7 +860,7 @@
|
||||
|
||||
// Skeleton
|
||||
// ---
|
||||
@skeleton-color: #f2f2f2;
|
||||
@skeleton-color: rgba(190, 190, 190, .2);
|
||||
@skeleton-to-color: shade(@skeleton-color, 5%);
|
||||
@skeleton-paragraph-margin-top: 28px;
|
||||
@skeleton-paragraph-li-margin-top: @margin-md;
|
||||
|
@ -107,11 +107,10 @@
|
||||
|
||||
// ======================== Loading =========================
|
||||
&-loading-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
position: relative;
|
||||
top: (@switch-pin-size - @font-size-base) / 2;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
transform: translate(-50%, -50%);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
&-checked &-loading-icon {
|
||||
@ -135,7 +134,8 @@
|
||||
}
|
||||
|
||||
.@{switch-prefix-cls}-loading-icon {
|
||||
transform: translate(-50%, -50%) scale(0.66667);
|
||||
top: (@switch-sm-pin-size - 9px) / 2;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
&.@{switch-prefix-cls}-checked {
|
||||
|
@ -1266,7 +1266,6 @@ exports[`Transfer should support render value and label in item 1`] = `
|
||||
>
|
||||
<Checkbox
|
||||
checked={false}
|
||||
indeterminate={false}
|
||||
>
|
||||
<label
|
||||
className="ant-checkbox-wrapper"
|
||||
|
@ -23,18 +23,12 @@
|
||||
&_close {
|
||||
.@{tree-prefix-cls}-switcher-icon {
|
||||
svg {
|
||||
.@{tree-prefix-cls}-rtl& {
|
||||
.@{tree-prefix-cls}-rtl & {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-loading-icon {
|
||||
.@{tree-prefix-cls}-rtl& {
|
||||
transform: scaleY(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
// ==================== Show Line =====================
|
||||
&-show-line {
|
||||
|
@ -132,7 +132,7 @@ class Base extends React.Component<InternalBlockProps, BaseState> {
|
||||
|
||||
context: ConfigConsumerProps;
|
||||
|
||||
editIcon?: TransButton;
|
||||
editIcon?: HTMLDivElement;
|
||||
|
||||
contentRef = React.createRef<HTMLElement>();
|
||||
|
||||
@ -257,7 +257,7 @@ class Base extends React.Component<InternalBlockProps, BaseState> {
|
||||
};
|
||||
}
|
||||
|
||||
setEditRef = (node: TransButton) => {
|
||||
setEditRef = (node: HTMLDivElement) => {
|
||||
this.editIcon = node;
|
||||
};
|
||||
|
||||
|
@ -115,6 +115,7 @@ const Demo = () => (
|
||||
- Form rewrite.
|
||||
- No need to use `Form.create`.
|
||||
- Nest fields definition changes from `'xxx.yyy'` to `['xxx', 'yyy']`.
|
||||
- `validateTrigger` no long collect field value.
|
||||
- See [here](/components/form/v3) for migration documentation.
|
||||
- DatePicker rewrite
|
||||
- Provide the `picker` property for selector switching.
|
||||
@ -124,6 +125,7 @@ const Demo = () => (
|
||||
- Tree, Select, TreeSelect, AutoComplete rewrite
|
||||
- use virtual scrolling.
|
||||
- `onBlur` no longer trigger value change and return React origin `event` object instead.
|
||||
- If config `validateTrigger` as `onBlur` with compatible Form, please change to `onChange` instead.
|
||||
- AutoComplete no longer support `optionLabelProp`. Please set Option `value` directly.
|
||||
- AutoComplete options definition align with Select. Please use `options` instead of `dataSource`.
|
||||
- Select remove `dropdownMenuStyle` prop.
|
||||
|
@ -115,6 +115,7 @@ const Demo = () => (
|
||||
- Form 重写
|
||||
- 不再需要 `Form.create`。
|
||||
- 嵌套字段支持从 `'xxx.yyy'` 改成 `['xxx', 'yyy']`。
|
||||
- `validateTrigger` 不再收集字段值。
|
||||
- 迁移文档请查看[此处](/components/form/v3)。
|
||||
- DatePicker 重写
|
||||
- 提供 `picker` 属性用于选择器切换。
|
||||
@ -124,6 +125,7 @@ const Demo = () => (
|
||||
- Tree、Select、TreeSelect、AutoComplete 重新写
|
||||
- 使用虚拟滚动。
|
||||
- `onBlur` 时不再修改选中值,且返回 React 原生的 `event` 对象。
|
||||
- 如果你在使用兼容包的 Form 且配置了 `validateTrigger` 为 `onBlur`,请改至 `onChange` 以做兼容。
|
||||
- AutoComplete 不再支持 `optionLabelProp`,请直接设置 Option `value` 属性。
|
||||
- AutoComplete 选项与 Select 对齐,请使用 `options` 代替 `dataSource`。
|
||||
- Select 移除 `dropdownMenuStyle` 属性。
|
||||
|
@ -175,7 +175,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^4.1.1",
|
||||
"@typescript-eslint/parser": "^4.1.1",
|
||||
"antd-img-crop": "^3.1.1",
|
||||
"antd-pro-merge-less": "^3.0.9",
|
||||
"antd-pro-merge-less": "^3.0.11",
|
||||
"antd-theme-generator": "^1.2.3",
|
||||
"argos-cli": "^0.3.0",
|
||||
"array-move": "^3.0.0",
|
||||
|
Loading…
Reference in New Issue
Block a user