@genspectrum/dashboard-components 0.12.1 → 0.13.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 (63) hide show
  1. package/custom-elements.json +292 -25
  2. package/dist/{LocationChangedEvent-CORvQvXv.js → LineageFilterChangedEvent-GedKNGFI.js} +25 -3
  3. package/dist/LineageFilterChangedEvent-GedKNGFI.js.map +1 -0
  4. package/dist/assets/mutationOverTimeWorker-B1-WrM4b.js.map +1 -0
  5. package/dist/components.d.ts +124 -25
  6. package/dist/components.js +765 -572
  7. package/dist/components.js.map +1 -1
  8. package/dist/style.css +3 -0
  9. package/dist/util.d.ts +48 -18
  10. package/dist/util.js +3 -1
  11. package/package.json +2 -2
  12. package/src/constants.ts +6 -0
  13. package/src/lapisApi/__mockData__/wiseReferenceGenome.json +9 -0
  14. package/src/lapisApi/lapisApi.ts +17 -0
  15. package/src/lapisApi/lapisTypes.ts +7 -1
  16. package/src/operator/FetchDetailsOperator.ts +28 -0
  17. package/src/preact/components/downshift-combobox.tsx +145 -0
  18. package/src/preact/components/tabs.tsx +1 -1
  19. package/src/preact/lineageFilter/LineageFilterChangedEvent.ts +11 -0
  20. package/src/preact/lineageFilter/fetchLineageAutocompleteList.spec.ts +16 -2
  21. package/src/preact/lineageFilter/fetchLineageAutocompleteList.ts +13 -2
  22. package/src/preact/lineageFilter/lineage-filter.stories.tsx +110 -9
  23. package/src/preact/lineageFilter/lineage-filter.tsx +40 -50
  24. package/src/preact/locationFilter/LocationChangedEvent.ts +1 -1
  25. package/src/preact/locationFilter/fetchAutocompletionList.spec.ts +6 -2
  26. package/src/preact/locationFilter/fetchAutocompletionList.ts +16 -6
  27. package/src/preact/locationFilter/location-filter.stories.tsx +33 -30
  28. package/src/preact/locationFilter/location-filter.tsx +47 -144
  29. package/src/preact/mutationsOverTime/MutationOverTimeData.ts +9 -5
  30. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +5 -3
  31. package/src/preact/shared/sort/sortSubstitutionsAndDeletions.ts +4 -7
  32. package/src/preact/textInput/TextInputChangedEvent.ts +1 -1
  33. package/src/preact/textInput/fetchStringAutocompleteList.ts +20 -0
  34. package/src/preact/textInput/text-input.stories.tsx +14 -11
  35. package/src/preact/textInput/text-input.tsx +39 -140
  36. package/src/preact/wastewater/mutationsOverTime/__mockData__/details.json +88 -0
  37. package/src/preact/wastewater/mutationsOverTime/computeWastewaterMutationsOverTimeDataPerLocation.spec.ts +159 -0
  38. package/src/preact/wastewater/mutationsOverTime/computeWastewaterMutationsOverTimeDataPerLocation.ts +51 -0
  39. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +71 -0
  40. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +151 -0
  41. package/src/query/queryMutationsOverTime.ts +6 -14
  42. package/src/query/queryWastewaterMutationsOverTime.spec.ts +94 -0
  43. package/src/query/queryWastewaterMutationsOverTime.ts +55 -0
  44. package/src/types.ts +3 -0
  45. package/src/utilEntrypoint.ts +2 -0
  46. package/src/utils/map2d.ts +39 -0
  47. package/src/web-components/index.ts +1 -0
  48. package/src/web-components/input/gs-lineage-filter.stories.ts +120 -31
  49. package/src/web-components/input/gs-lineage-filter.tsx +24 -8
  50. package/src/web-components/input/gs-location-filter.stories.ts +9 -0
  51. package/src/web-components/input/gs-location-filter.tsx +21 -3
  52. package/src/web-components/input/gs-text-input.stories.ts +14 -5
  53. package/src/web-components/input/gs-text-input.tsx +23 -7
  54. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.stories.ts +82 -0
  55. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +112 -0
  56. package/src/web-components/wastewaterVisualization/index.ts +1 -0
  57. package/standalone-bundle/assets/{mutationOverTimeWorker-DEybsZ5r.js.map → mutationOverTimeWorker-Cls1J0cl.js.map} +1 -1
  58. package/standalone-bundle/dashboard-components.js +6972 -6796
  59. package/standalone-bundle/dashboard-components.js.map +1 -1
  60. package/standalone-bundle/style.css +1 -1
  61. package/dist/LocationChangedEvent-CORvQvXv.js.map +0 -1
  62. package/dist/assets/mutationOverTimeWorker-DTv93Ere.js.map +0 -1
  63. package/src/preact/textInput/fetchAutocompleteList.ts +0 -9
@@ -1,35 +1,37 @@
1
- import { useCombobox } from 'downshift/preact';
2
1
  import { type FunctionComponent } from 'preact';
3
- import { useContext, useMemo, useRef, useState } from 'preact/hooks';
2
+ import { useContext, useMemo } from 'preact/hooks';
4
3
  import z from 'zod';
5
4
 
6
5
  import { fetchAutocompletionList } from './fetchAutocompletionList';
7
6
  import { LapisUrlContext } from '../LapisUrlContext';
8
- import { type LapisLocationFilter, LocationChangedEvent } from './LocationChangedEvent';
7
+ import { LocationChangedEvent } from './LocationChangedEvent';
8
+ import { lapisFilterSchema, type LapisLocationFilter, lapisLocationFilterSchema } from '../../types';
9
+ import { DownshiftCombobox } from '../components/downshift-combobox';
9
10
  import { ErrorBoundary } from '../components/error-boundary';
10
11
  import { LoadingDisplay } from '../components/loading-display';
11
12
  import { ResizeContainer } from '../components/resize-container';
12
13
  import { useQuery } from '../useQuery';
13
14
 
14
- const lineageFilterInnerPropsSchema = z.object({
15
- value: z.record(z.string().nullable().optional()).optional(),
15
+ const locationSelectorPropsSchema = z.object({
16
+ value: lapisLocationFilterSchema.optional(),
16
17
  placeholderText: z.string().optional(),
17
18
  fields: z.array(z.string()).min(1),
18
19
  });
19
-
20
- const lineageFilterPropsSchema = lineageFilterInnerPropsSchema.extend({
20
+ const locationFilterInnerPropsSchema = locationSelectorPropsSchema.extend({ lapisFilter: lapisFilterSchema });
21
+ const locationFilterPropsSchema = locationFilterInnerPropsSchema.extend({
21
22
  width: z.string(),
22
23
  });
23
24
 
24
- export type LocationFilterInnerProps = z.infer<typeof lineageFilterInnerPropsSchema>;
25
- export type LocationFilterProps = z.infer<typeof lineageFilterPropsSchema>;
25
+ export type LocationFilterInnerProps = z.infer<typeof locationFilterInnerPropsSchema>;
26
+ export type LocationFilterProps = z.infer<typeof locationFilterPropsSchema>;
27
+ type LocationSelectorProps = z.infer<typeof locationSelectorPropsSchema>;
26
28
 
27
29
  export const LocationFilter: FunctionComponent<LocationFilterProps> = (props) => {
28
30
  const { width, ...innerProps } = props;
29
31
  const size = { width, height: '3rem' };
30
32
 
31
33
  return (
32
- <ErrorBoundary size={size} layout='horizontal' componentProps={props} schema={lineageFilterPropsSchema}>
34
+ <ErrorBoundary size={size} layout='horizontal' componentProps={props} schema={locationFilterPropsSchema}>
33
35
  <ResizeContainer size={size}>
34
36
  <LocationFilterInner {...innerProps} />
35
37
  </ResizeContainer>
@@ -37,10 +39,13 @@ export const LocationFilter: FunctionComponent<LocationFilterProps> = (props) =>
37
39
  );
38
40
  };
39
41
 
40
- export const LocationFilterInner = ({ value, fields, placeholderText }: LocationFilterInnerProps) => {
42
+ export const LocationFilterInner = ({ value, fields, placeholderText, lapisFilter }: LocationFilterInnerProps) => {
41
43
  const lapis = useContext(LapisUrlContext);
42
44
 
43
- const { data, error, isLoading } = useQuery(() => fetchAutocompletionList(fields, lapis), [fields, lapis]);
45
+ const { data, error, isLoading } = useQuery(
46
+ () => fetchAutocompletionList({ fields, lapis, lapisFilter }),
47
+ [fields, lapis, lapisFilter],
48
+ );
44
49
 
45
50
  if (isLoading) {
46
51
  return <LoadingDisplay />;
@@ -54,7 +59,7 @@ export const LocationFilterInner = ({ value, fields, placeholderText }: Location
54
59
 
55
60
  type SelectItem = {
56
61
  lapisFilter: LapisLocationFilter;
57
- label: string;
62
+ label: string | null | undefined;
58
63
  description: string;
59
64
  };
60
65
 
@@ -63,140 +68,38 @@ const LocationSelector = ({
63
68
  value,
64
69
  placeholderText,
65
70
  locationData,
66
- }: LocationFilterInnerProps & {
71
+ }: LocationSelectorProps & {
67
72
  locationData: LapisLocationFilter[];
68
73
  }) => {
69
- const allItems = useMemo(
70
- () =>
71
- locationData
72
- .map((locationFilter) => {
73
- return toSelectOption(locationFilter, fields);
74
- })
75
- .filter((item): item is SelectItem => item !== undefined),
76
- [locationData, fields],
77
- );
74
+ const allItems = useMemo(() => {
75
+ return locationData
76
+ .map((location) => toSelectItem(location, fields))
77
+ .filter((item): item is SelectItem => item !== undefined);
78
+ }, [fields, locationData]);
78
79
 
79
- const initialSelectedItem = useMemo(
80
- () => (value !== undefined ? toSelectOption(value, fields) : null),
81
- [value, fields],
82
- );
83
-
84
- const [items, setItems] = useState(allItems.filter((item) => filterByInputValue(item, initialSelectedItem?.label)));
85
- const divRef = useRef<HTMLDivElement>(null);
86
-
87
- const shadowRoot = divRef.current?.shadowRoot ?? undefined;
88
-
89
- const environment =
90
- shadowRoot !== undefined
91
- ? {
92
- addEventListener: window.addEventListener.bind(window),
93
- removeEventListener: window.removeEventListener.bind(window),
94
- document: shadowRoot.ownerDocument,
95
- Node: window.Node,
96
- }
97
- : undefined;
98
-
99
- const {
100
- isOpen,
101
- getToggleButtonProps,
102
- getMenuProps,
103
- getInputProps,
104
- highlightedIndex,
105
- getItemProps,
106
- selectedItem,
107
- inputValue,
108
- selectItem,
109
- setInputValue,
110
- closeMenu,
111
- } = useCombobox({
112
- onInputValueChange({ inputValue }) {
113
- setItems(allItems.filter((item) => filterByInputValue(item, inputValue)));
114
- },
115
- onSelectedItemChange({ selectedItem }) {
116
- if (selectedItem !== null) {
117
- divRef.current?.dispatchEvent(new LocationChangedEvent(selectedItem.lapisFilter));
118
- }
119
- },
120
- items,
121
- itemToString(item) {
122
- return item?.label ?? '';
123
- },
124
- initialSelectedItem,
125
- environment,
126
- });
127
-
128
- const onInputBlur = () => {
129
- if (inputValue === '') {
130
- divRef.current?.dispatchEvent(new LocationChangedEvent(emptyLocationFilter(fields)));
131
- selectItem(null);
132
- } else if (inputValue !== selectedItem?.label) {
133
- setInputValue(selectedItem?.label || '');
134
- }
135
- };
136
-
137
- const clearInput = () => {
138
- divRef.current?.dispatchEvent(new LocationChangedEvent(emptyLocationFilter(fields)));
139
- selectItem(null);
140
- };
141
-
142
- const buttonRef = useRef(null);
80
+ const selectedItem = useMemo(() => {
81
+ return value !== undefined ? toSelectItem(value, fields) : undefined;
82
+ }, [fields, value]);
143
83
 
144
84
  return (
145
- <div ref={divRef} className={'relative w-full'}>
146
- <div className='w-full flex flex-col gap-1'>
147
- <div
148
- className='flex gap-0.5 input input-bordered min-w-32'
149
- onBlur={(event) => {
150
- if (event.relatedTarget != buttonRef.current) {
151
- closeMenu();
152
- }
153
- }}
154
- >
155
- <input
156
- placeholder={placeholderText}
157
- className='w-full p-1.5'
158
- {...getInputProps()}
159
- onBlur={onInputBlur}
160
- />
161
- <button
162
- aria-label='clear selection'
163
- className={`px-2 ${inputValue === '' && 'hidden'}`}
164
- type='button'
165
- onClick={clearInput}
166
- tabIndex={-1}
167
- >
168
- ×
169
- </button>
170
- <button
171
- aria-label='toggle menu'
172
- className='px-2'
173
- type='button'
174
- {...getToggleButtonProps()}
175
- ref={buttonRef}
176
- >
177
- {isOpen ? <>↑</> : <>↓</>}
178
- </button>
179
- </div>
180
- </div>
181
- <ul
182
- className={`absolute bg-white mt-1 shadow-md max-h-80 overflow-scroll z-10 w-full min-w-32 ${
183
- !(isOpen && items.length > 0) && 'hidden'
184
- }`}
185
- {...getMenuProps()}
186
- >
187
- {isOpen &&
188
- items.map((item, index) => (
189
- <li
190
- className={`${highlightedIndex === index && 'bg-blue-300'} ${selectedItem !== null && selectedItem.description === item.description && 'font-bold'} py-2 px-3 shadow-sm flex flex-col`}
191
- key={item.description}
192
- {...getItemProps({ item, index })}
193
- >
194
- <span>{item.label}</span>
195
- <span className='text-sm text-gray-500'>{item.description}</span>
196
- </li>
197
- ))}
198
- </ul>
199
- </div>
85
+ <DownshiftCombobox
86
+ allItems={allItems}
87
+ value={selectedItem}
88
+ filterItemsByInputValue={filterByInputValue}
89
+ createEvent={(item: SelectItem | null) =>
90
+ new LocationChangedEvent(item?.lapisFilter ?? emptyLocationFilter(fields))
91
+ }
92
+ itemToString={(item: SelectItem | undefined | null) => item?.label ?? ''}
93
+ placeholderText={placeholderText}
94
+ formatItemInList={(item: SelectItem) => {
95
+ return (
96
+ <>
97
+ <span>{item.label}</span>
98
+ <span className='text-sm text-gray-500'>{item.description}</span>
99
+ </>
100
+ );
101
+ }}
102
+ />
200
103
  );
201
104
  };
202
105
 
@@ -205,12 +108,12 @@ function filterByInputValue(item: SelectItem, inputValue: string | undefined | n
205
108
  return true;
206
109
  }
207
110
  return (
208
- item?.label.toLowerCase().includes(inputValue.toLowerCase()) ||
111
+ item?.label?.toLowerCase().includes(inputValue.toLowerCase()) ||
209
112
  item?.description.toLowerCase().includes(inputValue.toLowerCase())
210
113
  );
211
114
  }
212
115
 
213
- function toSelectOption(locationFilter: LapisLocationFilter, fields: string[]) {
116
+ function toSelectItem(locationFilter: LapisLocationFilter, fields: string[]): SelectItem | undefined {
214
117
  const concatenatedLocation = concatenateLocation(locationFilter, fields);
215
118
 
216
119
  const lastNonUndefinedField = [...fields]
@@ -5,16 +5,20 @@ import {
5
5
  } from '../../query/queryMutationsOverTime';
6
6
  import { type Map2d, Map2dBase, type Map2DContents } from '../../utils/map2d';
7
7
  import type { Deletion, Substitution } from '../../utils/mutations';
8
- import type { Temporal } from '../../utils/temporalClass';
8
+ import type { Temporal, TemporalClass } from '../../utils/temporalClass';
9
9
 
10
- export type MutationOverTimeDataMap = Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>;
10
+ export type MutationOverTimeDataMap<T extends Temporal | TemporalClass = Temporal> = Map2d<
11
+ Substitution | Deletion,
12
+ T,
13
+ MutationOverTimeMutationValue
14
+ >;
11
15
 
12
- export class BaseMutationOverTimeDataMap extends Map2dBase<
16
+ export class BaseMutationOverTimeDataMap<T extends Temporal | TemporalClass = Temporal> extends Map2dBase<
13
17
  Substitution | Deletion,
14
- Temporal,
18
+ T,
15
19
  MutationOverTimeMutationValue
16
20
  > {
17
- constructor(initialContent?: Map2DContents<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>) {
21
+ constructor(initialContent?: Map2DContents<Substitution | Deletion, T, MutationOverTimeMutationValue>) {
18
22
  super(serializeSubstitutionOrDeletion, serializeTemporal, initialContent);
19
23
  }
20
24
  }
@@ -111,9 +111,11 @@ const ProportionCell: FunctionComponent<{
111
111
  ) : (
112
112
  <>
113
113
  <p>Proportion: {formatProportion(value.proportion)}</p>
114
- <p>
115
- Count: {value.count} / {value.totalCount} total
116
- </p>
114
+ {value.count !== null && value.totalCount !== null && (
115
+ <p>
116
+ Count: {value.count} / {value.totalCount} total
117
+ </p>
118
+ )}
117
119
  </>
118
120
  )}
119
121
  </div>
@@ -1,9 +1,6 @@
1
- import { DeletionClass, type SubstitutionClass } from '../../../utils/mutations';
1
+ import type { Deletion, Substitution } from '../../../utils/mutations';
2
2
 
3
- export const sortSubstitutionsAndDeletions = (
4
- a: SubstitutionClass | DeletionClass,
5
- b: SubstitutionClass | DeletionClass,
6
- ) => {
3
+ export const sortSubstitutionsAndDeletions = (a: Substitution | Deletion, b: Substitution | Deletion) => {
7
4
  if (a.segment !== b.segment) {
8
5
  return compareSegments(a.segment, b.segment);
9
6
  }
@@ -12,8 +9,8 @@ export const sortSubstitutionsAndDeletions = (
12
9
  return comparePositions(a.position, b.position);
13
10
  }
14
11
 
15
- const aIsDeletion = a instanceof DeletionClass;
16
- const bIsDeletion = b instanceof DeletionClass;
12
+ const aIsDeletion = a.type === 'deletion';
13
+ const bIsDeletion = b.type === 'deletion';
17
14
 
18
15
  if (aIsDeletion !== bIsDeletion) {
19
16
  return aIsDeletion ? 1 : -1;
@@ -1,4 +1,4 @@
1
- type LapisTextFilter = Record<string, string | null | undefined>;
1
+ type LapisTextFilter = Record<string, string | undefined>;
2
2
 
3
3
  export class TextInputChangedEvent extends CustomEvent<LapisTextFilter> {
4
4
  constructor(detail: LapisTextFilter) {
@@ -0,0 +1,20 @@
1
+ import { FetchAggregatedOperator } from '../../operator/FetchAggregatedOperator';
2
+ import { type LapisFilter } from '../../types';
3
+
4
+ export async function fetchStringAutocompleteList({
5
+ lapis,
6
+ field,
7
+ lapisFilter,
8
+ signal,
9
+ }: {
10
+ lapis: string;
11
+ field: string;
12
+ lapisFilter?: LapisFilter;
13
+ signal?: AbortSignal;
14
+ }) {
15
+ const fetchAggregatedOperator = new FetchAggregatedOperator<Record<string, string>>(lapisFilter ?? {}, [field]);
16
+
17
+ const data = (await fetchAggregatedOperator.evaluate(lapis, signal)).content;
18
+
19
+ return data.map((item) => item[field]).sort();
20
+ }
@@ -23,6 +23,7 @@ const meta: Meta<TextInputProps> = {
23
23
  url: AGGREGATED_ENDPOINT,
24
24
  body: {
25
25
  fields: ['host'],
26
+ country: 'Germany',
26
27
  },
27
28
  },
28
29
  response: {
@@ -36,16 +37,15 @@ const meta: Meta<TextInputProps> = {
36
37
  argTypes: {
37
38
  lapisField: {
38
39
  control: {
39
- type: 'select',
40
+ type: 'text',
40
41
  },
41
- options: ['host'],
42
42
  },
43
43
  placeholderText: {
44
44
  control: {
45
45
  type: 'text',
46
46
  },
47
47
  },
48
- initialValue: {
48
+ value: {
49
49
  control: {
50
50
  type: 'text',
51
51
  },
@@ -55,6 +55,11 @@ const meta: Meta<TextInputProps> = {
55
55
  type: 'text',
56
56
  },
57
57
  },
58
+ lapisFilter: {
59
+ control: {
60
+ type: 'object',
61
+ },
62
+ },
58
63
  },
59
64
  };
60
65
 
@@ -63,19 +68,17 @@ export default meta;
63
68
  export const Default: StoryObj<TextInputProps> = {
64
69
  render: (args) => (
65
70
  <LapisUrlContext.Provider value={LAPIS_URL}>
66
- <TextInput
67
- lapisField={args.lapisField}
68
- placeholderText={args.placeholderText}
69
- initialValue={args.initialValue}
70
- width={args.width}
71
- />
71
+ <TextInput {...args} />
72
72
  </LapisUrlContext.Provider>
73
73
  ),
74
74
  args: {
75
75
  lapisField: 'host',
76
76
  placeholderText: 'Enter a host name',
77
- initialValue: '',
77
+ value: '',
78
78
  width: '100%',
79
+ lapisFilter: {
80
+ country: 'Germany',
81
+ },
79
82
  },
80
83
  };
81
84
 
@@ -83,7 +86,7 @@ export const RemoveInitialValue: StoryObj<TextInputProps> = {
83
86
  ...Default,
84
87
  args: {
85
88
  ...Default.args,
86
- initialValue: 'Homo sapiens',
89
+ value: 'Homo sapiens',
87
90
  },
88
91
  play: async ({ canvasElement, step }) => {
89
92
  const canvas = within(canvasElement);
@@ -1,29 +1,31 @@
1
- import { useCombobox } from 'downshift/preact';
2
1
  import { type FunctionComponent } from 'preact';
3
- import { useContext, useRef, useState } from 'preact/hooks';
2
+ import { useContext } from 'preact/hooks';
4
3
  import z from 'zod';
5
4
 
6
- import { fetchAutocompleteList } from './fetchAutocompleteList';
5
+ import { fetchStringAutocompleteList } from './fetchStringAutocompleteList';
7
6
  import { LapisUrlContext } from '../LapisUrlContext';
8
7
  import { TextInputChangedEvent } from './TextInputChangedEvent';
8
+ import { lapisFilterSchema } from '../../types';
9
+ import { DownshiftCombobox } from '../components/downshift-combobox';
9
10
  import { ErrorBoundary } from '../components/error-boundary';
10
11
  import { LoadingDisplay } from '../components/loading-display';
11
12
  import { NoDataDisplay } from '../components/no-data-display';
12
13
  import { ResizeContainer } from '../components/resize-container';
13
14
  import { useQuery } from '../useQuery';
14
15
 
15
- const textInputInnerPropsSchema = z.object({
16
+ const textSelectorPropsSchema = z.object({
16
17
  lapisField: z.string().min(1),
17
18
  placeholderText: z.string().optional(),
18
- initialValue: z.string().optional(),
19
+ value: z.string().optional(),
19
20
  });
20
-
21
+ const textInputInnerPropsSchema = textSelectorPropsSchema.extend({ lapisFilter: lapisFilterSchema });
21
22
  const textInputPropsSchema = textInputInnerPropsSchema.extend({
22
23
  width: z.string(),
23
24
  });
24
25
 
25
26
  export type TextInputInnerProps = z.infer<typeof textInputInnerPropsSchema>;
26
27
  export type TextInputProps = z.infer<typeof textInputPropsSchema>;
28
+ type TextSelectorProps = z.infer<typeof textSelectorPropsSchema>;
27
29
 
28
30
  export const TextInput: FunctionComponent<TextInputProps> = (props) => {
29
31
  const { width, ...innerProps } = props;
@@ -38,10 +40,18 @@ export const TextInput: FunctionComponent<TextInputProps> = (props) => {
38
40
  );
39
41
  };
40
42
 
41
- const TextInputInner: FunctionComponent<TextInputInnerProps> = ({ initialValue, lapisField, placeholderText }) => {
43
+ const TextInputInner: FunctionComponent<TextInputInnerProps> = ({
44
+ value,
45
+ lapisField,
46
+ placeholderText,
47
+ lapisFilter,
48
+ }) => {
42
49
  const lapis = useContext(LapisUrlContext);
43
50
 
44
- const { data, error, isLoading } = useQuery(() => fetchAutocompleteList(lapis, lapisField), [lapisField, lapis]);
51
+ const { data, error, isLoading } = useQuery(
52
+ () => fetchStringAutocompleteList({ lapis, field: lapisField, lapisFilter }),
53
+ [lapisField, lapis, lapisFilter],
54
+ );
45
55
 
46
56
  if (isLoading) {
47
57
  return <LoadingDisplay />;
@@ -55,146 +65,35 @@ const TextInputInner: FunctionComponent<TextInputInnerProps> = ({ initialValue,
55
65
  return <NoDataDisplay />;
56
66
  }
57
67
 
58
- return (
59
- <TextSelector
60
- lapisField={lapisField}
61
- initialValue={initialValue}
62
- placeholderText={placeholderText}
63
- data={data}
64
- />
65
- );
68
+ return <TextSelector lapisField={lapisField} value={value} placeholderText={placeholderText} data={data} />;
66
69
  };
67
70
 
68
71
  const TextSelector = ({
69
72
  lapisField,
70
- initialValue,
73
+ value,
71
74
  placeholderText,
72
75
  data,
73
- }: TextInputInnerProps & {
76
+ }: TextSelectorProps & {
74
77
  data: string[];
75
78
  }) => {
76
- const [items, setItems] = useState(data.filter((item) => filterByInputValue(item, initialValue)));
77
-
78
- const divRef = useRef<HTMLDivElement>(null);
79
-
80
- const shadowRoot = divRef.current?.shadowRoot ?? undefined;
81
-
82
- const environment =
83
- shadowRoot !== undefined
84
- ? {
85
- addEventListener: window.addEventListener.bind(window),
86
- removeEventListener: window.removeEventListener.bind(window),
87
- document: shadowRoot.ownerDocument,
88
- Node: window.Node,
89
- }
90
- : undefined;
91
-
92
- function filterByInputValue(item: string, inputValue: string | undefined | null) {
93
- if (inputValue === undefined || inputValue === null || inputValue === '') {
94
- return true;
95
- }
96
- return item?.toLowerCase().includes(inputValue?.toLowerCase() || '');
97
- }
98
-
99
- const {
100
- isOpen,
101
- getToggleButtonProps,
102
- getMenuProps,
103
- getInputProps,
104
- highlightedIndex,
105
- getItemProps,
106
- selectedItem,
107
- inputValue,
108
- selectItem,
109
- setInputValue,
110
- closeMenu,
111
- } = useCombobox({
112
- onInputValueChange({ inputValue }) {
113
- setItems(data.filter((item) => filterByInputValue(item, inputValue)));
114
- },
115
- onSelectedItemChange({ selectedItem }) {
116
- if (selectedItem !== null) {
117
- divRef.current?.dispatchEvent(new TextInputChangedEvent({ [lapisField]: selectedItem }));
118
- }
119
- },
120
- items,
121
- itemToString(item) {
122
- return item ?? '';
123
- },
124
- initialSelectedItem: initialValue,
125
- environment,
126
- });
127
-
128
- const onInputBlur = () => {
129
- if (inputValue === '') {
130
- divRef.current?.dispatchEvent(new TextInputChangedEvent({ [lapisField]: undefined }));
131
- selectItem(null);
132
- } else if (inputValue !== selectedItem) {
133
- setInputValue(selectedItem ?? '');
134
- }
135
- };
136
-
137
- const clearInput = () => {
138
- divRef.current?.dispatchEvent(new TextInputChangedEvent({ [lapisField]: undefined }));
139
- selectItem(null);
140
- };
141
-
142
- const buttonRef = useRef(null);
143
-
144
79
  return (
145
- <div ref={divRef} className={'relative w-full'}>
146
- <div className='w-full flex flex-col gap-1'>
147
- <div
148
- className='flex gap-0.5 input input-bordered min-w-32'
149
- onBlur={(event) => {
150
- if (event.relatedTarget != buttonRef.current) {
151
- closeMenu();
152
- }
153
- }}
154
- >
155
- <input
156
- placeholder={placeholderText}
157
- className='w-full p-1.5'
158
- {...getInputProps()}
159
- onBlur={onInputBlur}
160
- />
161
- <button
162
- aria-label='clear selection'
163
- className={`px-2 ${inputValue === '' && 'hidden'}`}
164
- type='button'
165
- onClick={clearInput}
166
- tabIndex={-1}
167
- >
168
- ×
169
- </button>
170
- <button
171
- aria-label='toggle menu'
172
- className='px-2'
173
- type='button'
174
- {...getToggleButtonProps()}
175
- ref={buttonRef}
176
- >
177
- {isOpen ? <>↑</> : <>↓</>}
178
- </button>
179
- </div>
180
- </div>
181
- <ul
182
- className={`absolute bg-white mt-1 shadow-md max-h-80 overflow-scroll z-10 w-full min-w-32 ${
183
- !(isOpen && items.length > 0) && 'hidden'
184
- }`}
185
- {...getMenuProps()}
186
- >
187
- {isOpen &&
188
- items.map((item, index) => (
189
- <li
190
- className={`${highlightedIndex === index && 'bg-blue-300'} ${selectedItem !== null} py-2 px-3 shadow-sm flex flex-col`}
191
- key={item}
192
- {...getItemProps({ item, index })}
193
- >
194
- <span>{item}</span>
195
- </li>
196
- ))}
197
- </ul>
198
- </div>
80
+ <DownshiftCombobox
81
+ allItems={data}
82
+ value={value}
83
+ filterItemsByInputValue={filterByInputValue}
84
+ createEvent={(item: string | null) => new TextInputChangedEvent({ [lapisField]: item ?? undefined })}
85
+ itemToString={(item: string | undefined | null) => item ?? ''}
86
+ placeholderText={placeholderText}
87
+ formatItemInList={(item: string) => {
88
+ return <span>{item}</span>;
89
+ }}
90
+ />
199
91
  );
200
92
  };
93
+
94
+ function filterByInputValue(item: string, inputValue: string | undefined | null) {
95
+ if (inputValue === undefined || inputValue === null || inputValue === '') {
96
+ return true;
97
+ }
98
+ return item?.toLowerCase().includes(inputValue?.toLowerCase() || '');
99
+ }