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