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

feat: add hover tooltip for off-screen indicators displaying user name and color

Yuki Takei 2 дней назад
Родитель
Сommit
c25b50ba45

+ 17 - 0
packages/editor/src/client/services-internal/extensions/y-rich-cursors/off-screen-indicator.spec.ts

@@ -196,6 +196,23 @@ describe('createOffScreenIndicator', () => {
   });
 });
 
+describe('createOffScreenIndicator — hover tooltip', () => {
+  it('renders a tooltip with the user name and cursor color', () => {
+    const el = createOffScreenIndicator({
+      direction: 'above',
+      clientId: 1,
+      color: '#ff0000',
+      name: 'Alice',
+      imageUrlCached: '/avatar.png',
+      isActive: false,
+    });
+
+    const tooltip = el.querySelector('.cm-offScreenTooltip') as HTMLElement;
+    expect(tooltip.textContent).toBe('Alice');
+    expect(tooltip.style.backgroundColor).toBe('#ff0000');
+  });
+});
+
 /**
  * Task 20.1 — Click behavior tests for off-screen indicators
  * Requirements: 6.6, 6.7

+ 8 - 0
packages/editor/src/client/services-internal/extensions/y-rich-cursors/off-screen-indicator.ts

@@ -86,15 +86,23 @@ export function createOffScreenIndicator(
     avatarEl = initials;
   }
 
+  const tooltip = document.createElement('span');
+  tooltip.className = 'cm-offScreenTooltip';
+  tooltip.style.backgroundColor = color;
+  tooltip.textContent = name;
+
   // "above": arrow points up (toward the off-screen cursor), avatar below
   // "below": avatar above, arrow points down (toward the off-screen cursor)
   if (direction === 'above') {
     indicator.appendChild(arrow);
     indicator.appendChild(avatarEl);
+    tooltip.style.bottom = '0';
   } else {
     indicator.appendChild(avatarEl);
     indicator.appendChild(arrow);
+    tooltip.style.top = '0';
   }
+  indicator.appendChild(tooltip);
 
   return indicator;
 }

+ 22 - 12
packages/editor/src/client/services-internal/extensions/y-rich-cursors/theme.ts

@@ -127,6 +127,9 @@ export const richCursorsTheme = EditorView.baseTheme({
   // bounding box. clip-path trims both top and bottom simultaneously so the
   // visible triangle is flush with both the avatar AND the editor edge.
   // Negative margins compensate the clip so the flex layout stays correct.
+  // Arrow — always fully opaque; negative margins trim the ~20% internal
+  // whitespace of Material Symbols so the triangle sits flush with the avatar
+  // and the editor edge.
   '.cm-offScreenArrow': {
     fontFamily: 'var(--grw-font-family-material-symbols-outlined)',
     fontSize: '20px',
@@ -134,18 +137,6 @@ export const richCursorsTheme = EditorView.baseTheme({
     display: 'block',
     userSelect: 'none',
     opacity: '1',
-  },
-  // "above" indicator: arrow (first child) → avatar (second child)
-  //   top clip  = trim the whitespace at the editor edge
-  //   bottom clip = trim the gap between arrow and avatar
-  '.cm-offScreenArrow:first-child': {
-    marginTop: '-8px',
-    marginBottom: '-8px',
-  },
-  // "below" indicator: avatar (first child) → arrow (last child)
-  //   top clip  = trim the gap between avatar and arrow
-  //   bottom clip = trim the whitespace at the editor edge
-  '.cm-offScreenArrow:last-child': {
     marginTop: '-8px',
     marginBottom: '-8px',
   },
@@ -181,4 +172,23 @@ export const richCursorsTheme = EditorView.baseTheme({
     {
       opacity: '1',
     },
+
+  // Name tooltip — appears to the right of the avatar on hover.
+  // top/bottom set inline in JS to align with the avatar per direction.
+  '.cm-offScreenTooltip': {
+    display: 'none',
+    position: 'absolute',
+    left: '100%',
+    marginLeft: '2px',
+    whiteSpace: 'nowrap',
+    padding: '0 6px',
+    borderRadius: '3px',
+    color: 'white',
+    fontSize: '12px',
+    height: AVATAR_SIZE,
+    lineHeight: AVATAR_SIZE,
+  },
+  '.cm-offScreenIndicator:hover .cm-offScreenTooltip': {
+    display: 'block',
+  },
 });