@genspectrum/dashboard-components 0.8.3 → 0.8.5

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.
@@ -411,8 +411,9 @@ export declare class MutationComparisonComponent extends PreactLitAdapterWithGri
411
411
  * ## Context
412
412
  * This component provides an input field to specify filters for nucleotide and amino acid mutations and insertions.
413
413
  *
414
- * Input values have to be provided one at a time and submitted by pressing the Enter key or by clicking the '+' button.
415
- * After submission, the component validates the input and fires an event with the selected mutations.
414
+ * Input values have to be provided one at a time and submitted by pressing the Enter key or by selecting an option from the dropdown.
415
+ * Alternatively, they can be provided as a string of comma-separated values, which will be directly parsed and validated.
416
+ * After submission (after pressing Enter or pasting a comma-separated string) an event is fired with the selected mutations.
416
417
  * All previously selected mutations are displayed at the input field and added to the event.
417
418
  * Users can remove a mutation by clicking the 'x' button next to the mutation.
418
419
  *
@@ -1045,7 +1046,7 @@ declare global {
1045
1046
 
1046
1047
  declare global {
1047
1048
  interface HTMLElementTagNameMap {
1048
- 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
1049
+ 'gs-mutations-component': MutationsComponent;
1049
1050
  }
1050
1051
  }
1051
1052
 
@@ -1053,7 +1054,7 @@ declare global {
1053
1054
  declare global {
1054
1055
  namespace JSX {
1055
1056
  interface IntrinsicElements {
1056
- 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1057
+ 'gs-mutations-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1057
1058
  }
1058
1059
  }
1059
1060
  }
@@ -1061,7 +1062,7 @@ declare global {
1061
1062
 
1062
1063
  declare global {
1063
1064
  interface HTMLElementTagNameMap {
1064
- 'gs-mutations-component': MutationsComponent;
1065
+ 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
1065
1066
  }
1066
1067
  }
1067
1068
 
@@ -1069,7 +1070,7 @@ declare global {
1069
1070
  declare global {
1070
1071
  namespace JSX {
1071
1072
  interface IntrinsicElements {
1072
- 'gs-mutations-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1073
+ 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1073
1074
  }
1074
1075
  }
1075
1076
  }
package/dist/style.css CHANGED
@@ -3029,15 +3029,15 @@ input.tab:checked + .tab-content,
3029
3029
  .mt-4 {
3030
3030
  margin-top: 1rem;
3031
3031
  }
3032
- .inline-block {
3033
- display: inline-block;
3034
- }
3035
3032
  .inline {
3036
3033
  display: inline;
3037
3034
  }
3038
3035
  .flex {
3039
3036
  display: flex;
3040
3037
  }
3038
+ .inline-flex {
3039
+ display: inline-flex;
3040
+ }
3041
3041
  .table {
3042
3042
  display: table;
3043
3043
  }
@@ -3107,6 +3107,9 @@ input.tab:checked + .tab-content,
3107
3107
  --tw-translate-y: -50%;
3108
3108
  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));
3109
3109
  }
3110
+ .cursor-pointer {
3111
+ cursor: pointer;
3112
+ }
3110
3113
  .resize {
3111
3114
  resize: both;
3112
3115
  }
@@ -3155,9 +3158,6 @@ input.tab:checked + .tab-content,
3155
3158
  .break-words {
3156
3159
  overflow-wrap: break-word;
3157
3160
  }
3158
- .rounded-full {
3159
- border-radius: 9999px;
3160
- }
3161
3161
  .rounded-lg {
3162
3162
  border-radius: 0.5rem;
3163
3163
  }
@@ -3186,12 +3186,6 @@ input.tab:checked + .tab-content,
3186
3186
  .border-b-2 {
3187
3187
  border-bottom-width: 2px;
3188
3188
  }
3189
- .border-solid {
3190
- border-style: solid;
3191
- }
3192
- .border-none {
3193
- border-style: none;
3194
- }
3195
3189
  .border-error {
3196
3190
  --tw-border-opacity: 1;
3197
3191
  border-color: var(--fallback-er,oklch(var(--er)/var(--tw-border-opacity, 1)));
@@ -3212,14 +3206,18 @@ input.tab:checked + .tab-content,
3212
3206
  --tw-border-opacity: 1;
3213
3207
  border-color: rgb(156 163 175 / var(--tw-border-opacity, 1));
3214
3208
  }
3215
- .border-red-500 {
3209
+ .border-slate-500 {
3216
3210
  --tw-border-opacity: 1;
3217
- border-color: rgb(239 68 68 / var(--tw-border-opacity, 1));
3211
+ border-color: rgb(100 116 139 / var(--tw-border-opacity, 1));
3218
3212
  }
3219
3213
  .bg-red-200 {
3220
3214
  --tw-bg-opacity: 1;
3221
3215
  background-color: rgb(254 202 202 / var(--tw-bg-opacity, 1));
3222
3216
  }
3217
+ .bg-slate-200 {
3218
+ --tw-bg-opacity: 1;
3219
+ background-color: rgb(226 232 240 / var(--tw-bg-opacity, 1));
3220
+ }
3223
3221
  .bg-white {
3224
3222
  --tw-bg-opacity: 1;
3225
3223
  background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
@@ -3233,10 +3231,6 @@ input.tab:checked + .tab-content,
3233
3231
  .p-4 {
3234
3232
  padding: 1rem;
3235
3233
  }
3236
- .px-2 {
3237
- padding-left: 0.5rem;
3238
- padding-right: 0.5rem;
3239
- }
3240
3234
  .px-4 {
3241
3235
  padding-left: 1rem;
3242
3236
  padding-right: 1rem;
@@ -3303,6 +3297,10 @@ input.tab:checked + .tab-content,
3303
3297
  --tw-text-opacity: 1;
3304
3298
  color: rgb(96 96 96 / var(--tw-text-opacity, 1));
3305
3299
  }
3300
+ .text-black {
3301
+ --tw-text-opacity: 1;
3302
+ color: rgb(0 0 0 / var(--tw-text-opacity, 1));
3303
+ }
3306
3304
  .text-blue-600 {
3307
3305
  --tw-text-opacity: 1;
3308
3306
  color: rgb(37 99 235 / var(--tw-text-opacity, 1));
@@ -3378,10 +3376,6 @@ input.tab:checked + .tab-content,
3378
3376
  border-bottom-left-radius: var(--rounded-box, 1rem);
3379
3377
  }
3380
3378
  }
3381
- .focus-within\:border-gray-400:focus-within {
3382
- --tw-border-opacity: 1;
3383
- border-color: rgb(156 163 175 / var(--tw-border-opacity, 1));
3384
- }
3385
3379
  .hover\:scale-110:hover {
3386
3380
  --tw-scale-x: 1.1;
3387
3381
  --tw-scale-y: 1.1;
@@ -3396,6 +3390,10 @@ input.tab:checked + .tab-content,
3396
3390
  --tw-bg-opacity: 1;
3397
3391
  background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
3398
3392
  }
3393
+ .hover\:bg-gray-300:hover {
3394
+ --tw-bg-opacity: 1;
3395
+ background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1));
3396
+ }
3399
3397
  .hover\:font-bold:hover {
3400
3398
  font-weight: 700;
3401
3399
  }
@@ -3415,15 +3413,6 @@ input.tab:checked + .tab-content,
3415
3413
  --tw-text-opacity: 1;
3416
3414
  color: rgb(55 65 81 / var(--tw-text-opacity, 1));
3417
3415
  }
3418
- .focus\:outline-none:focus {
3419
- outline: 2px solid transparent;
3420
- outline-offset: 2px;
3421
- }
3422
- .focus\:ring-0:focus {
3423
- --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
3424
- --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);
3425
- box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
3426
- }
3427
3416
  .peer:hover ~ .peer-hover\:visible {
3428
3417
  visibility: visible;
3429
3418
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -1,4 +1,4 @@
1
- import { type Meta, type PreactRenderer, type StoryObj } from '@storybook/preact';
1
+ import { type PreactRenderer, type Meta, type StoryObj } from '@storybook/preact';
2
2
  import { expect, fireEvent, fn, userEvent, waitFor, within } from '@storybook/test';
3
3
  import { type StepFunction } from '@storybook/types';
4
4
 
@@ -43,13 +43,75 @@ export const Default: StoryObj<MutationFilterProps> = {
43
43
  },
44
44
  };
45
45
 
46
+ export const FiresFilterMultipleCommaSeparatedQueries: StoryObj<MutationFilterProps> = {
47
+ ...Default,
48
+ play: async ({ canvasElement, step }) => {
49
+ const { canvas, changedListenerMock } = await prepare(canvasElement, step);
50
+
51
+ await step('Enter a valid mutation', async () => {
52
+ await submitMutation(canvas, 'A123T');
53
+
54
+ await waitFor(() =>
55
+ expect(changedListenerMock).toHaveBeenCalledWith(
56
+ expect.objectContaining({
57
+ detail: {
58
+ nucleotideMutations: ['A123T'],
59
+ aminoAcidMutations: [],
60
+ nucleotideInsertions: [],
61
+ aminoAcidInsertions: [],
62
+ },
63
+ }),
64
+ ),
65
+ );
66
+ });
67
+
68
+ await step('Enter a comma separated list of valid and invalid mutations', async () => {
69
+ await pasteMutations(canvas, 'A123T, error_insX, A234T, ins_123:AA');
70
+
71
+ await waitFor(() =>
72
+ expect(changedListenerMock).toHaveBeenCalledWith(
73
+ expect.objectContaining({
74
+ detail: {
75
+ nucleotideMutations: ['A123T', 'A234T'],
76
+ aminoAcidMutations: [],
77
+ nucleotideInsertions: ['ins_123:AA'],
78
+ aminoAcidInsertions: [],
79
+ },
80
+ }),
81
+ ),
82
+ );
83
+ await expect(canvas.queryByText('A123T')).toBeVisible();
84
+ await expect(canvas.queryByText('A234T')).toBeVisible();
85
+ await expect(inputField(canvas)).toHaveValue('error_insX');
86
+ });
87
+
88
+ await step('Remove the first mutation', async () => {
89
+ const mutationItem = within(canvas.getByText('A234T'));
90
+ await fireEvent.click(mutationItem.getByRole('button', { name: '×' }));
91
+
92
+ await waitFor(() =>
93
+ expect(changedListenerMock).toHaveBeenCalledWith(
94
+ expect.objectContaining({
95
+ detail: {
96
+ nucleotideMutations: ['A123T'],
97
+ aminoAcidMutations: [],
98
+ nucleotideInsertions: ['ins_123:AA'],
99
+ aminoAcidInsertions: [],
100
+ },
101
+ }),
102
+ ),
103
+ );
104
+ });
105
+ },
106
+ };
107
+
46
108
  export const FiresFilterChangedEvents: StoryObj<MutationFilterProps> = {
47
109
  ...Default,
48
110
  play: async ({ canvasElement, step }) => {
49
111
  const { canvas, changedListenerMock } = await prepare(canvasElement, step);
50
112
 
51
113
  await step('Enters an invalid mutation', async () => {
52
- await submitMutation(canvas, 'notAMutation');
114
+ await testNoOptionsExist(canvas, 'notAMutation');
53
115
  await expect(changedListenerMock).not.toHaveBeenCalled();
54
116
 
55
117
  await userEvent.type(inputField(canvas), '{backspace>12/}');
@@ -88,7 +150,7 @@ export const FiresFilterChangedEvents: StoryObj<MutationFilterProps> = {
88
150
  });
89
151
 
90
152
  await step('Enter another valid mutation', async () => {
91
- await submitMutation(canvas, 'ins_123:AA');
153
+ await submitMutation(canvas, 'ins_123:AA', 'enter');
92
154
 
93
155
  await expect(changedListenerMock).toHaveBeenCalledWith(
94
156
  expect.objectContaining({
@@ -103,13 +165,13 @@ export const FiresFilterChangedEvents: StoryObj<MutationFilterProps> = {
103
165
  });
104
166
 
105
167
  await step('Remove the first mutation', async () => {
106
- const firstMutationDeleteButton = canvas.getAllByRole('button', { name: '✕' })[1];
107
- await waitFor(() => fireEvent.click(firstMutationDeleteButton));
168
+ const mutationItem = within(canvas.getByText('A234-'));
169
+ await fireEvent.click(mutationItem.getByRole('button', { name: '×' }));
108
170
 
109
171
  await expect(changedListenerMock).toHaveBeenCalledWith(
110
172
  expect.objectContaining({
111
173
  detail: {
112
- nucleotideMutations: ['A234-'],
174
+ nucleotideMutations: ['A123T'],
113
175
  aminoAcidMutations: [],
114
176
  nucleotideInsertions: ['ins_123:AA'],
115
177
  aminoAcidInsertions: [],
@@ -140,7 +202,7 @@ export const WithInitialValue: StoryObj<MutationFilterProps> = {
140
202
  play: async ({ canvasElement, step }) => {
141
203
  const { canvas, changedListenerMock } = await prepare(canvasElement, step);
142
204
 
143
- await step('Enter additional input', async () => {
205
+ await step('Add input to initial value', async () => {
144
206
  await submitMutation(canvas, 'G500T');
145
207
 
146
208
  await expect(changedListenerMock).toHaveBeenCalledWith(
@@ -174,12 +236,34 @@ async function prepare(canvasElement: HTMLElement, step: StepFunction<PreactRend
174
236
  return { canvas, changedListenerMock };
175
237
  }
176
238
 
177
- const submitMutation = async (canvas: ReturnType<typeof within>, mutation: string) => {
239
+ export type SubmissionMethod = 'click' | 'enter';
240
+
241
+ const submitMutation = async (
242
+ canvas: ReturnType<typeof within>,
243
+ mutation: string,
244
+ submissionMethod: SubmissionMethod = 'click',
245
+ ) => {
178
246
  await userEvent.type(inputField(canvas), mutation);
179
- await waitFor(() => submitButton(canvas).click());
247
+ const firstOption = await canvas.findByRole('option', { name: mutation });
248
+ if (submissionMethod === 'click') {
249
+ await userEvent.click(firstOption);
250
+ }
251
+ if (submissionMethod === 'enter') {
252
+ await userEvent.keyboard('{enter}');
253
+ }
254
+ };
255
+
256
+ const pasteMutations = async (canvas: ReturnType<typeof within>, mutation: string) => {
257
+ await userEvent.click(inputField(canvas));
258
+ await userEvent.paste(mutation);
259
+ };
260
+
261
+ const testNoOptionsExist = async (canvas: ReturnType<typeof within>, mutation: string) => {
262
+ await userEvent.type(inputField(canvas), mutation);
263
+ const options = canvas.queryAllByRole('option');
264
+
265
+ await expect(options).toHaveLength(0);
180
266
  };
181
267
 
182
268
  const inputField = (canvas: ReturnType<typeof within>) =>
183
269
  canvas.getByPlaceholderText('Enter a mutation', { exact: false });
184
-
185
- const submitButton = (canvas: ReturnType<typeof within>) => canvas.getByRole('button', { name: '+' });