mirror of
https://github.com/ant-design/ant-design.git
synced 2025-06-07 09:26:06 +08:00
commit
5a93cc024d
@ -257,6 +257,16 @@ export default class Anchor extends React.Component<AnchorProps, AnchorState, Co
|
||||
}
|
||||
};
|
||||
|
||||
getMemoizedContextValue = memoizeOne(
|
||||
(link: AntAnchor['activeLink'], onClickFn: AnchorProps['onClick']): AntAnchor => ({
|
||||
registerLink: this.registerLink,
|
||||
unregisterLink: this.unregisterLink,
|
||||
scrollTo: this.handleScrollTo,
|
||||
activeLink: link,
|
||||
onClick: onClickFn,
|
||||
}),
|
||||
);
|
||||
|
||||
render() {
|
||||
const { getPrefixCls, direction } = this.context;
|
||||
const {
|
||||
@ -310,13 +320,7 @@ export default class Anchor extends React.Component<AnchorProps, AnchorState, Co
|
||||
</div>
|
||||
);
|
||||
|
||||
const contextValue = memoizeOne((link, onClickFn) => ({
|
||||
registerLink: this.registerLink,
|
||||
unregisterLink: this.unregisterLink,
|
||||
scrollTo: this.handleScrollTo,
|
||||
activeLink: link,
|
||||
onClick: onClickFn,
|
||||
}))(activeLink, onClick);
|
||||
const contextValue = this.getMemoizedContextValue(activeLink, onClick);
|
||||
|
||||
return (
|
||||
<AnchorContext.Provider value={contextValue}>
|
||||
|
51
components/anchor/__tests__/cached-context.test.tsx
Normal file
51
components/anchor/__tests__/cached-context.test.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import React, { memo, useState, useRef, useContext } from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import Anchor from '../Anchor';
|
||||
import AnchorContext from '../context';
|
||||
|
||||
// we use'memo' here in order to only render inner component while context changed.
|
||||
const CacheInner = memo(() => {
|
||||
const countRef = useRef(0);
|
||||
countRef.current++;
|
||||
// subscribe anchor context
|
||||
useContext(AnchorContext);
|
||||
return (
|
||||
<div>
|
||||
Child Rendering Count: <span id="child_count">{countRef.current}</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const CacheOuter = () => {
|
||||
// We use 'useState' here in order to trigger parent component rendering.
|
||||
const [count, setCount] = useState(1);
|
||||
const handleClick = () => {
|
||||
setCount(count + 1);
|
||||
};
|
||||
// During each rendering phase, the cached context value returned from method 'Anchor#getMemoizedContextValue' will take effect.
|
||||
// So 'CacheInner' component won't rerender.
|
||||
return (
|
||||
<div>
|
||||
<button type="button" onClick={handleClick} id="parent_btn">
|
||||
Click
|
||||
</button>
|
||||
Parent Rendering Count: <span id="parent_count">{count}</span>
|
||||
<Anchor affix={false}>
|
||||
<CacheInner />
|
||||
</Anchor>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
it("Rendering on Anchor without changed AnchorContext won't trigger rendering on child component.", () => {
|
||||
const wrapper = mount(<CacheOuter />);
|
||||
const childCount = wrapper.find('#child_count').text();
|
||||
wrapper.find('#parent_btn').at(0).simulate('click');
|
||||
expect(wrapper.find('#parent_count').text()).toBe('2');
|
||||
// child component won't rerender
|
||||
expect(wrapper.find('#child_count').text()).toBe(childCount);
|
||||
wrapper.find('#parent_btn').at(0).simulate('click');
|
||||
expect(wrapper.find('#parent_count').text()).toBe('3');
|
||||
// child component won't rerender
|
||||
expect(wrapper.find('#child_count').text()).toBe(childCount);
|
||||
});
|
@ -40,6 +40,57 @@ exports[`renders ./components/image/demo/basic.md extend context correctly 1`] =
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/image/demo/controlled-preview.md extend context correctly 1`] = `
|
||||
Array [
|
||||
<button
|
||||
class="ant-btn ant-btn-primary"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
show image preview
|
||||
</span>
|
||||
</button>,
|
||||
<div
|
||||
class="ant-image"
|
||||
style="width:200px"
|
||||
>
|
||||
<img
|
||||
class="ant-image-img"
|
||||
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png?x-oss-process=image/blur,r_50,s_50/quality,q_1/resize,m_mfit,h_200,w_200"
|
||||
style="display:none"
|
||||
/>
|
||||
<div
|
||||
class="ant-image-mask"
|
||||
>
|
||||
<div
|
||||
class="ant-image-mask-info"
|
||||
>
|
||||
<span
|
||||
aria-label="eye"
|
||||
class="anticon anticon-eye"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="eye"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
Preview
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`renders ./components/image/demo/fallback.md extend context correctly 1`] = `
|
||||
<div
|
||||
class="ant-image"
|
||||
|
@ -40,6 +40,57 @@ exports[`renders ./components/image/demo/basic.md correctly 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/image/demo/controlled-preview.md correctly 1`] = `
|
||||
Array [
|
||||
<button
|
||||
class="ant-btn ant-btn-primary"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
show image preview
|
||||
</span>
|
||||
</button>,
|
||||
<div
|
||||
class="ant-image"
|
||||
style="width:200px"
|
||||
>
|
||||
<img
|
||||
class="ant-image-img"
|
||||
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png?x-oss-process=image/blur,r_50,s_50/quality,q_1/resize,m_mfit,h_200,w_200"
|
||||
style="display:none"
|
||||
/>
|
||||
<div
|
||||
class="ant-image-mask"
|
||||
>
|
||||
<div
|
||||
class="ant-image-mask-info"
|
||||
>
|
||||
<span
|
||||
aria-label="eye"
|
||||
class="anticon anticon-eye"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="eye"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
Preview
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`renders ./components/image/demo/fallback.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-image"
|
||||
|
44
components/image/demo/controlled-preview.md
Normal file
44
components/image/demo/controlled-preview.md
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
order: 7
|
||||
title:
|
||||
zh-CN: 受控的预览
|
||||
en-US: Controlled Preview
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
可以使预览受控。
|
||||
|
||||
## en-US
|
||||
|
||||
You can make preview controlled.
|
||||
|
||||
```jsx
|
||||
import React, { useState } from 'react';
|
||||
import { Image, Button } from 'antd';
|
||||
|
||||
function ImageDemo() {
|
||||
const [visible, setVisible] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<Button type="primary" onClick={() => setVisible(true)}>
|
||||
show image preview
|
||||
</Button>
|
||||
<Image
|
||||
width={200}
|
||||
style={{ display: 'none' }}
|
||||
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png?x-oss-process=image/blur,r_50,s_50/quality,q_1/resize,m_mfit,h_200,w_200"
|
||||
preview={{
|
||||
visible,
|
||||
src: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
|
||||
onVisibleChange: value => {
|
||||
setVisible(value);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<ImageDemo />, mountNode);
|
||||
```
|
@ -148,7 +148,7 @@ class ClearableLabeledInput extends React.Component<ClearableInputProps> {
|
||||
}
|
||||
|
||||
renderInputWithLabel(prefixCls: string, labeledElement: React.ReactElement) {
|
||||
const { addonBefore, addonAfter, style, size, className, direction } = this.props;
|
||||
const { addonBefore, addonAfter, style, size, className, direction, hidden } = this.props;
|
||||
// Not wrap when there is not addons
|
||||
if (!hasAddon(this.props)) {
|
||||
return labeledElement;
|
||||
@ -178,7 +178,7 @@ class ClearableLabeledInput extends React.Component<ClearableInputProps> {
|
||||
// Need another wrapper for changing display:table to display:inline-block
|
||||
// and put style prop in wrapper
|
||||
return (
|
||||
<span className={mergedGroupClassName} style={style}>
|
||||
<span className={mergedGroupClassName} style={style} hidden={hidden}>
|
||||
<span className={mergedWrapperClassName}>
|
||||
{addonBeforeNode}
|
||||
{cloneElement(labeledElement, { style: null })}
|
||||
@ -189,7 +189,7 @@ class ClearableLabeledInput extends React.Component<ClearableInputProps> {
|
||||
}
|
||||
|
||||
renderTextAreaWithClearIcon(prefixCls: string, element: React.ReactElement) {
|
||||
const { value, allowClear, className, style, direction, bordered } = this.props;
|
||||
const { value, allowClear, className, style, direction, bordered, hidden } = this.props;
|
||||
if (!allowClear) {
|
||||
return cloneElement(element, {
|
||||
value,
|
||||
@ -206,7 +206,7 @@ class ClearableLabeledInput extends React.Component<ClearableInputProps> {
|
||||
},
|
||||
);
|
||||
return (
|
||||
<span className={affixWrapperCls} style={style}>
|
||||
<span className={affixWrapperCls} style={style} hidden={hidden}>
|
||||
{cloneElement(element, {
|
||||
style: null,
|
||||
value,
|
||||
|
@ -58,6 +58,7 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
|
||||
const [value, setValue] = useMergedState(props.defaultValue, {
|
||||
value: props.value,
|
||||
});
|
||||
const { hidden } = props;
|
||||
|
||||
const handleSetValue = (val: string, callback?: () => void) => {
|
||||
if (props.value === undefined) {
|
||||
@ -174,6 +175,7 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
|
||||
|
||||
return (
|
||||
<div
|
||||
hidden={hidden}
|
||||
className={classNames(
|
||||
`${prefixCls}-textarea`,
|
||||
{
|
||||
|
@ -111,6 +111,64 @@ describe('prefix and suffix', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Input setting hidden', () => {
|
||||
it('should support hidden when has prefix or suffix or showCount or allowClear or addonBefore or addonAfter', () => {
|
||||
const wrapper = mount(
|
||||
<>
|
||||
<Input
|
||||
hidden
|
||||
className="input"
|
||||
showCount
|
||||
allowClear
|
||||
prefix="11"
|
||||
suffix="22"
|
||||
addonBefore="http://"
|
||||
addonAfter=".com"
|
||||
defaultValue="mysite1"
|
||||
/>
|
||||
<Input.Search
|
||||
hidden
|
||||
className="input-search"
|
||||
showCount
|
||||
allowClear
|
||||
prefix="11"
|
||||
suffix="22"
|
||||
addonBefore="http://"
|
||||
addonAfter=".com"
|
||||
defaultValue="mysite1"
|
||||
/>
|
||||
<Input.TextArea
|
||||
hidden
|
||||
className="input-textarea"
|
||||
showCount
|
||||
allowClear
|
||||
prefix="11"
|
||||
suffix="22"
|
||||
addonBefore="http://"
|
||||
addonAfter=".com"
|
||||
defaultValue="mysite1"
|
||||
/>
|
||||
<Input.Password
|
||||
hidden
|
||||
className="input-password"
|
||||
showCount
|
||||
allowClear
|
||||
prefix="11"
|
||||
suffix="22"
|
||||
addonBefore="http://"
|
||||
addonAfter=".com"
|
||||
defaultValue="mysite1"
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
|
||||
expect(wrapper.find('.input').at(0).getDOMNode().hidden).toBe(true);
|
||||
expect(wrapper.find('.input-search').at(0).getDOMNode().hidden).toBe(true);
|
||||
expect(wrapper.find('.input-textarea').at(0).getDOMNode().hidden).toBe(true);
|
||||
expect(wrapper.find('.input-password').at(0).getDOMNode().hidden).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('As Form Control', () => {
|
||||
it('should be reset when wrapped in form.getFieldDecorator without initialValue', () => {
|
||||
const Demo = () => {
|
||||
|
@ -167819,7 +167819,7 @@ exports[`Locale Provider should display the text as km 1`] = `
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
យល់ព្រម
|
||||
បោះបង់
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
@ -169383,7 +169383,7 @@ exports[`Locale Provider should display the text as km 1`] = `
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
យល់ព្រម
|
||||
បោះបង់
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
|
53
components/locale-provider/__tests__/cached-context.test.tsx
Normal file
53
components/locale-provider/__tests__/cached-context.test.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import React, { memo, useState, useRef, useContext } from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import LocaleProvider, { ANT_MARK } from '..';
|
||||
import LocaleContext from '../context';
|
||||
|
||||
const defaultLocale = {
|
||||
locale: 'locale',
|
||||
};
|
||||
// we use'memo' here in order to only render inner component while context changed.
|
||||
const CacheInner = memo(() => {
|
||||
const countRef = useRef(0);
|
||||
countRef.current++;
|
||||
// subscribe locale context
|
||||
useContext(LocaleContext);
|
||||
return (
|
||||
<div>
|
||||
Child Rendering Count: <span id="child_count">{countRef.current}</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const CacheOuter = () => {
|
||||
// We use 'useState' here in order to trigger parent component rendering.
|
||||
const [count, setCount] = useState(1);
|
||||
const handleClick = () => {
|
||||
setCount(count + 1);
|
||||
};
|
||||
// During each rendering phase, the cached context value returned from method 'LocaleProvider#getMemoizedContextValue' will take effect.
|
||||
// So 'CacheInner' component won't rerender.
|
||||
return (
|
||||
<div>
|
||||
<button type="button" onClick={handleClick} id="parent_btn">
|
||||
Click
|
||||
</button>
|
||||
Parent Rendering Count: <span id="parent_count">{count}</span>
|
||||
<LocaleProvider locale={defaultLocale} _ANT_MARK__={ANT_MARK}>
|
||||
<CacheInner />
|
||||
</LocaleProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
it("Rendering on LocaleProvider won't trigger rendering on child component.", () => {
|
||||
const wrapper = mount(<CacheOuter />);
|
||||
wrapper.find('#parent_btn').at(0).simulate('click');
|
||||
expect(wrapper.find('#parent_count').text()).toBe('2');
|
||||
// child component won't rerender
|
||||
expect(wrapper.find('#child_count').text()).toBe('1');
|
||||
wrapper.find('#parent_btn').at(0).simulate('click');
|
||||
expect(wrapper.find('#parent_count').text()).toBe('3');
|
||||
// child component won't rerender
|
||||
expect(wrapper.find('#child_count').text()).toBe('1');
|
||||
});
|
@ -78,12 +78,14 @@ export default class LocaleProvider extends React.Component<LocaleProviderProps,
|
||||
changeConfirmLocale();
|
||||
}
|
||||
|
||||
getMemoizedContextValue = memoizeOne((localeValue: Locale): Locale & { exist?: boolean } => ({
|
||||
...localeValue,
|
||||
exist: true,
|
||||
}));
|
||||
|
||||
render() {
|
||||
const { locale, children } = this.props;
|
||||
const contextValue = memoizeOne(localeValue => ({
|
||||
...localeValue,
|
||||
exist: true,
|
||||
}))(locale);
|
||||
const contextValue = this.getMemoizedContextValue(locale);
|
||||
return <LocaleContext.Provider value={contextValue}>{children}</LocaleContext.Provider>;
|
||||
}
|
||||
}
|
||||
|
@ -26,18 +26,18 @@ const localeValues: Locale = {
|
||||
sortTitle: 'តម្រៀប',
|
||||
expand: 'ពន្លាត',
|
||||
collapse: 'បិតបាំង',
|
||||
triggerDesc: 'ចុចដើម្បីរៀបតាំលំដាប់ធំ',
|
||||
triggerAsc: 'ចុចដើម្បីរៀបតាំលំដាប់តូច',
|
||||
triggerDesc: 'ចុចដើម្បីរៀបតាមលំដាប់ធំ',
|
||||
triggerAsc: 'ចុចដើម្បីរៀបតាមលំដាប់តូច',
|
||||
cancelSort: 'ចុចដើម្បីបោះបង់',
|
||||
},
|
||||
Modal: {
|
||||
okText: 'យល់ព្រម',
|
||||
cancelText: 'យល់ព្រម',
|
||||
cancelText: 'បោះបង់',
|
||||
justOkText: 'យល់ព្រម',
|
||||
},
|
||||
Popconfirm: {
|
||||
okText: 'យល់ព្រម',
|
||||
cancelText: 'យល់ព្រម',
|
||||
cancelText: 'បោះបង់',
|
||||
},
|
||||
Transfer: {
|
||||
searchPlaceholder: 'ស្វែងរកនៅទីនេះ',
|
||||
|
@ -124,3 +124,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{steps-prefix-cls}-navigation.@{steps-prefix-cls}-horizontal {
|
||||
> .@{steps-prefix-cls}-item
|
||||
> .@{steps-prefix-cls}-item-container
|
||||
> .@{steps-prefix-cls}-item-tail {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/33703
|
||||
& > & {
|
||||
line-height: 0;
|
||||
vertical-align: 0;
|
||||
}
|
||||
|
||||
|
@ -15,11 +15,11 @@ Alternatively you can implement drag sorting with handler using [react-sortable-
|
||||
|
||||
```jsx
|
||||
import { Table } from 'antd';
|
||||
import { sortableContainer, sortableElement, sortableHandle } from 'react-sortable-hoc';
|
||||
import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
|
||||
import { MenuOutlined } from '@ant-design/icons';
|
||||
import { arrayMoveImmutable } from 'array-move';
|
||||
|
||||
const DragHandle = sortableHandle(() => <MenuOutlined style={{ cursor: 'grab', color: '#999' }} />);
|
||||
const DragHandle = SortableHandle(() => <MenuOutlined style={{ cursor: 'grab', color: '#999' }} />);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
@ -68,8 +68,8 @@ const data = [
|
||||
},
|
||||
];
|
||||
|
||||
const SortableItem = sortableElement(props => <tr {...props} />);
|
||||
const SortableContainer = sortableContainer(props => <tbody {...props} />);
|
||||
const SortableItem = SortableElement(props => <tr {...props} />);
|
||||
const SortableBody = SortableContainer(props => <tbody {...props} />);
|
||||
|
||||
class SortableTable extends React.Component {
|
||||
state = {
|
||||
@ -79,14 +79,16 @@ class SortableTable extends React.Component {
|
||||
onSortEnd = ({ oldIndex, newIndex }) => {
|
||||
const { dataSource } = this.state;
|
||||
if (oldIndex !== newIndex) {
|
||||
const newData = arrayMoveImmutable([].concat(dataSource), oldIndex, newIndex).filter(el => !!el);
|
||||
const newData = arrayMoveImmutable([].concat(dataSource), oldIndex, newIndex).filter(
|
||||
el => !!el,
|
||||
);
|
||||
console.log('Sorted items: ', newData);
|
||||
this.setState({ dataSource: newData });
|
||||
}
|
||||
};
|
||||
|
||||
DraggableContainer = props => (
|
||||
<SortableContainer
|
||||
<SortableBody
|
||||
useDragHandle
|
||||
disableAutoscroll
|
||||
helperClass="row-dragging"
|
||||
|
Loading…
Reference in New Issue
Block a user