@genspectrum/dashboard-components 0.12.0 → 0.12.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.
package/dist/util.d.ts CHANGED
@@ -824,10 +824,7 @@ declare global {
824
824
 
825
825
  declare global {
826
826
  interface HTMLElementTagNameMap {
827
- 'gs-location-filter': LocationFilterComponent;
828
- }
829
- interface HTMLElementEventMap {
830
- 'gs-location-changed': LocationChangedEvent;
827
+ 'gs-mutation-comparison-component': MutationComparisonComponent;
831
828
  }
832
829
  }
833
830
 
@@ -835,7 +832,7 @@ declare global {
835
832
  declare global {
836
833
  namespace JSX {
837
834
  interface IntrinsicElements {
838
- 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
835
+ 'gs-mutation-comparison-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
839
836
  }
840
837
  }
841
838
  }
@@ -843,11 +840,7 @@ declare global {
843
840
 
844
841
  declare global {
845
842
  interface HTMLElementTagNameMap {
846
- 'gs-date-range-selector': DateRangeSelectorComponent;
847
- }
848
- interface HTMLElementEventMap {
849
- 'gs-date-range-filter-changed': CustomEvent<Record<string, string>>;
850
- 'gs-date-range-option-changed': DateRangeOptionChangedEvent;
843
+ 'gs-mutations-component': MutationsComponent;
851
844
  }
852
845
  }
853
846
 
@@ -855,7 +848,7 @@ declare global {
855
848
  declare global {
856
849
  namespace JSX {
857
850
  interface IntrinsicElements {
858
- 'gs-date-range-selector': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
851
+ 'gs-mutations-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
859
852
  }
860
853
  }
861
854
  }
@@ -863,10 +856,7 @@ declare global {
863
856
 
864
857
  declare global {
865
858
  interface HTMLElementTagNameMap {
866
- 'gs-text-input': TextInputComponent;
867
- }
868
- interface HTMLElementEventMap {
869
- 'gs-text-input-changed': CustomEvent<Record<string, string>>;
859
+ 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
870
860
  }
871
861
  }
872
862
 
@@ -874,7 +864,7 @@ declare global {
874
864
  declare global {
875
865
  namespace JSX {
876
866
  interface IntrinsicElements {
877
- 'gs-text-input': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
867
+ 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
878
868
  }
879
869
  }
880
870
  }
@@ -882,10 +872,7 @@ declare global {
882
872
 
883
873
  declare global {
884
874
  interface HTMLElementTagNameMap {
885
- 'gs-mutation-filter': MutationFilterComponent;
886
- }
887
- interface HTMLElementEventMap {
888
- 'gs-mutation-filter-changed': CustomEvent<MutationsFilter>;
875
+ 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
889
876
  }
890
877
  }
891
878
 
@@ -893,7 +880,7 @@ declare global {
893
880
  declare global {
894
881
  namespace JSX {
895
882
  interface IntrinsicElements {
896
- 'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
883
+ 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
897
884
  }
898
885
  }
899
886
  }
@@ -901,10 +888,7 @@ declare global {
901
888
 
902
889
  declare global {
903
890
  interface HTMLElementTagNameMap {
904
- 'gs-lineage-filter': LineageFilterComponent;
905
- }
906
- interface HTMLElementEventMap {
907
- 'gs-lineage-filter-changed': CustomEvent<Record<string, string>>;
891
+ 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
908
892
  }
909
893
  }
910
894
 
@@ -912,7 +896,7 @@ declare global {
912
896
  declare global {
913
897
  namespace JSX {
914
898
  interface IntrinsicElements {
915
- 'gs-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
899
+ 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
916
900
  }
917
901
  }
918
902
  }
@@ -920,7 +904,7 @@ declare global {
920
904
 
921
905
  declare global {
922
906
  interface HTMLElementTagNameMap {
923
- 'gs-mutation-comparison-component': MutationComparisonComponent;
907
+ 'gs-aggregate': AggregateComponent;
924
908
  }
925
909
  }
926
910
 
@@ -928,7 +912,7 @@ declare global {
928
912
  declare global {
929
913
  namespace JSX {
930
914
  interface IntrinsicElements {
931
- 'gs-mutation-comparison-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
915
+ 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
932
916
  }
933
917
  }
934
918
  }
@@ -936,7 +920,7 @@ declare global {
936
920
 
937
921
  declare global {
938
922
  interface HTMLElementTagNameMap {
939
- 'gs-mutations-component': MutationsComponent;
923
+ 'gs-mutations-over-time': MutationsOverTimeComponent;
940
924
  }
941
925
  }
942
926
 
@@ -944,7 +928,7 @@ declare global {
944
928
  declare global {
945
929
  namespace JSX {
946
930
  interface IntrinsicElements {
947
- 'gs-mutations-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
931
+ 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
948
932
  }
949
933
  }
950
934
  }
@@ -952,7 +936,7 @@ declare global {
952
936
 
953
937
  declare global {
954
938
  interface HTMLElementTagNameMap {
955
- 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
939
+ 'gs-sequences-by-location': SequencesByLocationComponent;
956
940
  }
957
941
  }
958
942
 
@@ -960,7 +944,7 @@ declare global {
960
944
  declare global {
961
945
  namespace JSX {
962
946
  interface IntrinsicElements {
963
- 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
947
+ 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
964
948
  }
965
949
  }
966
950
  }
@@ -968,7 +952,7 @@ declare global {
968
952
 
969
953
  declare global {
970
954
  interface HTMLElementTagNameMap {
971
- 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
955
+ 'gs-statistics': StatisticsComponent;
972
956
  }
973
957
  }
974
958
 
@@ -976,7 +960,7 @@ declare global {
976
960
  declare global {
977
961
  namespace JSX {
978
962
  interface IntrinsicElements {
979
- 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
963
+ 'gs-statistics': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
980
964
  }
981
965
  }
982
966
  }
@@ -984,7 +968,11 @@ declare global {
984
968
 
985
969
  declare global {
986
970
  interface HTMLElementTagNameMap {
987
- 'gs-aggregate': AggregateComponent;
971
+ 'gs-date-range-selector': DateRangeSelectorComponent;
972
+ }
973
+ interface HTMLElementEventMap {
974
+ 'gs-date-range-filter-changed': CustomEvent<Record<string, string>>;
975
+ 'gs-date-range-option-changed': DateRangeOptionChangedEvent;
988
976
  }
989
977
  }
990
978
 
@@ -992,7 +980,7 @@ declare global {
992
980
  declare global {
993
981
  namespace JSX {
994
982
  interface IntrinsicElements {
995
- 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
983
+ 'gs-date-range-selector': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
996
984
  }
997
985
  }
998
986
  }
@@ -1000,7 +988,10 @@ declare global {
1000
988
 
1001
989
  declare global {
1002
990
  interface HTMLElementTagNameMap {
1003
- 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
991
+ 'gs-location-filter': LocationFilterComponent;
992
+ }
993
+ interface HTMLElementEventMap {
994
+ 'gs-location-changed': LocationChangedEvent;
1004
995
  }
1005
996
  }
1006
997
 
@@ -1008,7 +999,7 @@ declare global {
1008
999
  declare global {
1009
1000
  namespace JSX {
1010
1001
  interface IntrinsicElements {
1011
- 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1002
+ 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1012
1003
  }
1013
1004
  }
1014
1005
  }
@@ -1016,7 +1007,10 @@ declare global {
1016
1007
 
1017
1008
  declare global {
1018
1009
  interface HTMLElementTagNameMap {
1019
- 'gs-mutations-over-time': MutationsOverTimeComponent;
1010
+ 'gs-mutation-filter': MutationFilterComponent;
1011
+ }
1012
+ interface HTMLElementEventMap {
1013
+ 'gs-mutation-filter-changed': CustomEvent<MutationsFilter>;
1020
1014
  }
1021
1015
  }
1022
1016
 
@@ -1024,7 +1018,7 @@ declare global {
1024
1018
  declare global {
1025
1019
  namespace JSX {
1026
1020
  interface IntrinsicElements {
1027
- 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1021
+ 'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1028
1022
  }
1029
1023
  }
1030
1024
  }
@@ -1032,7 +1026,10 @@ declare global {
1032
1026
 
1033
1027
  declare global {
1034
1028
  interface HTMLElementTagNameMap {
1035
- 'gs-sequences-by-location': SequencesByLocationComponent;
1029
+ 'gs-lineage-filter': LineageFilterComponent;
1030
+ }
1031
+ interface HTMLElementEventMap {
1032
+ 'gs-lineage-filter-changed': CustomEvent<Record<string, string>>;
1036
1033
  }
1037
1034
  }
1038
1035
 
@@ -1040,7 +1037,7 @@ declare global {
1040
1037
  declare global {
1041
1038
  namespace JSX {
1042
1039
  interface IntrinsicElements {
1043
- 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1040
+ 'gs-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1044
1041
  }
1045
1042
  }
1046
1043
  }
@@ -1048,7 +1045,10 @@ declare global {
1048
1045
 
1049
1046
  declare global {
1050
1047
  interface HTMLElementTagNameMap {
1051
- 'gs-statistics': StatisticsComponent;
1048
+ 'gs-text-input': TextInputComponent;
1049
+ }
1050
+ interface HTMLElementEventMap {
1051
+ 'gs-text-input-changed': CustomEvent<Record<string, string>>;
1052
1052
  }
1053
1053
  }
1054
1054
 
@@ -1056,7 +1056,7 @@ declare global {
1056
1056
  declare global {
1057
1057
  namespace JSX {
1058
1058
  interface IntrinsicElements {
1059
- 'gs-statistics': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1059
+ 'gs-text-input': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1060
1060
  }
1061
1061
  }
1062
1062
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -1,5 +1,5 @@
1
1
  import type { Feature, Geometry, GeometryObject } from 'geojson';
2
- import Leaflet, { type Layer, type LayerGroup } from 'leaflet';
2
+ import { geoJson, type Layer, type LayerGroup, map } from 'leaflet';
3
3
  import type { FunctionComponent } from 'preact';
4
4
  import { useEffect, useRef } from 'preact/hooks';
5
5
 
@@ -42,7 +42,7 @@ export const SequencesByLocationMap: FunctionComponent<SequencesByLocationMapPro
42
42
  return;
43
43
  }
44
44
 
45
- const leafletMap = Leaflet.map(ref.current, {
45
+ const leafletMap = map(ref.current, {
46
46
  scrollWheelZoom: enableMapNavigation,
47
47
  zoomControl: enableMapNavigation,
48
48
  keyboard: enableMapNavigation,
@@ -52,7 +52,7 @@ export const SequencesByLocationMap: FunctionComponent<SequencesByLocationMapPro
52
52
  center: [offsetY, offsetX],
53
53
  });
54
54
 
55
- Leaflet.geoJson(locations, {
55
+ geoJson(locations, {
56
56
  style: (feature: Feature<GeometryObject, EnhancedGeoJsonFeatureProperties> | undefined) => ({
57
57
  fillColor: getColor(feature?.properties.data?.proportion),
58
58
  fillOpacity: 1,
@@ -0,0 +1,11 @@
1
+ type LapisTextFilter = Record<string, string | null | undefined>;
2
+
3
+ export class TextInputChangedEvent extends CustomEvent<LapisTextFilter> {
4
+ constructor(detail: LapisTextFilter) {
5
+ super('gs-text-input-changed', {
6
+ detail,
7
+ bubbles: true,
8
+ composed: true,
9
+ });
10
+ }
11
+ }
@@ -5,5 +5,5 @@ export async function fetchAutocompleteList(lapis: string, field: string, signal
5
5
 
6
6
  const data = (await fetchAggregatedOperator.evaluate(lapis, signal)).content;
7
7
 
8
- return data.map((item) => item[field]);
8
+ return data.map((item) => item[field]).sort();
9
9
  }
@@ -1,5 +1,5 @@
1
1
  import { type Meta, type StoryObj } from '@storybook/preact';
2
- import { expect, waitFor, within } from '@storybook/test';
2
+ import { expect, fireEvent, fn, waitFor, within } from '@storybook/test';
3
3
 
4
4
  import data from './__mockData__/aggregated_hosts.json';
5
5
  import { TextInput, type TextInputProps } from './text-input';
@@ -79,19 +79,36 @@ export const Default: StoryObj<TextInputProps> = {
79
79
  },
80
80
  };
81
81
 
82
- export const WithInitialValue: StoryObj<TextInputProps> = {
82
+ export const RemoveInitialValue: StoryObj<TextInputProps> = {
83
83
  ...Default,
84
84
  args: {
85
85
  ...Default.args,
86
86
  initialValue: 'Homo sapiens',
87
87
  },
88
- play: async ({ canvasElement }) => {
88
+ play: async ({ canvasElement, step }) => {
89
89
  const canvas = within(canvasElement);
90
90
 
91
+ const changedListenerMock = fn();
92
+ await step('Setup event listener mock', async () => {
93
+ canvasElement.addEventListener('gs-text-input-changed', changedListenerMock);
94
+ });
95
+
91
96
  await waitFor(() => {
92
97
  const input = canvas.getByPlaceholderText('Enter a host name', { exact: false });
93
98
  expect(input).toHaveValue('Homo sapiens');
94
99
  });
100
+
101
+ await step('Remove initial value', async () => {
102
+ await fireEvent.click(canvas.getByRole('button', { name: 'clear selection' }));
103
+
104
+ await expect(changedListenerMock).toHaveBeenCalledWith(
105
+ expect.objectContaining({
106
+ detail: {
107
+ host: undefined,
108
+ },
109
+ }),
110
+ );
111
+ });
95
112
  },
96
113
  };
97
114
 
@@ -1,9 +1,11 @@
1
+ import { useCombobox } from 'downshift/preact';
1
2
  import { type FunctionComponent } from 'preact';
2
- import { useContext, useRef } from 'preact/hooks';
3
+ import { useContext, useRef, useState } from 'preact/hooks';
3
4
  import z from 'zod';
4
5
 
5
6
  import { fetchAutocompleteList } from './fetchAutocompleteList';
6
7
  import { LapisUrlContext } from '../LapisUrlContext';
8
+ import { TextInputChangedEvent } from './TextInputChangedEvent';
7
9
  import { ErrorBoundary } from '../components/error-boundary';
8
10
  import { LoadingDisplay } from '../components/loading-display';
9
11
  import { NoDataDisplay } from '../components/no-data-display';
@@ -36,11 +38,9 @@ export const TextInput: FunctionComponent<TextInputProps> = (props) => {
36
38
  );
37
39
  };
38
40
 
39
- const TextInputInner: FunctionComponent<TextInputInnerProps> = ({ lapisField, placeholderText, initialValue }) => {
41
+ const TextInputInner: FunctionComponent<TextInputInnerProps> = ({ initialValue, lapisField, placeholderText }) => {
40
42
  const lapis = useContext(LapisUrlContext);
41
43
 
42
- const inputRef = useRef<HTMLInputElement>(null);
43
-
44
44
  const { data, error, isLoading } = useQuery(() => fetchAutocompleteList(lapis, lapisField), [lapisField, lapis]);
45
45
 
46
46
  if (isLoading) {
@@ -55,43 +55,146 @@ const TextInputInner: FunctionComponent<TextInputInnerProps> = ({ lapisField, pl
55
55
  return <NoDataDisplay />;
56
56
  }
57
57
 
58
- const onInput = () => {
59
- const value = inputRef.current?.value === '' ? undefined : inputRef.current?.value;
60
-
61
- if (isValidValue(value)) {
62
- inputRef.current?.dispatchEvent(
63
- new CustomEvent('gs-text-input-changed', {
64
- detail: { [lapisField]: value },
65
- bubbles: true,
66
- composed: true,
67
- }),
68
- );
69
- }
70
- };
58
+ return (
59
+ <TextSelector
60
+ lapisField={lapisField}
61
+ initialValue={initialValue}
62
+ placeholderText={placeholderText}
63
+ data={data}
64
+ />
65
+ );
66
+ };
71
67
 
72
- const isValidValue = (value: string | undefined) => {
73
- if (value === undefined) {
68
+ const TextSelector = ({
69
+ lapisField,
70
+ initialValue,
71
+ placeholderText,
72
+ data,
73
+ }: TextInputInnerProps & {
74
+ data: string[];
75
+ }) => {
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 === '') {
74
94
  return true;
75
95
  }
76
- return data.includes(value);
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
+ }
77
135
  };
78
136
 
137
+ const clearInput = () => {
138
+ divRef.current?.dispatchEvent(new TextInputChangedEvent({ [lapisField]: undefined }));
139
+ selectItem(null);
140
+ };
141
+
142
+ const buttonRef = useRef(null);
143
+
79
144
  return (
80
- <>
81
- <input
82
- type='text'
83
- class='input input-bordered w-full'
84
- placeholder={placeholderText ?? lapisField}
85
- onInput={onInput}
86
- ref={inputRef}
87
- list={lapisField}
88
- value={initialValue}
89
- />
90
- <datalist id={lapisField}>
91
- {data.map((item) => (
92
- <option value={item} key={item} />
93
- ))}
94
- </datalist>
95
- </>
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>
96
199
  );
97
200
  };
@@ -1,4 +1,4 @@
1
- import { expect, fn, userEvent, waitFor } from '@storybook/test';
1
+ import { expect, fireEvent, fn, userEvent, waitFor } from '@storybook/test';
2
2
  import type { Meta, StoryObj } from '@storybook/web-components';
3
3
  import { html } from 'lit';
4
4
 
@@ -97,7 +97,7 @@ export const Default: StoryObj<Required<TextInputProps>> = {
97
97
  },
98
98
  };
99
99
 
100
- export const FiresEvent: StoryObj<Required<TextInputProps>> = {
100
+ export const FiresEvents: StoryObj<Required<TextInputProps>> = {
101
101
  ...Default,
102
102
  play: async ({ canvasElement, step }) => {
103
103
  const canvas = await withinShadowRoot(canvasElement, 'gs-text-input');
@@ -121,22 +121,45 @@ export const FiresEvent: StoryObj<Required<TextInputProps>> = {
121
121
 
122
122
  await step('Empty input', async () => {
123
123
  await userEvent.type(inputField(), '{backspace>9/}');
124
- await expect(listenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
125
- host: undefined,
126
- });
127
124
  });
128
125
 
129
126
  await step('Enter a valid host name', async () => {
130
- await userEvent.type(inputField(), 'Homo');
127
+ await userEvent.type(inputField(), 'Homo s');
128
+
129
+ const dropdownOption = await canvas.findByText('Homo sapiens');
130
+ await userEvent.click(dropdownOption);
131
+ });
132
+
133
+ await step('Verify event is fired with correct detail', async () => {
134
+ await waitFor(() => {
135
+ expect(listenerMock).toHaveBeenCalledWith(
136
+ expect.objectContaining({
137
+ detail: {
138
+ host: 'Homo sapiens',
139
+ },
140
+ }),
141
+ );
142
+ });
143
+ });
144
+
145
+ await step('Remove initial value', async () => {
146
+ await fireEvent.click(canvas.getByRole('button', { name: 'clear selection' }));
131
147
 
132
148
  await expect(listenerMock).toHaveBeenCalledWith(
133
149
  expect.objectContaining({
134
150
  detail: {
135
- host: 'Homo',
151
+ host: undefined,
136
152
  },
137
153
  }),
138
154
  );
139
155
  });
156
+
157
+ await step('Empty input', async () => {
158
+ inputField().blur();
159
+ await expect(listenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
160
+ host: undefined,
161
+ });
162
+ });
140
163
  },
141
164
  args: {
142
165
  ...Default.args,