-
Notifications
You must be signed in to change notification settings - Fork 277
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat: WalletIsland mobile design #1827
Changes from all commits
303847a
34244d3
6b2269c
d31d65e
2d2b5d2
bd34637
f3c5cba
9c214b6
f5ad70a
caca2af
a6b5abe
6f975f9
407551e
2010cee
24dc445
ce65ac7
e3e4d88
2b78039
c95172c
48e35a8
878ded6
e212f66
4b9600f
9d5316c
e26e0cb
5da65b6
fd6f38a
8a03fa1
d2818eb
9cdcb75
116ec65
fcb5148
5cd4aa9
4812476
0685de3
0e371f5
69ca7e2
2db901c
5355bbf
426cffc
5200db1
d7d2d5f
349be65
d6c11d0
bf359a3
7e80c75
f9890bb
98cd192
fce4c60
c16fb4a
06edbaf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { fireEvent, render, screen } from '@testing-library/react'; | ||
import { beforeEach, describe, expect, it, vi } from 'vitest'; | ||
import { BottomSheet } from './BottomSheet'; | ||
|
||
vi.mock('../../internal/hooks/useTheme', () => ({ | ||
useTheme: vi.fn(), | ||
})); | ||
|
||
describe('BottomSheet', () => { | ||
const defaultProps = { | ||
isOpen: true, | ||
onClose: vi.fn(), | ||
children: <div>Test Content</div>, | ||
}; | ||
|
||
beforeEach(() => { | ||
vi.clearAllMocks(); | ||
}); | ||
|
||
it('renders children when open', () => { | ||
render(<BottomSheet {...defaultProps} />); | ||
expect(screen.getByText('Test Content')).toBeInTheDocument(); | ||
}); | ||
|
||
it('does not render children when closed', () => { | ||
render(<BottomSheet {...defaultProps} isOpen={false} />); | ||
expect(screen.queryByText('Test Content')).not.toBeInTheDocument(); | ||
}); | ||
|
||
it('calls onClose when Escape key is pressed on overlay', () => { | ||
render(<BottomSheet {...defaultProps} />); | ||
fireEvent.keyDown(screen.getByTestId('ockDismissableLayer'), { | ||
key: 'Escape', | ||
}); | ||
expect(defaultProps.onClose).toHaveBeenCalled(); | ||
}); | ||
|
||
it('applies custom className when provided', () => { | ||
render(<BottomSheet {...defaultProps} className="custom-class" />); | ||
expect(screen.getByTestId('ockBottomSheet')).toHaveClass('custom-class'); | ||
}); | ||
|
||
it('sets all ARIA attributes correctly', () => { | ||
render( | ||
<BottomSheet | ||
{...defaultProps} | ||
aria-label="Test Dialog" | ||
aria-describedby="desc" | ||
aria-labelledby="title" | ||
> | ||
<div>Content</div> | ||
</BottomSheet>, | ||
); | ||
|
||
const sheet = screen.getByTestId('ockBottomSheet'); | ||
expect(sheet).toHaveAttribute('role', 'dialog'); | ||
expect(sheet).toHaveAttribute('aria-label', 'Test Dialog'); | ||
expect(sheet).toHaveAttribute('aria-describedby', 'desc'); | ||
expect(sheet).toHaveAttribute('aria-labelledby', 'title'); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { DismissableLayer } from '@/internal/components/primitives/DismissableLayer'; | ||
import { FocusTrap } from '@/internal/components/primitives/FocusTrap'; | ||
import { useTheme } from '@/internal/hooks/useTheme'; | ||
import { zIndex } from '@/styles/constants'; | ||
import { background, cn } from '@/styles/theme'; | ||
import { createPortal } from 'react-dom'; | ||
|
||
type BottomSheetProps = { | ||
children: React.ReactNode; | ||
isOpen: boolean; | ||
onClose: () => void; | ||
triggerRef?: React.RefObject<HTMLElement>; | ||
className?: string; | ||
'aria-label'?: string; | ||
'aria-labelledby'?: string; | ||
'aria-describedby'?: string; | ||
}; | ||
|
||
export function BottomSheet({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Move to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just put it in |
||
children, | ||
className, | ||
isOpen, | ||
onClose, | ||
triggerRef, | ||
'aria-label': ariaLabel, | ||
'aria-labelledby': ariaLabelledby, | ||
'aria-describedby': ariaDescribedby, | ||
}: BottomSheetProps) { | ||
const componentTheme = useTheme(); | ||
|
||
if (!isOpen) { | ||
return null; | ||
} | ||
|
||
const bottomSheet = ( | ||
<FocusTrap active={isOpen}> | ||
<DismissableLayer | ||
onDismiss={onClose} | ||
triggerRef={triggerRef} | ||
preventTriggerEvents={!!triggerRef} | ||
> | ||
<div | ||
aria-describedby={ariaDescribedby} | ||
aria-label={ariaLabel} | ||
aria-labelledby={ariaLabelledby} | ||
data-testid="ockBottomSheet" | ||
role="dialog" | ||
className={cn( | ||
componentTheme, | ||
background.default, | ||
zIndex.modal, | ||
'fixed right-0 bottom-0 left-0', | ||
'transform rounded-t-3xl p-2 transition-transform', | ||
'fade-in slide-in-from-bottom-1/2 animate-in', | ||
className, | ||
)} | ||
> | ||
{children} | ||
</div> | ||
</DismissableLayer> | ||
</FocusTrap> | ||
); | ||
|
||
return createPortal(bottomSheet, document.body); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import { renderHook } from '@testing-library/react'; | ||
import { beforeEach, describe, expect, it, vi } from 'vitest'; | ||
import { useRespositionOnWindowResize } from './useRepositionOnResize'; | ||
|
||
describe('useRespositionOnWindowResize', () => { | ||
const mockRef = { current: document.createElement('div') }; | ||
const mockResetPosition = vi.fn(); | ||
|
||
beforeEach(() => { | ||
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1024); | ||
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(768); | ||
mockRef.current = document.createElement('div'); | ||
|
||
vi.clearAllMocks(); | ||
}); | ||
|
||
it('should not reposition when element is within viewport', () => { | ||
const initialPosition = { x: 100, y: 100 }; | ||
mockRef.current.getBoundingClientRect = vi.fn().mockReturnValue({ | ||
width: 100, | ||
height: 100, | ||
top: 100, | ||
left: 100, | ||
right: 200, | ||
bottom: 200, | ||
}); | ||
|
||
renderHook(() => | ||
useRespositionOnWindowResize(mockRef, initialPosition, mockResetPosition), | ||
); | ||
|
||
window.dispatchEvent(new Event('resize')); | ||
|
||
// Get the callback function that was passed to resetPosition | ||
const callback = mockResetPosition.mock.calls[0][0]; | ||
// Call the callback with current position and verify it returns same position | ||
expect(callback(initialPosition)).toEqual(initialPosition); | ||
}); | ||
|
||
it('should not reposition when ref.current is falsey', () => { | ||
const initialPosition = { x: 100, y: 100 }; | ||
// @ts-expect-error - we are testing the case where ref.current is falsey | ||
mockRef.current = null; | ||
|
||
renderHook(() => | ||
useRespositionOnWindowResize(mockRef, initialPosition, mockResetPosition), | ||
); | ||
|
||
window.dispatchEvent(new Event('resize')); | ||
expect(mockResetPosition).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('should reposition when element is outside right viewport boundary', () => { | ||
mockRef.current.getBoundingClientRect = vi.fn().mockReturnValue({ | ||
width: 100, | ||
height: 100, | ||
top: 100, | ||
left: 1000, | ||
right: 1100, | ||
bottom: 200, | ||
}); | ||
|
||
renderHook(() => | ||
useRespositionOnWindowResize( | ||
mockRef, | ||
{ x: 1000, y: 100 }, | ||
mockResetPosition, | ||
), | ||
); | ||
|
||
window.dispatchEvent(new Event('resize')); | ||
const callback = mockResetPosition.mock.calls[0][0]; | ||
const newPosition = callback({ x: 1000, y: 100 }); | ||
expect(newPosition.x).toBe(914); // 1024 - 100 - 10 | ||
expect(newPosition.y).toBe(100); // y shouldn't change | ||
}); | ||
|
||
it('should reposition when element is outside bottom viewport boundary', () => { | ||
mockRef.current.getBoundingClientRect = vi.fn().mockReturnValue({ | ||
width: 100, | ||
height: 100, | ||
top: 700, | ||
left: 100, | ||
right: 200, | ||
bottom: 800, | ||
}); | ||
|
||
renderHook(() => | ||
useRespositionOnWindowResize( | ||
mockRef, | ||
{ x: 100, y: 700 }, | ||
mockResetPosition, | ||
), | ||
); | ||
|
||
window.dispatchEvent(new Event('resize')); | ||
const callback = mockResetPosition.mock.calls[0][0]; | ||
const newPosition = callback({ x: 100, y: 700 }); | ||
expect(newPosition.x).toBe(100); // x shouldn't change | ||
expect(newPosition.y).toBe(658); // 768 - 100 - 10 | ||
}); | ||
|
||
it('should reposition when element is outside left viewport boundary', () => { | ||
mockRef.current.getBoundingClientRect = vi.fn().mockReturnValue({ | ||
width: 100, | ||
height: 100, | ||
top: 100, | ||
left: -100, | ||
right: 0, | ||
bottom: 200, | ||
}); | ||
|
||
renderHook(() => | ||
useRespositionOnWindowResize( | ||
mockRef, | ||
{ x: -100, y: 100 }, | ||
mockResetPosition, | ||
), | ||
); | ||
|
||
window.dispatchEvent(new Event('resize')); | ||
const callback = mockResetPosition.mock.calls[0][0]; | ||
const newPosition = callback({ x: -100, y: 100 }); | ||
expect(newPosition.x).toBe(10); // reset to 10 | ||
expect(newPosition.y).toBe(100); // y shouldn't change | ||
}); | ||
|
||
it('should reposition when element is outside top viewport boundary', () => { | ||
// Mock viewport size | ||
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(800); | ||
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(600); | ||
|
||
mockRef.current.getBoundingClientRect = vi.fn().mockReturnValue({ | ||
width: 100, | ||
height: 100, | ||
top: -50, | ||
left: 100, | ||
right: 200, | ||
bottom: 50, | ||
}); | ||
|
||
renderHook(() => | ||
useRespositionOnWindowResize( | ||
mockRef, | ||
{ x: 100, y: -50 }, | ||
mockResetPosition, | ||
), | ||
); | ||
|
||
window.dispatchEvent(new Event('resize')); | ||
|
||
const callback = mockResetPosition.mock.calls[0][0]; | ||
const newPosition = callback({ x: 100, y: -50 }); | ||
expect(newPosition.x).toBe(100); // x shouldn't change | ||
expect(newPosition.y).toBe(10); // reset to 10 | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The BottomSheet should probably be a Portal!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah we were talking about this last week. I did some research and have seen both ways. I don't have a strong opinion, wanted to ask the group at our session today