@genspectrum/dashboard-components 0.6.11 → 0.6.13

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/dist/dashboard-components.js +1079 -873
  2. package/dist/dashboard-components.js.map +1 -1
  3. package/dist/genspectrum-components.d.ts +3 -3
  4. package/dist/style.css +117 -24
  5. package/package.json +2 -2
  6. package/src/preact/components/checkbox-selector.stories.tsx +93 -11
  7. package/src/preact/components/checkbox-selector.tsx +19 -0
  8. package/src/preact/components/color-scale-selector-dropdown.tsx +5 -3
  9. package/src/preact/components/dropdown.tsx +3 -3
  10. package/src/preact/components/loading-display.tsx +8 -1
  11. package/src/preact/components/mutation-type-selector.stories.tsx +115 -0
  12. package/src/preact/components/mutation-type-selector.tsx +33 -8
  13. package/src/preact/components/percent-input.stories.tsx +93 -0
  14. package/src/preact/components/percent-intput.tsx +4 -0
  15. package/src/preact/components/proportion-selector-dropdown.stories.tsx +2 -2
  16. package/src/preact/components/proportion-selector-dropdown.tsx +9 -7
  17. package/src/preact/components/proportion-selector.stories.tsx +4 -4
  18. package/src/preact/components/proportion-selector.tsx +46 -12
  19. package/src/preact/components/segment-selector.stories.tsx +151 -0
  20. package/src/preact/components/{SegmentSelector.tsx → segment-selector.tsx} +29 -20
  21. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +1 -1
  22. package/src/preact/mutationComparison/mutation-comparison.tsx +1 -1
  23. package/src/preact/mutationComparison/queryMutationData.ts +1 -1
  24. package/src/preact/mutations/mutations-grid.tsx +5 -1
  25. package/src/preact/mutations/mutations.tsx +1 -1
  26. package/src/preact/mutations/queryMutations.ts +1 -1
  27. package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +4 -4
  28. package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +3 -2
  29. package/src/preact/mutationsOverTime/mutations-over-time.tsx +1 -1
  30. package/src/preact/numberSequencesOverTime/number-sequences-over-time.tsx +3 -2
  31. package/src/preact/useQuery.ts +1 -1
  32. package/src/query/queryMutationsOverTime.ts +3 -3
  33. package/src/utils/map2d.spec.ts +83 -22
  34. package/src/utils/map2d.ts +158 -0
  35. package/src/utils/Map2d.ts +0 -75
@@ -990,21 +990,21 @@ declare global {
990
990
 
991
991
  declare global {
992
992
  interface HTMLElementTagNameMap {
993
- 'gs-aggregate-component': AggregateComponent;
993
+ 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
994
994
  }
995
995
  }
996
996
 
997
997
 
998
998
  declare global {
999
999
  interface HTMLElementTagNameMap {
1000
- 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
1000
+ 'gs-mutations-over-time-component': MutationsOverTimeComponent;
1001
1001
  }
1002
1002
  }
1003
1003
 
1004
1004
 
1005
1005
  declare global {
1006
1006
  interface HTMLElementTagNameMap {
1007
- 'gs-mutations-over-time-component': MutationsOverTimeComponent;
1007
+ 'gs-aggregate-component': AggregateComponent;
1008
1008
  }
1009
1009
  }
1010
1010
 
package/dist/style.css CHANGED
@@ -983,6 +983,39 @@ html {
983
983
  --tw-contain-paint: ;
984
984
  --tw-contain-style: ;
985
985
  }
986
+ .container {
987
+ width: 100%;
988
+ }
989
+ @media (min-width: 640px) {
990
+
991
+ .container {
992
+ max-width: 640px;
993
+ }
994
+ }
995
+ @media (min-width: 768px) {
996
+
997
+ .container {
998
+ max-width: 768px;
999
+ }
1000
+ }
1001
+ @media (min-width: 1024px) {
1002
+
1003
+ .container {
1004
+ max-width: 1024px;
1005
+ }
1006
+ }
1007
+ @media (min-width: 1280px) {
1008
+
1009
+ .container {
1010
+ max-width: 1280px;
1011
+ }
1012
+ }
1013
+ @media (min-width: 1536px) {
1014
+
1015
+ .container {
1016
+ max-width: 1536px;
1017
+ }
1018
+ }
986
1019
  .alert {
987
1020
  display: grid;
988
1021
  width: 100%;
@@ -1024,6 +1057,15 @@ html {
1024
1057
  color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
1025
1058
  }
1026
1059
 
1060
+ .menu li > *:not(ul, .menu-title, details, .btn):active,
1061
+ .menu li > *:not(ul, .menu-title, details, .btn).active,
1062
+ .menu li > details > summary:active {
1063
+ --tw-bg-opacity: 1;
1064
+ background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
1065
+ --tw-text-opacity: 1;
1066
+ color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)));
1067
+ }
1068
+
1027
1069
  .tab:hover {
1028
1070
  --tw-text-opacity: 1;
1029
1071
  }
@@ -1117,6 +1159,25 @@ html {
1117
1159
  container-type: inline-size;
1118
1160
  grid-template-columns: auto 1fr;
1119
1161
  }
1162
+ .divider {
1163
+ display: flex;
1164
+ flex-direction: row;
1165
+ align-items: center;
1166
+ align-self: stretch;
1167
+ margin-top: 1rem;
1168
+ margin-bottom: 1rem;
1169
+ height: 1rem;
1170
+ white-space: nowrap;
1171
+ }
1172
+ .divider:before,
1173
+ .divider:after {
1174
+ height: 0.125rem;
1175
+ width: 100%;
1176
+ flex-grow: 1;
1177
+ --tw-content: '';
1178
+ content: var(--tw-content);
1179
+ background-color: var(--fallback-bc,oklch(var(--bc)/0.1));
1180
+ }
1120
1181
  .dropdown {
1121
1182
  position: relative;
1122
1183
  display: inline-block;
@@ -1633,6 +1694,11 @@ input.tab:checked + .tab-content,
1633
1694
  --alert-bg: var(--fallback-er,oklch(var(--er)/1));
1634
1695
  --alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1));
1635
1696
  }
1697
+ .btm-nav > *:where(.active) {
1698
+ border-top-width: 2px;
1699
+ --tw-bg-opacity: 1;
1700
+ background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
1701
+ }
1636
1702
  .btm-nav > *.disabled,
1637
1703
  .btm-nav > *[disabled] {
1638
1704
  pointer-events: none;
@@ -1785,6 +1851,9 @@ input.tab:checked + .tab-content,
1785
1851
  background-position-y: 0;
1786
1852
  }
1787
1853
  }
1854
+ .divider:not(:empty) {
1855
+ gap: 1rem;
1856
+ }
1788
1857
  .dropdown.dropdown-open .dropdown-content,
1789
1858
  .dropdown:focus .dropdown-content,
1790
1859
  .dropdown:focus-within .dropdown-content {
@@ -1900,6 +1969,14 @@ input.tab:checked + .tab-content,
1900
1969
  outline: 2px solid transparent;
1901
1970
  outline-offset: 2px;
1902
1971
  }
1972
+ .menu li > *:not(ul, .menu-title, details, .btn):active,
1973
+ .menu li > *:not(ul, .menu-title, details, .btn).active,
1974
+ .menu li > details > summary:active {
1975
+ --tw-bg-opacity: 1;
1976
+ background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
1977
+ --tw-text-opacity: 1;
1978
+ color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)));
1979
+ }
1903
1980
  .mockup-phone .display {
1904
1981
  overflow: hidden;
1905
1982
  border-radius: 40px;
@@ -2135,30 +2212,6 @@ input.tab:checked + .tab-content,
2135
2212
  background-position: calc(0% + 12px) calc(1px + 50%),
2136
2213
  calc(0% + 16px) calc(1px + 50%);
2137
2214
  }
2138
- .skeleton {
2139
- border-radius: var(--rounded-box, 1rem);
2140
- --tw-bg-opacity: 1;
2141
- background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));
2142
- will-change: background-position;
2143
- animation: skeleton 1.8s ease-in-out infinite;
2144
- background-image: linear-gradient(
2145
- 105deg,
2146
- transparent 0%,
2147
- transparent 40%,
2148
- var(--fallback-b1,oklch(var(--b1)/1)) 50%,
2149
- transparent 60%,
2150
- transparent 100%
2151
- );
2152
- background-size: 200% auto;
2153
- background-repeat: no-repeat;
2154
- background-position-x: -50%;
2155
- }
2156
- @media (prefers-reduced-motion) {
2157
-
2158
- .skeleton {
2159
- animation-duration: 15s;
2160
- }
2161
- }
2162
2215
  @keyframes skeleton {
2163
2216
 
2164
2217
  from {
@@ -2396,6 +2449,12 @@ input.tab:checked + .tab-content,
2396
2449
  --tw-bg-opacity: 1;
2397
2450
  background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
2398
2451
  }
2452
+ .table-zebra tr.active,
2453
+ .table-zebra tr.active:nth-child(even),
2454
+ .table-zebra-zebra tbody tr:nth-child(even) {
2455
+ --tw-bg-opacity: 1;
2456
+ background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));
2457
+ }
2399
2458
  .table :where(thead tr, tbody tr:not(:last-child), tbody tr:first-child:last-child) {
2400
2459
  border-bottom-width: 1px;
2401
2460
  --tw-border-opacity: 1;
@@ -2468,6 +2527,18 @@ input.tab:checked + .tab-content,
2468
2527
  --togglehandleborder: 0 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset,
2469
2528
  var(--handleoffsetcalculator) 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset;
2470
2529
  }
2530
+ .btm-nav-xs > *:where(.active) {
2531
+ border-top-width: 1px;
2532
+ }
2533
+ .btm-nav-sm > *:where(.active) {
2534
+ border-top-width: 2px;
2535
+ }
2536
+ .btm-nav-md > *:where(.active) {
2537
+ border-top-width: 2px;
2538
+ }
2539
+ .btm-nav-lg > *:where(.active) {
2540
+ border-top-width: 4px;
2541
+ }
2471
2542
  .btn-xs {
2472
2543
  height: 1.5rem;
2473
2544
  min-height: 1.5rem;
@@ -2887,6 +2958,9 @@ input.tab:checked + .tab-content,
2887
2958
  margin-top: 1rem;
2888
2959
  margin-bottom: 1rem;
2889
2960
  }
2961
+ .mb-0 {
2962
+ margin-bottom: 0px;
2963
+ }
2890
2964
  .mb-1 {
2891
2965
  margin-bottom: 0.25rem;
2892
2966
  }
@@ -2911,6 +2985,9 @@ input.tab:checked + .tab-content,
2911
2985
  .mr-2 {
2912
2986
  margin-right: 0.5rem;
2913
2987
  }
2988
+ .mt-0 {
2989
+ margin-top: 0px;
2990
+ }
2914
2991
  .mt-0\.5 {
2915
2992
  margin-top: 0.125rem;
2916
2993
  }
@@ -2950,12 +3027,24 @@ input.tab:checked + .tab-content,
2950
3027
  .w-16 {
2951
3028
  width: 4rem;
2952
3029
  }
3030
+ .w-20 {
3031
+ width: 5rem;
3032
+ }
3033
+ .w-24 {
3034
+ width: 6rem;
3035
+ }
2953
3036
  .w-32 {
2954
3037
  width: 8rem;
2955
3038
  }
3039
+ .w-44 {
3040
+ width: 11rem;
3041
+ }
2956
3042
  .w-64 {
2957
3043
  width: 16rem;
2958
3044
  }
3045
+ .w-\[6rem\] {
3046
+ width: 6rem;
3047
+ }
2959
3048
  .w-\[7\.5rem\] {
2960
3049
  width: 7.5rem;
2961
3050
  }
@@ -3193,6 +3282,10 @@ input.tab:checked + .tab-content,
3193
3282
  --tw-text-opacity: 1;
3194
3283
  color: rgb(75 85 99 / var(--tw-text-opacity));
3195
3284
  }
3285
+ .text-neutral-500 {
3286
+ --tw-text-opacity: 1;
3287
+ color: rgb(115 115 115 / var(--tw-text-opacity));
3288
+ }
3196
3289
  .text-red-700 {
3197
3290
  --tw-text-opacity: 1;
3198
3291
  color: rgb(185 28 28 / var(--tw-text-opacity));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.6.11",
3
+ "version": "0.6.13",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -90,7 +90,7 @@
90
90
  "@storybook/types": "^8.0.9",
91
91
  "@storybook/web-components": "^8.0.9",
92
92
  "@storybook/web-components-vite": "^8.0.9",
93
- "@types/node": "^20.12.7",
93
+ "@types/node": "^22.0.0",
94
94
  "@types/object-hash": "^3.0.6",
95
95
  "@typescript-eslint/eslint-plugin": "^7.14.1",
96
96
  "@typescript-eslint/parser": "^7.14.1",
@@ -1,5 +1,7 @@
1
1
  import { type Meta, type StoryObj } from '@storybook/preact';
2
2
  import { expect, fn, waitFor, within } from '@storybook/test';
3
+ import { type FunctionComponent } from 'preact';
4
+ import { useState } from 'preact/hooks';
3
5
 
4
6
  import { type CheckboxItem, CheckboxSelector, type CheckboxSelectorProps } from './checkbox-selector';
5
7
 
@@ -14,12 +16,33 @@ const meta: Meta<CheckboxSelectorProps> = {
14
16
 
15
17
  export default meta;
16
18
 
19
+ const WrapperWithState: FunctionComponent<CheckboxSelectorProps> = ({
20
+ items: initialItems,
21
+ label,
22
+ setItems: setItemsMock,
23
+ }) => {
24
+ const [items, setItems] = useState<CheckboxItem[]>(initialItems);
25
+
26
+ return (
27
+ <div className='w-32'>
28
+ <CheckboxSelector
29
+ items={items}
30
+ label={label}
31
+ setItems={(items: CheckboxItem[]) => {
32
+ setItemsMock(items);
33
+ setItems(items);
34
+ }}
35
+ />
36
+ </div>
37
+ );
38
+ };
39
+
17
40
  export const CheckboxSelectorStory: StoryObj<CheckboxSelectorProps> = {
18
41
  render: (args) => {
19
42
  let wrapperStateItems = args.items;
20
43
 
21
44
  return (
22
- <CheckboxSelector
45
+ <WrapperWithState
23
46
  items={wrapperStateItems}
24
47
  label={args.label}
25
48
  setItems={(items: CheckboxItem[]) => {
@@ -37,20 +60,79 @@ export const CheckboxSelectorStory: StoryObj<CheckboxSelectorProps> = {
37
60
  label: 'Some label',
38
61
  setItems: fn(),
39
62
  },
40
- play: async ({ canvasElement, args }) => {
63
+ play: async ({ canvasElement, args, step }) => {
41
64
  const canvas = within(canvasElement);
42
65
 
43
- const open = () => canvas.getByText('Some label', { exact: false });
66
+ const open = () => canvas.getByText('Some label');
67
+ const selectAll = () => canvas.getByText('Select all');
68
+ const selectNone = () => canvas.getByText('Select none');
69
+ const firstItem = () => canvas.getByLabelText('item1');
44
70
  open().click();
45
71
 
46
- const item1 = canvas.getByLabelText('item1', { exact: false });
47
- item1.click();
72
+ await step('Select one item', async () => {
73
+ firstItem().click();
48
74
 
49
- await waitFor(() =>
50
- expect(args.setItems).toHaveBeenCalledWith([
51
- { checked: true, label: 'item1' },
52
- { checked: false, label: 'item2' },
53
- ]),
54
- );
75
+ await waitFor(() =>
76
+ expect(args.setItems).toHaveBeenCalledWith([
77
+ { checked: true, label: 'item1' },
78
+ { checked: false, label: 'item2' },
79
+ ]),
80
+ );
81
+ });
82
+
83
+ await step('Select all items with one item already selected', async () => {
84
+ selectAll().click();
85
+
86
+ await waitFor(() =>
87
+ expect(args.setItems).toHaveBeenCalledWith([
88
+ { checked: true, label: 'item1' },
89
+ { checked: true, label: 'item2' },
90
+ ]),
91
+ );
92
+ });
93
+
94
+ await step('Deselect one item', async () => {
95
+ firstItem().click();
96
+
97
+ await waitFor(() =>
98
+ expect(args.setItems).toHaveBeenCalledWith([
99
+ { checked: false, label: 'item1' },
100
+ { checked: true, label: 'item2' },
101
+ ]),
102
+ );
103
+ });
104
+
105
+ await step('Select none with one item already selected', async () => {
106
+ selectNone().click();
107
+
108
+ await waitFor(() =>
109
+ expect(args.setItems).toHaveBeenCalledWith([
110
+ { checked: false, label: 'item1' },
111
+ { checked: false, label: 'item2' },
112
+ ]),
113
+ );
114
+ });
115
+
116
+ await step('Select all items with none selected', async () => {
117
+ selectAll().click();
118
+
119
+ await waitFor(() =>
120
+ expect(args.setItems).toHaveBeenCalledWith([
121
+ { checked: true, label: 'item1' },
122
+ { checked: true, label: 'item2' },
123
+ ]),
124
+ );
125
+ });
126
+
127
+ await step('Select none with all items selected', async () => {
128
+ selectNone().click();
129
+
130
+ await waitFor(() =>
131
+ expect(args.setItems).toHaveBeenCalledWith([
132
+ { checked: false, label: 'item1' },
133
+ { checked: false, label: 'item2' },
134
+ ]),
135
+ );
136
+ });
55
137
  },
56
138
  };
@@ -18,6 +18,25 @@ export const CheckboxSelector = <Item extends CheckboxItem>({
18
18
  }: CheckboxSelectorProps<Item>) => {
19
19
  return (
20
20
  <Dropdown buttonTitle={label} placement={'bottom-start'}>
21
+ <button
22
+ className='btn btn-xs btn-ghost'
23
+ onClick={() => {
24
+ const newItems = items.map((item) => ({ ...item, checked: true }));
25
+ setItems(newItems);
26
+ }}
27
+ >
28
+ Select all
29
+ </button>
30
+ <button
31
+ className='btn btn-xs btn-ghost'
32
+ onClick={() => {
33
+ const newItems = items.map((item) => ({ ...item, checked: false }));
34
+ setItems(newItems);
35
+ }}
36
+ >
37
+ Select none
38
+ </button>
39
+ <div className='divider mt-0 mb-0' />
21
40
  <ul>
22
41
  {items.map((item, index) => (
23
42
  <li className='flex flex-row items-center' key={item.label}>
@@ -10,8 +10,10 @@ export const ColorScaleSelectorDropdown: FunctionComponent<ColorScaleSelectorDro
10
10
  setColorScale,
11
11
  }) => {
12
12
  return (
13
- <Dropdown buttonTitle={`Color scale`} placement={'bottom-start'}>
14
- <ColorScaleSelector colorScale={colorScale} setColorScale={setColorScale} />
15
- </Dropdown>
13
+ <div className='w-20'>
14
+ <Dropdown buttonTitle={`Color scale`} placement={'bottom-start'}>
15
+ <ColorScaleSelector colorScale={colorScale} setColorScale={setColorScale} />
16
+ </Dropdown>
17
+ </div>
16
18
  );
17
19
  };
@@ -28,13 +28,13 @@ export const Dropdown: FunctionComponent<DropdownProps> = ({ children, buttonTit
28
28
  };
29
29
 
30
30
  return (
31
- <div>
32
- <button type='button' className='btn btn-xs whitespace-nowrap' onClick={toggle} ref={referenceRef}>
31
+ <>
32
+ <button type='button' className='btn btn-xs whitespace-nowrap w-full' onClick={toggle} ref={referenceRef}>
33
33
  {buttonTitle}
34
34
  </button>
35
35
  <div ref={floatingRef} className={`${dropdownClass} ${showContent ? '' : 'hidden'}`}>
36
36
  {children}
37
37
  </div>
38
- </div>
38
+ </>
39
39
  );
40
40
  };
@@ -1,5 +1,12 @@
1
1
  import { type FunctionComponent } from 'preact';
2
2
 
3
3
  export const LoadingDisplay: FunctionComponent = () => {
4
- return <div aria-label={'Loading'} className='h-full w-full skeleton' />;
4
+ return (
5
+ <div
6
+ aria-label={'Loading'}
7
+ className='h-full w-full border-2 border-gray-100 rounded-md flex justify-center items-center'
8
+ >
9
+ <div className='loading loading-spinner loading-md text-neutral-500' />
10
+ </div>
11
+ );
5
12
  };
@@ -0,0 +1,115 @@
1
+ import { type Meta, type StoryObj } from '@storybook/preact';
2
+ import { expect, within } from '@storybook/test';
3
+ import { type FunctionComponent } from 'preact';
4
+ import { useState } from 'preact/hooks';
5
+
6
+ import {
7
+ type DisplayedMutationType,
8
+ MutationTypeSelector,
9
+ type MutationTypeSelectorProps,
10
+ } from './mutation-type-selector';
11
+
12
+ const meta: Meta<MutationTypeSelectorProps> = {
13
+ title: 'Component/Mutation type selector',
14
+ component: MutationTypeSelector,
15
+ parameters: { fetchMock: {} },
16
+ };
17
+
18
+ export default meta;
19
+
20
+ const WrapperWithState: FunctionComponent<{
21
+ displayedMutationTypes: DisplayedMutationType[];
22
+ }> = ({ displayedMutationTypes: initialMutationTypes }) => {
23
+ const [displayedMutationTypes, setDisplayedMutationTypes] = useState<DisplayedMutationType[]>(initialMutationTypes);
24
+
25
+ return (
26
+ <MutationTypeSelector
27
+ displayedMutationTypes={displayedMutationTypes}
28
+ setDisplayedMutationTypes={(mutationTypes: DisplayedMutationType[]) => {
29
+ setDisplayedMutationTypes(mutationTypes);
30
+ }}
31
+ />
32
+ );
33
+ };
34
+
35
+ const MutationTypeSelectorStory: StoryObj<MutationTypeSelectorProps> = {
36
+ render: (args) => {
37
+ return <WrapperWithState {...args} />;
38
+ },
39
+ };
40
+
41
+ export const AllMutationTypesSelected: StoryObj<MutationTypeSelectorProps> = {
42
+ ...MutationTypeSelectorStory,
43
+ args: {
44
+ displayedMutationTypes: [
45
+ {
46
+ label: 'Substitution',
47
+ type: 'substitution',
48
+ checked: true,
49
+ },
50
+ {
51
+ label: 'Deletion',
52
+ type: 'deletion',
53
+ checked: true,
54
+ },
55
+ ],
56
+ },
57
+ play: async ({ canvasElement, step }) => {
58
+ const canvas = within(canvasElement);
59
+
60
+ await step('Show short form of types as label', async () => {
61
+ await expect(canvas.getByText('Subst., Del.')).toBeInTheDocument();
62
+ });
63
+ },
64
+ };
65
+
66
+ export const NoMutationTypesSelected: StoryObj<MutationTypeSelectorProps> = {
67
+ ...MutationTypeSelectorStory,
68
+ args: {
69
+ displayedMutationTypes: [
70
+ {
71
+ label: 'Substitution',
72
+ type: 'substitution',
73
+ checked: false,
74
+ },
75
+ {
76
+ label: 'Deletion',
77
+ type: 'deletion',
78
+ checked: false,
79
+ },
80
+ ],
81
+ },
82
+ play: async ({ canvasElement, step }) => {
83
+ const canvas = within(canvasElement);
84
+
85
+ await step("Show 'No types' as label", async () => {
86
+ await expect(canvas.getByText('No types')).toBeInTheDocument();
87
+ });
88
+ },
89
+ };
90
+
91
+ export const OneTypesSelected: StoryObj<MutationTypeSelectorProps> = {
92
+ ...MutationTypeSelectorStory,
93
+ args: {
94
+ displayedMutationTypes: [
95
+ {
96
+ label: 'Substitution',
97
+ type: 'substitution',
98
+ checked: true,
99
+ },
100
+ {
101
+ label: 'Deletion',
102
+ type: 'deletion',
103
+ checked: false,
104
+ },
105
+ ],
106
+ },
107
+ play: async ({ canvasElement, step }) => {
108
+ const canvas = within(canvasElement);
109
+
110
+ await step('Show the selected type as label', async () => {
111
+ const substitutionElements = await canvas.getAllByText('Substitution');
112
+ await expect(substitutionElements.length).toBe(2);
113
+ });
114
+ },
115
+ };
@@ -16,14 +16,39 @@ export const MutationTypeSelector: FunctionComponent<MutationTypeSelectorProps>
16
16
  displayedMutationTypes,
17
17
  setDisplayedMutationTypes,
18
18
  }) => {
19
- const checkedLabels = displayedMutationTypes.filter((type) => type.checked).map((type) => type.label);
20
- const mutationTypesSelectorLabel = `Types: ${checkedLabels.length > 0 ? checkedLabels.join(', ') : 'None'}`;
21
-
22
19
  return (
23
- <CheckboxSelector
24
- items={displayedMutationTypes}
25
- label={mutationTypesSelectorLabel}
26
- setItems={(items) => setDisplayedMutationTypes(items)}
27
- />
20
+ <div className='w-[6rem]'>
21
+ <CheckboxSelector
22
+ items={displayedMutationTypes}
23
+ label={getMutationTypesSelectorLabel(displayedMutationTypes)}
24
+ setItems={(items) => setDisplayedMutationTypes(items)}
25
+ />
26
+ </div>
28
27
  );
29
28
  };
29
+
30
+ const getMutationTypesSelectorLabel = (displayedMutationTypes: DisplayedMutationType[]) => {
31
+ const checkedLabels = displayedMutationTypes.filter((displayedMutationType) => displayedMutationType.checked);
32
+
33
+ if (checkedLabels.length === 0) {
34
+ return `No types`;
35
+ }
36
+ if (displayedMutationTypes.length === checkedLabels.length) {
37
+ return displayedMutationTypes
38
+ .map((type) => {
39
+ switch (type.type) {
40
+ case 'substitution':
41
+ return 'Subst.';
42
+ case 'deletion':
43
+ return 'Del.';
44
+ }
45
+ })
46
+ .join(', ');
47
+ }
48
+
49
+ return checkedLabels
50
+ .map((type) => {
51
+ return type.label;
52
+ })
53
+ .join(', ');
54
+ };