@genspectrum/dashboard-components 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/custom-elements.json +488 -117
  2. package/dist/dashboard-components.js +904 -466
  3. package/dist/dashboard-components.js.map +1 -1
  4. package/dist/genspectrum-components.d.ts +473 -67
  5. package/dist/style.css +273 -153
  6. package/package.json +11 -7
  7. package/src/preact/aggregatedData/aggregate.stories.tsx +7 -5
  8. package/src/preact/aggregatedData/aggregate.tsx +16 -7
  9. package/src/preact/components/ReferenceGenomesAwaiter.tsx +25 -0
  10. package/src/preact/components/csv-download-button.tsx +8 -2
  11. package/src/preact/components/headline.stories.tsx +19 -1
  12. package/src/preact/components/headline.tsx +25 -5
  13. package/src/preact/components/info.stories.tsx +24 -3
  14. package/src/preact/components/info.tsx +49 -5
  15. package/src/preact/components/min-max-range-slider.tsx +4 -4
  16. package/src/preact/components/percent-intput.tsx +2 -3
  17. package/src/preact/components/resize-container.tsx +23 -0
  18. package/src/preact/components/table.tsx +1 -0
  19. package/src/preact/components/tabs.stories.tsx +2 -2
  20. package/src/preact/components/tabs.tsx +47 -24
  21. package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +36 -4
  22. package/src/preact/dateRangeSelector/date-range-selector.tsx +67 -53
  23. package/src/preact/locationFilter/location-filter.tsx +2 -2
  24. package/src/preact/mutationComparison/getMutationComparisonTableData.spec.ts +5 -5
  25. package/src/preact/mutationComparison/getMutationComparisonTableData.ts +45 -10
  26. package/src/preact/mutationComparison/mutation-comparison-table.tsx +20 -22
  27. package/src/preact/mutationComparison/mutation-comparison-venn.tsx +6 -3
  28. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +11 -1
  29. package/src/preact/mutationComparison/mutation-comparison.tsx +16 -7
  30. package/src/preact/mutationFilter/mutation-filter.stories.tsx +70 -31
  31. package/src/preact/mutationFilter/mutation-filter.tsx +62 -14
  32. package/src/preact/mutations/getInsertionsTableData.spec.ts +6 -4
  33. package/src/preact/mutations/getInsertionsTableData.ts +1 -1
  34. package/src/preact/mutations/getMutationsTableData.spec.ts +9 -19
  35. package/src/preact/mutations/getMutationsTableData.ts +1 -1
  36. package/src/preact/mutations/mutations-insertions-table.tsx +3 -1
  37. package/src/preact/mutations/mutations-table.tsx +3 -1
  38. package/src/preact/mutations/mutations.stories.tsx +11 -1
  39. package/src/preact/mutations/mutations.tsx +24 -7
  40. package/src/preact/prevalenceOverTime/prevalence-over-time-bar-chart.tsx +1 -0
  41. package/src/preact/prevalenceOverTime/prevalence-over-time-bubble-chart.tsx +1 -0
  42. package/src/preact/prevalenceOverTime/prevalence-over-time-line-chart.tsx +1 -0
  43. package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +8 -0
  44. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +31 -13
  45. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage-chart.tsx +8 -5
  46. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +15 -0
  47. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +62 -12
  48. package/src/preact/shared/sort/sortInsertions.spec.ts +11 -10
  49. package/src/preact/shared/sort/sortInsertions.ts +10 -17
  50. package/src/preact/shared/sort/sortSubstitutionsAndDeletions.spec.ts +19 -10
  51. package/src/preact/shared/sort/sortSubstitutionsAndDeletions.ts +45 -12
  52. package/src/preact/textInput/text-input.stories.tsx +22 -1
  53. package/src/preact/textInput/text-input.tsx +3 -1
  54. package/src/utils/typeAssertions.spec.ts +31 -0
  55. package/src/utils/typeAssertions.ts +16 -0
  56. package/src/web-components/PreactLitAdapter.tsx +0 -1
  57. package/src/web-components/app.stories.ts +129 -0
  58. package/src/web-components/app.ts +27 -6
  59. package/src/web-components/display/aggregate-component.stories.ts +24 -11
  60. package/src/web-components/display/aggregate-component.tsx +26 -5
  61. package/src/web-components/display/mutation-comparison-component.stories.ts +32 -11
  62. package/src/web-components/display/mutation-comparison-component.tsx +79 -4
  63. package/src/web-components/display/mutations-component.stories.ts +40 -19
  64. package/src/web-components/display/mutations-component.tsx +71 -4
  65. package/src/web-components/display/prevalence-over-time-component.stories.ts +44 -18
  66. package/src/web-components/display/prevalence-over-time-component.tsx +105 -5
  67. package/src/web-components/display/relative-growth-advantage-component.stories.ts +32 -10
  68. package/src/web-components/display/relative-growth-advantage-component.tsx +66 -3
  69. package/src/web-components/input/date-range-selector-component.stories.ts +51 -9
  70. package/src/web-components/input/date-range-selector-component.tsx +69 -4
  71. package/src/web-components/input/location-filter-component.stories.ts +15 -4
  72. package/src/web-components/input/location-filter-component.tsx +2 -6
  73. package/src/web-components/input/mutation-filter-component.stories.ts +33 -12
  74. package/src/web-components/input/mutation-filter-component.tsx +60 -4
  75. package/src/web-components/input/text-input-component.stories.ts +26 -6
  76. package/src/web-components/input/text-input-component.tsx +34 -3
  77. package/src/web-components/display/aggregate-component.mdx +0 -25
  78. package/src/web-components/input/location-filter.mdx +0 -25
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -35,9 +35,10 @@
35
35
  "lint:lit-analyzer": "lit-analyzer",
36
36
  "generate-manifest": "npx custom-elements-manifest analyze --litelement --globs src/web-components/**",
37
37
  "generate-manifest:watch": "npm run generate-manifest -- --watch",
38
- "format": "prettier \"**/*.{cjs,html,js,json,md,ts,tsx}\" --ignore-path ./.eslintignore --write",
38
+ "format": "prettier \"**/*.{cjs,html,js,json,md,ts,tsx}\" --write",
39
39
  "check-format": "prettier --check \"**/*.{ts,tsx,json,md,mdx,mjs,cjs}\"",
40
40
  "check-types": "tsc --noEmit",
41
+ "check-dependencies": "depcheck",
41
42
  "storybook": "storybook dev -p 6006",
42
43
  "storybook-preact": "storybook dev --port 6007 --config-dir .storybook-preact",
43
44
  "build-storybook": "storybook build",
@@ -55,8 +56,8 @@
55
56
  "lit"
56
57
  ],
57
58
  "dependencies": {
58
- "@floating-ui/dom": "^1.6.3",
59
59
  "@lit/context": "^1.1.1",
60
+ "@lit/reactive-element": "^2.0.4",
60
61
  "@lit/task": "^1.0.0",
61
62
  "chart.js": "^4.4.2",
62
63
  "chartjs-chart-error-bars": "^4.4.0",
@@ -69,17 +70,18 @@
69
70
  "zod": "^3.23.0"
70
71
  },
71
72
  "devDependencies": {
72
- "@custom-elements-manifest/analyzer": "^0.9.4",
73
+ "@custom-elements-manifest/analyzer": "^0.10.2",
73
74
  "@playwright/test": "^1.43.1",
74
- "@preact/preset-vite": "^2.8.2",
75
+ "@storybook/addon-actions": "^8.0.9",
75
76
  "@storybook/addon-essentials": "^8.0.9",
76
77
  "@storybook/addon-interactions": "^8.0.9",
77
78
  "@storybook/addon-links": "^8.0.9",
78
- "@storybook/blocks": "^8.0.0",
79
+ "@storybook/blocks": "^8.0.10",
79
80
  "@storybook/preact": "^8.0.9",
80
81
  "@storybook/preact-vite": "^8.0.9",
81
82
  "@storybook/test": "^8.0.0",
82
- "@storybook/test-runner": "^0.17.0",
83
+ "@storybook/test-runner": "^0.18.0",
84
+ "@storybook/types": "^8.0.9",
83
85
  "@storybook/web-components": "^8.0.9",
84
86
  "@storybook/web-components-vite": "^8.0.9",
85
87
  "@types/node": "^20.12.7",
@@ -87,6 +89,7 @@
87
89
  "@typescript-eslint/parser": "^7.7.0",
88
90
  "autoprefixer": "^10.4.19",
89
91
  "daisyui": "^4.10.2",
92
+ "depcheck": "^1.4.7",
90
93
  "eslint": "^8.57.0",
91
94
  "eslint-config-preact": "^1.3.0",
92
95
  "eslint-plugin-import": "^2.29.1",
@@ -97,6 +100,7 @@
97
100
  "msw": "^2.2.14",
98
101
  "postcss": "^8.4.38",
99
102
  "prettier": "^3.2.5",
103
+ "react": "^18.3.1",
100
104
  "release-please": "^16.10.2",
101
105
  "storybook": "^8.0.9",
102
106
  "storybook-addon-fetch-mock": "^2.0.0",
@@ -10,6 +10,8 @@ const meta: Meta<AggregateProps> = {
10
10
  component: Aggregate,
11
11
  argTypes: {
12
12
  fields: [{ control: 'object' }],
13
+ size: [{ control: 'object' }],
14
+ headline: { control: 'text' },
13
15
  },
14
16
  parameters: {
15
17
  fetchMock: {
@@ -37,11 +39,9 @@ export default meta;
37
39
 
38
40
  export const Default: StoryObj<AggregateProps> = {
39
41
  render: (args) => (
40
- <div class='max-w-screen-lg'>
41
- <LapisUrlContext.Provider value={LAPIS_URL}>
42
- <Aggregate {...args} />
43
- </LapisUrlContext.Provider>
44
- </div>
42
+ <LapisUrlContext.Provider value={LAPIS_URL}>
43
+ <Aggregate {...args} />
44
+ </LapisUrlContext.Provider>
45
45
  ),
46
46
  args: {
47
47
  fields: ['division', 'host'],
@@ -49,5 +49,7 @@ export const Default: StoryObj<AggregateProps> = {
49
49
  filter: {
50
50
  country: 'USA',
51
51
  },
52
+ size: { width: '100%', height: '70vh' },
53
+ headline: 'Aggregate',
52
54
  },
53
55
  };
@@ -11,6 +11,7 @@ import Headline from '../components/headline';
11
11
  import Info from '../components/info';
12
12
  import { LoadingDisplay } from '../components/loading-display';
13
13
  import { NoDataDisplay } from '../components/no-data-display';
14
+ import { ResizeContainer, type Size } from '../components/resize-container';
14
15
  import Tabs from '../components/tabs';
15
16
  import { useQuery } from '../useQuery';
16
17
 
@@ -20,17 +21,23 @@ export interface AggregateProps {
20
21
  filter: LapisFilter;
21
22
  fields: string[];
22
23
  views: View[];
24
+ size?: Size;
25
+ headline?: string;
23
26
  }
24
27
 
25
- export const Aggregate: FunctionComponent<AggregateProps> = ({ fields, views, filter }) => {
28
+ export const Aggregate: FunctionComponent<AggregateProps> = ({
29
+ fields,
30
+ views,
31
+ filter,
32
+ size,
33
+ headline = 'Aggregate',
34
+ }) => {
26
35
  const lapis = useContext(LapisUrlContext);
27
36
 
28
37
  const { data, error, isLoading } = useQuery(async () => {
29
38
  return queryAggregateData(filter, fields, lapis);
30
39
  }, [filter, fields, lapis]);
31
40
 
32
- const headline = 'Aggregate';
33
-
34
41
  if (isLoading) {
35
42
  return (
36
43
  <Headline heading={headline}>
@@ -56,9 +63,11 @@ export const Aggregate: FunctionComponent<AggregateProps> = ({ fields, views, fi
56
63
  }
57
64
 
58
65
  return (
59
- <Headline heading={headline}>
60
- <AggregatedDataTabs data={data} views={views} fields={fields} />
61
- </Headline>
66
+ <ResizeContainer size={size} defaultSize={{ height: '700px', width: '100%' }}>
67
+ <Headline heading={headline}>
68
+ <AggregatedDataTabs data={data} views={views} fields={fields} />
69
+ </Headline>
70
+ </ResizeContainer>
62
71
  );
63
72
  };
64
73
 
@@ -92,7 +101,7 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({ data }) => {
92
101
  return (
93
102
  <div class='flex flex-row'>
94
103
  <CsvDownloadButton className='mx-1 btn btn-xs' getData={() => data} filename='aggregate.csv' />
95
- <Info className='mx-1' content='Info for aggregate' />
104
+ <Info>Info for aggregate</Info>
96
105
  </div>
97
106
  );
98
107
  };
@@ -0,0 +1,25 @@
1
+ import { type ComponentChildren, type FunctionalComponent } from 'preact';
2
+ import { useContext } from 'preact/hooks';
3
+
4
+ import { type ReferenceGenome } from '../../lapisApi/ReferenceGenome';
5
+ import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
6
+
7
+ /**
8
+ * Sometimes the reference genome is not immediately available.
9
+ * This component will display a loading spinner until the reference genome is available.
10
+ * Child components can assume that the reference genome is available on the first render,
11
+ * which e.g. matters for initial values of `useState`.
12
+ */
13
+ export const ReferenceGenomesAwaiter: FunctionalComponent<{ children: ComponentChildren }> = ({ children }) => {
14
+ const referenceGenome = useContext(ReferenceGenomeContext);
15
+
16
+ if (isNotInitialized(referenceGenome)) {
17
+ return <div className='laoding loading-spinner loading-md'>Loading...</div>;
18
+ }
19
+
20
+ return <>{children}</>;
21
+ };
22
+
23
+ function isNotInitialized(referenceGenome: ReferenceGenome) {
24
+ return referenceGenome.nucleotideSequences.length === 0 && referenceGenome.genes.length === 0;
25
+ }
@@ -1,9 +1,15 @@
1
1
  import { type FunctionComponent } from 'preact';
2
2
 
3
+ type ToStringable = {
4
+ toString: () => string;
5
+ };
6
+
7
+ type DataValue = string | number | boolean | null | ToStringable;
8
+
3
9
  export interface CsvDownloadButtonProps {
4
10
  label?: string;
5
11
  filename?: string;
6
- getData: () => Record<string, string | number | boolean | null>[];
12
+ getData: () => Record<string, DataValue>[];
7
13
  className?: string;
8
14
  }
9
15
 
@@ -32,7 +38,7 @@ export const CsvDownloadButton: FunctionComponent<CsvDownloadButtonProps> = ({
32
38
  return header + rows;
33
39
  };
34
40
 
35
- const getDataKeys = (data: Record<string, string | number | boolean | null>[]) => {
41
+ const getDataKeys = (data: Record<string, DataValue>[]) => {
36
42
  const keysSet = data
37
43
  .map((row) => Object.keys(row))
38
44
  .reduce((accumulatedKeys, keys) => {
@@ -3,10 +3,13 @@ import { expect, within } from '@storybook/test';
3
3
 
4
4
  import Headline, { type HeadlineProps } from './headline';
5
5
 
6
- const meta: Meta<typeof Headline> = {
6
+ const meta: Meta<HeadlineProps> = {
7
7
  title: 'Component/Headline',
8
8
  component: Headline,
9
9
  parameters: { fetchMock: {} },
10
+ argTypes: {
11
+ heading: { control: 'text' },
12
+ },
10
13
  };
11
14
 
12
15
  export default meta;
@@ -27,3 +30,18 @@ export const HeadlineStory: StoryObj<HeadlineProps> = {
27
30
  await expect(canvas.getByText('Some Content')).toBeInTheDocument();
28
31
  },
29
32
  };
33
+
34
+ export const NoHeadlineStory: StoryObj<HeadlineProps> = {
35
+ render: (args) => (
36
+ <Headline {...args}>
37
+ <div class='flex justify-center px-4 py-16 bg-base-200'>Some Content</div>
38
+ </Headline>
39
+ ),
40
+ args: {},
41
+ play: async ({ canvasElement }) => {
42
+ const canvas = within(canvasElement);
43
+
44
+ await expect(canvas.queryByText('My Headline')).not.toBeInTheDocument();
45
+ await expect(canvas.getByText('Some Content')).toBeInTheDocument();
46
+ },
47
+ };
@@ -1,15 +1,35 @@
1
1
  import { type FunctionComponent } from 'preact';
2
+ import { useEffect, useRef, useState } from 'preact/hooks';
2
3
 
3
4
  export interface HeadlineProps {
4
- heading: string;
5
+ heading?: string;
5
6
  }
6
7
 
7
8
  const Headline: FunctionComponent<HeadlineProps> = ({ heading, children }) => {
9
+ if (!heading) {
10
+ return <>{children}</>;
11
+ }
12
+
13
+ return <ResizingHeadline heading={heading}>{children}</ResizingHeadline>;
14
+ };
15
+
16
+ const ResizingHeadline: FunctionComponent<HeadlineProps> = ({ heading, children }) => {
17
+ const ref = useRef<HTMLHeadingElement>(null);
18
+
19
+ const [h1Height, setH1Height] = useState('2rem');
20
+
21
+ useEffect(() => {
22
+ if (ref.current) {
23
+ const h1Height = ref.current.getBoundingClientRect().height;
24
+ setH1Height(`${h1Height}px`);
25
+ }
26
+ }, []);
27
+
8
28
  return (
9
- <>
10
- <h1>{heading}</h1>
11
- {children}
12
- </>
29
+ <div className='h-full w-full'>
30
+ <h1 ref={ref}>{heading}</h1>
31
+ <div style={{ height: `calc(100% - ${h1Height})` }}>{children}</div>
32
+ </div>
13
33
  );
14
34
  };
15
35
 
@@ -1,4 +1,5 @@
1
1
  import { type Meta, type StoryObj } from '@storybook/preact';
2
+ import { expect, fireEvent, waitFor, within } from '@storybook/test';
2
3
 
3
4
  import Info, { type InfoProps } from './info';
4
5
 
@@ -7,16 +8,36 @@ const meta: Meta<InfoProps> = {
7
8
  component: Info,
8
9
  parameters: { fetchMock: {} },
9
10
  args: {
10
- content: 'This is a tooltip which shows some information.',
11
+ size: { width: '400px', height: '100px' },
11
12
  },
12
13
  };
13
14
 
14
15
  export default meta;
15
16
 
17
+ const tooltipText = 'This is a tooltip which shows some information.';
18
+
16
19
  export const InfoStory: StoryObj<InfoProps> = {
17
20
  render: (args) => (
18
- <div class='flex justify-center px-4 py-16 bg-base-200'>
19
- <Info {...args} />
21
+ <div class='flex justify-center px-4 py-16'>
22
+ <Info {...args}>{tooltipText}</Info>
20
23
  </div>
21
24
  ),
22
25
  };
26
+
27
+ export const ShowsInfoOnClick: StoryObj<InfoProps> = {
28
+ ...InfoStory,
29
+ play: async ({ canvasElement }) => {
30
+ const canvas = within(canvasElement);
31
+ const loading = canvas.getByRole('button', { name: '?' });
32
+
33
+ await waitFor(() => expect(loading).toBeInTheDocument());
34
+
35
+ await fireEvent.click(loading);
36
+
37
+ await waitFor(() => expect(canvas.getByText(tooltipText, { exact: false })).toBeInTheDocument());
38
+
39
+ await fireEvent.click(canvas.getByRole('button', { name: 'Close' }));
40
+
41
+ await waitFor(() => expect(canvas.queryByText(tooltipText, { exact: false })).not.toBeInTheDocument());
42
+ },
43
+ };
@@ -1,16 +1,60 @@
1
1
  import { type FunctionComponent } from 'preact';
2
+ import { useState } from 'preact/hooks';
2
3
 
3
4
  export interface InfoProps {
4
- content: string;
5
- className?: string;
5
+ size?: {
6
+ height?: string;
7
+ width?: string;
8
+ };
6
9
  }
7
10
 
8
- const Info: FunctionComponent<InfoProps> = ({ content, className }) => {
11
+ const Info: FunctionComponent<InfoProps> = ({ children, size }) => {
12
+ const [showHelp, setShowHelp] = useState(false);
13
+
14
+ const toggleHelp = () => {
15
+ setShowHelp(!showHelp);
16
+ };
17
+
9
18
  return (
10
- <div class={`${className} tooltip`} data-tip={content}>
11
- <button class='btn btn-xs'>?</button>
19
+ <div className='relative'>
20
+ <button className='btn btn-xs' onClick={toggleHelp}>
21
+ ?
22
+ </button>
23
+ {showHelp && (
24
+ <div
25
+ className='absolute top-8 right-6 bg-white p-2 border border-black flex flex-col overflow-auto shadow-lg rounded z-50'
26
+ style={size}
27
+ >
28
+ <div className='flex flex-col'>{children}</div>
29
+ <div className='flex justify-end'>
30
+ <button className='text-sm underline mt-2' onClick={toggleHelp}>
31
+ Close
32
+ </button>
33
+ </div>
34
+ </div>
35
+ )}
12
36
  </div>
13
37
  );
14
38
  };
15
39
 
40
+ export const InfoHeadline1: FunctionComponent = ({ children }) => {
41
+ return <h1 className='text-lg font-bold'>{children}</h1>;
42
+ };
43
+
44
+ export const InfoHeadline2: FunctionComponent = ({ children }) => {
45
+ return <h2 className='text-base font-bold mt-4'>{children}</h2>;
46
+ };
47
+
48
+ export const InfoParagraph: FunctionComponent = ({ children }) => {
49
+ return <p className='text-justify my-1'>{children}</p>;
50
+ };
51
+
52
+ export const InfoLink: FunctionComponent<{ href: string }> = ({ children, href }) => {
53
+ return (
54
+ <a className='text-blue-600 hover:text-blue-800' href={href} target='_blank' rel='noopener noreferrer'>
55
+ {children}
56
+ </a>
57
+ );
58
+ };
59
+
16
60
  export default Info;
@@ -1,6 +1,6 @@
1
- import { type FunctionComponent } from 'preact';
1
+ import { type FunctionComponent, type JSX } from 'preact';
2
2
  import { useState } from 'preact/hooks';
3
- import { type ChangeEvent } from 'react';
3
+
4
4
  import './min-max-percent-slider.css';
5
5
 
6
6
  export interface MinMaxPercentSliderProps {
@@ -27,7 +27,7 @@ export const MinMaxRangeSlider: FunctionComponent<MinMaxPercentSliderProps> = ({
27
27
 
28
28
  const [zIndexTo, setZIndexTo] = useState(0);
29
29
 
30
- const onMinChange = (event: ChangeEvent<HTMLInputElement>) => {
30
+ const onMinChange = (event: JSX.TargetedInputEvent<HTMLInputElement>) => {
31
31
  const input = event.target as HTMLInputElement;
32
32
  const minValue = Number(input.value);
33
33
 
@@ -39,7 +39,7 @@ export const MinMaxRangeSlider: FunctionComponent<MinMaxPercentSliderProps> = ({
39
39
  }
40
40
  };
41
41
 
42
- const onMaxChange = (event: ChangeEvent<HTMLInputElement>) => {
42
+ const onMaxChange = (event: JSX.TargetedInputEvent<HTMLInputElement>) => {
43
43
  const input = event.target as HTMLInputElement;
44
44
  const maxValue = Number(input.value);
45
45
 
@@ -1,6 +1,5 @@
1
- import { type FunctionComponent } from 'preact';
1
+ import { type FunctionComponent, type JSX } from 'preact';
2
2
  import { useEffect, useState } from 'preact/hooks';
3
- import { type ChangeEvent } from 'react';
4
3
 
5
4
  export type PercentInputProps = {
6
5
  percentage: number;
@@ -18,7 +17,7 @@ export const PercentInput: FunctionComponent<PercentInputProps> = ({ percentage,
18
17
  setInternalPercentage(percentage);
19
18
  }, [percentage]);
20
19
 
21
- const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
20
+ const handleInputChange = (event: JSX.TargetedInputEvent<HTMLInputElement>) => {
22
21
  const input = event.target as HTMLInputElement;
23
22
  const value = Number(input.value);
24
23
 
@@ -0,0 +1,23 @@
1
+ import { type FunctionComponent } from 'preact';
2
+
3
+ export type Size = {
4
+ width?: string;
5
+ height?: string;
6
+ };
7
+
8
+ export interface ResizeContainerProps {
9
+ size?: Size;
10
+ defaultSize: Size;
11
+ }
12
+
13
+ export const ResizeContainer: FunctionComponent<ResizeContainerProps> = ({ children, size, defaultSize }) => {
14
+ return <div style={extendByDefault(size, defaultSize)}>{children}</div>;
15
+ };
16
+
17
+ const extendByDefault = (size: Size | undefined, defaultSize: Size) => {
18
+ if (size === undefined) {
19
+ return defaultSize;
20
+ }
21
+
22
+ return { ...defaultSize, ...size };
23
+ };
@@ -3,6 +3,7 @@ import { type OneDArray, type TColumn, type TData } from 'gridjs/dist/src/types'
3
3
  import { type PaginationConfig } from 'gridjs/dist/src/view/plugin/pagination';
4
4
  import { type ComponentChild } from 'preact';
5
5
  import { useEffect, useRef } from 'preact/hooks';
6
+
6
7
  import 'gridjs/dist/theme/mermaid.css';
7
8
 
8
9
  export const tableStyle = {
@@ -51,10 +51,10 @@ export const TabsWithToolbarOnlyShowingOnSecondTab: StoryObj = {
51
51
  play: async ({ canvasElement }) => {
52
52
  const canvas = within(canvasElement);
53
53
 
54
- await waitFor(() => expect(canvas.getByLabelText('FirstTab', { selector: 'input' })).toBeVisible());
54
+ await waitFor(() => expect(canvas.getByRole('button', { name: 'SecondTab' })).toBeVisible());
55
55
  await expect(canvas.queryByText('Toolbar')).not.toBeInTheDocument();
56
56
 
57
- await fireEvent.click(canvas.getByLabelText('SecondTab', { selector: 'input' }));
57
+ await fireEvent.click(canvas.getByRole('button', { name: 'SecondTab' }));
58
58
  await waitFor(() => expect(canvas.getByText('Toolbar')).toBeVisible());
59
59
  },
60
60
  };
@@ -1,5 +1,5 @@
1
1
  import { Fragment, type FunctionComponent } from 'preact';
2
- import { useState } from 'preact/hooks';
2
+ import { useEffect, useRef, useState } from 'preact/hooks';
3
3
  import { type JSXInternal } from 'preact/src/jsx';
4
4
 
5
5
  type Tab = {
@@ -14,34 +14,57 @@ interface ComponentTabsProps {
14
14
 
15
15
  const Tabs: FunctionComponent<ComponentTabsProps> = ({ tabs, toolbar }) => {
16
16
  const [activeTab, setActiveTab] = useState(tabs[0].title);
17
+ const [heightOfTabs, setHeightOfTabs] = useState('3rem');
18
+ const tabRef = useRef<HTMLDivElement>(null);
17
19
 
18
- const tabNames = tabs.map((tab) => tab.title).join(', ');
19
-
20
- const tabElements = tabs.map((tab) => {
21
- return (
22
- <Fragment key={tab.title}>
23
- <input
24
- type='radio'
25
- name={tabNames}
26
- role='tab'
27
- className='tab'
28
- aria-label={tab.title}
29
- checked={activeTab === tab.title}
30
- onChange={() => setActiveTab(tab.title)}
31
- />
32
- <div role='tabpanel' className='tab-content bg-base-100 border-base-300 rounded-box p-1'>
33
- {tab.content}
34
- </div>
35
- </Fragment>
36
- );
37
- });
20
+ useEffect(() => {
21
+ if (tabRef.current) {
22
+ const heightOfTabs = tabRef.current.getBoundingClientRect().height;
23
+ setHeightOfTabs(`${heightOfTabs}px`);
24
+ }
25
+ }, []);
26
+
27
+ const tabElements = (
28
+ <div className='flex flex-row'>
29
+ {tabs.map((tab) => {
30
+ return (
31
+ <Fragment key={tab.title}>
32
+ <button
33
+ className={`px-4 py-2 text-sm font-medium leading-5 transition-colors duration-150 ${
34
+ activeTab === tab.title
35
+ ? 'border-b-2 border-gray-400'
36
+ : 'text-gray-600 hover:bg-gray-100 hover:text-gray-700'
37
+ }`}
38
+ onClick={() => {
39
+ setActiveTab(tab.title);
40
+ }}
41
+ >
42
+ {tab.title}
43
+ </button>
44
+ </Fragment>
45
+ );
46
+ })}
47
+ </div>
48
+ );
38
49
 
39
50
  const toolbarElement = typeof toolbar === 'function' ? toolbar(activeTab) : toolbar;
40
51
 
41
52
  return (
42
- <div role='tablist' className='tabs tabs-lifted'>
43
- {tabElements}
44
- {toolbar && <div className='m-1 col-[9999]'>{toolbarElement}</div>}
53
+ <div className='h-full w-full'>
54
+ <div ref={tabRef} className='flex flex-row justify-between'>
55
+ {tabElements}
56
+ {toolbar && <div className='py-2'>{toolbarElement}</div>}
57
+ </div>
58
+ <div
59
+ className={`p-2 border-2 border-gray-100 rounded-b-md rounded-tr-md ${activeTab === tabs[0].title ? '' : 'rounded-tl-md'}`}
60
+ style={{ height: `calc(100% - ${heightOfTabs})` }}
61
+ >
62
+ {tabs.map((tab) => (
63
+ <div className='h-full overflow-auto' key={tab.title} hidden={activeTab !== tab.title}>
64
+ {tab.content}
65
+ </div>
66
+ ))}
67
+ </div>
45
68
  </div>
46
69
  );
47
70
  };
@@ -1,11 +1,21 @@
1
1
  import { withActions } from '@storybook/addon-actions/decorator';
2
2
  import { type Meta, type StoryObj } from '@storybook/preact';
3
3
 
4
- import { DateRangeSelector, type DateRangeSelectorProps } from './date-range-selector';
4
+ import {
5
+ DateRangeSelector,
6
+ type DateRangeSelectorProps,
7
+ PRESET_VALUE_ALL_TIMES,
8
+ PRESET_VALUE_CUSTOM,
9
+ PRESET_VALUE_LAST_2_MONTHS,
10
+ PRESET_VALUE_LAST_2_WEEKS,
11
+ PRESET_VALUE_LAST_3_MONTHS,
12
+ PRESET_VALUE_LAST_6_MONTHS,
13
+ PRESET_VALUE_LAST_MONTH,
14
+ } from './date-range-selector';
5
15
  import { LAPIS_URL } from '../../constants';
6
16
  import { LapisUrlContext } from '../LapisUrlContext';
7
17
 
8
- const meta: Meta<DateRangeSelectorProps> = {
18
+ const meta: Meta<DateRangeSelectorProps<'CustomDateRange'>> = {
9
19
  title: 'Input/DateRangeSelector',
10
20
  component: DateRangeSelector,
11
21
  parameters: {
@@ -14,19 +24,41 @@ const meta: Meta<DateRangeSelectorProps> = {
14
24
  },
15
25
  fetchMock: {},
16
26
  },
27
+ argTypes: {
28
+ initialValue: {
29
+ control: {
30
+ type: 'select',
31
+ },
32
+ options: [
33
+ PRESET_VALUE_CUSTOM,
34
+ PRESET_VALUE_ALL_TIMES,
35
+ PRESET_VALUE_LAST_2_WEEKS,
36
+ PRESET_VALUE_LAST_MONTH,
37
+ PRESET_VALUE_LAST_2_MONTHS,
38
+ PRESET_VALUE_LAST_3_MONTHS,
39
+ PRESET_VALUE_LAST_6_MONTHS,
40
+ 'CustomDateRange',
41
+ ],
42
+ },
43
+ },
17
44
  args: {
18
45
  customSelectOptions: [{ label: 'CustomDateRange', dateFrom: '2021-01-01', dateTo: '2021-12-31' }],
19
46
  earliestDate: '1970-01-01',
47
+ initialValue: PRESET_VALUE_LAST_3_MONTHS,
20
48
  },
21
49
  decorators: [withActions],
22
50
  };
23
51
 
24
52
  export default meta;
25
53
 
26
- export const Primary: StoryObj<DateRangeSelectorProps> = {
54
+ export const Primary: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
27
55
  render: (args) => (
28
56
  <LapisUrlContext.Provider value={LAPIS_URL}>
29
- <DateRangeSelector customSelectOptions={args.customSelectOptions} earliestDate={args.earliestDate} />
57
+ <DateRangeSelector
58
+ customSelectOptions={args.customSelectOptions}
59
+ earliestDate={args.earliestDate}
60
+ initialValue={args.initialValue}
61
+ />
30
62
  </LapisUrlContext.Provider>
31
63
  ),
32
64
  };