-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* chore: remove contextMenuCombiner component * refactor: replace contextMenu with dropdown * fix: improve ci * test(contextMenu): update test cases * fix: support popconfirm in ContextMenu * test: update tests * fix: prevent render dropdown with empty data
- Loading branch information
1 parent
d4e6496
commit dadfedc
Showing
10 changed files
with
378 additions
and
684 deletions.
There are no files selected for viewing
63 changes: 63 additions & 0 deletions
63
src/components/contextMenu/__tests__/__snapshots__/contextMenu.test.tsx.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`test contextMenu should match snapshot 1`] = ` | ||
<DocumentFragment> | ||
<span | ||
class="ant-dropdown-trigger ant-dropdown-open" | ||
> | ||
<span | ||
data-testid="test" | ||
> | ||
test | ||
</span> | ||
</span> | ||
<div | ||
style="position: absolute; top: 0px; left: 0px; width: 100%;" | ||
> | ||
<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 dt-contextMenu-menu" | ||
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-test" | ||
role="menuitem" | ||
tabindex="-1" | ||
> | ||
<span | ||
class="ant-dropdown-menu-title-content" | ||
> | ||
test | ||
</span> | ||
</li> | ||
<li | ||
class="ant-dropdown-menu-item ant-dropdown-menu-item-only-child" | ||
data-menu-id="rc-menu-uuid-test-confirm" | ||
role="menuitem" | ||
tabindex="-1" | ||
> | ||
<span | ||
class="ant-dropdown-menu-title-content" | ||
> | ||
<div> | ||
confirm | ||
</div> | ||
</span> | ||
</li> | ||
</ul> | ||
<div | ||
aria-hidden="true" | ||
style="display: none;" | ||
/> | ||
</div> | ||
</div> | ||
</div> | ||
</DocumentFragment> | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import React from 'react'; | ||
import { cleanup, fireEvent, render } from '@testing-library/react'; | ||
import ContextMenu from '..'; | ||
|
||
describe('test contextMenu', () => { | ||
afterEach(cleanup); | ||
|
||
test('should match snapshot', () => { | ||
const { asFragment, getByTestId } = render( | ||
<ContextMenu | ||
data={[ | ||
{ text: 'test', key: 'test', cb: () => {} }, | ||
{ | ||
text: 'confirm', | ||
key: 'confirm', | ||
confirm: true, | ||
confirmProps: { | ||
title: 'confirmTitle', | ||
getPopupContainer: (node) => node.parentElement, | ||
}, | ||
}, | ||
]} | ||
getPopupContainer={(node) => node.parentElement} | ||
> | ||
<span data-testid="test">test</span> | ||
</ContextMenu> | ||
); | ||
|
||
fireEvent.contextMenu(getByTestId('test').parentElement); | ||
|
||
expect(asFragment()).toMatchSnapshot(); | ||
}); | ||
|
||
test('should support to call cb function on data', () => { | ||
const testFn = jest.fn(); | ||
const { getByTestId, container } = render( | ||
<ContextMenu | ||
data={[{ text: 'test', key: 'test', cb: testFn }]} | ||
getPopupContainer={(node) => node.parentElement} | ||
> | ||
<span data-testid="test">test</span> | ||
</ContextMenu> | ||
); | ||
|
||
fireEvent.contextMenu(getByTestId('test').parentElement); | ||
|
||
const dom = container.querySelector('.ant-dropdown-menu-title-content'); | ||
|
||
expect(dom).not.toBe(null); | ||
|
||
fireEvent.click(dom); | ||
|
||
expect(testFn).toBeCalledTimes(1); | ||
}); | ||
|
||
test('should support wrapperClassName', () => { | ||
const { getByTestId } = render( | ||
<ContextMenu | ||
data={[{ text: 'test', key: 'test', cb: () => {} }]} | ||
getPopupContainer={(node) => node.parentElement} | ||
wrapperClassName="wrapper" | ||
> | ||
<span data-testid="test">test</span> | ||
</ContextMenu> | ||
); | ||
|
||
const children = getByTestId('test'); | ||
|
||
expect(children.parentElement.className).toContain('wrapper'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,132 +1,74 @@ | ||
import React from 'react'; | ||
const contextPrefix = 'dtc-context-menu'; | ||
|
||
export interface ContextMenuProps { | ||
key?: string; | ||
targetClassName?: string; | ||
onChange?: Function; | ||
[propName: string]: any; | ||
} | ||
export interface ContextMenuItemProps { | ||
key?: string; | ||
onClick?: () => void; | ||
value?: string; | ||
[propName: string]: any; | ||
import React, { CSSProperties, PropsWithChildren } from 'react'; | ||
import { Dropdown, Menu, DropdownProps, Popconfirm, PopconfirmProps } from 'antd'; | ||
|
||
interface IMenuProps { | ||
/** | ||
* @required | ||
*/ | ||
key: React.Key; | ||
/** | ||
* 菜单栏的标题文案 | ||
*/ | ||
text: React.ReactNode; | ||
/** | ||
* 菜单栏的样式 | ||
*/ | ||
style?: CSSProperties; | ||
/** | ||
* 是否支持 popconfirm 的弹出 | ||
*/ | ||
confirm?: boolean; | ||
/** | ||
* 只有设置了 `confirm` 项的情况下,该属性才会生效 | ||
*/ | ||
confirmProps?: PopconfirmProps; | ||
/** | ||
* 菜单栏的点击事件 | ||
*/ | ||
cb?: () => void; | ||
} | ||
|
||
export class ContextMenuItem extends React.Component<ContextMenuItemProps, any> { | ||
render() { | ||
return ( | ||
<li {...this.props} className={`${contextPrefix}-context-list_li`}> | ||
<a className={`${contextPrefix}-context-list_a`} data-value={this.props.value}> | ||
{this.props.children} | ||
</a> | ||
</li> | ||
); | ||
} | ||
interface IContextMenu | ||
extends Pick< | ||
DropdownProps, | ||
'destroyPopupOnHide' | 'getPopupContainer' | 'placement' | 'overlayClassName' | ||
> { | ||
data: IMenuProps[]; | ||
wrapperClassName?: string; | ||
} | ||
|
||
export default class ContextMenu extends React.Component<ContextMenuProps, any> { | ||
constructor(props: ContextMenuProps) { | ||
super(props); | ||
this.toggleMenu = this.toggleMenu.bind(this); | ||
this.removeMenu = this.removeMenu.bind(this); | ||
} | ||
static ContextMenuItem = ContextMenuItem; | ||
|
||
selfEle: HTMLElement; | ||
|
||
componentDidMount() { | ||
document.addEventListener('contextmenu', this.toggleMenu, false); | ||
document.addEventListener('click', this.removeMenu, false); | ||
} | ||
|
||
componentWillUnmount() { | ||
document.removeEventListener('click', this.removeMenu, false); | ||
document.removeEventListener('contextmenu', this.toggleMenu, false); | ||
} | ||
|
||
toggleMenu(evt: MouseEvent) { | ||
const { targetClassName, onChange } = this.props; | ||
const selfEle = this.selfEle; | ||
if (!selfEle) return; | ||
const parent = this.findParent(evt.target as HTMLElement, targetClassName); | ||
|
||
if (parent) { | ||
this.hideAll(); | ||
|
||
const style = selfEle.style; | ||
style.display = 'block'; | ||
|
||
const pointerY = evt.clientY; | ||
const pointerX = evt.clientX; | ||
const viewHeight = document.body.offsetHeight; // 可视区高度 | ||
const distanceToBottom = viewHeight - pointerY; | ||
const menuHeight = selfEle.offsetHeight; | ||
const menuTop = distanceToBottom > menuHeight ? pointerY : pointerY - menuHeight; | ||
|
||
style.cssText = ` | ||
top: ${menuTop}px; | ||
left: ${pointerX}px; | ||
display: block; | ||
`; | ||
if (onChange) { | ||
onChange(parent); | ||
} | ||
evt.preventDefault(); | ||
} | ||
} | ||
|
||
hideAll() { | ||
const allEles: any = document.querySelectorAll(`.${contextPrefix}`); | ||
for (let i = 0; i < allEles.length; i++) { | ||
allEles[i].style.display = 'none'; | ||
} | ||
} | ||
|
||
closeMenu(_evt: MouseEvent) { | ||
if (!this.selfEle) return; | ||
const style = this.selfEle.style; | ||
style.display = 'none'; | ||
} | ||
|
||
removeMenu(_evt: MouseEvent) { | ||
if (!this.selfEle) return; | ||
const style = this.selfEle.style; | ||
style.display = 'none'; | ||
} | ||
|
||
findParent(child: HTMLElement, selector: string) { | ||
let selectorTemp = selector; | ||
try { | ||
if (!selectorTemp || !child) return; | ||
selectorTemp = selectorTemp.toLowerCase(); | ||
let node: any = child; | ||
while (node) { | ||
if (node.nodeType === 1) { | ||
// just hand dom element | ||
const className = node.getAttribute('class'); | ||
if (className && className.includes(selectorTemp)) return node; | ||
} | ||
node = node.parentNode; | ||
} | ||
} catch (e) { | ||
throw new Error(e); | ||
} | ||
return null; | ||
} | ||
|
||
render() { | ||
return ( | ||
<div | ||
ref={(e) => { | ||
this.selfEle = e; | ||
}} | ||
className={contextPrefix} | ||
style={{ display: 'none' }} | ||
> | ||
<ul className={`${contextPrefix}-context-menu_list`}>{this.props.children}</ul> | ||
</div> | ||
); | ||
} | ||
export default function ContextMenu({ | ||
data = [], | ||
children, | ||
wrapperClassName, | ||
...restProps | ||
}: PropsWithChildren<IContextMenu>) { | ||
const menu = ( | ||
<Menu | ||
className="dt-contextMenu-menu" | ||
onClick={(item) => data.find((i) => i.key === item.key)?.cb?.()} | ||
> | ||
{data.map((item) => | ||
item.confirm ? ( | ||
<Menu.Item style={item.style} key={item.key}> | ||
<Popconfirm key={item.key} {...item.confirmProps}> | ||
<div>{item.text}</div> | ||
</Popconfirm> | ||
</Menu.Item> | ||
) : ( | ||
<Menu.Item style={item.style} key={item.key}> | ||
{item.text} | ||
</Menu.Item> | ||
) | ||
)} | ||
</Menu> | ||
); | ||
|
||
if (!data.length) return <span className={wrapperClassName}>{children}</span>; | ||
|
||
return ( | ||
<Dropdown overlay={menu} trigger={['contextMenu']} {...restProps}> | ||
<span className={wrapperClassName}>{children}</span> | ||
</Dropdown> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,34 +1,19 @@ | ||
$context-prefix: "dtc-context-menu"; | ||
.#{$context-prefix} { | ||
position: fixed; | ||
min-width: 90px; | ||
max-width: 160px; | ||
display: none; | ||
z-index: 9999; | ||
.dt-contextMenu-menu { | ||
padding: 0; | ||
background: #FFF; | ||
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.2); | ||
border-radius: 2px; | ||
.#{$context-prefix}-context-menu_list { | ||
margin: 0; | ||
padding: 0; | ||
.#{$context-prefix}-context-list_li { | ||
list-style-type: none; | ||
} | ||
.#{$context-prefix}-context-list_a { | ||
display: block; | ||
padding: 7px 8px; | ||
margin: 0; | ||
clear: both; | ||
font-size: 12px; | ||
font-weight: normal; | ||
color: rgba(0, 0, 0, 0.65); | ||
white-space: nowrap; | ||
cursor: pointer; | ||
transition: all 0.3s; | ||
text-decoration: none; | ||
&:hover { | ||
background: #EEF6FE; | ||
} | ||
min-width: 90px; | ||
max-width: 160px; | ||
.ant-dropdown-menu-item { | ||
color: rgba(0, 0, 0, 0.65); | ||
font-size: 12px; | ||
font-weight: normal; | ||
white-space: nowrap; | ||
text-decoration: none; | ||
line-height: inherit; | ||
&:hover { | ||
background: #EEF6FE; | ||
} | ||
} | ||
} |
Oops, something went wrong.