@genspectrum/dashboard-components 0.12.1 → 0.13.0

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 (36) hide show
  1. package/custom-elements.json +114 -25
  2. package/dist/{LocationChangedEvent-CORvQvXv.js → LineageFilterChangedEvent-GedKNGFI.js} +25 -3
  3. package/dist/LineageFilterChangedEvent-GedKNGFI.js.map +1 -0
  4. package/dist/components.d.ts +55 -21
  5. package/dist/components.js +229 -286
  6. package/dist/components.js.map +1 -1
  7. package/dist/util.d.ts +28 -14
  8. package/dist/util.js +3 -1
  9. package/package.json +1 -1
  10. package/src/preact/components/downshift-combobox.tsx +145 -0
  11. package/src/preact/lineageFilter/LineageFilterChangedEvent.ts +11 -0
  12. package/src/preact/lineageFilter/fetchLineageAutocompleteList.spec.ts +16 -2
  13. package/src/preact/lineageFilter/fetchLineageAutocompleteList.ts +13 -2
  14. package/src/preact/lineageFilter/lineage-filter.stories.tsx +110 -9
  15. package/src/preact/lineageFilter/lineage-filter.tsx +40 -50
  16. package/src/preact/locationFilter/LocationChangedEvent.ts +1 -1
  17. package/src/preact/locationFilter/fetchAutocompletionList.spec.ts +6 -2
  18. package/src/preact/locationFilter/fetchAutocompletionList.ts +16 -6
  19. package/src/preact/locationFilter/location-filter.stories.tsx +33 -30
  20. package/src/preact/locationFilter/location-filter.tsx +47 -144
  21. package/src/preact/textInput/TextInputChangedEvent.ts +1 -1
  22. package/src/preact/textInput/fetchStringAutocompleteList.ts +20 -0
  23. package/src/preact/textInput/text-input.stories.tsx +14 -11
  24. package/src/preact/textInput/text-input.tsx +39 -140
  25. package/src/types.ts +3 -0
  26. package/src/utilEntrypoint.ts +2 -0
  27. package/src/web-components/input/gs-lineage-filter.stories.ts +120 -31
  28. package/src/web-components/input/gs-lineage-filter.tsx +24 -8
  29. package/src/web-components/input/gs-location-filter.stories.ts +9 -0
  30. package/src/web-components/input/gs-location-filter.tsx +21 -3
  31. package/src/web-components/input/gs-text-input.stories.ts +14 -5
  32. package/src/web-components/input/gs-text-input.tsx +23 -7
  33. package/standalone-bundle/dashboard-components.js +5574 -5605
  34. package/standalone-bundle/dashboard-components.js.map +1 -1
  35. package/dist/LocationChangedEvent-CORvQvXv.js.map +0 -1
  36. package/src/preact/textInput/fetchAutocompleteList.ts +0 -9
package/dist/util.d.ts CHANGED
@@ -160,7 +160,17 @@ declare const lapisFilterSchema: default_2.ZodIntersection<default_2.ZodRecord<d
160
160
  aminoAcidInsertions?: string[] | undefined;
161
161
  }>>;
162
162
 
163
- declare type LapisLocationFilter = Record<string, string | null | undefined>;
163
+ declare type LapisLineageFilter = Record<string, string | undefined>;
164
+
165
+ declare type LapisLocationFilter = default_2.infer<typeof lapisLocationFilterSchema>;
166
+
167
+ declare const lapisLocationFilterSchema: default_2.ZodRecord<default_2.ZodString, default_2.ZodUnion<[default_2.ZodString, default_2.ZodUndefined]>>;
168
+
169
+ declare type LapisTextFilter = Record<string, string | undefined>;
170
+
171
+ export declare class LineageFilterChangedEvent extends CustomEvent<LapisLineageFilter> {
172
+ constructor(detail: LapisLineageFilter);
173
+ }
164
174
 
165
175
  export declare class LocationChangedEvent extends CustomEvent<LapisLocationFilter> {
166
176
  constructor(detail: LapisLocationFilter);
@@ -772,6 +782,10 @@ export declare type TemporalGranularity = default_2.infer<typeof temporalGranula
772
782
 
773
783
  declare const temporalGranularitySchema: default_2.ZodUnion<[default_2.ZodLiteral<"day">, default_2.ZodLiteral<"week">, default_2.ZodLiteral<"month">, default_2.ZodLiteral<"year">]>;
774
784
 
785
+ export declare class TextInputChangedEvent extends CustomEvent<LapisTextFilter> {
786
+ constructor(detail: LapisTextFilter);
787
+ }
788
+
775
789
  export declare const views: {
776
790
  readonly table: "table";
777
791
  readonly venn: "venn";
@@ -888,7 +902,7 @@ declare global {
888
902
 
889
903
  declare global {
890
904
  interface HTMLElementTagNameMap {
891
- 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
905
+ 'gs-aggregate': AggregateComponent;
892
906
  }
893
907
  }
894
908
 
@@ -896,7 +910,7 @@ declare global {
896
910
  declare global {
897
911
  namespace JSX {
898
912
  interface IntrinsicElements {
899
- 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
913
+ 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
900
914
  }
901
915
  }
902
916
  }
@@ -904,7 +918,7 @@ declare global {
904
918
 
905
919
  declare global {
906
920
  interface HTMLElementTagNameMap {
907
- 'gs-aggregate': AggregateComponent;
921
+ 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
908
922
  }
909
923
  }
910
924
 
@@ -912,7 +926,7 @@ declare global {
912
926
  declare global {
913
927
  namespace JSX {
914
928
  interface IntrinsicElements {
915
- 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
929
+ 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
916
930
  }
917
931
  }
918
932
  }
@@ -1007,10 +1021,10 @@ declare global {
1007
1021
 
1008
1022
  declare global {
1009
1023
  interface HTMLElementTagNameMap {
1010
- 'gs-mutation-filter': MutationFilterComponent;
1024
+ 'gs-text-input': TextInputComponent;
1011
1025
  }
1012
1026
  interface HTMLElementEventMap {
1013
- 'gs-mutation-filter-changed': CustomEvent<MutationsFilter>;
1027
+ 'gs-text-input-changed': TextInputChangedEvent;
1014
1028
  }
1015
1029
  }
1016
1030
 
@@ -1018,7 +1032,7 @@ declare global {
1018
1032
  declare global {
1019
1033
  namespace JSX {
1020
1034
  interface IntrinsicElements {
1021
- 'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1035
+ 'gs-text-input': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1022
1036
  }
1023
1037
  }
1024
1038
  }
@@ -1026,10 +1040,10 @@ declare global {
1026
1040
 
1027
1041
  declare global {
1028
1042
  interface HTMLElementTagNameMap {
1029
- 'gs-lineage-filter': LineageFilterComponent;
1043
+ 'gs-mutation-filter': MutationFilterComponent;
1030
1044
  }
1031
1045
  interface HTMLElementEventMap {
1032
- 'gs-lineage-filter-changed': CustomEvent<Record<string, string>>;
1046
+ 'gs-mutation-filter-changed': CustomEvent<MutationsFilter>;
1033
1047
  }
1034
1048
  }
1035
1049
 
@@ -1037,7 +1051,7 @@ declare global {
1037
1051
  declare global {
1038
1052
  namespace JSX {
1039
1053
  interface IntrinsicElements {
1040
- 'gs-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1054
+ 'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1041
1055
  }
1042
1056
  }
1043
1057
  }
@@ -1045,10 +1059,10 @@ declare global {
1045
1059
 
1046
1060
  declare global {
1047
1061
  interface HTMLElementTagNameMap {
1048
- 'gs-text-input': TextInputComponent;
1062
+ 'gs-lineage-filter': LineageFilterComponent;
1049
1063
  }
1050
1064
  interface HTMLElementEventMap {
1051
- 'gs-text-input-changed': CustomEvent<Record<string, string>>;
1065
+ 'gs-lineage-filter-changed': LineageFilterChangedEvent;
1052
1066
  }
1053
1067
  }
1054
1068
 
@@ -1056,7 +1070,7 @@ declare global {
1056
1070
  declare global {
1057
1071
  namespace JSX {
1058
1072
  interface IntrinsicElements {
1059
- 'gs-text-input': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1073
+ 'gs-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1060
1074
  }
1061
1075
  }
1062
1076
  }
package/dist/util.js CHANGED
@@ -1,7 +1,9 @@
1
- import { D, L, d, v } from "./LocationChangedEvent-CORvQvXv.js";
1
+ import { D, a, L, T, d, v } from "./LineageFilterChangedEvent-GedKNGFI.js";
2
2
  export {
3
3
  D as DateRangeOptionChangedEvent,
4
+ a as LineageFilterChangedEvent,
4
5
  L as LocationChangedEvent,
6
+ T as TextInputChangedEvent,
5
7
  d as dateRangeOptionPresets,
6
8
  v as views
7
9
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -0,0 +1,145 @@
1
+ import { useCombobox } from 'downshift/preact';
2
+ import { type ComponentChild } from 'preact';
3
+ import { useRef, useState } from 'preact/hooks';
4
+
5
+ export function DownshiftCombobox<Item>({
6
+ allItems,
7
+ value,
8
+ filterItemsByInputValue,
9
+ createEvent,
10
+ itemToString,
11
+ placeholderText,
12
+ formatItemInList,
13
+ }: {
14
+ allItems: Item[];
15
+ value?: Item;
16
+ filterItemsByInputValue: (item: Item, value: string) => boolean;
17
+ createEvent: (item: Item | null) => CustomEvent;
18
+ itemToString: (item: Item | undefined | null) => string;
19
+ placeholderText?: string;
20
+ formatItemInList: (item: Item) => ComponentChild;
21
+ }) {
22
+ const initialSelectedItem = value ?? null;
23
+
24
+ const [items, setItems] = useState(
25
+ allItems.filter((item) => filterItemsByInputValue(item, itemToString(initialSelectedItem))),
26
+ );
27
+ const divRef = useRef<HTMLDivElement>(null);
28
+
29
+ const shadowRoot = divRef.current?.shadowRoot ?? undefined;
30
+
31
+ const environment =
32
+ shadowRoot !== undefined
33
+ ? {
34
+ addEventListener: window.addEventListener.bind(window),
35
+ removeEventListener: window.removeEventListener.bind(window),
36
+ document: shadowRoot.ownerDocument,
37
+ Node: window.Node,
38
+ }
39
+ : undefined;
40
+
41
+ const {
42
+ isOpen,
43
+ getToggleButtonProps,
44
+ getMenuProps,
45
+ getInputProps,
46
+ highlightedIndex,
47
+ getItemProps,
48
+ selectedItem,
49
+ inputValue,
50
+ selectItem,
51
+ setInputValue,
52
+ closeMenu,
53
+ } = useCombobox({
54
+ onInputValueChange({ inputValue }) {
55
+ setItems(allItems.filter((item) => filterItemsByInputValue(item, inputValue)));
56
+ },
57
+ onSelectedItemChange({ selectedItem }) {
58
+ if (selectedItem !== null) {
59
+ divRef.current?.dispatchEvent(createEvent(selectedItem));
60
+ }
61
+ },
62
+ items,
63
+ itemToString(item) {
64
+ return itemToString(item);
65
+ },
66
+ initialSelectedItem,
67
+ environment,
68
+ });
69
+
70
+ const onInputBlur = () => {
71
+ if (inputValue === '') {
72
+ divRef.current?.dispatchEvent(createEvent(null));
73
+ selectItem(null);
74
+ } else if (inputValue !== itemToString(selectedItem)) {
75
+ setInputValue(itemToString(selectedItem) || '');
76
+ }
77
+ };
78
+
79
+ const clearInput = () => {
80
+ divRef.current?.dispatchEvent(createEvent(null));
81
+ selectItem(null);
82
+ };
83
+
84
+ const buttonRef = useRef(null);
85
+
86
+ return (
87
+ <div ref={divRef} className={'relative w-full'}>
88
+ <div className='w-full flex flex-col gap-1'>
89
+ <div
90
+ className='flex gap-0.5 input input-bordered min-w-32'
91
+ onBlur={(event) => {
92
+ if (event.relatedTarget != buttonRef.current) {
93
+ closeMenu();
94
+ }
95
+ }}
96
+ >
97
+ <input
98
+ placeholder={placeholderText}
99
+ className='w-full p-1.5'
100
+ {...getInputProps()}
101
+ onBlur={onInputBlur}
102
+ />
103
+ <button
104
+ aria-label='clear selection'
105
+ className={`px-2 ${inputValue === '' && 'hidden'}`}
106
+ type='button'
107
+ onClick={clearInput}
108
+ tabIndex={-1}
109
+ >
110
+ ×
111
+ </button>
112
+ <button
113
+ aria-label='toggle menu'
114
+ className='px-2'
115
+ type='button'
116
+ {...getToggleButtonProps()}
117
+ ref={buttonRef}
118
+ >
119
+ {isOpen ? <>↑</> : <>↓</>}
120
+ </button>
121
+ </div>
122
+ </div>
123
+ {isOpen && (
124
+ <ul
125
+ className='absolute bg-white mt-1 shadow-md max-h-80 overflow-scroll z-10 w-full min-w-32'
126
+ {...getMenuProps()}
127
+ >
128
+ {items.length > 0 ? (
129
+ items.map((item, index) => (
130
+ <li
131
+ className={`${highlightedIndex === index ? 'bg-blue-300' : ''} ${selectedItem !== null && itemToString(selectedItem) === itemToString(item) ? 'font-bold' : ''} py-2 px-3 shadow-sm flex flex-col`}
132
+ key={itemToString(item)}
133
+ {...getItemProps({ item, index })}
134
+ >
135
+ {formatItemInList(item)}
136
+ </li>
137
+ ))
138
+ ) : (
139
+ <li className='py-2 px-3 shadow-sm flex flex-col'>No elements to select.</li>
140
+ )}
141
+ </ul>
142
+ )}
143
+ </div>
144
+ );
145
+ }
@@ -0,0 +1,11 @@
1
+ type LapisLineageFilter = Record<string, string | undefined>;
2
+
3
+ export class LineageFilterChangedEvent extends CustomEvent<LapisLineageFilter> {
4
+ constructor(detail: LapisLineageFilter) {
5
+ super('gs-lineage-filter-changed', {
6
+ detail,
7
+ bubbles: true,
8
+ composed: true,
9
+ });
10
+ }
11
+ }
@@ -5,9 +5,23 @@ import { DUMMY_LAPIS_URL, lapisRequestMocks } from '../../../vitest.setup';
5
5
 
6
6
  describe('fetchLineageAutocompleteList', () => {
7
7
  test('should add sublineage values', async () => {
8
- lapisRequestMocks.aggregated({ fields: ['lineageField'] }, { data: [{ lineageField: 'B.1.1.7', count: 1 }] });
8
+ lapisRequestMocks.aggregated(
9
+ { fields: ['lineageField'], country: 'Germany' },
10
+ {
11
+ data: [
12
+ {
13
+ lineageField: 'B.1.1.7',
14
+ count: 1,
15
+ },
16
+ ],
17
+ },
18
+ );
9
19
 
10
- const result = await fetchLineageAutocompleteList(DUMMY_LAPIS_URL, 'lineageField');
20
+ const result = await fetchLineageAutocompleteList({
21
+ lapis: DUMMY_LAPIS_URL,
22
+ field: 'lineageField',
23
+ lapisFilter: { country: 'Germany' },
24
+ });
11
25
 
12
26
  expect(result).to.deep.equal(['B.1.1.7', 'B.1.1.7*']);
13
27
  });
@@ -1,7 +1,18 @@
1
1
  import { FetchAggregatedOperator } from '../../operator/FetchAggregatedOperator';
2
+ import type { LapisFilter } from '../../types';
2
3
 
3
- export async function fetchLineageAutocompleteList(lapis: string, field: string, signal?: AbortSignal) {
4
- const fetchAggregatedOperator = new FetchAggregatedOperator<Record<string, string>>({}, [field]);
4
+ export async function fetchLineageAutocompleteList({
5
+ lapis,
6
+ field,
7
+ signal,
8
+ lapisFilter,
9
+ }: {
10
+ lapis: string;
11
+ field: string;
12
+ lapisFilter?: LapisFilter;
13
+ signal?: AbortSignal;
14
+ }) {
15
+ const fetchAggregatedOperator = new FetchAggregatedOperator<Record<string, string>>(lapisFilter ?? {}, [field]);
5
16
 
6
17
  const data = (await fetchAggregatedOperator.evaluate(lapis, signal)).content;
7
18
 
@@ -1,4 +1,6 @@
1
- import { type Meta, type StoryObj } from '@storybook/preact';
1
+ import { type Meta, type PreactRenderer, type StoryObj } from '@storybook/preact';
2
+ import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
3
+ import type { StepFunction } from '@storybook/types';
2
4
 
3
5
  import { LineageFilter, type LineageFilterProps } from './lineage-filter';
4
6
  import { previewHandles } from '../../../.storybook/preview';
@@ -22,6 +24,7 @@ const meta: Meta = {
22
24
  url: AGGREGATED_ENDPOINT,
23
25
  body: {
24
26
  fields: ['pangoLineage'],
27
+ country: 'Germany',
25
28
  },
26
29
  },
27
30
  response: {
@@ -32,10 +35,41 @@ const meta: Meta = {
32
35
  ],
33
36
  },
34
37
  },
38
+ argTypes: {
39
+ lapisField: {
40
+ control: {
41
+ type: 'text',
42
+ },
43
+ },
44
+ placeholderText: {
45
+ control: {
46
+ type: 'text',
47
+ },
48
+ },
49
+ value: {
50
+ control: {
51
+ type: 'text',
52
+ },
53
+ },
54
+ width: {
55
+ control: {
56
+ type: 'text',
57
+ },
58
+ },
59
+ lapisFilter: {
60
+ control: {
61
+ type: 'object',
62
+ },
63
+ },
64
+ },
65
+
35
66
  args: {
36
67
  lapisField: 'pangoLineage',
37
- placeholderText: 'Enter lineage',
38
- initialValue: '',
68
+ lapisFilter: {
69
+ country: 'Germany',
70
+ },
71
+ placeholderText: 'Enter a lineage',
72
+ value: 'A.1',
39
73
  width: '100%',
40
74
  },
41
75
  };
@@ -45,14 +79,62 @@ export default meta;
45
79
  export const Default: StoryObj<LineageFilterProps> = {
46
80
  render: (args) => (
47
81
  <LapisUrlContext.Provider value={LAPIS_URL}>
48
- <LineageFilter
49
- lapisField={args.lapisField}
50
- placeholderText={args.placeholderText}
51
- initialValue={args.initialValue}
52
- width={args.width}
53
- />
82
+ <LineageFilter {...args} />
54
83
  </LapisUrlContext.Provider>
55
84
  ),
85
+ play: async ({ canvasElement, step }) => {
86
+ const { canvas, lineageChangedListenerMock } = await prepare(canvasElement, step);
87
+
88
+ step('change lineage filter value fires event', async () => {
89
+ const input = await inputField(canvas);
90
+ await userEvent.clear(input);
91
+ await userEvent.type(input, 'B.1');
92
+ await userEvent.click(canvas.getByRole('option', { name: 'B.1' }));
93
+
94
+ await waitFor(() => {
95
+ return expect(lineageChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
96
+ pangoLineage: 'B.1',
97
+ });
98
+ });
99
+ });
100
+ },
101
+ };
102
+
103
+ export const ClearSelection: StoryObj<LineageFilterProps> = {
104
+ ...Default,
105
+ play: async ({ canvasElement, step }) => {
106
+ const { canvas, lineageChangedListenerMock } = await prepare(canvasElement, step);
107
+
108
+ step('clear selection fires event with empty filter', async () => {
109
+ const clearSelectionButton = await canvas.findByLabelText('clear selection');
110
+ await userEvent.click(clearSelectionButton);
111
+
112
+ await waitFor(() => {
113
+ return expect(lineageChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
114
+ pangoLineage: undefined,
115
+ });
116
+ });
117
+ });
118
+ },
119
+ };
120
+
121
+ export const OnBlurInput: StoryObj<LineageFilterProps> = {
122
+ ...Default,
123
+ play: async ({ canvasElement, step }) => {
124
+ const { canvas, lineageChangedListenerMock } = await prepare(canvasElement, step);
125
+
126
+ step('after cleared selection by hand and then blur fires event with empty filter', async () => {
127
+ const input = await inputField(canvas);
128
+ await userEvent.clear(input);
129
+ await userEvent.click(canvas.getByLabelText('toggle menu'));
130
+
131
+ await waitFor(() => {
132
+ return expect(lineageChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
133
+ pangoLineage: undefined,
134
+ });
135
+ });
136
+ });
137
+ },
56
138
  };
57
139
 
58
140
  export const WithNoLapisField: StoryObj<LineageFilterProps> = {
@@ -67,3 +149,22 @@ export const WithNoLapisField: StoryObj<LineageFilterProps> = {
67
149
  });
68
150
  },
69
151
  };
152
+
153
+ async function prepare(canvasElement: HTMLElement, step: StepFunction<PreactRenderer, unknown>) {
154
+ const canvas = within(canvasElement);
155
+
156
+ const lineageChangedListenerMock = fn();
157
+ step('Setup event listener mock', () => {
158
+ canvasElement.addEventListener('gs-lineage-filter-changed', lineageChangedListenerMock);
159
+ });
160
+
161
+ step('location filter is rendered with value', async () => {
162
+ await waitFor(async () => {
163
+ return expect(await inputField(canvas)).toHaveValue('A.1');
164
+ });
165
+ });
166
+
167
+ return { canvas, lineageChangedListenerMock };
168
+ }
169
+
170
+ const inputField = (canvas: ReturnType<typeof within>) => canvas.findByPlaceholderText('Enter a lineage');
@@ -1,27 +1,32 @@
1
1
  import { type FunctionComponent } from 'preact';
2
- import { useContext, useRef } from 'preact/hooks';
2
+ import { useContext } from 'preact/hooks';
3
3
  import z from 'zod';
4
4
 
5
5
  import { fetchLineageAutocompleteList } from './fetchLineageAutocompleteList';
6
6
  import { LapisUrlContext } from '../LapisUrlContext';
7
+ import { LineageFilterChangedEvent } from './LineageFilterChangedEvent';
8
+ import { lapisFilterSchema } from '../../types';
9
+ import { DownshiftCombobox } from '../components/downshift-combobox';
7
10
  import { ErrorBoundary } from '../components/error-boundary';
8
11
  import { LoadingDisplay } from '../components/loading-display';
9
- import { NoDataDisplay } from '../components/no-data-display';
10
12
  import { ResizeContainer } from '../components/resize-container';
11
13
  import { useQuery } from '../useQuery';
12
14
 
13
- const lineageFilterInnerPropsSchema = z.object({
15
+ const lineageSelectorPropsSchema = z.object({
14
16
  lapisField: z.string().min(1),
15
17
  placeholderText: z.string().optional(),
16
- initialValue: z.string(),
18
+ value: z.string(),
19
+ });
20
+ const lineageFilterInnerPropsSchema = lineageSelectorPropsSchema.extend({
21
+ lapisFilter: lapisFilterSchema,
17
22
  });
18
-
19
23
  const lineageFilterPropsSchema = lineageFilterInnerPropsSchema.extend({
20
24
  width: z.string(),
21
25
  });
22
26
 
23
27
  export type LineageFilterInnerProps = z.infer<typeof lineageFilterInnerPropsSchema>;
24
28
  export type LineageFilterProps = z.infer<typeof lineageFilterPropsSchema>;
29
+ type LineageSelectorProps = z.infer<typeof lineageSelectorPropsSchema>;
25
30
 
26
31
  export const LineageFilter: FunctionComponent<LineageFilterProps> = (props) => {
27
32
  const { width, ...innerProps } = props;
@@ -39,15 +44,14 @@ export const LineageFilter: FunctionComponent<LineageFilterProps> = (props) => {
39
44
  const LineageFilterInner: FunctionComponent<LineageFilterInnerProps> = ({
40
45
  lapisField,
41
46
  placeholderText,
42
- initialValue,
47
+ value,
48
+ lapisFilter,
43
49
  }) => {
44
50
  const lapis = useContext(LapisUrlContext);
45
51
 
46
- const inputRef = useRef<HTMLInputElement>(null);
47
-
48
52
  const { data, error, isLoading } = useQuery(
49
- () => fetchLineageAutocompleteList(lapis, lapisField),
50
- [lapisField, lapis],
53
+ () => fetchLineageAutocompleteList({ lapis, field: lapisField, lapisFilter }),
54
+ [lapisField, lapis, lapisFilter],
51
55
  );
52
56
 
53
57
  if (isLoading) {
@@ -58,47 +62,33 @@ const LineageFilterInner: FunctionComponent<LineageFilterInnerProps> = ({
58
62
  throw error;
59
63
  }
60
64
 
61
- if (data === null) {
62
- return <NoDataDisplay />;
63
- }
64
-
65
- const onInput = () => {
66
- const value = inputRef.current?.value === '' ? undefined : inputRef.current?.value;
67
-
68
- if (isValidValue(value)) {
69
- inputRef.current?.dispatchEvent(
70
- new CustomEvent('gs-lineage-filter-changed', {
71
- detail: { [lapisField]: value },
72
- bubbles: true,
73
- composed: true,
74
- }),
75
- );
76
- }
77
- };
78
-
79
- const isValidValue = (value: string | undefined) => {
80
- if (value === undefined) {
81
- return true;
82
- }
83
- return data.includes(value);
84
- };
65
+ return <LineageSelector lapisField={lapisField} value={value} placeholderText={placeholderText} data={data} />;
66
+ };
85
67
 
68
+ const LineageSelector = ({
69
+ lapisField,
70
+ value,
71
+ placeholderText,
72
+ data,
73
+ }: LineageSelectorProps & {
74
+ data: string[];
75
+ }) => {
86
76
  return (
87
- <>
88
- <input
89
- type='text'
90
- class='input input-bordered w-full'
91
- placeholder={placeholderText !== undefined ? placeholderText : lapisField}
92
- onInput={onInput}
93
- ref={inputRef}
94
- list={lapisField}
95
- value={initialValue}
96
- />
97
- <datalist id={lapisField}>
98
- {data.map((item) => (
99
- <option value={item} key={item} />
100
- ))}
101
- </datalist>
102
- </>
77
+ <DownshiftCombobox
78
+ allItems={data}
79
+ value={value}
80
+ filterItemsByInputValue={filterByInputValue}
81
+ createEvent={(item: string | null) => new LineageFilterChangedEvent({ [lapisField]: item ?? undefined })}
82
+ itemToString={(item: string | undefined | null) => item ?? ''}
83
+ placeholderText={placeholderText}
84
+ formatItemInList={(item: string) => item}
85
+ />
103
86
  );
104
87
  };
88
+
89
+ function filterByInputValue(item: string, inputValue: string | undefined | null) {
90
+ if (inputValue === undefined || inputValue === null || inputValue === '') {
91
+ return true;
92
+ }
93
+ return item?.toLowerCase().includes(inputValue?.toLowerCase() || '');
94
+ }
@@ -1,4 +1,4 @@
1
- export type LapisLocationFilter = Record<string, string | null | undefined>;
1
+ import { type LapisLocationFilter } from '../../types';
2
2
 
3
3
  export class LocationChangedEvent extends CustomEvent<LapisLocationFilter> {
4
4
  constructor(detail: LapisLocationFilter) {
@@ -8,7 +8,7 @@ describe('fetchAutocompletionList', () => {
8
8
  const fields = ['region', 'country', 'division'];
9
9
 
10
10
  lapisRequestMocks.aggregated(
11
- { fields },
11
+ { fields, country: 'Germany' },
12
12
  {
13
13
  data: [
14
14
  { count: 0, region: 'region1', country: 'country1_1', division: 'division1_1_1' },
@@ -20,7 +20,11 @@ describe('fetchAutocompletionList', () => {
20
20
  },
21
21
  );
22
22
 
23
- const result = await fetchAutocompletionList(fields, DUMMY_LAPIS_URL);
23
+ const result = await fetchAutocompletionList({
24
+ fields,
25
+ lapis: DUMMY_LAPIS_URL,
26
+ lapisFilter: { country: 'Germany' },
27
+ });
24
28
 
25
29
  expect(result).to.deep.equal([
26
30
  { region: 'region1', country: undefined, division: undefined },