Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions packages/public/common-ui-web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,39 @@ function App() {

---

## FullscreenImageModal
### Description
Component used to display a full-screen modal for an image and able the user to zoom on it.


### Example
```tsx
import { FullscreenImageModal } from '@monkvision/common-ui-web';

function App() {
const [showFullscreenImageModal, setShowFullscreenImageModal] = useState(true);

return (
<FullscreenImageModal
url={'https://example.com/image.jpg'}
show={showFullscreenImageModal}
label='Hello World!'
onClose={() => setShowFullscreenImageModal(false)}
/>
);
}
```

### Props
| Prop | Type | Description | Required | Default Value |
|---------|--------------|-----------------------------------------------------------------------------------------------|----------|---------------|
| url | string | The URL of the image to display. | ✔️ | |
| show | boolean | Boolean indicating if the fullscreen image modal is displayed on the screen. | | `false` |
| label | string | Label displayed in the header at the top of the image modal. | | `''` |
| onClose | `() => void` | Callback called when the user presses the close button in the header at the top of the modal. | | |

---

## FullscreenModal
### Description
Component used to display a full screen modal on top of the screen. The content of the modal must be passed as children
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Styles } from '@monkvision/types';

export const styles: Styles = {
image: {
maxWidth: '100%',
maxHeight: '100%',
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { useState, MouseEvent } from 'react';
import { FullscreenModal } from '../FullscreenModal';
import { styles } from './FullscreenImageModal.styles';

const ZOOM_SCALE = 3;

/**
* Props for the FullscreenImageModal component.
*/
export interface FullscreenImageModalProps {
/**
* The URL of the image to display.
*/
url: string;
/**
* Boolean indicating if the modal is shown or not.
*/
show?: boolean;
/**
* Optional label for the image.
*/
label?: string;
/**
* Callback function invoked when the modal is closed.
*/
onClose?: () => void;
}

function calculatePosition(
viewPort: number,
imageDimension: number,
clickPosition: number,
zoomScale: number,
): number {
if (viewPort > imageDimension * 3) {
return 0;
}
const blackBand = (viewPort - imageDimension) / 2;
const maxPosition = (imageDimension - blackBand) / zoomScale;
return Math.min(maxPosition, Math.max(-maxPosition, imageDimension / 2 - clickPosition));
}

/**
* FullscreenImageModal component used to display a full-screen modal for an image and able the user to zoom on it.
*/
export function FullscreenImageModal({
url,
show = false,
label = '',
onClose,
}: FullscreenImageModalProps) {
const [isZoomed, setIsZoomed] = useState(false);
const [position, setPosition] = useState({ x: 0, y: 0 });

const handleZoom = (event: MouseEvent<HTMLElement>) => {
if (isZoomed) {
setPosition({ x: 0, y: 0 });
} else {
const positionX = calculatePosition(
window.innerWidth,
event.currentTarget.offsetWidth,
event.nativeEvent.offsetX,
ZOOM_SCALE,
);
const positionY = calculatePosition(
window.innerHeight,
event.currentTarget.offsetHeight,
event.nativeEvent.offsetY,
ZOOM_SCALE,
);
setPosition({ x: positionX, y: positionY });
}
setIsZoomed(!isZoomed);
};

return (
<FullscreenModal show={show} title={label} onClose={onClose}>
<img
style={{
...styles['image'],
transform: `scale(${isZoomed ? 3 : 1}) translate(${position.x}px, ${position.y}px)`,
cursor: isZoomed ? 'zoom-out' : 'zoom-in',
zIndex: isZoomed ? '10' : 'auto',
}}
src={url}
alt={label}
onClick={handleZoom}
onKeyDown={() => {}}
data-testid='image'
/>
</FullscreenModal>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { FullscreenImageModal, type FullscreenImageModalProps } from './FullscreenImageModal';
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,17 @@ import { styles } from './FullscreenModal.styles';
* Props that can be passed to the Fullscreen Modal component.
*/
export interface FullscreenModalProps {
/**
* Boolean indicating if the modal is shown or not.
*/
show?: boolean;
/**
* Callback function invoked when the modal is closed.
*/
onClose?: () => void;
/**
* Optional title for the modal.
*/
title?: string;
}

Expand Down
1 change: 1 addition & 0 deletions packages/public/common-ui-web/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './SwitchButton';
export * from './FullscreenModal';
export * from './BackdropDialog';
export * from './Slider';
export * from './FullscreenImageModal';
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import '@testing-library/jest-dom';
import { screen, render, fireEvent } from '@testing-library/react';
import { FullscreenImageModal, FullscreenImageModalProps } from '../../src';

const mockProps: FullscreenImageModalProps = {
show: true,
label: 'Test Label',
onClose: jest.fn(),
url: 'test-image-url',
};

describe('FullsreenImageModal component', () => {
afterEach(() => {
jest.clearAllMocks();
});

it('should', () => {
const { unmount } = render(<FullscreenImageModal {...mockProps} />);

const image = screen.getByTestId('image') as HTMLImageElement;
expect(image.src).toContain(mockProps.url);
expect(image.alt).toEqual(mockProps.label);
expect(image.style.cursor).toEqual('zoom-in');
expect(image.style.transform).toContain('scale(1)');

fireEvent.click(image);
expect(image.style.cursor).toEqual('zoom-out');
expect(image.style.transform).toContain('scale(3)');

unmount();
});
});