@genspectrum/dashboard-components 0.19.1 → 0.19.2

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.
package/dist/util.d.ts CHANGED
@@ -902,7 +902,7 @@ declare global {
902
902
 
903
903
  declare global {
904
904
  interface HTMLElementTagNameMap {
905
- 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
905
+ 'gs-mutation-comparison-component': MutationComparisonComponent;
906
906
  }
907
907
  }
908
908
 
@@ -910,7 +910,7 @@ declare global {
910
910
  declare global {
911
911
  namespace JSX {
912
912
  interface IntrinsicElements {
913
- 'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
913
+ 'gs-mutation-comparison-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
914
914
  }
915
915
  }
916
916
  }
@@ -918,7 +918,7 @@ declare global {
918
918
 
919
919
  declare global {
920
920
  interface HTMLElementTagNameMap {
921
- 'gs-mutation-comparison-component': MutationComparisonComponent;
921
+ 'gs-mutations-component': MutationsComponent;
922
922
  }
923
923
  }
924
924
 
@@ -926,7 +926,7 @@ declare global {
926
926
  declare global {
927
927
  namespace JSX {
928
928
  interface IntrinsicElements {
929
- 'gs-mutation-comparison-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
929
+ 'gs-mutations-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
930
930
  }
931
931
  }
932
932
  }
@@ -934,7 +934,7 @@ declare global {
934
934
 
935
935
  declare global {
936
936
  interface HTMLElementTagNameMap {
937
- 'gs-mutations-component': MutationsComponent;
937
+ 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
938
938
  }
939
939
  }
940
940
 
@@ -942,7 +942,7 @@ declare global {
942
942
  declare global {
943
943
  namespace JSX {
944
944
  interface IntrinsicElements {
945
- 'gs-mutations-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
945
+ 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
946
946
  }
947
947
  }
948
948
  }
@@ -950,7 +950,7 @@ declare global {
950
950
 
951
951
  declare global {
952
952
  interface HTMLElementTagNameMap {
953
- 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
953
+ 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
954
954
  }
955
955
  }
956
956
 
@@ -958,7 +958,7 @@ declare global {
958
958
  declare global {
959
959
  namespace JSX {
960
960
  interface IntrinsicElements {
961
- 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
961
+ 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
962
962
  }
963
963
  }
964
964
  }
@@ -966,7 +966,7 @@ declare global {
966
966
 
967
967
  declare global {
968
968
  interface HTMLElementTagNameMap {
969
- 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
969
+ 'gs-aggregate': AggregateComponent;
970
970
  }
971
971
  }
972
972
 
@@ -974,7 +974,7 @@ declare global {
974
974
  declare global {
975
975
  namespace JSX {
976
976
  interface IntrinsicElements {
977
- 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
977
+ 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
978
978
  }
979
979
  }
980
980
  }
@@ -982,7 +982,7 @@ declare global {
982
982
 
983
983
  declare global {
984
984
  interface HTMLElementTagNameMap {
985
- 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
985
+ 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
986
986
  }
987
987
  }
988
988
 
@@ -990,7 +990,7 @@ declare global {
990
990
  declare global {
991
991
  namespace JSX {
992
992
  interface IntrinsicElements {
993
- 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
993
+ 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
994
994
  }
995
995
  }
996
996
  }
@@ -1014,7 +1014,7 @@ declare global {
1014
1014
 
1015
1015
  declare global {
1016
1016
  interface HTMLElementTagNameMap {
1017
- 'gs-aggregate': AggregateComponent;
1017
+ 'gs-sequences-by-location': SequencesByLocationComponent;
1018
1018
  }
1019
1019
  }
1020
1020
 
@@ -1022,7 +1022,7 @@ declare global {
1022
1022
  declare global {
1023
1023
  namespace JSX {
1024
1024
  interface IntrinsicElements {
1025
- 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1025
+ 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1026
1026
  }
1027
1027
  }
1028
1028
  }
@@ -1030,7 +1030,7 @@ declare global {
1030
1030
 
1031
1031
  declare global {
1032
1032
  interface HTMLElementTagNameMap {
1033
- 'gs-sequences-by-location': SequencesByLocationComponent;
1033
+ 'gs-statistics': StatisticsComponent;
1034
1034
  }
1035
1035
  }
1036
1036
 
@@ -1038,7 +1038,7 @@ declare global {
1038
1038
  declare global {
1039
1039
  namespace JSX {
1040
1040
  interface IntrinsicElements {
1041
- 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1041
+ 'gs-statistics': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1042
1042
  }
1043
1043
  }
1044
1044
  }
@@ -1046,7 +1046,7 @@ declare global {
1046
1046
 
1047
1047
  declare global {
1048
1048
  interface HTMLElementTagNameMap {
1049
- 'gs-statistics': StatisticsComponent;
1049
+ 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
1050
1050
  }
1051
1051
  }
1052
1052
 
@@ -1054,7 +1054,7 @@ declare global {
1054
1054
  declare global {
1055
1055
  namespace JSX {
1056
1056
  interface IntrinsicElements {
1057
- 'gs-statistics': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1057
+ 'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1058
1058
  }
1059
1059
  }
1060
1060
  }
@@ -1062,11 +1062,10 @@ declare global {
1062
1062
 
1063
1063
  declare global {
1064
1064
  interface HTMLElementTagNameMap {
1065
- 'gs-date-range-filter': DateRangeFilterComponent;
1065
+ 'gs-location-filter': LocationFilterComponent;
1066
1066
  }
1067
1067
  interface HTMLElementEventMap {
1068
- 'gs-date-range-filter-changed': CustomEvent<Record<string, string>>;
1069
- 'gs-date-range-option-changed': DateRangeOptionChangedEvent;
1068
+ 'gs-location-changed': LocationChangedEvent;
1070
1069
  }
1071
1070
  }
1072
1071
 
@@ -1074,7 +1073,7 @@ declare global {
1074
1073
  declare global {
1075
1074
  namespace JSX {
1076
1075
  interface IntrinsicElements {
1077
- 'gs-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1076
+ 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1078
1077
  }
1079
1078
  }
1080
1079
  }
@@ -1082,10 +1081,11 @@ declare global {
1082
1081
 
1083
1082
  declare global {
1084
1083
  interface HTMLElementTagNameMap {
1085
- 'gs-location-filter': LocationFilterComponent;
1084
+ 'gs-date-range-filter': DateRangeFilterComponent;
1086
1085
  }
1087
1086
  interface HTMLElementEventMap {
1088
- 'gs-location-changed': LocationChangedEvent;
1087
+ 'gs-date-range-filter-changed': CustomEvent<Record<string, string>>;
1088
+ 'gs-date-range-option-changed': DateRangeOptionChangedEvent;
1089
1089
  }
1090
1090
  }
1091
1091
 
@@ -1093,7 +1093,7 @@ declare global {
1093
1093
  declare global {
1094
1094
  namespace JSX {
1095
1095
  interface IntrinsicElements {
1096
- 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1096
+ 'gs-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1097
1097
  }
1098
1098
  }
1099
1099
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.19.1",
3
+ "version": "0.19.2",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -1,6 +1,6 @@
1
1
  import { useCombobox } from 'downshift/preact';
2
2
  import { type ComponentChild } from 'preact';
3
- import { useMemo, useRef, useState } from 'preact/hooks';
3
+ import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
4
4
 
5
5
  import { DeleteIcon } from '../shared/icons/DeleteIcon';
6
6
 
@@ -15,7 +15,7 @@ export function DownshiftCombobox<Item>({
15
15
  inputClassName = '',
16
16
  }: {
17
17
  allItems: Item[];
18
- value?: Item | null;
18
+ value: Item | null;
19
19
  filterItemsByInputValue: (item: Item, value: string) => boolean;
20
20
  createEvent: (item: Item | null) => CustomEvent;
21
21
  itemToString: (item: Item | undefined | null) => string;
@@ -23,12 +23,25 @@ export function DownshiftCombobox<Item>({
23
23
  formatItemInList: (item: Item) => ComponentChild;
24
24
  inputClassName?: string;
25
25
  }) {
26
- const [itemsFilter, setItemsFilter] = useState(itemToString(value));
26
+ const [selectedItem, setSelectedItem] = useState<Item | null>(() => value);
27
+ const [itemsFilter, setItemsFilter] = useState(() => itemToString(selectedItem));
28
+
29
+ useEffect(() => {
30
+ setSelectedItem(value);
31
+ setItemsFilter(itemToString(value));
32
+ }, [itemToString, value]);
33
+
27
34
  const items = useMemo(
28
35
  () => allItems.filter((item) => filterItemsByInputValue(item, itemsFilter)),
29
36
  [allItems, filterItemsByInputValue, itemsFilter],
30
37
  );
31
38
  const divRef = useRef<HTMLDivElement>(null);
39
+ const [inputIsInvalid, setInputIsInvalid] = useState(false);
40
+
41
+ const selectItem = (item: Item | null) => {
42
+ setSelectedItem(item);
43
+ divRef.current?.dispatchEvent(createEvent(item));
44
+ };
32
45
 
33
46
  const shadowRoot = divRef.current?.shadowRoot ?? undefined;
34
47
 
@@ -49,39 +62,41 @@ export function DownshiftCombobox<Item>({
49
62
  getInputProps,
50
63
  highlightedIndex,
51
64
  getItemProps,
52
- selectedItem,
53
65
  inputValue,
54
- selectItem,
55
- setInputValue,
56
66
  closeMenu,
57
67
  } = useCombobox({
58
68
  onInputValueChange({ inputValue }) {
59
- setItemsFilter(inputValue);
69
+ setInputIsInvalid(false);
70
+ setItemsFilter(inputValue.trim());
60
71
  },
61
72
  onSelectedItemChange({ selectedItem }) {
62
- if (selectedItem !== null) {
63
- divRef.current?.dispatchEvent(createEvent(selectedItem));
64
- }
73
+ selectItem(selectedItem);
65
74
  },
66
75
  items,
67
76
  itemToString(item) {
68
77
  return itemToString(item);
69
78
  },
70
- selectedItem: value,
79
+ selectedItem,
71
80
  environment,
72
81
  });
73
82
 
74
83
  const onInputBlur = () => {
75
84
  if (inputValue === '') {
76
- divRef.current?.dispatchEvent(createEvent(null));
77
85
  selectItem(null);
78
- } else if (inputValue !== itemToString(selectedItem)) {
79
- setInputValue(itemToString(selectedItem) || '');
86
+ return;
87
+ }
88
+
89
+ const trimmedInput = inputValue.trim();
90
+ const matchingItem = items.find((item) => itemToString(item) === trimmedInput);
91
+ if (matchingItem !== undefined) {
92
+ selectItem(matchingItem);
93
+ return;
80
94
  }
95
+
96
+ setInputIsInvalid(true);
81
97
  };
82
98
 
83
99
  const clearInput = () => {
84
- divRef.current?.dispatchEvent(createEvent(null));
85
100
  selectItem(null);
86
101
  };
87
102
 
@@ -91,7 +106,7 @@ export function DownshiftCombobox<Item>({
91
106
  <div ref={divRef} className={'relative w-full'}>
92
107
  <div className='w-full flex flex-col gap-1'>
93
108
  <div
94
- className={`flex gap-0.5 input min-w-32 w-full ${inputClassName}`}
109
+ className={`flex gap-0.5 input min-w-32 w-full ${inputClassName} ${inputIsInvalid ? 'input-error' : ''}`}
95
110
  onBlur={(event) => {
96
111
  if (event.relatedTarget != buttonRef.current) {
97
112
  closeMenu();
@@ -77,16 +77,16 @@ const LineageSelector = ({
77
77
  allItems={data}
78
78
  value={value}
79
79
  filterItemsByInputValue={filterByInputValue}
80
- createEvent={(item: string | null) => new LineageFilterChangedEvent({ [lapisField]: item ?? undefined })}
81
- itemToString={(item: string | undefined | null) => item ?? ''}
80
+ createEvent={(item) => new LineageFilterChangedEvent({ [lapisField]: item ?? undefined })}
81
+ itemToString={(item) => item ?? ''}
82
82
  placeholderText={placeholderText}
83
83
  formatItemInList={(item: string) => item}
84
84
  />
85
85
  );
86
86
  };
87
87
 
88
- function filterByInputValue(item: string, inputValue: string | undefined | null) {
89
- if (inputValue === undefined || inputValue === null || inputValue === '') {
88
+ function filterByInputValue(item: string, inputValue: string | null) {
89
+ if (inputValue === null || inputValue === '') {
90
90
  return true;
91
91
  }
92
92
  return item?.toLowerCase().includes(inputValue?.toLowerCase() || '');
@@ -87,12 +87,10 @@ const LocationSelector = ({
87
87
  return (
88
88
  <DownshiftCombobox
89
89
  allItems={allItems}
90
- value={selectedItem}
90
+ value={selectedItem ?? null}
91
91
  filterItemsByInputValue={filterByInputValue}
92
- createEvent={(item: SelectItem | null) =>
93
- new LocationChangedEvent(item?.lapisFilter ?? emptyLocationFilter(fields))
94
- }
95
- itemToString={(item: SelectItem | undefined | null) => item?.label ?? ''}
92
+ createEvent={(item) => new LocationChangedEvent(item?.lapisFilter ?? emptyLocationFilter(fields))}
93
+ itemToString={(item) => item?.label ?? ''}
96
94
  placeholderText={placeholderText}
97
95
  formatItemInList={(item: SelectItem) => (
98
96
  <>
@@ -107,8 +105,8 @@ const LocationSelector = ({
107
105
  );
108
106
  };
109
107
 
110
- function filterByInputValue(item: SelectItem, inputValue: string | undefined | null) {
111
- if (inputValue === undefined || inputValue === null) {
108
+ function filterByInputValue(item: SelectItem, inputValue: string | null) {
109
+ if (inputValue === null) {
112
110
  return true;
113
111
  }
114
112
  return (
@@ -1,5 +1,5 @@
1
1
  import { type Meta, type StoryObj } from '@storybook/preact';
2
- import { expect, fireEvent, fn, waitFor, within } from '@storybook/test';
2
+ import { expect, fireEvent, fn, userEvent, waitFor, within } from '@storybook/test';
3
3
 
4
4
  import data from './__mockData__/aggregated_hosts.json';
5
5
  import { TextFilter, type TextFilterProps } from './text-filter';
@@ -104,13 +104,73 @@ export const RemoveInitialValue: StoryObj<TextFilterProps> = {
104
104
  await step('Remove initial value', async () => {
105
105
  await fireEvent.click(canvas.getByRole('button', { name: 'clear selection' }));
106
106
 
107
- await expect(changedListenerMock).toHaveBeenCalledWith(
108
- expect.objectContaining({
109
- detail: {
110
- host: undefined,
111
- },
112
- }),
113
- );
107
+ await waitFor(async () => {
108
+ await expect(changedListenerMock).toHaveBeenCalledWith(
109
+ expect.objectContaining({
110
+ detail: {
111
+ host: undefined,
112
+ },
113
+ }),
114
+ );
115
+ });
116
+ });
117
+ },
118
+ };
119
+
120
+ export const KeepsPartialInputInInputField: StoryObj<TextFilterProps> = {
121
+ ...Default,
122
+ render: (args) => (
123
+ <>
124
+ <button data-testid='focusHelper'>Focus helper</button>
125
+ <LapisUrlContextProvider value={LAPIS_URL}>
126
+ <TextFilter {...args} />
127
+ </LapisUrlContextProvider>
128
+ </>
129
+ ),
130
+ args: {
131
+ ...Default.args,
132
+ value: '',
133
+ },
134
+ play: async ({ canvasElement, step }) => {
135
+ const canvas = within(canvasElement);
136
+
137
+ const changedListenerMock = fn();
138
+ await step('Setup event listener mock', () => {
139
+ canvasElement.addEventListener('gs-text-filter-changed', changedListenerMock);
140
+ });
141
+ const inputField = () => canvas.getByPlaceholderText('Enter a host name', { exact: false });
142
+ async function typeAndBlur(input: string) {
143
+ await userEvent.click(inputField());
144
+ await userEvent.type(inputField(), input);
145
+ await userEvent.click(canvas.getByTestId('focusHelper'));
146
+ }
147
+
148
+ await waitFor(async () => {
149
+ await expect(inputField()).toHaveValue('');
150
+ });
151
+
152
+ await step('Type a value that it not valid yet and focus out', async () => {
153
+ await typeAndBlur('Homo sapi');
154
+ await waitFor(async () => {
155
+ await expect(inputField()).toHaveValue('Homo sapi');
156
+ });
157
+ await expect(changedListenerMock).not.toHaveBeenCalled();
158
+ });
159
+
160
+ await step('Complete the input value and expect an event', async () => {
161
+ await typeAndBlur('ens');
162
+ await waitFor(async () => {
163
+ await expect(inputField()).toHaveValue('Homo sapiens');
164
+ });
165
+ await waitFor(async () => {
166
+ await expect(changedListenerMock).toHaveBeenLastCalledWith(
167
+ expect.objectContaining({
168
+ detail: {
169
+ host: 'Homo sapiens',
170
+ },
171
+ }),
172
+ );
173
+ });
114
174
  });
115
175
  },
116
176
  };
@@ -85,12 +85,10 @@ const TextSelector = ({
85
85
  return (
86
86
  <DownshiftCombobox
87
87
  allItems={data}
88
- value={initialSelectedItem}
88
+ value={initialSelectedItem ?? null}
89
89
  filterItemsByInputValue={filterByInputValue}
90
- createEvent={(item: SelectItem | null) =>
91
- new TextFilterChangedEvent({ [lapisField]: item?.value ?? undefined })
92
- }
93
- itemToString={(item: SelectItem | undefined | null) => item?.value ?? ''}
90
+ createEvent={(item) => new TextFilterChangedEvent({ [lapisField]: item?.value ?? undefined })}
91
+ itemToString={(item) => item?.value ?? ''}
94
92
  placeholderText={placeholderText}
95
93
  formatItemInList={(item: SelectItem) => {
96
94
  return (
@@ -104,8 +102,8 @@ const TextSelector = ({
104
102
  );
105
103
  };
106
104
 
107
- function filterByInputValue(item: SelectItem, inputValue: string | undefined | null) {
108
- if (inputValue === undefined || inputValue === null || inputValue === '') {
105
+ function filterByInputValue(item: SelectItem, inputValue: string | null) {
106
+ if (inputValue === null || inputValue === '') {
109
107
  return true;
110
108
  }
111
109
  return item.value?.toLowerCase().includes(inputValue?.toLowerCase() || '');
@@ -1,4 +1,4 @@
1
- import { expect, fireEvent, fn, userEvent, waitFor } from '@storybook/test';
1
+ import { expect, fn, userEvent, waitFor } from '@storybook/test';
2
2
  import type { Meta, StoryObj } from '@storybook/web-components';
3
3
  import { html } from 'lit';
4
4
 
@@ -152,9 +152,9 @@ export const FiresEvents: StoryObj<Required<TextFilterProps>> = {
152
152
  });
153
153
 
154
154
  await step('Remove initial value', async () => {
155
- await fireEvent.click(canvas.getByRole('button', { name: 'clear selection' }));
155
+ await userEvent.click(canvas.getByRole('button', { name: 'clear selection' }));
156
156
 
157
- await expect(listenerMock).toHaveBeenCalledWith(
157
+ await expect(listenerMock).toHaveBeenLastCalledWith(
158
158
  expect.objectContaining({
159
159
  detail: {
160
160
  host: undefined,
@@ -162,13 +162,6 @@ export const FiresEvents: StoryObj<Required<TextFilterProps>> = {
162
162
  }),
163
163
  );
164
164
  });
165
-
166
- await step('Empty input', async () => {
167
- inputField().blur();
168
- await expect(listenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
169
- host: undefined,
170
- });
171
- });
172
165
  },
173
166
  args: {
174
167
  ...Default.args,