Merge pull request #27893 from ant-design/merge-feature

chore: mergin master into feature
This commit is contained in:
二货机器人 2020-11-20 11:42:41 +08:00 committed by GitHub
commit e18c13940f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 2234 additions and 2751 deletions

View File

@ -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)
## ⌨️ 本地开发

View File

@ -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)

View File

@ -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', () => {

View File

@ -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;
}

View File

@ -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;

View File

@ -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<

View File

@ -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>
);
};

View File

@ -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;

View File

@ -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

View File

@ -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();
});
});

View File

@ -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>
);
};

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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>

View File

@ -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;

View File

@ -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}

View File

@ -9,4 +9,6 @@ const Image: React.FC<ImageProps> = ({ prefixCls: customizePrefixCls, ...otherPr
return <RcImage prefixCls={prefixCls} {...otherProps} />;
};
export { ImageProps };
export default Image;

View File

@ -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?.();
}
};

View File

@ -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;

View File

@ -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);
});
});

View File

@ -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;

View File

@ -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>
`;

View File

@ -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`', () => {

View File

@ -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;

View File

@ -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>

View File

@ -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%;

View File

@ -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');
});
});

View File

@ -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) => {

View File

@ -409,7 +409,6 @@
// Skeleton
// ---
@skeleton-color: #303030;
@skeleton-to-color: fade(@white, 16%);
// Alert

View File

@ -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;

View File

@ -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 {

View File

@ -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"

View File

@ -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 {

View File

@ -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;
};

View File

@ -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.

View File

@ -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` 属性。

View File

@ -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",