diff --git a/services/web/frontend/js/features/share-project-modal/components/add-collaborators.jsx b/services/web/frontend/js/features/share-project-modal/components/add-collaborators.jsx index 822d2cf6f3..e53f7b33eb 100644 --- a/services/web/frontend/js/features/share-project-modal/components/add-collaborators.jsx +++ b/services/web/frontend/js/features/share-project-modal/components/add-collaborators.jsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from 'react' +import { useState, useMemo, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { Form, FormGroup, FormControl, Button } from 'react-bootstrap' import { useMultipleSelection } from 'downshift' @@ -9,6 +9,7 @@ import { useUserContacts } from '../hooks/use-user-contacts' import useIsMounted from '../../../shared/hooks/use-is-mounted' import { useProjectContext } from '../../../shared/context/project-context' import { sendMB } from '../../../infrastructure/event-tracking' +import ClickableElementEnhancer from '@/shared/components/clickable-element-enhancer' export default function AddCollaborators() { const [privileges, setPrivileges] = useState('readAndWrite') @@ -45,9 +46,7 @@ export default function AddCollaborators() { const { reset, selectedItems } = multipleSelectionProps - async function handleSubmit(event) { - event.preventDefault() - + const handleSubmit = useCallback(async () => { if (!selectedItems.length) { return } @@ -125,10 +124,22 @@ export default function AddCollaborators() { } setInFlight(false) - } + }, [ + currentMemberEmails, + invites, + isMounted, + members, + privileges, + projectId, + reset, + selectedItems, + setError, + setInFlight, + updateProject, + ]) return ( -
+ {t('read_only')}    - +
diff --git a/services/web/frontend/js/features/share-project-modal/components/share-project-modal-content.tsx b/services/web/frontend/js/features/share-project-modal/components/share-project-modal-content.tsx index 77d173b317..ec89b2dd1f 100644 --- a/services/web/frontend/js/features/share-project-modal/components/share-project-modal-content.tsx +++ b/services/web/frontend/js/features/share-project-modal/components/share-project-modal-content.tsx @@ -5,6 +5,7 @@ import AccessibleModal from '../../../shared/components/accessible-modal' import { useEditorContext } from '../../../shared/context/editor-context' import { lazy, Suspense } from 'react' import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner' +import ClickableElementEnhancer from '@/shared/components/clickable-element-enhancer' const ReadOnlyTokenLink = lazy(() => import('./link-sharing').then(({ ReadOnlyTokenLink }) => ({ @@ -63,15 +64,16 @@ export default function ShareProjectModalContent({
- +
diff --git a/services/web/frontend/js/shared/components/clickable-element-enhancer.tsx b/services/web/frontend/js/shared/components/clickable-element-enhancer.tsx new file mode 100644 index 0000000000..b028db07fd --- /dev/null +++ b/services/web/frontend/js/shared/components/clickable-element-enhancer.tsx @@ -0,0 +1,75 @@ +import { useRef, useEffect } from 'react' +import PolymorphicComponent, { + PolymorphicComponentProps, +} from '@/shared/components/polymorphic-component' +import { MergeAndOverride } from '../../../../types/utils' + +// Performs a click event on elements that has been clicked, +// but when releasing the mouse button are no longer hovered +// by the cursor (which by default cancels the event). + +type ClickableElementEnhancerOwnProps = { + onClick: () => void + onMouseDown?: (e: React.MouseEvent) => void + offset?: number +} + +type ClickableElementEnhancerProps = + MergeAndOverride< + PolymorphicComponentProps, + ClickableElementEnhancerOwnProps + > + +function ClickableElementEnhancer({ + onClick, + onMouseDown, + offset = 50, // the offset around the clicked element which should still trigger the click + ...rest +}: ClickableElementEnhancerProps) { + const isClickedRef = useRef(false) + const elRectRef = useRef() + const restProps = rest as PolymorphicComponentProps + + const handleMouseDown = (e: React.MouseEvent) => { + isClickedRef.current = true + elRectRef.current = (e.target as HTMLElement).getBoundingClientRect() + onMouseDown?.(e) + } + + useEffect(() => { + const handleMouseUp = (e: MouseEvent) => { + if (isClickedRef.current) { + isClickedRef.current = false + + if (!elRectRef.current) { + return + } + + const halfWidth = elRectRef.current.width / 2 + const halfHeight = elRectRef.current.height / 2 + + const centerX = elRectRef.current.x + halfWidth + const centerY = elRectRef.current.y + halfHeight + + const deltaX = Math.abs(e.clientX - centerX) + const deltaY = Math.abs(e.clientY - centerY) + + // Check if the mouse has moved significantly from the element position + if (deltaX < halfWidth + offset && deltaY < halfHeight + offset) { + // If the mouse hasn't moved much, consider it a click + onClick() + } + } + } + + document.addEventListener('mouseup', handleMouseUp) + + return () => { + document.removeEventListener('mouseup', handleMouseUp) + } + }, [onClick, offset]) + + return +} + +export default ClickableElementEnhancer diff --git a/services/web/frontend/js/shared/components/polymorphic-component.tsx b/services/web/frontend/js/shared/components/polymorphic-component.tsx new file mode 100644 index 0000000000..44cbb0ea31 --- /dev/null +++ b/services/web/frontend/js/shared/components/polymorphic-component.tsx @@ -0,0 +1,19 @@ +import { MergeAndOverride } from '../../../../types/utils' + +type PolymorphicComponentOwnProps = { + as?: E +} + +export type PolymorphicComponentProps = + MergeAndOverride, PolymorphicComponentOwnProps> + +function PolymorphicComponent({ + as, + ...props +}: PolymorphicComponentProps) { + const Component = as || 'div' + + return +} + +export default PolymorphicComponent diff --git a/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx b/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx index 761bcfe905..c41efdcc52 100644 --- a/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx +++ b/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx @@ -124,8 +124,8 @@ describe('', function () { { name: 'Close' } ) - fireEvent.click(headerCloseButton) - fireEvent.click(footerCloseButton) + await userEvent.click(headerCloseButton) + await userEvent.click(footerCloseButton) expect(handleHide.callCount).to.equal(2) }) @@ -620,7 +620,7 @@ describe('', function () { fireEvent.change(privilegesElement, { target: { value: 'readOnly' } }) const submitButton = screen.getByRole('button', { name: 'Share' }) - submitButton.click() + await userEvent.click(submitButton) let calls await waitFor( @@ -713,7 +713,7 @@ describe('', function () { ) expect(submitButton.disabled).to.be.false - submitButton.click() + await userEvent.click(submitButton) await fetchMock.flush(true) expect(fetchMock.done()).to.be.true }