@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
package/dist/util.d.ts CHANGED
@@ -73,8 +73,8 @@ declare const confidenceIntervalMethodSchema: default_2.ZodUnion<[default_2.ZodL
73
73
 
74
74
  export declare type DateRangeOption = default_2.infer<typeof dateRangeOptionSchema>;
75
75
 
76
- export declare class DateRangeOptionChangedEvent extends CustomEvent<DateRangeSelectOption> {
77
- constructor(detail: DateRangeSelectOption);
76
+ export declare class DateRangeOptionChangedEvent extends CustomEvent<DateRangeValue> {
77
+ constructor(detail: DateRangeValue);
78
78
  }
79
79
 
80
80
  /**
@@ -136,11 +136,9 @@ declare const dateRangeOptionSchema: default_2.ZodObject<{
136
136
  dateTo?: string | undefined;
137
137
  }>;
138
138
 
139
- export declare type DateRangeSelectOption = Required<DateRangeValue>;
139
+ export declare type DateRangeValue = default_2.infer<typeof dateRangeValueSchema>;
140
140
 
141
- declare type DateRangeValue = default_2.infer<typeof dateRangeValueSchema>;
142
-
143
- declare const dateRangeValueSchema: default_2.ZodUnion<[default_2.ZodString, default_2.ZodObject<{
141
+ declare const dateRangeValueSchema: default_2.ZodOptional<default_2.ZodUnion<[default_2.ZodString, default_2.ZodObject<{
144
142
  dateFrom: default_2.ZodOptional<default_2.ZodString>;
145
143
  dateTo: default_2.ZodOptional<default_2.ZodString>;
146
144
  }, "strip", default_2.ZodTypeAny, {
@@ -149,7 +147,7 @@ declare const dateRangeValueSchema: default_2.ZodUnion<[default_2.ZodString, def
149
147
  }, {
150
148
  dateFrom?: string | undefined;
151
149
  dateTo?: string | undefined;
152
- }>]>;
150
+ }>]>>;
153
151
 
154
152
  export declare type LapisFilter = default_2.infer<typeof lapisFilterSchema>;
155
153
 
@@ -972,7 +970,7 @@ declare global {
972
970
 
973
971
  declare global {
974
972
  interface HTMLElementTagNameMap {
975
- 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
973
+ 'gs-mutations-over-time': MutationsOverTimeComponent;
976
974
  }
977
975
  }
978
976
 
@@ -980,7 +978,7 @@ declare global {
980
978
  declare global {
981
979
  namespace JSX {
982
980
  interface IntrinsicElements {
983
- 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
981
+ 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
984
982
  }
985
983
  }
986
984
  }
@@ -988,7 +986,7 @@ declare global {
988
986
 
989
987
  declare global {
990
988
  interface HTMLElementTagNameMap {
991
- 'gs-mutations-over-time': MutationsOverTimeComponent;
989
+ 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
992
990
  }
993
991
  }
994
992
 
@@ -996,7 +994,7 @@ declare global {
996
994
  declare global {
997
995
  namespace JSX {
998
996
  interface IntrinsicElements {
999
- 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
997
+ 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1000
998
  }
1001
999
  }
1002
1000
  }
@@ -1036,11 +1034,7 @@ declare global {
1036
1034
 
1037
1035
  declare global {
1038
1036
  interface HTMLElementTagNameMap {
1039
- 'gs-date-range-filter': DateRangeFilterComponent;
1040
- }
1041
- interface HTMLElementEventMap {
1042
- 'gs-date-range-filter-changed': CustomEvent<Record<string, string>>;
1043
- 'gs-date-range-option-changed': DateRangeOptionChangedEvent;
1037
+ 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
1044
1038
  }
1045
1039
  }
1046
1040
 
@@ -1048,7 +1042,7 @@ declare global {
1048
1042
  declare global {
1049
1043
  namespace JSX {
1050
1044
  interface IntrinsicElements {
1051
- 'gs-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1045
+ 'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1052
1046
  }
1053
1047
  }
1054
1048
  }
@@ -1056,7 +1050,11 @@ declare global {
1056
1050
 
1057
1051
  declare global {
1058
1052
  interface HTMLElementTagNameMap {
1059
- 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
1053
+ 'gs-date-range-filter': DateRangeFilterComponent;
1054
+ }
1055
+ interface HTMLElementEventMap {
1056
+ 'gs-date-range-filter-changed': CustomEvent<Record<string, string>>;
1057
+ 'gs-date-range-option-changed': DateRangeOptionChangedEvent;
1060
1058
  }
1061
1059
  }
1062
1060
 
@@ -1064,7 +1062,7 @@ declare global {
1064
1062
  declare global {
1065
1063
  namespace JSX {
1066
1064
  interface IntrinsicElements {
1067
- 'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1065
+ 'gs-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1068
1066
  }
1069
1067
  }
1070
1068
  }
@@ -1110,10 +1108,10 @@ declare global {
1110
1108
 
1111
1109
  declare global {
1112
1110
  interface HTMLElementTagNameMap {
1113
- 'gs-lineage-filter': LineageFilterComponent;
1111
+ 'gs-mutation-filter': MutationFilterComponent;
1114
1112
  }
1115
1113
  interface HTMLElementEventMap {
1116
- 'gs-lineage-filter-changed': LineageFilterChangedEvent;
1114
+ 'gs-mutation-filter-changed': CustomEvent<MutationsFilter>;
1117
1115
  }
1118
1116
  }
1119
1117
 
@@ -1121,7 +1119,7 @@ declare global {
1121
1119
  declare global {
1122
1120
  namespace JSX {
1123
1121
  interface IntrinsicElements {
1124
- 'gs-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1122
+ 'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1125
1123
  }
1126
1124
  }
1127
1125
  }
@@ -1129,10 +1127,10 @@ declare global {
1129
1127
 
1130
1128
  declare global {
1131
1129
  interface HTMLElementTagNameMap {
1132
- 'gs-mutation-filter': MutationFilterComponent;
1130
+ 'gs-lineage-filter': LineageFilterComponent;
1133
1131
  }
1134
1132
  interface HTMLElementEventMap {
1135
- 'gs-mutation-filter-changed': CustomEvent<MutationsFilter>;
1133
+ 'gs-lineage-filter-changed': LineageFilterChangedEvent;
1136
1134
  }
1137
1135
  }
1138
1136
 
@@ -1140,7 +1138,7 @@ declare global {
1140
1138
  declare global {
1141
1139
  namespace JSX {
1142
1140
  interface IntrinsicElements {
1143
- 'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1141
+ 'gs-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1144
1142
  }
1145
1143
  }
1146
1144
  }
package/dist/util.js CHANGED
@@ -1,4 +1,4 @@
1
- import { D, a, L, T, d, v } from "./LineageFilterChangedEvent-COWV-Y0k.js";
1
+ import { D, a, L, T, d, v } from "./LineageFilterChangedEvent-DkvWdq_G.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": "0.16.4",
3
+ "version": "0.17.1",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -83,6 +83,7 @@
83
83
  "@lit/context": "^1.1.1",
84
84
  "@lit/reactive-element": "^2.0.4",
85
85
  "@lit/task": "^1.0.0",
86
+ "@tanstack/table-core": "^8.21.2",
86
87
  "chart.js": "^4.4.6",
87
88
  "chartjs-chart-error-bars": "^4.4.0",
88
89
  "chartjs-chart-venn": "^4.3.0",
@@ -0,0 +1,75 @@
1
+ import { type Meta, type StoryObj } from '@storybook/preact';
2
+ import { fn, userEvent, within } from '@storybook/test';
3
+
4
+ import { ClearableSelect, type ClearableSelectProps } from './clearable-select';
5
+ import { expectOptionSelected } from '../shared/stories/expectOptionSelected';
6
+
7
+ const meta: Meta<ClearableSelectProps> = {
8
+ title: 'Component/ClearableSelect',
9
+ component: ClearableSelect,
10
+ parameters: { fetchMock: {} },
11
+ };
12
+
13
+ export default meta;
14
+
15
+ export const Default: StoryObj<ClearableSelectProps> = {
16
+ render: (args) => (
17
+ <div class='flex justify-center px-4 py-16'>
18
+ <ClearableSelect {...args} />
19
+ </div>
20
+ ),
21
+ args: {
22
+ items: ['firstOption', 'secondOption'],
23
+ onChange: fn(),
24
+ },
25
+ play: async ({ canvasElement, step }) => {
26
+ await step('Show default placeholder', async () => {
27
+ await expectOptionSelected(canvasElement, 'Select an option');
28
+ });
29
+ },
30
+ };
31
+
32
+ export const UseInitialSelectedItem: StoryObj<ClearableSelectProps> = {
33
+ ...Default,
34
+ args: {
35
+ ...Default.args,
36
+ initiallySelectedItem: 'firstOption',
37
+ },
38
+ play: async ({ canvasElement, step }) => {
39
+ await step('Show initiallySelectedItem', async () => {
40
+ await expectOptionSelected(canvasElement, 'firstOption');
41
+ });
42
+ },
43
+ };
44
+
45
+ export const SwitchToOption: StoryObj<ClearableSelectProps> = {
46
+ ...Default,
47
+ play: async ({ canvasElement, step }) => {
48
+ const canvas = within(canvasElement);
49
+
50
+ await step('Select an option', async () => {
51
+ await userEvent.selectOptions(getSelectElement(canvas), 'firstOption');
52
+ await expectOptionSelected(canvasElement, 'firstOption');
53
+ });
54
+ },
55
+ };
56
+
57
+ export const ClearOption: StoryObj<ClearableSelectProps> = {
58
+ ...Default,
59
+ args: {
60
+ ...Default.args,
61
+ initiallySelectedItem: 'firstOption',
62
+ },
63
+ play: async ({ canvasElement, step }) => {
64
+ const canvas = within(canvasElement);
65
+
66
+ await step('Clear the selected option', async () => {
67
+ await userEvent.click(canvas.getByRole('button', { name: '×' }));
68
+ await expectOptionSelected(canvasElement, 'Select an option');
69
+ });
70
+ },
71
+ };
72
+
73
+ const getSelectElement = (canvas: ReturnType<typeof within>) => {
74
+ return canvas.getByRole('combobox');
75
+ };
@@ -0,0 +1,76 @@
1
+ import { type ChangeEvent } from 'preact/compat';
2
+ import { useEffect, useState } from 'preact/hooks';
3
+
4
+ import { type WithClassName } from '../shared/WithClassName/WithClassName';
5
+ import { DeleteIcon } from '../shared/icons/DeleteIcon';
6
+
7
+ export const undefinedValue = '__undefined__';
8
+
9
+ export type ClearableSelectProps = {
10
+ items: string[];
11
+ initiallySelectedItem?: string | null;
12
+ onChange?: (item: string | null) => void;
13
+ placeholderText?: string;
14
+ value?: string | null;
15
+ selectClassName?: string;
16
+ };
17
+
18
+ export function ClearableSelect({
19
+ items,
20
+ initiallySelectedItem,
21
+ onChange,
22
+ placeholderText,
23
+ className,
24
+ value,
25
+ selectClassName,
26
+ }: WithClassName<ClearableSelectProps>) {
27
+ const [selectedOption, setSelectedOption] = useState<string | null>(initiallySelectedItem ?? null);
28
+
29
+ useEffect(() => {
30
+ if (value !== undefined) {
31
+ setSelectedOption(value);
32
+ }
33
+ }, [value]);
34
+
35
+ const handleClear = () => {
36
+ setSelectedOption(null);
37
+ if (onChange) {
38
+ onChange(null);
39
+ }
40
+ };
41
+
42
+ const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
43
+ const newValue = event.currentTarget.value;
44
+ setSelectedOption(newValue);
45
+ if (onChange) {
46
+ onChange(newValue);
47
+ }
48
+ };
49
+
50
+ return (
51
+ <div className={`relative inline min-w-24 ${className}`}>
52
+ <select
53
+ className={`w-full select select-bordered pr-14 ${selectClassName}`}
54
+ value={selectedOption ?? undefinedValue}
55
+ onChange={handleChange}
56
+ >
57
+ <option value={undefinedValue} disabled>
58
+ {placeholderText ?? 'Select an option'}
59
+ </option>
60
+ {items.map((item) => (
61
+ <option key={item} value={item}>
62
+ {item}
63
+ </option>
64
+ ))}
65
+ </select>
66
+ {selectedOption && (
67
+ <button
68
+ onClick={handleClear}
69
+ className='absolute right-10 top-1/2 -translate-y-1/2 bg-transparent border-0 cursor-pointer'
70
+ >
71
+ <DeleteIcon />
72
+ </button>
73
+ )}
74
+ </div>
75
+ );
76
+ }
@@ -2,6 +2,8 @@ import { useCombobox } from 'downshift/preact';
2
2
  import { type ComponentChild } from 'preact';
3
3
  import { useMemo, useRef, useState } from 'preact/hooks';
4
4
 
5
+ import { DeleteIcon } from '../shared/icons/DeleteIcon';
6
+
5
7
  export function DownshiftCombobox<Item>({
6
8
  allItems,
7
9
  value,
@@ -10,18 +12,18 @@ export function DownshiftCombobox<Item>({
10
12
  itemToString,
11
13
  placeholderText,
12
14
  formatItemInList,
15
+ inputClassName = '',
13
16
  }: {
14
17
  allItems: Item[];
15
- value?: Item;
18
+ value?: Item | null;
16
19
  filterItemsByInputValue: (item: Item, value: string) => boolean;
17
20
  createEvent: (item: Item | null) => CustomEvent;
18
21
  itemToString: (item: Item | undefined | null) => string;
19
22
  placeholderText?: string;
20
23
  formatItemInList: (item: Item) => ComponentChild;
24
+ inputClassName?: string;
21
25
  }) {
22
- const initialSelectedItem = value ?? null;
23
-
24
- const [itemsFilter, setItemsFilter] = useState(itemToString(initialSelectedItem));
26
+ const [itemsFilter, setItemsFilter] = useState(itemToString(value));
25
27
  const items = useMemo(
26
28
  () => allItems.filter((item) => filterItemsByInputValue(item, itemsFilter)),
27
29
  [allItems, filterItemsByInputValue, itemsFilter],
@@ -65,7 +67,7 @@ export function DownshiftCombobox<Item>({
65
67
  itemToString(item) {
66
68
  return itemToString(item);
67
69
  },
68
- initialSelectedItem,
70
+ selectedItem: value,
69
71
  environment,
70
72
  });
71
73
 
@@ -89,7 +91,7 @@ export function DownshiftCombobox<Item>({
89
91
  <div ref={divRef} className={'relative w-full'}>
90
92
  <div className='w-full flex flex-col gap-1'>
91
93
  <div
92
- className='flex gap-0.5 input input-bordered min-w-32'
94
+ className={`flex gap-0.5 input input-bordered min-w-32 ${inputClassName}`}
93
95
  onBlur={(event) => {
94
96
  if (event.relatedTarget != buttonRef.current) {
95
97
  closeMenu();
@@ -109,7 +111,7 @@ export function DownshiftCombobox<Item>({
109
111
  onClick={clearInput}
110
112
  tabIndex={-1}
111
113
  >
112
- ×
114
+ <DeleteIcon />
113
115
  </button>
114
116
  <button
115
117
  aria-label='toggle menu'
@@ -18,36 +18,34 @@ const dateRangeOptions = [
18
18
  ];
19
19
 
20
20
  describe('computeInitialValues', () => {
21
+ it('should return undefined for unedfined value', () => {
22
+ const result = computeInitialValues(undefined, earliestDate, dateRangeOptions);
23
+
24
+ expect(result).toBeUndefined();
25
+ });
26
+
21
27
  it('should compute initial value if value is dateRangeOption label', () => {
22
28
  const result = computeInitialValues(fromToOption, earliestDate, dateRangeOptions);
23
29
 
24
- expect(result.initialSelectedDateRange).toEqual(fromToOption);
25
- expectDateMatches(result.initialSelectedDateFrom, new Date(dateFromOptionValue));
26
- expectDateMatches(result.initialSelectedDateTo, new Date(dateToOptionValue));
30
+ expect(result?.initialSelectedDateRange).toEqual(fromToOption);
31
+ expectDateMatches(result?.initialSelectedDateFrom, new Date(dateFromOptionValue));
32
+ expectDateMatches(result?.initialSelectedDateTo, new Date(dateToOptionValue));
27
33
  });
28
34
 
29
35
  it('should use today as "dateTo" if it is unset in selected option', () => {
30
36
  const result = computeInitialValues(fromOption, earliestDate, dateRangeOptions);
31
37
 
32
- expect(result.initialSelectedDateRange).toEqual(fromOption);
33
- expectDateMatches(result.initialSelectedDateFrom, new Date(dateFromOptionValue));
34
- expectDateMatches(result.initialSelectedDateTo, today);
38
+ expect(result?.initialSelectedDateRange).toEqual(fromOption);
39
+ expectDateMatches(result?.initialSelectedDateFrom, new Date(dateFromOptionValue));
40
+ expectDateMatches(result?.initialSelectedDateTo, today);
35
41
  });
36
42
 
37
43
  it('should use earliest date as "dateFrom" if it is unset in selected option', () => {
38
44
  const result = computeInitialValues(toOption, earliestDate, dateRangeOptions);
39
45
 
40
- expect(result.initialSelectedDateRange).toEqual(toOption);
41
- expectDateMatches(result.initialSelectedDateFrom, new Date(earliestDate));
42
- expectDateMatches(result.initialSelectedDateTo, new Date(dateToOptionValue));
43
- });
44
-
45
- it('should fall back to full range if initial value is not set', () => {
46
- const result = computeInitialValues(undefined, earliestDate, dateRangeOptions);
47
-
48
- expect(result.initialSelectedDateRange).toBeUndefined();
49
- expectDateMatches(result.initialSelectedDateFrom, new Date(earliestDate));
50
- expectDateMatches(result.initialSelectedDateTo, today);
46
+ expect(result?.initialSelectedDateRange).toEqual(toOption);
47
+ expectDateMatches(result?.initialSelectedDateFrom, new Date(earliestDate));
48
+ expectDateMatches(result?.initialSelectedDateTo, new Date(dateToOptionValue));
51
49
  });
52
50
 
53
51
  it('should throw when initial value is unknown', () => {
@@ -66,18 +64,18 @@ describe('computeInitialValues', () => {
66
64
  const initialDateFrom = '2020-01-01';
67
65
  const result = computeInitialValues({ dateFrom: initialDateFrom }, earliestDate, dateRangeOptions);
68
66
 
69
- expect(result.initialSelectedDateRange).toBeUndefined();
70
- expectDateMatches(result.initialSelectedDateFrom, new Date(initialDateFrom));
71
- expectDateMatches(result.initialSelectedDateTo, today);
67
+ expect(result?.initialSelectedDateRange).toBeUndefined();
68
+ expectDateMatches(result?.initialSelectedDateFrom, new Date(initialDateFrom));
69
+ expectDateMatches(result?.initialSelectedDateTo, today);
72
70
  });
73
71
 
74
72
  it('should select from earliest date until date if only dateTo is given', () => {
75
73
  const initialDateTo = '2020-01-01';
76
74
  const result = computeInitialValues({ dateTo: initialDateTo }, earliestDate, dateRangeOptions);
77
75
 
78
- expect(result.initialSelectedDateRange).toBeUndefined();
79
- expectDateMatches(result.initialSelectedDateFrom, new Date(earliestDate));
80
- expectDateMatches(result.initialSelectedDateTo, new Date(initialDateTo));
76
+ expect(result?.initialSelectedDateRange).toBeUndefined();
77
+ expectDateMatches(result?.initialSelectedDateFrom, new Date(earliestDate));
78
+ expectDateMatches(result?.initialSelectedDateTo, new Date(initialDateTo));
81
79
  });
82
80
 
83
81
  it('should select date range is dateFrom and dateTo are given', () => {
@@ -92,9 +90,9 @@ describe('computeInitialValues', () => {
92
90
  dateRangeOptions,
93
91
  );
94
92
 
95
- expect(result.initialSelectedDateRange).toBeUndefined();
96
- expectDateMatches(result.initialSelectedDateFrom, new Date(initialDateFrom));
97
- expectDateMatches(result.initialSelectedDateTo, new Date(initialDateTo));
93
+ expect(result?.initialSelectedDateRange).toBeUndefined();
94
+ expectDateMatches(result?.initialSelectedDateFrom, new Date(initialDateFrom));
95
+ expectDateMatches(result?.initialSelectedDateTo, new Date(initialDateTo));
98
96
  });
99
97
 
100
98
  it('should set initial "to" to "from" if "from" is after "to"', () => {
@@ -109,9 +107,9 @@ describe('computeInitialValues', () => {
109
107
  dateRangeOptions,
110
108
  );
111
109
 
112
- expect(result.initialSelectedDateRange).toBeUndefined();
113
- expectDateMatches(result.initialSelectedDateFrom, new Date(initialDateFrom));
114
- expectDateMatches(result.initialSelectedDateTo, new Date(initialDateFrom));
110
+ expect(result?.initialSelectedDateRange).toBeUndefined();
111
+ expectDateMatches(result?.initialSelectedDateFrom, new Date(initialDateFrom));
112
+ expectDateMatches(result?.initialSelectedDateTo, new Date(initialDateFrom));
115
113
  });
116
114
 
117
115
  it('should throw if initial "from" is not a valid date', () => {
@@ -126,9 +124,9 @@ describe('computeInitialValues', () => {
126
124
  );
127
125
  });
128
126
 
129
- function expectDateMatches(actual: Date, expected: Date) {
130
- expect(actual.getFullYear()).toEqual(expected.getFullYear());
131
- expect(actual.getMonth()).toEqual(expected.getMonth());
132
- expect(actual.getDate()).toEqual(expected.getDate());
127
+ function expectDateMatches(actual: Date | undefined, expected: Date | undefined) {
128
+ expect(actual?.getFullYear()).toEqual(expected?.getFullYear());
129
+ expect(actual?.getMonth()).toEqual(expected?.getMonth());
130
+ expect(actual?.getDate()).toEqual(expected?.getDate());
133
131
  }
134
132
  });
@@ -2,22 +2,9 @@ import { type DateRangeOption, type DateRangeValue } from './dateRangeOption';
2
2
  import { getDatesForSelectorValue, getSelectableOptions } from './selectableOptions';
3
3
  import { UserFacingError } from '../components/error-display';
4
4
 
5
- export function computeInitialValues(
6
- value: DateRangeValue | undefined,
7
- earliestDate: string,
8
- dateRangeOptions: DateRangeOption[],
9
- ): {
10
- initialSelectedDateRange: string | undefined;
11
- initialSelectedDateFrom: Date;
12
- initialSelectedDateTo: Date;
13
- } {
5
+ export function computeInitialValues(value: DateRangeValue, earliestDate: string, dateRangeOptions: DateRangeOption[]) {
14
6
  if (value === undefined) {
15
- const { dateFrom, dateTo } = getDatesForSelectorValue(undefined, dateRangeOptions, earliestDate);
16
- return {
17
- initialSelectedDateRange: undefined,
18
- initialSelectedDateFrom: dateFrom,
19
- initialSelectedDateTo: dateTo,
20
- };
7
+ return undefined;
21
8
  }
22
9
 
23
10
  if (typeof value === 'string') {
@@ -0,0 +1,66 @@
1
+ import 'flatpickr/dist/flatpickr.min.css';
2
+ import flatpickr from 'flatpickr';
3
+ import { useEffect, useRef, useState } from 'preact/hooks';
4
+
5
+ import { type WithClassName } from '../shared/WithClassName/WithClassName';
6
+
7
+ export function DatePicker({
8
+ onChange,
9
+ value,
10
+ minDate,
11
+ maxDate,
12
+ placeholderText,
13
+ className,
14
+ }: WithClassName<{
15
+ onChange?: (date: Date | undefined) => void;
16
+ value?: Date;
17
+ minDate?: Date;
18
+ maxDate?: Date;
19
+ placeholderText?: string;
20
+ }>) {
21
+ const inputRef = useRef<HTMLInputElement>(null);
22
+
23
+ const [datePicker, setDatePicker] = useState<flatpickr.Instance | null>(null);
24
+
25
+ useEffect(() => {
26
+ if (!inputRef.current) {
27
+ return;
28
+ }
29
+
30
+ const instance = flatpickr(inputRef.current, {
31
+ allowInput: true,
32
+ dateFormat: 'Y-m-d',
33
+ defaultDate: value,
34
+ minDate,
35
+ maxDate,
36
+ });
37
+
38
+ setDatePicker(instance);
39
+
40
+ return () => {
41
+ instance.destroy();
42
+ };
43
+ }, [maxDate, minDate, onChange, value]);
44
+
45
+ if (value === undefined && inputRef.current) {
46
+ inputRef.current.value = '';
47
+ }
48
+
49
+ const handleChange = () => {
50
+ const newValue = datePicker?.selectedDates[0];
51
+ if (onChange) {
52
+ onChange(newValue);
53
+ }
54
+ };
55
+
56
+ return (
57
+ <input
58
+ className={`input input-bordered w-full ${className}`}
59
+ type='text'
60
+ placeholder={placeholderText}
61
+ ref={inputRef}
62
+ onChange={handleChange}
63
+ onBlur={handleChange}
64
+ />
65
+ );
66
+ }