前端自定义右键菜单
- 废话少说,先上效果

菜单容器
容器主要代码
class App extends Component<appProps, appState> {
constructor(props: appProps) {
super(props)
this.state = {
position: { top: 0, left: 0 },
menuVisiable: false,
menuWidth: 0,
menuHeight: 0,
menuDatas: [{
id: "A000001",
value: "apple",
children: []
},
{
id: "A000003",
value: "AAAAAAAAAAAAAAAAAAAA",
children: [
{
id: "A000031",
value: "BBBBBBBBBBBBBBB",
children: [{
id: "A000311",
value: "ddddddddddddd",
children: []
}]
},
{
id: "A000032",
value: "CCCCCCCCCCCCCCCCCCC",
children: []
}
]
}, {
id: "A000002",
value: "banana",
children: []
}
]
}
}
contextMenuHandle = (e: React.MouseEvent<HTMLDivElement>) => {
this.setState({ menuVisiable: true, position: { top: top, left: left } })
}
handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault()
this.setState({ menuVisiable: false })
}
onMenuClick = (data: menu) => {
console.log(data)
this.setState({ menuVisiable: false })
}
syncParams = (width: number, height: number) => {
this.setState({ menuHeight: height, menuWidth: width })
}
showMenus = () => {
if (!this.state.menuVisiable) {
return null;
}
return <Content containerId='content-container' syncParams={this.syncParams} menuItams={this.state.menuDatas} position={this.state.position} onMenuClick={this.onMenuClick} />
}
render(): ReactNode {
return (
<section id="content-container" onClick={this.handleClick} onContextMenu={this.contextMenuHandle}>
{this.showMenus()}
</section>
)
}
}
export default App
容器样式
#content-container {
height: 80vh;
width: 90vw;
background-color: rgb(255, 255, 255);
border-radius: 8px;
}
菜单内容
菜单主要代码
export interface menu {
id: string
value: string
children: menu[]
}
export interface position {
top: number
left: number
}
interface contentProps {
menuItams: menu[]
onMenuClick: (data: menu) => void
position: position
syncParams: Function
containerId: string
}
interface contentState {
selfHeight: number
}
class Content extends Component<contentProps, contentState> {
innerRef = createRef<HTMLDivElement>();
observer: ResizeObserver | null = null;
state: contentState = {
selfHeight: 0
}
componentDidMount = () => {
if (this.innerRef.current) {
this.setState({ selfHeight: this.innerRef.current.clientHeight })
// 实例化 ResizeObserver,回调函数会在尺寸变化时触发
this.observer = new ResizeObserver(([entries]) => {
// 获取最新的 contentRect 尺寸(不包含 padding 和 border)
const { width, height } = entries.contentRect;
this.props.syncParams(width, height)
});
// 开始监听绑定的 DOM 元素
this.observer.observe(this.innerRef.current);
}
}
componentWillUnmount(): void {
if (this.observer) {
this.observer.disconnect();
}
}
handleClick = (data: menu): void => {
this.props.onMenuClick(data)
}
getTop = () => {
return this.props.position.top
}
getLeft = () => {
return this.props.position.left
}
generateItems = (menuItams: menu[]): React.ReactNode => {
let menuList = menuItams.map((el, i) => {
let children: React.ReactNode
if (el.children.length > 0) {
children = this.generateItems(el.children)
}
return <MenuItem containerId={this.props.containerId} level={i} originPosition={this.props.position} onChildClick={this.handleClick} key={el.id} item={el} children={children} />
})
return menuList;
}
render(): ReactNode {
let menuList = this.generateItems(this.props.menuItams);
return (
<>
<div ref={this.innerRef} id="content-menu" style={{ left: this.getLeft() + "px", top: this.getTop() + "px" }} onContextMenu={(e) => { e.stopPropagation(); e.preventDefault() }}>
{menuList}
</div>
</>
)
}
}
interface menuItemProps {
item: menu
children: React.ReactNode
level: number
brother: number
originPosition: position
containerId: string
onChildClick: (data: menu) => void
}
interface menuItemState {
top: number
left: number
showChild: boolean
hoverMenu: boolean
}
class MenuItem extends Component<menuItemProps, menuItemState> {
innerRef = createRef<HTMLDivElement>();
constructor(props: menuItemProps) {
super(props)
this.state = {
top: 0,
left: 0,
showChild: false,
hoverMenu: false
}
}
handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
e.preventDefault()
this.props.onChildClick(this.props.item)
}
getTop = () => {
return this.state.top
}
getLeft = () => {
return this.state.left
}
handleMouseOver = (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
this.setState({ hoverMenu: true })
if (this.props.children) {
this.setState({ showChild: true })
}
}
handleMouseLeave = (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
this.setState({ hoverMenu: false, showChild: false })
}
render(): ReactNode {
return (
<>
<div key={this.props.item.id} className={this.state.hoverMenu ? "hightlight" : ""} ref={this.innerRef} id="menu-item" onClick={this.handleClick} onMouseEnter={this.handleMouseOver} onMouseLeave={() => this.setState({ hoverMenu: false, showChild: false })}>
<QuestionCircleOutlined />
<div id="item-context">{this.props.item.value}</div>
{this.props.children ? <RightOutlined /> : null}
{this.state.showChild ?
<div id="children-menu" style={{ left: this.getLeft() + "px", top: this.getTop() + "px" }}>
{this.props.children}
</div>
: null}
</div>
</>
)
}
}
export default Content
菜单样式
#content-menu {
box-shadow: 0 6px 16px 0 rgba(0,0,0,0.08), 0 3px 6px -4px rgba(0,0,0,0.12), 0 9px 28px 8px rgba(0,0,0,0.05);
border-radius: 4px;
position: relative;
width: 200px;
background-color: rgb(255, 255, 255);
}
#menu-item{
display: flex;
justify-content: space-between;
align-items: center;
padding: 3px 10px;
height: 26px;
#item-context{
padding: 0 10px;
max-width: 100px;
margin-right: auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.hightlight{
color: #1677ff;
cursor: pointer;
}
#children-menu{
color: initial;
box-shadow: 0 6px 16px 0 rgba(0,0,0,0.08), 0 3px 6px -4px rgba(0,0,0,0.12), 0 9px 28px 8px rgba(0,0,0,0.05);
position: absolute;
width: 200px;
background-color: rgb(255, 255, 255);
}
感谢交流、留言!!!

浙公网安备 33010602011771号