mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-24 02:59:58 +08:00
chore: merge master
This commit is contained in:
commit
d5cfd195dd
@ -28,6 +28,7 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"import-notation": null,
|
||||
"no-descending-specificity": null,
|
||||
"no-invalid-position-at-import-rule": null,
|
||||
"declaration-empty-line-before": null,
|
||||
|
@ -15,6 +15,64 @@ timeline: true
|
||||
|
||||
---
|
||||
|
||||
## 4.24.1
|
||||
|
||||
`2022-11-04`
|
||||
|
||||
- 🐞 Revert [#38160](https://github.com/ant-design/ant-design/pull/38160) to fix Table render `column.title` not as expect. [#38383](https://github.com/ant-design/ant-design/pull/38383)
|
||||
- 💄 Alert add `@alert-padding-vertical` & `@alert-padding-horizontal` less variables. [#38369](https://github.com/ant-design/ant-design/pull/38369) [@imoctopus](https://github.com/imoctopus)
|
||||
- 🐞 Fix Steps with customize Step `status` not work. [#38319](https://github.com/ant-design/ant-design/pull/38319) [@heiyu4585](https://github.com/heiyu4585)
|
||||
- 🐞 Fix Popconfirm icon color get polluted. [#38355](https://github.com/ant-design/ant-design/pull/38355)
|
||||
- 🐞 Fix Anchor missing dot style. [#38338](https://github.com/ant-design/ant-design/pull/38338) [@li-jia-nan](https://github.com/li-jia-nan)
|
||||
- 🐞 Fix DatePicker missing `popupClassName` definition. [#38325](https://github.com/ant-design/ant-design/pull/38325) [@Cedong.Lee](https://github.com/Cedong.Lee)
|
||||
|
||||
## 4.24.0
|
||||
|
||||
`2022-11-01`
|
||||
|
||||
- 🔥 Add new component Space.Compact used to replace Input.Group and Button.Group. [#37652](https://github.com/ant-design/ant-design/pull/37652) [@foryuki](https://github.com/foryuki)
|
||||
- 🆕 The `disabled` property on components inside a Form will now take precedence over the `disabled` property of the Form. [#37628](https://github.com/ant-design/ant-design/pull/37628) [@kiner-tang](https://github.com/kiner-tang)
|
||||
- 🆕 Add `text` config for editable Typograph, support enabling ellipsis and editable at the same time. [#37761](https://github.com/ant-design/ant-design/pull/37761) [@zheeeng](https://github.com/zheeeng)
|
||||
- 🆕 Row `align` and `justify` support reponsive value. [#37860](https://github.com/ant-design/ant-design/pull/37860) [@kiner-tang](https://github.com/kiner-tang)
|
||||
- 🆕 Image add `preview.scaleStep` prop to adjust the magnitude of zoom in and out and set the default `scaleOffset` to 0.5. [#37340](https://github.com/ant-design/ant-design/pull/37340) [@coldice945](https://github.com/coldice945)
|
||||
- 🆕 Steps support `items`. [#37531](https://github.com/ant-design/ant-design/pull/37531) [@heiyu4585](https://github.com/heiyu4585)
|
||||
- 🆕 Collapse supports `collapsible="icon"` to collapse by clicking icon. [#37566](https://github.com/ant-design/ant-design/pull/37566) [@Sheepeer](https://github.com/Sheepeer)
|
||||
- 🆕 Input.Password supports `visibilityToggle={{ visible, onVisibleChange }}` so that you can manually control password display and hide. [#38216](https://github.com/ant-design/ant-design/pull/38216) [@MrHeer](https://github.com/MrHeer)
|
||||
- 🆕 Breadcrumb added the `menu` property. [#37885](https://github.com/ant-design/ant-design/pull/37885) [@JarvisArt](https://github.com/JarvisArt)
|
||||
- 🆕 Dropdown added the `menu` `dropdownRender` property, and deprecated the `overlay` property. [#37885](https://github.com/ant-design/ant-design/pull/37885) [@JarvisArt](https://github.com/JarvisArt)
|
||||
- Table
|
||||
- 🆕 Table `filterDropdown` add close in argument to `close` filter dropdown only. [#37745](https://github.com/ant-design/ant-design/pull/37745) [@kiner-tang](https://github.com/kiner-tang)
|
||||
- 🐞 Fix Table `aria-label` contains `[object Object]`. [#38160](https://github.com/ant-design/ant-design/pull/38160) [@kiner-tang](https://github.com/kiner-tang)
|
||||
- 🐞 Fix Tabs component not reading the `getPopupContainer` property of ConfigProvider. [#38238](https://github.com/ant-design/ant-design/pull/38238) [@ZH-seven](https://github.com/ZH-seven)
|
||||
- 🐞 Fix Tooltip is broken when used in a disabled Menu.Item. [#38273](https://github.com/ant-design/ant-design/pull/38273)
|
||||
- 🐞 Fix the issue of miscalculated transform-origin for Tooltip with `placement` values like `topRight` or `bottomLeft`. [#38159](https://github.com/ant-design/ant-design/pull/38159) [@strear](https://github.com/strear)
|
||||
- 🐞 TimePicker remove redundant warning about using `popupClassName`. [#38190](https://github.com/ant-design/ant-design/pull/38190) [@kiner-tang](https://github.com/kiner-tang)
|
||||
- 🐞 Fix nest Drawer with default `open` append document order issue. [#37767](https://github.com/ant-design/ant-design/pull/37767) [#37790](https://github.com/ant-design/ant-design/pull/37790)
|
||||
- 🐞 Fix issue where numbers were not displayed when Badge set both `color` and `count`. [#37609](https://github.com/ant-design/ant-design/pull/37609) [@kiner-tang](https://github.com/kiner-tang)
|
||||
- 🐞 Fix Progress zoom behavior in Safari. [#38301](https://github.com/ant-design/ant-design/pull/38301)
|
||||
- Modal
|
||||
- 🐞 Fix Modal animation flush issue in React 18. [#38275](https://github.com/ant-design/ant-design/pull/38275)
|
||||
- 🐞 Fix Modal.method() not focus trigger after close. [#38275](https://github.com/ant-design/ant-design/pull/38275)
|
||||
- Transfer
|
||||
- 🐞 Fix Transfer titles is undefined. [#38182](https://github.com/ant-design/ant-design/pull/38182) [@kiner-tang](https://github.com/kiner-tang)
|
||||
- 🛠 Remove Transfer `defaultprops` spelling. [#38164](https://github.com/ant-design/ant-design/pull/38164) [#38154](https://github.com/ant-design/ant-design/pull/38154) [@li-jia-nan](https://github.com/li-jia-nan)
|
||||
- 🛠 Refactor Anchor to Function Component, some methods of obtaining `ref` and calling internal instance methods will invalid.. [#38265](https://github.com/ant-design/ant-design/pull/38265) [#37957](https://github.com/ant-design/ant-design/pull/37957) [@li-jia-nan](https://github.com/li-jia-nan) [@RexSkz](https://github.com/RexSkz)
|
||||
- 🛠 The layout of the Dropdown.Button component is implemented using Space.Compact instead. [#38090](https://github.com/ant-design/ant-design/pull/38090) [@foryuki](https://github.com/foryuki)
|
||||
- 🛠 Optimize the internal implementation of DirectoryTree Typography component. [#38184](https://github.com/ant-design/ant-design/pull/38184) [#38181](https://github.com/ant-design/ant-design/pull/38181) [@holazz](https://github.com/holazz) [#37716](https://github.com/ant-design/ant-design/pull/37716) [@zheeeng](https://github.com/zheeeng)
|
||||
- 💄 Fix TextArea custom border style not working when allowClear is enable. [#38101](https://github.com/ant-design/ant-design/pull/38101)
|
||||
- 💄 Fix Popconfirm style issue when icon={null}, now an additional span tag will be wrapped around the icon element. [#37384](https://github.com/ant-design/ant-design/pull/37384) [@edc-hui](https://github.com/edc-hui)
|
||||
- 💄 Fix Menu highlight style in compact mode. [#38223](https://github.com/ant-design/ant-design/pull/38223) [@messaooudi](https://github.com/messaooudi)
|
||||
- Carousel
|
||||
- 💄 Enlarge Carousel dots hover area for better experience. [#38257](https://github.com/ant-design/ant-design/pull/38257)
|
||||
- 💄 Fix Carousel dots margin style not being reset. [#38100](https://github.com/ant-design/ant-design/pull/38100)
|
||||
- TypeScript
|
||||
- 🤖 Mentions additionally exports MentionsRef. [#38028](https://github.com/ant-design/ant-design/pull/38028) [@simonwong](https://github.com/simonwong)
|
||||
- 🌐 Localization
|
||||
- Add Transfer `titles` property internationalization configuration. [#38168](https://github.com/ant-design/ant-design/pull/38168) [@kiner-tang](https://github.com/kiner-tang)
|
||||
- Fix default Empty description text. [#38127](https://github.com/ant-design/ant-design/pull/38127) [@HelloBojack](https://github.com/HelloBojack)
|
||||
- 🇮🇹 Add missing `it_IT` locale. [#38108](https://github.com/ant-design/ant-design/pull/38108) [@ernestfolch](https://github.com/ernestfolch)
|
||||
- 🇫🇷 Add missing `fr_FR` locale. [#38072](https://github.com/ant-design/ant-design/pull/38072) [@smwhr](https://github.com/smwhr)
|
||||
|
||||
## 4.23.6
|
||||
|
||||
`2022-10-17`
|
||||
|
@ -15,6 +15,64 @@ timeline: true
|
||||
|
||||
---
|
||||
|
||||
## 4.24.1
|
||||
|
||||
`2022-11-04`
|
||||
|
||||
- 🐞 回滚 [#38160](https://github.com/ant-design/ant-design/pull/38160) 以修复 Table 的 `column.title` 渲染不正确的问题。[#38383](https://github.com/ant-design/ant-design/pull/38383)
|
||||
- 💄 Alert 增加 `@alert-padding-vertical` 和 `@alert-padding-horizontal` 变量。[#38369](https://github.com/ant-design/ant-design/pull/38369) [@imoctopus](https://github.com/imoctopus)
|
||||
- 🐞 修复 Steps 中 用户配置的 Step `status` 优先级被覆盖的问题。[#38319](https://github.com/ant-design/ant-design/pull/38319) [@heiyu4585](https://github.com/heiyu4585)
|
||||
- 🐞 修复 Popconfirm 图标颜色会被污染的问题。[#38355](https://github.com/ant-design/ant-design/pull/38355)
|
||||
- 🐞 修复 Anchor 组件圆点样式丢失的问题。[#38338](https://github.com/ant-design/ant-design/pull/38338) [@li-jia-nan](https://github.com/li-jia-nan)
|
||||
- 🐞 修复 DatePicker `popupClassName` 定义丢失的问题。[#38325](https://github.com/ant-design/ant-design/pull/38325) [@Cedong.Lee](https://github.com/Cedong.Lee)
|
||||
|
||||
## 4.24.0
|
||||
|
||||
`2022-11-01`
|
||||
|
||||
- 🔥 新增组件 Space.Compact 用以替代 Input.Group 和 Button.Group 组件。[#37652](https://github.com/ant-design/ant-design/pull/37652) [@foryuki](https://github.com/foryuki)
|
||||
- 🆕 Form 内组件上的 `disabled` 属性现在将优先于 Form 的 `disabled` 属性。[#37628](https://github.com/ant-design/ant-design/pull/37628) [@kiner-tang](https://github.com/kiner-tang)
|
||||
- 🆕 Typograph 增加 `text` 配置,支持同时开启省略与编辑模式时的使用。[#37761](https://github.com/ant-design/ant-design/pull/37761) [@zheeeng](https://github.com/zheeeng)
|
||||
- 🆕 Row 组件的 `align` 和 `justify` 属性支持设置响应式的值。[#37860](https://github.com/ant-design/ant-design/pull/37860) [@kiner-tang](https://github.com/kiner-tang)
|
||||
- 🆕 Image 增加 `preview.scaleStep` 属性调整放大缩小的幅度,并将默认的 `scaleOffset` 设置为 0.5。[#37340](https://github.com/ant-design/ant-design/pull/37340) [@coldice945](https://github.com/coldice945)
|
||||
- 🆕 Steps 新增支持 `items`。[#37531](https://github.com/ant-design/ant-design/pull/37531) [@heiyu4585](https://github.com/heiyu4585)
|
||||
- 🆕 Collapse 新增 `collapsible="icon"` 从而支持点击图标展开收起。[#37566](https://github.com/ant-design/ant-design/pull/37566) [@Sheepeer](https://github.com/Sheepeer)
|
||||
- 🆕 Input.Password 支持 `visibilityToggle={{ visible, onVisibleChange }}` 从而可以手动控制密码显隐。[#38216](https://github.com/ant-design/ant-design/pull/38216) [@MrHeer](https://github.com/MrHeer)
|
||||
- 🆕 Breadcrumb 新增 `menu` 属性。[#37885](https://github.com/ant-design/ant-design/pull/37885) [@JarvisArt](https://github.com/JarvisArt)
|
||||
- 🆕 Dropdown 新增 `menu` `dropdownRender` 属性,并废弃了 `overlay` 属性。[#37885](https://github.com/ant-design/ant-design/pull/37885) [@JarvisArt](https://github.com/JarvisArt)
|
||||
- Table
|
||||
- 🆕 Table `filterDropdown` 新增一个 `close` 参数对象用于关闭筛选菜单。[#37745](https://github.com/ant-design/ant-design/pull/37745) [@kiner-tang](https://github.com/kiner-tang)
|
||||
- 🐞 修复 Table 组件 `aria-label` 出现 `[object Object]` 的问题。[#38160](https://github.com/ant-design/ant-design/pull/38160) [@kiner-tang](https://github.com/kiner-tang)
|
||||
- 🐞 修复 Tabs 组件没有读取 ConfigProvider 的 `getPopupContainer` 属性的问题。[#38238](https://github.com/ant-design/ant-design/pull/38238) [@ZH-seven](https://github.com/ZH-seven)
|
||||
- 🐞 修复一个在 Menu.Item `disabled` 内使用 Tooltip 不生效的问题。[#38273](https://github.com/ant-design/ant-design/pull/38273)
|
||||
- 🐞 修复 Tooltip 在 `placement` 值为 `topRight` 或 `bottomLeft` 时动画原点计算错误的问题。[#38159](https://github.com/ant-design/ant-design/pull/38159) [@strear](https://github.com/strear)
|
||||
- 🐞 TimePicker 移除使用了 `popupClassName` 冗余警告。[#38190](https://github.com/ant-design/ant-design/pull/38190) [@kiner-tang](https://github.com/kiner-tang)
|
||||
- 🐞 修复嵌套 Drawer 在默认都设置 `open` 时,添加至 document 顺序出错的问题。[#37767](https://github.com/ant-design/ant-design/pull/37767) [#37790](https://github.com/ant-design/ant-design/pull/37790)
|
||||
- 🐞 修复 Badge 同时设置 `color` 和 `count` 时,数字不展示的问题。[#37609](https://github.com/ant-design/ant-design/pull/37609) [@kiner-tang](https://github.com/kiner-tang)
|
||||
- 🐞 修复 Progress 在 Safari 下缩放异常的问题。[#38301](https://github.com/ant-design/ant-design/pull/38301)
|
||||
- Modal
|
||||
- 🐞 修复在 React 18 下 Modal 动画闪烁的问题。[#38275](https://github.com/ant-design/ant-design/pull/38275)
|
||||
- 🐞 修复 Modal.method() 关闭时默认没有聚焦触发元素的问题。[#38275](https://github.com/ant-design/ant-design/pull/38275)
|
||||
- Transfer
|
||||
- 🐞 修复 Transfer `titles` 为空时报错的问题。[#38182](https://github.com/ant-design/ant-design/pull/38182) [@kiner-tang](https://github.com/kiner-tang)
|
||||
- 🛠 移除 Transfer `defaultprops` 写法。[#38164](https://github.com/ant-design/ant-design/pull/38164) [#38154](https://github.com/ant-design/ant-design/pull/38154) [@li-jia-nan](https://github.com/li-jia-nan)
|
||||
- 🛠 重构 Anchor 为 Function Component,之前一些获取 `ref` 并调用内部实例方法的写法都会失效。[#38265](https://github.com/ant-design/ant-design/pull/38265) [#37957](https://github.com/ant-design/ant-design/pull/37957) [@li-jia-nan](https://github.com/li-jia-nan) [@RexSkz](https://github.com/RexSkz)
|
||||
- 🛠 Dropdown.Button 改用 Space.Compact 实现。[#38090](https://github.com/ant-design/ant-design/pull/38090) [@foryuki](https://github.com/foryuki)
|
||||
- 🛠 优化 DirectoryTree Typography 组件的内部实现。[#38184](https://github.com/ant-design/ant-design/pull/38184) [#38181](https://github.com/ant-design/ant-design/pull/38181) [@holazz](https://github.com/holazz) [#37716](https://github.com/ant-design/ant-design/pull/37716) [@zheeeng](https://github.com/zheeeng)
|
||||
- 💄 修复 TextArea 开启 `allowClear` 时自定义 border 样式无法生效的问题。[#38101](https://github.com/ant-design/ant-design/pull/38101)
|
||||
- 💄 修复 Popconfirm 设置 `icon={null}` 的时 `title` padding 仍然存在的问题,现在 icon 元素外会额外包裹一个 span 标签。[#37384](https://github.com/ant-design/ant-design/pull/37384) [@edc-hui](https://github.com/edc-hui)
|
||||
- 💄 修复 Menu 在紧凑模式下的高亮条样式。[#38223](https://github.com/ant-design/ant-design/pull/38223) [@messaooudi](https://github.com/messaooudi)
|
||||
- Carousel
|
||||
- 💄 扩大 Carousel 切换点的鼠标响应范围,优化切换体验。[#38257](https://github.com/ant-design/ant-design/pull/38257)
|
||||
- 💄 修复 Carousel `dots` 样式未被正确 reset 的问题。[#38100](https://github.com/ant-design/ant-design/pull/38100)
|
||||
- TypeScript
|
||||
- 🤖 Mentions 额外导出 MentionsRef。[#38028](https://github.com/ant-design/ant-design/pull/38028) [@simonwong](https://github.com/simonwong)
|
||||
- 🌐 国际化
|
||||
- 添加 Transfer `titles` 属性国际化配置。[#38168](https://github.com/ant-design/ant-design/pull/38168) [@kiner-tang](https://github.com/kiner-tang)
|
||||
- 修正默认 Empty 描述文案。[#38127](https://github.com/ant-design/ant-design/pull/38127) [@HelloBojack](https://github.com/HelloBojack)
|
||||
- 🇮🇹 补全 `it_IT` 文案。[#38108](https://github.com/ant-design/ant-design/pull/38108) [@ernestfolch](https://github.com/ernestfolch)
|
||||
- 🇫🇷 补全 `fr_FR` 文案。[#38072](https://github.com/ant-design/ant-design/pull/38072) [@smwhr](https://github.com/smwhr)
|
||||
|
||||
## 4.23.6
|
||||
|
||||
`2022-10-17`
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`antd exports modules correctly 1`] = `
|
||||
Array [
|
||||
[
|
||||
"Affix",
|
||||
"Alert",
|
||||
"Anchor",
|
||||
|
@ -22,7 +22,7 @@ function isThenable(thing?: PromiseLike<any>): boolean {
|
||||
|
||||
const ActionButton: React.FC<ActionButtonProps> = props => {
|
||||
const clickedRef = React.useRef<boolean>(false);
|
||||
const ref = React.useRef<any>();
|
||||
const ref = React.useRef<HTMLInputElement>(null);
|
||||
const [loading, setLoading] = useState<ButtonProps['loading']>(false);
|
||||
const { close } = props;
|
||||
const onInternalClose = (...args: any[]) => {
|
||||
@ -30,10 +30,11 @@ const ActionButton: React.FC<ActionButtonProps> = props => {
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
let timeoutId: any;
|
||||
let timeoutId: NodeJS.Timer | null = null;
|
||||
if (props.autoFocus) {
|
||||
const $this = ref.current as HTMLInputElement;
|
||||
timeoutId = setTimeout(() => $this.focus());
|
||||
timeoutId = setTimeout(() => {
|
||||
ref.current?.focus();
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
|
@ -40,8 +40,10 @@ function isNotGrey(color: string) {
|
||||
function isValidWaveColor(color: string) {
|
||||
return (
|
||||
color &&
|
||||
color !== '#fff' &&
|
||||
color !== '#ffffff' &&
|
||||
color !== 'rgb(255, 255, 255)' &&
|
||||
color !== 'rgba(255, 255, 255, 1)' &&
|
||||
isNotGrey(color) &&
|
||||
!/rgba\((?:\d*, ){3}0\)/.test(color) && // any transparent rgba color
|
||||
color !== 'transparent'
|
||||
|
@ -15,6 +15,7 @@ class AffixMounter extends React.Component<{
|
||||
onTestUpdatePosition?(): void;
|
||||
onChange?: () => void;
|
||||
getInstance?: (inst: InternalAffixClass) => void;
|
||||
style?: React.CSSProperties;
|
||||
}> {
|
||||
private container: HTMLDivElement;
|
||||
|
||||
@ -201,6 +202,7 @@ describe('Affix Render', () => {
|
||||
expect(getObserverEntities()).toHaveLength(1);
|
||||
expect(getObserverEntities()[0].target).toBe(window);
|
||||
});
|
||||
|
||||
it('check position change before measure', async () => {
|
||||
const { container } = render(
|
||||
<>
|
||||
@ -216,6 +218,35 @@ describe('Affix Render', () => {
|
||||
await movePlaceholder(1000);
|
||||
expect(container.querySelector('.ant-affix')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('do not measure when hidden', async () => {
|
||||
let affixInstance: InternalAffixClass | null = null;
|
||||
|
||||
const { rerender } = render(
|
||||
<AffixMounter
|
||||
getInstance={inst => {
|
||||
affixInstance = inst;
|
||||
}}
|
||||
offsetBottom={0}
|
||||
/>,
|
||||
);
|
||||
await waitFakeTimer();
|
||||
const firstAffixStyle = affixInstance!.state.affixStyle;
|
||||
|
||||
rerender(
|
||||
<AffixMounter
|
||||
getInstance={inst => {
|
||||
affixInstance = inst;
|
||||
}}
|
||||
offsetBottom={0}
|
||||
style={{ display: 'none' }}
|
||||
/>,
|
||||
);
|
||||
await waitFakeTimer();
|
||||
const secondAffixStyle = affixInstance!.state.affixStyle;
|
||||
|
||||
expect(firstAffixStyle).toEqual(secondAffixStyle);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePosition when size changed', () => {
|
||||
|
@ -171,6 +171,15 @@ class Affix extends React.Component<InternalAffixProps, AffixState> {
|
||||
const fixedTop = getFixedTop(placeholderReact, targetRect, offsetTop);
|
||||
const fixedBottom = getFixedBottom(placeholderReact, targetRect, offsetBottom);
|
||||
|
||||
if (
|
||||
placeholderReact.top === 0 &&
|
||||
placeholderReact.left === 0 &&
|
||||
placeholderReact.width === 0 &&
|
||||
placeholderReact.height === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fixedTop !== undefined) {
|
||||
newState.affixStyle = {
|
||||
position: 'fixed',
|
||||
|
@ -7,15 +7,14 @@ interface ErrorBoundaryProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default class ErrorBoundary extends React.Component<
|
||||
ErrorBoundaryProps,
|
||||
{
|
||||
error?: Error | null;
|
||||
info: {
|
||||
componentStack?: string;
|
||||
};
|
||||
}
|
||||
> {
|
||||
interface ErrorBoundaryStates {
|
||||
error?: Error | null;
|
||||
info?: {
|
||||
componentStack?: string;
|
||||
};
|
||||
}
|
||||
|
||||
class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryStates> {
|
||||
state = {
|
||||
error: undefined,
|
||||
info: {
|
||||
@ -41,3 +40,5 @@ export default class ErrorBoundary extends React.Component<
|
||||
return children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import classNames from 'classnames';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import addEventListener from 'rc-util/lib/Dom/addEventListener';
|
||||
import * as React from 'react';
|
||||
import Affix from '../affix';
|
||||
@ -91,87 +90,69 @@ export interface AntAnchor {
|
||||
) => void;
|
||||
}
|
||||
|
||||
class Anchor extends React.Component<InternalAnchorProps, AnchorState, ConfigConsumerProps> {
|
||||
static contextType = ConfigContext;
|
||||
const AnchorContent: React.FC<InternalAnchorProps> = props => {
|
||||
const {
|
||||
rootClassName,
|
||||
anchorPrefixCls: prefixCls,
|
||||
className = '',
|
||||
style,
|
||||
offsetTop,
|
||||
affix = true,
|
||||
showInkInFixed = false,
|
||||
children,
|
||||
bounds,
|
||||
targetOffset,
|
||||
onClick,
|
||||
onChange,
|
||||
getContainer,
|
||||
getCurrentAnchor,
|
||||
} = props;
|
||||
|
||||
state = {
|
||||
activeLink: null,
|
||||
};
|
||||
const [links, setLinks] = React.useState<string[]>([]);
|
||||
const [activeLink, setActiveLink] = React.useState<string | null>(null);
|
||||
const activeLinkRef = React.useRef<string | null>(activeLink);
|
||||
|
||||
context: ConfigConsumerProps;
|
||||
const wrapperRef = React.useRef<HTMLDivElement>(null);
|
||||
const spanLinkNode = React.useRef<HTMLSpanElement>(null);
|
||||
const animating = React.useRef<boolean>(false);
|
||||
|
||||
private wrapperRef = React.createRef<HTMLDivElement>();
|
||||
const { direction, getTargetContainer } = React.useContext<ConfigConsumerProps>(ConfigContext);
|
||||
|
||||
private inkNode: HTMLSpanElement;
|
||||
const getCurrentContainer = getContainer ?? getTargetContainer ?? getDefaultContainer;
|
||||
|
||||
// scroll scope's container
|
||||
private scrollContainer: HTMLElement | Window;
|
||||
const dependencyListItem: React.DependencyList[number] = JSON.stringify(links);
|
||||
|
||||
private links: string[] = [];
|
||||
|
||||
private scrollEvent: ReturnType<typeof addEventListener>;
|
||||
|
||||
private animating: boolean;
|
||||
|
||||
private prefixCls?: string;
|
||||
|
||||
// Context
|
||||
registerLink: AntAnchor['registerLink'] = link => {
|
||||
if (!this.links.includes(link)) {
|
||||
this.links.push(link);
|
||||
}
|
||||
};
|
||||
|
||||
unregisterLink: AntAnchor['unregisterLink'] = link => {
|
||||
const index = this.links.indexOf(link);
|
||||
if (index !== -1) {
|
||||
this.links.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
getContainer = () => {
|
||||
const { getTargetContainer } = this.context;
|
||||
const { getContainer } = this.props;
|
||||
|
||||
const getFunc = getContainer ?? getTargetContainer ?? getDefaultContainer;
|
||||
|
||||
return getFunc();
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.scrollContainer = this.getContainer();
|
||||
this.scrollEvent = addEventListener(this.scrollContainer, 'scroll', this.handleScroll);
|
||||
this.handleScroll();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { getCurrentAnchor } = this.props;
|
||||
const { activeLink } = this.state;
|
||||
if (this.scrollEvent) {
|
||||
const currentContainer = this.getContainer();
|
||||
if (this.scrollContainer !== currentContainer) {
|
||||
this.scrollContainer = currentContainer;
|
||||
this.scrollEvent.remove();
|
||||
this.scrollEvent = addEventListener(this.scrollContainer, 'scroll', this.handleScroll);
|
||||
this.handleScroll();
|
||||
const registerLink = React.useCallback<AntAnchor['registerLink']>(
|
||||
link => {
|
||||
if (!links.includes(link)) {
|
||||
setLinks(prev => [...prev, link]);
|
||||
}
|
||||
}
|
||||
if (typeof getCurrentAnchor === 'function') {
|
||||
this.setCurrentActiveLink(getCurrentAnchor(activeLink || ''), false);
|
||||
}
|
||||
this.updateInk();
|
||||
}
|
||||
},
|
||||
[dependencyListItem],
|
||||
);
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.scrollEvent) {
|
||||
this.scrollEvent.remove();
|
||||
}
|
||||
}
|
||||
const unregisterLink = React.useCallback<AntAnchor['unregisterLink']>(
|
||||
link => {
|
||||
if (links.includes(link)) {
|
||||
setLinks(prev => prev.filter(i => i !== link));
|
||||
}
|
||||
},
|
||||
[dependencyListItem],
|
||||
);
|
||||
|
||||
getCurrentAnchor(offsetTop = 0, bounds = 5): string {
|
||||
const linkSections: Array<Section> = [];
|
||||
const container = this.getContainer();
|
||||
this.links.forEach(link => {
|
||||
const updateInk = () => {
|
||||
const linkNode = wrapperRef.current?.querySelector<HTMLElement>(
|
||||
`.${prefixCls}-link-title-active`,
|
||||
);
|
||||
if (linkNode && spanLinkNode.current) {
|
||||
spanLinkNode.current.style.top = `${linkNode.offsetTop + linkNode.clientHeight / 2 - 4.5}px`;
|
||||
}
|
||||
};
|
||||
|
||||
const getInternalCurrentAnchor = (_links: string[], _offsetTop = 0, _bounds = 5): string => {
|
||||
const linkSections: Section[] = [];
|
||||
const container = getCurrentContainer();
|
||||
_links.forEach(link => {
|
||||
const sharpLinkMatch = sharpMatcherRegx.exec(link?.toString());
|
||||
if (!sharpLinkMatch) {
|
||||
return;
|
||||
@ -179,7 +160,7 @@ class Anchor extends React.Component<InternalAnchorProps, AnchorState, ConfigCon
|
||||
const target = document.getElementById(sharpLinkMatch[1]);
|
||||
if (target) {
|
||||
const top = getOffsetTop(target, container);
|
||||
if (top < offsetTop + bounds) {
|
||||
if (top < _offsetTop + _bounds) {
|
||||
linkSections.push({ link, top });
|
||||
}
|
||||
}
|
||||
@ -190,173 +171,155 @@ class Anchor extends React.Component<InternalAnchorProps, AnchorState, ConfigCon
|
||||
return maxSection.link;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
handleScrollTo = (link: string) => {
|
||||
const { offsetTop, targetOffset } = this.props;
|
||||
|
||||
this.setCurrentActiveLink(link);
|
||||
const container = this.getContainer();
|
||||
const scrollTop = getScroll(container, true);
|
||||
const sharpLinkMatch = sharpMatcherRegx.exec(link);
|
||||
if (!sharpLinkMatch) {
|
||||
return;
|
||||
}
|
||||
const targetElement = document.getElementById(sharpLinkMatch[1]);
|
||||
if (!targetElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eleOffsetTop = getOffsetTop(targetElement, container);
|
||||
let y = scrollTop + eleOffsetTop;
|
||||
y -= targetOffset !== undefined ? targetOffset : offsetTop || 0;
|
||||
this.animating = true;
|
||||
|
||||
scrollTo(y, {
|
||||
callback: () => {
|
||||
this.animating = false;
|
||||
},
|
||||
getContainer: this.getContainer,
|
||||
});
|
||||
};
|
||||
|
||||
saveInkNode = (node: HTMLSpanElement) => {
|
||||
this.inkNode = node;
|
||||
};
|
||||
|
||||
setCurrentActiveLink = (link: string, triggerChange = true) => {
|
||||
const { activeLink } = this.state;
|
||||
const { onChange, getCurrentAnchor } = this.props;
|
||||
if (activeLink === link) {
|
||||
const setCurrentActiveLink = (link: string) => {
|
||||
if (activeLinkRef.current === link) {
|
||||
return;
|
||||
}
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/30584
|
||||
this.setState({
|
||||
activeLink: typeof getCurrentAnchor === 'function' ? getCurrentAnchor(link) : link,
|
||||
});
|
||||
if (triggerChange) {
|
||||
onChange?.(link);
|
||||
}
|
||||
const newLink = typeof getCurrentAnchor === 'function' ? getCurrentAnchor(link) : link;
|
||||
setActiveLink(newLink);
|
||||
activeLinkRef.current = newLink;
|
||||
|
||||
// onChange should respect the original link (which may caused by
|
||||
// window scroll or user click), not the new link
|
||||
onChange?.(link);
|
||||
};
|
||||
|
||||
handleScroll = () => {
|
||||
if (this.animating) {
|
||||
const handleScroll = React.useCallback(() => {
|
||||
if (animating.current) {
|
||||
return;
|
||||
}
|
||||
const { offsetTop, bounds, targetOffset } = this.props;
|
||||
const currentActiveLink = this.getCurrentAnchor(
|
||||
if (typeof getCurrentAnchor === 'function') {
|
||||
return;
|
||||
}
|
||||
const currentActiveLink = getInternalCurrentAnchor(
|
||||
links,
|
||||
targetOffset !== undefined ? targetOffset : offsetTop || 0,
|
||||
bounds,
|
||||
);
|
||||
this.setCurrentActiveLink(currentActiveLink);
|
||||
};
|
||||
setCurrentActiveLink(currentActiveLink);
|
||||
}, [dependencyListItem, targetOffset, offsetTop]);
|
||||
|
||||
updateInk = () => {
|
||||
const { prefixCls, wrapperRef } = this;
|
||||
const anchorNode = wrapperRef.current;
|
||||
const linkNode = anchorNode?.querySelector<HTMLElement>(`.${prefixCls}-link-title-active`);
|
||||
if (linkNode) {
|
||||
this.inkNode.style.top = `${linkNode.offsetTop + linkNode.clientHeight / 2 - 4.5}px`;
|
||||
}
|
||||
};
|
||||
const handleScrollTo = React.useCallback<(link: string) => void>(
|
||||
link => {
|
||||
setCurrentActiveLink(link);
|
||||
const container = getCurrentContainer();
|
||||
const scrollTop = getScroll(container, true);
|
||||
const sharpLinkMatch = sharpMatcherRegx.exec(link);
|
||||
if (!sharpLinkMatch) {
|
||||
return;
|
||||
}
|
||||
const targetElement = document.getElementById(sharpLinkMatch[1]);
|
||||
if (!targetElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
getMemoizedContextValue = memoizeOne(
|
||||
(link: AntAnchor['activeLink'], onClickFn: AnchorProps['onClick']): AntAnchor => ({
|
||||
registerLink: this.registerLink,
|
||||
unregisterLink: this.unregisterLink,
|
||||
scrollTo: this.handleScrollTo,
|
||||
activeLink: link,
|
||||
onClick: onClickFn,
|
||||
}),
|
||||
const eleOffsetTop = getOffsetTop(targetElement, container);
|
||||
let y = scrollTop + eleOffsetTop;
|
||||
y -= targetOffset !== undefined ? targetOffset : offsetTop || 0;
|
||||
animating.current = true;
|
||||
scrollTo(y, {
|
||||
getContainer: getCurrentContainer,
|
||||
callback() {
|
||||
animating.current = false;
|
||||
},
|
||||
});
|
||||
},
|
||||
[targetOffset, offsetTop],
|
||||
);
|
||||
|
||||
render() {
|
||||
const { direction } = this.context;
|
||||
const {
|
||||
anchorPrefixCls: prefixCls,
|
||||
className = '',
|
||||
style,
|
||||
offsetTop,
|
||||
affix = true,
|
||||
showInkInFixed = false,
|
||||
children,
|
||||
onClick,
|
||||
rootClassName,
|
||||
} = this.props;
|
||||
const { activeLink } = this.state;
|
||||
const inkClass = classNames(
|
||||
{
|
||||
[`${prefixCls}-ink-ball-visible`]: activeLink,
|
||||
},
|
||||
`${prefixCls}-ink-ball`,
|
||||
);
|
||||
|
||||
// To support old version react.
|
||||
// Have to add prefixCls on the instance.
|
||||
// https://github.com/facebook/react/issues/12397
|
||||
this.prefixCls = prefixCls;
|
||||
const wrapperClass = classNames(
|
||||
rootClassName,
|
||||
`${prefixCls}-wrapper`,
|
||||
{
|
||||
[`${prefixCls}-rtl`]: direction === 'rtl',
|
||||
},
|
||||
className,
|
||||
);
|
||||
|
||||
const inkClass = classNames(`${prefixCls}-ink-ball`, {
|
||||
visible: activeLink,
|
||||
});
|
||||
const anchorClass = classNames(prefixCls, {
|
||||
[`${prefixCls}-fixed`]: !affix && !showInkInFixed,
|
||||
});
|
||||
|
||||
const wrapperClass = classNames(
|
||||
rootClassName,
|
||||
`${prefixCls}-wrapper`,
|
||||
{
|
||||
[`${prefixCls}-rtl`]: direction === 'rtl',
|
||||
},
|
||||
className,
|
||||
);
|
||||
const wrapperStyle: React.CSSProperties = {
|
||||
maxHeight: offsetTop ? `calc(100vh - ${offsetTop}px)` : '100vh',
|
||||
...style,
|
||||
};
|
||||
|
||||
const anchorClass = classNames(prefixCls, {
|
||||
[`${prefixCls}-fixed`]: !affix && !showInkInFixed,
|
||||
});
|
||||
|
||||
const wrapperStyle: React.CSSProperties = {
|
||||
maxHeight: offsetTop ? `calc(100vh - ${offsetTop}px)` : '100vh',
|
||||
...style,
|
||||
};
|
||||
|
||||
const anchorContent = (
|
||||
<div ref={this.wrapperRef} className={wrapperClass} style={wrapperStyle}>
|
||||
<div className={anchorClass}>
|
||||
<div className={`${prefixCls}-ink`}>
|
||||
<span className={inkClass} ref={this.saveInkNode} />
|
||||
</div>
|
||||
{children}
|
||||
const anchorContent = (
|
||||
<div ref={wrapperRef} className={wrapperClass} style={wrapperStyle}>
|
||||
<div className={anchorClass}>
|
||||
<div className={`${prefixCls}-ink`}>
|
||||
<span className={inkClass} ref={spanLinkNode} />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
);
|
||||
|
||||
const contextValue = this.getMemoizedContextValue(activeLink, onClick);
|
||||
React.useEffect(() => {
|
||||
const scrollContainer = getCurrentContainer();
|
||||
const scrollEvent = addEventListener(scrollContainer, 'scroll', handleScroll);
|
||||
handleScroll();
|
||||
return () => {
|
||||
scrollEvent?.remove();
|
||||
};
|
||||
}, [dependencyListItem]);
|
||||
|
||||
return (
|
||||
<AnchorContext.Provider value={contextValue}>
|
||||
{affix ? (
|
||||
<Affix offsetTop={offsetTop} target={this.getContainer}>
|
||||
{anchorContent}
|
||||
</Affix>
|
||||
) : (
|
||||
anchorContent
|
||||
)}
|
||||
</AnchorContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
React.useEffect(() => {
|
||||
if (typeof getCurrentAnchor === 'function') {
|
||||
setCurrentActiveLink(getCurrentAnchor(activeLinkRef.current || ''));
|
||||
}
|
||||
}, [getCurrentAnchor]);
|
||||
|
||||
// just use in test
|
||||
export type InternalAnchorClass = Anchor;
|
||||
React.useEffect(() => {
|
||||
updateInk();
|
||||
}, [getCurrentAnchor, dependencyListItem, activeLink]);
|
||||
|
||||
const AnchorFC = React.forwardRef<Anchor, AnchorProps>((props, ref) => {
|
||||
const memoizedContextValue = React.useMemo<AntAnchor>(
|
||||
() => ({
|
||||
registerLink,
|
||||
unregisterLink,
|
||||
scrollTo: handleScrollTo,
|
||||
activeLink,
|
||||
onClick,
|
||||
}),
|
||||
[activeLink, onClick, handleScrollTo],
|
||||
);
|
||||
|
||||
return (
|
||||
<AnchorContext.Provider value={memoizedContextValue}>
|
||||
{affix ? (
|
||||
<Affix offsetTop={offsetTop} target={getCurrentContainer}>
|
||||
{anchorContent}
|
||||
</Affix>
|
||||
) : (
|
||||
anchorContent
|
||||
)}
|
||||
</AnchorContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const Anchor: React.FC<AnchorProps> = props => {
|
||||
const { prefixCls: customizePrefixCls } = props;
|
||||
const { getPrefixCls } = React.useContext(ConfigContext);
|
||||
const { getPrefixCls } = React.useContext<ConfigConsumerProps>(ConfigContext);
|
||||
const anchorPrefixCls = getPrefixCls('anchor', customizePrefixCls);
|
||||
|
||||
const [wrapSSR, hashId] = useStyle(anchorPrefixCls);
|
||||
|
||||
const anchorProps: InternalAnchorProps = {
|
||||
...props,
|
||||
return wrapSSR(
|
||||
<AnchorContent {...props} rootClassName={hashId} anchorPrefixCls={anchorPrefixCls} />,
|
||||
);
|
||||
};
|
||||
|
||||
anchorPrefixCls,
|
||||
rootClassName: hashId,
|
||||
};
|
||||
|
||||
return wrapSSR(<Anchor {...anchorProps} ref={ref} />);
|
||||
});
|
||||
|
||||
export default AnchorFC;
|
||||
export default Anchor;
|
||||
|
@ -26,7 +26,7 @@ const AnchorLink: React.FC<AnchorLinkProps> = props => {
|
||||
return () => {
|
||||
unregisterLink?.(href);
|
||||
};
|
||||
}, [href]);
|
||||
}, [href, registerLink, unregisterLink]);
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLElement>) => {
|
||||
onClick?.(e, { title, href });
|
||||
|
@ -1,20 +1,9 @@
|
||||
import React from 'react';
|
||||
import Anchor from '..';
|
||||
import { fireEvent, render, waitFakeTimer } from '../../../tests/utils';
|
||||
import type { InternalAnchorClass } from '../Anchor';
|
||||
|
||||
const { Link } = Anchor;
|
||||
|
||||
function createGetContainer(id: string) {
|
||||
return () => {
|
||||
const container = document.getElementById(id);
|
||||
if (container == null) {
|
||||
throw new Error();
|
||||
}
|
||||
return container;
|
||||
};
|
||||
}
|
||||
|
||||
function createDiv() {
|
||||
const root = document.createElement('div');
|
||||
document.body.appendChild(root);
|
||||
@ -57,122 +46,77 @@ describe('Anchor Render', () => {
|
||||
getClientRectsMock.mockRestore();
|
||||
});
|
||||
|
||||
it('Anchor render perfectly', () => {
|
||||
it('renders correctly', () => {
|
||||
const hash = getHashUrl();
|
||||
let anchorInstance: InternalAnchorClass;
|
||||
const { container } = render(
|
||||
<Anchor
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
>
|
||||
<Anchor>
|
||||
<Link href={`#${hash}`} title={hash} />
|
||||
</Anchor>,
|
||||
);
|
||||
|
||||
fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!);
|
||||
anchorInstance!.handleScroll();
|
||||
expect(anchorInstance!.state).not.toBe(null);
|
||||
expect(container.querySelector(`a[href="#${hash}"]`)).not.toBe(null);
|
||||
});
|
||||
|
||||
it('Anchor render perfectly for complete href - click', async () => {
|
||||
it('actives the target when clicking a link', async () => {
|
||||
const hash = getHashUrl();
|
||||
let anchorInstance: InternalAnchorClass;
|
||||
const { container } = render(
|
||||
<Anchor
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
>
|
||||
<Anchor prefixCls="ant-anchor">
|
||||
<Link href={`http://www.example.com/#${hash}`} title={hash} />
|
||||
</Anchor>,
|
||||
);
|
||||
fireEvent.click(container.querySelector(`a[href="http://www.example.com/#${hash}"]`)!);
|
||||
const link = container.querySelector(`a[href="http://www.example.com/#${hash}"]`)!;
|
||||
fireEvent.click(link);
|
||||
await waitFakeTimer();
|
||||
expect(anchorInstance!.state!.activeLink).toBe(`http://www.example.com/#${hash}`);
|
||||
expect(link.classList).toContain('ant-anchor-link-title-active');
|
||||
});
|
||||
|
||||
it('Anchor render perfectly for complete href - hash router', async () => {
|
||||
it('scrolls the page when clicking a link', async () => {
|
||||
const root = createDiv();
|
||||
const scrollToSpy = jest.spyOn(window, 'scrollTo');
|
||||
render(<div id="/faq?locale=en#Q1">Q1</div>, { container: root });
|
||||
let anchorInstance: InternalAnchorClass;
|
||||
render(
|
||||
<Anchor
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
>
|
||||
const { container } = render(
|
||||
<Anchor>
|
||||
<Link href="/#/faq?locale=en#Q1" title="Q1" />
|
||||
</Anchor>,
|
||||
);
|
||||
anchorInstance!.handleScrollTo('/#/faq?locale=en#Q1');
|
||||
const link = container.querySelector(`a[href="/#/faq?locale=en#Q1"]`)!;
|
||||
fireEvent.click(link);
|
||||
await waitFakeTimer();
|
||||
expect(anchorInstance!.state.activeLink).toBe('/#/faq?locale=en#Q1');
|
||||
expect(scrollToSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Anchor render perfectly for complete href - scroll', async () => {
|
||||
const hash = getHashUrl();
|
||||
it('handleScroll should not be triggered when scrolling caused by clicking a link', async () => {
|
||||
const hash1 = getHashUrl();
|
||||
const hash2 = getHashUrl();
|
||||
const root = createDiv();
|
||||
render(<div id={hash}>Hello</div>, { container: root });
|
||||
let anchorInstance: InternalAnchorClass;
|
||||
const onChange = jest.fn();
|
||||
render(
|
||||
<Anchor
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
>
|
||||
<Link href={`http://www.example.com/#${hash}`} title={hash} />
|
||||
<div>
|
||||
<div id={hash1}>Hello</div>
|
||||
<div id={hash2}>World</div>
|
||||
</div>,
|
||||
{ container: root },
|
||||
);
|
||||
const { container } = render(
|
||||
<Anchor onChange={onChange}>
|
||||
<Link href={`#${hash1}`} title={hash1} />
|
||||
<Link href={`#${hash2}`} title={hash2} />
|
||||
</Anchor>,
|
||||
);
|
||||
anchorInstance!.handleScroll();
|
||||
onChange.mockClear();
|
||||
|
||||
const link = container.querySelector(`a[href="#${hash2}"]`)!;
|
||||
// this will trigger 1 onChange
|
||||
fireEvent.click(link);
|
||||
// smooth scroll caused by clicking needs time to finish.
|
||||
// we scroll the window before it finish, the scroll listener should not be triggered,
|
||||
fireEvent.scroll(window);
|
||||
|
||||
await waitFakeTimer();
|
||||
expect(anchorInstance!.state!.activeLink).toBe(`http://www.example.com/#${hash}`);
|
||||
// if the scroll listener is triggered, we will get 2 onChange, now we expect only 1.
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Anchor render perfectly for complete href - scrollTo', async () => {
|
||||
const hash = getHashUrl();
|
||||
const scrollToSpy = jest.spyOn(window, 'scrollTo');
|
||||
const root = createDiv();
|
||||
render(<div id={`#${hash}`}>Hello</div>, { container: root });
|
||||
let anchorInstance: InternalAnchorClass;
|
||||
render(
|
||||
<Anchor
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
>
|
||||
<Link href={`##${hash}`} title={hash} />
|
||||
</Anchor>,
|
||||
);
|
||||
|
||||
anchorInstance!.handleScrollTo(`##${hash}`);
|
||||
await waitFakeTimer();
|
||||
expect(anchorInstance!.state.activeLink).toBe(`##${hash}`);
|
||||
const calls = scrollToSpy.mock.calls.length;
|
||||
expect(scrollToSpy.mock.calls.length).toBe(calls);
|
||||
});
|
||||
|
||||
it('should remove listener when unmount', async () => {
|
||||
const hash = getHashUrl();
|
||||
let anchorInstance: InternalAnchorClass;
|
||||
const { unmount } = render(
|
||||
<Anchor
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
>
|
||||
<Link href={`#${hash}`} title={hash} />
|
||||
</Anchor>,
|
||||
);
|
||||
|
||||
const removeListenerSpy = jest.spyOn((anchorInstance! as any).scrollEvent, 'remove');
|
||||
unmount();
|
||||
expect(removeListenerSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should unregister link when unmount children', () => {
|
||||
it('should update DOM when children are unmounted', () => {
|
||||
const hash = getHashUrl();
|
||||
const { container, rerender } = render(
|
||||
<Anchor>
|
||||
@ -187,32 +131,94 @@ describe('Anchor Render', () => {
|
||||
expect(container.querySelector('.ant-anchor-link-title')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should update links when link href update', async () => {
|
||||
it('should update DOM when link href is changed', async () => {
|
||||
const hash = getHashUrl();
|
||||
let anchorInstance: InternalAnchorClass;
|
||||
function AnchorUpdate({ href }: { href: string }) {
|
||||
return (
|
||||
<Anchor
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
>
|
||||
<Anchor>
|
||||
<Link href={href} title={hash} />
|
||||
</Anchor>
|
||||
);
|
||||
}
|
||||
const { rerender } = render(<AnchorUpdate href={`#${hash}`} />);
|
||||
const { container, rerender } = render(<AnchorUpdate href={`#${hash}`} />);
|
||||
|
||||
if (anchorInstance! == null) {
|
||||
throw new Error('anchorInstance should not be null');
|
||||
}
|
||||
|
||||
expect((anchorInstance as any)!.links).toEqual([`#${hash}`]);
|
||||
expect(container.querySelector(`a[href="#${hash}"]`)).toBeTruthy();
|
||||
rerender(<AnchorUpdate href={`#${hash}_1`} />);
|
||||
expect((anchorInstance as any)!.links).toEqual([`#${hash}_1`]);
|
||||
expect(container.querySelector(`a[href="#${hash}_1"]`)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Anchor onClick event', () => {
|
||||
it('targetOffset prop', async () => {
|
||||
const hash = getHashUrl();
|
||||
|
||||
const scrollToSpy = jest.spyOn(window, 'scrollTo');
|
||||
const root = createDiv();
|
||||
render(<h1 id={hash}>Hello</h1>, { container: root });
|
||||
const { container, rerender } = render(
|
||||
<Anchor>
|
||||
<Link href={`#${hash}`} title={hash} />
|
||||
</Anchor>,
|
||||
);
|
||||
|
||||
const setProps = (props: Record<string, any>) =>
|
||||
rerender(
|
||||
<Anchor {...props}>
|
||||
<Link href={`#${hash}`} title={hash} />
|
||||
</Anchor>,
|
||||
);
|
||||
|
||||
fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!);
|
||||
await waitFakeTimer();
|
||||
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 1000);
|
||||
|
||||
setProps({ offsetTop: 100 });
|
||||
|
||||
fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!);
|
||||
await waitFakeTimer();
|
||||
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 900);
|
||||
|
||||
setProps({ targetOffset: 200 });
|
||||
|
||||
fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!);
|
||||
await waitFakeTimer();
|
||||
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800);
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/31941
|
||||
it('targetOffset prop when contain spaces', async () => {
|
||||
const hash = `${getHashUrl()} s p a c e s`;
|
||||
|
||||
const scrollToSpy = jest.spyOn(window, 'scrollTo');
|
||||
const root = createDiv();
|
||||
render(<h1 id={hash}>Hello</h1>, { container: root });
|
||||
const { container, rerender } = render(
|
||||
<Anchor>
|
||||
<Link href={`#${hash}`} title={hash} />
|
||||
</Anchor>,
|
||||
);
|
||||
|
||||
const setProps = (props: Record<string, any>) =>
|
||||
rerender(
|
||||
<Anchor {...props}>
|
||||
<Link href={`#${hash}`} title={hash} />
|
||||
</Anchor>,
|
||||
);
|
||||
|
||||
fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!);
|
||||
await waitFakeTimer();
|
||||
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 1000);
|
||||
|
||||
setProps({ offsetTop: 100 });
|
||||
fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!);
|
||||
await waitFakeTimer();
|
||||
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 900);
|
||||
|
||||
setProps({ targetOffset: 200 });
|
||||
fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!);
|
||||
await waitFakeTimer();
|
||||
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800);
|
||||
});
|
||||
|
||||
it('onClick event', () => {
|
||||
const hash = getHashUrl();
|
||||
let event;
|
||||
let link;
|
||||
@ -226,297 +232,23 @@ describe('Anchor Render', () => {
|
||||
|
||||
const href = `#${hash}`;
|
||||
const title = hash;
|
||||
let anchorInstance: InternalAnchorClass;
|
||||
const { container } = render(
|
||||
<Anchor
|
||||
onClick={handleClick}
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
>
|
||||
<Anchor onClick={handleClick}>
|
||||
<Link href={href} title={title} />
|
||||
</Anchor>,
|
||||
);
|
||||
|
||||
fireEvent.click(container.querySelector(`a[href="${href}"]`)!);
|
||||
anchorInstance!.handleScroll();
|
||||
expect(event).not.toBe(undefined);
|
||||
expect(link).toEqual({ href, title });
|
||||
});
|
||||
|
||||
it('Different function returns the same DOM', async () => {
|
||||
const hash = getHashUrl();
|
||||
const root = createDiv();
|
||||
render(<div id={hash}>Hello</div>, { container: root });
|
||||
const getContainerA = createGetContainer(hash);
|
||||
const getContainerB = createGetContainer(hash);
|
||||
let anchorInstance: InternalAnchorClass;
|
||||
const { rerender } = render(
|
||||
<Anchor
|
||||
getContainer={getContainerA}
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
>
|
||||
<Link href={`#${hash}`} title={hash} />
|
||||
</Anchor>,
|
||||
);
|
||||
|
||||
const removeListenerSpy = jest.spyOn((anchorInstance! as any).scrollEvent, 'remove');
|
||||
await waitFakeTimer();
|
||||
rerender(
|
||||
<Anchor getContainer={getContainerB}>
|
||||
<Link href={`#${hash}`} title={hash} />
|
||||
</Anchor>,
|
||||
);
|
||||
expect(removeListenerSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Different function returns different DOM', async () => {
|
||||
const hash1 = getHashUrl();
|
||||
const hash2 = getHashUrl();
|
||||
const root = createDiv();
|
||||
render(
|
||||
<div>
|
||||
<div id={hash1}>Hello</div>
|
||||
<div id={hash2}>World</div>
|
||||
</div>,
|
||||
{ container: root },
|
||||
);
|
||||
const getContainerA = createGetContainer(hash1);
|
||||
const getContainerB = createGetContainer(hash2);
|
||||
let anchorInstance: InternalAnchorClass;
|
||||
const { rerender } = render(
|
||||
<Anchor
|
||||
getContainer={getContainerA}
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
>
|
||||
<Link href={`#${hash1}`} title={hash1} />
|
||||
<Link href={`#${hash2}`} title={hash2} />
|
||||
</Anchor>,
|
||||
);
|
||||
|
||||
const removeListenerSpy = jest.spyOn((anchorInstance! as any).scrollEvent, 'remove');
|
||||
expect(removeListenerSpy).not.toHaveBeenCalled();
|
||||
await waitFakeTimer();
|
||||
rerender(
|
||||
<Anchor getContainer={getContainerB}>
|
||||
<Link href={`#${hash1}`} title={hash1} />
|
||||
<Link href={`#${hash2}`} title={hash2} />
|
||||
</Anchor>,
|
||||
);
|
||||
expect(removeListenerSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Same function returns the same DOM', () => {
|
||||
const hash = getHashUrl();
|
||||
const root = createDiv();
|
||||
render(<div id={hash}>Hello</div>, { container: root });
|
||||
const getContainer = createGetContainer(hash);
|
||||
let anchorInstance: InternalAnchorClass;
|
||||
const { container } = render(
|
||||
<Anchor
|
||||
getContainer={getContainer}
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
>
|
||||
<Link href={`#${hash}`} title={hash} />
|
||||
</Anchor>,
|
||||
);
|
||||
|
||||
fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!);
|
||||
|
||||
anchorInstance!.handleScroll();
|
||||
expect(anchorInstance!.state).not.toBe(null);
|
||||
});
|
||||
|
||||
it('Same function returns different DOM', async () => {
|
||||
const hash1 = getHashUrl();
|
||||
const hash2 = getHashUrl();
|
||||
const root = createDiv();
|
||||
render(
|
||||
<div>
|
||||
<div id={hash1}>Hello</div>
|
||||
<div id={hash2}>World</div>
|
||||
</div>,
|
||||
{ container: root },
|
||||
);
|
||||
const holdContainer = {
|
||||
container: document.getElementById(hash1),
|
||||
};
|
||||
const getContainer = () => {
|
||||
if (holdContainer.container == null) {
|
||||
throw new Error('container should not be null');
|
||||
}
|
||||
return holdContainer.container;
|
||||
};
|
||||
let anchorInstance: InternalAnchorClass;
|
||||
const { rerender } = render(
|
||||
<Anchor
|
||||
getContainer={getContainer}
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
>
|
||||
<Link href={`#${hash1}`} title={hash1} />
|
||||
<Link href={`#${hash2}`} title={hash2} />
|
||||
</Anchor>,
|
||||
);
|
||||
const removeListenerSpy = jest.spyOn((anchorInstance! as any).scrollEvent, 'remove');
|
||||
expect(removeListenerSpy).not.toHaveBeenCalled();
|
||||
await waitFakeTimer();
|
||||
holdContainer.container = document.getElementById(hash2);
|
||||
rerender(
|
||||
<Anchor getContainer={getContainer}>
|
||||
<Link href={`#${hash1}`} title={hash1} />
|
||||
<Link href={`#${hash2}`} title={hash2} />
|
||||
</Anchor>,
|
||||
);
|
||||
expect(removeListenerSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Anchor targetOffset prop', async () => {
|
||||
const hash = getHashUrl();
|
||||
let dateNowMock: jest.SpyInstance;
|
||||
|
||||
function dataNowMockFn() {
|
||||
let start = 0;
|
||||
|
||||
const handler = () => {
|
||||
start += 1000;
|
||||
return start;
|
||||
};
|
||||
|
||||
return jest.spyOn(Date, 'now').mockImplementation(handler);
|
||||
}
|
||||
|
||||
dateNowMock = dataNowMockFn();
|
||||
|
||||
const scrollToSpy = jest.spyOn(window, 'scrollTo');
|
||||
const root = createDiv();
|
||||
render(<h1 id={hash}>Hello</h1>, { container: root });
|
||||
let anchorInstance: InternalAnchorClass;
|
||||
const { rerender } = render(
|
||||
<Anchor
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
>
|
||||
<Link href={`#${hash}`} title={hash} />
|
||||
</Anchor>,
|
||||
);
|
||||
|
||||
const setProps = (props: Record<string, any>) =>
|
||||
rerender(
|
||||
<Anchor
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<Link href={`#${hash}`} title={hash} />
|
||||
</Anchor>,
|
||||
);
|
||||
|
||||
anchorInstance!.handleScrollTo(`#${hash}`);
|
||||
await waitFakeTimer();
|
||||
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 1000);
|
||||
dateNowMock = dataNowMockFn();
|
||||
|
||||
setProps({ offsetTop: 100 });
|
||||
|
||||
anchorInstance!.handleScrollTo(`#${hash}`);
|
||||
await waitFakeTimer();
|
||||
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 900);
|
||||
dateNowMock = dataNowMockFn();
|
||||
|
||||
setProps({ targetOffset: 200 });
|
||||
|
||||
anchorInstance!.handleScrollTo(`#${hash}`);
|
||||
await waitFakeTimer();
|
||||
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800);
|
||||
|
||||
dateNowMock.mockRestore();
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/31941
|
||||
it('Anchor targetOffset prop when contain spaces', async () => {
|
||||
const hash = `${getHashUrl()} s p a c e s`;
|
||||
let dateNowMock: jest.SpyInstance;
|
||||
|
||||
function dataNowMockFn() {
|
||||
let start = 0;
|
||||
|
||||
const handler = () => {
|
||||
start += 1000;
|
||||
return start;
|
||||
};
|
||||
|
||||
return jest.spyOn(Date, 'now').mockImplementation(handler);
|
||||
}
|
||||
|
||||
dateNowMock = dataNowMockFn();
|
||||
|
||||
const scrollToSpy = jest.spyOn(window, 'scrollTo');
|
||||
const root = createDiv();
|
||||
render(<h1 id={hash}>Hello</h1>, { container: root });
|
||||
let anchorInstance: InternalAnchorClass;
|
||||
const { rerender } = render(
|
||||
<Anchor
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
>
|
||||
<Link href={`#${hash}`} title={hash} />
|
||||
</Anchor>,
|
||||
);
|
||||
|
||||
const setProps = (props: Record<string, any>) =>
|
||||
rerender(
|
||||
<Anchor
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<Link href={`#${hash}`} title={hash} />
|
||||
</Anchor>,
|
||||
);
|
||||
|
||||
anchorInstance!.handleScrollTo(`#${hash}`);
|
||||
await waitFakeTimer();
|
||||
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 1000);
|
||||
dateNowMock = dataNowMockFn();
|
||||
|
||||
setProps({ offsetTop: 100 });
|
||||
anchorInstance!.handleScrollTo(`#${hash}`);
|
||||
await waitFakeTimer();
|
||||
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 900);
|
||||
dateNowMock = dataNowMockFn();
|
||||
|
||||
setProps({ targetOffset: 200 });
|
||||
anchorInstance!.handleScrollTo(`#${hash}`);
|
||||
await waitFakeTimer();
|
||||
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800);
|
||||
|
||||
dateNowMock.mockRestore();
|
||||
});
|
||||
|
||||
it('Anchor onChange prop', async () => {
|
||||
it('onChange event', () => {
|
||||
const hash1 = getHashUrl();
|
||||
const hash2 = getHashUrl();
|
||||
const onChange = jest.fn();
|
||||
let anchorInstance: InternalAnchorClass;
|
||||
render(
|
||||
<Anchor
|
||||
onChange={onChange}
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
>
|
||||
const { container } = render(
|
||||
<Anchor onChange={onChange}>
|
||||
<Link href={`#${hash1}`} title={hash1} />
|
||||
<Link href={`#${hash2}`} title={hash2} />
|
||||
</Anchor>,
|
||||
@ -526,89 +258,57 @@ describe('Anchor Render', () => {
|
||||
);
|
||||
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
anchorInstance!.handleScrollTo(hash2);
|
||||
fireEvent.click(container.querySelector(`a[href="#${hash2}"]`)!);
|
||||
expect(onChange).toHaveBeenCalledTimes(2);
|
||||
expect(onChange).toHaveBeenCalledWith(hash2);
|
||||
expect(onChange).toHaveBeenLastCalledWith(`#${hash2}`);
|
||||
});
|
||||
|
||||
it('invalid hash', async () => {
|
||||
let anchorInstance: InternalAnchorClass;
|
||||
it('handles invalid hash correctly', () => {
|
||||
const { container } = render(
|
||||
<Anchor
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
>
|
||||
<Anchor>
|
||||
<Link href="notexsited" title="title" />
|
||||
</Anchor>,
|
||||
);
|
||||
|
||||
fireEvent.click(container.querySelector(`a[href="notexsited"]`)!);
|
||||
|
||||
anchorInstance!.handleScrollTo('notexsited');
|
||||
expect(anchorInstance!.state).not.toBe(null);
|
||||
const link = container.querySelector(`a[href="notexsited"]`)!;
|
||||
fireEvent.click(link);
|
||||
expect(container.querySelector(`.ant-anchor-link-title-active`)?.textContent).toBe('title');
|
||||
});
|
||||
|
||||
it('test edge case when getBoundingClientRect return zero size', async () => {
|
||||
getBoundingClientRectMock.mockReturnValue({ width: 0, height: 0, top: 1000 } as DOMRect);
|
||||
const hash = getHashUrl();
|
||||
let dateNowMock: jest.SpyInstance;
|
||||
|
||||
function dataNowMockFn() {
|
||||
let start = 0;
|
||||
|
||||
const handler = () => {
|
||||
start += 1000;
|
||||
return start;
|
||||
};
|
||||
|
||||
return jest.spyOn(Date, 'now').mockImplementation(handler);
|
||||
}
|
||||
|
||||
dateNowMock = dataNowMockFn();
|
||||
|
||||
const scrollToSpy = jest.spyOn(window, 'scrollTo');
|
||||
const root = createDiv();
|
||||
render(<h1 id={hash}>Hello</h1>, { container: root });
|
||||
let anchorInstance: InternalAnchorClass;
|
||||
const { rerender } = render(
|
||||
<Anchor
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
>
|
||||
const { container, rerender } = render(
|
||||
<Anchor>
|
||||
<Link href={`#${hash}`} title={hash} />
|
||||
</Anchor>,
|
||||
);
|
||||
|
||||
const setProps = (props: Record<string, any>) =>
|
||||
rerender(
|
||||
<Anchor
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<Anchor {...props}>
|
||||
<Link href={`#${hash}`} title={hash} />
|
||||
</Anchor>,
|
||||
);
|
||||
anchorInstance!.handleScrollTo(`#${hash}`);
|
||||
|
||||
fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!);
|
||||
await waitFakeTimer();
|
||||
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 1000);
|
||||
dateNowMock = dataNowMockFn();
|
||||
|
||||
setProps({ offsetTop: 100 });
|
||||
anchorInstance!.handleScrollTo(`#${hash}`);
|
||||
fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!);
|
||||
await waitFakeTimer();
|
||||
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 900);
|
||||
dateNowMock = dataNowMockFn();
|
||||
|
||||
setProps({ targetOffset: 200 });
|
||||
anchorInstance!.handleScrollTo(`#${hash}`);
|
||||
fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!);
|
||||
await waitFakeTimer();
|
||||
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800);
|
||||
|
||||
dateNowMock.mockRestore();
|
||||
getBoundingClientRectMock.mockReturnValue({
|
||||
width: 100,
|
||||
height: 100,
|
||||
@ -618,102 +318,61 @@ describe('Anchor Render', () => {
|
||||
|
||||
it('test edge case when container is not windows', async () => {
|
||||
const hash = getHashUrl();
|
||||
let dateNowMock: jest.SpyInstance;
|
||||
|
||||
function dataNowMockFn() {
|
||||
let start = 0;
|
||||
|
||||
const handler = () => {
|
||||
start += 1000;
|
||||
return start;
|
||||
};
|
||||
|
||||
return jest.spyOn(Date, 'now').mockImplementation(handler);
|
||||
}
|
||||
|
||||
dateNowMock = dataNowMockFn();
|
||||
|
||||
const scrollToSpy = jest.spyOn(window, 'scrollTo');
|
||||
const root = createDiv();
|
||||
render(<h1 id={hash}>Hello</h1>, { container: root });
|
||||
|
||||
let anchorInstance: InternalAnchorClass;
|
||||
const { rerender } = render(
|
||||
<Anchor
|
||||
getContainer={() => document.body}
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
>
|
||||
const { container, rerender } = render(
|
||||
<Anchor getContainer={() => document.body}>
|
||||
<Link href={`#${hash}`} title={hash} />
|
||||
</Anchor>,
|
||||
);
|
||||
|
||||
const setProps = (props: Record<string, any>) =>
|
||||
rerender(
|
||||
<Anchor
|
||||
getContainer={() => document.body}
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<Anchor getContainer={() => document.body} {...props}>
|
||||
<Link href={`#${hash}`} title={hash} />
|
||||
</Anchor>,
|
||||
);
|
||||
anchorInstance!.handleScrollTo(`#${hash}`);
|
||||
|
||||
fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!);
|
||||
await waitFakeTimer();
|
||||
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800);
|
||||
dateNowMock = dataNowMockFn();
|
||||
|
||||
setProps({ offsetTop: 100 });
|
||||
anchorInstance!.handleScrollTo(`#${hash}`);
|
||||
fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!);
|
||||
await waitFakeTimer();
|
||||
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800);
|
||||
dateNowMock = dataNowMockFn();
|
||||
|
||||
setProps({ targetOffset: 200 });
|
||||
anchorInstance!.handleScrollTo(`#${hash}`);
|
||||
fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!);
|
||||
await waitFakeTimer();
|
||||
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800);
|
||||
|
||||
dateNowMock.mockRestore();
|
||||
});
|
||||
|
||||
describe('getCurrentAnchor', () => {
|
||||
it('Anchor getCurrentAnchor prop', () => {
|
||||
it('getCurrentAnchor prop', () => {
|
||||
const hash1 = getHashUrl();
|
||||
const hash2 = getHashUrl();
|
||||
const getCurrentAnchor = () => `#${hash2}`;
|
||||
let anchorInstance: InternalAnchorClass;
|
||||
render(
|
||||
<Anchor
|
||||
getCurrentAnchor={getCurrentAnchor}
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
>
|
||||
const { container } = render(
|
||||
<Anchor getCurrentAnchor={getCurrentAnchor}>
|
||||
<Link href={`#${hash1}`} title={hash1} />
|
||||
<Link href={`#${hash2}`} title={hash2} />
|
||||
</Anchor>,
|
||||
);
|
||||
|
||||
expect(anchorInstance!.state.activeLink).toBe(`#${hash2}`);
|
||||
expect(container.querySelector(`.ant-anchor-link-title-active`)?.textContent).toBe(hash2);
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/30584
|
||||
it('should trigger onChange when have getCurrentAnchor', async () => {
|
||||
it('should trigger onChange when have getCurrentAnchor', () => {
|
||||
const hash1 = getHashUrl();
|
||||
const hash2 = getHashUrl();
|
||||
const onChange = jest.fn();
|
||||
let anchorInstance: InternalAnchorClass;
|
||||
render(
|
||||
<Anchor
|
||||
onChange={onChange}
|
||||
getCurrentAnchor={() => hash1}
|
||||
ref={node => {
|
||||
anchorInstance = node as InternalAnchorClass;
|
||||
}}
|
||||
>
|
||||
const { container } = render(
|
||||
<Anchor onChange={onChange} getCurrentAnchor={() => hash1}>
|
||||
<Link href={`#${hash1}`} title={hash1} />
|
||||
<Link href={`#${hash2}`} title={hash2} />
|
||||
</Anchor>,
|
||||
@ -723,13 +382,13 @@ describe('Anchor Render', () => {
|
||||
);
|
||||
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
anchorInstance!.handleScrollTo(hash2);
|
||||
fireEvent.click(container.querySelector(`a[href="#${hash2}"]`)!);
|
||||
expect(onChange).toHaveBeenCalledTimes(2);
|
||||
expect(onChange).toHaveBeenCalledWith(hash2);
|
||||
expect(onChange).toHaveBeenLastCalledWith(`#${hash2}`);
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/34784
|
||||
it('getCurrentAnchor have default link as argument', async () => {
|
||||
it('getCurrentAnchor have default link as argument', () => {
|
||||
const hash1 = getHashUrl();
|
||||
const hash2 = getHashUrl();
|
||||
const getCurrentAnchor = jest.fn();
|
||||
@ -747,7 +406,7 @@ describe('Anchor Render', () => {
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/37627
|
||||
it('should update anchorLink when component is rerender', async () => {
|
||||
it('should update active link when getCurrentAnchor changes', async () => {
|
||||
const hash1 = getHashUrl();
|
||||
const hash2 = getHashUrl();
|
||||
const Demo: React.FC<{ current: string }> = ({ current }) => (
|
||||
@ -761,7 +420,8 @@ describe('Anchor Render', () => {
|
||||
rerender(<Demo current={hash2} />);
|
||||
expect(container.querySelector(`.ant-anchor-link-title-active`)?.textContent).toBe(hash2);
|
||||
});
|
||||
it('should correct render when href is null', () => {
|
||||
|
||||
it('should render correctly when href is null', () => {
|
||||
expect(() => {
|
||||
render(
|
||||
<Anchor>
|
||||
|
@ -1,53 +1,40 @@
|
||||
import React, { memo, useContext, useRef, useState } from 'react';
|
||||
import { fireEvent, getNodeText, render } from '../../../tests/utils';
|
||||
import React, { memo, useContext } from 'react';
|
||||
import { fireEvent, pureRender } from '../../../tests/utils';
|
||||
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>
|
||||
);
|
||||
});
|
||||
let innerCount = 0;
|
||||
let outerCount = 0;
|
||||
|
||||
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>
|
||||
);
|
||||
const handleClick = () => {
|
||||
outerCount++;
|
||||
};
|
||||
|
||||
it("Rendering on Anchor without changed AnchorContext won't trigger rendering on child component.", () => {
|
||||
const { container } = render(<CacheOuter />);
|
||||
const childCount = getNodeText(container.querySelector('#child_count')!);
|
||||
|
||||
fireEvent.click(container.querySelector('#parent_btn')!);
|
||||
|
||||
expect(getNodeText(container.querySelector('#parent_count')!)).toBe('2');
|
||||
// child component won't rerender
|
||||
expect(getNodeText(container.querySelector('#child_count')!)).toBe(childCount);
|
||||
fireEvent.click(container.querySelector('#parent_btn')!);
|
||||
expect(getNodeText(container.querySelector('#parent_count')!)).toBe('3');
|
||||
// child component won't rerender
|
||||
expect(getNodeText(container.querySelector('#child_count')!)).toBe(childCount);
|
||||
// we use'memo' here in order to only render inner component while context changed.
|
||||
const CacheInner: React.FC = memo(() => {
|
||||
innerCount++;
|
||||
// subscribe locale context
|
||||
useContext(AnchorContext);
|
||||
return null;
|
||||
});
|
||||
|
||||
const CacheOuter: React.FC = memo(() => (
|
||||
<>
|
||||
<button type="button" onClick={handleClick} id="parent_btn">
|
||||
Click
|
||||
</button>
|
||||
<Anchor affix={false}>
|
||||
<CacheInner />
|
||||
</Anchor>
|
||||
</>
|
||||
));
|
||||
|
||||
it("Rendering on Anchor without changed won't trigger rendering on child component.", () => {
|
||||
const { container, unmount } = pureRender(<CacheOuter />);
|
||||
expect(outerCount).toBe(0);
|
||||
expect(innerCount).toBe(2);
|
||||
fireEvent.click(container.querySelector('#parent_btn')!);
|
||||
expect(outerCount).toBe(1);
|
||||
expect(innerCount).toBe(2);
|
||||
unmount();
|
||||
});
|
||||
|
@ -12,6 +12,10 @@ Hyperlinks to scroll on one page.
|
||||
|
||||
For displaying anchor hyperlinks on page and jumping between them.
|
||||
|
||||
> Notes for developers
|
||||
>
|
||||
> After version `4.24.0`, we rewrite Anchor use FC, Some methods of obtaining `ref` and calling internal instance methods will invalid.
|
||||
|
||||
## API
|
||||
|
||||
### Anchor Props
|
||||
|
@ -13,6 +13,10 @@ cover: https://gw.alipayobjects.com/zos/alicdn/_1-C1JwsC/Anchor.svg
|
||||
|
||||
需要展现当前页面上可供跳转的锚点链接,以及快速在锚点之间跳转。
|
||||
|
||||
> 开发者注意事项:
|
||||
>
|
||||
> 自 `4.24.0` 起,由于组件从 class 重构成 FC,之前一些获取 `ref` 并调用内部实例方法的写法都会失效
|
||||
|
||||
## API
|
||||
|
||||
### Anchor Props
|
||||
|
@ -65,7 +65,7 @@ const genSharedAnchorStyle: GenerateStyle<AnchorToken> = (token): CSSObject => {
|
||||
transform: 'translateX(-50%)',
|
||||
transition: `top ${token.motionDurationSlow} ease-in-out`,
|
||||
|
||||
'&.visible': {
|
||||
[`&.${componentCls}-anchor-ink-ball-visible`]: {
|
||||
display: 'inline-block',
|
||||
},
|
||||
},
|
||||
|
@ -701,7 +701,7 @@ exports[`renders ./components/auto-complete/demo/form-debug.md extend context co
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -849,7 +849,7 @@ exports[`renders ./components/auto-complete/demo/form-debug.md extend context co
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1137,7 +1137,7 @@ exports[`renders ./components/auto-complete/demo/form-debug.md extend context co
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1354,7 +1354,7 @@ exports[`renders ./components/auto-complete/demo/form-debug.md extend context co
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1580,7 +1580,7 @@ exports[`renders ./components/auto-complete/demo/form-debug.md extend context co
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -58,7 +58,7 @@ const BackTop: React.FC<BackTopProps> = props => {
|
||||
});
|
||||
|
||||
const ref = React.createRef<HTMLDivElement>();
|
||||
const scrollEvent = React.useRef<any>();
|
||||
const scrollEvent = React.useRef<ReturnType<typeof addEventListener>>(null);
|
||||
|
||||
const getDefaultTarget = () =>
|
||||
ref.current && ref.current.ownerDocument ? ref.current.ownerDocument : window;
|
||||
|
@ -69,7 +69,7 @@ const Badge: CompoundedComponent = ({
|
||||
|
||||
const isZero = numberedDisplayCount === '0' || numberedDisplayCount === 0;
|
||||
|
||||
const ignoreCount = count === null || (count !== null && isZero);
|
||||
const ignoreCount = count === null || isZero;
|
||||
|
||||
const hasStatus =
|
||||
((status !== null && status !== undefined) || (color !== null && color !== undefined)) &&
|
||||
|
@ -1,6 +1,7 @@
|
||||
import DownOutlined from '@ant-design/icons/DownOutlined';
|
||||
import * as React from 'react';
|
||||
|
||||
import warning from '../_util/warning';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
import type { DropdownProps } from '../dropdown/dropdown';
|
||||
import Dropdown from '../dropdown/dropdown';
|
||||
@ -9,30 +10,47 @@ export interface BreadcrumbItemProps {
|
||||
prefixCls?: string;
|
||||
separator?: React.ReactNode;
|
||||
href?: string;
|
||||
overlay?: DropdownProps['overlay'];
|
||||
menu?: DropdownProps['menu'];
|
||||
dropdownProps?: DropdownProps;
|
||||
onClick?: React.MouseEventHandler<HTMLAnchorElement | HTMLSpanElement>;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
|
||||
// Deprecated
|
||||
/** @deprecated Please use `menu` instead */
|
||||
overlay?: DropdownProps['overlay'];
|
||||
}
|
||||
interface BreadcrumbItemInterface extends React.FC<BreadcrumbItemProps> {
|
||||
__ANT_BREADCRUMB_ITEM: boolean;
|
||||
}
|
||||
const BreadcrumbItem: BreadcrumbItemInterface = ({
|
||||
prefixCls: customizePrefixCls,
|
||||
separator = '/',
|
||||
children,
|
||||
overlay,
|
||||
dropdownProps,
|
||||
...restProps
|
||||
}) => {
|
||||
const BreadcrumbItem: BreadcrumbItemInterface = props => {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
separator = '/',
|
||||
children,
|
||||
menu,
|
||||
overlay,
|
||||
dropdownProps,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const { getPrefixCls } = React.useContext(ConfigContext);
|
||||
const prefixCls = getPrefixCls('breadcrumb', customizePrefixCls);
|
||||
|
||||
// Warning for deprecated usage
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
warning(
|
||||
!('overlay' in props),
|
||||
'Breadcrumb.Item',
|
||||
'`overlay` is deprecated. Please use `menu` instead.',
|
||||
);
|
||||
}
|
||||
|
||||
/** If overlay is have Wrap a Dropdown */
|
||||
const renderBreadcrumbNode = (breadcrumbItem: React.ReactNode) => {
|
||||
if (overlay) {
|
||||
if (menu || overlay) {
|
||||
return (
|
||||
<Dropdown overlay={overlay} placement="bottom" {...dropdownProps}>
|
||||
<Dropdown menu={menu} overlay={overlay} placement="bottom" {...dropdownProps}>
|
||||
<span className={`${prefixCls}-overlay-link`}>
|
||||
{breadcrumbItem}
|
||||
<DownOutlined />
|
||||
|
@ -33,6 +33,19 @@ describe('Breadcrumb', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('overlay deprecation warning', () => {
|
||||
render(
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item overlay={<div>menu</div>}>
|
||||
<a href="">General</a>
|
||||
</Breadcrumb.Item>
|
||||
</Breadcrumb>,
|
||||
);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
'Warning: [antd: Breadcrumb.Item] `overlay` is deprecated. Please use `menu` instead.',
|
||||
);
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/5015
|
||||
it('should allow Breadcrumb.Item is null or undefined', () => {
|
||||
const { asFragment } = render(
|
||||
|
@ -14,39 +14,35 @@ title:
|
||||
Breadcrumbs support drop down menu.
|
||||
|
||||
```tsx
|
||||
import { Breadcrumb, Menu } from 'antd';
|
||||
import { Breadcrumb } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const menu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="http://www.alipay.com/">
|
||||
General
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="http://www.taobao.com/">
|
||||
Layout
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="http://www.tmall.com/">
|
||||
Navigation
|
||||
</a>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const items = [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="http://www.alipay.com/">
|
||||
General
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="http://www.taobao.com/">
|
||||
Layout
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="http://www.tmall.com/">
|
||||
Navigation
|
||||
</a>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const App: React.FC = () => (
|
||||
<Breadcrumb>
|
||||
@ -54,7 +50,7 @@ const App: React.FC = () => (
|
||||
<Breadcrumb.Item>
|
||||
<a href="">Component</a>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item overlay={menu}>
|
||||
<Breadcrumb.Item menu={{ items }}>
|
||||
<a href="">General</a>
|
||||
</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>Button</Breadcrumb.Item>
|
||||
|
@ -13,6 +13,39 @@ A breadcrumb displays the current location within a hierarchy. It allows going b
|
||||
- When you need to inform the user of where they are.
|
||||
- When the user may need to navigate back to a higher level.
|
||||
|
||||
### Usage upgrade after 4.24.0
|
||||
|
||||
```__react
|
||||
import Alert from '../alert';
|
||||
ReactDOM.render(<Alert message="After version 4.24.0, we provide a simpler usage <Breadcrumb.Item menu={{ items: [...] }}> with better performance and potential of writing simpler code style in your applications. Meanwhile, we deprecated the old usage in browser console, we will remove it in antd 5.0." />, mountNode);
|
||||
```
|
||||
|
||||
```jsx
|
||||
// works when >=4.24.0, recommended ✅
|
||||
const items = [
|
||||
{ label: 'item 1', key: 'item-1' }, // remember to pass the key prop
|
||||
{ label: 'item 2', key: 'item-2' },
|
||||
];
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item menu={{ items }}>Ant Design</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
);
|
||||
|
||||
// works when <4.24.0, deprecated when >=4.24.0 🙅🏻♀️
|
||||
const menu = (
|
||||
<Menu>
|
||||
<Menu.Item>item 1</Menu.Item>
|
||||
<Menu.Item>item 2</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item overlay={menu}>Ant Design</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
);
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### Breadcrumb
|
||||
@ -31,14 +64,14 @@ A breadcrumb displays the current location within a hierarchy. It allows going b
|
||||
| className | The additional css class | string | - | |
|
||||
| dropdownProps | The dropdown props | [Dropdown](/components/dropdown) | - | |
|
||||
| href | Target of hyperlink | string | - | |
|
||||
| overlay | The dropdown menu | [Menu](/components/menu) \| () => Menu | - | |
|
||||
| menu | The menu props | [MenuProps](/components/menu/#API) | - | 4.24.0 |
|
||||
| onClick | Set the handler to handle click event | (e:MouseEvent) => void | - | |
|
||||
|
||||
### Breadcrumb.Separator
|
||||
|
||||
| Property | Description | Type | Default | Version |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| children | Custom separator | ReactNode | `/` | |
|
||||
| Property | Description | Type | Default | Version |
|
||||
| -------- | ---------------- | --------- | ------- | ------- |
|
||||
| children | Custom separator | ReactNode | `/` | |
|
||||
|
||||
> When using `Breadcrumb.Separator`,its parent component must be set to `separator=""`, otherwise the default separator of the parent component will appear.
|
||||
|
||||
|
@ -14,6 +14,39 @@ cover: https://gw.alipayobjects.com/zos/alicdn/9Ltop8JwH/Breadcrumb.svg
|
||||
- 当需要告知用户『你在哪里』时;
|
||||
- 当需要向上导航的功能时。
|
||||
|
||||
### 4.24.0 用法升级
|
||||
|
||||
```__react
|
||||
import Alert from '../alert';
|
||||
ReactDOM.render(<Alert message="在 4.24.0 版本后,我们提供了 <Breadcrumb.Item menu={{ items: [...] }}> 的简写方式,有更好的性能和更方便的数据组织方式,开发者不再需要自行拼接 JSX。同时我们废弃了原先的写法,你还是可以在 4.x 继续使用,但会在控制台看到警告,并会在 5.0 后移除。" />, mountNode);
|
||||
```
|
||||
|
||||
```jsx
|
||||
// >=4.24.0 可用,推荐的写法 ✅
|
||||
const items = [
|
||||
{ label: '菜单项一', key: 'item-1' }, // 菜单项务必填写 key
|
||||
{ label: '菜单项二', key: 'item-2' },
|
||||
];
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item menu={{ items }}>Ant Design</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
);
|
||||
|
||||
// <4.24.0 可用,>=4.24.0 时不推荐 🙅🏻♀️
|
||||
const menu = (
|
||||
<Menu>
|
||||
<Menu.Item>菜单项一</Menu.Item>
|
||||
<Menu.Item>菜单项二</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item overlay={menu}>Ant Design</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
);
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### Breadcrumb
|
||||
@ -27,19 +60,19 @@ cover: https://gw.alipayobjects.com/zos/alicdn/9Ltop8JwH/Breadcrumb.svg
|
||||
|
||||
### Breadcrumb.Item
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| className | 自定义类名 | string | - | |
|
||||
| dropdownProps | 弹出下拉菜单的自定义配置 | [Dropdown](/components/dropdown) | - | |
|
||||
| href | 链接的目的地 | string | - | |
|
||||
| overlay | 下拉菜单的内容 | [Menu](/components/menu) \| () => Menu | - | |
|
||||
| onClick | 单击事件 | (e:MouseEvent) => void | - | |
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| ------------- | ------------------------ | ---------------------------------- | ------ | ------ |
|
||||
| className | 自定义类名 | string | - | |
|
||||
| dropdownProps | 弹出下拉菜单的自定义配置 | [Dropdown](/components/dropdown) | - | |
|
||||
| href | 链接的目的地 | string | - | |
|
||||
| menu | 菜单配置项 | [MenuProps](/components/menu/#API) | - | 4.24.0 |
|
||||
| onClick | 单击事件 | (e:MouseEvent) => void | - | |
|
||||
|
||||
### Breadcrumb.Separator
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| children | 要显示的分隔符 | ReactNode | `/` | |
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| -------- | -------------- | --------- | ------ | ---- |
|
||||
| children | 要显示的分隔符 | ReactNode | `/` | |
|
||||
|
||||
> 注意:在使用 `Breadcrumb.Separator` 时,其父组件的分隔符必须设置为 `separator=""`,否则会出现父组件默认的分隔符。
|
||||
|
||||
|
@ -15,38 +15,33 @@ If you need several buttons, we recommend that you use 1 primary button + n seco
|
||||
|
||||
```tsx
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Button, Dropdown, Menu } from 'antd';
|
||||
import { Button, Dropdown } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const onMenuClick: MenuProps['onClick'] = e => {
|
||||
console.log('click', e);
|
||||
};
|
||||
|
||||
const menu = (
|
||||
<Menu
|
||||
onClick={onMenuClick}
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: '1st item',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: '2nd item',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: '3rd item',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const items = [
|
||||
{
|
||||
key: '1',
|
||||
label: '1st item',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: '2nd item',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: '3rd item',
|
||||
},
|
||||
];
|
||||
|
||||
const App: React.FC = () => (
|
||||
<>
|
||||
<Button type="primary">primary</Button>
|
||||
<Button>secondary</Button>
|
||||
<Dropdown.Button overlay={menu}>Actions</Dropdown.Button>
|
||||
<Dropdown.Button menu={{ items, onClick: onMenuClick }}>Actions</Dropdown.Button>
|
||||
</>
|
||||
);
|
||||
|
||||
|
@ -57,7 +57,6 @@ describe('Carousel', () => {
|
||||
});
|
||||
|
||||
it('should trigger autoPlay after window resize', async () => {
|
||||
jest.useRealTimers();
|
||||
const ref = React.createRef<CarouselRef>();
|
||||
render(
|
||||
<Carousel autoplay ref={ref}>
|
||||
|
@ -15,9 +15,18 @@ interface CarouselToken extends FullToken<'Carousel'> {
|
||||
}
|
||||
|
||||
const genCarouselStyle: GenerateStyle<CarouselToken> = token => {
|
||||
const { componentCls, antCls, carouselArrowSize, carouselDotOffset, carouselDotInline } = token;
|
||||
const {
|
||||
componentCls,
|
||||
antCls,
|
||||
carouselArrowSize,
|
||||
carouselDotOffset,
|
||||
marginXXS,
|
||||
carouselDotInline,
|
||||
} = token;
|
||||
const arrowOffset = -carouselArrowSize * 1.25;
|
||||
|
||||
const carouselDotMargin = marginXXS;
|
||||
|
||||
return {
|
||||
[componentCls]: {
|
||||
...resetComponent(token),
|
||||
@ -196,7 +205,7 @@ const genCarouselStyle: GenerateStyle<CarouselToken> = token => {
|
||||
boxSizing: 'content-box',
|
||||
width: token.dotWidth,
|
||||
height: token.dotHeight,
|
||||
marginInline: carouselDotInline,
|
||||
marginInline: carouselDotMargin,
|
||||
padding: 0,
|
||||
textAlign: 'center',
|
||||
textIndent: -999,
|
||||
@ -204,6 +213,7 @@ const genCarouselStyle: GenerateStyle<CarouselToken> = token => {
|
||||
transition: `all ${token.motionDurationSlow}`,
|
||||
|
||||
button: {
|
||||
position: 'relative',
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: token.dotHeight,
|
||||
@ -221,6 +231,12 @@ const genCarouselStyle: GenerateStyle<CarouselToken> = token => {
|
||||
'&: hover, &:focus': {
|
||||
opacity: 0.75,
|
||||
},
|
||||
|
||||
'&::after': {
|
||||
position: 'absolute',
|
||||
inset: -carouselDotMargin,
|
||||
content: '""',
|
||||
},
|
||||
},
|
||||
|
||||
'&.slick-active': {
|
||||
@ -242,7 +258,7 @@ const genCarouselStyle: GenerateStyle<CarouselToken> = token => {
|
||||
};
|
||||
|
||||
const genCarouselVerticalStyle: GenerateStyle<CarouselToken> = token => {
|
||||
const { componentCls, carouselDotOffset } = token;
|
||||
const { componentCls, carouselDotOffset, marginXXS } = token;
|
||||
|
||||
const reverseSizeOfDot = {
|
||||
width: token.dotHeight,
|
||||
@ -273,7 +289,7 @@ const genCarouselVerticalStyle: GenerateStyle<CarouselToken> = token => {
|
||||
li: {
|
||||
// reverse width and height in vertical situation
|
||||
...reverseSizeOfDot,
|
||||
margin: '4px 2px',
|
||||
margin: `${marginXXS}px 0`,
|
||||
verticalAlign: 'baseline',
|
||||
|
||||
button: reverseSizeOfDot,
|
||||
|
@ -3149,7 +3149,7 @@ exports[`renders ./components/cascader/demo/status.md extend context correctly 1
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -3299,7 +3299,7 @@ exports[`renders ./components/cascader/demo/status.md extend context correctly 1
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1617,9 +1617,9 @@ exports[`Cascader rtl render component should be rendered correctly in RTL direc
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Cascader should highlight keyword and filter when search in Cascader 1`] = `"<div><div class=\\"ant-cascader-menus\\"><ul class=\\"ant-cascader-menu\\" role=\\"menu\\"><li class=\\"ant-cascader-menu-item\\" role=\\"menuitemcheckbox\\" aria-checked=\\"false\\" data-path-key=\\"zhejiang__RC_CASCADER_SPLIT__hangzhou__RC_CASCADER_SPLIT__xihu\\"><div class=\\"ant-cascader-menu-item-content\\"><span class=\\"ant-cascader-menu-item-keyword\\">Z</span>hejiang / Hang<span class=\\"ant-cascader-menu-item-keyword\\">z</span>hou / West Lake</div></li><li class=\\"ant-cascader-menu-item\\" role=\\"menuitemcheckbox\\" aria-checked=\\"false\\" data-path-key=\\"jiangsu__RC_CASCADER_SPLIT__nanjing__RC_CASCADER_SPLIT__zhonghuamen\\"><div class=\\"ant-cascader-menu-item-content\\">Jiangsu / Nanjing / <span class=\\"ant-cascader-menu-item-keyword\\">Z</span>hong Hua Men</div></li></ul></div></div>"`;
|
||||
exports[`Cascader should highlight keyword and filter when search in Cascader 1`] = `"<div><div class="ant-cascader-menus"><ul class="ant-cascader-menu" role="menu"><li class="ant-cascader-menu-item" role="menuitemcheckbox" aria-checked="false" data-path-key="zhejiang__RC_CASCADER_SPLIT__hangzhou__RC_CASCADER_SPLIT__xihu"><div class="ant-cascader-menu-item-content"><span class="ant-cascader-menu-item-keyword">Z</span>hejiang / Hang<span class="ant-cascader-menu-item-keyword">z</span>hou / West Lake</div></li><li class="ant-cascader-menu-item" role="menuitemcheckbox" aria-checked="false" data-path-key="jiangsu__RC_CASCADER_SPLIT__nanjing__RC_CASCADER_SPLIT__zhonghuamen"><div class="ant-cascader-menu-item-content">Jiangsu / Nanjing / <span class="ant-cascader-menu-item-keyword">Z</span>hong Hua Men</div></li></ul></div></div>"`;
|
||||
|
||||
exports[`Cascader should highlight keyword and filter when search in Cascader with same field name of label and value 1`] = `"<div><div class=\\"ant-cascader-menus\\"><ul class=\\"ant-cascader-menu\\" role=\\"menu\\"><li class=\\"ant-cascader-menu-item\\" role=\\"menuitemcheckbox\\" aria-checked=\\"false\\" data-path-key=\\"Zhejiang__RC_CASCADER_SPLIT__Hangzhou__RC_CASCADER_SPLIT__West Lake\\"><div class=\\"ant-cascader-menu-item-content\\"><span class=\\"ant-cascader-menu-item-keyword\\">Z</span>hejiang / Hang<span class=\\"ant-cascader-menu-item-keyword\\">z</span>hou / West Lake</div></li><li class=\\"ant-cascader-menu-item ant-cascader-menu-item-disabled\\" role=\\"menuitemcheckbox\\" aria-checked=\\"false\\" data-path-key=\\"Zhejiang__RC_CASCADER_SPLIT__Hangzhou__RC_CASCADER_SPLIT__Xia Sha\\"><div class=\\"ant-cascader-menu-item-content\\"><span class=\\"ant-cascader-menu-item-keyword\\">Z</span>hejiang / Hang<span class=\\"ant-cascader-menu-item-keyword\\">z</span>hou / Xia Sha</div></li></ul></div></div>"`;
|
||||
exports[`Cascader should highlight keyword and filter when search in Cascader with same field name of label and value 1`] = `"<div><div class="ant-cascader-menus"><ul class="ant-cascader-menu" role="menu"><li class="ant-cascader-menu-item" role="menuitemcheckbox" aria-checked="false" data-path-key="Zhejiang__RC_CASCADER_SPLIT__Hangzhou__RC_CASCADER_SPLIT__West Lake"><div class="ant-cascader-menu-item-content"><span class="ant-cascader-menu-item-keyword">Z</span>hejiang / Hang<span class="ant-cascader-menu-item-keyword">z</span>hou / West Lake</div></li><li class="ant-cascader-menu-item ant-cascader-menu-item-disabled" role="menuitemcheckbox" aria-checked="false" data-path-key="Zhejiang__RC_CASCADER_SPLIT__Hangzhou__RC_CASCADER_SPLIT__Xia Sha"><div class="ant-cascader-menu-item-content"><span class="ant-cascader-menu-item-keyword">Z</span>hejiang / Hang<span class="ant-cascader-menu-item-keyword">z</span>hou / Xia Sha</div></li></ul></div></div>"`;
|
||||
|
||||
exports[`Cascader should render not found content 1`] = `
|
||||
<div
|
||||
@ -1685,7 +1685,7 @@ exports[`Cascader should render not found content 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1760,7 +1760,7 @@ exports[`Cascader should show not found content when options.length is 0 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -15033,7 +15033,7 @@ exports[`ConfigProvider components Empty configProvider 1`] = `
|
||||
<div
|
||||
class="config-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -15108,7 +15108,7 @@ exports[`ConfigProvider components Empty configProvider componentDisabled 1`] =
|
||||
<div
|
||||
class="config-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -15183,7 +15183,7 @@ exports[`ConfigProvider components Empty configProvider componentSize large 1`]
|
||||
<div
|
||||
class="config-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -15258,7 +15258,7 @@ exports[`ConfigProvider components Empty configProvider componentSize middle 1`]
|
||||
<div
|
||||
class="config-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -15333,7 +15333,7 @@ exports[`ConfigProvider components Empty configProvider virtual and dropdownMatc
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -15408,7 +15408,7 @@ exports[`ConfigProvider components Empty normal 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -15483,7 +15483,7 @@ exports[`ConfigProvider components Empty prefixCls 1`] = `
|
||||
<div
|
||||
class="prefix-Empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -18863,7 +18863,6 @@ exports[`ConfigProvider components Pagination configProvider 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="config-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -19010,7 +19009,6 @@ exports[`ConfigProvider components Pagination configProvider 1`] = `
|
||||
</ul>
|
||||
<ul
|
||||
class="config-pagination config-pagination-mini"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -19162,7 +19160,6 @@ exports[`ConfigProvider components Pagination configProvider componentDisabled 1
|
||||
<div>
|
||||
<ul
|
||||
class="config-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -19310,7 +19307,6 @@ exports[`ConfigProvider components Pagination configProvider componentDisabled 1
|
||||
</ul>
|
||||
<ul
|
||||
class="config-pagination config-pagination-mini"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -19463,7 +19459,6 @@ exports[`ConfigProvider components Pagination configProvider componentSize large
|
||||
<div>
|
||||
<ul
|
||||
class="config-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -19610,7 +19605,6 @@ exports[`ConfigProvider components Pagination configProvider componentSize large
|
||||
</ul>
|
||||
<ul
|
||||
class="config-pagination config-pagination-mini"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -19762,7 +19756,6 @@ exports[`ConfigProvider components Pagination configProvider componentSize middl
|
||||
<div>
|
||||
<ul
|
||||
class="config-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -19909,7 +19902,6 @@ exports[`ConfigProvider components Pagination configProvider componentSize middl
|
||||
</ul>
|
||||
<ul
|
||||
class="config-pagination config-pagination-mini"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -20061,7 +20053,6 @@ exports[`ConfigProvider components Pagination configProvider virtual and dropdow
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -20208,7 +20199,6 @@ exports[`ConfigProvider components Pagination configProvider virtual and dropdow
|
||||
</ul>
|
||||
<ul
|
||||
class="ant-pagination ant-pagination-mini"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -20360,7 +20350,6 @@ exports[`ConfigProvider components Pagination normal 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -20507,7 +20496,6 @@ exports[`ConfigProvider components Pagination normal 1`] = `
|
||||
</ul>
|
||||
<ul
|
||||
class="ant-pagination ant-pagination-mini"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -20659,7 +20647,6 @@ exports[`ConfigProvider components Pagination prefixCls 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="prefix-Pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -20806,7 +20793,6 @@ exports[`ConfigProvider components Pagination prefixCls 1`] = `
|
||||
</ul>
|
||||
<ul
|
||||
class="prefix-Pagination prefix-Pagination-mini"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -26611,7 +26597,7 @@ exports[`ConfigProvider components Table configProvider 1`] = `
|
||||
>
|
||||
<tr>
|
||||
<th
|
||||
aria-label="Name sortable"
|
||||
aria-label="this column's title is Name,this column is sortable"
|
||||
class="config-table-cell config-table-column-has-sorters"
|
||||
tabindex="0"
|
||||
>
|
||||
@ -26873,7 +26859,7 @@ exports[`ConfigProvider components Table configProvider 1`] = `
|
||||
<div
|
||||
class="config-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@ -26916,7 +26902,7 @@ exports[`ConfigProvider components Table configProvider componentDisabled 1`] =
|
||||
>
|
||||
<tr>
|
||||
<th
|
||||
aria-label="Name sortable"
|
||||
aria-label="this column's title is Name,this column is sortable"
|
||||
class="config-table-cell config-table-column-has-sorters"
|
||||
tabindex="0"
|
||||
>
|
||||
@ -27180,7 +27166,7 @@ exports[`ConfigProvider components Table configProvider componentDisabled 1`] =
|
||||
<div
|
||||
class="config-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@ -27223,7 +27209,7 @@ exports[`ConfigProvider components Table configProvider componentSize large 1`]
|
||||
>
|
||||
<tr>
|
||||
<th
|
||||
aria-label="Name sortable"
|
||||
aria-label="this column's title is Name,this column is sortable"
|
||||
class="config-table-cell config-table-column-has-sorters"
|
||||
tabindex="0"
|
||||
>
|
||||
@ -27485,7 +27471,7 @@ exports[`ConfigProvider components Table configProvider componentSize large 1`]
|
||||
<div
|
||||
class="config-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@ -27528,7 +27514,7 @@ exports[`ConfigProvider components Table configProvider componentSize middle 1`]
|
||||
>
|
||||
<tr>
|
||||
<th
|
||||
aria-label="Name sortable"
|
||||
aria-label="this column's title is Name,this column is sortable"
|
||||
class="config-table-cell config-table-column-has-sorters"
|
||||
tabindex="0"
|
||||
>
|
||||
@ -27790,7 +27776,7 @@ exports[`ConfigProvider components Table configProvider componentSize middle 1`]
|
||||
<div
|
||||
class="config-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@ -27833,7 +27819,7 @@ exports[`ConfigProvider components Table configProvider virtual and dropdownMatc
|
||||
>
|
||||
<tr>
|
||||
<th
|
||||
aria-label="Name sortable"
|
||||
aria-label="this column's title is Name,this column is sortable"
|
||||
class="ant-table-cell ant-table-column-has-sorters"
|
||||
tabindex="0"
|
||||
>
|
||||
@ -28095,7 +28081,7 @@ exports[`ConfigProvider components Table configProvider virtual and dropdownMatc
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@ -28138,7 +28124,7 @@ exports[`ConfigProvider components Table normal 1`] = `
|
||||
>
|
||||
<tr>
|
||||
<th
|
||||
aria-label="Name sortable"
|
||||
aria-label="this column's title is Name,this column is sortable"
|
||||
class="ant-table-cell ant-table-column-has-sorters"
|
||||
tabindex="0"
|
||||
>
|
||||
@ -28400,7 +28386,7 @@ exports[`ConfigProvider components Table normal 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@ -28443,7 +28429,7 @@ exports[`ConfigProvider components Table prefixCls 1`] = `
|
||||
>
|
||||
<tr>
|
||||
<th
|
||||
aria-label="Name sortable"
|
||||
aria-label="this column's title is Name,this column is sortable"
|
||||
class="prefix-Table-cell prefix-Table-column-has-sorters"
|
||||
tabindex="0"
|
||||
>
|
||||
@ -28705,7 +28691,7 @@ exports[`ConfigProvider components Table prefixCls 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@ -38481,7 +38467,7 @@ exports[`ConfigProvider components Transfer configProvider 1`] = `
|
||||
<div
|
||||
class="config-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -38640,7 +38626,7 @@ exports[`ConfigProvider components Transfer configProvider 1`] = `
|
||||
<div
|
||||
class="config-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -38753,7 +38739,7 @@ exports[`ConfigProvider components Transfer configProvider componentDisabled 1`]
|
||||
<div
|
||||
class="config-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -38913,7 +38899,7 @@ exports[`ConfigProvider components Transfer configProvider componentDisabled 1`]
|
||||
<div
|
||||
class="config-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -39025,7 +39011,7 @@ exports[`ConfigProvider components Transfer configProvider componentSize large 1
|
||||
<div
|
||||
class="config-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -39184,7 +39170,7 @@ exports[`ConfigProvider components Transfer configProvider componentSize large 1
|
||||
<div
|
||||
class="config-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -39296,7 +39282,7 @@ exports[`ConfigProvider components Transfer configProvider componentSize middle
|
||||
<div
|
||||
class="config-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -39455,7 +39441,7 @@ exports[`ConfigProvider components Transfer configProvider componentSize middle
|
||||
<div
|
||||
class="config-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -39567,7 +39553,7 @@ exports[`ConfigProvider components Transfer configProvider virtual and dropdownM
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -39726,7 +39712,7 @@ exports[`ConfigProvider components Transfer configProvider virtual and dropdownM
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -39838,7 +39824,7 @@ exports[`ConfigProvider components Transfer normal 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -39997,7 +39983,7 @@ exports[`ConfigProvider components Transfer normal 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -40109,7 +40095,7 @@ exports[`ConfigProvider components Transfer prefixCls 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -40268,7 +40254,7 @@ exports[`ConfigProvider components Transfer prefixCls 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -274,19 +274,7 @@ describe('ConfigProvider', () => {
|
||||
testPair('Drawer', props => <Drawer {...props} open getContainer={false} />);
|
||||
|
||||
// Dropdown
|
||||
testPair('Dropdown', props => {
|
||||
const menu = (
|
||||
<Menu {...props}>
|
||||
<Menu.Item {...props}>Bamboo</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown.Button {...props} overlay={menu}>
|
||||
Light
|
||||
</Dropdown.Button>
|
||||
);
|
||||
});
|
||||
testPair('Dropdown', props => <Dropdown.Button {...props}>Light</Dropdown.Button>);
|
||||
|
||||
// Form
|
||||
testPair('Form', props => (
|
||||
|
@ -312,21 +312,19 @@ const App: React.FC = () => {
|
||||
|
||||
{/* Dropdown */}
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: '1st menu item',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: 'a danger item',
|
||||
danger: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
}
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: '1',
|
||||
label: '1st menu item',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: 'a danger item',
|
||||
danger: true,
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<a onClick={e => e.preventDefault()}>
|
||||
<Space>
|
||||
|
@ -58,4 +58,11 @@ describe('DatePicker.typescript', () => {
|
||||
);
|
||||
expect(datePicker).toBeTruthy();
|
||||
});
|
||||
|
||||
it('DatePicker and RangePicker supports popupClassName', () => {
|
||||
const datePicker = <DatePicker popupClassName="popupClassName" />;
|
||||
expect(datePicker).toBeTruthy();
|
||||
const rangePicker = <DatePicker.RangePicker popupClassName="popupClassName" />;
|
||||
expect(rangePicker).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
@ -27,14 +27,18 @@ export default function generateRangePicker<DateType>(
|
||||
generateConfig: GenerateConfig<DateType>,
|
||||
): PickerComponentClass<RangePickerProps<DateType>> {
|
||||
type InternalRangePickerProps = RangePickerProps<DateType> & {};
|
||||
type DateRangePickerProps = RangePickerProps<DateType> & {
|
||||
/**
|
||||
* @deprecated `dropdownClassName` is deprecated which will be removed in next major
|
||||
* version.Please use `popupClassName` instead.
|
||||
*/
|
||||
dropdownClassName?: string;
|
||||
popupClassName?: string;
|
||||
};
|
||||
|
||||
const RangePicker = forwardRef<
|
||||
InternalRangePickerProps | CommonPickerMethods,
|
||||
RangePickerProps<DateType> & {
|
||||
popupClassName?: string;
|
||||
/** @deprecated Please use `popupClassName` instead */
|
||||
dropdownClassName?: string;
|
||||
}
|
||||
DateRangePickerProps
|
||||
>((props, ref) => {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
@ -154,5 +158,5 @@ export default function generateRangePicker<DateType>(
|
||||
);
|
||||
});
|
||||
|
||||
return RangePicker as unknown as PickerComponentClass<RangePickerProps<DateType>>;
|
||||
return RangePicker as unknown as PickerComponentClass<DateRangePickerProps>;
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import type { PickerMode } from 'rc-picker/lib/interface';
|
||||
import * as React from 'react';
|
||||
import { forwardRef, useContext, useImperativeHandle } from 'react';
|
||||
import { useCompactItemContext } from '../../space/Compact';
|
||||
import type { PickerDateProps, PickerProps, PickerTimeProps } from '.';
|
||||
import type { PickerProps, PickerTimeProps } from '.';
|
||||
import { Components, getTimeProps } from '.';
|
||||
import { ConfigContext } from '../../config-provider';
|
||||
import DisabledContext from '../../config-provider/DisabledContext';
|
||||
@ -25,13 +25,14 @@ import type { CommonPickerMethods, DatePickRef, PickerComponentClass } from './i
|
||||
import useStyle from '../style';
|
||||
|
||||
export default function generatePicker<DateType>(generateConfig: GenerateConfig<DateType>) {
|
||||
type DatePickerProps = PickerProps<DateType> & {
|
||||
type CustomPickerProps = {
|
||||
status?: InputStatus;
|
||||
hashId?: string;
|
||||
popupClassName?: string;
|
||||
/** @deprecated Please use `popupClassName` instead */
|
||||
dropdownClassName?: string;
|
||||
};
|
||||
type DatePickerProps = PickerProps<DateType> & CustomPickerProps;
|
||||
type TimePickerProps = PickerTimeProps<DateType> & CustomPickerProps;
|
||||
|
||||
function getPicker<InnerPickerProps extends DatePickerProps>(
|
||||
picker?: PickerMode,
|
||||
displayName?: string,
|
||||
@ -178,14 +179,11 @@ export default function generatePicker<DateType>(generateConfig: GenerateConfig<
|
||||
}
|
||||
|
||||
const DatePicker = getPicker<DatePickerProps>();
|
||||
const WeekPicker = getPicker<Omit<PickerDateProps<DateType>, 'picker'>>('week', 'WeekPicker');
|
||||
const MonthPicker = getPicker<Omit<PickerDateProps<DateType>, 'picker'>>('month', 'MonthPicker');
|
||||
const YearPicker = getPicker<Omit<PickerDateProps<DateType>, 'picker'>>('year', 'YearPicker');
|
||||
const TimePicker = getPicker<Omit<PickerTimeProps<DateType>, 'picker'>>('time', 'TimePicker');
|
||||
const QuarterPicker = getPicker<Omit<PickerTimeProps<DateType>, 'picker'>>(
|
||||
'quarter',
|
||||
'QuarterPicker',
|
||||
);
|
||||
const WeekPicker = getPicker<Omit<DatePickerProps, 'picker'>>('week', 'WeekPicker');
|
||||
const MonthPicker = getPicker<Omit<DatePickerProps, 'picker'>>('month', 'MonthPicker');
|
||||
const YearPicker = getPicker<Omit<DatePickerProps, 'picker'>>('year', 'YearPicker');
|
||||
const TimePicker = getPicker<Omit<TimePickerProps, 'picker'>>('time', 'TimePicker');
|
||||
const QuarterPicker = getPicker<Omit<TimePickerProps, 'picker'>>('quarter', 'QuarterPicker');
|
||||
|
||||
return { DatePicker, WeekPicker, MonthPicker, YearPicker, TimePicker, QuarterPicker };
|
||||
}
|
||||
|
@ -153,7 +153,7 @@ const genDrawerStyle: GenerateStyle<DrawerToken> = (token: DrawerToken) => {
|
||||
},
|
||||
|
||||
[`${componentCls}-extra`]: {
|
||||
flex: 0,
|
||||
flex: 'none',
|
||||
},
|
||||
|
||||
[`${componentCls}-close`]: {
|
||||
|
@ -3081,6 +3081,594 @@ Array [
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`renders ./components/dropdown/demo/custom-dropdown.md extend context correctly 1`] = `
|
||||
Array [
|
||||
<a
|
||||
class="ant-dropdown-trigger"
|
||||
>
|
||||
<div
|
||||
class="ant-space ant-space-horizontal ant-space-align-center"
|
||||
>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
style="margin-right:8px"
|
||||
>
|
||||
Hover me
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<span
|
||||
aria-label="down"
|
||||
class="anticon anticon-down"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="down"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>,
|
||||
<div>
|
||||
<div
|
||||
class="ant-dropdown"
|
||||
style="opacity:0"
|
||||
>
|
||||
<div
|
||||
class="dropdown-content"
|
||||
>
|
||||
<ul
|
||||
class="ant-dropdown-menu ant-dropdown-menu-root ant-dropdown-menu-vertical ant-dropdown-menu-light"
|
||||
data-menu-list="true"
|
||||
role="menu"
|
||||
tabindex="0"
|
||||
>
|
||||
<li
|
||||
class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
class="ant-dropdown-menu-title-content"
|
||||
>
|
||||
<a
|
||||
href="https://www.antgroup.com"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
1st menu item
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
<div>
|
||||
<div
|
||||
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
|
||||
style="opacity:0"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-content"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-arrow"
|
||||
>
|
||||
<span
|
||||
class="ant-tooltip-arrow-content"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ant-tooltip-inner"
|
||||
role="tooltip"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
class="ant-dropdown-menu-item ant-dropdown-menu-item-disabled ant-dropdown-menu-item-only-child"
|
||||
role="menuitem"
|
||||
>
|
||||
<span
|
||||
class="ant-dropdown-menu-title-content"
|
||||
>
|
||||
<a
|
||||
href="https://www.aliyun.com"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
2nd menu item (disabled)
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
<div>
|
||||
<div
|
||||
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
|
||||
style="opacity:0"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-content"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-arrow"
|
||||
>
|
||||
<span
|
||||
class="ant-tooltip-arrow-content"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ant-tooltip-inner"
|
||||
role="tooltip"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
class="ant-dropdown-menu-item ant-dropdown-menu-item-disabled ant-dropdown-menu-item-only-child"
|
||||
role="menuitem"
|
||||
>
|
||||
<span
|
||||
class="ant-dropdown-menu-title-content"
|
||||
>
|
||||
<a
|
||||
href="https://www.luohanacademy.com"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
3rd menu item (disabled)
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
<div>
|
||||
<div
|
||||
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
|
||||
style="opacity:0"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-content"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-arrow"
|
||||
>
|
||||
<span
|
||||
class="ant-tooltip-arrow-content"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ant-tooltip-inner"
|
||||
role="tooltip"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ul>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style="display:none"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
|
||||
style="opacity:0"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-content"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-arrow"
|
||||
>
|
||||
<span
|
||||
class="ant-tooltip-arrow-content"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ant-tooltip-inner"
|
||||
role="tooltip"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
|
||||
style="opacity:0"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-content"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-arrow"
|
||||
>
|
||||
<span
|
||||
class="ant-tooltip-arrow-content"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ant-tooltip-inner"
|
||||
role="tooltip"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
|
||||
style="opacity:0"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-content"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-arrow"
|
||||
>
|
||||
<span
|
||||
class="ant-tooltip-arrow-content"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ant-tooltip-inner"
|
||||
role="tooltip"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-divider ant-divider-horizontal"
|
||||
role="separator"
|
||||
style="margin:0"
|
||||
/>
|
||||
<div
|
||||
class="ant-space ant-space-horizontal ant-space-align-center"
|
||||
style="padding:8px"
|
||||
>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<button
|
||||
class="ant-btn ant-btn-primary"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Click me!
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`renders ./components/dropdown/demo/deprecated.md extend context correctly 1`] = `
|
||||
Array [
|
||||
<a
|
||||
class="ant-dropdown-trigger"
|
||||
>
|
||||
<div
|
||||
class="ant-space ant-space-horizontal ant-space-align-center"
|
||||
>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
style="margin-right:8px"
|
||||
>
|
||||
Hover me
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<span
|
||||
aria-label="down"
|
||||
class="anticon anticon-down"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="down"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>,
|
||||
<div>
|
||||
<div
|
||||
class="ant-dropdown"
|
||||
style="opacity:0"
|
||||
>
|
||||
<ul
|
||||
class="ant-dropdown-menu ant-dropdown-menu-root ant-dropdown-menu-vertical ant-dropdown-menu-light"
|
||||
data-menu-list="true"
|
||||
role="menu"
|
||||
tabindex="0"
|
||||
>
|
||||
<li
|
||||
class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
class="ant-dropdown-menu-title-content"
|
||||
>
|
||||
<a
|
||||
href="https://www.antgroup.com"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
1st menu item
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
<div>
|
||||
<div
|
||||
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
|
||||
style="opacity:0"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-content"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-arrow"
|
||||
>
|
||||
<span
|
||||
class="ant-tooltip-arrow-content"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ant-tooltip-inner"
|
||||
role="tooltip"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
class="ant-dropdown-menu-item ant-dropdown-menu-item-disabled"
|
||||
role="menuitem"
|
||||
>
|
||||
<span
|
||||
aria-label="smile"
|
||||
class="anticon anticon-smile ant-dropdown-menu-item-icon"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="smile"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M288 421a48 48 0 1096 0 48 48 0 10-96 0zm352 0a48 48 0 1096 0 48 48 0 10-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 01248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 01249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 01775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 01775 775zM664 533h-48.1c-4.2 0-7.8 3.2-8.1 7.4C604 589.9 562.5 629 512 629s-92.1-39.1-95.8-88.6c-.3-4.2-3.9-7.4-8.1-7.4H360a8 8 0 00-8 8.4c4.4 84.3 74.5 151.6 160 151.6s155.6-67.3 160-151.6a8 8 0 00-8-8.4z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="ant-dropdown-menu-title-content"
|
||||
>
|
||||
<a
|
||||
href="https://www.aliyun.com"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
2nd menu item (disabled)
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
<div>
|
||||
<div
|
||||
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
|
||||
style="opacity:0"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-content"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-arrow"
|
||||
>
|
||||
<span
|
||||
class="ant-tooltip-arrow-content"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ant-tooltip-inner"
|
||||
role="tooltip"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
class="ant-dropdown-menu-item ant-dropdown-menu-item-disabled ant-dropdown-menu-item-only-child"
|
||||
role="menuitem"
|
||||
>
|
||||
<span
|
||||
class="ant-dropdown-menu-title-content"
|
||||
>
|
||||
<a
|
||||
href="https://www.luohanacademy.com"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
3rd menu item (disabled)
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
<div>
|
||||
<div
|
||||
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
|
||||
style="opacity:0"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-content"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-arrow"
|
||||
>
|
||||
<span
|
||||
class="ant-tooltip-arrow-content"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ant-tooltip-inner"
|
||||
role="tooltip"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<li
|
||||
class="ant-dropdown-menu-item ant-dropdown-menu-item-danger ant-dropdown-menu-item-only-child"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
class="ant-dropdown-menu-title-content"
|
||||
>
|
||||
a danger item
|
||||
</span>
|
||||
</li>
|
||||
<div>
|
||||
<div
|
||||
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
|
||||
style="opacity:0"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-content"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-arrow"
|
||||
>
|
||||
<span
|
||||
class="ant-tooltip-arrow-content"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ant-tooltip-inner"
|
||||
role="tooltip"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ul>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style="display:none"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
|
||||
style="opacity:0"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-content"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-arrow"
|
||||
>
|
||||
<span
|
||||
class="ant-tooltip-arrow-content"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ant-tooltip-inner"
|
||||
role="tooltip"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
|
||||
style="opacity:0"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-content"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-arrow"
|
||||
>
|
||||
<span
|
||||
class="ant-tooltip-arrow-content"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ant-tooltip-inner"
|
||||
role="tooltip"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
|
||||
style="opacity:0"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-content"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-arrow"
|
||||
>
|
||||
<span
|
||||
class="ant-tooltip-arrow-content"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ant-tooltip-inner"
|
||||
role="tooltip"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="ant-tooltip ant-dropdown-menu-inline-collapsed-tooltip"
|
||||
style="opacity:0"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-content"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-arrow"
|
||||
>
|
||||
<span
|
||||
class="ant-tooltip-arrow-content"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ant-tooltip-inner"
|
||||
role="tooltip"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`renders ./components/dropdown/demo/dropdown-button.md extend context correctly 1`] = `
|
||||
<div
|
||||
class="ant-space ant-space-horizontal ant-space-align-center"
|
||||
|
@ -157,6 +157,86 @@ exports[`renders ./components/dropdown/demo/context-menu.md correctly 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/dropdown/demo/custom-dropdown.md correctly 1`] = `
|
||||
<a
|
||||
class="ant-dropdown-trigger"
|
||||
>
|
||||
<div
|
||||
class="ant-space ant-space-horizontal ant-space-align-center"
|
||||
>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
style="margin-right:8px"
|
||||
>
|
||||
Hover me
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<span
|
||||
aria-label="down"
|
||||
class="anticon anticon-down"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="down"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/dropdown/demo/deprecated.md correctly 1`] = `
|
||||
<a
|
||||
class="ant-dropdown-trigger"
|
||||
>
|
||||
<div
|
||||
class="ant-space ant-space-horizontal ant-space-align-center"
|
||||
>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
style="margin-right:8px"
|
||||
>
|
||||
Hover me
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<span
|
||||
aria-label="down"
|
||||
class="anticon anticon-down"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="down"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/dropdown/demo/dropdown-button.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-space ant-space-horizontal ant-space-align-center"
|
||||
|
@ -47,3 +47,51 @@ exports[`Dropdown rtl render component should be rendered correctly in RTL direc
|
||||
class="ant-dropdown-trigger ant-dropdown-rtl"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`Dropdown should render custom dropdown correctly 1`] = `
|
||||
Array [
|
||||
<button
|
||||
class="ant-dropdown-trigger ant-dropdown-open"
|
||||
type="button"
|
||||
>
|
||||
button
|
||||
</button>,
|
||||
<div>
|
||||
<div
|
||||
class="ant-dropdown ant-slide-up-appear ant-slide-up-appear-prepare ant-slide-up"
|
||||
style="opacity: 0;"
|
||||
>
|
||||
<div>
|
||||
<ul
|
||||
class="ant-dropdown-menu ant-dropdown-menu-root ant-dropdown-menu-vertical ant-dropdown-menu-light"
|
||||
data-menu-list="true"
|
||||
role="menu"
|
||||
tabindex="0"
|
||||
>
|
||||
<li
|
||||
class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child"
|
||||
data-menu-id="rc-menu-uuid-test-1"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
class="ant-dropdown-menu-title-content"
|
||||
>
|
||||
foo
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style="display: none;"
|
||||
/>
|
||||
<div
|
||||
class="dropdown-custom-node"
|
||||
>
|
||||
CUSTOM NODE
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
@ -2,7 +2,6 @@ import React from 'react';
|
||||
import DropdownButton from '../dropdown-button';
|
||||
import mountTest from '../../../tests/shared/mountTest';
|
||||
import rtlTest from '../../../tests/shared/rtlTest';
|
||||
import Menu from '../../menu';
|
||||
import type { DropdownProps } from '../dropdown';
|
||||
import { render } from '../../../tests/utils';
|
||||
|
||||
@ -34,15 +33,18 @@ describe('DropdownButton', () => {
|
||||
rtlTest(DropdownButton);
|
||||
|
||||
it('pass appropriate props to Dropdown', () => {
|
||||
const items = [
|
||||
{
|
||||
label: 'foo',
|
||||
key: '1',
|
||||
},
|
||||
];
|
||||
|
||||
const props: DropdownProps = {
|
||||
align: {
|
||||
offset: [10, 20],
|
||||
},
|
||||
overlay: (
|
||||
<Menu>
|
||||
<Menu.Item key="1">foo</Menu.Item>
|
||||
</Menu>
|
||||
),
|
||||
menu: { items },
|
||||
disabled: false,
|
||||
trigger: ['hover'],
|
||||
open: true,
|
||||
@ -55,27 +57,29 @@ describe('DropdownButton', () => {
|
||||
expect(dropdownProps[key]).toBe(props[key]);
|
||||
});
|
||||
|
||||
rerender(<DropdownButton overlay={<div>123</div>} open />);
|
||||
rerender(<DropdownButton menu={{ items }} open />);
|
||||
expect(dropdownProps.open).toBe(true);
|
||||
});
|
||||
|
||||
it("don't pass open to Dropdown if it's not exits", () => {
|
||||
const menu = (
|
||||
<Menu>
|
||||
<Menu.Item key="1">foo</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
render(<DropdownButton overlay={menu} />);
|
||||
const items = [
|
||||
{
|
||||
label: 'foo',
|
||||
key: '1',
|
||||
},
|
||||
];
|
||||
render(<DropdownButton menu={{ items }} />);
|
||||
expect('open' in dropdownProps).toBe(false);
|
||||
});
|
||||
|
||||
it('should support href like Button', () => {
|
||||
const menu = (
|
||||
<Menu>
|
||||
<Menu.Item key="1">foo</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
const { asFragment } = render(<DropdownButton overlay={menu} href="https://ant.design" />);
|
||||
const items = [
|
||||
{
|
||||
label: 'foo',
|
||||
key: '1',
|
||||
},
|
||||
];
|
||||
const { asFragment } = render(<DropdownButton menu={{ items }} href="https://ant.design" />);
|
||||
expect(asFragment().firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@ -84,27 +88,29 @@ describe('DropdownButton', () => {
|
||||
});
|
||||
|
||||
it('should pass mouseEnterDelay and mouseLeaveDelay to Dropdown', () => {
|
||||
const menu = (
|
||||
<Menu>
|
||||
<Menu.Item key="1">foo</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
render(<DropdownButton mouseEnterDelay={1} mouseLeaveDelay={2} overlay={menu} />);
|
||||
const items = [
|
||||
{
|
||||
label: 'foo',
|
||||
key: '1',
|
||||
},
|
||||
];
|
||||
render(<DropdownButton mouseEnterDelay={1} mouseLeaveDelay={2} menu={{ items }} />);
|
||||
expect(dropdownProps.mouseEnterDelay).toBe(1);
|
||||
expect(dropdownProps.mouseLeaveDelay).toBe(2);
|
||||
});
|
||||
|
||||
it('should support overlayClassName and overlayStyle', () => {
|
||||
const menu = (
|
||||
<Menu>
|
||||
<Menu.Item key="1">foo</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
const items = [
|
||||
{
|
||||
label: 'foo',
|
||||
key: '1',
|
||||
},
|
||||
];
|
||||
const { container } = render(
|
||||
<DropdownButton
|
||||
overlayClassName="className"
|
||||
overlayStyle={{ color: 'red' }}
|
||||
overlay={menu}
|
||||
menu={{ items }}
|
||||
open
|
||||
/>,
|
||||
);
|
||||
@ -113,7 +119,13 @@ describe('DropdownButton', () => {
|
||||
});
|
||||
|
||||
it('should support loading', () => {
|
||||
const { container } = render(<DropdownButton overlay={<div />} loading />);
|
||||
const items = [
|
||||
{
|
||||
label: 'foo',
|
||||
key: '1',
|
||||
},
|
||||
];
|
||||
const { container } = render(<DropdownButton menu={{ items }} loading />);
|
||||
|
||||
expect(container.querySelector('.ant-dropdown-button .ant-btn-loading')?.classList).toContain(
|
||||
'ant-btn',
|
||||
|
@ -25,14 +25,21 @@ jest.mock('rc-trigger', () => {
|
||||
});
|
||||
|
||||
describe('Dropdown', () => {
|
||||
const items = [
|
||||
{
|
||||
label: 'foo',
|
||||
key: '1',
|
||||
},
|
||||
];
|
||||
|
||||
mountTest(() => (
|
||||
<Dropdown overlay={<Menu />}>
|
||||
<Dropdown menu={{ items }}>
|
||||
<span />
|
||||
</Dropdown>
|
||||
));
|
||||
|
||||
rtlTest(() => (
|
||||
<Dropdown overlay={<Menu />}>
|
||||
<Dropdown menu={{ items }}>
|
||||
<span />
|
||||
</Dropdown>
|
||||
));
|
||||
@ -55,17 +62,46 @@ describe('Dropdown', () => {
|
||||
expect(Array.from(asFragment().childNodes)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render custom dropdown correctly', () => {
|
||||
const { asFragment } = render(
|
||||
<Dropdown
|
||||
open
|
||||
menu={{ items }}
|
||||
dropdownRender={menu => (
|
||||
<div>
|
||||
{menu}
|
||||
<div className="dropdown-custom-node">CUSTOM NODE</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<button type="button">button</button>
|
||||
</Dropdown>,
|
||||
);
|
||||
expect(Array.from(asFragment().childNodes)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('support Menu expandIcon', async () => {
|
||||
jest.useFakeTimers();
|
||||
const props: DropDownProps = {
|
||||
overlay: (
|
||||
<Menu expandIcon={<span id="customExpandIcon" />}>
|
||||
<Menu.Item key="1">foo</Menu.Item>
|
||||
<Menu.SubMenu title="SubMenu">
|
||||
<Menu.Item key="1">foo</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
</Menu>
|
||||
),
|
||||
menu: {
|
||||
items: [
|
||||
{
|
||||
label: 'foo',
|
||||
key: '1',
|
||||
},
|
||||
{
|
||||
label: 'SubMenu',
|
||||
key: 'submenu',
|
||||
children: [
|
||||
{
|
||||
label: 'foo',
|
||||
key: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
expandIcon: <span id="customExpandIcon" />,
|
||||
},
|
||||
open: true,
|
||||
getPopupContainer: node => node,
|
||||
};
|
||||
@ -84,10 +120,10 @@ describe('Dropdown', () => {
|
||||
const error = jest.spyOn(console, 'error');
|
||||
render(
|
||||
<div>
|
||||
<Dropdown overlay={'123' as any} placement="bottomCenter">
|
||||
<Dropdown menu={{ items }} placement="bottomCenter">
|
||||
<button type="button">bottomCenter</button>
|
||||
</Dropdown>
|
||||
<Dropdown overlay={'123' as any} placement="topCenter">
|
||||
<Dropdown menu={{ items }} placement="topCenter">
|
||||
<button type="button">topCenter</button>
|
||||
</Dropdown>
|
||||
</div>,
|
||||
@ -104,7 +140,7 @@ describe('Dropdown', () => {
|
||||
// zombieJ: when replaced with react test lib, it may be mock fully content
|
||||
it('dropdown should support auto adjust placement', () => {
|
||||
render(
|
||||
<Dropdown overlay={<div>menu</div>} open>
|
||||
<Dropdown menu={{ items }} open>
|
||||
<button type="button">button</button>
|
||||
</Dropdown>,
|
||||
);
|
||||
@ -126,22 +162,20 @@ describe('Dropdown', () => {
|
||||
const { container } = render(
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
overlay={
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
label: 'grp',
|
||||
type: 'group',
|
||||
children: [
|
||||
{
|
||||
label: '1',
|
||||
key: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
}
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
label: 'grp',
|
||||
type: 'group',
|
||||
children: [
|
||||
{
|
||||
label: '1',
|
||||
key: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<a />
|
||||
</Dropdown>,
|
||||
@ -177,13 +211,20 @@ describe('Dropdown', () => {
|
||||
const onOpenChange = jest.fn();
|
||||
const onVisibleChange = jest.fn();
|
||||
|
||||
const { container } = render(
|
||||
const { container, rerender } = render(
|
||||
<Dropdown
|
||||
visible
|
||||
onOpenChange={onOpenChange}
|
||||
onVisibleChange={onVisibleChange}
|
||||
trigger={['click']}
|
||||
overlay={<div className="bamboo" />}
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
label: <div className="bamboo" />,
|
||||
key: 'bamboo',
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<a className="little" />
|
||||
</Dropdown>,
|
||||
@ -201,6 +242,15 @@ describe('Dropdown', () => {
|
||||
expect(onOpenChange).toHaveBeenCalled();
|
||||
expect(onVisibleChange).toHaveBeenCalled();
|
||||
|
||||
rerender(
|
||||
<Dropdown overlay={<div>menu</div>}>
|
||||
<a className="little" />
|
||||
</Dropdown>,
|
||||
);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
'Warning: [antd: Dropdown] `overlay` is deprecated. Please use `menu` instead.',
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
order: 3
|
||||
order: 4
|
||||
title:
|
||||
zh-CN: 箭头指向
|
||||
en-US: Arrow pointing at the center
|
||||
@ -14,59 +14,56 @@ title:
|
||||
By specifying `arrow` prop with `{ pointAtCenter: true }`, the arrow will point to the center of the target element.
|
||||
|
||||
```tsx
|
||||
import { Button, Dropdown, Menu } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Button, Dropdown } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const menu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.antgroup.com">
|
||||
1st menu item
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.aliyun.com">
|
||||
2nd menu item
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.luohanacademy.com">
|
||||
3rd menu item
|
||||
</a>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.antgroup.com">
|
||||
1st menu item
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.aliyun.com">
|
||||
2nd menu item
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.luohanacademy.com">
|
||||
3rd menu item
|
||||
</a>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const App: React.FC = () => (
|
||||
<>
|
||||
<Dropdown overlay={menu} placement="bottomLeft" arrow={{ pointAtCenter: true }}>
|
||||
<Dropdown menu={{ items }} placement="bottomLeft" arrow={{ pointAtCenter: true }}>
|
||||
<Button>bottomLeft</Button>
|
||||
</Dropdown>
|
||||
<Dropdown overlay={menu} placement="bottom" arrow={{ pointAtCenter: true }}>
|
||||
<Dropdown menu={{ items }} placement="bottom" arrow={{ pointAtCenter: true }}>
|
||||
<Button>bottom</Button>
|
||||
</Dropdown>
|
||||
<Dropdown overlay={menu} placement="bottomRight" arrow={{ pointAtCenter: true }}>
|
||||
<Dropdown menu={{ items }} placement="bottomRight" arrow={{ pointAtCenter: true }}>
|
||||
<Button>bottomRight</Button>
|
||||
</Dropdown>
|
||||
<br />
|
||||
<Dropdown overlay={menu} placement="topLeft" arrow={{ pointAtCenter: true }}>
|
||||
<Dropdown menu={{ items }} placement="topLeft" arrow={{ pointAtCenter: true }}>
|
||||
<Button>topLeft</Button>
|
||||
</Dropdown>
|
||||
<Dropdown overlay={menu} placement="top" arrow={{ pointAtCenter: true }}>
|
||||
<Dropdown menu={{ items }} placement="top" arrow={{ pointAtCenter: true }}>
|
||||
<Button>top</Button>
|
||||
</Dropdown>
|
||||
<Dropdown overlay={menu} placement="topRight" arrow={{ pointAtCenter: true }}>
|
||||
<Dropdown menu={{ items }} placement="topRight" arrow={{ pointAtCenter: true }}>
|
||||
<Button>topRight</Button>
|
||||
</Dropdown>
|
||||
</>
|
||||
|
@ -14,59 +14,56 @@ title:
|
||||
You could display an arrow.
|
||||
|
||||
```tsx
|
||||
import { Button, Dropdown, Menu } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Button, Dropdown } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const menu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.antgroup.com">
|
||||
1st menu item
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.aliyun.com">
|
||||
2nd menu item
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.luohanacademy.com">
|
||||
3rd menu item
|
||||
</a>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.antgroup.com">
|
||||
1st menu item
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.aliyun.com">
|
||||
2nd menu item
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.luohanacademy.com">
|
||||
3rd menu item
|
||||
</a>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const App: React.FC = () => (
|
||||
<>
|
||||
<Dropdown overlay={menu} placement="bottomLeft" arrow>
|
||||
<Dropdown menu={{ items }} placement="bottomLeft" arrow>
|
||||
<Button>bottomLeft</Button>
|
||||
</Dropdown>
|
||||
<Dropdown overlay={menu} placement="bottom" arrow>
|
||||
<Dropdown menu={{ items }} placement="bottom" arrow>
|
||||
<Button>bottom</Button>
|
||||
</Dropdown>
|
||||
<Dropdown overlay={menu} placement="bottomRight" arrow>
|
||||
<Dropdown menu={{ items }} placement="bottomRight" arrow>
|
||||
<Button>bottomRight</Button>
|
||||
</Dropdown>
|
||||
<br />
|
||||
<Dropdown overlay={menu} placement="topLeft" arrow>
|
||||
<Dropdown menu={{ items }} placement="topLeft" arrow>
|
||||
<Button>topLeft</Button>
|
||||
</Dropdown>
|
||||
<Dropdown overlay={menu} placement="top" arrow>
|
||||
<Dropdown menu={{ items }} placement="top" arrow>
|
||||
<Button>top</Button>
|
||||
</Dropdown>
|
||||
<Dropdown overlay={menu} placement="topRight" arrow>
|
||||
<Dropdown menu={{ items }} placement="topRight" arrow>
|
||||
<Button>topRight</Button>
|
||||
</Dropdown>
|
||||
</>
|
||||
|
@ -15,50 +15,47 @@ The most basic dropdown menu.
|
||||
|
||||
```tsx
|
||||
import { DownOutlined, SmileOutlined } from '@ant-design/icons';
|
||||
import { Dropdown, Menu, Space } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Dropdown, Space } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const menu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.antgroup.com">
|
||||
1st menu item
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.aliyun.com">
|
||||
2nd menu item (disabled)
|
||||
</a>
|
||||
),
|
||||
icon: <SmileOutlined />,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.luohanacademy.com">
|
||||
3rd menu item (disabled)
|
||||
</a>
|
||||
),
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
danger: true,
|
||||
label: 'a danger item',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.antgroup.com">
|
||||
1st menu item
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.aliyun.com">
|
||||
2nd menu item (disabled)
|
||||
</a>
|
||||
),
|
||||
icon: <SmileOutlined />,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.luohanacademy.com">
|
||||
3rd menu item (disabled)
|
||||
</a>
|
||||
),
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
danger: true,
|
||||
label: 'a danger item',
|
||||
},
|
||||
];
|
||||
|
||||
const App: React.FC = () => (
|
||||
<Dropdown overlay={menu}>
|
||||
<Dropdown menu={{ items }}>
|
||||
<a onClick={e => e.preventDefault()}>
|
||||
<Space>
|
||||
Hover me
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
order: 8
|
||||
order: 10
|
||||
title:
|
||||
zh-CN: 右键菜单
|
||||
en-US: Context Menu
|
||||
@ -14,30 +14,27 @@ title:
|
||||
The default trigger mode is `hover`, you can change it to `contextMenu`.
|
||||
|
||||
```tsx
|
||||
import { Dropdown, Menu } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Dropdown } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const menu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
label: '1st menu item',
|
||||
key: '1',
|
||||
},
|
||||
{
|
||||
label: '2nd menu item',
|
||||
key: '2',
|
||||
},
|
||||
{
|
||||
label: '3rd menu item',
|
||||
key: '3',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
label: '1st menu item',
|
||||
key: '1',
|
||||
},
|
||||
{
|
||||
label: '2nd menu item',
|
||||
key: '2',
|
||||
},
|
||||
{
|
||||
label: '3rd menu item',
|
||||
key: '3',
|
||||
},
|
||||
];
|
||||
|
||||
const App: React.FC = () => (
|
||||
<Dropdown overlay={menu} trigger={['contextMenu']}>
|
||||
<Dropdown menu={{ items }} trigger={['contextMenu']}>
|
||||
<div
|
||||
className="site-dropdown-context-menu"
|
||||
style={{
|
||||
|
91
components/dropdown/demo/custom-dropdown.md
Normal file
91
components/dropdown/demo/custom-dropdown.md
Normal file
@ -0,0 +1,91 @@
|
||||
---
|
||||
order: 8
|
||||
title:
|
||||
zh-CN: 扩展菜单
|
||||
en-US: Custom dropdown
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
使用 `dropdownRender` 对下拉菜单进行自由扩展。如果你并不需要 Menu 内容,请直接使用 Popover 组件。
|
||||
|
||||
## en-US
|
||||
|
||||
Customize the dropdown menu via `dropdownRender`. If you don't need the Menu content, use the Popover component directly.
|
||||
|
||||
```tsx
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Dropdown, Space, Divider, Button } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.antgroup.com">
|
||||
1st menu item
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.aliyun.com">
|
||||
2nd menu item (disabled)
|
||||
</a>
|
||||
),
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.luohanacademy.com">
|
||||
3rd menu item (disabled)
|
||||
</a>
|
||||
),
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
const App: React.FC = () => (
|
||||
<Dropdown
|
||||
menu={{ items }}
|
||||
dropdownRender={menu => (
|
||||
<div className="dropdown-content">
|
||||
{menu}
|
||||
<Divider style={{ margin: 0 }} />
|
||||
<Space style={{ padding: 8 }}>
|
||||
<Button type="primary">Click me!</Button>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<a onClick={e => e.preventDefault()}>
|
||||
<Space>
|
||||
Hover me
|
||||
<DownOutlined />
|
||||
</Space>
|
||||
</a>
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
```css
|
||||
.dropdown-content {
|
||||
background: #fff;
|
||||
box-shadow: 0 3px 6px -4px rgb(0 0 0 / 12%), 0 6px 16px 0 rgb(0 0 0 / 8%),
|
||||
0 9px 28px 8px rgb(0 0 0 / 5%);
|
||||
}
|
||||
.dropdown-content .ant-dropdown-menu {
|
||||
box-shadow: none;
|
||||
}
|
||||
```
|
||||
|
||||
<style>
|
||||
[data-theme="dark"] .head-example {
|
||||
background: #1f1f1f;
|
||||
}
|
||||
</style>
|
73
components/dropdown/demo/deprecated.md
Normal file
73
components/dropdown/demo/deprecated.md
Normal file
@ -0,0 +1,73 @@
|
||||
---
|
||||
order: -1
|
||||
title:
|
||||
zh-CN: 基础用法(废弃的语法糖)
|
||||
en-US: Basic usage (deprecated syntactic sugar)
|
||||
version: < 4.24.0
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
最简单的下拉菜单。
|
||||
|
||||
## en-US
|
||||
|
||||
The most basic dropdown menu.
|
||||
|
||||
```tsx
|
||||
import { DownOutlined, SmileOutlined } from '@ant-design/icons';
|
||||
import { Dropdown, Menu, Space } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const menu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.antgroup.com">
|
||||
1st menu item
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.aliyun.com">
|
||||
2nd menu item (disabled)
|
||||
</a>
|
||||
),
|
||||
icon: <SmileOutlined />,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.luohanacademy.com">
|
||||
3rd menu item (disabled)
|
||||
</a>
|
||||
),
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
danger: true,
|
||||
label: 'a danger item',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const App: React.FC = () => (
|
||||
<Dropdown overlay={menu}>
|
||||
<a onClick={e => e.preventDefault()}>
|
||||
<Space>
|
||||
Hover me
|
||||
<DownOutlined />
|
||||
</Space>
|
||||
</a>
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
export default App;
|
||||
```
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
order: 5
|
||||
order: 7
|
||||
title:
|
||||
zh-CN: 带下拉框的按钮
|
||||
en-US: Button with dropdown menu
|
||||
@ -16,7 +16,7 @@ A button is on the left, and a related functional menu is on the right. You can
|
||||
```tsx
|
||||
import { DownOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Button, Dropdown, Menu, message, Space, Tooltip } from 'antd';
|
||||
import { Button, Dropdown, message, Space, Tooltip } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const handleButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
@ -29,42 +29,42 @@ const handleMenuClick: MenuProps['onClick'] = e => {
|
||||
console.log('click', e);
|
||||
};
|
||||
|
||||
const menu = (
|
||||
<Menu
|
||||
onClick={handleMenuClick}
|
||||
items={[
|
||||
{
|
||||
label: '1st menu item',
|
||||
key: '1',
|
||||
icon: <UserOutlined />,
|
||||
},
|
||||
{
|
||||
label: '2nd menu item',
|
||||
key: '2',
|
||||
icon: <UserOutlined />,
|
||||
},
|
||||
{
|
||||
label: '3rd menu item',
|
||||
key: '3',
|
||||
icon: <UserOutlined />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
label: '1st menu item',
|
||||
key: '1',
|
||||
icon: <UserOutlined />,
|
||||
},
|
||||
{
|
||||
label: '2nd menu item',
|
||||
key: '2',
|
||||
icon: <UserOutlined />,
|
||||
},
|
||||
{
|
||||
label: '3rd menu item',
|
||||
key: '3',
|
||||
icon: <UserOutlined />,
|
||||
},
|
||||
];
|
||||
|
||||
const menuProps = {
|
||||
items,
|
||||
onClick: handleMenuClick,
|
||||
};
|
||||
|
||||
const App: React.FC = () => (
|
||||
<Space wrap>
|
||||
<Dropdown.Button onClick={handleButtonClick} overlay={menu}>
|
||||
<Dropdown.Button menu={menuProps} onClick={handleButtonClick}>
|
||||
Dropdown
|
||||
</Dropdown.Button>
|
||||
<Dropdown.Button overlay={menu} placement="bottom" icon={<UserOutlined />}>
|
||||
<Dropdown.Button menu={menuProps} placement="bottom" icon={<UserOutlined />}>
|
||||
Dropdown
|
||||
</Dropdown.Button>
|
||||
<Dropdown.Button onClick={handleButtonClick} overlay={menu} disabled>
|
||||
<Dropdown.Button menu={menuProps} onClick={handleButtonClick} disabled>
|
||||
Dropdown
|
||||
</Dropdown.Button>
|
||||
<Dropdown.Button
|
||||
overlay={menu}
|
||||
menu={menuProps}
|
||||
buttonsRender={([leftButton, rightButton]) => [
|
||||
<Tooltip title="tooltip" key="leftButton">
|
||||
{leftButton}
|
||||
@ -74,7 +74,7 @@ const App: React.FC = () => (
|
||||
>
|
||||
With Tooltip
|
||||
</Dropdown.Button>
|
||||
<Dropdown overlay={menu}>
|
||||
<Dropdown menu={menuProps}>
|
||||
<Button>
|
||||
<Space>
|
||||
Button
|
||||
@ -82,7 +82,7 @@ const App: React.FC = () => (
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<Dropdown.Button danger onClick={handleButtonClick} overlay={menu}>
|
||||
<Dropdown.Button menu={menuProps} onClick={handleButtonClick} danger>
|
||||
Danger
|
||||
</Dropdown.Button>
|
||||
</Space>
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
order: 4
|
||||
order: 6
|
||||
title:
|
||||
zh-CN: 触发事件
|
||||
en-US: Click event
|
||||
@ -16,35 +16,30 @@ An event will be triggered when you click menu items, in which you can make diff
|
||||
```tsx
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Dropdown, Menu, message, Space } from 'antd';
|
||||
import { Dropdown, message, Space } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const onClick: MenuProps['onClick'] = ({ key }) => {
|
||||
message.info(`Click on item ${key}`);
|
||||
};
|
||||
|
||||
const menu = (
|
||||
<Menu
|
||||
onClick={onClick}
|
||||
items={[
|
||||
{
|
||||
label: '1st menu item',
|
||||
key: '1',
|
||||
},
|
||||
{
|
||||
label: '2nd menu item',
|
||||
key: '2',
|
||||
},
|
||||
{
|
||||
label: '3rd menu item',
|
||||
key: '3',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
label: '1st menu item',
|
||||
key: '1',
|
||||
},
|
||||
{
|
||||
label: '2nd menu item',
|
||||
key: '2',
|
||||
},
|
||||
{
|
||||
label: '3rd menu item',
|
||||
key: '3',
|
||||
},
|
||||
];
|
||||
|
||||
const App: React.FC = () => (
|
||||
<Dropdown overlay={menu}>
|
||||
<Dropdown menu={{ items, onClick }}>
|
||||
<a onClick={e => e.preventDefault()}>
|
||||
<Space>
|
||||
Hover me, Click menu item
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
order: 2
|
||||
order: 3
|
||||
title:
|
||||
zh-CN: 其他元素
|
||||
en-US: Other elements
|
||||
@ -15,42 +15,39 @@ Divider and disabled menu item.
|
||||
|
||||
```tsx
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import { Dropdown, Menu, Space } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Dropdown, Space } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const menu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.antgroup.com">
|
||||
1st menu item
|
||||
</a>
|
||||
),
|
||||
key: '0',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.aliyun.com">
|
||||
2nd menu item
|
||||
</a>
|
||||
),
|
||||
key: '1',
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
label: '3rd menu item(disabled)',
|
||||
key: '3',
|
||||
disabled: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.antgroup.com">
|
||||
1st menu item
|
||||
</a>
|
||||
),
|
||||
key: '0',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.aliyun.com">
|
||||
2nd menu item
|
||||
</a>
|
||||
),
|
||||
key: '1',
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
label: '3rd menu item(disabled)',
|
||||
key: '3',
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
const App: React.FC = () => (
|
||||
<Dropdown overlay={menu}>
|
||||
<Dropdown menu={{ items }}>
|
||||
<a onClick={e => e.preventDefault()}>
|
||||
<Space>
|
||||
Hover me
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
order: 9
|
||||
order: 12
|
||||
title:
|
||||
zh-CN: 加载中状态
|
||||
en-US: Loading
|
||||
@ -15,19 +15,16 @@ A loading indicator can be added to a button by setting the `loading` property o
|
||||
|
||||
```tsx
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import { Dropdown, Menu, Space } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Dropdown, Space } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const menu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
label: 'Submit and continue',
|
||||
key: '1',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
label: 'Submit and continue',
|
||||
key: '1',
|
||||
},
|
||||
];
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [loadings, setLoadings] = useState<boolean[]>([]);
|
||||
@ -50,16 +47,16 @@ const App: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Space direction="vertical">
|
||||
<Dropdown.Button type="primary" loading overlay={menu}>
|
||||
<Dropdown.Button type="primary" loading menu={{ items }}>
|
||||
Submit
|
||||
</Dropdown.Button>
|
||||
<Dropdown.Button type="primary" size="small" loading overlay={menu}>
|
||||
<Dropdown.Button type="primary" size="small" loading menu={{ items }}>
|
||||
Submit
|
||||
</Dropdown.Button>
|
||||
<Dropdown.Button
|
||||
type="primary"
|
||||
loading={loadings[0]}
|
||||
overlay={menu}
|
||||
menu={{ items }}
|
||||
onClick={() => enterLoading(0)}
|
||||
>
|
||||
Submit
|
||||
@ -67,7 +64,7 @@ const App: React.FC = () => {
|
||||
<Dropdown.Button
|
||||
icon={<DownOutlined />}
|
||||
loading={loadings[1]}
|
||||
overlay={menu}
|
||||
menu={{ items }}
|
||||
onClick={() => enterLoading(1)}
|
||||
>
|
||||
Submit
|
||||
|
@ -21,7 +21,7 @@ This demo was created for debugging Menu styles inside Dropdown.
|
||||
```tsx
|
||||
import { AppstoreOutlined, DownOutlined, MailOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Dropdown, Menu, Space } from 'antd';
|
||||
import { Dropdown, Space } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
type MenuItem = Required<MenuProps>['items'][number];
|
||||
@ -69,10 +69,14 @@ const items: MenuItem[] = [
|
||||
null as any,
|
||||
];
|
||||
|
||||
const menu = <Menu selectedKeys={['1']} openKeys={['sub1']} items={items} />;
|
||||
|
||||
const App: React.FC = () => (
|
||||
<Dropdown overlay={menu}>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items,
|
||||
selectedKeys: ['1'],
|
||||
openKeys: ['sub1'],
|
||||
}}
|
||||
>
|
||||
<a onClick={e => e.preventDefault()}>
|
||||
<Space>
|
||||
Hover to check menu style
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
order: 7
|
||||
order: 12
|
||||
title:
|
||||
zh-CN: 菜单隐藏方式
|
||||
en-US: The way of hiding menu.
|
||||
@ -16,7 +16,7 @@ The default is to close the menu when you click on menu items, this feature can
|
||||
```tsx
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Dropdown, Menu, Space } from 'antd';
|
||||
import { Dropdown, Space } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const App: React.FC = () => {
|
||||
@ -32,28 +32,30 @@ const App: React.FC = () => {
|
||||
setOpen(flag);
|
||||
};
|
||||
|
||||
const menu = (
|
||||
<Menu
|
||||
onClick={handleMenuClick}
|
||||
items={[
|
||||
{
|
||||
label: 'Clicking me will not close the menu.',
|
||||
key: '1',
|
||||
},
|
||||
{
|
||||
label: 'Clicking me will not close the menu also.',
|
||||
key: '2',
|
||||
},
|
||||
{
|
||||
label: 'Clicking me will close the menu.',
|
||||
key: '3',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
label: 'Clicking me will not close the menu.',
|
||||
key: '1',
|
||||
},
|
||||
{
|
||||
label: 'Clicking me will not close the menu also.',
|
||||
key: '2',
|
||||
},
|
||||
{
|
||||
label: 'Clicking me will close the menu.',
|
||||
key: '3',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Dropdown overlay={menu} onOpenChange={handleOpenChange} open={open}>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items,
|
||||
onClick: handleMenuClick,
|
||||
}}
|
||||
onOpenChange={handleOpenChange}
|
||||
open={open}
|
||||
>
|
||||
<a onClick={e => e.preventDefault()}>
|
||||
<Space>
|
||||
Hover me
|
||||
|
@ -14,61 +14,58 @@ title:
|
||||
Support 6 placements.
|
||||
|
||||
```tsx
|
||||
import { Button, Dropdown, Menu, Space } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Button, Dropdown, Space } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const menu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.antgroup.com">
|
||||
1st menu item
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.aliyun.com">
|
||||
2nd menu item
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.luohanacademy.com">
|
||||
3rd menu item
|
||||
</a>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.antgroup.com">
|
||||
1st menu item
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.aliyun.com">
|
||||
2nd menu item
|
||||
</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: (
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.luohanacademy.com">
|
||||
3rd menu item
|
||||
</a>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const App: React.FC = () => (
|
||||
<Space direction="vertical">
|
||||
<Space wrap>
|
||||
<Dropdown overlay={menu} placement="bottomLeft">
|
||||
<Dropdown menu={{ items }} placement="bottomLeft">
|
||||
<Button>bottomLeft</Button>
|
||||
</Dropdown>
|
||||
<Dropdown overlay={menu} placement="bottom">
|
||||
<Dropdown menu={{ items }} placement="bottom">
|
||||
<Button>bottom</Button>
|
||||
</Dropdown>
|
||||
<Dropdown overlay={menu} placement="bottomRight">
|
||||
<Dropdown menu={{ items }} placement="bottomRight">
|
||||
<Button>bottomRight</Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
<Space wrap>
|
||||
<Dropdown overlay={menu} placement="topLeft">
|
||||
<Dropdown menu={{ items }} placement="topLeft">
|
||||
<Button>topLeft</Button>
|
||||
</Dropdown>
|
||||
<Dropdown overlay={menu} placement="top">
|
||||
<Dropdown menu={{ items }} placement="top">
|
||||
<Button>top</Button>
|
||||
</Dropdown>
|
||||
<Dropdown overlay={menu} placement="topRight">
|
||||
<Dropdown menu={{ items }} placement="topRight">
|
||||
<Button>topRight</Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
order: 10
|
||||
order: 13
|
||||
title:
|
||||
zh-CN: 菜单可选选择
|
||||
en-US: Selectable Menu
|
||||
@ -7,40 +7,41 @@ title:
|
||||
|
||||
## zh-CN
|
||||
|
||||
为 Menu 添加 `selectable` 属性可以开启选择能力。
|
||||
添加 `menu` 中的 `selectable` 属性可以开启选择能力。
|
||||
|
||||
## en-US
|
||||
|
||||
Config Menu `selectable` prop to enable selectable ability.
|
||||
Configure the `selectable` property in `menu` to enable selectable ability.
|
||||
|
||||
```tsx
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import { Dropdown, Menu, Space, Typography } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Dropdown, Space, Typography } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const menu = (
|
||||
<Menu
|
||||
selectable
|
||||
defaultSelectedKeys={['3']}
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: 'Item 1',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: 'Item 2',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: 'Item 3',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
label: 'Item 1',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: 'Item 2',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: 'Item 3',
|
||||
},
|
||||
];
|
||||
|
||||
const App: React.FC = () => (
|
||||
<Dropdown overlay={menu}>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items,
|
||||
selectable: true,
|
||||
defaultSelectedKeys: ['3'],
|
||||
}}
|
||||
>
|
||||
<Typography.Link>
|
||||
<Space>
|
||||
Selectable
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
order: 6
|
||||
order: 9
|
||||
title:
|
||||
zh-CN: 多级菜单
|
||||
en-US: Cascading menu
|
||||
@ -15,62 +15,59 @@ The menu has multiple levels.
|
||||
|
||||
```tsx
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import { Dropdown, Menu, Space } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Dropdown, Space } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const menu = (
|
||||
<Menu
|
||||
items={[
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
type: 'group',
|
||||
label: 'Group title',
|
||||
children: [
|
||||
{
|
||||
key: '1',
|
||||
type: 'group',
|
||||
label: 'Group title',
|
||||
children: [
|
||||
{
|
||||
key: '1-1',
|
||||
label: '1st menu item',
|
||||
},
|
||||
{
|
||||
key: '1-2',
|
||||
label: '2nd menu item',
|
||||
},
|
||||
],
|
||||
key: '1-1',
|
||||
label: '1st menu item',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: 'sub menu',
|
||||
children: [
|
||||
{
|
||||
key: '2-1',
|
||||
label: '3rd menu item',
|
||||
},
|
||||
{
|
||||
key: '2-2',
|
||||
label: '4th menu item',
|
||||
},
|
||||
],
|
||||
key: '1-2',
|
||||
label: '2nd menu item',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: 'sub menu',
|
||||
children: [
|
||||
{
|
||||
key: '2-1',
|
||||
label: '3rd menu item',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: 'disabled sub menu',
|
||||
disabled: true,
|
||||
children: [
|
||||
{
|
||||
key: '3-1',
|
||||
label: '5d menu item',
|
||||
},
|
||||
{
|
||||
key: '3-2',
|
||||
label: '6th menu item',
|
||||
},
|
||||
],
|
||||
key: '2-2',
|
||||
label: '4th menu item',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
],
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: 'disabled sub menu',
|
||||
disabled: true,
|
||||
children: [
|
||||
{
|
||||
key: '3-1',
|
||||
label: '5d menu item',
|
||||
},
|
||||
{
|
||||
key: '3-2',
|
||||
label: '6th menu item',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const App: React.FC = () => (
|
||||
<Dropdown overlay={menu}>
|
||||
<Dropdown menu={{ items }}>
|
||||
<a onClick={e => e.preventDefault()}>
|
||||
<Space>
|
||||
Cascading menu
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
order: 3
|
||||
order: 5
|
||||
title:
|
||||
zh-CN: 触发方式
|
||||
en-US: Trigger mode
|
||||
@ -15,33 +15,30 @@ The default trigger mode is `hover`, you can change it to `click`.
|
||||
|
||||
```tsx
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import { Dropdown, Menu, Space } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Dropdown, Space } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const menu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
label: <a href="https://www.antgroup.com">1st menu item</a>,
|
||||
key: '0',
|
||||
},
|
||||
{
|
||||
label: <a href="https://www.aliyun.com">2nd menu item</a>,
|
||||
key: '1',
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
label: '3rd menu item',
|
||||
key: '3',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
label: <a href="https://www.antgroup.com">1st menu item</a>,
|
||||
key: '0',
|
||||
},
|
||||
{
|
||||
label: <a href="https://www.aliyun.com">2nd menu item</a>,
|
||||
key: '1',
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
label: '3rd menu item',
|
||||
key: '3',
|
||||
},
|
||||
];
|
||||
|
||||
const App: React.FC = () => (
|
||||
<Dropdown overlay={menu} trigger={['click']}>
|
||||
<Dropdown menu={{ items }} trigger={['click']}>
|
||||
<a onClick={e => e.preventDefault()}>
|
||||
<Space>
|
||||
Click me
|
||||
|
@ -50,6 +50,9 @@ const DropdownButton: DropdownButtonInterface = props => {
|
||||
htmlType,
|
||||
children,
|
||||
className,
|
||||
menu,
|
||||
arrow,
|
||||
autoFocus,
|
||||
overlay,
|
||||
trigger,
|
||||
align,
|
||||
@ -74,6 +77,9 @@ const DropdownButton: DropdownButtonInterface = props => {
|
||||
const [wrapSSR, hashId] = useStyle(prefixCls);
|
||||
|
||||
const dropdownProps: DropdownProps = {
|
||||
menu,
|
||||
arrow,
|
||||
autoFocus,
|
||||
align,
|
||||
overlay,
|
||||
disabled,
|
||||
@ -85,7 +91,7 @@ const DropdownButton: DropdownButtonInterface = props => {
|
||||
overlayClassName,
|
||||
overlayStyle,
|
||||
destroyPopupOnHide,
|
||||
} as DropdownProps;
|
||||
};
|
||||
const { compactSize, compactItemClassnames } = useCompactItemContext(prefixCls, direction);
|
||||
|
||||
const classes = classNames(buttonPrefixCls, compactItemClassnames, className, hashId);
|
||||
|
@ -4,6 +4,8 @@ import RcDropdown from 'rc-dropdown';
|
||||
import useEvent from 'rc-util/lib/hooks/useEvent';
|
||||
import useMergedState from 'rc-util/lib/hooks/useMergedState';
|
||||
import * as React from 'react';
|
||||
import Menu from '../menu';
|
||||
import type { MenuProps } from '../menu';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
import { OverrideProvider } from '../menu/OverrideContext';
|
||||
import genPurePanel from '../_util/PurePanel';
|
||||
@ -47,10 +49,11 @@ export type DropdownArrowOptions = {
|
||||
};
|
||||
|
||||
export interface DropdownProps {
|
||||
menu?: MenuProps;
|
||||
autoFocus?: boolean;
|
||||
arrow?: boolean | DropdownArrowOptions;
|
||||
trigger?: ('click' | 'hover' | 'contextMenu')[];
|
||||
overlay: React.ReactElement | OverlayFunc;
|
||||
dropdownRender?: (originNode: React.ReactNode) => React.ReactNode;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
open?: boolean;
|
||||
disabled?: boolean;
|
||||
@ -70,6 +73,8 @@ export interface DropdownProps {
|
||||
children?: React.ReactNode;
|
||||
|
||||
// Deprecated
|
||||
/** @deprecated Please use `menu` instead */
|
||||
overlay?: React.ReactElement | OverlayFunc;
|
||||
/** @deprecated Please use `open` instead */
|
||||
visible?: boolean;
|
||||
/** @deprecated Please use `onOpenChange` instead */
|
||||
@ -88,6 +93,26 @@ const Dropdown: DropdownInterface = props => {
|
||||
direction,
|
||||
} = React.useContext(ConfigContext);
|
||||
|
||||
// Warning for deprecated usage
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
[
|
||||
['visible', 'open'],
|
||||
['onVisibleChange', 'onOpenChange'],
|
||||
].forEach(([deprecatedName, newName]) => {
|
||||
warning(
|
||||
!(deprecatedName in props),
|
||||
'Dropdown',
|
||||
`\`${deprecatedName}\` is deprecated which will be removed in next major version, please use \`${newName}\` instead.`,
|
||||
);
|
||||
});
|
||||
|
||||
warning(
|
||||
!('overlay' in props),
|
||||
'Dropdown',
|
||||
'`overlay` is deprecated. Please use `menu` instead.',
|
||||
);
|
||||
}
|
||||
|
||||
const getTransitionName = () => {
|
||||
const rootPrefixCls = getPrefixCls();
|
||||
const { placement = '', transitionName } = props;
|
||||
@ -120,11 +145,13 @@ const Dropdown: DropdownInterface = props => {
|
||||
};
|
||||
|
||||
const {
|
||||
menu,
|
||||
arrow,
|
||||
prefixCls: customizePrefixCls,
|
||||
children,
|
||||
trigger,
|
||||
disabled,
|
||||
dropdownRender,
|
||||
getPopupContainer,
|
||||
overlayClassName,
|
||||
open,
|
||||
@ -203,11 +230,16 @@ const Dropdown: DropdownInterface = props => {
|
||||
const { overlay } = props;
|
||||
|
||||
let overlayNode: React.ReactNode;
|
||||
if (typeof overlay === 'function') {
|
||||
overlayNode = overlay();
|
||||
if (menu?.items) {
|
||||
overlayNode = <Menu {...menu} />;
|
||||
} else if (typeof overlay === 'function') {
|
||||
overlayNode = (overlay as OverlayFunc)();
|
||||
} else {
|
||||
overlayNode = overlay;
|
||||
}
|
||||
if (dropdownRender) {
|
||||
overlayNode = dropdownRender(overlayNode);
|
||||
}
|
||||
overlayNode = React.Children.only(
|
||||
typeof overlayNode === 'string' ? <span>{overlayNode}</span> : overlayNode,
|
||||
);
|
||||
|
@ -11,6 +11,39 @@ A dropdown list.
|
||||
|
||||
When there are more than a few options to choose from, you can wrap them in a `Dropdown`. By hovering or clicking on the trigger, a dropdown menu will appear, which allows you to choose an option and execute the relevant action.
|
||||
|
||||
### Usage upgrade after 4.24.0
|
||||
|
||||
```__react
|
||||
import Alert from '../alert';
|
||||
ReactDOM.render(<Alert message="After version 4.24.0, we provide a simpler usage <Dropdown menu={{ items: [...] }} /> with better performance and potential of writing simpler code style in your applications. Meanwhile, we deprecated the old usage in browser console, we will remove it in antd 5.0." />, mountNode);
|
||||
```
|
||||
|
||||
```jsx
|
||||
// works when >=4.24.0, recommended ✅
|
||||
const items = [
|
||||
{ label: 'item 1', key: 'item-1' }, // remember to pass the key prop
|
||||
{ label: 'item 2', key: 'item-2' },
|
||||
];
|
||||
return (
|
||||
<Dropdown menu={{ items }}>
|
||||
<a>Hover me</a>
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
// works when <4.24.0, deprecated when >=4.24.0 🙅🏻♀️
|
||||
const menu = (
|
||||
<Menu>
|
||||
<Menu.Item>item 1</Menu.Item>
|
||||
<Menu.Item>item 2</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
return (
|
||||
<Dropdown overlay={menu}>
|
||||
<a>Hover me</a>
|
||||
</Dropdown>
|
||||
);
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### Dropdown
|
||||
@ -21,8 +54,9 @@ When there are more than a few options to choose from, you can wrap them in a `D
|
||||
| autoFocus | Focus element in `overlay` when opened | boolean | false | 4.21.0 |
|
||||
| disabled | Whether the dropdown menu is disabled | boolean | - | |
|
||||
| destroyPopupOnHide | Whether destroy dropdown when hidden | boolean | false | |
|
||||
| dropdownRender | Customize dropdown content | (menus: ReactNode) => ReactNode | - | 4.24.0 |
|
||||
| getPopupContainer | To set the container of the dropdown menu. The default is to create a div element in body, but you can reset it to the scrolling area and make a relative reposition. [Example on CodePen](https://codepen.io/afc163/pen/zEjNOy?editors=0010) | (triggerNode: HTMLElement) => HTMLElement | () => document.body | |
|
||||
| overlay | The dropdown menu | [Menu](/components/menu) \| () => Menu | - | |
|
||||
| menu | The menu props | [MenuProps](/components/menu/#API) | - | 4.24.0 |
|
||||
| overlayClassName | The class name of the dropdown root element | string | - | |
|
||||
| overlayStyle | The style of the dropdown root element | CSSProperties | - | |
|
||||
| placement | Placement of popup menu: `bottom` `bottomLeft` `bottomRight` `top` `topLeft` `topRight` | string | `bottomLeft` | |
|
||||
@ -30,26 +64,16 @@ When there are more than a few options to choose from, you can wrap them in a `D
|
||||
| open | Whether the dropdown menu is currently open. Use `visible` under 4.23.0 ([why?](/docs/react/faq#why-open)) | boolean | - | 4.23.0 |
|
||||
| onOpenChange | Called when the open state is changed. Not trigger when hidden by click item. Use `onVisibleChange` under 4.23.0 ([why?](/docs/react/faq#why-open)) | (open: boolean) => void | - | 4.23.0 |
|
||||
|
||||
You should use [Menu](/components/menu/) as `overlay`. The menu items and dividers are also available by using `Menu.Item` and `Menu.Divider`.
|
||||
|
||||
> Warning: You must set a unique `key` for `Menu.Item`.
|
||||
>
|
||||
> Menu of Dropdown is unselectable by default, you can make it selectable via `<Menu selectable>`.
|
||||
|
||||
### Dropdown.Button
|
||||
|
||||
Same props from Dropdown. And includes additional props:
|
||||
|
||||
| Property | Description | Type | Default | Version |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| buttonsRender | Custom buttons inside Dropdown.Button | (buttons: ReactNode\[]) => ReactNode\[] | - | |
|
||||
| loading | Set the loading status of button | boolean \| { delay: number } | false | |
|
||||
| danger | Set the danger status of button | boolean | - | 4.23.0 |
|
||||
| disabled | Whether the dropdown menu is disabled | boolean | - | |
|
||||
| icon | Icon (appears on the right) | ReactNode | - | |
|
||||
| overlay | The dropdown menu | [Menu](/components/menu) | - | |
|
||||
| placement | Placement of popup menu: `bottom` `bottomLeft` `bottomRight` `top` `topLeft` `topRight` | string | `bottomRight` | |
|
||||
| size | Size of the button, the same as [Button](/components/button/#API) | string | `default` | |
|
||||
| trigger | The trigger mode which executes the dropdown action | Array<`click`\|`hover`\|`contextMenu`> | \[`hover`] | |
|
||||
| type | Type of the button, the same as [Button](/components/button/#API) | string | `default` | |
|
||||
| open | Whether the dropdown menu is currently open | boolean | - | 4.23.0 |
|
||||
| onClick | The same as [Button](/components/button/#API): called when you click the button on the left | (event) => void | - | |
|
||||
| onOpenChange | Called when the open state is changed | (open: boolean) => void | - | 4.23.0 |
|
||||
|
@ -15,6 +15,39 @@ cover: https://gw.alipayobjects.com/zos/alicdn/eedWN59yJ/Dropdown.svg
|
||||
- 用于收罗一组命令操作。
|
||||
- Select 用于选择,而 Dropdown 是命令集合。
|
||||
|
||||
### 4.24.0 用法升级
|
||||
|
||||
```__react
|
||||
import Alert from '../alert';
|
||||
ReactDOM.render(<Alert message="在 4.24.0 版本后,我们提供了 <Dropdown menu={{ items: [...] }} /> 的简写方式,有更好的性能和更方便的数据组织方式,开发者不再需要自行拼接 JSX。同时我们废弃了原先的写法,你还是可以在 4.x 继续使用,但会在控制台看到警告,并会在 5.0 后移除。" />, mountNode);
|
||||
```
|
||||
|
||||
```jsx
|
||||
// >=4.24.0 可用,推荐的写法 ✅
|
||||
const items = [
|
||||
{ label: '菜单项一', key: 'item-1' }, // 菜单项务必填写 key
|
||||
{ label: '菜单项二', key: 'item-2' },
|
||||
];
|
||||
return (
|
||||
<Dropdown menu={{ items }}>
|
||||
<a>Hover me</a>
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
// <4.24.0 可用,>=4.24.0 时不推荐 🙅🏻♀️
|
||||
const menu = (
|
||||
<Menu>
|
||||
<Menu.Item>菜单项一</Menu.Item>
|
||||
<Menu.Item>菜单项二</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
return (
|
||||
<Dropdown overlay={menu}>
|
||||
<a>Hover me</a>
|
||||
</Dropdown>
|
||||
);
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
属性如下
|
||||
@ -25,8 +58,9 @@ cover: https://gw.alipayobjects.com/zos/alicdn/eedWN59yJ/Dropdown.svg
|
||||
| autoFocus | 打开后自动聚焦下拉框 | boolean | false | 4.21.0 |
|
||||
| disabled | 菜单是否禁用 | boolean | - | |
|
||||
| destroyPopupOnHide | 关闭后是否销毁 Dropdown | boolean | false | |
|
||||
| dropdownRender | 自定义下拉框内容 | (menus: ReactNode) => ReactNode | - | 4.24.0 |
|
||||
| getPopupContainer | 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。[示例](https://codepen.io/afc163/pen/zEjNOy?editors=0010) | (triggerNode: HTMLElement) => HTMLElement | () => document.body | |
|
||||
| overlay | 菜单 | [Menu](/components/menu) \| () => Menu | - | |
|
||||
| menu | 菜单配置项 | [MenuProps](/components/menu/#API) | - | 4.24.0 |
|
||||
| overlayClassName | 下拉根元素的类名称 | string | - | |
|
||||
| overlayStyle | 下拉根元素的样式 | CSSProperties | - | |
|
||||
| placement | 菜单弹出位置:`bottom` `bottomLeft` `bottomRight` `top` `topLeft` `topRight` | string | `bottomLeft` | |
|
||||
@ -34,26 +68,16 @@ cover: https://gw.alipayobjects.com/zos/alicdn/eedWN59yJ/Dropdown.svg
|
||||
| open | 菜单是否显示,小于 4.23.0 使用 `visible`([为什么?](/docs/react/faq#why-open)) | boolean | - | 4.23.0 |
|
||||
| onOpenChange | 菜单显示状态改变时调用,点击菜单按钮导致的消失不会触发。小于 4.23.0 使用 `onVisibleChange`([为什么?](/docs/react/faq#why-open)) | (open: boolean) => void | - | 4.23.0 |
|
||||
|
||||
`overlay` 菜单使用 [Menu](/components/menu/),还包括菜单项 `Menu.Item`,分割线 `Menu.Divider`。
|
||||
|
||||
> 注意: Menu.Item 必须设置唯一的 key 属性。
|
||||
>
|
||||
> Dropdown 下的 Menu 默认不可选中。如果需要菜单可选中,可以指定 `<Menu selectable>`。
|
||||
|
||||
### Dropdown.Button
|
||||
|
||||
属性与 Dropdown 的相同。还包含以下属性:
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| buttonsRender | 自定义左右两个按钮 | (buttons: ReactNode\[]) => ReactNode\[] | - | |
|
||||
| loading | 设置按钮载入状态 | boolean \| { delay: number } | false | |
|
||||
| danger | 设置危险按钮 | boolean | - | 4.23.0 |
|
||||
| disabled | 菜单是否禁用 | boolean | - | |
|
||||
| icon | 右侧的 icon | ReactNode | - | |
|
||||
| overlay | 菜单 | [Menu](/components/menu/) | - | |
|
||||
| placement | 菜单弹出位置:`bottom` `bottomLeft` `bottomRight` `top` `topLeft` `topRight` | string | `bottomRight` | |
|
||||
| size | 按钮大小,和 [Button](/components/button/#API) 一致 | string | `default` | |
|
||||
| trigger | 触发下拉的行为 | Array<`click`\|`hover`\|`contextMenu`> | \[`hover`] | |
|
||||
| type | 按钮类型,和 [Button](/components/button/#API) 一致 | string | `default` | |
|
||||
| open | 菜单是否显示 | boolean | - | 4.23.0 |
|
||||
| onClick | 点击左侧按钮的回调,和 [Button](/components/button/#API) 一致 | (event) => void | - | |
|
||||
| onOpenChange | 菜单显示状态改变时调用 | (open: boolean) => void | - | 4.23.0 |
|
||||
|
@ -70,7 +70,7 @@ exports[`renders ./components/empty/demo/basic.md extend context correctly 1`] =
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -194,7 +194,7 @@ exports[`renders ./components/empty/demo/config-provider.md extend context corre
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -312,7 +312,7 @@ exports[`renders ./components/empty/demo/config-provider.md extend context corre
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -439,7 +439,7 @@ exports[`renders ./components/empty/demo/config-provider.md extend context corre
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -711,7 +711,7 @@ exports[`renders ./components/empty/demo/config-provider.md extend context corre
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1000,7 +1000,7 @@ exports[`renders ./components/empty/demo/config-provider.md extend context corre
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1101,7 +1101,7 @@ exports[`renders ./components/empty/demo/config-provider.md extend context corre
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@ -1171,7 +1171,7 @@ exports[`renders ./components/empty/demo/config-provider.md extend context corre
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1335,7 +1335,7 @@ exports[`renders ./components/empty/demo/simple.md extend context correctly 1`]
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -70,7 +70,7 @@ exports[`renders ./components/empty/demo/basic.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -389,7 +389,7 @@ exports[`renders ./components/empty/demo/config-provider.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -549,7 +549,7 @@ exports[`renders ./components/empty/demo/config-provider.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -650,7 +650,7 @@ exports[`renders ./components/empty/demo/config-provider.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@ -720,7 +720,7 @@ exports[`renders ./components/empty/demo/config-provider.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -884,7 +884,7 @@ exports[`renders ./components/empty/demo/simple.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -70,7 +70,7 @@ exports[`Empty rtl render component should be rendered correctly in RTL directio
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -145,7 +145,7 @@ exports[`Empty should render in RTL direction 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -284,27 +284,31 @@ function InternalFormItem<Values = any>(props: FormItemProps<Values>): React.Rea
|
||||
warning(
|
||||
!(shouldUpdate && dependencies),
|
||||
'Form.Item',
|
||||
"`shouldUpdate` and `dependencies` shouldn't be used together. See https://ant.design/components/form/#dependencies.",
|
||||
"`shouldUpdate` and `dependencies` shouldn't be used together. See https://u.ant.design/#form-deps.",
|
||||
);
|
||||
if (Array.isArray(children) && hasName) {
|
||||
warning(false, 'Form.Item', '`children` is array of render props cannot have `name`.');
|
||||
warning(
|
||||
false,
|
||||
'Form.Item',
|
||||
'A `Form.Item` with a `name` prop must have a single child element. For information on how to render more complex form items, see https://u.ant.design/#complex-form-item.',
|
||||
);
|
||||
childNode = children;
|
||||
} else if (isRenderProps && (!(shouldUpdate || dependencies) || hasName)) {
|
||||
warning(
|
||||
!!(shouldUpdate || dependencies),
|
||||
'Form.Item',
|
||||
'`children` of render props only work with `shouldUpdate` or `dependencies`.',
|
||||
'A `Form.Item` with a render function must have either `shouldUpdate` or `dependencies`.',
|
||||
);
|
||||
warning(
|
||||
!hasName,
|
||||
'Form.Item',
|
||||
"Do not use `name` with `children` of render props since it's not a field.",
|
||||
'A `Form.Item` with a render function cannot be a field, and thus cannot have a `name` prop.',
|
||||
);
|
||||
} else if (dependencies && !isRenderProps && !hasName) {
|
||||
warning(
|
||||
false,
|
||||
'Form.Item',
|
||||
'Must set `name` or use render props when `dependencies` is set.',
|
||||
'Must set `name` or use a render function when `dependencies` is set.',
|
||||
);
|
||||
} else if (isValidElement(children)) {
|
||||
warning(
|
||||
|
@ -5900,7 +5900,7 @@ exports[`renders ./components/form/demo/dynamic-form-items-complex.md extend con
|
||||
<form
|
||||
autocomplete="off"
|
||||
class="ant-form ant-form-horizontal"
|
||||
id="dynamic_form_nest_item"
|
||||
id="dynamic_form_complex"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item"
|
||||
@ -5913,7 +5913,7 @@ exports[`renders ./components/form/demo/dynamic-form-items-complex.md extend con
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="dynamic_form_nest_item_area"
|
||||
for="dynamic_form_complex_area"
|
||||
title="Area"
|
||||
>
|
||||
Area
|
||||
@ -5939,15 +5939,15 @@ exports[`renders ./components/form/demo/dynamic-form-items-complex.md extend con
|
||||
class="ant-select-selection-search"
|
||||
>
|
||||
<input
|
||||
aria-activedescendant="dynamic_form_nest_item_area_list_0"
|
||||
aria-activedescendant="dynamic_form_complex_area_list_0"
|
||||
aria-autocomplete="list"
|
||||
aria-controls="dynamic_form_nest_item_area_list"
|
||||
aria-controls="dynamic_form_complex_area_list"
|
||||
aria-haspopup="listbox"
|
||||
aria-owns="dynamic_form_nest_item_area_list"
|
||||
aria-owns="dynamic_form_complex_area_list"
|
||||
aria-required="true"
|
||||
autocomplete="off"
|
||||
class="ant-select-selection-search-input"
|
||||
id="dynamic_form_nest_item_area"
|
||||
id="dynamic_form_complex_area"
|
||||
readonly=""
|
||||
role="combobox"
|
||||
style="opacity:0"
|
||||
@ -5967,14 +5967,14 @@ exports[`renders ./components/form/demo/dynamic-form-items-complex.md extend con
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
id="dynamic_form_nest_item_area_list"
|
||||
id="dynamic_form_complex_area_list"
|
||||
role="listbox"
|
||||
style="height:0;width:0;overflow:hidden"
|
||||
>
|
||||
<div
|
||||
aria-label="Beijing"
|
||||
aria-selected="false"
|
||||
id="dynamic_form_nest_item_area_list_0"
|
||||
id="dynamic_form_complex_area_list_0"
|
||||
role="option"
|
||||
>
|
||||
Beijing
|
||||
@ -5982,7 +5982,7 @@ exports[`renders ./components/form/demo/dynamic-form-items-complex.md extend con
|
||||
<div
|
||||
aria-label="Shanghai"
|
||||
aria-selected="false"
|
||||
id="dynamic_form_nest_item_area_list_1"
|
||||
id="dynamic_form_complex_area_list_1"
|
||||
role="option"
|
||||
>
|
||||
Shanghai
|
||||
@ -6162,7 +6162,7 @@ exports[`renders ./components/form/demo/dynamic-form-items-no-style.md extend co
|
||||
<form
|
||||
autocomplete="off"
|
||||
class="ant-form ant-form-horizontal"
|
||||
id="dynamic_form_nest_item"
|
||||
id="dynamic_form_no_style"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item"
|
||||
|
@ -3373,7 +3373,7 @@ exports[`renders ./components/form/demo/dynamic-form-items-complex.md correctly
|
||||
<form
|
||||
autocomplete="off"
|
||||
class="ant-form ant-form-horizontal"
|
||||
id="dynamic_form_nest_item"
|
||||
id="dynamic_form_complex"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item"
|
||||
@ -3386,7 +3386,7 @@ exports[`renders ./components/form/demo/dynamic-form-items-complex.md correctly
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="dynamic_form_nest_item_area"
|
||||
for="dynamic_form_complex_area"
|
||||
title="Area"
|
||||
>
|
||||
Area
|
||||
@ -3412,15 +3412,15 @@ exports[`renders ./components/form/demo/dynamic-form-items-complex.md correctly
|
||||
class="ant-select-selection-search"
|
||||
>
|
||||
<input
|
||||
aria-activedescendant="dynamic_form_nest_item_area_list_0"
|
||||
aria-activedescendant="dynamic_form_complex_area_list_0"
|
||||
aria-autocomplete="list"
|
||||
aria-controls="dynamic_form_nest_item_area_list"
|
||||
aria-controls="dynamic_form_complex_area_list"
|
||||
aria-haspopup="listbox"
|
||||
aria-owns="dynamic_form_nest_item_area_list"
|
||||
aria-owns="dynamic_form_complex_area_list"
|
||||
aria-required="true"
|
||||
autocomplete="off"
|
||||
class="ant-select-selection-search-input"
|
||||
id="dynamic_form_nest_item_area"
|
||||
id="dynamic_form_complex_area"
|
||||
readonly=""
|
||||
role="combobox"
|
||||
style="opacity:0"
|
||||
@ -3553,7 +3553,7 @@ exports[`renders ./components/form/demo/dynamic-form-items-no-style.md correctly
|
||||
<form
|
||||
autocomplete="off"
|
||||
class="ant-form ant-form-horizontal"
|
||||
id="dynamic_form_nest_item"
|
||||
id="dynamic_form_no_style"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item"
|
||||
|
@ -179,27 +179,27 @@ describe('Form', () => {
|
||||
const { container } = render(<Demo />);
|
||||
|
||||
await changeValue(0, '1');
|
||||
await waitFakeTimer();
|
||||
await waitFakeTimer(2000, 2000);
|
||||
expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent('aaa');
|
||||
|
||||
await changeValue(0, '2');
|
||||
await waitFakeTimer();
|
||||
await waitFakeTimer(2000, 2000);
|
||||
expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent('ccc');
|
||||
|
||||
await changeValue(0, '1');
|
||||
await waitFakeTimer();
|
||||
await waitFakeTimer(2000, 2000);
|
||||
expect(container.querySelector('.ant-form-item-explain-error')).toHaveTextContent('aaa');
|
||||
});
|
||||
});
|
||||
|
||||
it('`shouldUpdate` should work with render props', () => {
|
||||
it('render functions require either `shouldUpdate` or `dependencies`', () => {
|
||||
render(
|
||||
<Form>
|
||||
<Form.Item>{() => null}</Form.Item>
|
||||
</Form>,
|
||||
);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
'Warning: [antd: Form.Item] `children` of render props only work with `shouldUpdate` or `dependencies`.',
|
||||
'Warning: [antd: Form.Item] A `Form.Item` with a render function must have either `shouldUpdate` or `dependencies`.',
|
||||
);
|
||||
});
|
||||
|
||||
@ -212,7 +212,7 @@ describe('Form', () => {
|
||||
</Form>,
|
||||
);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
"Warning: [antd: Form.Item] `shouldUpdate` and `dependencies` shouldn't be used together. See https://ant.design/components/form/#dependencies.",
|
||||
"Warning: [antd: Form.Item] `shouldUpdate` and `dependencies` shouldn't be used together. See https://u.ant.design/#form-deps.",
|
||||
);
|
||||
});
|
||||
|
||||
@ -225,11 +225,11 @@ describe('Form', () => {
|
||||
</Form>,
|
||||
);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
"Warning: [antd: Form.Item] Do not use `name` with `children` of render props since it's not a field.",
|
||||
'Warning: [antd: Form.Item] A `Form.Item` with a render function cannot be a field, and thus cannot have a `name` prop.',
|
||||
);
|
||||
});
|
||||
|
||||
it('children is array has name props', () => {
|
||||
it('multiple children with a name prop', () => {
|
||||
render(
|
||||
<Form>
|
||||
<Form.Item name="test">
|
||||
@ -239,7 +239,7 @@ describe('Form', () => {
|
||||
</Form>,
|
||||
);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
'Warning: [antd: Form.Item] `children` is array of render props cannot have `name`.',
|
||||
'Warning: [antd: Form.Item] A `Form.Item` with a `name` prop must have a single child element. For information on how to render more complex form items, see https://u.ant.design/#complex-form-item.',
|
||||
);
|
||||
});
|
||||
|
||||
@ -619,7 +619,7 @@ describe('Form', () => {
|
||||
</Form>,
|
||||
);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
'Warning: [antd: Form.Item] Must set `name` or use render props when `dependencies` is set.',
|
||||
'Warning: [antd: Form.Item] Must set `name` or use a render function when `dependencies` is set.',
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -44,7 +44,7 @@ const App: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Form form={form} name="dynamic_form_nest_item" onFinish={onFinish} autoComplete="off">
|
||||
<Form form={form} name="dynamic_form_complex" onFinish={onFinish} autoComplete="off">
|
||||
<Form.Item name="area" label="Area" rules={[{ required: true, message: 'Missing area' }]}>
|
||||
<Select options={areas} onChange={handleChange} />
|
||||
</Form.Item>
|
||||
|
@ -25,7 +25,7 @@ const App: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Form name="dynamic_form_nest_item" onFinish={onFinish} autoComplete="off">
|
||||
<Form name="dynamic_form_no_style" onFinish={onFinish} autoComplete="off">
|
||||
<Form.Item label="Users">
|
||||
<Form.List name="users">
|
||||
{(fields, { add, remove }) => (
|
||||
|
@ -527,3 +527,4 @@ React 中异步更新会导致受控组件交互行为异常。当用户交互
|
||||
|
||||
- 你可以阅读[《antd v4 Form 使用心得》](https://zhuanlan.zhihu.com/p/375753910)获得一些使用帮助以及建议。
|
||||
- 想在 DatePicker、Switch 也使用 before、after?可以参考[《如何优雅的对 Form.Item 的 children 增加 before、after》](https://zhuanlan.zhihu.com/p/422752055)。
|
||||
- 优雅的 Form + Modal 结合使用方案[《如何优雅的使用 Form + Modal》](https://zhuanlan.zhihu.com/p/388222294)。
|
||||
|
@ -1,48 +1,38 @@
|
||||
import React, { memo, useContext, useRef, useState } from 'react';
|
||||
import React, { memo, useContext } from 'react';
|
||||
import Row from '../row';
|
||||
import RowContext from '../RowContext';
|
||||
import { render, fireEvent } from '../../../tests/utils';
|
||||
import { fireEvent, pureRender } from '../../../tests/utils';
|
||||
|
||||
const CacheInner = memo(() => {
|
||||
const countRef = useRef(0);
|
||||
countRef.current++;
|
||||
useContext(RowContext);
|
||||
return (
|
||||
<div>
|
||||
Child Rendering Count: <span id="child_count">{countRef.current}</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
let innerCount = 0;
|
||||
let outerCount = 0;
|
||||
|
||||
const CacheOuter = () => {
|
||||
const [count, setCount] = useState(1);
|
||||
const handleClick = () => {
|
||||
setCount(count + 1);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<button type="button" onClick={handleClick} id="parent_btn">
|
||||
Click
|
||||
</button>
|
||||
Parent Rendering Count: <span id="parent_count">{count}</span>
|
||||
<Row>
|
||||
<CacheInner />
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
const handleClick = () => {
|
||||
outerCount++;
|
||||
};
|
||||
|
||||
it('Cached RowContext is working', () => {
|
||||
const { container } = render(<CacheOuter />);
|
||||
const childCount = container.querySelector('#child_count')?.textContent;
|
||||
|
||||
fireEvent.click(container.querySelector('#parent_btn')!);
|
||||
expect(container.querySelector('#parent_count')?.textContent).toBe('2');
|
||||
// child component won't rerender
|
||||
expect(container.querySelector('#child_count')?.textContent).toBe(childCount);
|
||||
|
||||
fireEvent.click(container.querySelector('#parent_btn')!);
|
||||
expect(container.querySelector('#parent_count')?.textContent).toBe('3');
|
||||
// child component won't rerender
|
||||
expect(container.querySelector('#child_count')?.textContent).toBe(childCount);
|
||||
const CacheInner: React.FC = memo(() => {
|
||||
innerCount++;
|
||||
useContext(RowContext);
|
||||
return null;
|
||||
});
|
||||
|
||||
const CacheOuter: React.FC = memo(() => (
|
||||
<>
|
||||
<button type="button" onClick={handleClick} id="parent_btn">
|
||||
Click
|
||||
</button>
|
||||
<Row>
|
||||
<CacheInner />
|
||||
</Row>
|
||||
</>
|
||||
));
|
||||
|
||||
it('Cached RowContext is working', () => {
|
||||
const { container, unmount } = pureRender(<CacheOuter />);
|
||||
expect(outerCount).toBe(0);
|
||||
expect(innerCount).toBe(1);
|
||||
fireEvent.click(container.querySelector('#parent_btn')!);
|
||||
expect(outerCount).toBe(1);
|
||||
expect(innerCount).toBe(1);
|
||||
unmount();
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { CSSObject } from '@ant-design/cssinjs';
|
||||
import { TinyColor } from '@ctrl/tinycolor';
|
||||
import { genModalMaskStyle } from '../../modal/style';
|
||||
import { initZoomMotion } from '../../style/motion';
|
||||
import { initZoomMotion, initFadeMotion } from '../../style/motion';
|
||||
import type { FullToken, GenerateStyle } from '../../theme';
|
||||
import { genComponentStyleHook, mergeToken } from '../../theme';
|
||||
import { resetComponent, textEllipsis } from '../../style';
|
||||
@ -51,78 +51,124 @@ export const genImageMaskStyle = (token: ImageToken): CSSObject => {
|
||||
};
|
||||
|
||||
export const genPreviewOperationsStyle = (token: ImageToken): CSSObject => {
|
||||
const { modalMaskBg, paddingSM, imagePreviewOperationDisabledColor } = token;
|
||||
const {
|
||||
previewCls,
|
||||
modalMaskBg,
|
||||
paddingSM,
|
||||
imagePreviewOperationDisabledColor,
|
||||
zIndexPopup,
|
||||
motionDurationSlow,
|
||||
} = token;
|
||||
|
||||
const operationBg = new TinyColor(modalMaskBg).setAlpha(0.1);
|
||||
const operationBgHover = operationBg.clone().setAlpha(0.2);
|
||||
|
||||
return {
|
||||
...resetComponent(token),
|
||||
position: 'absolute',
|
||||
insetBlockStart: 0,
|
||||
insetInlineEnd: 0,
|
||||
zIndex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'row-reverse',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
color: token.imagePreviewOperationColor,
|
||||
listStyle: 'none',
|
||||
background: new TinyColor(modalMaskBg).setAlpha(0.1).toRgbString(),
|
||||
pointerEvents: 'auto',
|
||||
[`${previewCls}-operations`]: {
|
||||
...resetComponent(token),
|
||||
position: 'fixed',
|
||||
insetBlockStart: 0,
|
||||
insetInlineEnd: 0,
|
||||
zIndex: zIndexPopup + 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'row-reverse',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
color: token.imagePreviewOperationColor,
|
||||
listStyle: 'none',
|
||||
background: operationBg.toRgbString(),
|
||||
pointerEvents: 'auto',
|
||||
|
||||
[`&-operation`]: {
|
||||
marginInlineStart: paddingSM,
|
||||
padding: paddingSM,
|
||||
cursor: 'pointer',
|
||||
'&-operation': {
|
||||
marginInlineStart: paddingSM,
|
||||
padding: paddingSM,
|
||||
cursor: 'pointer',
|
||||
transition: `all ${motionDurationSlow}`,
|
||||
|
||||
'&-disabled': {
|
||||
color: imagePreviewOperationDisabledColor,
|
||||
pointerEvents: 'none',
|
||||
'&:hover': {
|
||||
background: operationBgHover.toRgbString(),
|
||||
},
|
||||
|
||||
'&-disabled': {
|
||||
color: imagePreviewOperationDisabledColor,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
|
||||
'&:last-of-type': {
|
||||
marginInlineStart: 0,
|
||||
},
|
||||
},
|
||||
|
||||
'&:last-of-type': {
|
||||
marginInlineStart: 0,
|
||||
'&-progress': {
|
||||
position: 'absolute',
|
||||
left: { _skip_check_: true, value: '50%' },
|
||||
transform: 'translateX(-50%)',
|
||||
},
|
||||
},
|
||||
|
||||
[`&-progress`]: {
|
||||
position: 'absolute',
|
||||
left: { _skip_check_: true, value: '50%' },
|
||||
transform: 'translateX(-50%)',
|
||||
},
|
||||
|
||||
[`&-icon`]: {
|
||||
fontSize: token.imagePreviewOperationSize,
|
||||
'&-icon': {
|
||||
fontSize: token.imagePreviewOperationSize,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const genPreviewSwitchStyle = (token: ImageToken): CSSObject => {
|
||||
const { modalMaskBg, iconCls, imagePreviewOperationDisabledColor, previewCls } = token;
|
||||
const {
|
||||
modalMaskBg,
|
||||
iconCls,
|
||||
imagePreviewOperationDisabledColor,
|
||||
previewCls,
|
||||
zIndexPopup,
|
||||
motionDurationSlow,
|
||||
} = token;
|
||||
|
||||
const operationBg = new TinyColor(modalMaskBg).setAlpha(0.1);
|
||||
const operationBgHover = operationBg.clone().setAlpha(0.2);
|
||||
|
||||
return {
|
||||
position: 'absolute',
|
||||
insetBlockStart: '50%',
|
||||
zIndex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: token.imagePreviewSwitchSize,
|
||||
height: token.imagePreviewSwitchSize,
|
||||
marginTop: -token.imagePreviewSwitchSize / 2,
|
||||
color: token.imagePreviewOperationColor,
|
||||
background: new TinyColor(modalMaskBg).setAlpha(0.1).toRgbString(),
|
||||
borderRadius: '50%',
|
||||
cursor: 'pointer',
|
||||
pointerEvents: 'auto',
|
||||
[`${previewCls}-switch-left, ${previewCls}-switch-right`]: {
|
||||
position: 'fixed',
|
||||
insetBlockStart: '50%',
|
||||
zIndex: zIndexPopup + 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: token.imagePreviewSwitchSize,
|
||||
height: token.imagePreviewSwitchSize,
|
||||
marginTop: -token.imagePreviewSwitchSize / 2,
|
||||
color: token.imagePreviewOperationColor,
|
||||
background: operationBg.toRgbString(),
|
||||
borderRadius: '50%',
|
||||
transform: `translateY(-50%)`,
|
||||
cursor: 'pointer',
|
||||
transition: `all ${motionDurationSlow}`,
|
||||
pointerEvents: 'auto',
|
||||
|
||||
[`${previewCls}-disabled`]: {
|
||||
color: imagePreviewOperationDisabledColor,
|
||||
cursor: 'not-allowed',
|
||||
'&:hover': {
|
||||
background: operationBgHover.toRgbString(),
|
||||
},
|
||||
|
||||
[`&-disabled`]: {
|
||||
'&, &:hover': {
|
||||
color: imagePreviewOperationDisabledColor,
|
||||
background: 'transparent',
|
||||
cursor: 'not-allowed',
|
||||
[`> ${iconCls}`]: {
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
},
|
||||
},
|
||||
[`> ${iconCls}`]: {
|
||||
cursor: 'not-allowed',
|
||||
fontSize: token.imagePreviewOperationSize,
|
||||
},
|
||||
},
|
||||
[`> ${iconCls}`]: {
|
||||
fontSize: token.imagePreviewOperationSize,
|
||||
|
||||
[`${previewCls}-switch-left`]: {
|
||||
insetInlineStart: token.marginSM,
|
||||
},
|
||||
|
||||
[`${previewCls}-switch-right`]: {
|
||||
insetInlineEnd: token.marginSM,
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -177,22 +223,6 @@ export const genImagePreviewStyle: GenerateStyle<ImageToken> = (token: ImageToke
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
[`${previewCls}-operations`]: {
|
||||
...genPreviewOperationsStyle(token),
|
||||
},
|
||||
|
||||
[`${previewCls}-switch-left, ${previewCls}-switch-right`]: {
|
||||
...genPreviewSwitchStyle(token),
|
||||
},
|
||||
|
||||
[`${previewCls}-switch-left`]: {
|
||||
insetInlineStart: token.marginSM,
|
||||
},
|
||||
|
||||
[`${previewCls}-switch-right`]: {
|
||||
insetInlineEnd: token.marginSM,
|
||||
},
|
||||
},
|
||||
},
|
||||
// Override
|
||||
@ -203,6 +233,11 @@ export const genImagePreviewStyle: GenerateStyle<ImageToken> = (token: ImageToke
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Preview operations & switch
|
||||
{
|
||||
'&': [genPreviewOperationsStyle(token), genPreviewSwitchStyle(token)],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@ -244,6 +279,7 @@ const genPreviewMotion: GenerateStyle<ImageToken> = token => {
|
||||
|
||||
return {
|
||||
[`${previewCls}-root`]: initZoomMotion(token, 'zoom'),
|
||||
'&': initFadeMotion(token),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -731,7 +731,7 @@ exports[`renders ./components/input-number/demo/addon.md extend context correctl
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -11,13 +11,18 @@ import useRemovePasswordTimeout from './hooks/useRemovePasswordTimeout';
|
||||
import type { InputProps, InputRef } from './Input';
|
||||
import Input from './Input';
|
||||
|
||||
const defaultIconRender = (visible: boolean) =>
|
||||
const defaultIconRender = (visible: boolean): React.ReactNode =>
|
||||
visible ? <EyeOutlined /> : <EyeInvisibleOutlined />;
|
||||
|
||||
type VisibilityToggle = {
|
||||
visible?: boolean;
|
||||
onVisibleChange?: (visible: boolean) => void;
|
||||
};
|
||||
|
||||
export interface PasswordProps extends InputProps {
|
||||
readonly inputPrefixCls?: string;
|
||||
readonly action?: string;
|
||||
visibilityToggle?: boolean;
|
||||
visibilityToggle?: boolean | VisibilityToggle;
|
||||
iconRender?: (visible: boolean) => React.ReactNode;
|
||||
}
|
||||
|
||||
@ -27,9 +32,20 @@ const ActionMap: Record<string, string> = {
|
||||
};
|
||||
|
||||
const Password = React.forwardRef<InputRef, PasswordProps>((props, ref) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { visibilityToggle = true } = props;
|
||||
const visibilityControlled =
|
||||
typeof visibilityToggle === 'object' && visibilityToggle.visible !== undefined;
|
||||
const [visible, setVisible] = useState(() =>
|
||||
visibilityControlled ? visibilityToggle.visible! : false,
|
||||
);
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (visibilityControlled) {
|
||||
setVisible(visibilityToggle.visible!);
|
||||
}
|
||||
}, [visibilityControlled, visibilityToggle]);
|
||||
|
||||
// Remove Password value
|
||||
const removePasswordTimeout = useRemovePasswordTimeout(inputRef);
|
||||
|
||||
@ -41,7 +57,13 @@ const Password = React.forwardRef<InputRef, PasswordProps>((props, ref) => {
|
||||
if (visible) {
|
||||
removePasswordTimeout();
|
||||
}
|
||||
setVisible(prevState => !prevState);
|
||||
setVisible(prevState => {
|
||||
const newState = !prevState;
|
||||
if (typeof visibilityToggle === 'object') {
|
||||
visibilityToggle.onVisibleChange?.(newState);
|
||||
}
|
||||
return newState;
|
||||
});
|
||||
};
|
||||
|
||||
const getIcon = (prefixCls: string) => {
|
||||
@ -72,7 +94,6 @@ const Password = React.forwardRef<InputRef, PasswordProps>((props, ref) => {
|
||||
prefixCls: customizePrefixCls,
|
||||
inputPrefixCls: customizeInputPrefixCls,
|
||||
size,
|
||||
visibilityToggle = true,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
@ -85,7 +106,7 @@ const Password = React.forwardRef<InputRef, PasswordProps>((props, ref) => {
|
||||
});
|
||||
|
||||
const omittedProps: InputProps = {
|
||||
...omit(restProps, ['suffix', 'iconRender']),
|
||||
...omit(restProps, ['suffix', 'iconRender', 'visibilityToggle']),
|
||||
type: visible ? 'text' : 'password',
|
||||
className: inputClassName,
|
||||
prefixCls: inputPrefixCls,
|
||||
|
@ -4,7 +4,7 @@ import Input from '..';
|
||||
import focusTest from '../../../tests/shared/focusTest';
|
||||
import mountTest from '../../../tests/shared/mountTest';
|
||||
import rtlTest from '../../../tests/shared/rtlTest';
|
||||
import { fireEvent, render, sleep } from '../../../tests/utils';
|
||||
import { fireEvent, render, waitFakeTimer } from '../../../tests/utils';
|
||||
import Password from '../Password';
|
||||
|
||||
describe('Input.Password', () => {
|
||||
@ -81,16 +81,19 @@ describe('Input.Password', () => {
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/24526
|
||||
it('should not show value attribute in input element after blur it', async () => {
|
||||
jest.useFakeTimers();
|
||||
const { container } = render(<Input.Password />);
|
||||
fireEvent.change(container.querySelector('input')!, { target: { value: 'value' } });
|
||||
await sleep();
|
||||
await waitFakeTimer();
|
||||
expect(container.querySelector('input')?.getAttribute('value')).toBeFalsy();
|
||||
fireEvent.blur(container.querySelector('input')!);
|
||||
await sleep();
|
||||
await waitFakeTimer();
|
||||
expect(container.querySelector('input')?.getAttribute('value')).toBeFalsy();
|
||||
fireEvent.focus(container.querySelector('input')!);
|
||||
await sleep();
|
||||
await waitFakeTimer();
|
||||
expect(container.querySelector('input')?.getAttribute('value')).toBeFalsy();
|
||||
jest.clearAllTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/20541
|
||||
@ -104,21 +107,49 @@ describe('Input.Password', () => {
|
||||
|
||||
// https://github.com/ant-design/ant-design/pull/20544#issuecomment-569861679
|
||||
it('should not contain value attribute in input element with defaultValue', async () => {
|
||||
jest.useFakeTimers();
|
||||
const { container } = render(<Input.Password defaultValue="value" />);
|
||||
await sleep();
|
||||
await waitFakeTimer();
|
||||
expect(container.querySelector('input')?.getAttribute('value')).toBeFalsy();
|
||||
jest.clearAllTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should not show value attribute in input element after toggle visibility', async () => {
|
||||
jest.useFakeTimers();
|
||||
const { container } = render(<Input.Password />);
|
||||
fireEvent.change(container.querySelector('input')!, { target: { value: 'value' } });
|
||||
await sleep();
|
||||
await waitFakeTimer();
|
||||
expect(container.querySelector('input')?.getAttribute('value')).toBeFalsy();
|
||||
fireEvent.click(container.querySelector('.ant-input-password-icon')!);
|
||||
await sleep();
|
||||
await waitFakeTimer();
|
||||
expect(container.querySelector('input')?.getAttribute('value')).toBeTruthy();
|
||||
fireEvent.click(container.querySelector('.ant-input-password-icon')!);
|
||||
await sleep();
|
||||
await waitFakeTimer();
|
||||
expect(container.querySelector('input')?.getAttribute('value')).toBeFalsy();
|
||||
jest.clearAllTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should control password visible', () => {
|
||||
const { container, rerender } = render(<Input.Password visibilityToggle={{ visible: true }} />);
|
||||
expect(container.querySelectorAll('.anticon-eye').length).toBe(1);
|
||||
rerender(<Input.Password visibilityToggle={{ visible: false }} />);
|
||||
expect(container.querySelectorAll('.anticon-eye-invisible').length).toBe(1);
|
||||
});
|
||||
|
||||
it('should call onPasswordVisibleChange when visible is changed', () => {
|
||||
const handlePasswordVisibleChange = jest.fn();
|
||||
const { container, rerender } = render(
|
||||
<Input.Password visibilityToggle={{ onVisibleChange: handlePasswordVisibleChange }} />,
|
||||
);
|
||||
fireEvent.click(container.querySelector('.ant-input-password-icon')!);
|
||||
expect(handlePasswordVisibleChange).toHaveBeenCalledTimes(1);
|
||||
rerender(
|
||||
<Input.Password visibilityToggle={{ onVisibleChange: handlePasswordVisibleChange }} />,
|
||||
);
|
||||
expect(handlePasswordVisibleChange).toHaveBeenCalledTimes(1);
|
||||
fireEvent.click(container.querySelector('.ant-input-password-icon')!);
|
||||
expect(handlePasswordVisibleChange).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
@ -554,7 +554,7 @@ exports[`renders ./components/input/demo/addon.md extend context correctly 1`] =
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -3074,7 +3074,7 @@ Array [
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -8904,6 +8904,7 @@ exports[`renders ./components/input/demo/password-input.md extend context correc
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
style="margin-bottom:8px"
|
||||
>
|
||||
<span
|
||||
class="ant-input-affix-wrapper ant-input-password"
|
||||
@ -8943,6 +8944,69 @@ exports[`renders ./components/input/demo/password-input.md extend context correc
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<div
|
||||
class="ant-space ant-space-horizontal ant-space-align-center"
|
||||
>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
style="margin-right:8px"
|
||||
>
|
||||
<span
|
||||
class="ant-input-affix-wrapper ant-input-password"
|
||||
>
|
||||
<input
|
||||
class="ant-input"
|
||||
placeholder="input password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
class="ant-input-suffix"
|
||||
>
|
||||
<span
|
||||
aria-label="eye-invisible"
|
||||
class="anticon anticon-eye-invisible ant-input-password-icon"
|
||||
role="img"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="eye-invisible"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M942.2 486.2Q889.47 375.11 816.7 305l-50.88 50.88C807.31 395.53 843.45 447.4 874.7 512 791.5 684.2 673.4 766 512 766q-72.67 0-133.87-22.38L323 798.75Q408 838 512 838q288.3 0 430.2-300.3a60.29 60.29 0 000-51.5zm-63.57-320.64L836 122.88a8 8 0 00-11.32 0L715.31 232.2Q624.86 186 512 186q-288.3 0-430.2 300.3a60.3 60.3 0 000 51.5q56.69 119.4 136.5 191.41L112.48 835a8 8 0 000 11.31L155.17 889a8 8 0 0011.31 0l712.15-712.12a8 8 0 000-11.32zM149.3 512C232.6 339.8 350.7 258 512 258c54.54 0 104.13 9.36 149.12 28.39l-70.3 70.3a176 176 0 00-238.13 238.13l-83.42 83.42C223.1 637.49 183.3 582.28 149.3 512zm246.7 0a112.11 112.11 0 01146.2-106.69L401.31 546.2A112 112 0 01396 512z"
|
||||
/>
|
||||
<path
|
||||
d="M508 624c-3.46 0-6.87-.16-10.25-.47l-52.82 52.82a176.09 176.09 0 00227.42-227.42l-52.82 52.82c.31 3.38.47 6.79.47 10.25a111.94 111.94 0 01-112 112z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<button
|
||||
class="ant-btn ant-btn-default"
|
||||
style="width:80px"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Show
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
@ -2676,6 +2676,7 @@ exports[`renders ./components/input/demo/password-input.md correctly 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
style="margin-bottom:8px"
|
||||
>
|
||||
<span
|
||||
class="ant-input-affix-wrapper ant-input-password"
|
||||
@ -2715,6 +2716,69 @@ exports[`renders ./components/input/demo/password-input.md correctly 1`] = `
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<div
|
||||
class="ant-space ant-space-horizontal ant-space-align-center"
|
||||
>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
style="margin-right:8px"
|
||||
>
|
||||
<span
|
||||
class="ant-input-affix-wrapper ant-input-password"
|
||||
>
|
||||
<input
|
||||
class="ant-input"
|
||||
placeholder="input password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
class="ant-input-suffix"
|
||||
>
|
||||
<span
|
||||
aria-label="eye-invisible"
|
||||
class="anticon anticon-eye-invisible ant-input-password-icon"
|
||||
role="img"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="eye-invisible"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M942.2 486.2Q889.47 375.11 816.7 305l-50.88 50.88C807.31 395.53 843.45 447.4 874.7 512 791.5 684.2 673.4 766 512 766q-72.67 0-133.87-22.38L323 798.75Q408 838 512 838q288.3 0 430.2-300.3a60.29 60.29 0 000-51.5zm-63.57-320.64L836 122.88a8 8 0 00-11.32 0L715.31 232.2Q624.86 186 512 186q-288.3 0-430.2 300.3a60.3 60.3 0 000 51.5q56.69 119.4 136.5 191.41L112.48 835a8 8 0 000 11.31L155.17 889a8 8 0 0011.31 0l712.15-712.12a8 8 0 000-11.32zM149.3 512C232.6 339.8 350.7 258 512 258c54.54 0 104.13 9.36 149.12 28.39l-70.3 70.3a176 176 0 00-238.13 238.13l-83.42 83.42C223.1 637.49 183.3 582.28 149.3 512zm246.7 0a112.11 112.11 0 01146.2-106.69L401.31 546.2A112 112 0 01396 512z"
|
||||
/>
|
||||
<path
|
||||
d="M508 624c-3.46 0-6.87-.16-10.25-.47l-52.82 52.82a176.09 176.09 0 00227.42-227.42l-52.82 52.82c.31 3.38.47 6.79.47 10.25a111.94 111.94 0 01-112 112z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<button
|
||||
class="ant-btn ant-btn-default"
|
||||
style="width:80px"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Show
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
@ -3,14 +3,7 @@ import type { ChangeEventHandler, TextareaHTMLAttributes } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import Input from '..';
|
||||
import focusTest from '../../../tests/shared/focusTest';
|
||||
import {
|
||||
fireEvent,
|
||||
waitFakeTimer,
|
||||
render,
|
||||
sleep,
|
||||
triggerResize,
|
||||
pureRender,
|
||||
} from '../../../tests/utils';
|
||||
import { fireEvent, waitFakeTimer, render, triggerResize, pureRender } from '../../../tests/utils';
|
||||
import type { RenderOptions } from '../../../tests/utils';
|
||||
import type { TextAreaRef } from '../TextArea';
|
||||
|
||||
@ -439,6 +432,7 @@ describe('TextArea allowClear', () => {
|
||||
});
|
||||
|
||||
it('scroll to bottom when autoSize', async () => {
|
||||
jest.useFakeTimers();
|
||||
const ref = React.createRef<TextAreaRef>();
|
||||
const { container, unmount } = render(<Input.TextArea ref={ref} autoSize />, {
|
||||
container: document.body,
|
||||
@ -454,9 +448,11 @@ describe('TextArea allowClear', () => {
|
||||
fireEvent.input(container.querySelector('textarea')!, { target: { value: '\n1' } });
|
||||
const target = ref.current?.resizableTextArea?.textArea!;
|
||||
triggerResize(target);
|
||||
await sleep(100);
|
||||
await waitFakeTimer();
|
||||
expect(setSelectionRangeFn).toHaveBeenCalled();
|
||||
unmount();
|
||||
jest.clearAllTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/26308
|
||||
|
@ -15,18 +15,31 @@ Input type of password.
|
||||
|
||||
```tsx
|
||||
import { EyeInvisibleOutlined, EyeTwoTone } from '@ant-design/icons';
|
||||
import { Input, Space } from 'antd';
|
||||
import { Button, Input, Space } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const App: React.FC = () => (
|
||||
<Space direction="vertical">
|
||||
<Input.Password placeholder="input password" />
|
||||
<Input.Password
|
||||
placeholder="input password"
|
||||
iconRender={visible => (visible ? <EyeTwoTone /> : <EyeInvisibleOutlined />)}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
const App: React.FC = () => {
|
||||
const [passwordVisible, setPasswordVisible] = React.useState(false);
|
||||
|
||||
return (
|
||||
<Space direction="vertical">
|
||||
<Input.Password placeholder="input password" />
|
||||
<Input.Password
|
||||
placeholder="input password"
|
||||
iconRender={visible => (visible ? <EyeTwoTone /> : <EyeInvisibleOutlined />)}
|
||||
/>
|
||||
<Space direction="horizontal">
|
||||
<Input.Password
|
||||
placeholder="input password"
|
||||
visibilityToggle={{ visible: passwordVisible, onVisibleChange: setPasswordVisible }}
|
||||
/>
|
||||
<Button style={{ width: 80 }} onClick={() => setPasswordVisible(prevState => !prevState)}>
|
||||
{passwordVisible ? 'Hide' : 'Show'}
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
@ -85,7 +85,14 @@ Supports all props of `Input`.
|
||||
| Property | Description | Type | Default | Version |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| iconRender | Custom toggle button | (visible) => ReactNode | (visible) => (visible ? <EyeOutlined /> : <EyeInvisibleOutlined />) | 4.3.0 |
|
||||
| visibilityToggle | Whether show toggle button | boolean | true | |
|
||||
| visibilityToggle | Whether show toggle button or control password visible | boolean \| [VisibilityToggle](#VisibilityToggle) | true | |
|
||||
|
||||
#### VisibilityToggle
|
||||
|
||||
| Property | Description | Type | Default | Version |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| visible | Whether the password is show or hide | boolean | false | 4.24.0 |
|
||||
| onVisibleChange | Callback executed when visibility of the password is changed | boolean | - | 4.24.0 |
|
||||
|
||||
#### Input Methods
|
||||
|
||||
|
@ -86,7 +86,14 @@ Input 的其他属性和 React 自带的 [input](https://reactjs.org/docs/dom-el
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| iconRender | 自定义切换按钮 | (visible) => ReactNode | (visible) => (visible ? <EyeOutlined /> : <EyeInvisibleOutlined />) | 4.3.0 |
|
||||
| visibilityToggle | 是否显示切换按钮 | boolean | true | |
|
||||
| visibilityToggle | 是否显示切换按钮或者控制密码显隐 | boolean \| [VisibilityToggle](#VisibilityToggle) | true | |
|
||||
|
||||
#### VisibilityToggle
|
||||
|
||||
| Property | Description | Type | Default | Version |
|
||||
| --------------- | -------------------- | ------- | ------- | ------- |
|
||||
| visible | 用于手动控制密码显隐 | boolean | false | 4.24 |
|
||||
| onVisibleChange | 显隐密码的回调 | boolean | - | 4.24 |
|
||||
|
||||
#### Input Methods
|
||||
|
||||
|
@ -1085,7 +1085,7 @@ exports[`renders ./components/list/demo/infinite-load.md extend context correctl
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -2150,7 +2150,6 @@ exports[`renders ./components/list/demo/vertical.md extend context correctly 1`]
|
||||
>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
|
@ -1085,7 +1085,7 @@ exports[`renders ./components/list/demo/infinite-load.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -2150,7 +2150,6 @@ exports[`renders ./components/list/demo/vertical.md correctly 1`] = `
|
||||
>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
|
@ -55,7 +55,7 @@ exports[`List renders empty list 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -55,7 +55,7 @@ exports[`List rtl render component should be rendered correctly in RTL direction
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -31,7 +31,6 @@ exports[`List.pagination renders pagination correctly 1`] = `
|
||||
>
|
||||
<ul
|
||||
class="ant-pagination my-page"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -127,7 +126,6 @@ exports[`List.pagination renders pagination correctly 1`] = `
|
||||
exports[`List.pagination should change page size work 1`] = `
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -277,7 +275,6 @@ exports[`List.pagination should change page size work 1`] = `
|
||||
exports[`List.pagination should change page size work 2`] = `
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -546,7 +543,6 @@ exports[`List.pagination should change page size work 2`] = `
|
||||
exports[`List.pagination should default work 1`] = `
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="false"
|
||||
|
@ -1882,7 +1882,6 @@ exports[`Locale Provider should display the text as ar 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -6986,7 +6985,6 @@ exports[`Locale Provider should display the text as az 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -10674,7 +10672,7 @@ exports[`Locale Provider should display the text as az 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -10899,7 +10897,7 @@ exports[`Locale Provider should display the text as az 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -11970,7 +11968,7 @@ exports[`Locale Provider should display the text as az 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@ -12090,7 +12088,6 @@ exports[`Locale Provider should display the text as bg 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -17194,7 +17191,6 @@ exports[`Locale Provider should display the text as bn-bd 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -22298,7 +22294,6 @@ exports[`Locale Provider should display the text as by 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -27402,7 +27397,6 @@ exports[`Locale Provider should display the text as ca 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -32506,7 +32500,6 @@ exports[`Locale Provider should display the text as cs 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -37610,7 +37603,6 @@ exports[`Locale Provider should display the text as da 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -42714,7 +42706,6 @@ exports[`Locale Provider should display the text as de 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -47818,7 +47809,6 @@ exports[`Locale Provider should display the text as el 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -52922,7 +52912,6 @@ exports[`Locale Provider should display the text as en 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -56610,7 +56599,7 @@ exports[`Locale Provider should display the text as en 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -56835,7 +56824,7 @@ exports[`Locale Provider should display the text as en 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -57906,7 +57895,7 @@ exports[`Locale Provider should display the text as en 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@ -58026,7 +58015,6 @@ exports[`Locale Provider should display the text as en-gb 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -63130,7 +63118,6 @@ exports[`Locale Provider should display the text as es 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -68234,7 +68221,6 @@ exports[`Locale Provider should display the text as et 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -73338,7 +73324,6 @@ exports[`Locale Provider should display the text as fa 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -78442,7 +78427,6 @@ exports[`Locale Provider should display the text as fi 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -83546,7 +83530,6 @@ exports[`Locale Provider should display the text as fr 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -88650,7 +88633,6 @@ exports[`Locale Provider should display the text as fr 2`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -93754,7 +93736,6 @@ exports[`Locale Provider should display the text as fr 3`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -98858,7 +98839,6 @@ exports[`Locale Provider should display the text as ga 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -103962,7 +103942,6 @@ exports[`Locale Provider should display the text as gl 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -109066,7 +109045,6 @@ exports[`Locale Provider should display the text as he 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -114170,7 +114148,6 @@ exports[`Locale Provider should display the text as hi 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -119274,7 +119251,6 @@ exports[`Locale Provider should display the text as hr 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -124378,7 +124354,6 @@ exports[`Locale Provider should display the text as hu 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -129482,7 +129457,6 @@ exports[`Locale Provider should display the text as hy-am 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -134586,7 +134560,6 @@ exports[`Locale Provider should display the text as id 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -139690,7 +139663,6 @@ exports[`Locale Provider should display the text as is 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -144794,7 +144766,6 @@ exports[`Locale Provider should display the text as it 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -149898,7 +149869,6 @@ exports[`Locale Provider should display the text as ja 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -155002,7 +154972,6 @@ exports[`Locale Provider should display the text as ka 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -160106,7 +160075,6 @@ exports[`Locale Provider should display the text as kk 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -165210,7 +165178,6 @@ exports[`Locale Provider should display the text as km 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -170312,7 +170279,6 @@ exports[`Locale Provider should display the text as kn 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -174000,7 +173966,7 @@ exports[`Locale Provider should display the text as kn 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -174225,7 +174191,7 @@ exports[`Locale Provider should display the text as kn 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -175296,7 +175262,7 @@ exports[`Locale Provider should display the text as kn 1`] = `
|
||||
<div
|
||||
class="ant-empty-description"
|
||||
>
|
||||
No Data
|
||||
No data
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@ -175416,7 +175382,6 @@ exports[`Locale Provider should display the text as ko 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -180520,7 +180485,6 @@ exports[`Locale Provider should display the text as ku 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -185624,7 +185588,6 @@ exports[`Locale Provider should display the text as ku-iq 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -190728,7 +190691,6 @@ exports[`Locale Provider should display the text as lt 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -195832,7 +195794,6 @@ exports[`Locale Provider should display the text as lv 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -200936,7 +200897,6 @@ exports[`Locale Provider should display the text as mk 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -206040,7 +206000,6 @@ exports[`Locale Provider should display the text as ml 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -211144,7 +211103,6 @@ exports[`Locale Provider should display the text as mn-mn 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -216248,7 +216206,6 @@ exports[`Locale Provider should display the text as ms-my 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -221352,7 +221309,6 @@ exports[`Locale Provider should display the text as nb 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -226456,7 +226412,6 @@ exports[`Locale Provider should display the text as ne-np 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -231560,7 +231515,6 @@ exports[`Locale Provider should display the text as nl 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -236664,7 +236618,6 @@ exports[`Locale Provider should display the text as nl-be 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -241768,7 +241721,6 @@ exports[`Locale Provider should display the text as pl 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -246872,7 +246824,6 @@ exports[`Locale Provider should display the text as pt 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -251976,7 +251927,6 @@ exports[`Locale Provider should display the text as pt-br 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -257080,7 +257030,6 @@ exports[`Locale Provider should display the text as ro 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -262184,7 +262133,6 @@ exports[`Locale Provider should display the text as ru 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -267288,7 +267236,6 @@ exports[`Locale Provider should display the text as si 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -272392,7 +272339,6 @@ exports[`Locale Provider should display the text as sk 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -277496,7 +277442,6 @@ exports[`Locale Provider should display the text as sl 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -282600,7 +282545,6 @@ exports[`Locale Provider should display the text as sr 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -287704,7 +287648,6 @@ exports[`Locale Provider should display the text as sv 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -292808,7 +292751,6 @@ exports[`Locale Provider should display the text as ta 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -297912,7 +297854,6 @@ exports[`Locale Provider should display the text as th 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -303016,7 +302957,6 @@ exports[`Locale Provider should display the text as tk 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -308120,7 +308060,6 @@ exports[`Locale Provider should display the text as tr 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -313224,7 +313163,6 @@ exports[`Locale Provider should display the text as uk 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -318328,7 +318266,6 @@ exports[`Locale Provider should display the text as ur 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -323432,7 +323369,6 @@ exports[`Locale Provider should display the text as vi 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -328536,7 +328472,6 @@ exports[`Locale Provider should display the text as zh-cn 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -333640,7 +333575,6 @@ exports[`Locale Provider should display the text as zh-hk 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -338744,7 +338678,6 @@ exports[`Locale Provider should display the text as zh-tw 1`] = `
|
||||
<div>
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
|
@ -28,11 +28,13 @@ const CacheOuter: React.FC = memo(() => (
|
||||
</LocaleProvider>
|
||||
</>
|
||||
));
|
||||
|
||||
it("Rendering on LocaleProvider won't trigger rendering on child component.", () => {
|
||||
const { container } = pureRender(<CacheOuter />);
|
||||
const { container, unmount } = pureRender(<CacheOuter />);
|
||||
expect(outerCount).toBe(0);
|
||||
expect(innerCount).toBe(1);
|
||||
fireEvent.click(container.querySelector('#parent_btn')!);
|
||||
expect(outerCount).toBe(1);
|
||||
expect(innerCount).toBe(1);
|
||||
unmount();
|
||||
});
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Modal } from '../..';
|
||||
import { sleep, render, fireEvent } from '../../../tests/utils';
|
||||
import { waitFakeTimer, render, fireEvent } from '../../../tests/utils';
|
||||
import ConfigProvider from '../../config-provider';
|
||||
import zhCN from '../zh_CN';
|
||||
|
||||
@ -46,16 +45,10 @@ describe('Locale Provider demo', () => {
|
||||
const { container } = render(<BasicExample />);
|
||||
|
||||
fireEvent.click(container.querySelector('.about')!);
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
fireEvent.click(container.querySelector('.dashboard')!);
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
expect(document.body.querySelectorAll('.ant-btn-primary span')[0]?.textContent).toBe('确 定');
|
||||
Modal.destroyAll();
|
||||
|
@ -69,7 +69,7 @@ const localeValues: Locale = {
|
||||
downloadFile: 'Download file',
|
||||
},
|
||||
Empty: {
|
||||
description: 'No Data',
|
||||
description: 'No data',
|
||||
},
|
||||
Icon: {
|
||||
icon: 'icon',
|
||||
|
@ -1,55 +1,42 @@
|
||||
import React, { memo, useContext, useRef, useState } from 'react';
|
||||
import React, { memo, useContext } from 'react';
|
||||
import Menu from '../index';
|
||||
import MenuContext from '../MenuContext';
|
||||
import { render, fireEvent } from '../../../tests/utils';
|
||||
import { fireEvent, pureRender } from '../../../tests/utils';
|
||||
|
||||
// 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(MenuContext);
|
||||
return (
|
||||
<div>
|
||||
Child Rendering Count: <span id="child_count">{countRef.current}</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
let innerCount = 0;
|
||||
let outerCount = 0;
|
||||
|
||||
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 'Menu#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>
|
||||
<Menu>
|
||||
<Menu.Item key="test">
|
||||
<CacheInner />
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
const handleClick = () => {
|
||||
outerCount++;
|
||||
};
|
||||
|
||||
// we use'memo' here in order to only render inner component while context changed.
|
||||
const CacheInner: React.FC = memo(() => {
|
||||
innerCount++;
|
||||
// subscribe locale context
|
||||
useContext(MenuContext);
|
||||
return null;
|
||||
});
|
||||
|
||||
const CacheOuter: React.FC = memo(() => (
|
||||
<>
|
||||
<button type="button" onClick={handleClick} id="parent_btn">
|
||||
Click
|
||||
</button>
|
||||
<Menu>
|
||||
<Menu.Item key="test">
|
||||
<CacheInner />
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</>
|
||||
));
|
||||
|
||||
it("Rendering on Menu without changed MenuContext won't trigger rendering on child component.", () => {
|
||||
const { container, unmount } = render(<CacheOuter />);
|
||||
const childCount = container.querySelector('#child_count')?.textContent;
|
||||
const { container, unmount } = pureRender(<CacheOuter />);
|
||||
expect(outerCount).toBe(0);
|
||||
expect(innerCount).toBe(1);
|
||||
fireEvent.click(container.querySelector('#parent_btn')!);
|
||||
expect(container.querySelector('#parent_count')?.textContent).toBe('2');
|
||||
// child component won't rerender
|
||||
expect(container.querySelector('#child_count')?.textContent).toBe(childCount);
|
||||
fireEvent.click(container.querySelector('#parent_btn')!);
|
||||
expect(container.querySelector('#parent_count')?.textContent).toBe('3');
|
||||
// child component won't rerender
|
||||
expect(container.querySelector('#child_count')?.textContent).toBe(childCount);
|
||||
// in order to depress warning "Warning: An update to Menu inside a test was not wrapped in act(...)."
|
||||
expect(outerCount).toBe(1);
|
||||
expect(innerCount).toBe(1);
|
||||
unmount();
|
||||
});
|
||||
|
@ -261,7 +261,6 @@ const getBaseStyle: GenerateStyle<MenuToken> = token => {
|
||||
|
||||
a: {
|
||||
color: 'inherit !important',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
|
||||
[`> ${componentCls}-submenu-title`]: {
|
||||
|
@ -7,7 +7,7 @@ import * as React from 'react';
|
||||
import TestUtils from 'react-dom/test-utils';
|
||||
import type { ModalFuncProps } from '..';
|
||||
import Modal from '..';
|
||||
import { sleep, act } from '../../../tests/utils';
|
||||
import { waitFakeTimer, act } from '../../../tests/utils';
|
||||
import ConfigProvider from '../../config-provider';
|
||||
import type { ModalFunc } from '../confirm';
|
||||
import destroyFns from '../destroyFns';
|
||||
@ -56,16 +56,21 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
};
|
||||
/* eslint-enable */
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllTimers();
|
||||
errorSpy.mockReset();
|
||||
Modal.destroyAll();
|
||||
|
||||
await sleep();
|
||||
await waitFakeTimer();
|
||||
document.body.innerHTML = '';
|
||||
jest.clearAllTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
|
||||
@ -73,41 +78,32 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
return document.body.querySelectorAll<HTMLElement>(className);
|
||||
}
|
||||
|
||||
function open(args?: ModalFuncProps) {
|
||||
jest.useFakeTimers();
|
||||
async function open(args?: ModalFuncProps) {
|
||||
confirm({
|
||||
title: 'Want to delete these items?',
|
||||
content: 'some descriptions',
|
||||
...args,
|
||||
});
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
jest.useRealTimers();
|
||||
|
||||
await waitFakeTimer();
|
||||
}
|
||||
|
||||
it('should not render title when title not defined', () => {
|
||||
jest.useFakeTimers();
|
||||
it('should not render title when title not defined', async () => {
|
||||
confirm({
|
||||
content: 'some descriptions',
|
||||
});
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
expect(document.querySelector('.ant-modal-confirm-title')).toBe(null);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('trigger onCancel once when click on cancel button', async () => {
|
||||
const onCancel = jest.fn();
|
||||
const onOk = jest.fn();
|
||||
open({
|
||||
await open({
|
||||
onCancel,
|
||||
onOk,
|
||||
});
|
||||
|
||||
// first Modal
|
||||
await sleep();
|
||||
$$('.ant-btn')[0].click();
|
||||
expect(onCancel.mock.calls.length).toBe(1);
|
||||
expect(onOk.mock.calls.length).toBe(0);
|
||||
@ -116,21 +112,18 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
it('trigger onOk once when click on ok button', async () => {
|
||||
const onCancel = jest.fn();
|
||||
const onOk = jest.fn();
|
||||
open({
|
||||
await open({
|
||||
onCancel,
|
||||
onOk,
|
||||
});
|
||||
|
||||
// second Modal
|
||||
await sleep();
|
||||
$$('.ant-btn-primary')[0].click();
|
||||
expect(onCancel.mock.calls.length).toBe(0);
|
||||
expect(onOk.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should allow Modal.confirm without onCancel been set', async () => {
|
||||
open();
|
||||
await sleep();
|
||||
await open();
|
||||
|
||||
// Third Modal
|
||||
$$('.ant-btn')[0].click();
|
||||
@ -138,17 +131,14 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
});
|
||||
|
||||
it('should allow Modal.confirm without onOk been set', async () => {
|
||||
open();
|
||||
await open();
|
||||
|
||||
// Fourth Modal
|
||||
await sleep();
|
||||
$$('.ant-btn-primary')[0].click();
|
||||
expect(errorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should close confirm modal when press ESC', async () => {
|
||||
jest.useFakeTimers();
|
||||
jest.clearAllTimers();
|
||||
const onCancel = jest.fn();
|
||||
Modal.confirm({
|
||||
title: 'title',
|
||||
@ -156,100 +146,68 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
onCancel,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
await sleep();
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
expect($$(`.ant-modal-confirm-confirm`)).toHaveLength(1);
|
||||
TestUtils.Simulate.keyDown($$('.ant-modal')[0], {
|
||||
keyCode: KeyCode.ESC,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
await sleep(0);
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
await waitFakeTimer(0);
|
||||
|
||||
expect($$(`.ant-modal-confirm-confirm`)).toHaveLength(0);
|
||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should not hide confirm when onOk return Promise.resolve', async () => {
|
||||
open({
|
||||
await open({
|
||||
onOk: () => Promise.resolve(''),
|
||||
});
|
||||
|
||||
await sleep();
|
||||
$$('.ant-btn-primary')[0].click();
|
||||
expect($$('.ant-modal-confirm')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should emit error when onOk return Promise.reject', async () => {
|
||||
const error = new Error('something wrong');
|
||||
open({
|
||||
await open({
|
||||
onOk: () => Promise.reject(error),
|
||||
});
|
||||
|
||||
await sleep();
|
||||
$$('.ant-btn-primary')[0].click();
|
||||
|
||||
// wait promise
|
||||
await sleep();
|
||||
await waitFakeTimer();
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith(error);
|
||||
});
|
||||
|
||||
it('shows animation when close', async () => {
|
||||
open();
|
||||
|
||||
jest.useFakeTimers();
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
await sleep();
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
await open();
|
||||
|
||||
expect($$('.ant-modal-confirm')).toHaveLength(1);
|
||||
|
||||
await sleep();
|
||||
await waitFakeTimer();
|
||||
|
||||
$$('.ant-btn')[0].click();
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
await sleep();
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
expect($$('.ant-modal-confirm')).toHaveLength(0);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('ok only', async () => {
|
||||
open({ okCancel: false });
|
||||
await sleep();
|
||||
await open({ okCancel: false });
|
||||
expect($$('.ant-btn')).toHaveLength(1);
|
||||
expect($$('.ant-btn')[0].innerHTML).toContain('OK');
|
||||
});
|
||||
|
||||
it('allows extra props on buttons', async () => {
|
||||
open({
|
||||
await open({
|
||||
okButtonProps: { disabled: true },
|
||||
cancelButtonProps: { 'data-test': 'baz' } as ModalFuncProps['cancelButtonProps'],
|
||||
});
|
||||
|
||||
await sleep();
|
||||
expect($$('.ant-btn')).toHaveLength(2);
|
||||
expect(($$('.ant-btn')[0].attributes as any)['data-test'].value).toBe('baz');
|
||||
expect(($$('.ant-btn')[1] as HTMLButtonElement).disabled).toBe(true);
|
||||
@ -258,29 +216,18 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
describe('should close modals when click confirm button', () => {
|
||||
(['info', 'success', 'warning', 'error'] as const).forEach(type => {
|
||||
it(type, async () => {
|
||||
jest.useFakeTimers();
|
||||
Modal[type]?.({ title: 'title', content: 'content' });
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
jest.runAllTimers();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1);
|
||||
|
||||
$$('.ant-btn')[0].click();
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
jest.runAllTimers();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(0);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should close confirm modal when click cancel button', async () => {
|
||||
jest.useFakeTimers();
|
||||
const onCancel = jest.fn();
|
||||
Modal.confirm({
|
||||
// test legacy visible
|
||||
@ -289,22 +236,15 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
content: 'content',
|
||||
onCancel,
|
||||
});
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
expect($$(`.ant-modal-confirm-confirm`)).toHaveLength(1);
|
||||
$$('.ant-btn')[0].click();
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
expect($$(`.ant-modal-confirm-confirm`)).toHaveLength(0);
|
||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should close confirm modal when click close button', async () => {
|
||||
jest.useFakeTimers();
|
||||
const onCancel = jest.fn();
|
||||
Modal.confirm({
|
||||
title: 'title',
|
||||
@ -313,42 +253,28 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
closeIcon: 'X',
|
||||
onCancel,
|
||||
});
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
expect($$(`.ant-modal-close`)).toHaveLength(1);
|
||||
$$('.ant-btn')[0].click();
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
expect($$(`.ant-modal-close`)).toHaveLength(0);
|
||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('should not close modals when click confirm button when onOk has argument', () => {
|
||||
(['confirm', 'info', 'success', 'warning', 'error'] as const).forEach(type => {
|
||||
it(type, async () => {
|
||||
jest.useFakeTimers();
|
||||
Modal[type]?.({
|
||||
title: 'title',
|
||||
content: 'content',
|
||||
onOk: _ => null, // eslint-disable-line no-unused-vars
|
||||
});
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1);
|
||||
$$('.ant-btn-primary')[0].click();
|
||||
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -356,15 +282,11 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
describe('could be update by new config', () => {
|
||||
(['info', 'success', 'warning', 'error'] as const).forEach(type => {
|
||||
it(type, async () => {
|
||||
jest.useFakeTimers();
|
||||
const instance = Modal[type]?.({
|
||||
title: 'title',
|
||||
content: 'content',
|
||||
});
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1);
|
||||
expect($$('.ant-modal-confirm-title')[0].innerHTML).toBe('title');
|
||||
expect($$('.ant-modal-confirm-content')[0].innerHTML).toBe('content');
|
||||
@ -373,33 +295,25 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
content: 'new content',
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1);
|
||||
expect($$('.ant-modal-confirm-title')[0].innerHTML).toBe('new title');
|
||||
expect($$('.ant-modal-confirm-content')[0].innerHTML).toBe('new content');
|
||||
instance.destroy();
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
jest.useRealTimers();
|
||||
await waitFakeTimer();
|
||||
expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('could be update by call function', () => {
|
||||
(['info', 'success', 'warning', 'error'] as const).forEach(type => {
|
||||
it(type, () => {
|
||||
jest.useFakeTimers();
|
||||
it(type, async () => {
|
||||
const instance = Modal[type]?.({
|
||||
title: 'title',
|
||||
okButtonProps: { loading: true, style: { color: 'red' } },
|
||||
});
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1);
|
||||
expect($$('.ant-modal-confirm-title')[0].innerHTML).toBe('title');
|
||||
expect($$('.ant-modal-confirm-btns .ant-btn-primary')[0].classList).toContain(
|
||||
@ -413,9 +327,7 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
loading: false,
|
||||
},
|
||||
}));
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1);
|
||||
expect($$('.ant-modal-confirm-title')[0].innerHTML).toBe('title');
|
||||
expect($$('.ant-modal-confirm-btns .ant-btn-primary')[0].classList).not.toContain(
|
||||
@ -423,42 +335,31 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
);
|
||||
expect($$('.ant-modal-confirm-btns .ant-btn-primary')[0].style.color).toBe('red');
|
||||
instance.destroy();
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
jest.useRealTimers();
|
||||
|
||||
await waitFakeTimer();
|
||||
expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('could be destroy', () => {
|
||||
(['info', 'success', 'warning', 'error'] as const).forEach(type => {
|
||||
jest.useFakeTimers();
|
||||
it(type, async () => {
|
||||
const instance = Modal[type]?.({
|
||||
title: 'title',
|
||||
content: 'content',
|
||||
});
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1);
|
||||
|
||||
instance.destroy();
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(0);
|
||||
});
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
it('could be Modal.destroyAll', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
// Show
|
||||
(['info', 'success', 'warning', 'error'] as const).forEach(type => {
|
||||
Modal[type]?.({
|
||||
@ -467,10 +368,7 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
['info', 'success', 'warning', 'error'].forEach(type => {
|
||||
expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1);
|
||||
@ -479,39 +377,31 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
// Destroy
|
||||
Modal.destroyAll();
|
||||
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
['info', 'success', 'warning', 'error'].forEach(type => {
|
||||
expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(0);
|
||||
});
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('prefixCls', async () => {
|
||||
open({ prefixCls: 'custom-modal' });
|
||||
await open({ prefixCls: 'custom-modal' });
|
||||
|
||||
await sleep();
|
||||
expect($$('.custom-modal-mask')).toHaveLength(1);
|
||||
expect($$('.custom-modal-wrap')).toHaveLength(1);
|
||||
expect($$('.custom-modal-confirm')).toHaveLength(1);
|
||||
expect($$('.custom-modal-confirm-body-wrapper')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should be Modal.confirm without mask', () => {
|
||||
open({ mask: false });
|
||||
it('should be Modal.confirm without mask', async () => {
|
||||
await open({ mask: false });
|
||||
expect($$('.ant-modal-mask')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('destroyFns should reduce when instance.destroy', () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
it('destroyFns should reduce when instance.destroy', async () => {
|
||||
Modal.destroyAll(); // clear destroyFns
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
await waitFakeTimer();
|
||||
|
||||
const instances: ReturnType<ModalFunc>[] = [];
|
||||
(['info', 'success', 'warning', 'error'] as const).forEach(type => {
|
||||
@ -537,22 +427,16 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
});
|
||||
expect(destroyFns.length).toBe(length - index - 1);
|
||||
});
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should warning when pass a string as icon props', async () => {
|
||||
jest.useFakeTimers();
|
||||
const warnSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
confirm({
|
||||
content: 'some descriptions',
|
||||
icon: 'ab',
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
confirm({
|
||||
@ -560,16 +444,12 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
icon: 'question',
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
`Warning: [antd: Modal] \`icon\` is using ReactNode instead of string naming in v4. Please check \`question\` at https://ant.design/components/icon`,
|
||||
);
|
||||
warnSpy.mockRestore();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('icon can be null to hide icon', async () => {
|
||||
@ -580,10 +460,7 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
icon: null,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
// We check icon is not exist in the body
|
||||
expect(document.querySelector('.ant-modal-confirm-body')!.children).toHaveLength(2);
|
||||
@ -596,9 +473,8 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
|
||||
it('ok button should trigger onOk once when click it many times quickly', async () => {
|
||||
const onOk = jest.fn();
|
||||
open({ onOk });
|
||||
await open({ onOk });
|
||||
|
||||
await sleep();
|
||||
$$('.ant-btn-primary')[0].click();
|
||||
$$('.ant-btn-primary')[0].click();
|
||||
expect(onOk).toHaveBeenCalledTimes(1);
|
||||
@ -607,7 +483,7 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
// https://github.com/ant-design/ant-design/issues/23358
|
||||
it('ok button should trigger onOk multiple times when onOk has close argument', async () => {
|
||||
const onOk = jest.fn();
|
||||
open({
|
||||
await open({
|
||||
onOk(close?: any) {
|
||||
onOk();
|
||||
// @ts-ignore
|
||||
@ -615,7 +491,6 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await sleep();
|
||||
$$('.ant-btn-primary')[0].click();
|
||||
$$('.ant-btn-primary')[0].click();
|
||||
$$('.ant-btn-primary')[0].click();
|
||||
@ -623,28 +498,21 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
});
|
||||
|
||||
it('should be able to global config rootPrefixCls', async () => {
|
||||
jest.useFakeTimers();
|
||||
ConfigProvider.config({ prefixCls: 'my', iconPrefixCls: 'bamboo' });
|
||||
confirm({ title: 'title', icon: <SmileOutlined /> });
|
||||
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
expect(document.querySelectorAll('.ant-btn').length).toBe(0);
|
||||
expect(document.querySelectorAll('.my-btn').length).toBe(2);
|
||||
expect(document.querySelectorAll('.bamboo-smile').length).toBe(1);
|
||||
expect(document.querySelectorAll('.my-modal-confirm').length).toBe(1);
|
||||
ConfigProvider.config({ prefixCls: 'ant', iconPrefixCls: undefined });
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should be able to config rootPrefixCls', async () => {
|
||||
resetWarned();
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
Modal.config({
|
||||
rootPrefixCls: 'my',
|
||||
});
|
||||
@ -656,10 +524,7 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
title: 'title',
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
expect(document.querySelectorAll('.ant-btn').length).toBe(0);
|
||||
expect(document.querySelectorAll('.my-btn').length).toBe(2);
|
||||
@ -671,10 +536,7 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
title: 'title',
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
expect(document.querySelectorAll('.ant-btn').length).toBe(0);
|
||||
expect(document.querySelectorAll('.my-btn').length).toBe(2);
|
||||
@ -684,40 +546,36 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
Modal.config({
|
||||
rootPrefixCls: '',
|
||||
});
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('trigger afterClose once when click on cancel button', async () => {
|
||||
const afterClose = jest.fn();
|
||||
open({
|
||||
await open({
|
||||
afterClose,
|
||||
});
|
||||
// first Modal
|
||||
await sleep();
|
||||
$$('.ant-btn')[0].click();
|
||||
expect(afterClose).not.toHaveBeenCalled();
|
||||
await sleep(500);
|
||||
await waitFakeTimer(500);
|
||||
expect(afterClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('trigger afterClose once when click on ok button', async () => {
|
||||
const afterClose = jest.fn();
|
||||
open({
|
||||
await open({
|
||||
afterClose,
|
||||
});
|
||||
|
||||
// second Modal
|
||||
await sleep();
|
||||
$$('.ant-btn-primary')[0].click();
|
||||
expect(afterClose).not.toHaveBeenCalled();
|
||||
await sleep(500);
|
||||
await waitFakeTimer(500);
|
||||
expect(afterClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('bodyStyle', async () => {
|
||||
open({ bodyStyle: { width: 500 } });
|
||||
await open({ bodyStyle: { width: 500 } });
|
||||
|
||||
await sleep();
|
||||
const { width } = $$('.ant-modal-body')[0].style;
|
||||
expect(width).toBe('500px');
|
||||
});
|
||||
@ -725,7 +583,6 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
describe('the callback close should be a method when onCancel has a close parameter', () => {
|
||||
(['confirm', 'info', 'success', 'warning', 'error'] as const).forEach(type => {
|
||||
it(`click the close icon to trigger ${type} onCancel`, async () => {
|
||||
jest.useFakeTimers();
|
||||
const mock = jest.fn();
|
||||
|
||||
Modal[type]?.({
|
||||
@ -733,29 +590,20 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
onCancel: close => mock(close),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1);
|
||||
$$('.ant-modal-close')[0].click();
|
||||
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(0);
|
||||
expect(mock).toHaveBeenCalledWith(expect.any(Function));
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
(['confirm', 'info', 'success', 'warning', 'error'] as const).forEach(type => {
|
||||
it(`press ESC to trigger ${type} onCancel`, async () => {
|
||||
jest.useFakeTimers();
|
||||
const mock = jest.fn();
|
||||
|
||||
Modal[type]?.({
|
||||
@ -763,37 +611,22 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
onCancel: close => mock(close),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
await sleep();
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1);
|
||||
TestUtils.Simulate.keyDown($$('.ant-modal')[0], {
|
||||
keyCode: KeyCode.ESC,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
await sleep(0);
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
await waitFakeTimer(0);
|
||||
|
||||
expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(0);
|
||||
expect(mock).toHaveBeenCalledWith(expect.any(Function));
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
(['confirm', 'info', 'success', 'warning', 'error'] as const).forEach(type => {
|
||||
it(`click the mask to trigger ${type} onCancel`, async () => {
|
||||
jest.useFakeTimers();
|
||||
const mock = jest.fn();
|
||||
|
||||
Modal[type]?.({
|
||||
@ -801,104 +634,73 @@ describe('Modal.confirm triggers callbacks correctly', () => {
|
||||
onCancel: close => mock(close),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
expect($$('.ant-modal-mask')).toHaveLength(1);
|
||||
expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1);
|
||||
|
||||
$$('.ant-modal-wrap')[0].click();
|
||||
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(0);
|
||||
expect(mock).toHaveBeenCalledWith(expect.any(Function));
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('confirm modal click Cancel button close callback is a function', async () => {
|
||||
jest.useFakeTimers();
|
||||
const mock = jest.fn();
|
||||
|
||||
Modal.confirm({
|
||||
onCancel: close => mock(close),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
$$('.ant-modal-confirm-btns > .ant-btn')[0].click();
|
||||
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
expect(mock).toHaveBeenCalledWith(expect.any(Function));
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('close can close modal when onCancel has a close parameter', async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
Modal.confirm({
|
||||
onCancel: close => close(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
expect($$('.ant-modal-confirm-confirm')).toHaveLength(1);
|
||||
|
||||
$$('.ant-modal-confirm-btns > .ant-btn')[0].click();
|
||||
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
expect($$('.ant-modal-confirm-confirm')).toHaveLength(0);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/37461
|
||||
it('Update should closable', async () => {
|
||||
resetWarned();
|
||||
jest.useFakeTimers();
|
||||
const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
Modal.confirm({}).update({
|
||||
open: true,
|
||||
const modal = Modal.confirm({});
|
||||
|
||||
modal.update({
|
||||
visible: true,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
expect($$('.ant-modal-confirm-confirm')).toHaveLength(1);
|
||||
|
||||
$$('.ant-modal-confirm-btns > .ant-btn')[0].click();
|
||||
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
|
||||
expect($$('.ant-modal-confirm-confirm')).toHaveLength(0);
|
||||
|
||||
jest.useRealTimers();
|
||||
errSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
@ -5,7 +5,7 @@ import React from 'react';
|
||||
import TestUtils, { act } from 'react-dom/test-utils';
|
||||
|
||||
import Modal from '..';
|
||||
import { fireEvent, render, sleep } from '../../../tests/utils';
|
||||
import { fireEvent, render, waitFakeTimer } from '../../../tests/utils';
|
||||
import Button from '../../button';
|
||||
import ConfigProvider from '../../config-provider';
|
||||
import Input from '../../input';
|
||||
@ -200,13 +200,11 @@ describe('Modal.hook', () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const clear = async function clear() {
|
||||
await act(async () => {
|
||||
jest.runAllTimers();
|
||||
await sleep();
|
||||
});
|
||||
await waitFakeTimer();
|
||||
};
|
||||
|
||||
const mockFn = jest.fn();
|
||||
|
||||
const Demo = () => {
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
|
||||
|
@ -348,6 +348,11 @@ const genModalConfirmStyle: GenerateStyle<ModalToken> = token => {
|
||||
[`${confirmComponentCls}-success ${confirmComponentCls}-body > ${token.iconCls}`]: {
|
||||
color: token.colorSuccess,
|
||||
},
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/37329
|
||||
[`${componentCls}-zoom-leave ${componentCls}-btns`]: {
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -3,7 +3,6 @@
|
||||
exports[`renders ./components/pagination/demo/all.md extend context correctly 1`] = `
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
class="ant-pagination-total-text"
|
||||
@ -377,7 +376,6 @@ exports[`renders ./components/pagination/demo/all.md extend context correctly 1`
|
||||
exports[`renders ./components/pagination/demo/basic.md extend context correctly 1`] = `
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -505,7 +503,6 @@ exports[`renders ./components/pagination/demo/changer.md extend context correctl
|
||||
Array [
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="false"
|
||||
@ -861,7 +858,6 @@ Array [
|
||||
<br />,
|
||||
<ul
|
||||
class="ant-pagination ant-pagination-disabled"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="false"
|
||||
@ -1221,7 +1217,6 @@ Array [
|
||||
exports[`renders ./components/pagination/demo/controlled.md extend context correctly 1`] = `
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="false"
|
||||
@ -1348,7 +1343,6 @@ exports[`renders ./components/pagination/demo/controlled.md extend context corre
|
||||
exports[`renders ./components/pagination/demo/itemRender.md extend context correctly 1`] = `
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -1664,7 +1658,6 @@ exports[`renders ./components/pagination/demo/jump.md extend context correctly 1
|
||||
Array [
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="false"
|
||||
@ -2031,7 +2024,6 @@ Array [
|
||||
<br />,
|
||||
<ul
|
||||
class="ant-pagination ant-pagination-disabled"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="false"
|
||||
@ -2404,7 +2396,6 @@ exports[`renders ./components/pagination/demo/mini.md extend context correctly 1
|
||||
Array [
|
||||
<ul
|
||||
class="ant-pagination ant-pagination-mini"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -2528,7 +2519,6 @@ Array [
|
||||
</ul>,
|
||||
<ul
|
||||
class="ant-pagination ant-pagination-mini"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -2845,7 +2835,6 @@ Array [
|
||||
</ul>,
|
||||
<ul
|
||||
class="ant-pagination ant-pagination-mini"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
class="ant-pagination-total-text"
|
||||
@ -2974,7 +2963,6 @@ Array [
|
||||
</ul>,
|
||||
<ul
|
||||
class="ant-pagination ant-pagination-mini ant-pagination-disabled"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
class="ant-pagination-total-text"
|
||||
@ -3302,7 +3290,6 @@ Array [
|
||||
exports[`renders ./components/pagination/demo/more.md extend context correctly 1`] = `
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="false"
|
||||
@ -3885,7 +3872,6 @@ exports[`renders ./components/pagination/demo/total.md extend context correctly
|
||||
Array [
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
class="ant-pagination-total-text"
|
||||
@ -4197,7 +4183,6 @@ Array [
|
||||
<br />,
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
class="ant-pagination-total-text"
|
||||
|
@ -3,7 +3,6 @@
|
||||
exports[`renders ./components/pagination/demo/all.md correctly 1`] = `
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
class="ant-pagination-total-text"
|
||||
@ -261,7 +260,6 @@ exports[`renders ./components/pagination/demo/all.md correctly 1`] = `
|
||||
exports[`renders ./components/pagination/demo/basic.md correctly 1`] = `
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -389,7 +387,6 @@ exports[`renders ./components/pagination/demo/changer.md correctly 1`] = `
|
||||
Array [
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="false"
|
||||
@ -629,7 +626,6 @@ Array [
|
||||
<br />,
|
||||
<ul
|
||||
class="ant-pagination ant-pagination-disabled"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="false"
|
||||
@ -873,7 +869,6 @@ Array [
|
||||
exports[`renders ./components/pagination/demo/controlled.md correctly 1`] = `
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="false"
|
||||
@ -1000,7 +995,6 @@ exports[`renders ./components/pagination/demo/controlled.md correctly 1`] = `
|
||||
exports[`renders ./components/pagination/demo/itemRender.md correctly 1`] = `
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -1200,7 +1194,6 @@ exports[`renders ./components/pagination/demo/jump.md correctly 1`] = `
|
||||
Array [
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="false"
|
||||
@ -1451,7 +1444,6 @@ Array [
|
||||
<br />,
|
||||
<ul
|
||||
class="ant-pagination ant-pagination-disabled"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="false"
|
||||
@ -1708,7 +1700,6 @@ exports[`renders ./components/pagination/demo/mini.md correctly 1`] = `
|
||||
Array [
|
||||
<ul
|
||||
class="ant-pagination ant-pagination-mini"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -1832,7 +1823,6 @@ Array [
|
||||
</ul>,
|
||||
<ul
|
||||
class="ant-pagination ant-pagination-mini"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -2033,7 +2023,6 @@ Array [
|
||||
</ul>,
|
||||
<ul
|
||||
class="ant-pagination ant-pagination-mini"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
class="ant-pagination-total-text"
|
||||
@ -2162,7 +2151,6 @@ Array [
|
||||
</ul>,
|
||||
<ul
|
||||
class="ant-pagination ant-pagination-mini ant-pagination-disabled"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
class="ant-pagination-total-text"
|
||||
@ -2374,7 +2362,6 @@ Array [
|
||||
exports[`renders ./components/pagination/demo/more.md correctly 1`] = `
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="false"
|
||||
@ -2841,7 +2828,6 @@ exports[`renders ./components/pagination/demo/total.md correctly 1`] = `
|
||||
Array [
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
class="ant-pagination-total-text"
|
||||
@ -3037,7 +3023,6 @@ Array [
|
||||
<br />,
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
class="ant-pagination-total-text"
|
||||
|
@ -3,7 +3,6 @@
|
||||
exports[`Pagination ConfigProvider should be rendered correctly in RTL 1`] = `
|
||||
<ul
|
||||
class="ant-pagination ant-pagination-rtl"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -130,7 +129,6 @@ exports[`Pagination ConfigProvider should be rendered correctly in RTL 1`] = `
|
||||
exports[`Pagination ConfigProvider should be rendered correctly when componentSize is large 1`] = `
|
||||
<ul
|
||||
class="ant-pagination"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
@ -324,7 +322,6 @@ exports[`Pagination ConfigProvider should be rendered correctly when componentSi
|
||||
exports[`Pagination rtl render component should be rendered correctly in RTL direction 1`] = `
|
||||
<ul
|
||||
class="ant-pagination ant-pagination-rtl"
|
||||
unselectable="unselectable"
|
||||
>
|
||||
<li
|
||||
aria-disabled="true"
|
||||
|
18
components/pagination/__tests__/simple.test.tsx
Normal file
18
components/pagination/__tests__/simple.test.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import Pagination from '..';
|
||||
import { render } from '../../../tests/utils';
|
||||
|
||||
describe('Pagination simple mode', () => {
|
||||
it('should support showTotal in simple mode', () => {
|
||||
const { container } = render(
|
||||
<Pagination
|
||||
simple
|
||||
total={100}
|
||||
showTotal={(total: number, range: number[]) => `${range[0]}-${range[1]} of ${total} items`}
|
||||
/>,
|
||||
);
|
||||
expect(container?.querySelector('.ant-pagination-total-text')).toHaveTextContent(
|
||||
'1-10 of 100 items',
|
||||
);
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user