From fdac299fc7485c9bb94192dfb46f8ded180b713f Mon Sep 17 00:00:00 2001 From: Maksim Eltyshev Date: Thu, 17 Jul 2025 17:43:01 +0200 Subject: [PATCH] feat: Open card actions on right-click --- client/src/components/cards/Card/Card.jsx | 20 +++++++++++++++++--- client/src/lib/popup/use-popup.jsx | 20 ++++++++++++++++---- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/client/src/components/cards/Card/Card.jsx b/client/src/components/cards/Card/Card.jsx index c4d12b1a..37e614c0 100755 --- a/client/src/components/cards/Card/Card.jsx +++ b/client/src/components/cards/Card/Card.jsx @@ -5,13 +5,13 @@ import upperFirst from 'lodash/upperFirst'; import camelCase from 'lodash/camelCase'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { useDispatch, useSelector } from 'react-redux'; import { Button, Icon } from 'semantic-ui-react'; import { push } from '../../../lib/redux-router'; -import { usePopup } from '../../../lib/popup'; +import { closePopup, usePopup } from '../../../lib/popup'; import selectors from '../../../selectors'; import Paths from '../../../constants/Paths'; @@ -51,6 +51,8 @@ const Card = React.memo(({ id, isInline }) => { const dispatch = useDispatch(); const [isEditNameOpened, setIsEditNameOpened] = useState(false); + const actionsPopupRef = useRef(null); + const handleClick = useCallback(() => { if (document.activeElement) { document.activeElement.blur(); @@ -59,6 +61,17 @@ const Card = React.memo(({ id, isInline }) => { dispatch(push(Paths.CARDS.replace(':id', id))); }, [id, dispatch]); + const handleContextMenu = useCallback((event) => { + if (!actionsPopupRef.current) { + return; + } + + event.preventDefault(); + + closePopup(); + actionsPopupRef.current.open(); + }, []); + const handleNameEdit = useCallback(() => { setIsEditNameOpened(true); }, []); @@ -110,12 +123,13 @@ const Card = React.memo(({ id, isInline }) => {
{colorLineNode}
{canUseActions && ( - + diff --git a/client/src/lib/popup/use-popup.jsx b/client/src/lib/popup/use-popup.jsx index 58ef95a8..d0f59110 100644 --- a/client/src/lib/popup/use-popup.jsx +++ b/client/src/lib/popup/use-popup.jsx @@ -4,7 +4,7 @@ */ import { ResizeObserver } from '@juggle/resize-observer'; -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import { Button, Popup as SemanticUIPopup } from 'semantic-ui-react'; @@ -12,13 +12,13 @@ import styles from './Popup.module.css'; export default (Step, { position, onOpen, onClose } = {}) => { return useMemo(() => { - const Popup = React.memo(({ children, ...stepProps }) => { + const Popup = React.forwardRef(({ children, ...stepProps }, ref) => { const [isOpened, setIsOpened] = useState(false); const wrapperRef = useRef(null); const resizeObserverRef = useRef(null); - const handleOpen = useCallback(() => { + const open = useCallback(() => { setIsOpened(true); if (onOpen) { @@ -26,6 +26,10 @@ export default (Step, { position, onOpen, onClose } = {}) => { } }, []); + const handleOpen = useCallback(() => { + open(); + }, [open]); + const handleClose = useCallback(() => { setIsOpened(false); }, []); @@ -73,6 +77,14 @@ export default (Step, { position, onOpen, onClose } = {}) => { resizeObserverRef.current.observe(element); }, []); + useImperativeHandle( + ref, + () => ({ + open, + }), + [open], + ); + const tigger = React.cloneElement(children, { onClick: handleTriggerClick, }); @@ -116,6 +128,6 @@ export default (Step, { position, onOpen, onClose } = {}) => { children: PropTypes.node.isRequired, }; - return Popup; + return React.memo(Popup); }, [position, onOpen, onClose]); };