From 7c4a8216ce8e843640f057a99fbc180e63564df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Or=C5=82=C3=B3w?= <adrian.orlow@fishbrain.com> Date: Thu, 7 Dec 2023 03:58:21 +0100 Subject: [PATCH] feat: add modal --- .../Modal/Modal.component.test.tsx | 85 +++++++++++++++++++ .../FunctionalArea/Modal/Modal.component.tsx | 39 +++++++++ .../FunctionalArea/Modal/Modal.constants.ts | 1 + .../OverviewImagesModal.component.tsx | 5 ++ .../Modal/OverviewImagesModal/index.ts | 1 + src/components/FunctionalArea/Modal/index.ts | 1 + src/components/Map/Map.component.tsx | 2 +- .../MapAdditionalOptions.component.tsx | 24 ++++-- src/components/SPA/MinervaSPA.component.tsx | 2 + src/redux/modal/modal.constants.ts | 7 ++ src/redux/modal/modal.mock.ts | 7 ++ src/redux/modal/modal.reducers.ts | 19 +++++ src/redux/modal/modal.selector.ts | 6 ++ src/redux/modal/modal.slice.ts | 21 +++++ src/redux/modal/modal.types.ts | 7 ++ src/redux/root/root.fixtures.ts | 2 + src/redux/store.ts | 2 + src/types/modal.ts | 1 + tailwind.config.ts | 1 + 19 files changed, 227 insertions(+), 6 deletions(-) create mode 100644 src/components/FunctionalArea/Modal/Modal.component.test.tsx create mode 100644 src/components/FunctionalArea/Modal/Modal.component.tsx create mode 100644 src/components/FunctionalArea/Modal/Modal.constants.ts create mode 100644 src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx create mode 100644 src/components/FunctionalArea/Modal/OverviewImagesModal/index.ts create mode 100644 src/components/FunctionalArea/Modal/index.ts create mode 100644 src/redux/modal/modal.constants.ts create mode 100644 src/redux/modal/modal.mock.ts create mode 100644 src/redux/modal/modal.reducers.ts create mode 100644 src/redux/modal/modal.selector.ts create mode 100644 src/redux/modal/modal.slice.ts create mode 100644 src/redux/modal/modal.types.ts create mode 100644 src/types/modal.ts diff --git a/src/components/FunctionalArea/Modal/Modal.component.test.tsx b/src/components/FunctionalArea/Modal/Modal.component.test.tsx new file mode 100644 index 00000000..62644f66 --- /dev/null +++ b/src/components/FunctionalArea/Modal/Modal.component.test.tsx @@ -0,0 +1,85 @@ +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(); + }); + }); +}); diff --git a/src/components/FunctionalArea/Modal/Modal.component.tsx b/src/components/FunctionalArea/Modal/Modal.component.tsx new file mode 100644 index 00000000..1a0f2f38 --- /dev/null +++ b/src/components/FunctionalArea/Modal/Modal.component.tsx @@ -0,0 +1,39 @@ +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 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> + <div>{isOpen && modalName === 'overview-images' && <OverviewImagesModal />}</div> + </div> + </div> + </div> + ); +}; diff --git a/src/components/FunctionalArea/Modal/Modal.constants.ts b/src/components/FunctionalArea/Modal/Modal.constants.ts new file mode 100644 index 00000000..b31cdcfb --- /dev/null +++ b/src/components/FunctionalArea/Modal/Modal.constants.ts @@ -0,0 +1 @@ +export const MODAL_ROLE = 'modal'; diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx new file mode 100644 index 00000000..29c97f24 --- /dev/null +++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/OverviewImagesModal.component.tsx @@ -0,0 +1,5 @@ +import * as React from 'react'; + +export const OverviewImagesModal: React.FC = () => { + return <div className="h-[200px] w-[500px] bg-white " />; +}; diff --git a/src/components/FunctionalArea/Modal/OverviewImagesModal/index.ts b/src/components/FunctionalArea/Modal/OverviewImagesModal/index.ts new file mode 100644 index 00000000..ac977070 --- /dev/null +++ b/src/components/FunctionalArea/Modal/OverviewImagesModal/index.ts @@ -0,0 +1 @@ +export { OverviewImagesModal } from './OverviewImagesModal.component'; diff --git a/src/components/FunctionalArea/Modal/index.ts b/src/components/FunctionalArea/Modal/index.ts new file mode 100644 index 00000000..b0252805 --- /dev/null +++ b/src/components/FunctionalArea/Modal/index.ts @@ -0,0 +1 @@ +export { Modal } from './Modal.component'; diff --git a/src/components/Map/Map.component.tsx b/src/components/Map/Map.component.tsx index 27cfcff4..90492655 100644 --- a/src/components/Map/Map.component.tsx +++ b/src/components/Map/Map.component.tsx @@ -1,6 +1,6 @@ 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"> diff --git a/src/components/Map/MapAdditionalOptions/MapAdditionalOptions.component.tsx b/src/components/Map/MapAdditionalOptions/MapAdditionalOptions.component.tsx index a7845ac5..1435e558 100644 --- a/src/components/Map/MapAdditionalOptions/MapAdditionalOptions.component.tsx +++ b/src/components/Map/MapAdditionalOptions/MapAdditionalOptions.component.tsx @@ -1,10 +1,24 @@ +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> + ); +}; diff --git a/src/components/SPA/MinervaSPA.component.tsx b/src/components/SPA/MinervaSPA.component.tsx index 3376b1ca..ebcd9438 100644 --- a/src/components/SPA/MinervaSPA.component.tsx +++ b/src/components/SPA/MinervaSPA.component.tsx @@ -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> ); }; diff --git a/src/redux/modal/modal.constants.ts b/src/redux/modal/modal.constants.ts new file mode 100644 index 00000000..90ee066d --- /dev/null +++ b/src/redux/modal/modal.constants.ts @@ -0,0 +1,7 @@ +import { ModalState } from './modal.types'; + +export const MODAL_INITIAL_STATE: ModalState = { + isOpen: false, + modalName: 'none', + modalTitle: '', +}; diff --git a/src/redux/modal/modal.mock.ts b/src/redux/modal/modal.mock.ts new file mode 100644 index 00000000..f145fba0 --- /dev/null +++ b/src/redux/modal/modal.mock.ts @@ -0,0 +1,7 @@ +import { ModalState } from './modal.types'; + +export const MODAL_INITIAL_STATE_MOCK: ModalState = { + isOpen: false, + modalName: 'none', + modalTitle: '', +}; diff --git a/src/redux/modal/modal.reducers.ts b/src/redux/modal/modal.reducers.ts new file mode 100644 index 00000000..17687aca --- /dev/null +++ b/src/redux/modal/modal.reducers.ts @@ -0,0 +1,19 @@ +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'; +}; diff --git a/src/redux/modal/modal.selector.ts b/src/redux/modal/modal.selector.ts new file mode 100644 index 00000000..6221d93c --- /dev/null +++ b/src/redux/modal/modal.selector.ts @@ -0,0 +1,6 @@ +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); diff --git a/src/redux/modal/modal.slice.ts b/src/redux/modal/modal.slice.ts new file mode 100644 index 00000000..9ba21287 --- /dev/null +++ b/src/redux/modal/modal.slice.ts @@ -0,0 +1,21 @@ +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; diff --git a/src/redux/modal/modal.types.ts b/src/redux/modal/modal.types.ts new file mode 100644 index 00000000..77b0e71f --- /dev/null +++ b/src/redux/modal/modal.types.ts @@ -0,0 +1,7 @@ +import { ModalName } from '@/types/modal'; + +export interface ModalState { + isOpen: boolean; + modalName: ModalName; + modalTitle: string; +} diff --git a/src/redux/root/root.fixtures.ts b/src/redux/root/root.fixtures.ts index bf70be50..0d592254 100644 --- a/src/redux/root/root.fixtures.ts +++ b/src/redux/root/root.fixtures.ts @@ -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, }; diff --git a/src/redux/store.ts b/src/redux/store.ts index d98c85c3..e60dd6df 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -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, diff --git a/src/types/modal.ts b/src/types/modal.ts new file mode 100644 index 00000000..edfac1fd --- /dev/null +++ b/src/types/modal.ts @@ -0,0 +1 @@ +export type ModalName = 'none' | 'overview-images'; diff --git a/tailwind.config.ts b/tailwind.config.ts index 62c3f4f7..75ce9dc5 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -28,6 +28,7 @@ const config: Config = { orange: '#f48c40', purple: '#6400e3', pink: '#f1009f', + 'cetacean-blue': '#070130', }, height: { 'calc-drawer': 'calc(100% - 104px)', -- GitLab