fix(web): expose copied status outside button
This commit is contained in:
@@ -43,3 +43,12 @@
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|||||||
@@ -157,8 +157,11 @@ export function HoverCard({
|
|||||||
onClick={copyable
|
onClick={copyable
|
||||||
? (e) => {
|
? (e) => {
|
||||||
const selection = window.getSelection()
|
const selection = window.getSelection()
|
||||||
if (selection !== null && !selection.isCollapsed && selection.rangeCount > 0
|
if (selection !== null && !selection.isCollapsed) {
|
||||||
&& selection.getRangeAt(0).intersectsNode(e.currentTarget)) return
|
for (let i = 0; i < selection.rangeCount; i += 1) {
|
||||||
|
if (selection.getRangeAt(i).intersectsNode(e.currentTarget)) return
|
||||||
|
}
|
||||||
|
}
|
||||||
void copy(copyText)
|
void copy(copyText)
|
||||||
}
|
}
|
||||||
: undefined}
|
: undefined}
|
||||||
@@ -170,7 +173,7 @@ export function HoverCard({
|
|||||||
}
|
}
|
||||||
: undefined}
|
: undefined}
|
||||||
>
|
>
|
||||||
{copied ? <span className={css.copied} role="status">{copiedLabel}</span> : content}
|
{copied ? <span className={css.copied} aria-hidden="true">{copiedLabel}</span> : content}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -206,6 +209,7 @@ export function HoverCard({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{anchor}
|
{anchor}
|
||||||
|
{copyable && <span className={css.status} role="status">{copied ? copiedLabel : ''}</span>}
|
||||||
{card !== false && createPortal(card, document.body)}
|
{card !== false && createPortal(card, document.body)}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -159,8 +159,21 @@ describe('HoverCard', () => {
|
|||||||
expect(selection.toString()).toBe('card body')
|
expect(selection.toString()).toBe('card body')
|
||||||
expect(screen.getByText('card body')).toBeTruthy()
|
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()
|
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()
|
const anchorRange = document.createRange()
|
||||||
anchorRange.selectNodeContents(screen.getByText('row'))
|
anchorRange.selectNodeContents(screen.getByText('row'))
|
||||||
selection.addRange(anchorRange)
|
selection.addRange(anchorRange)
|
||||||
@@ -191,10 +204,13 @@ describe('HoverCard', () => {
|
|||||||
fireEvent.pointerEnter(wrapper)
|
fireEvent.pointerEnter(wrapper)
|
||||||
act(() => { vi.advanceTimersByTime(500) })
|
act(() => { vi.advanceTimersByTime(500) })
|
||||||
const card = screen.getByRole('button', { name: 'Copy path: /full/path' })
|
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 })
|
Object.defineProperty(card, 'offsetHeight', { configurable: true, value: 96 })
|
||||||
await act(async () => { fireEvent.click(card) })
|
await act(async () => { fireEvent.click(card) })
|
||||||
expect(writeText).toHaveBeenCalledWith('/full/path')
|
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(screen.getByRole('button', { name: 'Copy path: /full/path' })).toBe(card)
|
||||||
expect(card.style.minHeight).toBe('96px')
|
expect(card.style.minHeight).toBe('96px')
|
||||||
// Repeated activation while feedback is visible neither rewrites nor
|
// Repeated activation while feedback is visible neither rewrites nor
|
||||||
@@ -202,10 +218,11 @@ describe('HoverCard', () => {
|
|||||||
await act(async () => { fireEvent.click(card) })
|
await act(async () => { fireEvent.click(card) })
|
||||||
expect(writeText).toHaveBeenCalledOnce()
|
expect(writeText).toHaveBeenCalledOnce()
|
||||||
act(() => { vi.advanceTimersByTime(999) })
|
act(() => { vi.advanceTimersByTime(999) })
|
||||||
expect(screen.getByText('Copied')).toBeTruthy()
|
expect(status.textContent).toBe('Copied')
|
||||||
act(() => { vi.advanceTimersByTime(1) })
|
act(() => { vi.advanceTimersByTime(1) })
|
||||||
expect(screen.getByRole('button', { name: 'Copy path: /full/path' })).toBe(card)
|
expect(screen.getByRole('button', { name: 'Copy path: /full/path' })).toBe(card)
|
||||||
expect(card.style.minHeight).toBe('')
|
expect(card.style.minHeight).toBe('')
|
||||||
|
expect(status.textContent).toBe('')
|
||||||
expect(screen.getByText('card body')).toBeTruthy()
|
expect(screen.getByText('card body')).toBeTruthy()
|
||||||
} finally {
|
} finally {
|
||||||
restoreClipboard()
|
restoreClipboard()
|
||||||
@@ -271,7 +288,7 @@ describe('HoverCard', () => {
|
|||||||
fireEvent.pointerEnter(wrapper)
|
fireEvent.pointerEnter(wrapper)
|
||||||
act(() => { vi.advanceTimersByTime(500) })
|
act(() => { vi.advanceTimersByTime(500) })
|
||||||
await act(async () => { fireEvent.click(screen.getByRole('button')) })
|
await act(async () => { fireEvent.click(screen.getByRole('button')) })
|
||||||
expect(screen.getByText('Copied')).toBeTruthy()
|
expect(screen.getByRole('status').textContent).toBe('Copied')
|
||||||
fireEvent.pointerLeave(wrapper)
|
fireEvent.pointerLeave(wrapper)
|
||||||
act(() => { vi.advanceTimersByTime(POINTER_GRACE_MS) })
|
act(() => { vi.advanceTimersByTime(POINTER_GRACE_MS) })
|
||||||
expect(screen.queryByText('Copied')).toBeNull()
|
expect(screen.queryByText('Copied')).toBeNull()
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ describe('workspace browser rows', () => {
|
|||||||
expect(screen.getByText(/^创建于 \d+年\d+月\d+日 /)).toBeTruthy()
|
expect(screen.getByText(/^创建于 \d+年\d+月\d+日 /)).toBeTruthy()
|
||||||
await act(async () => { fireEvent.click(screen.getByRole('button', { name: '复制: /projects/project' })) })
|
await act(async () => { fireEvent.click(screen.getByRole('button', { name: '复制: /projects/project' })) })
|
||||||
expect(writeText).toHaveBeenCalledWith('/projects/project')
|
expect(writeText).toHaveBeenCalledWith('/projects/project')
|
||||||
expect(screen.getByText('已复制')).toBeTruthy()
|
expect(screen.getByRole('status').textContent).toBe('已复制')
|
||||||
} finally {
|
} finally {
|
||||||
restoreClipboard()
|
restoreClipboard()
|
||||||
vi.useRealTimers()
|
vi.useRealTimers()
|
||||||
|
|||||||
Reference in New Issue
Block a user