Forráskód Böngészése

fix(web): expose copied status outside button

creatixchu 1 hónapja
szülő
commit
0936200e0e

+ 9 - 0
packages/client/ui-primitives/src/HoverCard.module.css

@@ -43,3 +43,12 @@
   line-height: 20px;
   text-align: center;
 }
+
+.status {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  overflow: hidden;
+  clip: rect(0 0 0 0);
+  white-space: nowrap;
+}

+ 7 - 3
packages/client/ui-primitives/src/HoverCard.tsx

@@ -157,8 +157,11 @@ export function HoverCard({
       onClick={copyable
         ? (e) => {
           const selection = window.getSelection()
-          if (selection !== null && !selection.isCollapsed && selection.rangeCount > 0
-            && selection.getRangeAt(0).intersectsNode(e.currentTarget)) return
+          if (selection !== null && !selection.isCollapsed) {
+            for (let i = 0; i < selection.rangeCount; i += 1) {
+              if (selection.getRangeAt(i).intersectsNode(e.currentTarget)) return
+            }
+          }
           void copy(copyText)
         }
         : undefined}
@@ -170,7 +173,7 @@ export function HoverCard({
         }
         : undefined}
     >
-      {copied ? <span className={css.copied} role="status">{copiedLabel}</span> : content}
+      {copied ? <span className={css.copied} aria-hidden="true">{copiedLabel}</span> : content}
     </div>
   )
 
@@ -206,6 +209,7 @@ export function HoverCard({
       }}
     >
       {anchor}
+      {copyable && <span className={css.status} role="status">{copied ? copiedLabel : ''}</span>}
       {card !== false && createPortal(card, document.body)}
     </span>
   )

+ 21 - 4
packages/client/ui-primitives/tests/hover-card.spec.tsx

@@ -159,8 +159,21 @@ describe('HoverCard', () => {
       expect(selection.toString()).toBe('card body')
       expect(screen.getByText('card body')).toBeTruthy()
 
-      // A non-collapsed selection elsewhere does not block this card.
+      // Firefox supports multiple selection ranges: any range intersecting
+      // this card wins, not only the first.
       selection.removeAllRanges()
+      const getSelection = vi.spyOn(window, 'getSelection').mockReturnValue({
+        isCollapsed: false,
+        rangeCount: 2,
+        getRangeAt: vi.fn((index: number) => ({
+          intersectsNode: () => index === 1,
+        })),
+      } as unknown as Selection)
+      await act(async () => { fireEvent.click(card) })
+      expect(writeText).not.toHaveBeenCalled()
+      getSelection.mockRestore()
+
+      // A non-collapsed selection elsewhere does not block this card.
       const anchorRange = document.createRange()
       anchorRange.selectNodeContents(screen.getByText('row'))
       selection.addRange(anchorRange)
@@ -191,10 +204,13 @@ describe('HoverCard', () => {
       fireEvent.pointerEnter(wrapper)
       act(() => { vi.advanceTimersByTime(500) })
       const card = screen.getByRole('button', { name: 'Copy path: /full/path' })
+      const status = screen.getByRole('status')
+      expect(status.textContent).toBe('')
+      expect(card.contains(status)).toBe(false)
       Object.defineProperty(card, 'offsetHeight', { configurable: true, value: 96 })
       await act(async () => { fireEvent.click(card) })
       expect(writeText).toHaveBeenCalledWith('/full/path')
-      expect(screen.getByRole('status').textContent).toBe('Copied')
+      expect(status.textContent).toBe('Copied')
       expect(screen.getByRole('button', { name: 'Copy path: /full/path' })).toBe(card)
       expect(card.style.minHeight).toBe('96px')
       // Repeated activation while feedback is visible neither rewrites nor
@@ -202,10 +218,11 @@ describe('HoverCard', () => {
       await act(async () => { fireEvent.click(card) })
       expect(writeText).toHaveBeenCalledOnce()
       act(() => { vi.advanceTimersByTime(999) })
-      expect(screen.getByText('Copied')).toBeTruthy()
+      expect(status.textContent).toBe('Copied')
       act(() => { vi.advanceTimersByTime(1) })
       expect(screen.getByRole('button', { name: 'Copy path: /full/path' })).toBe(card)
       expect(card.style.minHeight).toBe('')
+      expect(status.textContent).toBe('')
       expect(screen.getByText('card body')).toBeTruthy()
     } finally {
       restoreClipboard()
@@ -271,7 +288,7 @@ describe('HoverCard', () => {
       fireEvent.pointerEnter(wrapper)
       act(() => { vi.advanceTimersByTime(500) })
       await act(async () => { fireEvent.click(screen.getByRole('button')) })
-      expect(screen.getByText('Copied')).toBeTruthy()
+      expect(screen.getByRole('status').textContent).toBe('Copied')
       fireEvent.pointerLeave(wrapper)
       act(() => { vi.advanceTimersByTime(POINTER_GRACE_MS) })
       expect(screen.queryByText('Copied')).toBeNull()

+ 1 - 1
packages/client/ui-workspace/tests/rows.spec.tsx

@@ -160,7 +160,7 @@ describe('workspace browser rows', () => {
       expect(screen.getByText(/^创建于 \d+年\d+月\d+日 /)).toBeTruthy()
       await act(async () => { fireEvent.click(screen.getByRole('button', { name: '复制: /projects/project' })) })
       expect(writeText).toHaveBeenCalledWith('/projects/project')
-      expect(screen.getByText('已复制')).toBeTruthy()
+      expect(screen.getByRole('status').textContent).toBe('已复制')
     } finally {
       restoreClipboard()
       vi.useRealTimers()