1- import React, { useEffect, useRef, useState } from 'react';
1+ import React, { createRef } from 'react';
22import styled from 'styled-components/macro';
33import tw from 'twin.macro';
44import Fade from '@/components/elements/Fade';
@@ -17,64 +17,91 @@ export const DropdownButtonRow = styled.button<{ danger?: boolean }>`
1717 }
1818`;
1919
20- const DropdownMenu = ({ renderToggle, children }: Props) => {
21- const menu = useRef<HTMLDivElement>(null) ;
22- const [ posX, setPosX ] = useState(0) ;
23- const [ visible, setVisible ] = useState(false);
20+ interface State {
21+ posX: number ;
22+ visible: boolean ;
23+ }
2424
25- const onClickHandler = (e: React.MouseEvent<any, MouseEvent>) => {
26- e.preventDefault();
25+ class DropdownMenu extends React.PureComponent<Props, State> {
26+ menu = createRef<HTMLDivElement>();
27+
28+ state: State = {
29+ posX: 0,
30+ visible: false,
31+ };
32+
33+ componentWillUnmount () {
34+ this.removeListeners();
35+ }
36+
37+ componentDidUpdate (prevProps: Readonly<Props>, prevState: Readonly<State>) {
38+ const menu = this.menu.current;
39+
40+ if (this.state.visible && !prevState.visible && menu) {
41+ document.addEventListener('click', this.windowListener);
42+ document.addEventListener('contextmenu', this.contextMenuListener);
43+ menu.setAttribute(
44+ 'style', `left: ${Math.round(this.state.posX - menu.clientWidth)}px`,
45+ );
46+ }
47+
48+ if (!this.state.visible && prevState.visible) {
49+ this.removeListeners();
50+ }
51+ }
52+
53+ removeListeners = () => {
54+ document.removeEventListener('click', this.windowListener);
55+ document.removeEventListener('contextmenu', this.contextMenuListener);
56+ };
2757
28- !visible && setPosX(e.clientX);
29- setVisible(s => !s);
58+ onClickHandler = (e: React.MouseEvent<any, MouseEvent>) => {
59+ e.preventDefault();
60+ this.triggerMenu(e.clientX);
3061 };
3162
32- const windowListener = (e: MouseEvent) => {
33- if (e.button === 2 || !visible || !menu.current) {
63+ contextMenuListener = () => this.setState({ visible: false });
64+
65+ windowListener = (e: MouseEvent) => {
66+ const menu = this.menu.current;
67+
68+ if (e.button === 2 || !this.state.visible || !menu) {
3469 return;
3570 }
3671
37- if (e.target === menu.current || menu.current .contains(e.target as Node)) {
72+ if (e.target === menu || menu.contains(e.target as Node)) {
3873 return;
3974 }
4075
41- if (e.target !== menu.current && !menu.current .contains(e.target as Node)) {
42- setVisible( false);
76+ if (e.target !== menu && !menu.contains(e.target as Node)) {
77+ this.setState({ visible: false } );
4378 }
4479 };
4580
46- useEffect(( ) => {
47- if (! visible || !menu.current) {
48- return;
49- }
81+ triggerMenu = (posX: number ) => this.setState(s => ( {
82+ posX: !s. visible ? posX : s.posX,
83+ visible: !s.visible,
84+ }));
5085
51- document.addEventListener('click', windowListener);
52- menu.current.setAttribute(
53- 'style', `left: ${Math.round(posX - menu.current.clientWidth)}px`,
86+ render () {
87+ return (
88+ <div>
89+ {this.props.renderToggle(this.onClickHandler)}
90+ <Fade timeout={150} in={this.state.visible} unmountOnExit>
91+ <div
92+ ref={this.menu}
93+ onClick={e => {
94+ e.stopPropagation();
95+ this.setState({ visible: false });
96+ }}
97+ css={tw`absolute bg-white p-2 rounded border border-neutral-700 shadow-lg text-neutral-500 min-w-48`}
98+ >
99+ {this.props.children}
100+ </div>
101+ </Fade>
102+ </div>
54103 );
55-
56- return () => {
57- document.removeEventListener('click', windowListener);
58- };
59- }, [ visible ]);
60-
61- return (
62- <div>
63- {renderToggle(onClickHandler)}
64- <Fade timeout={150} in={visible} unmountOnExit>
65- <div
66- ref={menu}
67- onClick={e => {
68- e.stopPropagation();
69- setVisible(false);
70- }}
71- css={tw`absolute bg-white p-2 rounded border border-neutral-700 shadow-lg text-neutral-500 min-w-48`}
72- >
73- {children}
74- </div>
75- </Fade>
76- </div>
77- );
78- };
104+ }
105+ }
79106
80107export default DropdownMenu;
0 commit comments