@genspectrum/dashboard-components 0.3.2 → 0.4.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 (35) hide show
  1. package/custom-elements.json +45 -26
  2. package/dist/dashboard-components.js +518 -434
  3. package/dist/dashboard-components.js.map +1 -1
  4. package/dist/genspectrum-components.d.ts +21 -12
  5. package/dist/style.css +150 -229
  6. package/package.json +3 -1
  7. package/src/preact/aggregatedData/aggregate.tsx +1 -1
  8. package/src/preact/components/SegmentSelector.tsx +0 -1
  9. package/src/preact/components/checkbox-selector.tsx +7 -9
  10. package/src/preact/components/dropdown.tsx +40 -0
  11. package/src/preact/components/info.stories.tsx +8 -8
  12. package/src/preact/components/info.tsx +38 -19
  13. package/src/preact/components/mutation-type-selector.tsx +0 -1
  14. package/src/preact/components/proportion-selector-dropdown.tsx +9 -18
  15. package/src/preact/components/tabs.tsx +12 -3
  16. package/src/preact/dateRangeSelector/computeInitialValues.spec.ts +99 -0
  17. package/src/preact/dateRangeSelector/computeInitialValues.ts +73 -0
  18. package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +93 -4
  19. package/src/preact/dateRangeSelector/date-range-selector.tsx +49 -106
  20. package/src/preact/dateRangeSelector/selectableOptions.ts +79 -0
  21. package/src/preact/locationFilter/location-filter.tsx +1 -1
  22. package/src/preact/mutationComparison/mutation-comparison.tsx +3 -3
  23. package/src/preact/mutationFilter/mutation-filter.stories.tsx +3 -6
  24. package/src/preact/mutationFilter/mutation-filter.tsx +48 -54
  25. package/src/preact/mutations/mutations.tsx +3 -4
  26. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +3 -5
  27. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +3 -3
  28. package/src/preact/shared/floating-ui/hooks.ts +83 -0
  29. package/src/web-components/input/gs-date-range-selector.stories.ts +11 -5
  30. package/src/web-components/input/gs-date-range-selector.tsx +22 -5
  31. package/src/web-components/input/gs-location-filter.stories.ts +6 -7
  32. package/src/web-components/input/gs-location-filter.tsx +3 -2
  33. package/src/web-components/input/gs-mutation-filter.stories.ts +1 -8
  34. package/src/web-components/input/gs-mutation-filter.tsx +1 -9
  35. package/src/web-components/visualization/gs-prevalence-over-time.tsx +1 -1
@@ -1,31 +1,29 @@
1
+ import { Dropdown } from './dropdown';
2
+
1
3
  export type CheckboxItem = {
2
4
  label: string;
3
5
  checked: boolean;
4
6
  };
5
7
 
6
8
  export interface CheckboxSelectorProps<Item extends CheckboxItem = CheckboxItem> {
7
- className?: string;
8
9
  items: Item[];
9
10
  label: string;
10
11
  setItems: (items: Item[]) => void;
11
12
  }
12
13
 
13
14
  export const CheckboxSelector = <Item extends CheckboxItem>({
14
- className,
15
15
  items,
16
16
  label,
17
17
  setItems,
18
18
  }: CheckboxSelectorProps<Item>) => {
19
19
  return (
20
- <div class={`dropdown ${className}`}>
21
- <div tabIndex={0} role='button' class='btn btn-xs text-nowrap'>
22
- {label}
23
- </div>
24
- <ul tabIndex={0} class='p-2 shadow menu dropdown-content z-[1] bg-base-100 rounded-box'>
20
+ <Dropdown buttonTitle={label} placement={'bottom-start'}>
21
+ <ul>
25
22
  {items.map((item, index) => (
26
- <li class='flex flex-row items-center' key={item.label}>
23
+ <li className='flex flex-row items-center' key={item.label}>
27
24
  <label>
28
25
  <input
26
+ className={'mr-2'}
29
27
  type='checkbox'
30
28
  id={`item-${index}`}
31
29
  checked={item.checked}
@@ -41,6 +39,6 @@ export const CheckboxSelector = <Item extends CheckboxItem>({
41
39
  </li>
42
40
  ))}
43
41
  </ul>
44
- </div>
42
+ </Dropdown>
45
43
  );
46
44
  };
@@ -0,0 +1,40 @@
1
+ import { flip, offset, shift } from '@floating-ui/dom';
2
+ import { type Placement } from '@floating-ui/utils';
3
+ import { type FunctionComponent } from 'preact';
4
+ import { useRef, useState } from 'preact/hooks';
5
+
6
+ import { useCloseOnClickOutside, useCloseOnEsc, useFloatingUi } from '../shared/floating-ui/hooks';
7
+
8
+ interface DropdownProps {
9
+ buttonTitle: string;
10
+ placement?: Placement;
11
+ }
12
+
13
+ export const dropdownClass =
14
+ 'z-10 absolute w-max top-0 left-0 bg-white p-4 border border-gray-200 shadow-lg rounded-md';
15
+
16
+ export const Dropdown: FunctionComponent<DropdownProps> = ({ children, buttonTitle, placement }) => {
17
+ const [showContent, setShowContent] = useState(false);
18
+ const referenceRef = useRef<HTMLButtonElement>(null);
19
+ const floatingRef = useRef<HTMLDivElement>(null);
20
+
21
+ useFloatingUi(referenceRef, floatingRef, [offset(4), shift(), flip()], placement);
22
+
23
+ useCloseOnClickOutside(floatingRef, referenceRef, setShowContent);
24
+ useCloseOnEsc(setShowContent);
25
+
26
+ const toggle = () => {
27
+ setShowContent(!showContent);
28
+ };
29
+
30
+ return (
31
+ <div>
32
+ <button type='button' className='btn btn-xs whitespace-nowrap' onClick={toggle} ref={referenceRef}>
33
+ {buttonTitle}
34
+ </button>
35
+ <div ref={floatingRef} className={`${dropdownClass} ${showContent ? '' : 'hidden'}`}>
36
+ {children}
37
+ </div>
38
+ </div>
39
+ );
40
+ };
@@ -1,5 +1,5 @@
1
1
  import { type Meta, type StoryObj } from '@storybook/preact';
2
- import { expect, fireEvent, waitFor, within } from '@storybook/test';
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
- size: { width: '400px', height: '100px' },
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 loading = canvas.getByRole('button', { name: '?' });
31
+ const openInfo = canvas.getByRole('button', { name: '?' });
32
32
 
33
- await waitFor(() => expect(loading).toBeInTheDocument());
33
+ await waitFor(() => expect(openInfo).toBeInTheDocument());
34
34
 
35
- await fireEvent.click(loading);
35
+ await userEvent.click(openInfo);
36
36
 
37
- await waitFor(() => expect(canvas.getByText(tooltipText, { exact: false })).toBeInTheDocument());
37
+ await waitFor(() => expect(canvas.getByText(tooltipText, { exact: false })).toBeVisible());
38
38
 
39
- await fireEvent.click(canvas.getByRole('button', { name: 'Close' }));
39
+ await userEvent.click(document.body);
40
40
 
41
- await waitFor(() => expect(canvas.queryByText(tooltipText, { exact: false })).not.toBeInTheDocument());
41
+ await waitFor(() => expect(canvas.queryByText(tooltipText, { exact: false })).not.toBeVisible());
42
42
  },
43
43
  };
@@ -1,38 +1,57 @@
1
+ import { offset, shift, size } from '@floating-ui/dom';
1
2
  import { type FunctionComponent } from 'preact';
2
- import { useState } from 'preact/hooks';
3
+ import { useRef, useState } from 'preact/hooks';
4
+
5
+ import { dropdownClass } from './dropdown';
6
+ import { useCloseOnClickOutside, useCloseOnEsc, useFloatingUi } from '../shared/floating-ui/hooks';
3
7
 
4
8
  export interface InfoProps {
5
- size?: {
6
- height?: string;
7
- width?: string;
8
- };
9
+ height?: string;
9
10
  }
10
11
 
11
- const Info: FunctionComponent<InfoProps> = ({ children, size }) => {
12
+ const Info: FunctionComponent<InfoProps> = ({ children, height }) => {
12
13
  const [showHelp, setShowHelp] = useState(false);
14
+ const referenceRef = useRef<HTMLButtonElement>(null);
15
+ const floatingRef = useRef<HTMLDivElement>(null);
16
+
17
+ useFloatingUi(referenceRef, floatingRef, [
18
+ offset(10),
19
+ shift(),
20
+ size({
21
+ apply() {
22
+ if (!floatingRef.current) {
23
+ return;
24
+ }
25
+ floatingRef.current.style.width = '100vw';
26
+ floatingRef.current.style.height = height ? height : '50vh';
27
+ },
28
+ }),
29
+ ]);
13
30
 
14
31
  const toggleHelp = () => {
15
32
  setShowHelp(!showHelp);
16
33
  };
17
34
 
35
+ useCloseOnEsc(setShowHelp);
36
+ useCloseOnClickOutside(floatingRef, referenceRef, setShowHelp);
37
+
18
38
  return (
19
39
  <div className='relative'>
20
- <button className='btn btn-xs' onClick={toggleHelp}>
40
+ <button type='button' className='btn btn-xs' onClick={toggleHelp} ref={referenceRef}>
21
41
  ?
22
42
  </button>
23
- {showHelp && (
24
- <div
25
- className='absolute top-8 right-6 bg-white p-2 border border-black flex flex-col overflow-auto shadow-lg rounded z-50'
26
- style={size}
43
+ <div
44
+ ref={floatingRef}
45
+ className={`${dropdownClass} overflow-y-auto opacity-90 ${showHelp ? '' : 'hidden'}`}
46
+ >
47
+ <div className={'flex flex-col'}>{children}</div>
48
+ <button
49
+ onClick={() => setShowHelp(false)}
50
+ className={'float-right underline text-sm hover:text-blue-700 mr-2'}
27
51
  >
28
- <div className='flex flex-col'>{children}</div>
29
- <div className='flex justify-end'>
30
- <button className='text-sm underline mt-2' onClick={toggleHelp}>
31
- Close
32
- </button>
33
- </div>
34
- </div>
35
- )}
52
+ Close
53
+ </button>
54
+ </div>
36
55
  </div>
37
56
  );
38
57
  };
@@ -21,7 +21,6 @@ export const MutationTypeSelector: FunctionComponent<MutationTypeSelectorProps>
21
21
 
22
22
  return (
23
23
  <CheckboxSelector
24
- className='mx-1'
25
24
  items={displayedMutationTypes}
26
25
  label={mutationTypesSelectorLabel}
27
26
  setItems={(items) => setDisplayedMutationTypes(items)}
@@ -1,33 +1,24 @@
1
1
  import { type FunctionComponent } from 'preact';
2
2
 
3
+ import { Dropdown } from './dropdown';
3
4
  import { ProportionSelector, type ProportionSelectorProps } from './proportion-selector';
4
5
 
5
- export interface ProportionSelectorDropdownProps extends ProportionSelectorProps {
6
- openDirection?: 'left' | 'right';
7
- }
6
+ export type ProportionSelectorDropdownProps = ProportionSelectorProps;
8
7
 
9
8
  export const ProportionSelectorDropdown: FunctionComponent<ProportionSelectorDropdownProps> = ({
10
9
  proportionInterval,
11
10
  setMinProportion,
12
11
  setMaxProportion,
13
- openDirection = 'right',
14
12
  }) => {
15
13
  const label = `${(proportionInterval.min * 100).toFixed(1)}% - ${(proportionInterval.max * 100).toFixed(1)}%`;
16
14
 
17
15
  return (
18
- <div class={`dropdown ${openDirection === 'left' ? 'dropdown-end' : ''}`}>
19
- <div tabIndex={0} role='button' class='btn btn-xs whitespace-nowrap'>
20
- Proportion {label}
21
- </div>
22
- <ul tabIndex={0} class='p-2 shadow menu dropdown-content z-[1] bg-base-100 rounded-box w-72'>
23
- <div class='mb-2 ml-2'>
24
- <ProportionSelector
25
- proportionInterval={proportionInterval}
26
- setMinProportion={setMinProportion}
27
- setMaxProportion={setMaxProportion}
28
- />
29
- </div>
30
- </ul>
31
- </div>
16
+ <Dropdown buttonTitle={`Proportion ${label}`} placement={'bottom-start'}>
17
+ <ProportionSelector
18
+ proportionInterval={proportionInterval}
19
+ setMinProportion={setMinProportion}
20
+ setMaxProportion={setMaxProportion}
21
+ />
22
+ </Dropdown>
32
23
  );
33
24
  };
@@ -17,11 +17,20 @@ const Tabs: FunctionComponent<ComponentTabsProps> = ({ tabs, toolbar }) => {
17
17
  const [heightOfTabs, setHeightOfTabs] = useState('3rem');
18
18
  const tabRef = useRef<HTMLDivElement>(null);
19
19
 
20
- useEffect(() => {
20
+ const updateHeightOfTabs = () => {
21
21
  if (tabRef.current) {
22
22
  const heightOfTabs = tabRef.current.getBoundingClientRect().height;
23
23
  setHeightOfTabs(`${heightOfTabs}px`);
24
24
  }
25
+ };
26
+
27
+ useEffect(() => {
28
+ updateHeightOfTabs();
29
+
30
+ window.addEventListener('resize', updateHeightOfTabs);
31
+ return () => {
32
+ window.removeEventListener('resize', updateHeightOfTabs);
33
+ };
25
34
  }, []);
26
35
 
27
36
  const tabElements = (
@@ -51,9 +60,9 @@ const Tabs: FunctionComponent<ComponentTabsProps> = ({ tabs, toolbar }) => {
51
60
 
52
61
  return (
53
62
  <div className='h-full w-full'>
54
- <div ref={tabRef} className='flex flex-row justify-between'>
63
+ <div ref={tabRef} className='flex flex-row justify-between flex-wrap'>
55
64
  {tabElements}
56
- {toolbar && <div className='py-2'>{toolbarElement}</div>}
65
+ {toolbar && <div className='py-2 flex flex-wrap gap-y-1'>{toolbarElement}</div>}
57
66
  </div>
58
67
  <div
59
68
  className={`p-2 border-2 border-gray-100 rounded-b-md rounded-tr-md ${activeTab === tabs[0].title ? '' : 'rounded-tl-md'}`}
@@ -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
+ }
@@ -1,9 +1,10 @@
1
1
  import { withActions } from '@storybook/addon-actions/decorator';
2
2
  import { type Meta, type StoryObj } from '@storybook/preact';
3
+ import { expect, waitFor, within } from '@storybook/test';
4
+ import dayjs from 'dayjs/esm';
3
5
 
6
+ import { DateRangeSelector, type DateRangeSelectorProps } from './date-range-selector';
4
7
  import {
5
- DateRangeSelector,
6
- type DateRangeSelectorProps,
7
8
  PRESET_VALUE_ALL_TIMES,
8
9
  PRESET_VALUE_CUSTOM,
9
10
  PRESET_VALUE_LAST_2_MONTHS,
@@ -11,10 +12,12 @@ import {
11
12
  PRESET_VALUE_LAST_3_MONTHS,
12
13
  PRESET_VALUE_LAST_6_MONTHS,
13
14
  PRESET_VALUE_LAST_MONTH,
14
- } from './date-range-selector';
15
+ } from './selectableOptions';
15
16
  import { LAPIS_URL } from '../../constants';
16
17
  import { LapisUrlContext } from '../LapisUrlContext';
17
18
 
19
+ const earliestDate = '1970-01-01';
20
+
18
21
  const meta: Meta<DateRangeSelectorProps<'CustomDateRange'>> = {
19
22
  title: 'Input/DateRangeSelector',
20
23
  component: DateRangeSelector,
@@ -58,10 +61,12 @@ const meta: Meta<DateRangeSelectorProps<'CustomDateRange'>> = {
58
61
  },
59
62
  args: {
60
63
  customSelectOptions: [{ label: 'CustomDateRange', dateFrom: '2021-01-01', dateTo: '2021-12-31' }],
61
- earliestDate: '1970-01-01',
64
+ earliestDate,
62
65
  initialValue: PRESET_VALUE_LAST_3_MONTHS,
63
66
  dateColumn: 'aDateColumn',
64
67
  width: '100%',
68
+ initialDateFrom: '',
69
+ initialDateTo: '',
65
70
  },
66
71
  decorators: [withActions],
67
72
  };
@@ -75,9 +80,93 @@ export const Primary: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
75
80
  customSelectOptions={args.customSelectOptions}
76
81
  earliestDate={args.earliestDate}
77
82
  initialValue={args.initialValue}
83
+ initialDateFrom={args.initialDateFrom}
84
+ initialDateTo={args.initialDateTo}
78
85
  width={args.width}
79
86
  dateColumn={args.dateColumn}
80
87
  />
81
88
  </LapisUrlContext.Provider>
82
89
  ),
83
90
  };
91
+
92
+ export const SetCorrectInitialValues: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
93
+ ...Primary,
94
+ args: {
95
+ ...Primary.args,
96
+ initialValue: 'CustomDateRange',
97
+ },
98
+ play: async ({ canvasElement }) => {
99
+ const canvas = within(canvasElement);
100
+
101
+ const dateFrom = () => canvas.getByPlaceholderText('Date from');
102
+ const dateTo = () => canvas.getByPlaceholderText('Date to');
103
+ const selectField = () => canvas.getByRole('combobox');
104
+
105
+ await waitFor(() => {
106
+ expect(selectField()).toHaveValue('CustomDateRange');
107
+ expect(dateFrom()).toHaveValue('2021-01-01');
108
+ expect(dateTo()).toHaveValue('2021-12-31');
109
+ });
110
+ },
111
+ };
112
+
113
+ const initialDateFrom = '2000-01-01';
114
+
115
+ export const SetCorrectInitialDateFrom: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
116
+ ...Primary,
117
+ args: {
118
+ ...Primary.args,
119
+ initialDateFrom,
120
+ },
121
+ play: async ({ canvasElement }) => {
122
+ const canvas = within(canvasElement);
123
+
124
+ const dateFrom = () => canvas.getByPlaceholderText('Date from');
125
+ const dateTo = () => canvas.getByPlaceholderText('Date to');
126
+ const selectField = () => canvas.getByRole('combobox');
127
+
128
+ await waitFor(() => {
129
+ expect(selectField()).toHaveValue(PRESET_VALUE_CUSTOM);
130
+ expect(dateFrom()).toHaveValue(initialDateFrom);
131
+ expect(dateTo()).toHaveValue(dayjs().format('YYYY-MM-DD'));
132
+ });
133
+ },
134
+ };
135
+
136
+ const initialDateTo = '2000-01-01';
137
+
138
+ export const SetCorrectInitialDateTo: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
139
+ ...Primary,
140
+ args: {
141
+ ...Primary.args,
142
+ initialDateTo,
143
+ },
144
+ play: async ({ canvasElement }) => {
145
+ const canvas = within(canvasElement);
146
+
147
+ const dateFrom = () => canvas.getByPlaceholderText('Date from');
148
+ const dateTo = () => canvas.getByPlaceholderText('Date to');
149
+ const selectField = () => canvas.getByRole('combobox');
150
+
151
+ await waitFor(() => {
152
+ expect(selectField()).toHaveValue(PRESET_VALUE_CUSTOM);
153
+ expect(dateFrom()).toHaveValue(earliestDate);
154
+ expect(dateTo()).toHaveValue(initialDateTo);
155
+ });
156
+ },
157
+ };
158
+
159
+ export const HandlesInvalidInitialDateFrom: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
160
+ ...Primary,
161
+ args: {
162
+ ...Primary.args,
163
+ initialDateFrom: 'not a date',
164
+ },
165
+ play: async ({ canvasElement }) => {
166
+ const canvas = within(canvasElement);
167
+
168
+ await waitFor(() => {
169
+ expect(canvas.getByText('Oops! Something went wrong.')).toBeVisible();
170
+ });
171
+ },
172
+ };