@genspectrum/dashboard-components 0.4.0 → 0.4.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.
@@ -1,33 +1,24 @@
1
1
  import { type FunctionComponent } from 'preact';
2
2
 
3
+ import { Dropdown } from './dropdown';
3
4
  import { ProportionSelector, type ProportionSelectorProps } from './proportion-selector';
4
5
 
5
- export interface ProportionSelectorDropdownProps extends ProportionSelectorProps {
6
- openDirection?: 'left' | 'right';
7
- }
6
+ export type ProportionSelectorDropdownProps = ProportionSelectorProps;
8
7
 
9
8
  export const ProportionSelectorDropdown: FunctionComponent<ProportionSelectorDropdownProps> = ({
10
9
  proportionInterval,
11
10
  setMinProportion,
12
11
  setMaxProportion,
13
- openDirection = 'right',
14
12
  }) => {
15
13
  const label = `${(proportionInterval.min * 100).toFixed(1)}% - ${(proportionInterval.max * 100).toFixed(1)}%`;
16
14
 
17
15
  return (
18
- <div class={`dropdown ${openDirection === 'left' ? 'dropdown-end' : ''}`}>
19
- <div tabIndex={0} role='button' class='btn btn-xs whitespace-nowrap'>
20
- Proportion {label}
21
- </div>
22
- <ul tabIndex={0} class='p-2 shadow menu dropdown-content z-[1] bg-base-100 rounded-box w-72'>
23
- <div class='mb-2 ml-2'>
24
- <ProportionSelector
25
- proportionInterval={proportionInterval}
26
- setMinProportion={setMinProportion}
27
- setMaxProportion={setMaxProportion}
28
- />
29
- </div>
30
- </ul>
31
- </div>
16
+ <Dropdown buttonTitle={`Proportion ${label}`} placement={'bottom-start'}>
17
+ <ProportionSelector
18
+ proportionInterval={proportionInterval}
19
+ setMinProportion={setMinProportion}
20
+ setMaxProportion={setMaxProportion}
21
+ />
22
+ </Dropdown>
32
23
  );
33
24
  };
@@ -17,11 +17,20 @@ const Tabs: FunctionComponent<ComponentTabsProps> = ({ tabs, toolbar }) => {
17
17
  const [heightOfTabs, setHeightOfTabs] = useState('3rem');
18
18
  const tabRef = useRef<HTMLDivElement>(null);
19
19
 
20
- useEffect(() => {
20
+ const updateHeightOfTabs = () => {
21
21
  if (tabRef.current) {
22
22
  const heightOfTabs = tabRef.current.getBoundingClientRect().height;
23
23
  setHeightOfTabs(`${heightOfTabs}px`);
24
24
  }
25
+ };
26
+
27
+ useEffect(() => {
28
+ updateHeightOfTabs();
29
+
30
+ window.addEventListener('resize', updateHeightOfTabs);
31
+ return () => {
32
+ window.removeEventListener('resize', updateHeightOfTabs);
33
+ };
25
34
  }, []);
26
35
 
27
36
  const tabElements = (
@@ -51,9 +60,9 @@ const Tabs: FunctionComponent<ComponentTabsProps> = ({ tabs, toolbar }) => {
51
60
 
52
61
  return (
53
62
  <div className='h-full w-full'>
54
- <div ref={tabRef} className='flex flex-row justify-between'>
63
+ <div ref={tabRef} className='flex flex-row justify-between flex-wrap'>
55
64
  {tabElements}
56
- {toolbar && <div className='py-2'>{toolbarElement}</div>}
65
+ {toolbar && <div className='py-2 flex flex-wrap gap-y-1'>{toolbarElement}</div>}
57
66
  </div>
58
67
  <div
59
68
  className={`p-2 border-2 border-gray-100 rounded-b-md rounded-tr-md ${activeTab === tabs[0].title ? '' : 'rounded-tl-md'}`}
@@ -179,11 +179,11 @@ export const DateRangeSelectorInner = <CustomLabel extends string>({
179
179
  };
180
180
 
181
181
  return (
182
- <div class='join w-full' ref={divRef}>
182
+ <div class='flex flex-wrap' ref={divRef}>
183
183
  <Select
184
184
  items={getSelectableOptions(customSelectOptions)}
185
185
  selected={selectedDateRange}
186
- selectStyle='select-bordered rounded-none join-item grow'
186
+ selectStyle='select-bordered rounded-none flex-grow w-40'
187
187
  onChange={(event: Event) => {
188
188
  event.preventDefault();
189
189
  const select = event.target as HTMLSelectElement;
@@ -191,22 +191,26 @@ export const DateRangeSelectorInner = <CustomLabel extends string>({
191
191
  onSelectChange(value as CustomLabel | PresetOptionValues);
192
192
  }}
193
193
  />
194
- <input
195
- class='input input-bordered rounded-none join-item grow'
196
- type='text'
197
- placeholder='Date from'
198
- ref={fromDatePickerRef}
199
- onChange={onChangeDateFrom}
200
- onBlur={onChangeDateFrom}
201
- />
202
- <input
203
- class='input input-bordered rounded-none join-item grow'
204
- type='text'
205
- placeholder='Date to'
206
- ref={toDatePickerRef}
207
- onChange={onChangeDateTo}
208
- onBlur={onChangeDateTo}
209
- />
194
+ <div className={'flex flex-wrap flex-grow'}>
195
+ <input
196
+ class='input input-bordered rounded-none flex-grow min-w-40'
197
+ type='text'
198
+ size={10}
199
+ placeholder='Date from'
200
+ ref={fromDatePickerRef}
201
+ onChange={onChangeDateFrom}
202
+ onBlur={onChangeDateFrom}
203
+ />
204
+ <input
205
+ class='input input-bordered rounded-none flex-grow min-w-40'
206
+ type='text'
207
+ size={10}
208
+ placeholder='Date to'
209
+ ref={toDatePickerRef}
210
+ onChange={onChangeDateTo}
211
+ onBlur={onChangeDateTo}
212
+ />
213
+ </div>
210
214
  </div>
211
215
  );
212
216
  };
@@ -51,11 +51,11 @@ export const LocationFilterInner = ({ initialValue, fields }: LocationFilterInne
51
51
  const onInput = (event: JSXInternal.TargetedInputEvent<HTMLInputElement>) => {
52
52
  const inputValue = event.currentTarget.value;
53
53
  setValue(inputValue);
54
- if (inputValue.trim() === value.trim()) {
54
+ if (inputValue.trim() === value.trim() && inputValue !== '') {
55
55
  return;
56
56
  }
57
57
  const eventDetail = parseLocation(inputValue, fields);
58
- if (hasMatchingEntry(data, eventDetail)) {
58
+ if (hasAllUndefined(eventDetail) || hasMatchingEntry(data, eventDetail)) {
59
59
  divRef.current?.dispatchEvent(
60
60
  new CustomEvent('gs-location-changed', {
61
61
  detail: eventDetail,
@@ -92,10 +92,17 @@ export const LocationFilterInner = ({ initialValue, fields }: LocationFilterInne
92
92
  };
93
93
 
94
94
  const parseLocation = (location: string, fields: string[]) => {
95
+ if (location === '') {
96
+ return fields.reduce((acc, field) => ({ ...acc, [field]: undefined }), {});
97
+ }
95
98
  const fieldValues = location.split('/').map((part) => part.trim());
99
+
96
100
  return fields.reduce((acc, field, i) => ({ ...acc, [field]: fieldValues[i] }), {});
97
101
  };
98
102
 
103
+ const hasAllUndefined = (obj: Record<string, string | undefined>) =>
104
+ Object.values(obj).every((value) => value === undefined);
105
+
99
106
  const hasMatchingEntry = (data: Record<string, string>[] | null, eventDetail: Record<string, string>) => {
100
107
  if (data === null) {
101
108
  return false;
@@ -167,7 +167,7 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
167
167
  setProportionInterval,
168
168
  }) => {
169
169
  return (
170
- <div class='flex flex-row'>
170
+ <>
171
171
  <ProportionSelectorDropdown
172
172
  proportionInterval={proportionInterval}
173
173
  setMinProportion={(min) => setProportionInterval((prev) => ({ ...prev, min }))}
@@ -184,6 +184,6 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
184
184
  filename='mutation_comparison.csv'
185
185
  />
186
186
  <Info height={'100px'}>Info for mutation comparison</Info>
187
- </div>
187
+ </>
188
188
  );
189
189
  };
@@ -168,7 +168,7 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
168
168
  setProportionInterval,
169
169
  }) => {
170
170
  return (
171
- <div class='flex flex-row'>
171
+ <>
172
172
  <SegmentSelector displayedSegments={displayedSegments} setDisplayedSegments={setDisplayedSegments} />
173
173
  {activeTab === 'Table' && (
174
174
  <MutationTypeSelector
@@ -182,7 +182,6 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
182
182
  proportionInterval={proportionInterval}
183
183
  setMinProportion={(min) => setProportionInterval((prev) => ({ ...prev, min }))}
184
184
  setMaxProportion={(max) => setProportionInterval((prev) => ({ ...prev, max }))}
185
- openDirection={'left'}
186
185
  />
187
186
  <CsvDownloadButton
188
187
  className='mx-1 btn btn-xs'
@@ -206,6 +205,6 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
206
205
  />
207
206
  )}
208
207
  <Info height={'100px'}>Info for mutations</Info>
209
- </div>
208
+ </>
210
209
  );
211
210
  };
@@ -208,7 +208,7 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
208
208
  granularity,
209
209
  }) => {
210
210
  return (
211
- <div class='flex'>
211
+ <>
212
212
  {activeTab !== 'Table' && (
213
213
  <ScalingSelector yAxisScaleType={yAxisScaleType} setYAxisScaleType={setYAxisScaleType} />
214
214
  )}
@@ -226,11 +226,11 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
226
226
  />
227
227
 
228
228
  <PrevalenceOverTimeInfo />
229
- </div>
229
+ </>
230
230
  );
231
231
  };
232
232
 
233
- const PrevalenceOverTimeInfo: FunctionComponent = ({}) => {
233
+ const PrevalenceOverTimeInfo: FunctionComponent = () => {
234
234
  return (
235
235
  <Info height={'100px'}>
236
236
  <InfoHeadline1>Prevalence over time</InfoHeadline1>
@@ -238,5 +238,3 @@ const PrevalenceOverTimeInfo: FunctionComponent = ({}) => {
238
238
  </Info>
239
239
  );
240
240
  };
241
-
242
- export default PrevalenceOverTime;
@@ -161,10 +161,10 @@ const RelativeGrowthAdvantageToolbar: FunctionComponent<RelativeGrowthAdvantageT
161
161
  generationTime,
162
162
  }) => {
163
163
  return (
164
- <div class='flex'>
164
+ <>
165
165
  <ScalingSelector yAxisScaleType={yAxisScaleType} setYAxisScaleType={setYAxisScaleType} />
166
166
  <RelativeGrowthAdvantageInfo generationTime={generationTime} />
167
- </div>
167
+ </>
168
168
  );
169
169
  };
170
170
 
@@ -0,0 +1,83 @@
1
+ import { autoUpdate, computePosition, type Middleware } from '@floating-ui/dom';
2
+ import type { Placement } from '@floating-ui/utils';
3
+ import { useEffect, useRef } from 'preact/hooks';
4
+ import type { MutableRefObject } from 'react';
5
+
6
+ export function useFloatingUi(
7
+ referenceRef: MutableRefObject<HTMLElement | null>,
8
+ floatingRef: MutableRefObject<HTMLElement | null>,
9
+ middleware?: Array<Middleware | null | undefined | false>,
10
+ placement?: Placement,
11
+ ) {
12
+ const cleanupRef = useRef<Function | null>(null);
13
+
14
+ useEffect(() => {
15
+ if (!referenceRef.current || !floatingRef.current) {
16
+ return;
17
+ }
18
+
19
+ const { current: reference } = referenceRef;
20
+ const { current: floating } = floatingRef;
21
+
22
+ const update = () => {
23
+ computePosition(reference, floating, {
24
+ placement,
25
+ middleware,
26
+ }).then(({ x, y }) => {
27
+ floating.style.left = `${x}px`;
28
+ floating.style.top = `${y}px`;
29
+ });
30
+ };
31
+
32
+ update();
33
+ cleanupRef.current = autoUpdate(reference, floating, update);
34
+
35
+ return () => {
36
+ if (cleanupRef.current) {
37
+ cleanupRef.current();
38
+ }
39
+ };
40
+ }, [placement, middleware, referenceRef, floatingRef]);
41
+ }
42
+
43
+ export function useCloseOnClickOutside(
44
+ floatingRef: MutableRefObject<HTMLElement | null>,
45
+ referenceRef: MutableRefObject<HTMLElement | null>,
46
+ setShowContent: (value: ((prevState: boolean) => boolean) | boolean) => void,
47
+ ) {
48
+ useEffect(() => {
49
+ const handleClickOutside = (event: MouseEvent) => {
50
+ const path = event.composedPath();
51
+ if (
52
+ floatingRef.current &&
53
+ !path.includes(floatingRef.current) &&
54
+ referenceRef.current &&
55
+ !path.includes(referenceRef.current)
56
+ ) {
57
+ setShowContent(false);
58
+ }
59
+ };
60
+
61
+ document.addEventListener('mousedown', handleClickOutside);
62
+
63
+ return () => {
64
+ document.removeEventListener('mousedown', handleClickOutside);
65
+ };
66
+ }, [floatingRef, referenceRef, setShowContent]);
67
+ }
68
+
69
+ export function useCloseOnEsc(setShowHelp: (value: ((prevState: boolean) => boolean) | boolean) => void) {
70
+ useEffect(() => {
71
+ const handleKeyDown = (event: KeyboardEvent) => {
72
+ if (event.key === 'Escape') {
73
+ setShowHelp(false);
74
+ }
75
+ };
76
+
77
+ document.addEventListener('keydown', handleKeyDown);
78
+
79
+ return () => {
80
+ document.removeEventListener('keydown', handleKeyDown);
81
+ };
82
+ }, [setShowHelp]);
83
+ }
@@ -56,7 +56,7 @@ export const TextInputInner: FunctionComponent<TextInputInnerProps> = ({
56
56
  }
57
57
 
58
58
  const onInput = () => {
59
- const value = inputRef.current?.value;
59
+ const value = inputRef.current?.value === '' ? undefined : inputRef.current?.value;
60
60
 
61
61
  if (isValidValue(value)) {
62
62
  inputRef.current?.dispatchEvent(
@@ -71,7 +71,7 @@ export const TextInputInner: FunctionComponent<TextInputInnerProps> = ({
71
71
 
72
72
  const isValidValue = (value: string | undefined) => {
73
73
  if (value === undefined) {
74
- return false;
74
+ return true;
75
75
  }
76
76
  return data.includes(value);
77
77
  };
@@ -185,7 +185,16 @@ export const FiresEvent: StoryObj<LocationFilterProps> = {
185
185
  await step('Input invalid location', async () => {
186
186
  await userEvent.type(inputField(), 'Not / A / Location');
187
187
  await expect(listenerMock).not.toHaveBeenCalled();
188
+ });
189
+
190
+ await step('Empty input', async () => {
188
191
  await userEvent.type(inputField(), '{backspace>18/}');
192
+ await expect(listenerMock.mock.calls.at(-1)[0].detail).toStrictEqual({
193
+ region: undefined,
194
+ country: undefined,
195
+ division: undefined,
196
+ location: undefined,
197
+ });
189
198
  });
190
199
 
191
200
  await step('Select Asia', async () => {
@@ -118,7 +118,13 @@ export const FiresEvent: StoryObj<Required<TextInputProps>> = {
118
118
  await step('Enters an invalid host name', async () => {
119
119
  await userEvent.type(inputField(), 'notInList');
120
120
  await expect(listenerMock).not.toHaveBeenCalled();
121
+ });
122
+
123
+ await step('Empty input', async () => {
121
124
  await userEvent.type(inputField(), '{backspace>9/}');
125
+ await expect(listenerMock.mock.calls.at(-1)[0].detail).toStrictEqual({
126
+ host: undefined,
127
+ });
122
128
  });
123
129
 
124
130
  await step('Enter a valid host name', async () => {
@@ -1,6 +1,6 @@
1
1
  import { customElement, property } from 'lit/decorators.js';
2
2
 
3
- import PrevalenceOverTime, { type PrevalenceOverTimeProps } from '../../preact/prevalenceOverTime/prevalence-over-time';
3
+ import { PrevalenceOverTime, type PrevalenceOverTimeProps } from '../../preact/prevalenceOverTime/prevalence-over-time';
4
4
  import { type Equals, type Expect } from '../../utils/typeAssertions';
5
5
  import { PreactLitAdapterWithGridJsStyles } from '../PreactLitAdapterWithGridJsStyles';
6
6