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

Merge pull request #20617 from overleaf/dk-review-indicator-hover

Improve "mini" view popovers for new review panel

GitOrigin-RevId: ff5fc0af70bd9660d5cc17437b25824ef4c9a704
David 1 год назад
Родитель
Сommit
56d396a6cf

+ 151 - 125
services/web/frontend/js/features/review-panel-new/components/review-panel-change.tsx

@@ -23,146 +23,172 @@ export const ReviewPanelChange = memo<{
   editable?: boolean
   docId: string
   hoverRanges?: boolean
-}>(({ change, aggregate, top, docId, hoverRanges, editable = true }) => {
-  const { t } = useTranslation()
-  const { acceptChanges, rejectChanges } = useRangesActionsContext()
-  const permissions = usePermissionsContext()
-  const changesUsers = useChangesUsersContext()
+  hovered?: boolean
+  onEnter?: () => void
+  onLeave?: () => void
+}>(
+  ({
+    change,
+    aggregate,
+    top,
+    docId,
+    hoverRanges,
+    editable = true,
+    hovered,
+    onEnter,
+    onLeave,
+  }) => {
+    const { t } = useTranslation()
+    const { acceptChanges, rejectChanges } = useRangesActionsContext()
+    const permissions = usePermissionsContext()
+    const changesUsers = useChangesUsersContext()
 
-  if (!changesUsers) {
-    // if users are not loaded yet, do not show "Unknown" user
-    return null
-  }
+    if (!changesUsers) {
+      // if users are not loaded yet, do not show "Unknown" user
+      return null
+    }
 
-  return (
-    <ReviewPanelEntry
-      className={classnames('review-panel-entry-change', {
-        'review-panel-entry-insert': 'i' in change.op,
-        'review-panel-entry-delete': 'd' in change.op,
-        // TODO: aggregate
-      })}
-      top={top}
-      op={change.op}
-      position={change.op.p}
-      docId={docId}
-      hoverRanges={hoverRanges}
-    >
-      <div className="review-panel-entry-indicator">
-        <MaterialIcon type="edit" className="review-panel-entry-icon" />
-      </div>
+    return (
+      <ReviewPanelEntry
+        className={classnames('review-panel-entry-change', {
+          'review-panel-entry-insert': 'i' in change.op,
+          'review-panel-entry-delete': 'd' in change.op,
+          'review-panel-entry-hover': hovered,
+          // TODO: aggregate
+        })}
+        top={top}
+        op={change.op}
+        position={change.op.p}
+        docId={docId}
+        hoverRanges={hoverRanges}
+      >
+        <div
+          className="review-panel-entry-indicator"
+          onMouseEnter={onEnter}
+          onMouseLeave={onLeave}
+        >
+          <MaterialIcon type="edit" className="review-panel-entry-icon" />
+        </div>
 
-      <div className="review-panel-entry-content">
-        <div className="review-panel-entry-header">
-          <div>
-            <div className="review-panel-entry-user">
-              <ReviewPanelChangeUser change={change} />
-            </div>
-            <div className="review-panel-entry-time">
-              {formatTimeBasedOnYear(change.metadata?.ts)}
+        <div
+          className="review-panel-entry-content"
+          onMouseEnter={onEnter}
+          onMouseLeave={onLeave}
+        >
+          <div className="review-panel-entry-header">
+            <div>
+              <div className="review-panel-entry-user">
+                <ReviewPanelChangeUser change={change} />
+              </div>
+              <div className="review-panel-entry-time">
+                {formatTimeBasedOnYear(change.metadata?.ts)}
+              </div>
             </div>
-          </div>
-          {editable && permissions.write && (
-            <div className="review-panel-entry-actions">
-              <Tooltip
-                id="accept-change"
-                overlayProps={{ placement: 'bottom' }}
-                description={t('accept_change')}
-              >
-                <Button
-                  onClick={() =>
-                    aggregate
-                      ? acceptChanges(change.id, aggregate.id)
-                      : acceptChanges(change.id)
-                  }
-                  bsStyle={null}
+            {editable && permissions.write && (
+              <div className="review-panel-entry-actions">
+                <Tooltip
+                  id="accept-change"
+                  overlayProps={{ placement: 'bottom' }}
+                  description={t('accept_change')}
+                  tooltipProps={{ className: 'review-panel-tooltip' }}
                 >
-                  <MaterialIcon
-                    type="check"
-                    className="review-panel-entry-actions-icon"
-                    accessibilityLabel={t('accept_change')}
-                  />
-                </Button>
-              </Tooltip>
+                  <Button
+                    onClick={() =>
+                      aggregate
+                        ? acceptChanges(change.id, aggregate.id)
+                        : acceptChanges(change.id)
+                    }
+                    bsStyle={null}
+                  >
+                    <MaterialIcon
+                      type="check"
+                      className="review-panel-entry-actions-icon"
+                      accessibilityLabel={t('accept_change')}
+                    />
+                  </Button>
+                </Tooltip>
 
-              <Tooltip
-                id="reject-change"
-                description={t('reject_change')}
-                overlayProps={{ placement: 'bottom' }}
-              >
-                <Button
-                  bsStyle={null}
-                  onClick={() =>
-                    aggregate
-                      ? rejectChanges(change.id, aggregate.id)
-                      : rejectChanges(change.id)
-                  }
+                <Tooltip
+                  id="reject-change"
+                  description={t('reject_change')}
+                  overlayProps={{ placement: 'bottom' }}
+                  tooltipProps={{ className: 'review-panel-tooltip' }}
                 >
+                  <Button
+                    bsStyle={null}
+                    onClick={() =>
+                      aggregate
+                        ? rejectChanges(change.id, aggregate.id)
+                        : rejectChanges(change.id)
+                    }
+                  >
+                    <MaterialIcon
+                      className="review-panel-entry-actions-icon"
+                      accessibilityLabel={t('reject_change')}
+                      type="close"
+                    />
+                  </Button>
+                </Tooltip>
+              </div>
+            )}
+          </div>
+
+          <div className="review-panel-change-body">
+            {'i' in change.op && (
+              <>
+                {aggregate ? (
                   <MaterialIcon
-                    className="review-panel-entry-actions-icon"
-                    accessibilityLabel={t('reject_change')}
-                    type="close"
+                    className="review-panel-entry-icon review-panel-entry-icon-changed"
+                    type="edit"
                   />
-                </Button>
-              </Tooltip>
-            </div>
-          )}
-        </div>
+                ) : (
+                  <MaterialIcon
+                    className="review-panel-entry-icon review-panel-entry-icon-accept"
+                    type="add_circle"
+                  />
+                )}
 
-        <div className="review-panel-change-body">
-          {'i' in change.op && (
-            <>
-              {aggregate ? (
-                <MaterialIcon
-                  className="review-panel-entry-icon review-panel-entry-icon-changed"
-                  type="edit"
-                />
-              ) : (
+                {aggregate ? (
+                  <span>
+                    {t('aggregate_changed')}:{' '}
+                    <del className="review-panel-content-highlight">
+                      {aggregate.op.d}
+                    </del>{' '}
+                    {t('aggregate_to')}{' '}
+                    <ins className="review-panel-content-highlight">
+                      {change.op.i}
+                    </ins>
+                  </span>
+                ) : (
+                  <span>
+                    {t('tracked_change_added')}:&nbsp;
+                    <ins className="review-panel-content-highlight">
+                      {change.op.i}
+                    </ins>
+                  </span>
+                )}
+              </>
+            )}
+
+            {'d' in change.op && (
+              <>
                 <MaterialIcon
-                  className="review-panel-entry-icon review-panel-entry-icon-accept"
-                  type="add_circle"
+                  className="review-panel-entry-icon review-panel-entry-icon-reject"
+                  type="delete"
                 />
-              )}
 
-              {aggregate ? (
                 <span>
-                  {t('aggregate_changed')}:{' '}
+                  {t('tracked_change_deleted')}:&nbsp;
                   <del className="review-panel-content-highlight">
-                    {aggregate.op.d}
-                  </del>{' '}
-                  {t('aggregate_to')}{' '}
-                  <ins className="review-panel-content-highlight">
-                    {change.op.i}
-                  </ins>
-                </span>
-              ) : (
-                <span>
-                  {t('tracked_change_added')}:&nbsp;
-                  <ins className="review-panel-content-highlight">
-                    {change.op.i}
-                  </ins>
+                    {change.op.d}
+                  </del>
                 </span>
-              )}
-            </>
-          )}
-
-          {'d' in change.op && (
-            <>
-              <MaterialIcon
-                className="review-panel-entry-icon review-panel-entry-icon-reject"
-                type="delete"
-              />
-
-              <span>
-                {t('tracked_change_deleted')}:&nbsp;
-                <del className="review-panel-content-highlight">
-                  {change.op.d}
-                </del>
-              </span>
-            </>
-          )}
+              </>
+            )}
+          </div>
         </div>
-      </div>
-    </ReviewPanelEntry>
-  )
-})
+      </ReviewPanelEntry>
+    )
+  }
+)
 ReviewPanelChange.displayName = 'ReviewPanelChange'

+ 8 - 2
services/web/frontend/js/features/review-panel-new/components/review-panel-comment-content.tsx

@@ -14,7 +14,9 @@ import useSubmittableTextInput from '../hooks/use-submittable-text-input'
 export const ReviewPanelCommentContent = memo<{
   comment: Change<CommentOperation>
   isResolved: boolean
-}>(({ comment, isResolved }) => {
+  onLeave?: () => void
+  onEnter?: () => void
+}>(({ comment, isResolved, onLeave, onEnter }) => {
   const { t } = useTranslation()
   const [submitting, setSubmitting] = useState(false)
   const [error, setError] = useState<Error>()
@@ -47,7 +49,11 @@ export const ReviewPanelCommentContent = memo<{
   }
 
   return (
-    <div className="review-panel-entry-content">
+    <div
+      className="review-panel-entry-content"
+      onMouseEnter={onEnter}
+      onMouseLeave={onLeave}
+    >
       {thread.messages.map((message, i) => {
         const isReply = i !== 0
 

+ 16 - 3
services/web/frontend/js/features/review-panel-new/components/review-panel-comment.tsx

@@ -11,7 +11,10 @@ export const ReviewPanelComment = memo<{
   docId: string
   top?: number
   hoverRanges?: boolean
-}>(({ comment, top, docId, hoverRanges }) => {
+  onEnter?: () => void
+  onLeave?: () => void
+  hovered?: boolean
+}>(({ comment, top, hovered, onEnter, onLeave, docId, hoverRanges }) => {
   const threads = useThreadsContext()
 
   const thread = threads?.[comment.op.t]
@@ -23,6 +26,7 @@ export const ReviewPanelComment = memo<{
     <ReviewPanelEntry
       className={classnames('review-panel-entry-comment', {
         'review-panel-entry-loaded': !!threads?.[comment.op.t],
+        'review-panel-entry-hover': hovered,
       })}
       docId={docId}
       top={top}
@@ -30,10 +34,19 @@ export const ReviewPanelComment = memo<{
       position={comment.op.p}
       hoverRanges={hoverRanges}
     >
-      <div className="review-panel-entry-indicator">
+      <div
+        className="review-panel-entry-indicator"
+        onMouseEnter={onEnter}
+        onMouseLeave={onLeave}
+      >
         <MaterialIcon type="comment" className="review-panel-entry-icon" />
       </div>
-      <ReviewPanelCommentContent comment={comment} isResolved={false} />
+      <ReviewPanelCommentContent
+        comment={comment}
+        isResolved={false}
+        onLeave={onLeave}
+        onEnter={onEnter}
+      />
     </ReviewPanelEntry>
   )
 })

+ 20 - 0
services/web/frontend/js/features/review-panel-new/components/review-panel-current-file.tsx

@@ -43,6 +43,20 @@ const ReviewPanelCurrentFile: FC = () => {
   const ranges = useRangesContext()
   const threads = useThreadsContext()
   const state = useCodeMirrorStateContext()
+  const [hoveredEntry, setHoveredEntry] = useState<string | null>(null)
+
+  const hoverTimeout = useRef<number>(0)
+  const handleEntryEnter = useCallback((id: string) => {
+    clearTimeout(hoverTimeout.current)
+    setHoveredEntry(id)
+  }, [])
+
+  const handleEntryLeave = useCallback((id: string) => {
+    clearTimeout(hoverTimeout.current)
+    hoverTimeout.current = window.setTimeout(() => {
+      setHoveredEntry(null)
+    }, 100)
+  }, [])
 
   const [aggregatedRanges, setAggregatedRanges] = useState<AggregatedRanges>()
 
@@ -298,6 +312,9 @@ const ReviewPanelCurrentFile: FC = () => {
                 change={change}
                 top={positions.get(change.id)}
                 aggregate={aggregatedRanges.aggregates.get(change.id)}
+                hovered={hoveredEntry === change.id}
+                onEnter={() => handleEntryEnter(change.id)}
+                onLeave={() => handleEntryLeave(change.id)}
               />
             )
         )}
@@ -310,6 +327,9 @@ const ReviewPanelCurrentFile: FC = () => {
                 key={comment.id}
                 comment={comment}
                 top={positions.get(comment.id)}
+                hovered={hoveredEntry === comment.id}
+                onEnter={() => handleEntryEnter(comment.id)}
+                onLeave={() => handleEntryLeave(comment.id)}
               />
             )
         )}

+ 1 - 0
services/web/frontend/js/features/review-panel-new/components/review-panel-message.tsx

@@ -106,6 +106,7 @@ export const ReviewPanelMessage: FC<{
               id="resolve-thread"
               overlayProps={{ placement: 'bottom' }}
               description={t('resolve_comment')}
+              tooltipProps={{ className: 'review-panel-tooltip' }}
             >
               <Button onClick={onResolve} bsStyle={null}>
                 <MaterialIcon

+ 6 - 4
services/web/frontend/stylesheets/app/editor/review-panel-new.less

@@ -554,15 +554,14 @@
       margin-left: 0;
       background-color: transparent;
       border: none;
+      width: 100%;
     }
 
     .review-panel-entry-indicator {
       position: absolute;
       left: 0;
-      top: 7px;
+      top: 0;
       display: flex;
-      width: 16px;
-      height: 16px;
       color: @content-secondary;
       cursor: pointer;
     }
@@ -576,7 +575,7 @@
       padding: @spacing-02;
     }
 
-    .review-panel-entry:hover {
+    .review-panel-entry-hover {
       .review-panel-entry-content {
         display: initial;
         position: absolute;
@@ -599,3 +598,6 @@
   align-items: center;
   gap: 2px;
 }
+.review-panel-tooltip {
+  pointer-events: none; // this is to prevent mouseLeave event from firing when hovering over the tooltip
+}