feat: dropdown support arrow pointing at center (#33658)

* feat: add pointAtCenter prop for dropdown

* feat: dropdown support arrow pointing at center

* chore: code clean

* chore: use getPlacements in tooltip

* feat: dropdown support 'top' and 'bottom'

* chore: update snapshot

* chore: move placements to util folder

* feat: make 'topCenter' and 'bottomCenter' in Dropdown deprecated

* test: update snapshot

* test: add test for topCenter and bottomCenter
This commit is contained in:
MadCcc 2022-01-13 13:34:34 +08:00 committed by GitHub
parent 5fb13e9681
commit e5451a95cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1510 additions and 53 deletions

View File

@ -42,6 +42,7 @@ export default function getPlacements(config: PlacementsConfig) {
horizontalArrowShift = 16,
verticalArrowShift = 8,
autoAdjustOverflow,
arrowPointAtCenter,
} = config;
const placementMap: BuildInPlacements = {
left: {
@ -94,7 +95,7 @@ export default function getPlacements(config: PlacementsConfig) {
},
};
Object.keys(placementMap).forEach(key => {
placementMap[key] = config.arrowPointAtCenter
placementMap[key] = arrowPointAtCenter
? {
...placementMap[key],
overflow: getOverflowOptions(autoAdjustOverflow),

View File

@ -30,7 +30,7 @@ const BreadcrumbItem: BreadcrumbItemInterface = ({
const renderBreadcrumbNode = (breadcrumbItem: React.ReactNode) => {
if (overlay) {
return (
<DropDown overlay={overlay} placement="bottomCenter" {...dropdownProps}>
<DropDown overlay={overlay} placement="bottom" {...dropdownProps}>
<span className={`${prefixCls}-overlay-link`}>
{breadcrumbItem}
<DownOutlined />
@ -62,9 +62,7 @@ const BreadcrumbItem: BreadcrumbItemInterface = ({
return (
<span>
{link}
{separator && (
<span className={`${prefixCls}-separator`}>{separator}</span>
)}
{separator && <span className={`${prefixCls}-separator`}>{separator}</span>}
</span>
);
}

View File

@ -15,7 +15,7 @@ Array [
type="button"
>
<span>
bottomCenter
bottom
</span>
</button>,
<button
@ -40,7 +40,61 @@ Array [
type="button"
>
<span>
topCenter
top
</span>
</button>,
<button
class="ant-btn ant-dropdown-trigger"
type="button"
>
<span>
topRight
</span>
</button>,
]
`;
exports[`renders ./components/dropdown/demo/arrow-center.md correctly 1`] = `
Array [
<button
class="ant-btn ant-dropdown-trigger"
type="button"
>
<span>
bottomLeft
</span>
</button>,
<button
class="ant-btn ant-dropdown-trigger"
type="button"
>
<span>
bottom
</span>
</button>,
<button
class="ant-btn ant-dropdown-trigger"
type="button"
>
<span>
bottomRight
</span>
</button>,
<br />,
<button
class="ant-btn ant-dropdown-trigger"
type="button"
>
<span>
topLeft
</span>
</button>,
<button
class="ant-btn ant-dropdown-trigger"
type="button"
>
<span>
top
</span>
</button>,
<button
@ -658,7 +712,7 @@ exports[`renders ./components/dropdown/demo/placement.md correctly 1`] = `
type="button"
>
<span>
bottomCenter
bottom
</span>
</button>
</div>
@ -706,7 +760,7 @@ exports[`renders ./components/dropdown/demo/placement.md correctly 1`] = `
type="button"
>
<span>
topCenter
top
</span>
</button>
</div>

View File

@ -59,4 +59,24 @@ describe('Dropdown', () => {
await sleep(500);
expect(wrapper.find(Dropdown).find('#customExpandIcon').length).toBe(1);
});
it('should warn if use topCenter or bottomCenter', () => {
const error = jest.spyOn(console, 'error');
mount(
<div>
<Dropdown overlay="123" placement="bottomCenter">
<button type="button">bottomCenter</button>
</Dropdown>
<Dropdown overlay="123" placement="topCenter">
<button type="button">topCenter</button>
</Dropdown>
</div>,
);
expect(error).toHaveBeenCalledWith(
expect.stringContaining("[antd: Dropdown] You are using 'bottomCenter'"),
);
expect(error).toHaveBeenCalledWith(
expect.stringContaining("[antd: Dropdown] You are using 'topCenter'"),
);
});
});

View File

@ -0,0 +1,75 @@
---
order: 3
title:
zh-CN: 箭头指向
en-US: Arrow pointing at the center
---
## zh-CN
设置 `arrow``{ pointAtCenter: true }` 后,箭头将指向目标元素的中心。
## en-US
By specifying `arrow` prop with `{ pointAtCenter: true }`, the arrow will point to the center of the target element.
```jsx
import { Menu, Dropdown, Button } from 'antd';
const menu = (
<Menu>
<Menu.Item>
<a target="_blank" rel="noopener noreferrer" href="https://www.antgroup.com">
1st menu item
</a>
</Menu.Item>
<Menu.Item>
<a target="_blank" rel="noopener noreferrer" href="https://www.aliyun.com">
2nd menu item
</a>
</Menu.Item>
<Menu.Item>
<a target="_blank" rel="noopener noreferrer" href="https://www.luohanacademy.com">
3rd menu item
</a>
</Menu.Item>
</Menu>
);
ReactDOM.render(
<>
<Dropdown overlay={menu} placement="bottomLeft" arrow={{ pointAtCenter: true }}>
<Button>bottomLeft</Button>
</Dropdown>
<Dropdown overlay={menu} placement="bottom" arrow={{ pointAtCenter: true }}>
<Button>bottom</Button>
</Dropdown>
<Dropdown overlay={menu} placement="bottomRight" arrow={{ pointAtCenter: true }}>
<Button>bottomRight</Button>
</Dropdown>
<br />
<Dropdown overlay={menu} placement="topLeft" arrow={{ pointAtCenter: true }}>
<Button>topLeft</Button>
</Dropdown>
<Dropdown overlay={menu} placement="top" arrow={{ pointAtCenter: true }}>
<Button>top</Button>
</Dropdown>
<Dropdown overlay={menu} placement="topRight" arrow={{ pointAtCenter: true }}>
<Button>topRight</Button>
</Dropdown>
</>,
mountNode,
);
```
```css
#components-dropdown-demo-arrow-center .ant-btn {
margin-right: 8px;
margin-bottom: 8px;
}
.ant-row-rtl #components-dropdown-demo-arrow-center .ant-btn {
margin-right: 0;
margin-bottom: 8px;
margin-left: 8px;
}
```

View File

@ -41,8 +41,8 @@ ReactDOM.render(
<Dropdown overlay={menu} placement="bottomLeft" arrow>
<Button>bottomLeft</Button>
</Dropdown>
<Dropdown overlay={menu} placement="bottomCenter" arrow>
<Button>bottomCenter</Button>
<Dropdown overlay={menu} placement="bottom" arrow>
<Button>bottom</Button>
</Dropdown>
<Dropdown overlay={menu} placement="bottomRight" arrow>
<Button>bottomRight</Button>
@ -51,8 +51,8 @@ ReactDOM.render(
<Dropdown overlay={menu} placement="topLeft" arrow>
<Button>topLeft</Button>
</Dropdown>
<Dropdown overlay={menu} placement="topCenter" arrow>
<Button>topCenter</Button>
<Dropdown overlay={menu} placement="top" arrow>
<Button>top</Button>
</Dropdown>
<Dropdown overlay={menu} placement="topRight" arrow>
<Button>topRight</Button>

View File

@ -46,7 +46,7 @@ ReactDOM.render(
<Dropdown.Button onClick={handleButtonClick} overlay={menu}>
Dropdown
</Dropdown.Button>
<Dropdown.Button overlay={menu} placement="bottomCenter" icon={<UserOutlined />}>
<Dropdown.Button overlay={menu} placement="bottom" icon={<UserOutlined />}>
Dropdown
</Dropdown.Button>
<Dropdown.Button onClick={handleButtonClick} overlay={menu} disabled>

View File

@ -42,8 +42,8 @@ ReactDOM.render(
<Dropdown overlay={menu} placement="bottomLeft">
<Button>bottomLeft</Button>
</Dropdown>
<Dropdown overlay={menu} placement="bottomCenter">
<Button>bottomCenter</Button>
<Dropdown overlay={menu} placement="bottom">
<Button>bottom</Button>
</Dropdown>
<Dropdown overlay={menu} placement="bottomRight">
<Button>bottomRight</Button>
@ -53,8 +53,8 @@ ReactDOM.render(
<Dropdown overlay={menu} placement="topLeft">
<Button>topLeft</Button>
</Dropdown>
<Dropdown overlay={menu} placement="topCenter">
<Button>topCenter</Button>
<Dropdown overlay={menu} placement="top">
<Button>top</Button>
</Dropdown>
<Dropdown overlay={menu} placement="topRight">
<Button>topRight</Button>

View File

@ -7,6 +7,7 @@ import { ConfigContext } from '../config-provider';
import devWarning from '../_util/devWarning';
import { tuple } from '../_util/type';
import { cloneElement } from '../_util/reactNode';
import getPlacements from '../_util/placements';
const Placements = tuple(
'topLeft',
@ -15,6 +16,8 @@ const Placements = tuple(
'bottomLeft',
'bottomCenter',
'bottomRight',
'top',
'bottom',
);
type Placement = typeof Placements[number];
@ -34,8 +37,12 @@ type Align = {
useCssTransform?: boolean;
};
export type DropdownArrowOptions = {
pointAtCenter?: boolean;
};
export interface DropDownProps {
arrow?: boolean;
arrow?: boolean | DropdownArrowOptions;
trigger?: ('click' | 'hover' | 'contextMenu')[];
overlay: React.ReactElement | OverlayFunc;
onVisibleChange?: (visible: boolean) => void;
@ -129,10 +136,21 @@ const Dropdown: DropdownInterface = props => {
const getPlacement = () => {
const { placement } = props;
if (placement !== undefined) {
return placement;
}
if (!placement) {
return direction === 'rtl' ? ('bottomRight' as Placement) : ('bottomLeft' as Placement);
}
if (placement.includes('Center')) {
const newPlacement = placement.slice(0, placement.indexOf('Center'));
devWarning(
!placement.includes('Center'),
'Dropdown',
`You are using '${placement}' placement in Dropdown, which is deprecated. Try to use '${newPlacement}' instead.`,
);
return newPlacement;
}
return placement;
};
const {
@ -169,11 +187,16 @@ const Dropdown: DropdownInterface = props => {
alignPoint = true;
}
const builtinPlacements = getPlacements({
arrowPointAtCenter: typeof arrow === 'object' && arrow.pointAtCenter,
});
return (
<RcDropdown
arrow={arrow}
alignPoint={alignPoint}
{...props}
builtinPlacements={builtinPlacements}
arrow={!!arrow}
overlayClassName={overlayClassNameCustomized}
prefixCls={prefixCls}
getPopupContainer={getPopupContainer || getContextPopupContainer}

View File

@ -17,14 +17,14 @@ When there are more than a few options to choose from, you can wrap them in a `D
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| arrow | Whether the dropdown arrow should be visible | boolean | false | |
| arrow | Whether the dropdown arrow should be visible | boolean \| { pointAtCenter: boolean } | false | |
| disabled | Whether the dropdown menu is disabled | boolean | - | |
| destroyPopupOnHide | Whether destroy dropdown when hidden | boolean | false | |
| 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 | - | |
| overlayClassName | The class name of the dropdown root element | string | - | |
| overlayStyle | The style of the dropdown root element | CSSProperties | - | |
| placement | Placement of popup menu: `bottomLeft`, `bottomCenter`, `bottomRight`, `topLeft`, `topCenter` or `topRight` | string | `bottomLeft` | |
| placement | Placement of popup menu: `bottom` `bottomLeft` `bottomRight` `top` `topLeft` `topRight` | string | `bottomLeft` | |
| trigger | The trigger mode which executes the dropdown action. Note that hover can't be used on touchscreens | Array&lt;`click`\|`hover`\|`contextMenu`> | \[`hover`] | |
| visible | Whether the dropdown menu is currently visible | boolean | - | |
| onVisibleChange | Called when the visible state is changed. Not trigger when hidden by click item | (visible: boolean) => void | - | |
@ -44,7 +44,7 @@ You should use [Menu](/components/menu/) as `overlay`. The menu items and divide
| 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: `bottomLeft` `bottomCenter` `bottomRight` `topLeft` `topCenter` `topRight` | string | `bottomLeft` | |
| placement | Placement of popup menu: `bottom` `bottomLeft` `bottomRight` `top` `topLeft` `topRight` | string | `bottomLeft` | |
| size | Size of the button, the same as [Button](/components/button/#API) | string | `default` | |
| trigger | The trigger mode which executes the dropdown action | Array&lt;`click`\|`hover`\|`contextMenu`> | \[`hover`] | |
| type | Type of the button, the same as [Button](/components/button/#API) | string | `default` | |

View File

@ -21,14 +21,14 @@ cover: https://gw.alipayobjects.com/zos/alicdn/eedWN59yJ/Dropdown.svg
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| arrow | 下拉框箭头是否显示 | boolean | false | |
| arrow | 下拉框箭头是否显示 | boolean \| { pointAtCenter: boolean } | false | |
| disabled | 菜单是否禁用 | boolean | - | |
| destroyPopupOnHide | 关闭后是否销毁 Dropdown | boolean | false | |
| getPopupContainer | 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。[示例](https://codepen.io/afc163/pen/zEjNOy?editors=0010) | (triggerNode: HTMLElement) => HTMLElement | () => document.body | |
| overlay | 菜单 | [Menu](/components/menu) \| () => Menu | - | |
| overlayClassName | 下拉根元素的类名称 | string | - | |
| overlayStyle | 下拉根元素的样式 | CSSProperties | - | |
| placement | 菜单弹出位置:`bottomLeft` `bottomCenter` `bottomRight` `topLeft` `topCenter` `topRight` | string | `bottomLeft` | |
| placement | 菜单弹出位置:`bottom` `bottomLeft` `bottomRight` `top` `topLeft` `topRight` | string | `bottomLeft` | |
| trigger | 触发下拉的行为, 移动端不支持 hover | Array&lt;`click`\|`hover`\|`contextMenu`> | \[`hover`] | |
| visible | 菜单是否显示 | boolean | - | |
| onVisibleChange | 菜单显示状态改变时调用,参数为 `visible`。点击菜单按钮导致的消失不会触发 | (visible: boolean) => void | - | |
@ -48,7 +48,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/eedWN59yJ/Dropdown.svg
| disabled | 菜单是否禁用 | boolean | - | |
| icon | 右侧的 icon | ReactNode | - | |
| overlay | 菜单 | [Menu](/components/menu/) | - | |
| placement | 菜单弹出位置:`bottomLeft` `bottomCenter` `bottomRight` `topLeft` `topCenter` `topRight` | string | `bottomLeft` | |
| placement | 菜单弹出位置:`bottom` `bottomLeft` `bottomRight` `top` `topLeft` `topRight` | string | `bottomLeft` | |
| size | 按钮大小,和 [Button](/components/button/#API) 一致 | string | `default` | |
| trigger | 触发下拉的行为 | Array&lt;`click`\|`hover`\|`contextMenu`> | \[`hover`] | |
| type | 按钮类型,和 [Button](/components/button/#API) 一致 | string | `default` | |

View File

@ -49,14 +49,14 @@
}
// Offset the popover to account for the dropdown arrow
&-show-arrow&-placement-topCenter,
&-show-arrow&-placement-topLeft,
&-show-arrow&-placement-top,
&-show-arrow&-placement-topRight {
padding-bottom: @popover-distance;
}
&-show-arrow&-placement-bottomCenter,
&-show-arrow&-placement-bottomLeft,
&-show-arrow&-placement-bottom,
&-show-arrow&-placement-bottomRight {
padding-top: @popover-distance;
}
@ -76,7 +76,7 @@
transform: rotate(45deg);
}
&-placement-topCenter > &-arrow,
&-placement-top > &-arrow,
&-placement-topLeft > &-arrow,
&-placement-topRight > &-arrow {
bottom: @popover-distance - @popover-arrow-width + 2.2px;
@ -84,7 +84,7 @@
box-shadow: 3px 3px 7px fade(@black, 7%);
}
&-placement-topCenter > &-arrow {
&-placement-top > &-arrow {
left: 50%;
transform: translateX(-50%) rotate(45deg);
}
@ -97,7 +97,7 @@
right: 16px;
}
&-placement-bottomCenter > &-arrow,
&-placement-bottom > &-arrow,
&-placement-bottomLeft > &-arrow,
&-placement-bottomRight > &-arrow {
top: @popover-distance - @popover-arrow-width + 2px;
@ -105,7 +105,7 @@
box-shadow: -2px -2px 5px fade(@black, 6%);
}
&-placement-bottomCenter > &-arrow {
&-placement-bottom > &-arrow {
left: 50%;
transform: translateX(-50%) rotate(45deg);
}
@ -300,8 +300,8 @@
&.@{ant-prefix}-slide-down-enter.@{ant-prefix}-slide-down-enter-active&-placement-bottomLeft,
&.@{ant-prefix}-slide-down-appear.@{ant-prefix}-slide-down-appear-active&-placement-bottomLeft,
&.@{ant-prefix}-slide-down-enter.@{ant-prefix}-slide-down-enter-active&-placement-bottomCenter,
&.@{ant-prefix}-slide-down-appear.@{ant-prefix}-slide-down-appear-active&-placement-bottomCenter,
&.@{ant-prefix}-slide-down-enter.@{ant-prefix}-slide-down-enter-active&-placement-bottom,
&.@{ant-prefix}-slide-down-appear.@{ant-prefix}-slide-down-appear-active&-placement-bottom,
&.@{ant-prefix}-slide-down-enter.@{ant-prefix}-slide-down-enter-active&-placement-bottomRight,
&.@{ant-prefix}-slide-down-appear.@{ant-prefix}-slide-down-appear-active&-placement-bottomRight {
animation-name: antSlideUpIn;
@ -309,21 +309,21 @@
&.@{ant-prefix}-slide-up-enter.@{ant-prefix}-slide-up-enter-active&-placement-topLeft,
&.@{ant-prefix}-slide-up-appear.@{ant-prefix}-slide-up-appear-active&-placement-topLeft,
&.@{ant-prefix}-slide-up-enter.@{ant-prefix}-slide-up-enter-active&-placement-topCenter,
&.@{ant-prefix}-slide-up-appear.@{ant-prefix}-slide-up-appear-active&-placement-topCenter,
&.@{ant-prefix}-slide-up-enter.@{ant-prefix}-slide-up-enter-active&-placement-top,
&.@{ant-prefix}-slide-up-appear.@{ant-prefix}-slide-up-appear-active&-placement-top,
&.@{ant-prefix}-slide-up-enter.@{ant-prefix}-slide-up-enter-active&-placement-topRight,
&.@{ant-prefix}-slide-up-appear.@{ant-prefix}-slide-up-appear-active&-placement-topRight {
animation-name: antSlideDownIn;
}
&.@{ant-prefix}-slide-down-leave.@{ant-prefix}-slide-down-leave-active&-placement-bottomLeft,
&.@{ant-prefix}-slide-down-leave.@{ant-prefix}-slide-down-leave-active&-placement-bottomCenter,
&.@{ant-prefix}-slide-down-leave.@{ant-prefix}-slide-down-leave-active&-placement-bottom,
&.@{ant-prefix}-slide-down-leave.@{ant-prefix}-slide-down-leave-active&-placement-bottomRight {
animation-name: antSlideUpOut;
}
&.@{ant-prefix}-slide-up-leave.@{ant-prefix}-slide-up-leave-active&-placement-topLeft,
&.@{ant-prefix}-slide-up-leave.@{ant-prefix}-slide-up-leave-active&-placement-topCenter,
&.@{ant-prefix}-slide-up-leave.@{ant-prefix}-slide-up-leave-active&-placement-top,
&.@{ant-prefix}-slide-up-leave.@{ant-prefix}-slide-up-leave-active&-placement-topRight {
animation-name: antSlideDownOut;
}

View File

@ -4,7 +4,7 @@ import useMergedState from 'rc-util/lib/hooks/useMergedState';
import { TooltipProps as RcTooltipProps } from 'rc-tooltip/lib/Tooltip';
import classNames from 'classnames';
import { placements as Placements } from 'rc-tooltip/lib/placements';
import getPlacements, { AdjustOverflow, PlacementsConfig } from './placements';
import getPlacements, { AdjustOverflow, PlacementsConfig } from '../_util/placements';
import { cloneElement, isValidElement } from '../_util/reactNode';
import { ConfigContext } from '../config-provider';
import { PresetColorType, PresetColorTypes } from '../_util/colors';
@ -130,9 +130,11 @@ function getDisabledCompatibleChildren(element: React.ReactElement<any>, prefixC
}
const Tooltip = React.forwardRef<unknown, TooltipProps>((props, ref) => {
const { getPopupContainer: getContextPopupContainer, getPrefixCls, direction } = React.useContext(
ConfigContext,
);
const {
getPopupContainer: getContextPopupContainer,
getPrefixCls,
direction,
} = React.useContext(ConfigContext);
const [visible, setVisible] = useMergedState(false, {
value: props.visible,
@ -167,11 +169,11 @@ const Tooltip = React.forwardRef<unknown, TooltipProps>((props, ref) => {
const onPopupAlign = (domNode: HTMLElement, align: any) => {
const placements: any = getTooltipPlacements();
// 当前返回的位置
const placement = Object.keys(placements).filter(
const placement = Object.keys(placements).find(
key =>
placements[key].points[0] === align.points[0] &&
placements[key].points[1] === align.points[1],
)[0];
);
if (!placement) {
return;
}

View File

@ -126,7 +126,7 @@
"rc-collapse": "~3.1.0",
"rc-dialog": "~8.6.0",
"rc-drawer": "~4.4.2",
"rc-dropdown": "~3.2.0",
"rc-dropdown": "~3.2.2",
"rc-field-form": "~1.22.0-2",
"rc-image": "~5.2.5",
"rc-input-number": "~7.3.0",

View File

@ -523,7 +523,7 @@ class MainContent extends Component {
</section>
{componentPage && (
<div className="fixed-widgets">
<Dropdown overlay={this.getThemeSwitchMenu()} placement="topCenter">
<Dropdown overlay={this.getThemeSwitchMenu()} placement="top">
<Avatar className="fixed-widgets-avatar" size={44} icon={<ThemeIcon />} />
</Dropdown>
</div>