@genspectrum/dashboard-components 0.5.7 → 0.6.1

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 (43) hide show
  1. package/custom-elements.json +194 -128
  2. package/dist/dashboard-components.js +14 -93
  3. package/dist/dashboard-components.js.map +1 -1
  4. package/dist/genspectrum-components.d.ts +24 -28
  5. package/dist/style.css +0 -4
  6. package/package.json +1 -1
  7. package/src/preact/aggregatedData/aggregate.stories.tsx +0 -2
  8. package/src/preact/aggregatedData/aggregate.tsx +3 -12
  9. package/src/preact/components/error-boundary.stories.tsx +2 -6
  10. package/src/preact/components/error-boundary.tsx +2 -5
  11. package/src/preact/lineageFilter/__mockData__/aggregated.json +14510 -0
  12. package/src/preact/lineageFilter/fetchLineageAutocompleteList.spec.ts +14 -0
  13. package/src/preact/lineageFilter/fetchLineageAutocompleteList.ts +9 -0
  14. package/src/preact/lineageFilter/lineage-filter.stories.tsx +56 -0
  15. package/src/preact/lineageFilter/lineage-filter.tsx +100 -0
  16. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +0 -3
  17. package/src/preact/mutationComparison/mutation-comparison.tsx +3 -12
  18. package/src/preact/mutations/mutations.stories.tsx +0 -3
  19. package/src/preact/mutations/mutations.tsx +3 -12
  20. package/src/preact/numberSequencesOverTime/__mockData__/twoVariantsEG.json +0 -8
  21. package/src/preact/numberSequencesOverTime/number-sequences-over-time.stories.tsx +0 -2
  22. package/src/preact/numberSequencesOverTime/number-sequences-over-time.tsx +3 -7
  23. package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +0 -4
  24. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +3 -12
  25. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +0 -3
  26. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +2 -7
  27. package/src/preact/textInput/text-input.tsx +1 -5
  28. package/src/web-components/input/gs-lineage-filter.stories.ts +137 -0
  29. package/src/web-components/input/gs-lineage-filter.tsx +79 -0
  30. package/src/web-components/visualization/gs-aggregate.stories.ts +0 -4
  31. package/src/web-components/visualization/gs-aggregate.tsx +0 -7
  32. package/src/web-components/visualization/gs-mutation-comparison.stories.ts +0 -4
  33. package/src/web-components/visualization/gs-mutation-comparison.tsx +0 -7
  34. package/src/web-components/visualization/gs-mutations.stories.ts +0 -4
  35. package/src/web-components/visualization/gs-mutations.tsx +0 -7
  36. package/src/web-components/visualization/gs-number-sequences-over-time.stories.ts +1 -3
  37. package/src/web-components/visualization/gs-number-sequences-over-time.tsx +14 -11
  38. package/src/web-components/visualization/gs-prevalence-over-time.stories.ts +0 -5
  39. package/src/web-components/visualization/gs-prevalence-over-time.tsx +0 -7
  40. package/src/web-components/visualization/gs-relative-growth-advantage.stories.ts +0 -4
  41. package/src/web-components/visualization/gs-relative-growth-advantage.tsx +0 -7
  42. package/src/preact/components/headline.stories.tsx +0 -47
  43. package/src/preact/components/headline.tsx +0 -36
@@ -0,0 +1,14 @@
1
+ import { describe, expect, test } from 'vitest';
2
+
3
+ import { fetchLineageAutocompleteList } from './fetchLineageAutocompleteList';
4
+ import { DUMMY_LAPIS_URL, lapisRequestMocks } from '../../../vitest.setup';
5
+
6
+ describe('fetchLineageAutocompleteList', () => {
7
+ test('should add sublineage values', async () => {
8
+ lapisRequestMocks.aggregated({ fields: ['lineageField'] }, { data: [{ lineageField: 'B.1.1.7', count: 1 }] });
9
+
10
+ const result = await fetchLineageAutocompleteList(DUMMY_LAPIS_URL, 'lineageField');
11
+
12
+ expect(result).to.deep.equal(['B.1.1.7', 'B.1.1.7*']);
13
+ });
14
+ });
@@ -0,0 +1,9 @@
1
+ import { FetchAggregatedOperator } from '../../operator/FetchAggregatedOperator';
2
+
3
+ export async function fetchLineageAutocompleteList(lapis: string, field: string, signal?: AbortSignal) {
4
+ const fetchAggregatedOperator = new FetchAggregatedOperator<Record<string, string>>({}, [field]);
5
+
6
+ const data = (await fetchAggregatedOperator.evaluate(lapis, signal)).content;
7
+
8
+ return data.flatMap((item) => [item[field], `${item[field]}*`]).sort();
9
+ }
@@ -0,0 +1,56 @@
1
+ import { withActions } from '@storybook/addon-actions/decorator';
2
+ import { type Meta, type StoryObj } from '@storybook/preact';
3
+
4
+ import { LineageFilter, type LineageFilterProps } from './lineage-filter';
5
+ import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
6
+ import aggregatedData from '../../preact/lineageFilter/__mockData__/aggregated.json';
7
+ import { LapisUrlContext } from '../LapisUrlContext';
8
+
9
+ const meta: Meta = {
10
+ title: 'Input/LineageFilter',
11
+ component: LineageFilter,
12
+ parameters: {
13
+ actions: {
14
+ handles: ['gs-lineage-filter-changed'],
15
+ },
16
+ fetchMock: {
17
+ mocks: [
18
+ {
19
+ matcher: {
20
+ name: 'pangoLineage',
21
+ url: AGGREGATED_ENDPOINT,
22
+ body: {
23
+ fields: ['pangoLineage'],
24
+ },
25
+ },
26
+ response: {
27
+ status: 200,
28
+ body: aggregatedData,
29
+ },
30
+ },
31
+ ],
32
+ },
33
+ },
34
+ decorators: [withActions],
35
+ };
36
+
37
+ export default meta;
38
+
39
+ export const Default: StoryObj<LineageFilterProps> = {
40
+ render: (args) => (
41
+ <LapisUrlContext.Provider value={LAPIS_URL}>
42
+ <LineageFilter
43
+ lapisField={args.lapisField}
44
+ placeholderText={args.placeholderText}
45
+ initialValue={args.initialValue}
46
+ width={args.width}
47
+ />
48
+ </LapisUrlContext.Provider>
49
+ ),
50
+ args: {
51
+ lapisField: 'pangoLineage',
52
+ placeholderText: 'Enter lineage',
53
+ initialValue: '',
54
+ width: '100%',
55
+ },
56
+ };
@@ -0,0 +1,100 @@
1
+ import { type FunctionComponent } from 'preact';
2
+ import { useContext, useRef } from 'preact/hooks';
3
+
4
+ import { fetchLineageAutocompleteList } from './fetchLineageAutocompleteList';
5
+ import { LapisUrlContext } from '../LapisUrlContext';
6
+ import { ErrorBoundary } from '../components/error-boundary';
7
+ import { ErrorDisplay } from '../components/error-display';
8
+ import { LoadingDisplay } from '../components/loading-display';
9
+ import { NoDataDisplay } from '../components/no-data-display';
10
+ import { ResizeContainer } from '../components/resize-container';
11
+ import { useQuery } from '../useQuery';
12
+
13
+ export interface LineageFilterInnerProps {
14
+ lapisField: string;
15
+ placeholderText?: string;
16
+ initialValue?: string;
17
+ }
18
+
19
+ export interface LineageFilterProps extends LineageFilterInnerProps {
20
+ width: string;
21
+ }
22
+
23
+ export const LineageFilter: FunctionComponent<LineageFilterProps> = ({ width, ...innerProps }) => {
24
+ const size = { width, height: '3rem' };
25
+
26
+ return (
27
+ <ErrorBoundary size={size}>
28
+ <ResizeContainer size={size}>
29
+ <LineageFilterInner {...innerProps} />
30
+ </ResizeContainer>
31
+ </ErrorBoundary>
32
+ );
33
+ };
34
+
35
+ const LineageFilterInner: FunctionComponent<LineageFilterInnerProps> = ({
36
+ lapisField,
37
+ placeholderText,
38
+ initialValue,
39
+ }) => {
40
+ const lapis = useContext(LapisUrlContext);
41
+
42
+ const inputRef = useRef<HTMLInputElement>(null);
43
+
44
+ const { data, error, isLoading } = useQuery(
45
+ () => fetchLineageAutocompleteList(lapis, lapisField),
46
+ [lapisField, lapis],
47
+ );
48
+
49
+ if (isLoading) {
50
+ return <LoadingDisplay />;
51
+ }
52
+
53
+ if (error !== null) {
54
+ return <ErrorDisplay error={error} />;
55
+ }
56
+
57
+ if (data === null) {
58
+ return <NoDataDisplay />;
59
+ }
60
+
61
+ const onInput = () => {
62
+ const value = inputRef.current?.value === '' ? undefined : inputRef.current?.value;
63
+
64
+ if (isValidValue(value)) {
65
+ inputRef.current?.dispatchEvent(
66
+ new CustomEvent('gs-lineage-filter-changed', {
67
+ detail: { [lapisField]: value },
68
+ bubbles: true,
69
+ composed: true,
70
+ }),
71
+ );
72
+ }
73
+ };
74
+
75
+ const isValidValue = (value: string | undefined) => {
76
+ if (value === undefined) {
77
+ return true;
78
+ }
79
+ return data.includes(value);
80
+ };
81
+
82
+ return (
83
+ <>
84
+ <input
85
+ type='text'
86
+ class='input input-bordered w-full'
87
+ placeholder={placeholderText !== undefined ? placeholderText : lapisField}
88
+ onInput={onInput}
89
+ ref={inputRef}
90
+ list={lapisField}
91
+ value={initialValue}
92
+ />
93
+ <datalist id={lapisField}>
94
+ {data.map((item) => (
95
+ <option value={item} key={item} />
96
+ ))}
97
+ </datalist>
98
+ </>
99
+ );
100
+ };
@@ -29,7 +29,6 @@ const meta: Meta<MutationComparisonProps> = {
29
29
  },
30
30
  width: { control: 'text' },
31
31
  height: { control: 'text' },
32
- headline: { control: 'text' },
33
32
  pageSize: { control: 'object' },
34
33
  },
35
34
  parameters: {
@@ -85,7 +84,6 @@ const Template: StoryObj<MutationComparisonProps> = {
85
84
  views={args.views}
86
85
  width={args.width}
87
86
  height={args.height}
88
- headline={args.headline}
89
87
  pageSize={args.pageSize}
90
88
  />
91
89
  </ReferenceGenomeContext.Provider>
@@ -115,7 +113,6 @@ export const TwoVariants: StoryObj<MutationComparisonProps> = {
115
113
  views: ['table', 'venn'],
116
114
  width: '100%',
117
115
  height: '700px',
118
- headline: 'Mutation comparison',
119
116
  pageSize: 10,
120
117
  },
121
118
  };
@@ -11,7 +11,6 @@ import { type DisplayedSegment, SegmentSelector, useDisplayedSegments } from '..
11
11
  import { CsvDownloadButton } from '../components/csv-download-button';
12
12
  import { ErrorBoundary } from '../components/error-boundary';
13
13
  import { ErrorDisplay } from '../components/error-display';
14
- import Headline from '../components/headline';
15
14
  import Info from '../components/info';
16
15
  import { LoadingDisplay } from '../components/loading-display';
17
16
  import { type DisplayedMutationType, MutationTypeSelector } from '../components/mutation-type-selector';
@@ -27,7 +26,6 @@ export type View = 'table' | 'venn';
27
26
  export interface MutationComparisonProps extends MutationComparisonInnerProps {
28
27
  width: string;
29
28
  height: string;
30
- headline?: string;
31
29
  }
32
30
 
33
31
  export interface MutationComparisonInnerProps {
@@ -37,20 +35,13 @@ export interface MutationComparisonInnerProps {
37
35
  pageSize: boolean | number;
38
36
  }
39
37
 
40
- export const MutationComparison: FunctionComponent<MutationComparisonProps> = ({
41
- width,
42
- height,
43
- headline = 'Mutation comparison',
44
- ...innerProps
45
- }) => {
38
+ export const MutationComparison: FunctionComponent<MutationComparisonProps> = ({ width, height, ...innerProps }) => {
46
39
  const size = { height, width };
47
40
 
48
41
  return (
49
- <ErrorBoundary size={size} headline={headline}>
42
+ <ErrorBoundary size={size}>
50
43
  <ResizeContainer size={size}>
51
- <Headline heading={headline}>
52
- <MutationComparisonInner {...innerProps} />
53
- </Headline>
44
+ <MutationComparisonInner {...innerProps} />
54
45
  </ResizeContainer>
55
46
  </ErrorBoundary>
56
47
  );
@@ -24,7 +24,6 @@ const meta: Meta<MutationsProps> = {
24
24
  },
25
25
  width: { control: 'text' },
26
26
  height: { control: 'text' },
27
- headline: { control: 'text' },
28
27
  pageSize: { control: 'object' },
29
28
  },
30
29
  };
@@ -41,7 +40,6 @@ const Template = {
41
40
  views={args.views}
42
41
  width={args.width}
43
42
  height={args.height}
44
- headline={args.headline}
45
43
  pageSize={args.pageSize}
46
44
  />
47
45
  </ReferenceGenomeContext.Provider>
@@ -57,7 +55,6 @@ export const Default: StoryObj<MutationsProps> = {
57
55
  views: ['grid', 'table', 'insertions'],
58
56
  width: '100%',
59
57
  height: '700px',
60
- headline: 'Mutations',
61
58
  pageSize: 10,
62
59
  },
63
60
  parameters: {
@@ -18,7 +18,6 @@ import { type DisplayedSegment, SegmentSelector, useDisplayedSegments } from '..
18
18
  import { CsvDownloadButton } from '../components/csv-download-button';
19
19
  import { ErrorBoundary } from '../components/error-boundary';
20
20
  import { ErrorDisplay } from '../components/error-display';
21
- import Headline from '../components/headline';
22
21
  import Info from '../components/info';
23
22
  import { LoadingDisplay } from '../components/loading-display';
24
23
  import { type DisplayedMutationType, MutationTypeSelector } from '../components/mutation-type-selector';
@@ -41,23 +40,15 @@ export interface MutationsInnerProps {
41
40
  export interface MutationsProps extends MutationsInnerProps {
42
41
  width: string;
43
42
  height: string;
44
- headline?: string;
45
43
  }
46
44
 
47
- export const Mutations: FunctionComponent<MutationsProps> = ({
48
- width,
49
- height,
50
- headline = 'Mutations',
51
- ...innerProps
52
- }) => {
45
+ export const Mutations: FunctionComponent<MutationsProps> = ({ width, height, ...innerProps }) => {
53
46
  const size = { height, width };
54
47
 
55
48
  return (
56
- <ErrorBoundary size={size} headline={headline}>
49
+ <ErrorBoundary size={size}>
57
50
  <ResizeContainer size={size}>
58
- <Headline heading={headline}>
59
- <MutationsInner {...innerProps} />
60
- </Headline>
51
+ <MutationsInner {...innerProps} />
61
52
  </ResizeContainer>
62
53
  </ErrorBoundary>
63
54
  );
@@ -244,10 +244,6 @@
244
244
  "count": 416,
245
245
  "date": "2023-09-18"
246
246
  },
247
- {
248
- "count": 208,
249
- "date": null
250
- },
251
247
  {
252
248
  "count": 271,
253
249
  "date": "2023-09-15"
@@ -624,10 +620,6 @@
624
620
  "count": 313,
625
621
  "date": "2023-10-26"
626
622
  },
627
- {
628
- "count": 1,
629
- "date": "2022-01-23"
630
- },
631
623
  {
632
624
  "count": 91,
633
625
  "date": "2023-12-31"
@@ -35,7 +35,6 @@ const Template: StoryObj<NumberSequencesOverTimeProps> = {
35
35
  views={args.views}
36
36
  width={args.width}
37
37
  height={args.height}
38
- headline={args.headline}
39
38
  granularity={args.granularity}
40
39
  smoothingWindow={args.smoothingWindow}
41
40
  pageSize={args.pageSize}
@@ -50,7 +49,6 @@ const Template: StoryObj<NumberSequencesOverTimeProps> = {
50
49
  lapisDateField: 'date',
51
50
  width: '100%',
52
51
  height: '700px',
53
- headline: 'Number of sequences over time',
54
52
  smoothingWindow: 0,
55
53
  granularity: 'month',
56
54
  pageSize: 10,
@@ -13,7 +13,6 @@ import { LapisUrlContext } from '../LapisUrlContext';
13
13
  import { CsvDownloadButton } from '../components/csv-download-button';
14
14
  import { ErrorBoundary } from '../components/error-boundary';
15
15
  import { ErrorDisplay } from '../components/error-display';
16
- import Headline from '../components/headline';
17
16
  import Info, { InfoHeadline1, InfoParagraph } from '../components/info';
18
17
  import { LoadingDisplay } from '../components/loading-display';
19
18
  import { NoDataDisplay } from '../components/no-data-display';
@@ -28,7 +27,6 @@ type NumberSequencesOverTimeView = 'bar' | 'line' | 'table';
28
27
  export interface NumberSequencesOverTimeProps extends NumberSequencesOverTimeInnerProps {
29
28
  width: string;
30
29
  height: string;
31
- headline: string;
32
30
  }
33
31
 
34
32
  interface NumberSequencesOverTimeInnerProps {
@@ -40,15 +38,13 @@ interface NumberSequencesOverTimeInnerProps {
40
38
  pageSize: boolean | number;
41
39
  }
42
40
 
43
- export const NumberSequencesOverTime = ({ width, height, headline, ...innerProps }: NumberSequencesOverTimeProps) => {
41
+ export const NumberSequencesOverTime = ({ width, height, ...innerProps }: NumberSequencesOverTimeProps) => {
44
42
  const size = { height, width };
45
43
 
46
44
  return (
47
- <ErrorBoundary size={size} headline={headline}>
45
+ <ErrorBoundary size={size}>
48
46
  <ResizeContainer size={size}>
49
- <Headline heading={headline}>
50
- <NumberSequencesOverTimeInner {...innerProps} />
51
- </Headline>
47
+ <NumberSequencesOverTimeInner {...innerProps} />
52
48
  </ResizeContainer>
53
49
  </ErrorBoundary>
54
50
  );
@@ -31,7 +31,6 @@ export default {
31
31
  },
32
32
  width: { control: 'text' },
33
33
  height: { control: 'text' },
34
- headline: { control: 'text' },
35
34
  pageSize: { control: 'object' },
36
35
  yAxisMaxConfig: { control: 'object' },
37
36
  },
@@ -49,7 +48,6 @@ const Template = {
49
48
  confidenceIntervalMethods={args.confidenceIntervalMethods}
50
49
  width={args.width}
51
50
  height={args.height}
52
- headline={args.headline}
53
51
  lapisDateField={args.lapisDateField}
54
52
  pageSize={args.pageSize}
55
53
  yAxisMaxConfig={args.yAxisMaxConfig}
@@ -72,7 +70,6 @@ export const TwoVariants = {
72
70
  confidenceIntervalMethods: ['wilson'],
73
71
  width: '100%',
74
72
  height: '700px',
75
- headline: 'Prevalence over time',
76
73
  lapisDateField: 'date',
77
74
  pageSize: 10,
78
75
  yAxisMaxConfig: {
@@ -149,7 +146,6 @@ export const OneVariant = {
149
146
  confidenceIntervalMethods: ['wilson'],
150
147
  width: '100%',
151
148
  height: '700px',
152
- headline: 'Prevalence over time',
153
149
  lapisDateField: 'date',
154
150
  pageSize: 10,
155
151
  yAxisMaxConfig: {
@@ -13,7 +13,6 @@ import { ConfidenceIntervalSelector } from '../components/confidence-interval-se
13
13
  import { CsvDownloadButton } from '../components/csv-download-button';
14
14
  import { ErrorBoundary } from '../components/error-boundary';
15
15
  import { ErrorDisplay } from '../components/error-display';
16
- import Headline from '../components/headline';
17
16
  import Info, { InfoHeadline1, InfoParagraph } from '../components/info';
18
17
  import { LoadingDisplay } from '../components/loading-display';
19
18
  import { NoDataDisplay } from '../components/no-data-display';
@@ -30,7 +29,6 @@ export type View = 'bar' | 'line' | 'bubble' | 'table';
30
29
  export interface PrevalenceOverTimeProps extends PrevalenceOverTimeInnerProps {
31
30
  width: string;
32
31
  height: string;
33
- headline?: string;
34
32
  }
35
33
 
36
34
  export interface PrevalenceOverTimeInnerProps {
@@ -45,20 +43,13 @@ export interface PrevalenceOverTimeInnerProps {
45
43
  yAxisMaxConfig: YAxisMaxConfig;
46
44
  }
47
45
 
48
- export const PrevalenceOverTime: FunctionComponent<PrevalenceOverTimeProps> = ({
49
- width,
50
- height,
51
- headline = 'Prevalence over time',
52
- ...innerProps
53
- }) => {
46
+ export const PrevalenceOverTime: FunctionComponent<PrevalenceOverTimeProps> = ({ width, height, ...innerProps }) => {
54
47
  const size = { height, width };
55
48
 
56
49
  return (
57
- <ErrorBoundary size={size} headline={headline}>
50
+ <ErrorBoundary size={size}>
58
51
  <ResizeContainer size={size}>
59
- <Headline heading={headline}>
60
- <PrevalenceOverTimeInner {...innerProps} />
61
- </Headline>
52
+ <PrevalenceOverTimeInner {...innerProps} />
62
53
  </ResizeContainer>
63
54
  </ErrorBoundary>
64
55
  );
@@ -20,7 +20,6 @@ export default {
20
20
  },
21
21
  width: { control: 'text' },
22
22
  height: { control: 'text' },
23
- headline: { control: 'text' },
24
23
  yAxisMaxConfig: { control: 'object' },
25
24
  },
26
25
  };
@@ -35,7 +34,6 @@ export const Primary = {
35
34
  views={args.views}
36
35
  width={args.width}
37
36
  height={args.height}
38
- headline={args.headline}
39
37
  lapisDateField={args.lapisDateField}
40
38
  yAxisMaxConfig={args.yAxisMaxConfig}
41
39
  />
@@ -48,7 +46,6 @@ export const Primary = {
48
46
  views: ['line'],
49
47
  width: '100%',
50
48
  height: '700px',
51
- headline: 'Relative growth advantage',
52
49
  lapisDateField: 'date',
53
50
  yAxisMaxConfig: {
54
51
  linear: 1,
@@ -10,7 +10,6 @@ import { type LapisFilter } from '../../types';
10
10
  import { LapisUrlContext } from '../LapisUrlContext';
11
11
  import { ErrorBoundary } from '../components/error-boundary';
12
12
  import { ErrorDisplay } from '../components/error-display';
13
- import Headline from '../components/headline';
14
13
  import Info, { InfoHeadline1, InfoHeadline2, InfoLink, InfoParagraph } from '../components/info';
15
14
  import { LoadingDisplay } from '../components/loading-display';
16
15
  import { NoDataDisplay } from '../components/no-data-display';
@@ -26,7 +25,6 @@ export type View = 'line';
26
25
  export interface RelativeGrowthAdvantageProps extends RelativeGrowthAdvantagePropsInner {
27
26
  width: string;
28
27
  height: string;
29
- headline?: string;
30
28
  }
31
29
 
32
30
  export interface RelativeGrowthAdvantagePropsInner {
@@ -41,17 +39,14 @@ export interface RelativeGrowthAdvantagePropsInner {
41
39
  export const RelativeGrowthAdvantage: FunctionComponent<RelativeGrowthAdvantageProps> = ({
42
40
  width,
43
41
  height,
44
- headline = 'Relative growth advantage',
45
42
  ...innerProps
46
43
  }) => {
47
44
  const size = { height, width };
48
45
 
49
46
  return (
50
- <ErrorBoundary size={size} headline={headline}>
47
+ <ErrorBoundary size={size}>
51
48
  <ResizeContainer size={size}>
52
- <Headline heading={headline}>
53
- <RelativeGrowthAdvantageInner {...innerProps} />
54
- </Headline>
49
+ <RelativeGrowthAdvantageInner {...innerProps} />
55
50
  </ResizeContainer>
56
51
  </ErrorBoundary>
57
52
  );
@@ -32,11 +32,7 @@ export const TextInput: FunctionComponent<TextInputProps> = ({ width, ...innerPr
32
32
  );
33
33
  };
34
34
 
35
- export const TextInputInner: FunctionComponent<TextInputInnerProps> = ({
36
- lapisField,
37
- placeholderText,
38
- initialValue,
39
- }) => {
35
+ const TextInputInner: FunctionComponent<TextInputInnerProps> = ({ lapisField, placeholderText, initialValue }) => {
40
36
  const lapis = useContext(LapisUrlContext);
41
37
 
42
38
  const inputRef = useRef<HTMLInputElement>(null);
@@ -0,0 +1,137 @@
1
+ import { withActions } from '@storybook/addon-actions/decorator';
2
+ import { expect, fn, userEvent, waitFor } from '@storybook/test';
3
+ import type { Meta, StoryObj } from '@storybook/web-components';
4
+ import { html } from 'lit';
5
+
6
+ import { withComponentDocs } from '../../../.storybook/ComponentDocsBlock';
7
+ import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
8
+ import '../app';
9
+ import './gs-lineage-filter';
10
+ import aggregatedData from '../../preact/lineageFilter/__mockData__/aggregated.json';
11
+ import { type LineageFilterProps } from '../../preact/lineageFilter/lineage-filter';
12
+ import { withinShadowRoot } from '../withinShadowRoot.story';
13
+
14
+ const codeExample = String.raw`
15
+ <gs-lineage-filter
16
+ lapisField="pangoLineage"
17
+ placeholderText="Enter lineage"
18
+ initialValue="B.1.1.7"
19
+ width="50%">
20
+ </gs-lineage-filter>`;
21
+
22
+ const meta: Meta<Required<LineageFilterProps>> = {
23
+ title: 'Input/Lineage filter',
24
+ component: 'gs-lineage-filter',
25
+ parameters: withComponentDocs({
26
+ actions: {
27
+ handles: ['gs-lineage-filter-changed'],
28
+ },
29
+ fetchMock: {
30
+ mocks: [
31
+ {
32
+ matcher: {
33
+ name: 'pangoLineage',
34
+ url: AGGREGATED_ENDPOINT,
35
+ body: {
36
+ fields: ['pangoLineage'],
37
+ },
38
+ },
39
+ response: {
40
+ status: 200,
41
+ body: aggregatedData,
42
+ },
43
+ },
44
+ ],
45
+ },
46
+ componentDocs: {
47
+ opensShadowDom: true,
48
+ expectsChildren: false,
49
+ codeExample,
50
+ },
51
+ }),
52
+ decorators: [withActions],
53
+ tags: ['autodocs'],
54
+ };
55
+
56
+ export default meta;
57
+
58
+ export const Default: StoryObj<Required<LineageFilterProps>> = {
59
+ render: (args) => {
60
+ return html` <gs-app lapis="${LAPIS_URL}">
61
+ <div class="max-w-screen-lg">
62
+ <gs-lineage-filter
63
+ .lapisField=${args.lapisField}
64
+ .placeholderText=${args.placeholderText}
65
+ .initialValue=${args.initialValue}
66
+ .width=${args.width}
67
+ ></gs-lineage-filter>
68
+ </div>
69
+ </gs-app>`;
70
+ },
71
+ args: {
72
+ lapisField: 'pangoLineage',
73
+ placeholderText: 'Enter lineage',
74
+ initialValue: 'B.1.1.7',
75
+ width: '100%',
76
+ },
77
+ };
78
+
79
+ export const FiresEvent: StoryObj<Required<LineageFilterProps>> = {
80
+ ...Default,
81
+ play: async ({ canvasElement, step }) => {
82
+ const canvas = await withinShadowRoot(canvasElement, 'gs-lineage-filter');
83
+
84
+ const inputField = () => canvas.getByPlaceholderText('Enter lineage');
85
+ const listenerMock = fn();
86
+ await step('Setup event listener mock', async () => {
87
+ canvasElement.addEventListener('gs-lineage-filter-changed', listenerMock);
88
+ });
89
+
90
+ await step('wait until data is loaded', async () => {
91
+ await waitFor(() => {
92
+ return expect(inputField()).toBeEnabled();
93
+ });
94
+ });
95
+
96
+ await step('Enters an invalid lineage value', async () => {
97
+ await userEvent.type(inputField(), 'notInList');
98
+ await expect(listenerMock).not.toHaveBeenCalled();
99
+ });
100
+
101
+ await step('Empty input', async () => {
102
+ await userEvent.type(inputField(), '{backspace>9/}');
103
+ await expect(listenerMock.mock.calls.at(-1)[0].detail).toStrictEqual({
104
+ pangoLineage: undefined,
105
+ });
106
+ });
107
+
108
+ await step('Enter a valid lineage value', async () => {
109
+ await userEvent.type(inputField(), 'B.1.1.7');
110
+
111
+ await expect(listenerMock).toHaveBeenCalledWith(
112
+ expect.objectContaining({
113
+ detail: {
114
+ pangoLineage: 'B.1.1.7',
115
+ },
116
+ }),
117
+ );
118
+ });
119
+
120
+ await step('Enter a valid lineage value', async () => {
121
+ await userEvent.type(inputField(), '{backspace>9/}');
122
+ await userEvent.type(inputField(), 'B.1.1.7*');
123
+
124
+ await expect(listenerMock).toHaveBeenCalledWith(
125
+ expect.objectContaining({
126
+ detail: {
127
+ pangoLineage: 'B.1.1.7*',
128
+ },
129
+ }),
130
+ );
131
+ });
132
+ },
133
+ args: {
134
+ ...Default.args,
135
+ initialValue: '',
136
+ },
137
+ };