Skip to content

Commit

Permalink
feat(components): add proportion selector to mutations
Browse files Browse the repository at this point in the history
  • Loading branch information
JonasKellerer committed Apr 4, 2024
1 parent 59d785b commit 74cfdac
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 28 deletions.
19 changes: 11 additions & 8 deletions components/src/preact/components/percent-intput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FunctionComponent } from 'preact';
import { useState } from 'preact/hooks';
import { useEffect, useState } from 'preact/hooks';
import { ChangeEvent } from 'react';

export type PercentInputProps = {
Expand All @@ -12,30 +12,33 @@ const percentageInRange = (percentage: number) => {
};

export const PercentInput: FunctionComponent<PercentInputProps> = ({ percentage, setPercentage }) => {
const [error, setError] = useState(!percentageInRange(percentage));
const [internalPercentage, setInternalPercentage] = useState(percentage);

useEffect(() => {
setInternalPercentage(percentage);
}, [percentage]);

const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
const input = event.target as HTMLInputElement;
const value = Number(input.value);

const inRange = percentageInRange(value);

if (!inRange) {
setError(true);
} else {
setError(false);
if (inRange) {
setPercentage(value);
}
setInternalPercentage(value);
};

const isError = !percentageInRange(internalPercentage);
return (
<label className={`input input-bordered flex items-center gap-2 w-32 ${error ? 'input-error' : ''}`}>
<label className={`input input-bordered flex items-center gap-2 w-32 ${isError ? 'input-error' : ''}`}>
<input
type='number'
step={0.1}
min={0}
max={100}
value={percentage}
value={internalPercentage}
onInput={handleInputChange}
lang='en'
className={`grow w-16`}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Meta, StoryObj } from '@storybook/preact';
import { ProportionSelector } from './proportion-selector';
import { ProportionSelectorDropdown, ProportionSelectorDropdownProps } from './proportion-selector-dropdown';
import { FunctionComponent } from 'preact';
import { useState } from 'preact/hooks';
import { expect, fn, userEvent, within } from '@storybook/test';

const meta: Meta<ProportionSelectorDropdownProps> = {
title: 'Component/Proportion selector dropdown',
component: ProportionSelector,
parameters: { fetchMock: {} },
};

export default meta;

const WrapperWithState: FunctionComponent<{
setMinProportion: (value: number) => void;
setMaxProportion: (value: number) => void;
}> = ({ setMinProportion, setMaxProportion }) => {
const [wrapperMinProportion, setWrapperMinProportion] = useState(0.05);
const [wrapperMaxProportion, setWrapperMaxProportion] = useState(1);

return (
<ProportionSelectorDropdown
minProportion={wrapperMinProportion}
maxProportion={wrapperMaxProportion}
setMinProportion={(value: number) => {
setWrapperMinProportion(value);
setMinProportion(value);
}}
setMaxProportion={(value: number) => {
setWrapperMaxProportion(value);
setMaxProportion(value);
}}
/>
);
};

export const ProportionSelectorStory: StoryObj<ProportionSelectorDropdownProps> = {
render: (args) => {
return <WrapperWithState setMinProportion={args.setMinProportion} setMaxProportion={args.setMaxProportion} />;
},
args: {
setMinProportion: fn(),
setMaxProportion: fn(),
},
play: async ({ canvasElement, step, args }) => {
const canvas = within(canvasElement);

await step('Expect initial proportion to show on the button', async () => {
const button = canvas.getByRole('button');
await expect(button).toHaveTextContent('Proportion 5.0% - 100.0%');
});

await step('Change min proportion and expect it to show on the button', async () => {
const button = canvas.getByRole('button');
await userEvent.click(button);

const minInput = canvas.getAllByLabelText('%')[0];
await userEvent.clear(minInput);
await userEvent.type(minInput, '10');

await expect(button).toHaveTextContent('Proportion 10.0% - 100.0%');
await expect(args.setMinProportion).toHaveBeenCalledWith(0.1);
});
},
};
34 changes: 34 additions & 0 deletions components/src/preact/components/proportion-selector-dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { FunctionComponent } from 'preact';
import { ProportionSelector, ProportionSelectorProps } from './proportion-selector';

export interface ProportionSelectorDropdownProps extends ProportionSelectorProps {
openDirection?: 'left' | 'right';
}

export const ProportionSelectorDropdown: FunctionComponent<ProportionSelectorDropdownProps> = ({
minProportion,
maxProportion,
setMinProportion,
setMaxProportion,
openDirection = 'right',
}) => {
const label = `${(minProportion * 100).toFixed(1)}% - ${(maxProportion * 100).toFixed(1)}%`;

return (
<div class={`dropdown ${openDirection === 'left' ? 'dropdown-end' : ''}`}>
<div tabIndex={0} role='button' class='btn btn-xs whitespace-nowrap'>
Proportion {label}
</div>
<ul tabIndex={0} class='p-2 shadow menu dropdown-content z-[1] bg-base-100 rounded-box w-72'>
<div class='mb-2 ml-2'>
<ProportionSelector
minProportion={minProportion}
maxProportion={maxProportion}
setMinProportion={setMinProportion}
setMaxProportion={setMaxProportion}
/>
</div>
</ul>
</div>
);
};
70 changes: 68 additions & 2 deletions components/src/preact/components/proportion-selector.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Meta, StoryObj } from '@storybook/preact';
import { ProportionSelector, ProportionSelectorProps } from './proportion-selector';
import { FunctionComponent } from 'preact';
import { useState } from 'preact/hooks';
import { expect, fireEvent, fn, userEvent, waitFor, within } from '@storybook/test';

const meta: Meta<ProportionSelectorProps> = {
title: 'Component/Proportion selector',
Expand All @@ -9,8 +12,71 @@ const meta: Meta<ProportionSelectorProps> = {

export default meta;

const WrapperWithState: FunctionComponent<{
setMinProportion: (value: number) => void;
setMaxProportion: (value: number) => void;
}> = ({ setMinProportion, setMaxProportion }) => {
const [wrapperMinProportion, setWrapperMinProportion] = useState(0.05);
const [wrapperMaxProportion, setWrapperMaxProportion] = useState(1);

return (
<ProportionSelector
minProportion={wrapperMinProportion}
maxProportion={wrapperMaxProportion}
setMinProportion={(value: number) => {
setWrapperMinProportion(value);
setMinProportion(value);
}}
setMaxProportion={(value: number) => {
setWrapperMaxProportion(value);
setMaxProportion(value);
}}
/>
);
};

export const ProportionSelectorStory: StoryObj<ProportionSelectorProps> = {
render: () => {
return <ProportionSelector />;
render: (args) => {
return <WrapperWithState setMinProportion={args.setMinProportion} setMaxProportion={args.setMaxProportion} />;
},
args: {
setMinProportion: fn(),
setMaxProportion: fn(),
},
play: async ({ canvasElement, step, args }) => {
const canvas = within(canvasElement);

await step('Expect initial min proportion to be 5% and max proportion 100%', async () => {
await expect(canvas.getAllByLabelText('%')[0]).toHaveValue(5);
await expect(canvas.getAllByLabelText('%')[1]).toHaveValue(100);
});

await step('Change min proportion to 10%', async () => {
const minInput = canvas.getAllByLabelText('%')[0];
await userEvent.clear(minInput);
await userEvent.type(minInput, '10');
await expect(args.setMinProportion).toHaveBeenCalledWith(0.1);
});

await step('Change max proportion to 50%', async () => {
const maxInput = canvas.getAllByLabelText('%')[1];
await userEvent.clear(maxInput);
await userEvent.type(maxInput, '50');
await expect(args.setMaxProportion).toHaveBeenCalledWith(0.5);
});

await step('Move min proportion silder to 20%', async () => {
const minSlider = canvas.getAllByRole('slider')[0];
await fireEvent.input(minSlider, { target: { value: '20' } });
await expect(args.setMinProportion).toHaveBeenCalledWith(0.2);
await waitFor(() => expect(canvas.getAllByLabelText('%')[0]).toHaveValue(20));
});

await step('Move max proportion silder to 80%', async () => {
const maxSlider = canvas.getAllByRole('slider')[1];
await fireEvent.input(maxSlider, { target: { value: '80' } });
await expect(args.setMaxProportion).toHaveBeenCalledWith(0.8);
await waitFor(() => expect(canvas.getAllByLabelText('%')[1]).toHaveValue(80));
});
},
};
20 changes: 13 additions & 7 deletions components/src/preact/components/proportion-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { FunctionComponent } from 'preact';
import { useState } from 'preact/hooks';
import { MinMaxRangeSlider } from './min-max-range-slider';
import { PercentInput } from './percent-intput';

export interface ProportionSelectorProps {}

export const ProportionSelector: FunctionComponent<ProportionSelectorProps> = () => {
const [minProportion, setMinProportion] = useState(0);
const [maxProportion, setMaxProportion] = useState(1);
export interface ProportionSelectorProps {
minProportion: number;
maxProportion: number;
setMinProportion: (minProportion: number) => void;
setMaxProportion: (maxProportion: number) => void;
}

export const ProportionSelector: FunctionComponent<ProportionSelectorProps> = ({
minProportion,
maxProportion,
setMinProportion,
setMaxProportion,
}) => {
return (
<div class='flex flex-col w-64'>
<div class='flex flex-col w-64 mb-2'>
<div class='flex items-center '>
<PercentInput
percentage={minProportion * 100}
Expand Down
42 changes: 32 additions & 10 deletions components/src/preact/mutations/mutations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import Info from '../components/info';
import Tabs from '../components/tabs';
import { CheckboxSelector } from '../components/checkbox-selector';
import { CsvDownloadButton } from '../components/csv-download-button';
import { ProportionSelectorDropdown } from '../components/proportion-selector-dropdown';

export type View = 'table' | 'grid';

Expand All @@ -33,6 +34,9 @@ type DisplayedSegment = {
export const Mutations: FunctionComponent<MutationsProps> = ({ variant, sequenceType, views }) => {
const lapis = useContext(LapisUrlContext);

const [minProportion, setMinProportion] = useState(0.05);
const [maxProportion, setMaxProportion] = useState(1);

const { data, error, isLoading } = useQuery(async () => {
const fetchedData = await queryMutations(variant, sequenceType, lapis);

Expand Down Expand Up @@ -114,15 +118,26 @@ export const Mutations: FunctionComponent<MutationsProps> = ({ variant, sequence
/>
);

const filteredData = data.data.content.filter((mutationEntry) => {
if (mutationEntry.mutation.segment === undefined) {
return true;
}
return displayedSegments.some(
(displayedSegment) =>
displayedSegment.segment === mutationEntry.mutation.segment && displayedSegment.checked,
);
});
const filterBySelectedSegments = (mutationEntries: MutationEntry[]) => {
return mutationEntries.filter((mutationEntry) => {
if (mutationEntry.mutation.segment === undefined) {
return true;
}
return displayedSegments.some(
(displayedSegment) =>
displayedSegment.segment === mutationEntry.mutation.segment && displayedSegment.checked,
);
});
};

const filterByProportion = (mutationEntries: MutationEntry[]) => {
return mutationEntries.filter((mutationEntry) => {
if (mutationEntry.type === 'insertion') {
return true;
}
return mutationEntry.proportion >= minProportion && mutationEntry.proportion <= maxProportion;
});
};

const getTab = (view: View, data: Dataset<MutationEntry>) => {
switch (view) {
Expand All @@ -134,11 +149,18 @@ export const Mutations: FunctionComponent<MutationsProps> = ({ variant, sequence
};

const tabs = views.map((view) => {
return getTab(view, { content: filteredData });
return getTab(view, { content: filterBySelectedSegments(filterByProportion(data.data.content)) });
});

const toolbar = (
<div class='flex flex-row'>
<ProportionSelectorDropdown
minProportion={minProportion}
maxProportion={maxProportion}
setMinProportion={setMinProportion}
setMaxProportion={setMaxProportion}
openDirection={'left'}
/>
{data.segments.length > 0 ? segmentSelector : null}
<CsvDownloadButton
className='mx-1 btn btn-xs'
Expand Down
5 changes: 4 additions & 1 deletion components/src/web-components/PreactLitAdapter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import { JSXInternal } from 'preact/src/jsx';
import { LapisUrlContext } from '../preact/LapisUrlContext';
import { LAPIS_URL } from '../constants';
import tailwindStyle from '../styles/tailwind.css?inline';
import minMaxPercentSliderCss from '../preact/components/min-max-percent-slider.css?inline';

import '../styles/tailwind.css';
import '../preact/components/min-max-percent-slider.css';

const tailwindElementCss = unsafeCSS(tailwindStyle);
const minMaxPercentSliderElementCss = unsafeCSS(minMaxPercentSliderCss);

export abstract class PreactLitAdapter extends ReactiveElement {
static override styles = [tailwindElementCss];
static override styles = [tailwindElementCss, minMaxPercentSliderElementCss];

@consume({ context: lapisContext })
lapis: string = '';
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 74cfdac

Please sign in to comment.