diff --git a/client/src/components/CardModal/DescriptionEdit.jsx b/client/src/components/CardModal/DescriptionEdit.jsx index 87092fce..2520b01b 100755 --- a/client/src/components/CardModal/DescriptionEdit.jsx +++ b/client/src/components/CardModal/DescriptionEdit.jsx @@ -1,8 +1,11 @@ -import React, { useCallback, useImperativeHandle, useMemo, useState } from 'react'; +import React, { useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; import { Button, Form } from 'semantic-ui-react'; import SimpleMDE from 'react-simplemde-editor'; +import { useClickAwayListener } from '../../lib/hooks'; + +import { useNestedRef } from '../../hooks'; import styles from './DescriptionEdit.module.scss'; @@ -11,6 +14,10 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate }, const [isOpened, setIsOpened] = useState(false); const [value, setValue] = useState(null); + const editorWrapperRef = useRef(null); + const codemirrorRef = useRef(null); + const [buttonRef, handleButtonRef] = useNestedRef(); + const open = useCallback(() => { setIsOpened(true); setValue(defaultValue || ''); @@ -55,6 +62,28 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate }, close(); }, [close]); + const handleAwayClick = useCallback(() => { + if (!isOpened) { + return; + } + + close(); + }, [isOpened, close]); + + const handleClickAwayCancel = useCallback(() => { + codemirrorRef.current.focus(); + }, []); + + const clickAwayProps = useClickAwayListener( + [editorWrapperRef, buttonRef], + handleAwayClick, + handleClickAwayCancel, + ); + + const handleGetCodemirrorInstance = useCallback((codemirror) => { + codemirrorRef.current = codemirror; + }, []); + const mdEditorOptions = useMemo( () => ({ autoDownloadFontAwesome: false, @@ -92,16 +121,20 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate }, return (
); diff --git a/client/src/hooks/index.js b/client/src/hooks/index.js index 77aee264..3fcaa54a 100644 --- a/client/src/hooks/index.js +++ b/client/src/hooks/index.js @@ -1,7 +1,8 @@ +import useNestedRef from './use-nested-ref'; import useField from './use-field'; import useForm from './use-form'; import useSteps from './use-steps'; import useModal from './use-modal'; import useClosableForm from './use-closable-form'; -export { useField, useForm, useSteps, useModal, useClosableForm }; +export { useNestedRef, useField, useForm, useSteps, useModal, useClosableForm }; diff --git a/client/src/hooks/use-nested-ref.js b/client/src/hooks/use-nested-ref.js new file mode 100644 index 00000000..24ca262e --- /dev/null +++ b/client/src/hooks/use-nested-ref.js @@ -0,0 +1,14 @@ +import { useCallback, useRef } from 'react'; + +export default (nestedRefName = 'ref') => { + const ref = useRef(null); + + const handleRef = useCallback( + (element) => { + ref.current = element?.[nestedRefName].current; + }, + [nestedRefName], + ); + + return [ref, handleRef]; +}; diff --git a/client/src/lib/hooks/index.js b/client/src/lib/hooks/index.js index 5835cc2a..2c86fcc2 100644 --- a/client/src/lib/hooks/index.js +++ b/client/src/lib/hooks/index.js @@ -2,5 +2,6 @@ import usePrevious from './use-previous'; import useToggle from './use-toggle'; import useForceUpdate from './use-force-update'; import useDidUpdate from './use-did-update'; +import useClickAwayListener from './use-click-away-listener'; -export { usePrevious, useToggle, useForceUpdate, useDidUpdate }; +export { usePrevious, useToggle, useForceUpdate, useDidUpdate, useClickAwayListener }; diff --git a/client/src/lib/hooks/use-click-away-listener.js b/client/src/lib/hooks/use-click-away-listener.js new file mode 100644 index 00000000..265a8642 --- /dev/null +++ b/client/src/lib/hooks/use-click-away-listener.js @@ -0,0 +1,45 @@ +import { useCallback, useEffect, useMemo, useRef } from 'react'; + +export default (elementRefs, onAwayClick, onCancel) => { + const pressedElement = useRef(null); + + const handlePress = useCallback((event) => { + pressedElement.current = event.target; + }, []); + + useEffect(() => { + const handleEvent = (event) => { + const element = elementRefs.find(({ current }) => current?.contains(event.target))?.current; + + if (element) { + if (!pressedElement.current || pressedElement.current !== element) { + onCancel(); + } + } else if (pressedElement.current) { + onCancel(); + } else { + onAwayClick(); + } + + pressedElement.current = null; + }; + + document.addEventListener('mouseup', handleEvent, true); + document.addEventListener('touchend', handleEvent, true); + + return () => { + document.removeEventListener('mouseup', handleEvent, true); + document.removeEventListener('touchend', handleEvent, true); + }; + }, [onAwayClick, onCancel]); // eslint-disable-line react-hooks/exhaustive-deps + + const props = useMemo( + () => ({ + onMouseDown: handlePress, + onTouchStart: handlePress, + }), + [handlePress], + ); + + return props; +};