@genspectrum/dashboard-components 0.16.4 → 0.17.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 (58) hide show
  1. package/custom-elements.json +130 -74
  2. package/dist/{LineageFilterChangedEvent-COWV-Y0k.js → LineageFilterChangedEvent-DkvWdq_G.js} +2 -2
  3. package/dist/LineageFilterChangedEvent-DkvWdq_G.js.map +1 -0
  4. package/dist/components.d.ts +64 -48
  5. package/dist/components.js +858 -242
  6. package/dist/components.js.map +1 -1
  7. package/dist/style.css +391 -12
  8. package/dist/util.d.ts +23 -25
  9. package/dist/util.js +1 -1
  10. package/package.json +2 -1
  11. package/src/preact/components/clearable-select.stories.tsx +75 -0
  12. package/src/preact/components/clearable-select.tsx +76 -0
  13. package/src/preact/components/downshift-combobox.tsx +9 -7
  14. package/src/preact/dateRangeFilter/computeInitialValues.spec.ts +31 -33
  15. package/src/preact/dateRangeFilter/computeInitialValues.ts +2 -15
  16. package/src/preact/dateRangeFilter/date-picker.tsx +66 -0
  17. package/src/preact/dateRangeFilter/date-range-filter.stories.tsx +69 -31
  18. package/src/preact/dateRangeFilter/date-range-filter.tsx +136 -139
  19. package/src/preact/dateRangeFilter/dateRangeOption.ts +11 -11
  20. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +133 -84
  21. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +46 -16
  22. package/src/preact/mutationsOverTime/mutations-over-time.tsx +3 -0
  23. package/src/preact/shared/WithClassName/WithClassName.ts +1 -0
  24. package/src/preact/shared/icons/DeleteIcon.tsx +3 -0
  25. package/src/preact/shared/stories/expectOptionSelected.tsx +7 -0
  26. package/src/preact/shared/tanstackTable/pagination.tsx +132 -0
  27. package/src/preact/shared/tanstackTable/tanstackTable.tsx +43 -0
  28. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +2 -1
  29. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +3 -5
  30. package/src/utilEntrypoint.ts +1 -1
  31. package/src/web-components/MutationAnnotations.mdx +33 -0
  32. package/src/web-components/ResizeContainer.mdx +1 -1
  33. package/src/web-components/errorHandling.mdx +1 -1
  34. package/src/web-components/gs-app.ts +2 -2
  35. package/src/web-components/input/gs-date-range-filter.stories.ts +38 -32
  36. package/src/web-components/input/gs-date-range-filter.tsx +8 -2
  37. package/src/web-components/input/gs-lineage-filter.tsx +1 -1
  38. package/src/web-components/input/gs-location-filter.tsx +1 -1
  39. package/src/web-components/input/gs-mutation-filter.tsx +1 -1
  40. package/src/web-components/input/gs-text-filter.tsx +1 -1
  41. package/src/web-components/visualization/gs-aggregate.tsx +2 -2
  42. package/src/web-components/visualization/gs-mutation-comparison.tsx +5 -2
  43. package/src/web-components/visualization/gs-mutations-over-time.spec-d.ts +39 -0
  44. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +4 -0
  45. package/src/web-components/visualization/gs-mutations-over-time.tsx +13 -33
  46. package/src/web-components/visualization/gs-mutations.tsx +5 -2
  47. package/src/web-components/visualization/gs-number-sequences-over-time.tsx +2 -2
  48. package/src/web-components/visualization/gs-prevalence-over-time.tsx +2 -2
  49. package/src/web-components/visualization/gs-relative-growth-advantage.tsx +2 -2
  50. package/src/web-components/visualization/gs-sequences-by-location.tsx +2 -2
  51. package/src/web-components/visualization/gs-statistics.tsx +2 -2
  52. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.spec-d.ts +24 -0
  53. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.stories.ts +3 -3
  54. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +7 -38
  55. package/standalone-bundle/dashboard-components.js +18384 -16486
  56. package/standalone-bundle/dashboard-components.js.map +1 -1
  57. package/standalone-bundle/style.css +1 -1
  58. package/dist/LineageFilterChangedEvent-COWV-Y0k.js.map +0 -1
@@ -8,8 +8,9 @@ import { DateRangeFilter, type DateRangeFilterProps } from './date-range-filter'
8
8
  import { previewHandles } from '../../../.storybook/preview';
9
9
  import { LAPIS_URL } from '../../constants';
10
10
  import { LapisUrlContextProvider } from '../LapisUrlContext';
11
- import { dateRangeOptionPresets } from './dateRangeOption';
11
+ import { dateRangeOptionPresets, type DateRangeValue } from './dateRangeOption';
12
12
  import { expectInvalidAttributesErrorMessage } from '../shared/stories/expectErrorMessage';
13
+ import { expectOptionSelected } from '../shared/stories/expectOptionSelected';
13
14
 
14
15
  const earliestDate = '1970-01-01';
15
16
 
@@ -19,6 +20,8 @@ const customDateRange = {
19
20
  dateTo: '2021-12-31',
20
21
  };
21
22
 
23
+ const placeholder = 'Date range';
24
+
22
25
  const meta: Meta<DateRangeFilterProps> = {
23
26
  title: 'Input/DateRangeFilter',
24
27
  component: DateRangeFilter,
@@ -56,6 +59,7 @@ const meta: Meta<DateRangeFilterProps> = {
56
59
  value: undefined,
57
60
  lapisDateField: 'aDateColumn',
58
61
  width: '100%',
62
+ placeholder,
59
63
  },
60
64
  };
61
65
 
@@ -69,6 +73,22 @@ const Primary: StoryObj<DateRangeFilterProps> = {
69
73
  ),
70
74
  };
71
75
 
76
+ export const WithUndefinedValue: StoryObj<DateRangeFilterProps> = {
77
+ ...Primary,
78
+ args: {
79
+ ...Primary.args,
80
+ },
81
+ play: async ({ canvasElement }) => {
82
+ const canvas = within(canvasElement);
83
+
84
+ await waitFor(async () => {
85
+ await expectOptionSelected(canvasElement, placeholder);
86
+ await expect(dateFromPicker(canvas)).toHaveValue('');
87
+ await expect(dateToPicker(canvas)).toHaveValue('');
88
+ });
89
+ },
90
+ };
91
+
72
92
  export const SetCorrectInitialValues: StoryObj<DateRangeFilterProps> = {
73
93
  ...Primary,
74
94
  args: {
@@ -79,6 +99,7 @@ export const SetCorrectInitialValues: StoryObj<DateRangeFilterProps> = {
79
99
  const canvas = within(canvasElement);
80
100
 
81
101
  await waitFor(async () => {
102
+ await expectOptionSelected(canvasElement, 'CustomDateRange');
82
103
  await expect(selectField(canvas)).toHaveValue('CustomDateRange');
83
104
  await expect(dateFromPicker(canvas)).toHaveValue('2021-01-01');
84
105
  await expect(dateToPicker(canvas)).toHaveValue('2021-12-31');
@@ -98,7 +119,7 @@ export const SetCorrectInitialDateFrom: StoryObj<DateRangeFilterProps> = {
98
119
  const canvas = within(canvasElement);
99
120
 
100
121
  await waitFor(async () => {
101
- await expect(selectField(canvas)).toHaveValue('Custom');
122
+ await expectOptionSelected(canvasElement, 'Custom');
102
123
  await expect(dateFromPicker(canvas)).toHaveValue(initialDateFrom);
103
124
  await expect(dateToPicker(canvas)).toHaveValue(dayjs().format('YYYY-MM-DD'));
104
125
  });
@@ -117,14 +138,14 @@ export const SetCorrectInitialDateTo: StoryObj<DateRangeFilterProps> = {
117
138
  const canvas = within(canvasElement);
118
139
 
119
140
  await waitFor(async () => {
120
- await expect(selectField(canvas)).toHaveValue('Custom');
141
+ await expectOptionSelected(canvasElement, 'Custom');
121
142
  await expect(dateFromPicker(canvas)).toHaveValue(earliestDate);
122
143
  await expect(dateToPicker(canvas)).toHaveValue(initialDateTo);
123
144
  });
124
145
  },
125
146
  };
126
147
 
127
- export const ChangingDateSetsOptionToCustom: StoryObj<DateRangeFilterProps> = {
148
+ export const SetsValueOnBlur: StoryObj<DateRangeFilterProps> = {
128
149
  ...Primary,
129
150
  args: {
130
151
  ...Primary.args,
@@ -143,26 +164,28 @@ export const ChangingDateSetsOptionToCustom: StoryObj<DateRangeFilterProps> = {
143
164
  await userEvent.click(dateToPicker(canvas));
144
165
 
145
166
  await waitFor(async () => {
146
- await expect(selectField(canvas)).toHaveValue('Custom');
167
+ await expectOptionSelected(canvasElement, 'Custom');
147
168
  });
148
169
 
149
- await expect(filterChangedListenerMock).toHaveBeenCalledWith(
150
- expect.objectContaining({
151
- detail: {
152
- aDateColumnFrom: '2000-01-01',
153
- aDateColumnTo: dayjs().format('YYYY-MM-DD'),
154
- },
155
- }),
156
- );
157
-
158
- await expect(optionChangedListenerMock).toHaveBeenCalledWith(
159
- expect.objectContaining({
160
- detail: {
161
- dateFrom: '2000-01-01',
162
- dateTo: dayjs().format('YYYY-MM-DD'),
163
- },
164
- }),
165
- );
170
+ await waitFor(async () => {
171
+ await expect(filterChangedListenerMock).toHaveBeenCalledWith(
172
+ expect.objectContaining({
173
+ detail: {
174
+ aDateColumnFrom: '2000-01-01',
175
+ aDateColumnTo: dayjs().format('YYYY-MM-DD'),
176
+ },
177
+ }),
178
+ );
179
+
180
+ await expect(optionChangedListenerMock).toHaveBeenCalledWith(
181
+ expect.objectContaining({
182
+ detail: {
183
+ dateFrom: '2000-01-01',
184
+ dateTo: dayjs().format('YYYY-MM-DD'),
185
+ },
186
+ }),
187
+ );
188
+ });
166
189
  });
167
190
  },
168
191
  };
@@ -171,12 +194,13 @@ export const ChangingTheValueProgrammatically: StoryObj<DateRangeFilterProps> =
171
194
  ...Primary,
172
195
  render: (args) => {
173
196
  const StatefulWrapper = () => {
174
- const [value, setValue] = useState('Last month');
197
+ const [value, setValue] = useState<DateRangeValue | undefined>('Last month');
175
198
  const ref = useRef<HTMLDivElement>(null);
176
199
 
177
200
  useEffect(() => {
178
201
  ref.current?.addEventListener('gs-date-range-option-changed', (event) => {
179
- setValue((event as CustomEvent).detail);
202
+ const newValue = (event as CustomEvent).detail;
203
+ setValue(newValue ?? undefined);
180
204
  });
181
205
  }, []);
182
206
 
@@ -207,12 +231,13 @@ export const ChangingTheValueProgrammatically: StoryObj<DateRangeFilterProps> =
207
231
  await step('Change the value of the component programmatically', async () => {
208
232
  await userEvent.click(canvas.getByRole('button', { name: 'Set to Custom' }));
209
233
  await waitFor(async () => {
234
+ await expectOptionSelected(canvasElement, customDateRange.label);
210
235
  await expect(selectField(canvas)).toHaveValue(customDateRange.label);
211
236
  });
212
237
 
213
238
  await userEvent.click(canvas.getByRole('button', { name: 'Set to Last month' }));
214
239
  await waitFor(async () => {
215
- await expect(selectField(canvas)).toHaveValue('Last month');
240
+ await expectOptionSelected(canvasElement, 'Last month');
216
241
  });
217
242
 
218
243
  await expect(filterChangedListenerMock).toHaveBeenCalledTimes(0);
@@ -220,12 +245,25 @@ export const ChangingTheValueProgrammatically: StoryObj<DateRangeFilterProps> =
220
245
  });
221
246
 
222
247
  await step('Changing the value from within the component is still possible', async () => {
223
- await userEvent.selectOptions(selectField(canvas), 'All times');
224
248
  await waitFor(async () => {
225
- await expect(selectField(canvas)).toHaveValue('All times');
249
+ await userEvent.selectOptions(selectField(canvas), 'All times');
250
+ await expectOptionSelected(canvasElement, 'All times');
251
+ });
252
+ await waitFor(async () => {
253
+ await expect(filterChangedListenerMock).toHaveBeenCalledTimes(1);
254
+ await expect(optionChangedListenerMock).toHaveBeenCalledTimes(1);
255
+ });
256
+ });
257
+
258
+ await step('Clearing the value from within the component is still possible', async () => {
259
+ await waitFor(async () => {
260
+ await userEvent.click(canvas.getByRole('button', { name: '×' }));
261
+ await expectOptionSelected(canvasElement, placeholder);
262
+ });
263
+ await waitFor(async () => {
264
+ await expect(filterChangedListenerMock).toHaveBeenCalledTimes(2);
265
+ await expect(optionChangedListenerMock).toHaveBeenCalledTimes(2);
226
266
  });
227
- await expect(filterChangedListenerMock).toHaveBeenCalledTimes(1);
228
- await expect(optionChangedListenerMock).toHaveBeenCalledTimes(1);
229
267
  });
230
268
  },
231
269
  };
@@ -236,13 +274,13 @@ export const ChangingDateOption: StoryObj<DateRangeFilterProps> = {
236
274
  const { canvas, filterChangedListenerMock, optionChangedListenerMock } = await prepare(canvasElement, step);
237
275
 
238
276
  await waitFor(async () => {
239
- await expect(selectField(canvas)).toHaveValue('Custom');
277
+ await expectOptionSelected(canvasElement, placeholder);
240
278
  });
241
279
 
242
280
  await step('Change date to custom', async () => {
243
281
  await waitFor(async () => {
244
282
  await userEvent.selectOptions(selectField(canvas), 'CustomDateRange');
245
- await expect(selectField(canvas)).toHaveValue('CustomDateRange');
283
+ await expectOptionSelected(canvasElement, 'CustomDateRange');
246
284
  });
247
285
 
248
286
  await expect(filterChangedListenerMock).toHaveBeenCalledWith(
@@ -1,28 +1,26 @@
1
- import flatpickr from 'flatpickr';
2
- import 'flatpickr/dist/flatpickr.min.css';
3
- import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
4
2
  import z from 'zod';
5
3
 
6
4
  import { computeInitialValues } from './computeInitialValues';
5
+ import { DatePicker } from './date-picker';
7
6
  import { toYYYYMMDD } from './dateConversion';
8
7
  import {
8
+ type DateRangeOption,
9
9
  DateRangeOptionChangedEvent,
10
10
  dateRangeOptionSchema,
11
- type DateRangeSelectOption,
12
11
  dateRangeValueSchema,
13
12
  } from './dateRangeOption';
14
- import { getDatesForSelectorValue, getSelectableOptions } from './selectableOptions';
13
+ import { ClearableSelect } from '../components/clearable-select';
15
14
  import { ErrorBoundary } from '../components/error-boundary';
16
- import { Select } from '../components/select';
17
- import type { ScaleType } from '../shared/charts/getYAxisScale';
18
15
 
19
16
  const customOption = 'Custom';
20
17
 
21
18
  const dateRangeFilterInnerPropsSchema = z.object({
22
19
  dateRangeOptions: z.array(dateRangeOptionSchema),
23
20
  earliestDate: z.string().date(),
24
- value: dateRangeValueSchema.optional(),
21
+ value: dateRangeValueSchema,
25
22
  lapisDateField: z.string().min(1),
23
+ placeholder: z.string().optional(),
26
24
  });
27
25
 
28
26
  const dateRangeFilterPropsSchema = dateRangeFilterInnerPropsSchema.extend({
@@ -32,6 +30,12 @@ const dateRangeFilterPropsSchema = dateRangeFilterInnerPropsSchema.extend({
32
30
  export type DateRangeFilterProps = z.infer<typeof dateRangeFilterPropsSchema>;
33
31
  export type DateRangeFilterInnerProps = z.infer<typeof dateRangeFilterInnerPropsSchema>;
34
32
 
33
+ type DateRangeFilterState = {
34
+ label: string;
35
+ dateFrom?: Date;
36
+ dateTo?: Date;
37
+ } | null;
38
+
35
39
  export const DateRangeFilter = (props: DateRangeFilterProps) => {
36
40
  const { width, ...innerProps } = props;
37
41
  const size = { width, height: '3rem' };
@@ -50,135 +54,116 @@ export const DateRangeFilterInner = ({
50
54
  earliestDate = '1900-01-01',
51
55
  value,
52
56
  lapisDateField,
57
+ placeholder,
53
58
  }: DateRangeFilterInnerProps) => {
54
59
  const initialValues = useMemo(
55
60
  () => computeInitialValues(value, earliestDate, dateRangeOptions),
56
61
  [value, earliestDate, dateRangeOptions],
57
62
  );
58
63
 
59
- const fromDatePickerRef = useRef<HTMLInputElement>(null);
60
- const toDatePickerRef = useRef<HTMLInputElement>(null);
61
64
  const divRef = useRef<HTMLDivElement>(null);
62
- const [dateFromPicker, setDateFromPicker] = useState<flatpickr.Instance | null>(null);
63
- const [dateToPicker, setDateToPicker] = useState<flatpickr.Instance | null>(null);
64
65
 
65
- const [selectedDateRange, setSelectedDateRange] = useState<string | undefined>(
66
- initialValues.initialSelectedDateRange,
66
+ const getInitialState = useCallback(() => {
67
+ if (!initialValues) {
68
+ return null;
69
+ }
70
+ return initialValues.initialSelectedDateRange
71
+ ? {
72
+ label: initialValues.initialSelectedDateRange,
73
+ dateFrom: initialValues.initialSelectedDateFrom,
74
+ dateTo: initialValues.initialSelectedDateTo,
75
+ }
76
+ : {
77
+ label: customOption,
78
+ dateFrom: initialValues.initialSelectedDateFrom,
79
+ dateTo: initialValues.initialSelectedDateTo,
80
+ };
81
+ }, [initialValues]);
82
+
83
+ const customComboboxValue = { label: customOption };
84
+ const [options, setOptions] = useState(
85
+ getInitialState()?.label === customOption ? [...dateRangeOptions, customComboboxValue] : [...dateRangeOptions],
67
86
  );
87
+ const [state, setState] = useState<DateRangeFilterState>(getInitialState());
68
88
 
69
- const [selectedDates, setSelectedDates] = useState<{ dateFrom: Date; dateTo: Date }>({
70
- dateFrom: initialValues.initialSelectedDateFrom,
71
- dateTo: initialValues.initialSelectedDateTo,
72
- });
89
+ function updateState(newState: DateRangeFilterState) {
90
+ setState(newState);
91
+ fireFilterChangedEvent({ dateFrom: newState?.dateFrom, dateTo: newState?.dateTo, lapisDateField });
92
+ fireOptionChangedEvent(newState);
93
+ }
73
94
 
74
95
  useEffect(() => {
75
- setSelectedDateRange(initialValues.initialSelectedDateRange);
76
- setSelectedDates({
77
- dateFrom: initialValues.initialSelectedDateFrom,
78
- dateTo: initialValues.initialSelectedDateTo,
79
- });
80
-
81
- const commonConfig = {
82
- allowInput: true,
83
- dateFormat: 'Y-m-d',
84
- };
85
-
86
- if (fromDatePickerRef.current) {
87
- setDateFromPicker(
88
- flatpickr(fromDatePickerRef.current, {
89
- ...commonConfig,
90
- defaultDate: initialValues.initialSelectedDateFrom,
91
- }),
92
- );
96
+ setState(getInitialState());
97
+ }, [getInitialState]);
98
+
99
+ const onSelectChange = (option: DateRangeOption | null) => {
100
+ updateState(
101
+ option !== null
102
+ ? {
103
+ label: option?.label,
104
+ dateFrom: getFromDate(option, earliestDate),
105
+ dateTo: getToDate(option),
106
+ }
107
+ : null,
108
+ );
109
+ if (option?.label !== customOption) {
110
+ setOptions([...dateRangeOptions]);
93
111
  }
112
+ };
94
113
 
95
- if (toDatePickerRef.current) {
96
- setDateToPicker(
97
- flatpickr(toDatePickerRef.current, {
98
- ...commonConfig,
99
- defaultDate: initialValues.initialSelectedDateTo,
100
- }),
101
- );
114
+ function getFromDate(option: DateRangeOption | null, earliestDate: string) {
115
+ if (!option || option.label === customOption) {
116
+ return undefined;
102
117
  }
118
+ return new Date(option?.dateFrom ?? earliestDate);
119
+ }
103
120
 
104
- return () => {
105
- setDateFromPicker((prev) => {
106
- prev?.destroy();
107
- return null;
108
- });
109
- setDateToPicker((prev) => {
110
- prev?.destroy();
111
- return null;
112
- });
113
- };
114
- }, [fromDatePickerRef, toDatePickerRef, initialValues]);
115
-
116
- const onSelectChange = (value: string) => {
117
- setSelectedDateRange(value);
118
-
119
- const dateRange = getDatesForSelectorValue(value, dateRangeOptions, earliestDate);
120
-
121
- dateToPicker?.set('minDate', dateRange.dateFrom);
122
- dateFromPicker?.set('maxDate', dateRange.dateTo);
123
-
124
- dateFromPicker?.setDate(dateRange.dateFrom);
125
- dateToPicker?.setDate(dateRange.dateTo);
126
-
127
- setSelectedDates({
128
- dateFrom: dateRange.dateFrom,
129
- dateTo: dateRange.dateTo,
130
- });
121
+ function getToDate(option: DateRangeOption | null) {
122
+ if (!option || option.label === customOption) {
123
+ return undefined;
124
+ }
125
+ if (!option.dateTo) {
126
+ return new Date();
127
+ }
131
128
 
132
- fireFilterChangedEvent();
133
- fireOptionChangedEvent(value);
134
- };
129
+ return new Date(option.dateTo);
130
+ }
135
131
 
136
- const onChangeDateFrom = () => {
137
- if (selectedDates.dateFrom.toDateString() === dateFromPicker?.selectedDates[0].toDateString()) {
132
+ const onChangeDateFrom = (date: Date | undefined) => {
133
+ if (date?.toDateString() === state?.dateFrom?.toDateString()) {
138
134
  return;
139
135
  }
140
136
 
141
- const dateTo = dateToPicker?.selectedDates[0];
142
- const dateFrom = dateFromPicker?.selectedDates[0];
143
-
144
- selectedDates.dateFrom = dateFrom || new Date();
145
- dateToPicker?.set('minDate', dateFrom);
146
- setSelectedDateRange(customOption);
147
-
148
- fireFilterChangedEvent();
149
- fireOptionChangedEvent({
150
- dateFrom: dateFrom !== undefined ? toYYYYMMDD(dateFrom) : earliestDate,
151
- dateTo: toYYYYMMDD(dateTo || new Date()),
137
+ updateState({
138
+ label: customOption,
139
+ dateFrom: date,
140
+ dateTo: state?.dateTo,
152
141
  });
142
+ setOptions([...dateRangeOptions, customComboboxValue]);
153
143
  };
154
144
 
155
- const onChangeDateTo = () => {
156
- if (selectedDates.dateTo.toDateString() === dateToPicker?.selectedDates[0].toDateString()) {
145
+ const onChangeDateTo = (date: Date | undefined) => {
146
+ if (date?.toDateString() === state?.dateTo?.toDateString()) {
157
147
  return;
158
148
  }
159
149
 
160
- const dateTo = dateToPicker?.selectedDates[0];
161
- const dateFrom = dateFromPicker?.selectedDates[0];
162
-
163
- selectedDates.dateTo = dateTo || new Date();
164
- dateFromPicker?.set('maxDate', dateTo);
165
- setSelectedDateRange(customOption);
166
-
167
- fireFilterChangedEvent();
168
- fireOptionChangedEvent({
169
- dateFrom: dateFrom !== undefined ? toYYYYMMDD(dateFrom) : earliestDate,
170
- dateTo: toYYYYMMDD(dateTo || new Date()),
150
+ updateState({
151
+ label: customOption,
152
+ dateFrom: state?.dateFrom,
153
+ dateTo: date,
171
154
  });
155
+ setOptions([...dateRangeOptions, customComboboxValue]);
172
156
  };
173
157
 
174
- const fireOptionChangedEvent = (option: DateRangeSelectOption) => {
175
- divRef.current?.dispatchEvent(new DateRangeOptionChangedEvent(option));
176
- };
177
-
178
- const fireFilterChangedEvent = () => {
179
- const dateFrom = dateFromPicker?.selectedDates[0];
180
- const dateTo = dateToPicker?.selectedDates[0];
181
-
158
+ const fireFilterChangedEvent = ({
159
+ dateFrom,
160
+ dateTo,
161
+ lapisDateField,
162
+ }: {
163
+ dateFrom: Date | undefined;
164
+ dateTo: Date | undefined;
165
+ lapisDateField: string;
166
+ }) => {
182
167
  const detail = {
183
168
  ...(dateFrom !== undefined && { [`${lapisDateField}From`]: toYYYYMMDD(dateFrom) }),
184
169
  ...(dateTo !== undefined && { [`${lapisDateField}To`]: toYYYYMMDD(dateTo) }),
@@ -193,39 +178,51 @@ export const DateRangeFilterInner = ({
193
178
  );
194
179
  };
195
180
 
181
+ const fireOptionChangedEvent = (state: DateRangeFilterState) => {
182
+ const eventDetail =
183
+ state?.label === customOption
184
+ ? {
185
+ dateFrom: state.dateFrom !== undefined ? toYYYYMMDD(state.dateFrom) : undefined,
186
+ dateTo: state.dateTo !== undefined ? toYYYYMMDD(state.dateTo) : undefined,
187
+ }
188
+ : state?.label;
189
+
190
+ divRef.current?.dispatchEvent(new DateRangeOptionChangedEvent(eventDetail));
191
+ };
192
+
196
193
  return (
197
- <div class='flex flex-wrap' ref={divRef}>
198
- <Select
199
- items={[
200
- ...getSelectableOptions(dateRangeOptions),
201
- { label: customOption, value: customOption, disabled: true },
202
- ]}
203
- selected={selectedDateRange ?? customOption}
204
- selectStyle='select-bordered rounded-none flex-grow min-w-[7.5rem]'
205
- onChange={(event: Event) => {
206
- event.preventDefault();
207
- const select = event.target as HTMLSelectElement;
208
- const value = select.value as ScaleType;
209
- onSelectChange(value);
210
- }}
211
- />
212
- <div className={'flex flex-wrap flex-grow'}>
213
- <input
214
- class='input input-bordered rounded-none flex-grow w-[7.5rem]'
215
- type='text'
216
- placeholder='Date from'
217
- ref={fromDatePickerRef}
218
- onChange={onChangeDateFrom}
219
- onBlur={onChangeDateFrom}
220
- />
221
- <input
222
- class='input input-bordered rounded-none flex-grow w-[7.5rem]'
223
- type='text'
224
- placeholder='Date to'
225
- ref={toDatePickerRef}
226
- onChange={onChangeDateTo}
227
- onBlur={onChangeDateTo}
228
- />
194
+ <div className={'@container'} ref={divRef}>
195
+ <div className='flex min-w-[7.5rem] flex-col @md:flex-row'>
196
+ <div className='flex-grow'>
197
+ <ClearableSelect
198
+ items={options.map((item) => item.label)}
199
+ placeholderText={placeholder}
200
+ onChange={(value) => {
201
+ const dateRangeOption = options.find((item) => item.label === value);
202
+ onSelectChange(dateRangeOption ?? null);
203
+ }}
204
+ value={state?.label ?? null}
205
+ selectClassName={'rounded-t-md rounded-b-none @md:rounded-l-md @md:rounded-r-none'}
206
+ />
207
+ </div>
208
+ <div className={'flex flex-grow flex-col @4xs:flex-row'}>
209
+ <DatePicker
210
+ className={'flex-grow min-w-[7.5rem] @4xs:rounded-bl-md @md:rounded-l-none rounded-none'}
211
+ value={state?.dateFrom}
212
+ onChange={onChangeDateFrom}
213
+ maxDate={state?.dateTo}
214
+ placeholderText={'Date from'}
215
+ />
216
+ <DatePicker
217
+ className={
218
+ 'flex-grow min-w-[7.5rem] rounded-b-md rounded-t-none @4xs:rounded-tr-none @4xs:rounded-l-none @md:rounded-r-md '
219
+ }
220
+ value={state?.dateTo}
221
+ onChange={onChangeDateTo}
222
+ minDate={state?.dateFrom}
223
+ placeholderText={'Date to'}
224
+ />
225
+ </div>
229
226
  </div>
230
227
  </div>
231
228
  );
@@ -22,20 +22,20 @@ export const dateRangeOptionSchema = z.object({
22
22
 
23
23
  export type DateRangeOption = z.infer<typeof dateRangeOptionSchema>;
24
24
 
25
- export const dateRangeValueSchema = z.union([
26
- z.string(),
27
- z.object({
28
- dateFrom: z.string().date().optional(),
29
- dateTo: z.string().date().optional(),
30
- }),
31
- ]);
25
+ export const dateRangeValueSchema = z
26
+ .union([
27
+ z.string(),
28
+ z.object({
29
+ dateFrom: z.string().date().optional(),
30
+ dateTo: z.string().date().optional(),
31
+ }),
32
+ ])
33
+ .optional();
32
34
 
33
35
  export type DateRangeValue = z.infer<typeof dateRangeValueSchema>;
34
36
 
35
- export type DateRangeSelectOption = Required<DateRangeValue>;
36
-
37
- export class DateRangeOptionChangedEvent extends CustomEvent<DateRangeSelectOption> {
38
- constructor(detail: DateRangeSelectOption) {
37
+ export class DateRangeOptionChangedEvent extends CustomEvent<DateRangeValue> {
38
+ constructor(detail: DateRangeValue) {
39
39
  super('gs-date-range-option-changed', {
40
40
  detail,
41
41
  bubbles: true,