github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/frontend-service/src/ui/components/ServiceLane/DotsMenu.tsx (about) 1 /*This file is part of kuberpult. 2 3 Kuberpult is free software: you can redistribute it and/or modify 4 it under the terms of the Expat(MIT) License as published by 5 the Free Software Foundation. 6 7 Kuberpult is distributed in the hope that it will be useful, 8 but WITHOUT ANY WARRANTY; without even the implied warranty of 9 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 MIT License for more details. 11 12 You should have received a copy of the MIT License 13 along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>. 14 15 Copyright 2023 freiheit.com*/ 16 import * as React from 'react'; 17 import { Button } from '../button'; 18 import { useState } from 'react'; 19 20 export type DotsMenuButton = { 21 label: string; 22 onClick: () => void; 23 icon?: JSX.Element; 24 }; 25 26 export type DotsMenuProps = { 27 buttons: DotsMenuButton[]; 28 }; 29 30 export const DotsMenu: React.FC<DotsMenuProps> = (props) => { 31 const [open, setOpen] = useState(false); 32 33 const initialRef: HTMLElement | null = null; 34 const rootRef = React.useRef(initialRef); 35 36 const openMenu = React.useCallback(() => { 37 setOpen(true); 38 }, []); 39 const closeMenu = React.useCallback(() => { 40 setOpen(false); 41 }, []); 42 43 const memoizedOnClick = React.useCallback( 44 (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { 45 const index = e.currentTarget.id; 46 props.buttons[Number(index)].onClick(); 47 setOpen(false); 48 }, 49 [props.buttons] 50 ); 51 52 React.useEffect(() => { 53 if (!open) { 54 return () => {}; 55 } 56 const winListener = (event: KeyboardEvent): void => { 57 if (event.key === 'Escape') { 58 closeMenu(); 59 } 60 }; 61 const docListener = (event: MouseEvent): void => { 62 if (!(event.target instanceof HTMLElement)) { 63 return; 64 } 65 const eventTarget: HTMLElement = event.target; 66 67 if (rootRef.current === null) { 68 return; 69 } 70 const rootRefCurrent: HTMLElement = rootRef.current; 71 72 const isOutside: boolean = !rootRefCurrent.contains(eventTarget); 73 if (isOutside) { 74 closeMenu(); 75 } 76 }; 77 window.addEventListener('keyup', winListener); 78 document.addEventListener('pointerup', docListener); 79 return () => { 80 document.removeEventListener('keyup', winListener); 81 document.removeEventListener('pointerup', docListener); 82 }; 83 }, [closeMenu, open]); 84 85 if (!open) { 86 return ( 87 <div className={'dots-menu dots-menu-hidden'}> 88 <Button className="mdc-button--unelevated" label={'⋮'} onClick={openMenu} /> 89 </div> 90 ); 91 } 92 93 return ( 94 <div className={'dots-menu dots-menu-open'} ref={rootRef}> 95 <ul className={'list'}> 96 {props.buttons.map((button, index) => ( 97 <li className={'item'} key={'button-menu-' + String(index)}> 98 <Button 99 id={String(index)} 100 icon={button.icon} 101 className="mdc-button--unelevated" 102 label={button.label} 103 onClick={memoizedOnClick} 104 /> 105 </li> 106 ))} 107 </ul> 108 </div> 109 ); 110 };