@genspectrum/dashboard-components 0.16.4 → 0.17.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.
Files changed (47) hide show
  1. package/custom-elements.json +86 -61
  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 +46 -34
  5. package/dist/components.js +304 -146
  6. package/dist/components.js.map +1 -1
  7. package/dist/style.css +76 -9
  8. package/dist/util.d.ts +11 -13
  9. package/dist/util.js +1 -1
  10. package/package.json +1 -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/shared/WithClassName/WithClassName.ts +1 -0
  21. package/src/preact/shared/icons/DeleteIcon.tsx +3 -0
  22. package/src/preact/shared/stories/expectOptionSelected.tsx +7 -0
  23. package/src/utilEntrypoint.ts +1 -1
  24. package/src/web-components/MutationAnnotations.mdx +33 -0
  25. package/src/web-components/ResizeContainer.mdx +1 -1
  26. package/src/web-components/errorHandling.mdx +1 -1
  27. package/src/web-components/gs-app.ts +2 -2
  28. package/src/web-components/input/gs-date-range-filter.stories.ts +38 -32
  29. package/src/web-components/input/gs-date-range-filter.tsx +8 -2
  30. package/src/web-components/input/gs-lineage-filter.tsx +1 -1
  31. package/src/web-components/input/gs-location-filter.tsx +1 -1
  32. package/src/web-components/input/gs-mutation-filter.tsx +1 -1
  33. package/src/web-components/input/gs-text-filter.tsx +1 -1
  34. package/src/web-components/visualization/gs-aggregate.tsx +2 -2
  35. package/src/web-components/visualization/gs-mutation-comparison.tsx +5 -2
  36. package/src/web-components/visualization/gs-mutations-over-time.tsx +5 -2
  37. package/src/web-components/visualization/gs-mutations.tsx +5 -2
  38. package/src/web-components/visualization/gs-number-sequences-over-time.tsx +2 -2
  39. package/src/web-components/visualization/gs-prevalence-over-time.tsx +2 -2
  40. package/src/web-components/visualization/gs-relative-growth-advantage.tsx +2 -2
  41. package/src/web-components/visualization/gs-sequences-by-location.tsx +2 -2
  42. package/src/web-components/visualization/gs-statistics.tsx +2 -2
  43. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +2 -2
  44. package/standalone-bundle/dashboard-components.js +6624 -6538
  45. package/standalone-bundle/dashboard-components.js.map +1 -1
  46. package/standalone-bundle/style.css +1 -1
  47. package/dist/LineageFilterChangedEvent-COWV-Y0k.js.map +0 -1
package/dist/style.css CHANGED
@@ -1073,12 +1073,6 @@ html {
1073
1073
  --tw-bg-opacity: 1;
1074
1074
  background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
1075
1075
  }
1076
-
1077
- .table-zebra tr.hover:hover,
1078
- .table-zebra tr.hover:nth-child(even):hover {
1079
- --tw-bg-opacity: 1;
1080
- background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));
1081
- }
1082
1076
  }
1083
1077
  .btn {
1084
1078
  display: inline-flex;
@@ -3098,6 +3092,9 @@ input.tab:checked + .tab-content,
3098
3092
  .right-0 {
3099
3093
  right: 0px;
3100
3094
  }
3095
+ .right-10 {
3096
+ right: 2.5rem;
3097
+ }
3101
3098
  .right-2 {
3102
3099
  right: 0.5rem;
3103
3100
  }
@@ -3247,9 +3244,6 @@ input.tab:checked + .tab-content,
3247
3244
  .w-\[6rem\] {
3248
3245
  width: 6rem;
3249
3246
  }
3250
- .w-\[7\.5rem\] {
3251
- width: 7.5rem;
3252
- }
3253
3247
  .w-full {
3254
3248
  width: 100%;
3255
3249
  }
@@ -3257,6 +3251,9 @@ input.tab:checked + .tab-content,
3257
3251
  width: -moz-max-content;
3258
3252
  width: max-content;
3259
3253
  }
3254
+ .min-w-24 {
3255
+ min-width: 6rem;
3256
+ }
3260
3257
  .min-w-32 {
3261
3258
  min-width: 8rem;
3262
3259
  }
@@ -3278,6 +3275,10 @@ input.tab:checked + .tab-content,
3278
3275
  .grow {
3279
3276
  flex-grow: 1;
3280
3277
  }
3278
+ .-translate-y-1\/2 {
3279
+ --tw-translate-y: -50%;
3280
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
3281
+ }
3281
3282
  .translate-x-\[-50\%\] {
3282
3283
  --tw-translate-x: -50%;
3283
3284
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@@ -3376,12 +3377,27 @@ input.tab:checked + .tab-content,
3376
3377
  border-bottom-right-radius: 0.375rem;
3377
3378
  border-bottom-left-radius: 0.375rem;
3378
3379
  }
3380
+ .rounded-b-none {
3381
+ border-bottom-right-radius: 0px;
3382
+ border-bottom-left-radius: 0px;
3383
+ }
3384
+ .rounded-t-md {
3385
+ border-top-left-radius: 0.375rem;
3386
+ border-top-right-radius: 0.375rem;
3387
+ }
3388
+ .rounded-t-none {
3389
+ border-top-left-radius: 0px;
3390
+ border-top-right-radius: 0px;
3391
+ }
3379
3392
  .rounded-tr-md {
3380
3393
  border-top-right-radius: 0.375rem;
3381
3394
  }
3382
3395
  .border {
3383
3396
  border-width: 1px;
3384
3397
  }
3398
+ .border-0 {
3399
+ border-width: 0px;
3400
+ }
3385
3401
  .border-2 {
3386
3402
  border-width: 2px;
3387
3403
  }
@@ -3423,6 +3439,9 @@ input.tab:checked + .tab-content,
3423
3439
  --tw-bg-opacity: 1;
3424
3440
  background-color: rgb(226 232 240 / var(--tw-bg-opacity, 1));
3425
3441
  }
3442
+ .bg-transparent {
3443
+ background-color: transparent;
3444
+ }
3426
3445
  .bg-white {
3427
3446
  --tw-bg-opacity: 1;
3428
3447
  background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
@@ -3470,6 +3489,9 @@ input.tab:checked + .tab-content,
3470
3489
  .pl-2 {
3471
3490
  padding-left: 0.5rem;
3472
3491
  }
3492
+ .pr-14 {
3493
+ padding-right: 3.5rem;
3494
+ }
3473
3495
  .text-center {
3474
3496
  text-align: center;
3475
3497
  }
@@ -3668,6 +3690,51 @@ input.tab:checked + .tab-content,
3668
3690
  visibility: visible;
3669
3691
  }
3670
3692
  }
3693
+ @container (min-width: 14rem) {
3694
+
3695
+ .\@4xs\:flex-row {
3696
+ flex-direction: row;
3697
+ }
3698
+
3699
+ .\@4xs\:rounded-l-none {
3700
+ border-top-left-radius: 0px;
3701
+ border-bottom-left-radius: 0px;
3702
+ }
3703
+
3704
+ .\@4xs\:rounded-bl-md {
3705
+ border-bottom-left-radius: 0.375rem;
3706
+ }
3707
+
3708
+ .\@4xs\:rounded-tr-none {
3709
+ border-top-right-radius: 0px;
3710
+ }
3711
+ }
3712
+ @container (min-width: 28rem) {
3713
+
3714
+ .\@md\:flex-row {
3715
+ flex-direction: row;
3716
+ }
3717
+
3718
+ .\@md\:rounded-l-md {
3719
+ border-top-left-radius: 0.375rem;
3720
+ border-bottom-left-radius: 0.375rem;
3721
+ }
3722
+
3723
+ .\@md\:rounded-l-none {
3724
+ border-top-left-radius: 0px;
3725
+ border-bottom-left-radius: 0px;
3726
+ }
3727
+
3728
+ .\@md\:rounded-r-md {
3729
+ border-top-right-radius: 0.375rem;
3730
+ border-bottom-right-radius: 0.375rem;
3731
+ }
3732
+
3733
+ .\@md\:rounded-r-none {
3734
+ border-top-right-radius: 0px;
3735
+ border-bottom-right-radius: 0px;
3736
+ }
3737
+ }
3671
3738
  @media (min-width: 640px) {
3672
3739
 
3673
3740
  .sm\:max-w-5xl {
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
 
@@ -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.0",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -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
+ }