@genspectrum/dashboard-components 1.9.1 → 1.10.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.
package/dist/util.d.ts CHANGED
@@ -135,6 +135,7 @@ export declare const gsEventNames: {
135
135
  readonly dateRangeOptionChanged: "gs-date-range-option-changed";
136
136
  readonly mutationFilterChanged: "gs-mutation-filter-changed";
137
137
  readonly lineageFilterChanged: "gs-lineage-filter-changed";
138
+ readonly lineageFilterMultiChanged: "gs-lineage-filter-multi-changed";
138
139
  readonly locationChanged: "gs-location-changed";
139
140
  readonly textFilterChanged: "gs-text-filter-changed";
140
141
  readonly numberRangeFilterChanged: "gs-number-range-filter-changed";
@@ -939,22 +940,6 @@ declare global {
939
940
  }
940
941
 
941
942
 
942
- declare global {
943
- interface HTMLElementTagNameMap {
944
- 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
945
- }
946
- }
947
-
948
-
949
- declare global {
950
- namespace JSX {
951
- interface IntrinsicElements {
952
- 'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
953
- }
954
- }
955
- }
956
-
957
-
958
943
  declare global {
959
944
  interface HTMLElementTagNameMap {
960
945
  'gs-genome-data-viewer': GenomeDataViewerComponent;
@@ -989,7 +974,7 @@ declare global {
989
974
 
990
975
  declare global {
991
976
  interface HTMLElementTagNameMap {
992
- 'gs-mutations': MutationsComponent;
977
+ 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
993
978
  }
994
979
  }
995
980
 
@@ -997,7 +982,7 @@ declare global {
997
982
  declare global {
998
983
  namespace JSX {
999
984
  interface IntrinsicElements {
1000
- 'gs-mutations': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
985
+ 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1001
986
  }
1002
987
  }
1003
988
  }
@@ -1005,7 +990,7 @@ declare global {
1005
990
 
1006
991
  declare global {
1007
992
  interface HTMLElementTagNameMap {
1008
- 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
993
+ 'gs-mutations': MutationsComponent;
1009
994
  }
1010
995
  }
1011
996
 
@@ -1013,7 +998,7 @@ declare global {
1013
998
  declare global {
1014
999
  namespace JSX {
1015
1000
  interface IntrinsicElements {
1016
- 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1001
+ 'gs-mutations': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1017
1002
  }
1018
1003
  }
1019
1004
  }
@@ -1117,10 +1102,11 @@ declare global {
1117
1102
 
1118
1103
  declare global {
1119
1104
  interface HTMLElementTagNameMap {
1120
- 'gs-location-filter': LocationFilterComponent;
1105
+ 'gs-date-range-filter': DateRangeFilterComponent;
1121
1106
  }
1122
1107
  interface HTMLElementEventMap {
1123
- [gsEventNames.locationChanged]: LocationChangedEvent;
1108
+ [gsEventNames.dateRangeFilterChanged]: CustomEvent<Record<string, string>>;
1109
+ [gsEventNames.dateRangeOptionChanged]: DateRangeOptionChangedEvent;
1124
1110
  }
1125
1111
  }
1126
1112
 
@@ -1128,7 +1114,7 @@ declare global {
1128
1114
  declare global {
1129
1115
  namespace JSX {
1130
1116
  interface IntrinsicElements {
1131
- 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1117
+ 'gs-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1132
1118
  }
1133
1119
  }
1134
1120
  }
@@ -1136,11 +1122,10 @@ declare global {
1136
1122
 
1137
1123
  declare global {
1138
1124
  interface HTMLElementTagNameMap {
1139
- 'gs-date-range-filter': DateRangeFilterComponent;
1125
+ 'gs-location-filter': LocationFilterComponent;
1140
1126
  }
1141
1127
  interface HTMLElementEventMap {
1142
- [gsEventNames.dateRangeFilterChanged]: CustomEvent<Record<string, string>>;
1143
- [gsEventNames.dateRangeOptionChanged]: DateRangeOptionChangedEvent;
1128
+ [gsEventNames.locationChanged]: LocationChangedEvent;
1144
1129
  }
1145
1130
  }
1146
1131
 
@@ -1148,7 +1133,7 @@ declare global {
1148
1133
  declare global {
1149
1134
  namespace JSX {
1150
1135
  interface IntrinsicElements {
1151
- 'gs-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1136
+ 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1152
1137
  }
1153
1138
  }
1154
1139
  }
@@ -1198,6 +1183,7 @@ declare global {
1198
1183
  }
1199
1184
  interface HTMLElementEventMap {
1200
1185
  [gsEventNames.lineageFilterChanged]: LineageFilterChangedEvent;
1186
+ [gsEventNames.lineageFilterMultiChanged]: LineageMultiFilterChangedEvent;
1201
1187
  }
1202
1188
  }
1203
1189
 
@@ -1231,6 +1217,22 @@ declare global {
1231
1217
  }
1232
1218
 
1233
1219
 
1220
+ declare global {
1221
+ interface HTMLElementTagNameMap {
1222
+ 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
1223
+ }
1224
+ }
1225
+
1226
+
1227
+ declare global {
1228
+ namespace JSX {
1229
+ interface IntrinsicElements {
1230
+ 'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1231
+ }
1232
+ }
1233
+ }
1234
+
1235
+
1234
1236
  declare module 'chart.js' {
1235
1237
  interface CartesianScaleTypeRegistry {
1236
1238
  logit: {
package/dist/util.js CHANGED
@@ -1,4 +1,4 @@
1
- import { D, a, L, N, b, T, d, g, m, v } from "./NumberRangeFilterChangedEvent-BnPI-Asz.js";
1
+ import { D, a, L, N, b, T, d, g, m, v } from "./NumberRangeFilterChangedEvent-Cdtcp9YL.js";
2
2
  export {
3
3
  D as DateRangeOptionChangedEvent,
4
4
  a as LineageFilterChangedEvent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "1.9.1",
3
+ "version": "1.10.0",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -1,4 +1,4 @@
1
- import { useCombobox } from 'downshift/preact';
1
+ import { useCombobox, useMultipleSelection } from 'downshift/preact';
2
2
  import { type ComponentChild } from 'preact';
3
3
  import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
4
4
 
@@ -43,17 +43,7 @@ export function DownshiftCombobox<Item>({
43
43
  divRef.current?.dispatchEvent(createEvent(item));
44
44
  };
45
45
 
46
- const shadowRoot = divRef.current?.shadowRoot ?? undefined;
47
-
48
- const environment =
49
- shadowRoot !== undefined
50
- ? {
51
- addEventListener: window.addEventListener.bind(window),
52
- removeEventListener: window.removeEventListener.bind(window),
53
- document: shadowRoot.ownerDocument,
54
- Node: window.Node,
55
- }
56
- : undefined;
46
+ const environment = useShadowEnvironment(divRef);
57
47
 
58
48
  const {
59
49
  isOpen,
@@ -120,44 +110,284 @@ export function DownshiftCombobox<Item>({
120
110
  {...getInputProps()}
121
111
  onBlur={onInputBlur}
122
112
  />
123
- <button
124
- aria-label='clear selection'
125
- className={`px-2 ${inputValue === '' && 'hidden'}`}
126
- type='button'
127
- onClick={clearInput}
128
- tabIndex={-1}
129
- >
130
- <DeleteIcon />
131
- </button>
132
- <button
133
- aria-label='toggle menu'
134
- className='px-2'
135
- type='button'
136
- {...getToggleButtonProps()}
137
- ref={buttonRef}
138
- >
139
- {isOpen ? <>↑</> : <>↓</>}
140
- </button>
113
+ <ClearButton onClick={clearInput} isHidden={inputValue === ''} />
114
+ <ToggleButton isOpen={isOpen} buttonRef={buttonRef} getToggleButtonProps={getToggleButtonProps} />
141
115
  </div>
142
116
  </div>
143
- <ul
144
- className={`absolute bg-white mt-1 shadow-md max-h-80 overflow-scroll z-10 w-full min-w-32 ${isOpen ? '' : 'hidden'}`}
145
- {...getMenuProps()}
146
- >
147
- {items.length > 0 ? (
148
- items.map((item, index) => (
149
- <li
150
- className={`${highlightedIndex === index ? 'bg-blue-300' : ''} ${selectedItem !== null && itemToString(selectedItem) === itemToString(item) ? 'font-bold' : ''} py-2 px-3 shadow-xs`}
151
- key={itemToString(item)}
152
- {...getItemProps({ item, index })}
117
+ <DropdownMenu
118
+ isOpen={isOpen}
119
+ getMenuProps={getMenuProps}
120
+ items={items}
121
+ highlightedIndex={highlightedIndex}
122
+ getItemProps={getItemProps}
123
+ formatItemInList={formatItemInList}
124
+ itemToString={itemToString}
125
+ selectedItem={selectedItem}
126
+ emptyMessage='No elements to select.'
127
+ />
128
+ </div>
129
+ );
130
+ }
131
+
132
+ export function DownshiftMultiCombobox<Item>({
133
+ allItems,
134
+ value,
135
+ filterItemsByInputValue,
136
+ createEvent,
137
+ itemToString,
138
+ placeholderText,
139
+ formatItemInList,
140
+ formatSelectedItem,
141
+ inputClassName = '',
142
+ }: {
143
+ allItems: Item[];
144
+ value: Item[];
145
+ filterItemsByInputValue: (item: Item, value: string) => boolean;
146
+ createEvent: (items: Item[]) => CustomEvent;
147
+ itemToString: (item: Item | undefined | null) => string;
148
+ placeholderText?: string;
149
+ formatItemInList: (item: Item) => ComponentChild;
150
+ formatSelectedItem?: (item: Item) => ComponentChild;
151
+ inputClassName?: string;
152
+ }) {
153
+ const [selectedItems, setSelectedItems] = useState<Item[]>(() => value);
154
+ const [itemsFilter, setItemsFilter] = useState('');
155
+
156
+ useEffect(() => {
157
+ setSelectedItems(value);
158
+ }, [value]);
159
+
160
+ const availableItems = useMemo(() => {
161
+ return allItems.filter((item) => {
162
+ const notAlreadySelected = !selectedItems.find(
163
+ (selectedItem) => itemToString(selectedItem) === itemToString(item),
164
+ );
165
+ const matchesFilter = filterItemsByInputValue(item, itemsFilter);
166
+ return notAlreadySelected && matchesFilter;
167
+ });
168
+ }, [allItems, selectedItems, filterItemsByInputValue, itemsFilter, itemToString]);
169
+
170
+ const divRef = useRef<HTMLDivElement>(null);
171
+
172
+ const dispatchEvent = (items: Item[]) => {
173
+ setSelectedItems(items);
174
+ divRef.current?.dispatchEvent(createEvent(items));
175
+ };
176
+
177
+ const environment = useShadowEnvironment(divRef);
178
+
179
+ const { getDropdownProps, removeSelectedItem } = useMultipleSelection({
180
+ selectedItems,
181
+ onStateChange({ selectedItems: newSelectedItems, type }) {
182
+ switch (type) {
183
+ case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
184
+ dispatchEvent(newSelectedItems ?? []);
185
+ break;
186
+ default:
187
+ break;
188
+ }
189
+ },
190
+ environment,
191
+ });
192
+
193
+ const { isOpen, getToggleButtonProps, getMenuProps, getInputProps, highlightedIndex, getItemProps, closeMenu } =
194
+ useCombobox({
195
+ items: availableItems,
196
+ itemToString(item) {
197
+ return itemToString(item);
198
+ },
199
+ inputValue: itemsFilter,
200
+ onStateChange({ inputValue: newInputValue, type, selectedItem: newSelectedItem }) {
201
+ switch (type) {
202
+ case useCombobox.stateChangeTypes.InputKeyDownEnter:
203
+ case useCombobox.stateChangeTypes.ItemClick:
204
+ if (newSelectedItem) {
205
+ dispatchEvent([...selectedItems, newSelectedItem]);
206
+ setItemsFilter('');
207
+ }
208
+ break;
209
+ case useCombobox.stateChangeTypes.InputChange:
210
+ setItemsFilter(newInputValue?.trim() ?? '');
211
+ break;
212
+ default:
213
+ break;
214
+ }
215
+ },
216
+ stateReducer(state, actionAndChanges) {
217
+ const { changes, type } = actionAndChanges;
218
+ switch (type) {
219
+ case useCombobox.stateChangeTypes.InputKeyDownEnter:
220
+ case useCombobox.stateChangeTypes.ItemClick:
221
+ return {
222
+ ...changes,
223
+ isOpen: true,
224
+ highlightedIndex: state.highlightedIndex,
225
+ inputValue: '',
226
+ };
227
+ default:
228
+ return changes;
229
+ }
230
+ },
231
+ environment,
232
+ });
233
+
234
+ const clearAll = () => {
235
+ dispatchEvent([]);
236
+ setItemsFilter('');
237
+ };
238
+
239
+ const buttonRef = useRef(null);
240
+
241
+ return (
242
+ <div ref={divRef} className={'relative w-full'}>
243
+ <div className='w-full flex flex-col gap-1'>
244
+ <div
245
+ className={`flex gap-1 flex-wrap p-1.5 input min-w-24 h-fit w-full ${inputClassName}`}
246
+ onBlur={(event) => {
247
+ if (event.relatedTarget != buttonRef.current) {
248
+ closeMenu();
249
+ }
250
+ }}
251
+ >
252
+ {selectedItems.map((selectedItem, index) => (
253
+ <span
254
+ key={`${itemToString(selectedItem)}-${index}`}
255
+ className='inline-flex items-center gap-1 px-2 py-0.5 bg-blue-100 text-black rounded'
153
256
  >
154
- {formatItemInList(item)}
155
- </li>
156
- ))
157
- ) : (
158
- <li className='py-2 px-3 shadow-xs'>No elements to select.</li>
159
- )}
160
- </ul>
257
+ {formatSelectedItem ? formatSelectedItem(selectedItem) : itemToString(selectedItem)}
258
+ <button
259
+ aria-label={`remove ${itemToString(selectedItem)}`}
260
+ className='cursor-pointer hover:text-red-600'
261
+ type='button'
262
+ onClick={() => removeSelectedItem(selectedItem)}
263
+ tabIndex={-1}
264
+ >
265
+ ×
266
+ </button>
267
+ </span>
268
+ ))}
269
+ <div className='flex gap-0.5 grow min-w-32'>
270
+ <input
271
+ placeholder={placeholderText}
272
+ className='w-full px-1 py-0.5 focus:outline-none min-w-24'
273
+ {...getInputProps(getDropdownProps({ preventKeyAction: isOpen }))}
274
+ />
275
+ <ClearButton onClick={clearAll} isHidden={selectedItems.length === 0} />
276
+ <ToggleButton
277
+ isOpen={isOpen}
278
+ buttonRef={buttonRef}
279
+ getToggleButtonProps={getToggleButtonProps}
280
+ />
281
+ </div>
282
+ </div>
283
+ </div>
284
+ <DropdownMenu
285
+ isOpen={isOpen}
286
+ getMenuProps={getMenuProps}
287
+ items={availableItems}
288
+ highlightedIndex={highlightedIndex}
289
+ getItemProps={getItemProps}
290
+ formatItemInList={formatItemInList}
291
+ itemToString={itemToString}
292
+ selectedItem={selectedItems}
293
+ emptyMessage={selectedItems.length > 0 ? 'No more elements to select.' : 'No elements to select.'}
294
+ />
161
295
  </div>
162
296
  );
163
297
  }
298
+
299
+ function useShadowEnvironment(divRef: React.RefObject<HTMLDivElement>) {
300
+ const shadowRoot = divRef.current?.shadowRoot ?? undefined;
301
+
302
+ return shadowRoot !== undefined
303
+ ? {
304
+ addEventListener: window.addEventListener.bind(window),
305
+ removeEventListener: window.removeEventListener.bind(window),
306
+ document: shadowRoot.ownerDocument,
307
+ Node: window.Node,
308
+ }
309
+ : undefined;
310
+ }
311
+
312
+ function ToggleButton({
313
+ isOpen,
314
+ buttonRef,
315
+ getToggleButtonProps,
316
+ onClick,
317
+ }: {
318
+ isOpen: boolean;
319
+ buttonRef?: React.Ref<HTMLButtonElement>;
320
+ getToggleButtonProps?: () => Record<string, unknown>;
321
+ onClick?: () => void;
322
+ }) {
323
+ const props = getToggleButtonProps ? getToggleButtonProps() : { onClick };
324
+ return (
325
+ <button aria-label='toggle menu' className='px-2' type='button' {...props} ref={buttonRef}>
326
+ {isOpen ? <>↑</> : <>↓</>}
327
+ </button>
328
+ );
329
+ }
330
+
331
+ function ClearButton({ onClick, isHidden }: { onClick: () => void; isHidden: boolean }) {
332
+ return (
333
+ <button
334
+ aria-label='clear selection'
335
+ className={`px-2 ${isHidden ? 'hidden' : ''}`}
336
+ type='button'
337
+ onClick={onClick}
338
+ tabIndex={-1}
339
+ >
340
+ <DeleteIcon />
341
+ </button>
342
+ );
343
+ }
344
+
345
+ function DropdownMenu<Item>({
346
+ isOpen,
347
+ getMenuProps,
348
+ items,
349
+ highlightedIndex,
350
+ getItemProps,
351
+ formatItemInList,
352
+ itemToString,
353
+ selectedItem,
354
+ emptyMessage,
355
+ }: {
356
+ isOpen: boolean;
357
+ getMenuProps: () => Record<string, unknown>;
358
+ items: Item[];
359
+ highlightedIndex: number;
360
+ getItemProps: (options: { item: Item; index: number }) => Record<string, unknown>;
361
+ formatItemInList: (item: Item) => ComponentChild;
362
+ itemToString: (item: Item) => string;
363
+ selectedItem?: Item | Item[] | null;
364
+ emptyMessage: string;
365
+ }) {
366
+ const isItemSelected = (item: Item) => {
367
+ if (Array.isArray(selectedItem)) {
368
+ return selectedItem.some((selected) => itemToString(selected) === itemToString(item));
369
+ }
370
+ return selectedItem !== null && selectedItem !== undefined && itemToString(selectedItem) === itemToString(item);
371
+ };
372
+
373
+ return (
374
+ <ul
375
+ className={`absolute bg-white mt-1 shadow-md max-h-80 overflow-scroll z-10 w-full min-w-32 ${isOpen ? '' : 'hidden'}`}
376
+ {...getMenuProps()}
377
+ >
378
+ {items.length > 0 ? (
379
+ items.map((item, index) => (
380
+ <li
381
+ className={`${highlightedIndex === index ? 'bg-blue-300' : ''} ${isItemSelected(item) ? 'font-bold' : ''} py-2 px-3 shadow-xs cursor-pointer`}
382
+ key={itemToString(item)}
383
+ {...getItemProps({ item, index })}
384
+ >
385
+ {formatItemInList(item)}
386
+ </li>
387
+ ))
388
+ ) : (
389
+ <li className='py-2 px-3 shadow-xs'>{emptyMessage}</li>
390
+ )}
391
+ </ul>
392
+ );
393
+ }
@@ -7,6 +7,7 @@ import {
7
7
  MutationsOverTimeMutationsFilter,
8
8
  type MutationsOverTimeMutationsFilterProps,
9
9
  } from './mutations-over-time-mutations-filter';
10
+ import { type MutationAnnotations } from '../../web-components/mutation-annotations-context';
10
11
  import { MutationAnnotationsContextProvider } from '../MutationAnnotationsContext';
11
12
  import { type MutationFilter } from '../mutationsOverTime/getFilteredMutationsOverTimeData';
12
13
 
@@ -18,30 +19,38 @@ const meta: Meta = {
18
19
 
19
20
  export default meta;
20
21
 
22
+ const manyMutationAnnotations = Array.from({ length: 300 }, (_, i) => ({
23
+ name: `Annotation ${i + 1}`,
24
+ description: `This is test annotation number ${i + 1} for testing many annotations.`,
25
+ symbol: String.fromCharCode(33 + (i % 94)), // Cycle through printable ASCII characters
26
+ nucleotideMutations: ['A23G'],
27
+ aminoAcidMutations: [],
28
+ })) satisfies MutationAnnotations;
29
+
21
30
  const WrapperWithState = ({
22
31
  setFilterValue,
23
32
  value,
33
+ annotations = [
34
+ {
35
+ name: 'Test Annotation 1',
36
+ description: 'Test Annotation 1',
37
+ symbol: '#',
38
+ },
39
+ {
40
+ name: 'Test Annotation 2',
41
+ description: 'Test Annotation 2',
42
+ symbol: '+',
43
+ },
44
+ ],
24
45
  }: {
25
46
  setFilterValue: Dispatch<StateUpdater<MutationFilter>>;
26
47
  value: MutationFilter;
48
+ annotations?: MutationAnnotations;
27
49
  }) => {
28
50
  const [state, setState] = useState(value);
29
51
 
30
52
  return (
31
- <MutationAnnotationsContextProvider
32
- value={[
33
- {
34
- name: 'Test Annotation 1',
35
- description: 'Test Annotation 1',
36
- symbol: '#',
37
- },
38
- {
39
- name: 'Test Annotation 2',
40
- description: 'Test Annotation 2',
41
- symbol: '+',
42
- },
43
- ]}
44
- >
53
+ <MutationAnnotationsContextProvider value={annotations}>
45
54
  <MutationsOverTimeMutationsFilter
46
55
  setFilterValue={(value) => {
47
56
  setFilterValue(value);
@@ -107,3 +116,53 @@ export const FilterByAnnotation: StoryObj<MutationsOverTimeMutationsFilterProps>
107
116
  });
108
117
  },
109
118
  };
119
+
120
+ export const WithManyMutationAnnotations: StoryObj<MutationsOverTimeMutationsFilterProps> = {
121
+ render: (args) => {
122
+ return (
123
+ <WrapperWithState
124
+ setFilterValue={args.setFilterValue}
125
+ value={args.value}
126
+ annotations={manyMutationAnnotations}
127
+ />
128
+ );
129
+ },
130
+ args: {
131
+ setFilterValue: fn(),
132
+ value: { textFilter: '', annotationNameFilter: new Set() },
133
+ },
134
+ play: async ({ canvasElement, step }) => {
135
+ const canvas = within(canvasElement);
136
+
137
+ await step('Open filter dropdown', async () => {
138
+ const filterButton = canvas.getByRole('button', { name: 'Filter mutations' });
139
+ await userEvent.click(filterButton);
140
+ });
141
+
142
+ await step('Verify scroll container is scrollable', () => {
143
+ const scrollContainer = canvas
144
+ .getByText('Filter by annotations')
145
+ .parentElement!.querySelector('.overflow-scroll')!;
146
+ void expect(scrollContainer).toBeInTheDocument();
147
+
148
+ // Verify the container has scrollable content
149
+ void expect(scrollContainer.scrollHeight).toBeGreaterThan(scrollContainer.clientHeight);
150
+ });
151
+
152
+ await step('Scroll to bottom and verify we can scroll', async () => {
153
+ const scrollContainer = canvas
154
+ .getByText('Filter by annotations')
155
+ .parentElement!.querySelector('.overflow-scroll')!;
156
+
157
+ const initialScrollTop = scrollContainer.scrollTop;
158
+
159
+ // Scroll to the bottom
160
+ scrollContainer.scrollTop = scrollContainer.scrollHeight;
161
+
162
+ await waitFor(async () => {
163
+ // Verify that scrollTop actually changed
164
+ await expect(scrollContainer.scrollTop).toBeGreaterThan(initialScrollTop);
165
+ });
166
+ });
167
+ },
168
+ };
@@ -105,34 +105,36 @@ const AnnotationCheckboxes: FunctionComponent<MutationsOverTimeMutationsFilterPr
105
105
  <div className='divider mt-0.5 mb-0' />
106
106
  <div className='text-sm'>
107
107
  <div className='font-bold mb-1'>Filter by annotations</div>
108
- {mutationAnnotations.map((annotation, index) => (
109
- <li className='flex flex-row items-center' key={annotation.name}>
110
- <label>
111
- <input
112
- className={'mr-2'}
113
- type='checkbox'
114
- id={`item-${index}`}
115
- checked={value.annotationNameFilter.has(annotation.name)}
116
- onChange={() => {
117
- setFilterValue((previousFilter) => {
118
- const newAnnotationFilter = previousFilter.annotationNameFilter.has(
119
- annotation.name,
120
- )
121
- ? [...previousFilter.annotationNameFilter].filter(
122
- (name) => name !== annotation.name,
123
- )
124
- : [...previousFilter.annotationNameFilter, annotation.name];
125
- return {
126
- ...previousFilter,
127
- annotationNameFilter: new Set(newAnnotationFilter),
128
- };
129
- });
130
- }}
131
- />
132
- {annotation.name} (<span className='text-red-600'>{annotation.symbol}</span>)
133
- </label>
134
- </li>
135
- ))}
108
+ <div className='max-h-72 overflow-scroll'>
109
+ {mutationAnnotations.map((annotation, index) => (
110
+ <li className='flex flex-row items-center' key={annotation.name}>
111
+ <label>
112
+ <input
113
+ className={'mr-2'}
114
+ type='checkbox'
115
+ id={`item-${index}`}
116
+ checked={value.annotationNameFilter.has(annotation.name)}
117
+ onChange={() => {
118
+ setFilterValue((previousFilter) => {
119
+ const newAnnotationFilter = previousFilter.annotationNameFilter.has(
120
+ annotation.name,
121
+ )
122
+ ? [...previousFilter.annotationNameFilter].filter(
123
+ (name) => name !== annotation.name,
124
+ )
125
+ : [...previousFilter.annotationNameFilter, annotation.name];
126
+ return {
127
+ ...previousFilter,
128
+ annotationNameFilter: new Set(newAnnotationFilter),
129
+ };
130
+ });
131
+ }}
132
+ />
133
+ {annotation.name} (<span className='text-red-600'>{annotation.symbol}</span>)
134
+ </label>
135
+ </li>
136
+ ))}
137
+ </div>
136
138
  </div>
137
139
  </>
138
140
  );