@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.
- package/custom-elements.json +86 -61
- package/dist/{LineageFilterChangedEvent-COWV-Y0k.js → LineageFilterChangedEvent-DkvWdq_G.js} +2 -2
- package/dist/LineageFilterChangedEvent-DkvWdq_G.js.map +1 -0
- package/dist/components.d.ts +46 -34
- package/dist/components.js +304 -146
- package/dist/components.js.map +1 -1
- package/dist/style.css +76 -9
- package/dist/util.d.ts +11 -13
- package/dist/util.js +1 -1
- package/package.json +1 -1
- package/src/preact/components/clearable-select.stories.tsx +75 -0
- package/src/preact/components/clearable-select.tsx +76 -0
- package/src/preact/components/downshift-combobox.tsx +9 -7
- package/src/preact/dateRangeFilter/computeInitialValues.spec.ts +31 -33
- package/src/preact/dateRangeFilter/computeInitialValues.ts +2 -15
- package/src/preact/dateRangeFilter/date-picker.tsx +66 -0
- package/src/preact/dateRangeFilter/date-range-filter.stories.tsx +69 -31
- package/src/preact/dateRangeFilter/date-range-filter.tsx +136 -139
- package/src/preact/dateRangeFilter/dateRangeOption.ts +11 -11
- package/src/preact/shared/WithClassName/WithClassName.ts +1 -0
- package/src/preact/shared/icons/DeleteIcon.tsx +3 -0
- package/src/preact/shared/stories/expectOptionSelected.tsx +7 -0
- package/src/utilEntrypoint.ts +1 -1
- package/src/web-components/MutationAnnotations.mdx +33 -0
- package/src/web-components/ResizeContainer.mdx +1 -1
- package/src/web-components/errorHandling.mdx +1 -1
- package/src/web-components/gs-app.ts +2 -2
- package/src/web-components/input/gs-date-range-filter.stories.ts +38 -32
- package/src/web-components/input/gs-date-range-filter.tsx +8 -2
- package/src/web-components/input/gs-lineage-filter.tsx +1 -1
- package/src/web-components/input/gs-location-filter.tsx +1 -1
- package/src/web-components/input/gs-mutation-filter.tsx +1 -1
- package/src/web-components/input/gs-text-filter.tsx +1 -1
- package/src/web-components/visualization/gs-aggregate.tsx +2 -2
- package/src/web-components/visualization/gs-mutation-comparison.tsx +5 -2
- package/src/web-components/visualization/gs-mutations-over-time.tsx +5 -2
- package/src/web-components/visualization/gs-mutations.tsx +5 -2
- package/src/web-components/visualization/gs-number-sequences-over-time.tsx +2 -2
- package/src/web-components/visualization/gs-prevalence-over-time.tsx +2 -2
- package/src/web-components/visualization/gs-relative-growth-advantage.tsx +2 -2
- package/src/web-components/visualization/gs-sequences-by-location.tsx +2 -2
- package/src/web-components/visualization/gs-statistics.tsx +2 -2
- package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +2 -2
- package/standalone-bundle/dashboard-components.js +6624 -6538
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/standalone-bundle/style.css +1 -1
- 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<
|
|
77
|
-
constructor(detail:
|
|
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
|
|
139
|
+
export declare type DateRangeValue = default_2.infer<typeof dateRangeValueSchema>;
|
|
140
140
|
|
|
141
|
-
declare
|
|
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-
|
|
1111
|
+
'gs-mutation-filter': MutationFilterComponent;
|
|
1114
1112
|
}
|
|
1115
1113
|
interface HTMLElementEventMap {
|
|
1116
|
-
'gs-
|
|
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-
|
|
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-
|
|
1130
|
+
'gs-lineage-filter': LineageFilterComponent;
|
|
1133
1131
|
}
|
|
1134
1132
|
interface HTMLElementEventMap {
|
|
1135
|
-
'gs-
|
|
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-
|
|
1141
|
+
'gs-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1144
1142
|
}
|
|
1145
1143
|
}
|
|
1146
1144
|
}
|
package/dist/util.js
CHANGED
package/package.json
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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=
|
|
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
|
|
25
|
-
expectDateMatches(result
|
|
26
|
-
expectDateMatches(result
|
|
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
|
|
33
|
-
expectDateMatches(result
|
|
34
|
-
expectDateMatches(result
|
|
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
|
|
41
|
-
expectDateMatches(result
|
|
42
|
-
expectDateMatches(result
|
|
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
|
|
70
|
-
expectDateMatches(result
|
|
71
|
-
expectDateMatches(result
|
|
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
|
|
79
|
-
expectDateMatches(result
|
|
80
|
-
expectDateMatches(result
|
|
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
|
|
96
|
-
expectDateMatches(result
|
|
97
|
-
expectDateMatches(result
|
|
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
|
|
113
|
-
expectDateMatches(result
|
|
114
|
-
expectDateMatches(result
|
|
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
|
|
131
|
-
expect(actual
|
|
132
|
-
expect(actual
|
|
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
|
-
|
|
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
|
+
}
|