Skip to content
Snippets Groups Projects
Commit 0567dff2 authored by Adrian Orłów's avatar Adrian Orłów :fire:
Browse files

Merge branch 'feature/add-modal' into 'development'

feat: add modal

See merge request !74
parents 342e71d0 dc81a3cc
No related branches found
No related tags found
2 merge requests!223reset the pin numbers before search results are fetch (so the results will be...,!74feat: add modal
Pipeline #83285 passed
Showing
with 275 additions and 8 deletions
import { MODAL_INITIAL_STATE } from '@/redux/modal/modal.constants';
import { modalSelector } from '@/redux/modal/modal.selector';
import { StoreType } from '@/redux/store';
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { render, screen } from '@testing-library/react';
import { Modal } from './Modal.component';
const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStore);
return (
render(
<Wrapper>
<Modal />
</Wrapper>,
),
{
store,
}
);
};
describe('Modal - Component', () => {
describe('when modal is hidden', () => {
beforeEach(() => {
renderComponent({
modal: {
...MODAL_INITIAL_STATE,
isOpen: false,
modalTitle: 'Modal Hidden Title',
},
});
});
it('should modal have hidden class', () => {
const modalElement = screen.getByRole('modal');
expect(modalElement).toBeInTheDocument();
expect(modalElement).toHaveClass('hidden');
});
});
describe('when modal is shown', () => {
let store: StoreType;
beforeEach(() => {
const { store: newStore } = renderComponent({
modal: {
...MODAL_INITIAL_STATE,
isOpen: true,
modalTitle: 'Modal Opened Title',
},
});
store = newStore;
});
it('should modal NOT have hidden class', () => {
const modalElement = screen.getByRole('modal');
expect(modalElement).toBeInTheDocument();
expect(modalElement).not.toHaveClass('hidden');
});
it('shows modal title', () => {
expect(screen.getByText('Modal Opened Title', { exact: false })).toBeInTheDocument();
});
it('shows modal close button', () => {
expect(screen.getByLabelText('close button')).toBeInTheDocument();
});
it('closes modal on close button click', () => {
const closeButton = screen.getByLabelText('close button');
closeButton.click();
const { isOpen } = modalSelector(store.getState());
expect(isOpen).toBeFalsy();
});
});
});
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { modalSelector } from '@/redux/modal/modal.selector';
import { closeModal } from '@/redux/modal/modal.slice';
import { Icon } from '@/shared/Icon';
import { twMerge } from 'tailwind-merge';
import { MODAL_ROLE } from './Modal.constants';
import { OverviewImagesModal } from './OverviewImagesModal';
export const Modal = (): React.ReactNode => {
const dispatch = useAppDispatch();
const { isOpen, modalName, modalTitle } = useAppSelector(modalSelector);
const handleCloseModal = (): void => {
dispatch(closeModal());
};
return (
<div
className={twMerge(
'absolute left-0 top-0 z-10 h-full w-full bg-cetacean-blue/[.48]',
isOpen ? '' : 'hidden',
)}
role={MODAL_ROLE}
>
<div className="flex h-full w-full items-center justify-center">
<div className="flex h-5/6 w-10/12 flex-col overflow-hidden rounded-lg">
<div className="flex items-center justify-between bg-white p-[24px] text-xl">
<div>{modalTitle}</div>
<button type="button" onClick={handleCloseModal} aria-label="close button">
<Icon name="close" className="fill-font-500" />
</button>
</div>
{isOpen && modalName === 'overview-images' && <OverviewImagesModal />}
</div>
</div>
</div>
);
};
export const MODAL_ROLE = 'modal';
import * as React from 'react';
export const OverviewImagesModal: React.FC = () => {
return <div className="h-[200px] w-[500px] bg-white " />;
};
export { OverviewImagesModal } from './OverviewImagesModal.component';
export { Modal } from './Modal.component';
import { Drawer } from '@/components/Map/Drawer';
import { MapViewer } from './MapViewer/MapViewer.component';
import { MapAdditionalOptions } from './MapAdditionalOptions';
import { MapViewer } from './MapViewer/MapViewer.component';
export const Map = (): JSX.Element => (
<div className="relative z-0 h-screen w-full bg-black" data-testid="map-container">
......
import { StoreType } from '@/redux/store';
import { render, screen } from '@testing-library/react';
import { FIRST_ARRAY_ELEMENT } from '@/constants/common';
import {
BACKGROUNDS_MOCK,
BACKGROUND_INITIAL_STATE_MOCK,
} from '@/redux/backgrounds/background.mock';
import { initialMapStateFixture } from '@/redux/map/map.fixtures';
import { AppDispatch, RootState, StoreType } from '@/redux/store';
import { getReduxStoreWithActionsListener } from '@/utils/testing/getReduxStoreActionsListener';
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { render, screen } from '@testing-library/react';
import { MockStoreEnhanced } from 'redux-mock-store';
import { MapAdditionalOptions } from './MapAdditionalOptions.component';
const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
......@@ -21,9 +29,47 @@ const renderComponent = (initialStoreState: InitialStoreState = {}): { store: St
);
};
const renderComponentWithActionListener = (
initialStoreState: InitialStoreState = {},
): { store: MockStoreEnhanced<Partial<RootState>, AppDispatch> } => {
const { Wrapper, store } = getReduxStoreWithActionsListener(initialStoreState);
return (
render(
<Wrapper>
<MapAdditionalOptions />
</Wrapper>,
),
{
store,
}
);
};
describe('MapAdditionalOptions - component', () => {
it('should display background selector', () => {
renderComponent();
expect(screen.getByTestId('background-selector')).toBeInTheDocument();
});
it('should render browse overview images button', () => {
renderComponent();
expect(screen.getByText('Browse overview images')).toBeInTheDocument();
});
it('should open overview image modal on button click', () => {
const { store } = renderComponentWithActionListener({
map: initialMapStateFixture,
backgrounds: { ...BACKGROUND_INITIAL_STATE_MOCK, data: BACKGROUNDS_MOCK },
});
const overviewImageButton = screen.getByText('Browse overview images');
overviewImageButton.click();
const actions = store.getActions();
expect(actions[FIRST_ARRAY_ELEMENT]).toStrictEqual({
payload: undefined,
type: 'modal/openOverviewImagesModal',
});
});
});
import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
import { openOverviewImagesModal } from '@/redux/modal/modal.slice';
import { Button } from '@/shared/Button';
import { twMerge } from 'tailwind-merge';
import { BackgroundSelector } from './BackgroundsSelector';
// top-[calc(64px+40px+24px)] -> TOP_BAR_HEIGHT+MAP_NAVIGATION_HEIGHT+DISTANCE_FROM_MAP_NAVIGATION
export const MapAdditionalOptions = (): JSX.Element => (
<div className={twMerge('absolute right-6 top-[calc(64px+40px+24px)] z-10')}>
<BackgroundSelector />
</div>
);
export const MapAdditionalOptions = (): JSX.Element => {
const dispatch = useAppDispatch();
const handleBrowseOverviewImagesClick = (): void => {
dispatch(openOverviewImagesModal());
};
return (
<div className={twMerge('absolute right-6 top-[calc(64px+40px+24px)] z-10 flex')}>
<Button className="mr-4" onClick={handleBrowseOverviewImagesClick}>
Browse overview images
</Button>
<BackgroundSelector />
</div>
);
};
......@@ -4,6 +4,7 @@ import { manrope } from '@/constants/font';
import { useReduxBusQueryManager } from '@/utils/query-manager/useReduxBusQueryManager';
import { twMerge } from 'tailwind-merge';
import { useInitializeStore } from '../../utils/initialize/useInitializeStore';
import { Modal } from '../FunctionalArea/Modal';
export const MinervaSPA = (): JSX.Element => {
useInitializeStore();
......@@ -13,6 +14,7 @@ export const MinervaSPA = (): JSX.Element => {
<div className={twMerge('relative', manrope.variable)}>
<FunctionalArea />
<Map />
<Modal />
</div>
);
};
import { ModalState } from './modal.types';
export const MODAL_INITIAL_STATE: ModalState = {
isOpen: false,
modalName: 'none',
modalTitle: '',
};
import { ModalState } from './modal.types';
export const MODAL_INITIAL_STATE_MOCK: ModalState = {
isOpen: false,
modalName: 'none',
modalTitle: '',
};
import { ModalName } from '@/types/modal';
import { PayloadAction } from '@reduxjs/toolkit';
import { ModalState } from './modal.types';
export const openModalReducer = (state: ModalState, action: PayloadAction<ModalName>): void => {
state.isOpen = true;
state.modalName = action.payload;
};
export const closeModalReducer = (state: ModalState): void => {
state.isOpen = false;
state.modalName = 'none';
};
export const openOverviewImagesModalReducer = (state: ModalState): void => {
state.isOpen = true;
state.modalName = 'overview-images';
state.modalTitle = 'Overview images';
};
import { createSelector } from '@reduxjs/toolkit';
import { rootSelector } from '../root/root.selectors';
export const modalSelector = createSelector(rootSelector, state => state.modal);
export const isModalOpenSelector = createSelector(modalSelector, state => state.isOpen);
import { createSlice } from '@reduxjs/toolkit';
import { MODAL_INITIAL_STATE } from './modal.constants';
import {
closeModalReducer,
openModalReducer,
openOverviewImagesModalReducer,
} from './modal.reducers';
const modalSlice = createSlice({
name: 'modal',
initialState: MODAL_INITIAL_STATE,
reducers: {
openModal: openModalReducer,
closeModal: closeModalReducer,
openOverviewImagesModal: openOverviewImagesModalReducer,
},
});
export const { openModal, closeModal, openOverviewImagesModal } = modalSlice.actions;
export default modalSlice.reducer;
import { ModalName } from '@/types/modal';
export interface ModalState {
isOpen: boolean;
modalName: ModalName;
modalTitle: string;
}
......@@ -4,6 +4,7 @@ import { CHEMICALS_INITIAL_STATE_MOCK } from '../chemicals/chemicals.mock';
import { initialStateFixture as drawerInitialStateMock } from '../drawer/drawerFixture';
import { DRUGS_INITIAL_STATE_MOCK } from '../drugs/drugs.mock';
import { initialMapStateFixture } from '../map/map.fixtures';
import { MODAL_INITIAL_STATE_MOCK } from '../modal/modal.mock';
import { MODELS_INITIAL_STATE_MOCK } from '../models/models.mock';
import { OVERLAYS_INITIAL_STATE_MOCK } from '../overlays/overlays.mock';
import { PROJECT_STATE_INITIAL_MOCK } from '../project/project.mock';
......@@ -23,4 +24,5 @@ export const INITIAL_STORE_STATE_MOCK: RootState = {
map: initialMapStateFixture,
overlays: OVERLAYS_INITIAL_STATE_MOCK,
reactions: REACTIONS_STATE_INITIAL_MOCK,
modal: MODAL_INITIAL_STATE_MOCK,
};
......@@ -4,6 +4,7 @@ import chemicalsReducer from '@/redux/chemicals/chemicals.slice';
import drawerReducer from '@/redux/drawer/drawer.slice';
import drugsReducer from '@/redux/drugs/drugs.slice';
import mapReducer from '@/redux/map/map.slice';
import modalReducer from '@/redux/modal/modal.slice';
import modelsReducer from '@/redux/models/models.slice';
import overlaysReducer from '@/redux/overlays/overlays.slice';
import projectReducer from '@/redux/project/project.slice';
......@@ -25,6 +26,7 @@ export const reducers = {
chemicals: chemicalsReducer,
bioEntity: bioEntityReducer,
drawer: drawerReducer,
modal: modalReducer,
map: mapReducer,
backgrounds: backgroundsReducer,
overlays: overlaysReducer,
......
export type ModalName = 'none' | 'overview-images';
......@@ -28,6 +28,7 @@ const config: Config = {
orange: '#f48c40',
purple: '#6400e3',
pink: '#f1009f',
'cetacean-blue': '#070130',
},
height: {
'calc-drawer': 'calc(100% - 104px)',
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment