Skip to content

Commit

Permalink
refactor(components): extract modal component (#619)
Browse files Browse the repository at this point in the history
  • Loading branch information
fengelniederhammer authored Dec 19, 2024
1 parent 5aa4802 commit 95358f9
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 66 deletions.
27 changes: 11 additions & 16 deletions components/src/preact/components/error-display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { type FunctionComponent } from 'preact';
import { useEffect, useRef } from 'preact/hooks';
import { type ZodError } from 'zod';

import { InfoHeadline1, InfoParagraph } from './info';
import { Modal, useModalRef } from './modal';
import { LapisError, UnknownLapisError } from '../../lapisApi/lapisApi';

export const GS_ERROR_EVENT_TYPE = 'gs-error';
Expand Down Expand Up @@ -46,7 +48,7 @@ export const ErrorDisplay: FunctionComponent<ErrorDisplayProps> = ({ error, rese
console.error(error);

const containerRef = useRef<HTMLInputElement>(null);
const ref = useRef<HTMLDialogElement>(null);
const modalRef = useModalRef();

useEffect(() => {
containerRef.current?.dispatchEvent(new ErrorEvent(error));
Expand All @@ -66,23 +68,16 @@ export const ErrorDisplay: FunctionComponent<ErrorDisplayProps> = ({ error, rese
{details !== undefined && (
<>
{' '}
<button className='underline hover:text-gray-400' onClick={() => ref.current?.showModal()}>
<button
className='underline hover:text-gray-400'
onClick={() => modalRef.current?.showModal()}
>
Show details.
</button>
<dialog ref={ref} class='modal'>
<div class='modal-box'>
<form method='dialog'>
<button className='btn btn-sm btn-circle btn-ghost absolute right-2 top-2'>
</button>
</form>
<h1 class='text-lg'>{details.headline}</h1>
<div class='py-4'>{details.message}</div>
</div>
<form method='dialog' class='modal-backdrop'>
<button>close</button>
</form>
</dialog>
<Modal modalRef={modalRef}>
<InfoHeadline1>{details.headline}</InfoHeadline1>
<InfoParagraph>{details.message}</InfoParagraph>
</Modal>
</>
)}
</div>
Expand Down
24 changes: 5 additions & 19 deletions components/src/preact/components/info.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,20 @@
import { type FunctionComponent } from 'preact';
import { useRef } from 'preact/hooks';

import { Modal, useModalRef } from './modal';

const Info: FunctionComponent = ({ children }) => {
const dialogRef = useRef<HTMLDialogElement>(null);
const modalRef = useModalRef();

const toggleHelp = () => {
dialogRef.current?.showModal();
modalRef.current?.showModal();
};

return (
<div className='relative'>
<button type='button' className='btn btn-xs' onClick={toggleHelp}>
?
</button>
<dialog ref={dialogRef} className={'modal modal-bottom sm:modal-middle'}>
<div className='modal-box sm:max-w-5xl'>
<form method='dialog'>
<button className='btn btn-sm btn-circle btn-ghost absolute right-2 top-2'></button>
</form>
<div className={'flex flex-col'}>{children}</div>
<div className='modal-action'>
<form method='dialog'>
<button className={'float-right underline text-sm hover:text-blue-700 mr-2'}>Close</button>
</form>
</div>
</div>
<form method='dialog' className='modal-backdrop'>
<button>Helper to close when clicked outside</button>
</form>
</dialog>
<Modal modalRef={modalRef}>{children}</Modal>
</div>
);
};
Expand Down
44 changes: 44 additions & 0 deletions components/src/preact/components/modal.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { type Meta, type StoryObj } from '@storybook/preact';
import { expect, waitFor, within } from '@storybook/test';
import { type FunctionComponent } from 'preact';

import { Modal, type ModalProps, useModalRef } from './modal';

const meta: Meta<ModalProps> = {
title: 'Component/Modal',
component: Modal,
parameters: { fetchMock: {} },
};

export default meta;

const WrapperWithButtonThatOpensTheModal: FunctionComponent = () => {
const modalRef = useModalRef();

return (
<div>
<button className='btn' onClick={() => modalRef.current?.showModal()}>
Open modal
</button>
<Modal modalRef={modalRef}>
<h1>Modal content</h1>
</Modal>
</div>
);
};

export const ModalStory: StoryObj<ModalProps> = {
render: () => {
return <WrapperWithButtonThatOpensTheModal />;
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);

await step('Open the modal', async () => {
const button = canvas.getByText('Open modal');
button.click();

await waitFor(() => expect(canvas.getByText('Modal content')).toBeVisible());
});
},
};
31 changes: 31 additions & 0 deletions components/src/preact/components/modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { type FunctionComponent, type Ref } from 'preact';
import { useRef } from 'preact/hooks';

export type ModalProps = {
modalRef: Ref<HTMLDialogElement>;
};

export function useModalRef() {
return useRef<HTMLDialogElement>(null);
}

export const Modal: FunctionComponent<ModalProps> = ({ children, modalRef }) => {
return (
<dialog ref={modalRef} className={'modal modal-bottom sm:modal-middle'}>
<div className='modal-box sm:max-w-5xl'>
<form method='dialog'>
<button className='btn btn-sm btn-circle btn-ghost absolute right-2 top-2'></button>
</form>
<div className={'flex flex-col'}>{children}</div>
<div className='modal-action'>
<form method='dialog'>
<button className={'float-right underline text-sm hover:text-blue-700 mr-2'}>Close</button>
</form>
</div>
</div>
<form method='dialog' className='modal-backdrop'>
<button>Helper to close when clicked outside</button>
</form>
</dialog>
);
};
53 changes: 22 additions & 31 deletions components/src/preact/map/sequences-by-location-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { type GeoJsonFeatureProperties, type MapSource, useGeoJsonMap } from './
import { type AggregateData } from '../../query/queryAggregateData';
import { InfoHeadline1, InfoParagraph } from '../components/info';
import { LoadingDisplay } from '../components/loading-display';
import { Modal, useModalRef } from '../components/modal';
import { formatProportion } from '../shared/table/formatProportion';

type FeatureData = { proportion: number; count: number };
Expand Down Expand Up @@ -135,48 +136,38 @@ const DataMatchInformation: FunctionComponent<DataMatchInformationProps> = ({
nullCount,
hasTableView,
}) => {
const dialogRef = useRef<HTMLDialogElement>(null);
const modalRef = useModalRef();

const proportion = formatProportion(countOfMatchedLocationData / totalCount);

return (
<>
<button
onClick={() => dialogRef.current?.showModal()}
onClick={() => modalRef.current?.showModal()}
className='text-sm absolute bottom-0 px-1 z-[1001] bg-white rounded border cursor-pointer tooltip'
data-tip='Click for detailed information'
>
This map shows {proportion} of the data.
</button>
<dialog ref={dialogRef} className={'modal modal-middle'}>
<div className='modal-box max-w-3xl'>
<InfoHeadline1>Sequences By Location - Map View</InfoHeadline1>
<InfoParagraph>
The current filter has matched {totalCount.toLocaleString('en-us')} sequences. From these
sequences, we were able to match {countOfMatchedLocationData.toLocaleString('en-us')} (
{proportion}) on locations on the map.
</InfoParagraph>
<InfoParagraph>
{unmatchedLocations.length > 0 && (
<>
The following locations from the data could not be matched on the map:{' '}
{unmatchedLocations.map((it) => `"${it}"`).join(', ')}.{' '}
</>
)}
{nullCount > 0 &&
`${nullCount.toLocaleString('en-us')} matching sequences have no location information. `}
{hasTableView && 'You can check the table view for more detailed information.'}
</InfoParagraph>
<div className='modal-action'>
<form method='dialog'>
<button className={'float-right underline text-sm hover:text-blue-700 mr-2'}>Close</button>
</form>
</div>
</div>
<form method='dialog' className='modal-backdrop'>
<button>Helper to close when clicked outside</button>
</form>
</dialog>
<Modal modalRef={modalRef}>
<InfoHeadline1>Sequences By Location - Map View</InfoHeadline1>
<InfoParagraph>
The current filter has matched {totalCount.toLocaleString('en-us')} sequences. From these sequences,
we were able to match {countOfMatchedLocationData.toLocaleString('en-us')} ({proportion}) on
locations on the map.
</InfoParagraph>
<InfoParagraph>
{unmatchedLocations.length > 0 && (
<>
The following locations from the data could not be matched on the map:{' '}
{unmatchedLocations.map((it) => `"${it}"`).join(', ')}.{' '}
</>
)}
{nullCount > 0 &&
`${nullCount.toLocaleString('en-us')} matching sequences have no location information. `}
{hasTableView && 'You can check the table view for more detailed information.'}
</InfoParagraph>
</Modal>
</>
);
};
Expand Down

0 comments on commit 95358f9

Please sign in to comment.