@genspectrum/dashboard-components 0.3.2 → 0.4.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 +45 -26
- package/dist/dashboard-components.js +340 -186
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +17 -8
- package/dist/style.css +58 -50
- package/package.json +2 -1
- package/src/preact/aggregatedData/aggregate.tsx +1 -1
- package/src/preact/components/info.stories.tsx +8 -8
- package/src/preact/components/info.tsx +113 -20
- package/src/preact/dateRangeSelector/computeInitialValues.spec.ts +99 -0
- package/src/preact/dateRangeSelector/computeInitialValues.ts +73 -0
- package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +93 -4
- package/src/preact/dateRangeSelector/date-range-selector.tsx +27 -88
- package/src/preact/dateRangeSelector/selectableOptions.ts +79 -0
- package/src/preact/locationFilter/location-filter.tsx +1 -1
- package/src/preact/mutationComparison/mutation-comparison.tsx +1 -1
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +3 -6
- package/src/preact/mutationFilter/mutation-filter.tsx +48 -54
- package/src/preact/mutations/mutations.tsx +1 -1
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +2 -2
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +1 -1
- package/src/web-components/input/gs-date-range-selector.stories.ts +11 -5
- package/src/web-components/input/gs-date-range-selector.tsx +22 -5
- package/src/web-components/input/gs-location-filter.stories.ts +6 -7
- package/src/web-components/input/gs-location-filter.tsx +3 -2
- package/src/web-components/input/gs-mutation-filter.stories.ts +1 -8
- package/src/web-components/input/gs-mutation-filter.tsx +1 -9
|
@@ -139,8 +139,22 @@ export declare class DateRangeSelectorComponent extends PreactLitAdapter {
|
|
|
139
139
|
* Must be a valid label from the preset labels or a `label` given in the `customSelectOptions`.
|
|
140
140
|
*
|
|
141
141
|
* If the value is invalid, the component will default to `'last6Months'`.
|
|
142
|
+
*
|
|
143
|
+
* It will be overwritten if `initialDateFrom` or `initialDateTo` is set.
|
|
142
144
|
*/
|
|
143
145
|
initialValue: 'custom' | 'allTimes' | 'last2Weeks' | 'lastMonth' | 'last2Months' | 'last3Months' | 'last6Months' | string;
|
|
146
|
+
/**
|
|
147
|
+
* A date string in the format `YYYY-MM-DD`.
|
|
148
|
+
* If set, the date range selector will be initialized with the given date (overwriting `initialValue` to `custom`).
|
|
149
|
+
* If `initialDateTo` is set, but this is unset, it will default to `earliestDate`.
|
|
150
|
+
*/
|
|
151
|
+
initialDateFrom: string;
|
|
152
|
+
/**
|
|
153
|
+
* A date string in the format `YYYY-MM-DD`.
|
|
154
|
+
* If set, the date range selector will be initialized with the given date (overwriting `initialValue` to `custom`).
|
|
155
|
+
* If `initialDateFrom` is set, but this is unset, it will default to the current date.
|
|
156
|
+
*/
|
|
157
|
+
initialDateTo: string;
|
|
144
158
|
/**
|
|
145
159
|
* The width of the component.
|
|
146
160
|
*
|
|
@@ -173,13 +187,14 @@ declare type LapisFilter = Record<string, string | number | null | boolean>;
|
|
|
173
187
|
* @fires {CustomEvent<Record<string, string>>} gs-location-changed
|
|
174
188
|
* Fired when a value from the datalist is selected or when a valid value is typed into the field.
|
|
175
189
|
* The `details` of this event contain an object with all `fields` as keys
|
|
176
|
-
* and the corresponding values as values, if they are
|
|
190
|
+
* and the corresponding values as values, even if they are `undefined`.
|
|
177
191
|
* Example:
|
|
178
192
|
* ```
|
|
179
193
|
* {
|
|
180
194
|
* continent: "Asia",
|
|
181
195
|
* country: "China",
|
|
182
|
-
* city: "Beijing"
|
|
196
|
+
* city: "Beijing",
|
|
197
|
+
* district: undefined,
|
|
183
198
|
* }
|
|
184
199
|
* ```
|
|
185
200
|
*/
|
|
@@ -347,12 +362,6 @@ export declare class MutationFilterComponent extends PreactLitAdapter {
|
|
|
347
362
|
* Visit https://genspectrum.github.io/dashboards/?path=/docs/components-size-of-components--docs for more information.
|
|
348
363
|
*/
|
|
349
364
|
width: string;
|
|
350
|
-
/**
|
|
351
|
-
* The height of the component.
|
|
352
|
-
*
|
|
353
|
-
* Visit https://genspectrum.github.io/dashboards/?path=/docs/components-size-of-components--docs for more information.
|
|
354
|
-
*/
|
|
355
|
-
height: string;
|
|
356
365
|
render(): JSX_2.Element;
|
|
357
366
|
}
|
|
358
367
|
|
package/dist/style.css
CHANGED
|
@@ -2853,24 +2853,30 @@ input.tab:checked + .tab-content,
|
|
|
2853
2853
|
.relative {
|
|
2854
2854
|
position: relative;
|
|
2855
2855
|
}
|
|
2856
|
+
.-right-3 {
|
|
2857
|
+
right: -0.75rem;
|
|
2858
|
+
}
|
|
2859
|
+
.-top-3 {
|
|
2860
|
+
top: -0.75rem;
|
|
2861
|
+
}
|
|
2856
2862
|
.right-2 {
|
|
2857
2863
|
right: 0.5rem;
|
|
2858
2864
|
}
|
|
2859
|
-
.right-6 {
|
|
2860
|
-
right: 1.5rem;
|
|
2861
|
-
}
|
|
2862
2865
|
.top-2 {
|
|
2863
2866
|
top: 0.5rem;
|
|
2864
2867
|
}
|
|
2865
|
-
.
|
|
2866
|
-
|
|
2867
|
-
}
|
|
2868
|
-
.z-50 {
|
|
2869
|
-
z-index: 50;
|
|
2868
|
+
.z-10 {
|
|
2869
|
+
z-index: 10;
|
|
2870
2870
|
}
|
|
2871
2871
|
.z-\[1\] {
|
|
2872
2872
|
z-index: 1;
|
|
2873
2873
|
}
|
|
2874
|
+
.float-right {
|
|
2875
|
+
float: right;
|
|
2876
|
+
}
|
|
2877
|
+
.m-1 {
|
|
2878
|
+
margin: 0.25rem;
|
|
2879
|
+
}
|
|
2874
2880
|
.m-2 {
|
|
2875
2881
|
margin: 0.5rem;
|
|
2876
2882
|
}
|
|
@@ -2886,24 +2892,21 @@ input.tab:checked + .tab-content,
|
|
|
2886
2892
|
margin-top: 1rem;
|
|
2887
2893
|
margin-bottom: 1rem;
|
|
2888
2894
|
}
|
|
2889
|
-
.mb-1 {
|
|
2890
|
-
margin-bottom: 0.25rem;
|
|
2891
|
-
}
|
|
2892
2895
|
.mb-2 {
|
|
2893
2896
|
margin-bottom: 0.5rem;
|
|
2894
2897
|
}
|
|
2895
|
-
.me-1 {
|
|
2896
|
-
margin-inline-end: 0.25rem;
|
|
2897
|
-
}
|
|
2898
2898
|
.ml-2 {
|
|
2899
2899
|
margin-left: 0.5rem;
|
|
2900
2900
|
}
|
|
2901
|
-
.
|
|
2902
|
-
margin-
|
|
2901
|
+
.mr-2 {
|
|
2902
|
+
margin-right: 0.5rem;
|
|
2903
2903
|
}
|
|
2904
2904
|
.mt-4 {
|
|
2905
2905
|
margin-top: 1rem;
|
|
2906
2906
|
}
|
|
2907
|
+
.inline-block {
|
|
2908
|
+
display: inline-block;
|
|
2909
|
+
}
|
|
2907
2910
|
.inline {
|
|
2908
2911
|
display: inline;
|
|
2909
2912
|
}
|
|
@@ -2937,13 +2940,6 @@ input.tab:checked + .tab-content,
|
|
|
2937
2940
|
.w-full {
|
|
2938
2941
|
width: 100%;
|
|
2939
2942
|
}
|
|
2940
|
-
.min-w-0 {
|
|
2941
|
-
min-width: 0px;
|
|
2942
|
-
}
|
|
2943
|
-
.min-w-max {
|
|
2944
|
-
min-width: -moz-max-content;
|
|
2945
|
-
min-width: max-content;
|
|
2946
|
-
}
|
|
2947
2943
|
.max-w-screen-lg {
|
|
2948
2944
|
max-width: 1024px;
|
|
2949
2945
|
}
|
|
@@ -2962,32 +2958,23 @@ input.tab:checked + .tab-content,
|
|
|
2962
2958
|
.flex-wrap {
|
|
2963
2959
|
flex-wrap: wrap;
|
|
2964
2960
|
}
|
|
2965
|
-
.flex-nowrap {
|
|
2966
|
-
flex-wrap: nowrap;
|
|
2967
|
-
}
|
|
2968
2961
|
.items-center {
|
|
2969
2962
|
align-items: center;
|
|
2970
2963
|
}
|
|
2971
|
-
.justify-end {
|
|
2972
|
-
justify-content: flex-end;
|
|
2973
|
-
}
|
|
2974
2964
|
.justify-center {
|
|
2975
2965
|
justify-content: center;
|
|
2976
2966
|
}
|
|
2977
2967
|
.justify-between {
|
|
2978
2968
|
justify-content: space-between;
|
|
2979
2969
|
}
|
|
2980
|
-
.gap-1 {
|
|
2981
|
-
gap: 0.25rem;
|
|
2982
|
-
}
|
|
2983
2970
|
.gap-2 {
|
|
2984
2971
|
gap: 0.5rem;
|
|
2985
2972
|
}
|
|
2986
2973
|
.overflow-auto {
|
|
2987
2974
|
overflow: auto;
|
|
2988
2975
|
}
|
|
2989
|
-
.overflow-
|
|
2990
|
-
overflow:
|
|
2976
|
+
.overflow-y-auto {
|
|
2977
|
+
overflow-y: auto;
|
|
2991
2978
|
}
|
|
2992
2979
|
.whitespace-nowrap {
|
|
2993
2980
|
white-space: nowrap;
|
|
@@ -3004,8 +2991,8 @@ input.tab:checked + .tab-content,
|
|
|
3004
2991
|
.rounded-box {
|
|
3005
2992
|
border-radius: var(--rounded-box, 1rem);
|
|
3006
2993
|
}
|
|
3007
|
-
.rounded-
|
|
3008
|
-
border-radius:
|
|
2994
|
+
.rounded-full {
|
|
2995
|
+
border-radius: 9999px;
|
|
3009
2996
|
}
|
|
3010
2997
|
.rounded-md {
|
|
3011
2998
|
border-radius: 0.375rem;
|
|
@@ -3032,9 +3019,11 @@ input.tab:checked + .tab-content,
|
|
|
3032
3019
|
.border-b-2 {
|
|
3033
3020
|
border-bottom-width: 2px;
|
|
3034
3021
|
}
|
|
3035
|
-
.border-
|
|
3036
|
-
|
|
3037
|
-
|
|
3022
|
+
.border-solid {
|
|
3023
|
+
border-style: solid;
|
|
3024
|
+
}
|
|
3025
|
+
.border-none {
|
|
3026
|
+
border-style: none;
|
|
3038
3027
|
}
|
|
3039
3028
|
.border-error {
|
|
3040
3029
|
--tw-border-opacity: 1;
|
|
@@ -3052,6 +3041,10 @@ input.tab:checked + .tab-content,
|
|
|
3052
3041
|
--tw-border-opacity: 1;
|
|
3053
3042
|
border-color: rgb(156 163 175 / var(--tw-border-opacity));
|
|
3054
3043
|
}
|
|
3044
|
+
.border-red-500 {
|
|
3045
|
+
--tw-border-opacity: 1;
|
|
3046
|
+
border-color: rgb(239 68 68 / var(--tw-border-opacity));
|
|
3047
|
+
}
|
|
3055
3048
|
.bg-base-100 {
|
|
3056
3049
|
--tw-bg-opacity: 1;
|
|
3057
3050
|
background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
|
|
@@ -3064,6 +3057,9 @@ input.tab:checked + .tab-content,
|
|
|
3064
3057
|
--tw-bg-opacity: 1;
|
|
3065
3058
|
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
|
3066
3059
|
}
|
|
3060
|
+
.p-1 {
|
|
3061
|
+
padding: 0.25rem;
|
|
3062
|
+
}
|
|
3067
3063
|
.p-2 {
|
|
3068
3064
|
padding: 0.5rem;
|
|
3069
3065
|
}
|
|
@@ -3071,21 +3067,13 @@ input.tab:checked + .tab-content,
|
|
|
3071
3067
|
padding-left: 0.5rem;
|
|
3072
3068
|
padding-right: 0.5rem;
|
|
3073
3069
|
}
|
|
3074
|
-
.px-2\.5 {
|
|
3075
|
-
padding-left: 0.625rem;
|
|
3076
|
-
padding-right: 0.625rem;
|
|
3077
|
-
}
|
|
3078
3070
|
.px-4 {
|
|
3079
3071
|
padding-left: 1rem;
|
|
3080
3072
|
padding-right: 1rem;
|
|
3081
3073
|
}
|
|
3082
|
-
.py-
|
|
3083
|
-
padding-top:
|
|
3084
|
-
padding-bottom:
|
|
3085
|
-
}
|
|
3086
|
-
.py-0\.5 {
|
|
3087
|
-
padding-top: 0.125rem;
|
|
3088
|
-
padding-bottom: 0.125rem;
|
|
3074
|
+
.py-1 {
|
|
3075
|
+
padding-top: 0.25rem;
|
|
3076
|
+
padding-bottom: 0.25rem;
|
|
3089
3077
|
}
|
|
3090
3078
|
.py-16 {
|
|
3091
3079
|
padding-top: 4rem;
|
|
@@ -3146,6 +3134,9 @@ input.tab:checked + .tab-content,
|
|
|
3146
3134
|
.underline {
|
|
3147
3135
|
text-decoration-line: underline;
|
|
3148
3136
|
}
|
|
3137
|
+
.opacity-90 {
|
|
3138
|
+
opacity: 0.9;
|
|
3139
|
+
}
|
|
3149
3140
|
.shadow {
|
|
3150
3141
|
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
|
3151
3142
|
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
|
|
@@ -3171,10 +3162,18 @@ input.tab:checked + .tab-content,
|
|
|
3171
3162
|
.duration-150 {
|
|
3172
3163
|
transition-duration: 150ms;
|
|
3173
3164
|
}
|
|
3165
|
+
.focus-within\:border-gray-400:focus-within {
|
|
3166
|
+
--tw-border-opacity: 1;
|
|
3167
|
+
border-color: rgb(156 163 175 / var(--tw-border-opacity));
|
|
3168
|
+
}
|
|
3174
3169
|
.hover\:bg-gray-100:hover {
|
|
3175
3170
|
--tw-bg-opacity: 1;
|
|
3176
3171
|
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
|
|
3177
3172
|
}
|
|
3173
|
+
.hover\:text-blue-700:hover {
|
|
3174
|
+
--tw-text-opacity: 1;
|
|
3175
|
+
color: rgb(29 78 216 / var(--tw-text-opacity));
|
|
3176
|
+
}
|
|
3178
3177
|
.hover\:text-blue-800:hover {
|
|
3179
3178
|
--tw-text-opacity: 1;
|
|
3180
3179
|
color: rgb(30 64 175 / var(--tw-text-opacity));
|
|
@@ -3186,6 +3185,15 @@ input.tab:checked + .tab-content,
|
|
|
3186
3185
|
.hover\:text-gray-700:hover {
|
|
3187
3186
|
--tw-text-opacity: 1;
|
|
3188
3187
|
color: rgb(55 65 81 / var(--tw-text-opacity));
|
|
3188
|
+
}
|
|
3189
|
+
.focus\:outline-none:focus {
|
|
3190
|
+
outline: 2px solid transparent;
|
|
3191
|
+
outline-offset: 2px;
|
|
3192
|
+
}
|
|
3193
|
+
.focus\:ring-0:focus {
|
|
3194
|
+
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
|
3195
|
+
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
|
3196
|
+
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
|
3189
3197
|
}.flatpickr-calendar{background:transparent;opacity:0;display:none;text-align:center;visibility:hidden;padding:0;-webkit-animation:none;animation:none;direction:ltr;border:0;font-size:14px;line-height:24px;border-radius:5px;position:absolute;width:307.875px;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-touch-action:manipulation;touch-action:manipulation;background:#fff;-webkit-box-shadow:1px 0 0 #e6e6e6,-1px 0 0 #e6e6e6,0 1px 0 #e6e6e6,0 -1px 0 #e6e6e6,0 3px 13px rgba(0,0,0,0.08);box-shadow:1px 0 0 #e6e6e6,-1px 0 0 #e6e6e6,0 1px 0 #e6e6e6,0 -1px 0 #e6e6e6,0 3px 13px rgba(0,0,0,0.08)}.flatpickr-calendar.open,.flatpickr-calendar.inline{opacity:1;max-height:640px;visibility:visible}.flatpickr-calendar.open{display:inline-block;z-index:99999}.flatpickr-calendar.animate.open{-webkit-animation:fpFadeInDown 300ms cubic-bezier(.23,1,.32,1);animation:fpFadeInDown 300ms cubic-bezier(.23,1,.32,1)}.flatpickr-calendar.inline{display:block;position:relative;top:2px}.flatpickr-calendar.static{position:absolute;top:calc(100% + 2px)}.flatpickr-calendar.static.open{z-index:999;display:block}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+1) .flatpickr-day.inRange:nth-child(7n+7){-webkit-box-shadow:none !important;box-shadow:none !important}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+2) .flatpickr-day.inRange:nth-child(7n+1){-webkit-box-shadow:-2px 0 0 #e6e6e6,5px 0 0 #e6e6e6;box-shadow:-2px 0 0 #e6e6e6,5px 0 0 #e6e6e6}.flatpickr-calendar .hasWeeks .dayContainer,.flatpickr-calendar .hasTime .dayContainer{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.flatpickr-calendar .hasWeeks .dayContainer{border-left:0}.flatpickr-calendar.hasTime .flatpickr-time{height:40px;border-top:1px solid #e6e6e6}.flatpickr-calendar.noCalendar.hasTime .flatpickr-time{height:auto}.flatpickr-calendar:before,.flatpickr-calendar:after{position:absolute;display:block;pointer-events:none;border:solid transparent;content:'';height:0;width:0;left:22px}.flatpickr-calendar.rightMost:before,.flatpickr-calendar.arrowRight:before,.flatpickr-calendar.rightMost:after,.flatpickr-calendar.arrowRight:after{left:auto;right:22px}.flatpickr-calendar.arrowCenter:before,.flatpickr-calendar.arrowCenter:after{left:50%;right:50%}.flatpickr-calendar:before{border-width:5px;margin:0 -5px}.flatpickr-calendar:after{border-width:4px;margin:0 -4px}.flatpickr-calendar.arrowTop:before,.flatpickr-calendar.arrowTop:after{bottom:100%}.flatpickr-calendar.arrowTop:before{border-bottom-color:#e6e6e6}.flatpickr-calendar.arrowTop:after{border-bottom-color:#fff}.flatpickr-calendar.arrowBottom:before,.flatpickr-calendar.arrowBottom:after{top:100%}.flatpickr-calendar.arrowBottom:before{border-top-color:#e6e6e6}.flatpickr-calendar.arrowBottom:after{border-top-color:#fff}.flatpickr-calendar:focus{outline:0}.flatpickr-wrapper{position:relative;display:inline-block}.flatpickr-months{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.flatpickr-months .flatpickr-month{background:transparent;color:rgba(0,0,0,0.9);fill:rgba(0,0,0,0.9);height:34px;line-height:1;text-align:center;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:hidden;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}.flatpickr-months .flatpickr-prev-month,.flatpickr-months .flatpickr-next-month{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;text-decoration:none;cursor:pointer;position:absolute;top:0;height:34px;padding:10px;z-index:3;color:rgba(0,0,0,0.9);fill:rgba(0,0,0,0.9)}.flatpickr-months .flatpickr-prev-month.flatpickr-disabled,.flatpickr-months .flatpickr-next-month.flatpickr-disabled{display:none}.flatpickr-months .flatpickr-prev-month i,.flatpickr-months .flatpickr-next-month i{position:relative}.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month,.flatpickr-months .flatpickr-next-month.flatpickr-prev-month{/*
|
|
3190
3198
|
/*rtl:begin:ignore*/left:0/*
|
|
3191
3199
|
/*rtl:end:ignore*/}/*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genspectrum/dashboard-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "GenSpectrum web components for building dashboards",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"lit"
|
|
57
57
|
],
|
|
58
58
|
"dependencies": {
|
|
59
|
+
"@floating-ui/dom": "^1.6.5",
|
|
59
60
|
"@lit/context": "^1.1.1",
|
|
60
61
|
"@lit/reactive-element": "^2.0.4",
|
|
61
62
|
"@lit/task": "^1.0.0",
|
|
@@ -103,7 +103,7 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({ data }) => {
|
|
|
103
103
|
return (
|
|
104
104
|
<div class='flex flex-row'>
|
|
105
105
|
<CsvDownloadButton className='mx-1 btn btn-xs' getData={() => data} filename='aggregate.csv' />
|
|
106
|
-
<Info>Info for aggregate</Info>
|
|
106
|
+
<Info height={'100px'}>Info for aggregate</Info>
|
|
107
107
|
</div>
|
|
108
108
|
);
|
|
109
109
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
-
import { expect,
|
|
2
|
+
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
|
3
3
|
|
|
4
4
|
import Info, { type InfoProps } from './info';
|
|
5
5
|
|
|
@@ -8,7 +8,7 @@ const meta: Meta<InfoProps> = {
|
|
|
8
8
|
component: Info,
|
|
9
9
|
parameters: { fetchMock: {} },
|
|
10
10
|
args: {
|
|
11
|
-
|
|
11
|
+
height: '100px',
|
|
12
12
|
},
|
|
13
13
|
};
|
|
14
14
|
|
|
@@ -28,16 +28,16 @@ export const ShowsInfoOnClick: StoryObj<InfoProps> = {
|
|
|
28
28
|
...InfoStory,
|
|
29
29
|
play: async ({ canvasElement }) => {
|
|
30
30
|
const canvas = within(canvasElement);
|
|
31
|
-
const
|
|
31
|
+
const openInfo = canvas.getByRole('button', { name: '?' });
|
|
32
32
|
|
|
33
|
-
await waitFor(() => expect(
|
|
33
|
+
await waitFor(() => expect(openInfo).toBeInTheDocument());
|
|
34
34
|
|
|
35
|
-
await
|
|
35
|
+
await userEvent.click(openInfo);
|
|
36
36
|
|
|
37
|
-
await waitFor(() => expect(canvas.getByText(tooltipText, { exact: false })).
|
|
37
|
+
await waitFor(() => expect(canvas.getByText(tooltipText, { exact: false })).toBeVisible());
|
|
38
38
|
|
|
39
|
-
await
|
|
39
|
+
await userEvent.click(document.body);
|
|
40
40
|
|
|
41
|
-
await waitFor(() => expect(canvas.queryByText(tooltipText, { exact: false })).not.
|
|
41
|
+
await waitFor(() => expect(canvas.queryByText(tooltipText, { exact: false })).not.toBeVisible());
|
|
42
42
|
},
|
|
43
43
|
};
|
|
@@ -1,42 +1,135 @@
|
|
|
1
|
+
import { autoUpdate, computePosition, offset, shift, size } from '@floating-ui/dom';
|
|
1
2
|
import { type FunctionComponent } from 'preact';
|
|
2
|
-
import { useState } from 'preact/hooks';
|
|
3
|
+
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
4
|
+
import { type MutableRefObject } from 'react';
|
|
3
5
|
|
|
4
6
|
export interface InfoProps {
|
|
5
|
-
|
|
6
|
-
height?: string;
|
|
7
|
-
width?: string;
|
|
8
|
-
};
|
|
7
|
+
height?: string;
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
const Info: FunctionComponent<InfoProps> = ({ children,
|
|
10
|
+
const Info: FunctionComponent<InfoProps> = ({ children, height }) => {
|
|
12
11
|
const [showHelp, setShowHelp] = useState(false);
|
|
12
|
+
const referenceRef = useRef<HTMLButtonElement>(null);
|
|
13
|
+
const floatingRef = useRef<HTMLDivElement>(null);
|
|
14
|
+
|
|
15
|
+
useFloatingUi(referenceRef, floatingRef, height, showHelp);
|
|
13
16
|
|
|
14
17
|
const toggleHelp = () => {
|
|
15
18
|
setShowHelp(!showHelp);
|
|
16
19
|
};
|
|
17
20
|
|
|
21
|
+
useCloseOnEsc(setShowHelp);
|
|
22
|
+
useCloseOnClickOutside(floatingRef, referenceRef, setShowHelp);
|
|
23
|
+
|
|
18
24
|
return (
|
|
19
|
-
<div className='relative'>
|
|
20
|
-
<button className='btn btn-xs' onClick={toggleHelp}>
|
|
25
|
+
<div className='relative z-10'>
|
|
26
|
+
<button type='button' className='btn btn-xs' onClick={toggleHelp} ref={referenceRef}>
|
|
21
27
|
?
|
|
22
28
|
</button>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
<div
|
|
30
|
+
ref={floatingRef}
|
|
31
|
+
className='bg-white p-2 border border-gray-100 shadow-lg rounded overflow-y-auto opacity-90'
|
|
32
|
+
style={{ position: 'absolute', zIndex: 10, display: showHelp ? '' : 'none' }}
|
|
33
|
+
>
|
|
34
|
+
<div className={'flex flex-col'}>{children}</div>
|
|
35
|
+
<button
|
|
36
|
+
onClick={() => setShowHelp(false)}
|
|
37
|
+
className={'float-right underline text-sm hover:text-blue-700 mr-2'}
|
|
27
38
|
>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Close
|
|
32
|
-
</button>
|
|
33
|
-
</div>
|
|
34
|
-
</div>
|
|
35
|
-
)}
|
|
39
|
+
Close
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
36
42
|
</div>
|
|
37
43
|
);
|
|
38
44
|
};
|
|
39
45
|
|
|
46
|
+
function useFloatingUi(
|
|
47
|
+
referenceRef: MutableRefObject<HTMLButtonElement | null>,
|
|
48
|
+
floatingRef: MutableRefObject<HTMLDivElement | null>,
|
|
49
|
+
height: string | undefined,
|
|
50
|
+
showHelp: boolean,
|
|
51
|
+
) {
|
|
52
|
+
const cleanupRef = useRef<Function | null>(null);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!referenceRef.current || !floatingRef.current) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const { current: reference } = referenceRef;
|
|
60
|
+
const { current: floating } = floatingRef;
|
|
61
|
+
|
|
62
|
+
const update = () => {
|
|
63
|
+
computePosition(reference, floating, {
|
|
64
|
+
middleware: [
|
|
65
|
+
offset(10),
|
|
66
|
+
shift(),
|
|
67
|
+
size({
|
|
68
|
+
apply({}) {
|
|
69
|
+
floating.style.width = '100vw';
|
|
70
|
+
floating.style.height = height ? height : '50vh';
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
],
|
|
74
|
+
}).then(({ x, y }) => {
|
|
75
|
+
floating.style.left = `${x}px`;
|
|
76
|
+
floating.style.top = `${y}px`;
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
update();
|
|
81
|
+
cleanupRef.current = autoUpdate(reference, floating, update);
|
|
82
|
+
|
|
83
|
+
return () => {
|
|
84
|
+
if (cleanupRef.current) {
|
|
85
|
+
cleanupRef.current();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}, [showHelp, height, referenceRef, floatingRef]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function useCloseOnClickOutside(
|
|
92
|
+
floatingRef: MutableRefObject<HTMLDivElement | null>,
|
|
93
|
+
referenceRef: MutableRefObject<HTMLButtonElement | null>,
|
|
94
|
+
setShowHelp: (value: ((prevState: boolean) => boolean) | boolean) => void,
|
|
95
|
+
) {
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
98
|
+
const path = event.composedPath();
|
|
99
|
+
if (
|
|
100
|
+
floatingRef.current &&
|
|
101
|
+
!path.includes(floatingRef.current) &&
|
|
102
|
+
referenceRef.current &&
|
|
103
|
+
!path.includes(referenceRef.current)
|
|
104
|
+
) {
|
|
105
|
+
setShowHelp(false);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
110
|
+
|
|
111
|
+
return () => {
|
|
112
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
113
|
+
};
|
|
114
|
+
}, [floatingRef, referenceRef, setShowHelp]);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function useCloseOnEsc(setShowHelp: (value: ((prevState: boolean) => boolean) | boolean) => void) {
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
120
|
+
if (event.key === 'Escape') {
|
|
121
|
+
setShowHelp(false);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
126
|
+
|
|
127
|
+
return () => {
|
|
128
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
129
|
+
};
|
|
130
|
+
}, [setShowHelp]);
|
|
131
|
+
}
|
|
132
|
+
|
|
40
133
|
export const InfoHeadline1: FunctionComponent = ({ children }) => {
|
|
41
134
|
return <h1 className='text-lg font-bold'>{children}</h1>;
|
|
42
135
|
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { computeInitialValues } from './computeInitialValues';
|
|
4
|
+
import { PRESET_VALUE_CUSTOM, PRESET_VALUE_LAST_3_MONTHS, PRESET_VALUE_LAST_6_MONTHS } from './selectableOptions';
|
|
5
|
+
|
|
6
|
+
const today = new Date();
|
|
7
|
+
const earliestDate = '1900-01-01';
|
|
8
|
+
|
|
9
|
+
describe('computeInitialValues', () => {
|
|
10
|
+
it('should compute for initial value if initial "from" and "to" are unset', () => {
|
|
11
|
+
const result = computeInitialValues(PRESET_VALUE_LAST_3_MONTHS, undefined, undefined, earliestDate, []);
|
|
12
|
+
|
|
13
|
+
const expectedFrom = new Date();
|
|
14
|
+
expectedFrom.setMonth(today.getMonth() - 3);
|
|
15
|
+
|
|
16
|
+
expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_LAST_3_MONTHS);
|
|
17
|
+
expectDateMatches(result.initialSelectedDateFrom, expectedFrom);
|
|
18
|
+
expectDateMatches(result.initialSelectedDateTo, today);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should fall back to default when initial value is unknown', () => {
|
|
22
|
+
const result = computeInitialValues('not a known value', undefined, undefined, earliestDate, []);
|
|
23
|
+
|
|
24
|
+
const expectedFrom = new Date();
|
|
25
|
+
expectedFrom.setMonth(today.getMonth() - 6);
|
|
26
|
+
|
|
27
|
+
expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_LAST_6_MONTHS);
|
|
28
|
+
expectDateMatches(result.initialSelectedDateFrom, expectedFrom);
|
|
29
|
+
expectDateMatches(result.initialSelectedDateTo, today);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should overwrite initial value if initial "from" is set', () => {
|
|
33
|
+
const initialDateFrom = '2020-01-01';
|
|
34
|
+
const result = computeInitialValues(PRESET_VALUE_LAST_3_MONTHS, initialDateFrom, undefined, earliestDate, []);
|
|
35
|
+
|
|
36
|
+
expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_CUSTOM);
|
|
37
|
+
expectDateMatches(result.initialSelectedDateFrom, new Date(initialDateFrom));
|
|
38
|
+
expectDateMatches(result.initialSelectedDateTo, today);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should overwrite initial value if initial "to" is set', () => {
|
|
42
|
+
const initialDateTo = '2020-01-01';
|
|
43
|
+
const result = computeInitialValues(PRESET_VALUE_LAST_3_MONTHS, undefined, initialDateTo, earliestDate, []);
|
|
44
|
+
|
|
45
|
+
expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_CUSTOM);
|
|
46
|
+
expectDateMatches(result.initialSelectedDateFrom, new Date(earliestDate));
|
|
47
|
+
expectDateMatches(result.initialSelectedDateTo, new Date(initialDateTo));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should overwrite initial value if initial "to" and "from" are set', () => {
|
|
51
|
+
const initialDateFrom = '2020-01-01';
|
|
52
|
+
const initialDateTo = '2022-01-01';
|
|
53
|
+
const result = computeInitialValues(
|
|
54
|
+
PRESET_VALUE_LAST_3_MONTHS,
|
|
55
|
+
initialDateFrom,
|
|
56
|
+
initialDateTo,
|
|
57
|
+
earliestDate,
|
|
58
|
+
[],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_CUSTOM);
|
|
62
|
+
expectDateMatches(result.initialSelectedDateFrom, new Date(initialDateFrom));
|
|
63
|
+
expectDateMatches(result.initialSelectedDateTo, new Date(initialDateTo));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should set initial "to" to "from" if "from" is after "to"', () => {
|
|
67
|
+
const initialDateFrom = '2020-01-01';
|
|
68
|
+
const initialDateTo = '1900-01-01';
|
|
69
|
+
const result = computeInitialValues(
|
|
70
|
+
PRESET_VALUE_LAST_3_MONTHS,
|
|
71
|
+
initialDateFrom,
|
|
72
|
+
initialDateTo,
|
|
73
|
+
earliestDate,
|
|
74
|
+
[],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(result.initialSelectedDateRange).toEqual(PRESET_VALUE_CUSTOM);
|
|
78
|
+
expectDateMatches(result.initialSelectedDateFrom, new Date(initialDateFrom));
|
|
79
|
+
expectDateMatches(result.initialSelectedDateTo, new Date(initialDateFrom));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should throw if initial "from" is not a valid date', () => {
|
|
83
|
+
expect(() =>
|
|
84
|
+
computeInitialValues(PRESET_VALUE_LAST_3_MONTHS, 'not a date', undefined, earliestDate, []),
|
|
85
|
+
).toThrowError('Invalid initialDateFrom');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should throw if initial "to" is not a valid date', () => {
|
|
89
|
+
expect(() =>
|
|
90
|
+
computeInitialValues(PRESET_VALUE_LAST_3_MONTHS, undefined, 'not a date', earliestDate, []),
|
|
91
|
+
).toThrowError('Invalid initialDateTo');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
function expectDateMatches(actual: Date, expected: Date) {
|
|
95
|
+
expect(actual.getFullYear()).toEqual(expected.getFullYear());
|
|
96
|
+
expect(actual.getMonth()).toEqual(expected.getMonth());
|
|
97
|
+
expect(actual.getDate()).toEqual(expected.getDate());
|
|
98
|
+
}
|
|
99
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CustomSelectOption,
|
|
3
|
+
getDatesForSelectorValue,
|
|
4
|
+
getSelectableOptions,
|
|
5
|
+
PRESET_VALUE_CUSTOM,
|
|
6
|
+
PRESET_VALUE_LAST_6_MONTHS,
|
|
7
|
+
type PresetOptionValues,
|
|
8
|
+
} from './selectableOptions';
|
|
9
|
+
import { UserFacingError } from '../components/error-display';
|
|
10
|
+
|
|
11
|
+
export function computeInitialValues<CustomLabel extends string>(
|
|
12
|
+
initialValue: PresetOptionValues | CustomLabel | undefined,
|
|
13
|
+
initialDateFrom: string | undefined,
|
|
14
|
+
initialDateTo: string | undefined,
|
|
15
|
+
earliestDate: string,
|
|
16
|
+
customSelectOptions: CustomSelectOption<CustomLabel>[],
|
|
17
|
+
): {
|
|
18
|
+
initialSelectedDateRange: CustomLabel | PresetOptionValues;
|
|
19
|
+
initialSelectedDateFrom: Date;
|
|
20
|
+
initialSelectedDateTo: Date;
|
|
21
|
+
} {
|
|
22
|
+
if (isUndefinedOrEmpty(initialDateFrom) && isUndefinedOrEmpty(initialDateTo)) {
|
|
23
|
+
const selectableOptions = getSelectableOptions(customSelectOptions);
|
|
24
|
+
const initialSelectedDateRange =
|
|
25
|
+
initialValue !== undefined && selectableOptions.some((option) => option.value === initialValue)
|
|
26
|
+
? initialValue
|
|
27
|
+
: PRESET_VALUE_LAST_6_MONTHS;
|
|
28
|
+
|
|
29
|
+
const { dateFrom, dateTo } = getDatesForSelectorValue(
|
|
30
|
+
initialSelectedDateRange,
|
|
31
|
+
customSelectOptions,
|
|
32
|
+
earliestDate,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
initialSelectedDateRange,
|
|
37
|
+
initialSelectedDateFrom: dateFrom,
|
|
38
|
+
initialSelectedDateTo: dateTo,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const initialSelectedDateFrom = isUndefinedOrEmpty(initialDateFrom)
|
|
43
|
+
? new Date(earliestDate)
|
|
44
|
+
: new Date(initialDateFrom);
|
|
45
|
+
let initialSelectedDateTo = isUndefinedOrEmpty(initialDateTo) ? new Date() : new Date(initialDateTo);
|
|
46
|
+
|
|
47
|
+
if (isNaN(initialSelectedDateFrom.getTime())) {
|
|
48
|
+
throw new UserFacingError(
|
|
49
|
+
'Invalid initialDateFrom',
|
|
50
|
+
`Invalid initialDateFrom "${initialDateFrom}", It must be of the format YYYY-MM-DD`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (isNaN(initialSelectedDateTo.getTime())) {
|
|
54
|
+
throw new UserFacingError(
|
|
55
|
+
'Invalid initialDateTo',
|
|
56
|
+
`Invalid initialDateTo "${initialDateTo}", It must be of the format YYYY-MM-DD`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (initialSelectedDateFrom > initialSelectedDateTo) {
|
|
61
|
+
initialSelectedDateTo = initialSelectedDateFrom;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
initialSelectedDateRange: PRESET_VALUE_CUSTOM,
|
|
66
|
+
initialSelectedDateFrom,
|
|
67
|
+
initialSelectedDateTo,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isUndefinedOrEmpty(value: string | undefined): value is undefined | '' {
|
|
72
|
+
return value === undefined || value === '';
|
|
73
|
+
}
|