Skip to content

Commit f6fe0b8

Browse files
authored
Refacto/MN-457/FullscreenImageModal (#671)
* Created FullscreenImageModal component * fixed PR + added TSDoc
1 parent 1a13435 commit f6fe0b8

File tree

7 files changed

+178
-0
lines changed

7 files changed

+178
-0
lines changed

packages/public/common-ui-web/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,39 @@ function App() {
132132

133133
---
134134

135+
## FullscreenImageModal
136+
### Description
137+
Component used to display a full-screen modal for an image and able the user to zoom on it.
138+
139+
140+
### Example
141+
```tsx
142+
import { FullscreenImageModal } from '@monkvision/common-ui-web';
143+
144+
function App() {
145+
const [showFullscreenImageModal, setShowFullscreenImageModal] = useState(true);
146+
147+
return (
148+
<FullscreenImageModal
149+
url={'https://example.com/image.jpg'}
150+
show={showFullscreenImageModal}
151+
label='Hello World!'
152+
onClose={() => setShowFullscreenImageModal(false)}
153+
/>
154+
);
155+
}
156+
```
157+
158+
### Props
159+
| Prop | Type | Description | Required | Default Value |
160+
|---------|--------------|-----------------------------------------------------------------------------------------------|----------|---------------|
161+
| url | string | The URL of the image to display. | ✔️ | |
162+
| show | boolean | Boolean indicating if the fullscreen image modal is displayed on the screen. | | `false` |
163+
| label | string | Label displayed in the header at the top of the image modal. | | `''` |
164+
| onClose | `() => void` | Callback called when the user presses the close button in the header at the top of the modal. | | |
165+
166+
---
167+
135168
## FullscreenModal
136169
### Description
137170
Component used to display a full screen modal on top of the screen. The content of the modal must be passed as children
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Styles } from '@monkvision/types';
2+
3+
export const styles: Styles = {
4+
image: {
5+
maxWidth: '100%',
6+
maxHeight: '100%',
7+
},
8+
};
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { useState, MouseEvent } from 'react';
2+
import { FullscreenModal } from '../FullscreenModal';
3+
import { styles } from './FullscreenImageModal.styles';
4+
5+
const ZOOM_SCALE = 3;
6+
7+
/**
8+
* Props for the FullscreenImageModal component.
9+
*/
10+
export interface FullscreenImageModalProps {
11+
/**
12+
* The URL of the image to display.
13+
*/
14+
url: string;
15+
/**
16+
* Boolean indicating if the modal is shown or not.
17+
*/
18+
show?: boolean;
19+
/**
20+
* Optional label for the image.
21+
*/
22+
label?: string;
23+
/**
24+
* Callback function invoked when the modal is closed.
25+
*/
26+
onClose?: () => void;
27+
}
28+
29+
function calculatePosition(
30+
viewPort: number,
31+
imageDimension: number,
32+
clickPosition: number,
33+
zoomScale: number,
34+
): number {
35+
if (viewPort > imageDimension * 3) {
36+
return 0;
37+
}
38+
const blackBand = (viewPort - imageDimension) / 2;
39+
const maxPosition = (imageDimension - blackBand) / zoomScale;
40+
return Math.min(maxPosition, Math.max(-maxPosition, imageDimension / 2 - clickPosition));
41+
}
42+
43+
/**
44+
* FullscreenImageModal component used to display a full-screen modal for an image and able the user to zoom on it.
45+
*/
46+
export function FullscreenImageModal({
47+
url,
48+
show = false,
49+
label = '',
50+
onClose,
51+
}: FullscreenImageModalProps) {
52+
const [isZoomed, setIsZoomed] = useState(false);
53+
const [position, setPosition] = useState({ x: 0, y: 0 });
54+
55+
const handleZoom = (event: MouseEvent<HTMLElement>) => {
56+
if (isZoomed) {
57+
setPosition({ x: 0, y: 0 });
58+
} else {
59+
const positionX = calculatePosition(
60+
window.innerWidth,
61+
event.currentTarget.offsetWidth,
62+
event.nativeEvent.offsetX,
63+
ZOOM_SCALE,
64+
);
65+
const positionY = calculatePosition(
66+
window.innerHeight,
67+
event.currentTarget.offsetHeight,
68+
event.nativeEvent.offsetY,
69+
ZOOM_SCALE,
70+
);
71+
setPosition({ x: positionX, y: positionY });
72+
}
73+
setIsZoomed(!isZoomed);
74+
};
75+
76+
return (
77+
<FullscreenModal show={show} title={label} onClose={onClose}>
78+
<img
79+
style={{
80+
...styles['image'],
81+
transform: `scale(${isZoomed ? 3 : 1}) translate(${position.x}px, ${position.y}px)`,
82+
cursor: isZoomed ? 'zoom-out' : 'zoom-in',
83+
zIndex: isZoomed ? '10' : 'auto',
84+
}}
85+
src={url}
86+
alt={label}
87+
onClick={handleZoom}
88+
onKeyDown={() => {}}
89+
data-testid='image'
90+
/>
91+
</FullscreenModal>
92+
);
93+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { FullscreenImageModal, type FullscreenImageModalProps } from './FullscreenImageModal';

packages/public/common-ui-web/src/components/FullscreenModal/FullscreenModal.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,17 @@ import { styles } from './FullscreenModal.styles';
66
* Props that can be passed to the Fullscreen Modal component.
77
*/
88
export interface FullscreenModalProps {
9+
/**
10+
* Boolean indicating if the modal is shown or not.
11+
*/
912
show?: boolean;
13+
/**
14+
* Callback function invoked when the modal is closed.
15+
*/
1016
onClose?: () => void;
17+
/**
18+
* Optional title for the modal.
19+
*/
1120
title?: string;
1221
}
1322

packages/public/common-ui-web/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * from './SwitchButton';
77
export * from './FullscreenModal';
88
export * from './BackdropDialog';
99
export * from './Slider';
10+
export * from './FullscreenImageModal';
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React from 'react';
2+
import '@testing-library/jest-dom';
3+
import { screen, render, fireEvent } from '@testing-library/react';
4+
import { FullscreenImageModal, FullscreenImageModalProps } from '../../src';
5+
6+
const mockProps: FullscreenImageModalProps = {
7+
show: true,
8+
label: 'Test Label',
9+
onClose: jest.fn(),
10+
url: 'test-image-url',
11+
};
12+
13+
describe('FullsreenImageModal component', () => {
14+
afterEach(() => {
15+
jest.clearAllMocks();
16+
});
17+
18+
it('should', () => {
19+
const { unmount } = render(<FullscreenImageModal {...mockProps} />);
20+
21+
const image = screen.getByTestId('image') as HTMLImageElement;
22+
expect(image.src).toContain(mockProps.url);
23+
expect(image.alt).toEqual(mockProps.label);
24+
expect(image.style.cursor).toEqual('zoom-in');
25+
expect(image.style.transform).toContain('scale(1)');
26+
27+
fireEvent.click(image);
28+
expect(image.style.cursor).toEqual('zoom-out');
29+
expect(image.style.transform).toContain('scale(3)');
30+
31+
unmount();
32+
});
33+
});

0 commit comments

Comments
 (0)