Просмотр исходного кода

Merge pull request #16857 from overleaf/ii-share-modal-click

[web] Clickable share modal buttons

GitOrigin-RevId: db6bef9b63e98007741d88b291a282d6ea4a68c5
ilkin-overleaf 2 лет назад
Родитель
Сommit
2f803292b4

+ 23 - 8
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 (
-    <Form onSubmit={handleSubmit}>
+    <Form>
       <FormGroup>
         <SelectCollaborators
           loading={!nonMemberContacts}
@@ -151,9 +162,13 @@ export default function AddCollaborators() {
             <option value="readOnly">{t('read_only')}</option>
           </FormControl>
           <span>&nbsp;&nbsp;</span>
-          <Button type="submit" bsStyle="primary">
+          <ClickableElementEnhancer
+            as={Button}
+            onClick={handleSubmit}
+            bsStyle="primary"
+          >
             {t('share')}
-          </Button>
+          </ClickableElementEnhancer>
         </div>
       </FormGroup>
     </Form>

+ 5 - 3
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({
         </div>
 
         <div className="modal-footer-right">
-          <Button
-            type="button"
+          <ClickableElementEnhancer
             onClick={cancel}
+            as={Button}
+            type="button"
             bsStyle={null}
             className="btn-secondary"
             disabled={inFlight}
           >
             {t('close')}
-          </Button>
+          </ClickableElementEnhancer>
         </div>
       </Modal.Footer>
     </AccessibleModal>

+ 75 - 0
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<E extends React.ElementType> =
+  MergeAndOverride<
+    PolymorphicComponentProps<E>,
+    ClickableElementEnhancerOwnProps
+  >
+
+function ClickableElementEnhancer<E extends React.ElementType>({
+  onClick,
+  onMouseDown,
+  offset = 50, // the offset around the clicked element which should still trigger the click
+  ...rest
+}: ClickableElementEnhancerProps<E>) {
+  const isClickedRef = useRef(false)
+  const elRectRef = useRef<DOMRect>()
+  const restProps = rest as PolymorphicComponentProps<E>
+
+  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 <PolymorphicComponent onMouseDown={handleMouseDown} {...restProps} />
+}
+
+export default ClickableElementEnhancer

+ 19 - 0
services/web/frontend/js/shared/components/polymorphic-component.tsx

@@ -0,0 +1,19 @@
+import { MergeAndOverride } from '../../../../types/utils'
+
+type PolymorphicComponentOwnProps<E extends React.ElementType> = {
+  as?: E
+}
+
+export type PolymorphicComponentProps<E extends React.ElementType> =
+  MergeAndOverride<React.ComponentProps<E>, PolymorphicComponentOwnProps<E>>
+
+function PolymorphicComponent<E extends React.ElementType = 'div'>({
+  as,
+  ...props
+}: PolymorphicComponentProps<E>) {
+  const Component = as || 'div'
+
+  return <Component {...props} />
+}
+
+export default PolymorphicComponent

+ 4 - 4
services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx

@@ -124,8 +124,8 @@ describe('<ShareProjectModal/>', 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('<ShareProjectModal/>', 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('<ShareProjectModal/>', function () {
       )
 
       expect(submitButton.disabled).to.be.false
-      submitButton.click()
+      await userEvent.click(submitButton)
       await fetchMock.flush(true)
       expect(fetchMock.done()).to.be.true
     }