Skip to content

Commit 8d7c5ad

Browse files
authored
Chore: WalletIsland Code Quality Fast Follows (#1842)
1 parent 8311826 commit 8d7c5ad

24 files changed

+437
-452
lines changed
+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { copyToClipboard } from './copyToClipboard';
3+
4+
describe('copyToClipboard', () => {
5+
beforeEach(() => {
6+
Object.assign(navigator, {
7+
clipboard: {
8+
writeText: vi.fn(),
9+
},
10+
});
11+
});
12+
13+
it('should copy text to clipboard successfully', async () => {
14+
const mockSuccess = vi.fn();
15+
const mockWriteText = navigator.clipboard.writeText as Mock;
16+
mockWriteText.mockResolvedValueOnce(undefined);
17+
18+
await copyToClipboard({
19+
copyValue: 'test text',
20+
onSuccess: mockSuccess,
21+
});
22+
23+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('test text');
24+
expect(mockSuccess).toHaveBeenCalled();
25+
});
26+
27+
it('should handle clipboard error', async () => {
28+
const mockError = vi.fn();
29+
const testError = new Error('Clipboard error');
30+
const mockWriteText = navigator.clipboard.writeText as Mock;
31+
mockWriteText.mockRejectedValueOnce(testError);
32+
33+
await copyToClipboard({
34+
copyValue: 'test text',
35+
onError: mockError,
36+
});
37+
38+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('test text');
39+
expect(mockError).toHaveBeenCalledWith(testError);
40+
});
41+
42+
it('should work without callbacks', async () => {
43+
const mockWriteText = navigator.clipboard.writeText as Mock;
44+
mockWriteText.mockResolvedValueOnce(undefined);
45+
46+
await expect(
47+
copyToClipboard({ copyValue: 'test text' }),
48+
).resolves.not.toThrow();
49+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('test text');
50+
});
51+
});

src/core/utils/copyToClipboard.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use client';
2+
3+
type CopyToClipboardParams = {
4+
copyValue: string;
5+
onSuccess?: () => void;
6+
onError?: (error: unknown) => void;
7+
};
8+
9+
export async function copyToClipboard({
10+
copyValue,
11+
onSuccess,
12+
onError,
13+
}: CopyToClipboardParams) {
14+
try {
15+
await navigator.clipboard.writeText(copyValue);
16+
onSuccess?.();
17+
} catch (err) {
18+
onError?.(err);
19+
}
20+
}

src/identity/components/Address.tsx

+17-12
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
'use client';
2-
import { useIdentityContext } from '@/identity/components/IdentityProvider';
3-
import type { AddressReact } from '@/identity/types';
4-
import { getSlicedAddress } from '@/identity/utils/getSlicedAddress';
2+
3+
import { copyToClipboard } from '@/core/utils/copyToClipboard';
54
import { useState } from 'react';
65
import { border, cn, color, pressable, text } from '../../styles/theme';
6+
import type { AddressReact } from '../types';
7+
import { getSlicedAddress } from '../utils/getSlicedAddress';
8+
import { useIdentityContext } from './IdentityProvider';
79

810
export function Address({
911
address = null,
@@ -40,15 +42,18 @@ export function Address({
4042

4143
// Interactive version with copy functionality
4244
const handleClick = async () => {
43-
try {
44-
await navigator.clipboard.writeText(accountAddress);
45-
setCopyText('Copied');
46-
setTimeout(() => setCopyText('Copy'), 2000);
47-
} catch (err) {
48-
console.error('Failed to copy address:', err);
49-
setCopyText('Failed to copy');
50-
setTimeout(() => setCopyText('Copy'), 2000);
51-
}
45+
await copyToClipboard({
46+
copyValue: accountAddress,
47+
onSuccess: () => {
48+
setCopyText('Copied');
49+
setTimeout(() => setCopyText('Copy'), 2000);
50+
},
51+
onError: (err: unknown) => {
52+
console.error('Failed to copy address:', err);
53+
setCopyText('Failed to copy');
54+
setTimeout(() => setCopyText('Copy'), 2000);
55+
},
56+
});
5257
};
5358

5459
const handleKeyDown = (e: React.KeyboardEvent) => {
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { copyToClipboard } from '@/core/utils/copyToClipboard';
2+
import type { ReactNode } from 'react';
3+
4+
type CopyButtonProps = {
5+
label: string | ReactNode;
6+
copyValue: string;
7+
onSuccess?: () => void;
8+
onError?: (error: unknown) => void;
9+
className?: string;
10+
'aria-label'?: string;
11+
};
12+
13+
export function CopyButton({
14+
label,
15+
copyValue,
16+
onSuccess,
17+
onError,
18+
className,
19+
'aria-label': ariaLabel,
20+
}: CopyButtonProps) {
21+
return (
22+
<button
23+
type="button"
24+
data-testid="ockCopyButton"
25+
className={className}
26+
onClick={() => copyToClipboard({ copyValue, onSuccess, onError })}
27+
aria-label={ariaLabel}
28+
>
29+
{label}
30+
</button>
31+
);
32+
}

src/internal/components/Draggable/Draggable.test.tsx

+2-33
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,14 @@ describe('Draggable', () => {
3737
expect(draggable).toHaveStyle({ left: '100px', top: '100px' });
3838
});
3939

40-
it('changes cursor style when dragging', () => {
40+
it('has correct cursor styles', () => {
4141
render(
4242
<Draggable>
4343
<div>Drag me</div>
4444
</Draggable>,
4545
);
4646
const draggable = screen.getByTestId('ockDraggable');
47-
expect(draggable).toHaveClass('cursor-grab');
48-
49-
fireEvent.pointerDown(draggable);
50-
expect(draggable).toHaveClass('cursor-grabbing');
51-
52-
fireEvent.pointerUp(draggable);
53-
expect(draggable).toHaveClass('cursor-grab');
47+
expect(draggable).toHaveClass('cursor-grab active:cursor-grabbing');
5448
});
5549

5650
it('snaps to grid when dragging ends if enableSnapToGrid is true', async () => {
@@ -95,29 +89,6 @@ describe('Draggable', () => {
9589
expect(draggable).toHaveStyle({ left: '14px', top: '16px' });
9690
});
9791

98-
it('handles touch events', () => {
99-
render(
100-
<Draggable>
101-
<div>Drag me</div>
102-
</Draggable>,
103-
);
104-
const draggable = screen.getByTestId('ockDraggable');
105-
106-
fireEvent.pointerDown(draggable, {
107-
clientX: 0,
108-
clientY: 0,
109-
});
110-
expect(draggable).toHaveClass('cursor-grabbing');
111-
112-
fireEvent.pointerMove(document, {
113-
clientX: 50,
114-
clientY: 50,
115-
});
116-
117-
fireEvent.pointerUp(document);
118-
expect(draggable).toHaveClass('cursor-grab');
119-
});
120-
12192
it('calculates drag offset correctly', async () => {
12293
const user = userEvent.setup();
12394
render(
@@ -209,7 +180,6 @@ describe('Draggable', () => {
209180
);
210181

211182
const draggable = screen.getByTestId('ockDraggable');
212-
expect(draggable).toHaveClass('default');
213183

214184
// Attempt to drag
215185
await user.pointer([
@@ -220,6 +190,5 @@ describe('Draggable', () => {
220190

221191
// Position should not change
222192
expect(draggable).toHaveStyle({ left: '0px', top: '0px' });
223-
expect(draggable).toHaveClass('default');
224193
});
225194
});

src/internal/components/Draggable/Draggable.tsx

+9-17
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { zIndex } from '@/styles/constants';
44
import { cn } from '@/styles/theme';
55
import { useCallback, useEffect, useRef, useState } from 'react';
6-
import { useRespositionOnWindowResize } from './useRespositionOnResize';
6+
import { getBoundedPosition } from './getBoundedPosition';
77

88
type DraggableProps = {
99
children: React.ReactNode;
@@ -24,18 +24,8 @@ export function Draggable({
2424
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
2525
const [dragStartPosition, setDragStartPosition] = useState({ x: 0, y: 0 });
2626
const [isDragging, setIsDragging] = useState(false);
27-
const [cursorDisplay, setCursorDisplay] = useState('default');
2827
const draggableRef = useRef<HTMLDivElement>(null);
2928

30-
useEffect(() => {
31-
if (disabled) {
32-
setCursorDisplay('default');
33-
return;
34-
}
35-
36-
setCursorDisplay(isDragging ? 'cursor-grabbing' : 'cursor-grab');
37-
}, [disabled, isDragging]);
38-
3929
const calculateSnapToGrid = useCallback(
4030
(positionValue: number) => {
4131
return Math.round(positionValue / gridSize) * gridSize;
@@ -65,10 +55,14 @@ export function Draggable({
6555
}
6656

6757
const handleGlobalMove = (e: PointerEvent) => {
68-
setPosition({
69-
x: e.clientX - dragOffset.x,
70-
y: e.clientY - dragOffset.y,
58+
const newPosition = getBoundedPosition({
59+
draggableRef,
60+
position: {
61+
x: e.clientX - dragOffset.x,
62+
y: e.clientY - dragOffset.y,
63+
},
7164
});
65+
setPosition(newPosition);
7266
};
7367

7468
const handleGlobalEnd = (e: PointerEvent) => {
@@ -110,16 +104,14 @@ export function Draggable({
110104
dragStartPosition,
111105
]);
112106

113-
useRespositionOnWindowResize(draggableRef, position, setPosition);
114-
115107
return (
116108
<div
117109
ref={draggableRef}
118110
data-testid="ockDraggable"
119111
className={cn(
120112
'fixed touch-none select-none',
113+
'cursor-grab active:cursor-grabbing',
121114
zIndex.modal,
122-
cursorDisplay,
123115
)}
124116
style={{
125117
left: `${position.x}px`,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { getBoundedPosition } from './getBoundedPosition';
3+
4+
describe('getBoundedPosition', () => {
5+
beforeEach(() => {
6+
vi.stubGlobal('window', { innerWidth: 1024, innerHeight: 768 });
7+
});
8+
9+
afterEach(() => {
10+
vi.unstubAllGlobals();
11+
vi.clearAllMocks();
12+
});
13+
14+
it('returns the position if the window is undefined', () => {
15+
vi.stubGlobal('window', undefined);
16+
const position = getBoundedPosition({
17+
draggableRef: { current: null },
18+
position: { x: 100, y: 100 },
19+
});
20+
expect(position).toEqual({ x: 100, y: 100 });
21+
});
22+
23+
it('returns the position if the draggableRef is null', () => {
24+
const position = getBoundedPosition({
25+
draggableRef: { current: null },
26+
position: { x: 100, y: 100 },
27+
});
28+
expect(position).toEqual({ x: 100, y: 100 });
29+
});
30+
31+
it('bounds position within viewport considering minGapToEdge', () => {
32+
const mockElement = {
33+
getBoundingClientRect: () => ({
34+
width: 100,
35+
height: 50,
36+
}),
37+
};
38+
39+
const cases = [
40+
// Test left boundary
41+
{
42+
input: { x: -5, y: 100 },
43+
expected: { x: 10, y: 100 },
44+
},
45+
// Test right boundary
46+
{
47+
input: { x: 1000, y: 100 },
48+
expected: { x: 914, y: 100 }, // 1024 - 100 - 10
49+
},
50+
// Test top boundary
51+
{
52+
input: { x: 100, y: -5 },
53+
expected: { x: 100, y: 10 },
54+
},
55+
// Test bottom boundary
56+
{
57+
input: { x: 100, y: 800 },
58+
expected: { x: 100, y: 708 }, // 768 - 50 - 10
59+
},
60+
];
61+
62+
for (const { input, expected } of cases) {
63+
const position = getBoundedPosition({
64+
draggableRef: { current: mockElement as HTMLDivElement },
65+
position: input,
66+
});
67+
expect(position).toEqual(expected);
68+
}
69+
});
70+
71+
it('respects custom minGapToEdge', () => {
72+
const mockElement = {
73+
getBoundingClientRect: () => ({
74+
width: 100,
75+
height: 50,
76+
}),
77+
};
78+
79+
const position = getBoundedPosition({
80+
draggableRef: { current: mockElement as HTMLDivElement },
81+
position: { x: -10, y: -10 },
82+
minGapToEdge: 20,
83+
});
84+
85+
expect(position).toEqual({ x: 20, y: 20 });
86+
});
87+
});

0 commit comments

Comments
 (0)