@genspectrum/dashboard-components 0.19.2 → 0.19.4

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 (61) hide show
  1. package/custom-elements.json +383 -10
  2. package/dist/{LineageFilterChangedEvent-ixHQkq8y.js → LineageFilterChangedEvent-GgkxoF3X.js} +17 -5
  3. package/dist/LineageFilterChangedEvent-GgkxoF3X.js.map +1 -0
  4. package/dist/assets/mutationOverTimeWorker-ChQTFL68.js.map +1 -1
  5. package/dist/components.d.ts +184 -21
  6. package/dist/components.js +9352 -8683
  7. package/dist/components.js.map +1 -1
  8. package/dist/util.d.ts +69 -21
  9. package/dist/util.js +2 -1
  10. package/package.json +1 -1
  11. package/src/componentsEntrypoint.ts +3 -1
  12. package/src/preact/components/error-display.stories.tsx +2 -1
  13. package/src/preact/components/error-display.tsx +2 -3
  14. package/src/preact/components/min-max-range-slider.tsx +19 -4
  15. package/src/preact/components/resize-container.tsx +7 -10
  16. package/src/preact/components/tooltip.tsx +7 -4
  17. package/src/preact/dateRangeFilter/date-range-filter.stories.tsx +9 -5
  18. package/src/preact/dateRangeFilter/date-range-filter.tsx +2 -1
  19. package/src/preact/dateRangeFilter/dateRangeOption.ts +2 -1
  20. package/src/preact/genomeViewer/CDSPlot.tsx +219 -0
  21. package/src/preact/genomeViewer/genome-data-viewer.stories.tsx +113 -0
  22. package/src/preact/genomeViewer/genome-data-viewer.tsx +69 -0
  23. package/src/preact/genomeViewer/loadGff3.spec.ts +61 -0
  24. package/src/preact/genomeViewer/loadGff3.ts +180 -0
  25. package/src/preact/lineageFilter/LineageFilterChangedEvent.ts +3 -1
  26. package/src/preact/lineageFilter/lineage-filter.stories.tsx +3 -2
  27. package/src/preact/locationFilter/LocationChangedEvent.ts +2 -1
  28. package/src/preact/locationFilter/location-filter.stories.tsx +3 -2
  29. package/src/preact/mutationFilter/mutation-filter.stories.tsx +3 -2
  30. package/src/preact/mutationFilter/mutation-filter.tsx +2 -1
  31. package/src/preact/numberRangeFilter/NumberRangeFilterChangedEvent.ts +31 -0
  32. package/src/preact/numberRangeFilter/number-range-filter.stories.tsx +383 -0
  33. package/src/preact/numberRangeFilter/number-range-filter.tsx +159 -0
  34. package/src/preact/numberRangeFilter/useSelectedRangeReducer.ts +137 -0
  35. package/src/preact/shared/charts/colors.ts +1 -1
  36. package/src/preact/textFilter/TextFilterChangedEvent.ts +3 -1
  37. package/src/preact/textFilter/text-filter.stories.tsx +4 -3
  38. package/src/utilEntrypoint.ts +2 -0
  39. package/src/utils/gsEventNames.ts +11 -0
  40. package/src/web-components/input/gs-date-range-filter.stories.ts +4 -3
  41. package/src/web-components/input/gs-date-range-filter.tsx +3 -2
  42. package/src/web-components/input/gs-lineage-filter.stories.ts +3 -2
  43. package/src/web-components/input/gs-lineage-filter.tsx +2 -1
  44. package/src/web-components/input/gs-location-filter.stories.ts +3 -2
  45. package/src/web-components/input/gs-location-filter.tsx +2 -1
  46. package/src/web-components/input/gs-mutation-filter.stories.ts +3 -2
  47. package/src/web-components/input/gs-mutation-filter.tsx +2 -1
  48. package/src/web-components/input/gs-number-range-filter.spec.ts +27 -0
  49. package/src/web-components/input/gs-number-range-filter.stories.ts +96 -0
  50. package/src/web-components/input/gs-number-range-filter.tsx +148 -0
  51. package/src/web-components/input/gs-text-filter.stories.ts +5 -4
  52. package/src/web-components/input/gs-text-filter.tsx +2 -1
  53. package/src/web-components/input/index.ts +1 -0
  54. package/src/web-components/visualization/gs-genome-data-viewer.spec-d.ts +18 -0
  55. package/src/web-components/visualization/gs-genome-data-viewer.stories.ts +108 -0
  56. package/src/web-components/visualization/gs-genome-data-viewer.tsx +59 -0
  57. package/src/web-components/visualization/index.ts +1 -0
  58. package/standalone-bundle/assets/mutationOverTimeWorker-jChgWnwp.js.map +1 -1
  59. package/standalone-bundle/dashboard-components.js +9613 -9059
  60. package/standalone-bundle/dashboard-components.js.map +1 -1
  61. package/dist/LineageFilterChangedEvent-ixHQkq8y.js.map +0 -1
@@ -0,0 +1,137 @@
1
+ import { useEffect, useReducer, useState } from 'preact/hooks';
2
+
3
+ import { type NumberRange } from './NumberRangeFilterChangedEvent';
4
+
5
+ type InputState = {
6
+ min: string;
7
+ max: string;
8
+ };
9
+
10
+ export type RangeState = {
11
+ inputState: InputState;
12
+ range: NumberRange & { isValidRange: boolean };
13
+ wasDispatched: boolean;
14
+ };
15
+
16
+ export function useSelectedRangeReducer(initialValue: NumberRange) {
17
+ const [range, dispatchRange] = useReducer(
18
+ rangeReducer,
19
+ addRange({
20
+ min: initialValue.min?.toString() ?? '',
21
+ max: initialValue.max?.toString() ?? '',
22
+ }),
23
+ );
24
+ const [isInitialRender, setIsInitialRender] = useState(true);
25
+
26
+ useEffect(
27
+ () => {
28
+ if (isInitialRender) {
29
+ setIsInitialRender(false);
30
+ return;
31
+ }
32
+
33
+ dispatchRange({
34
+ type: SetRangeActionType.SET_VALUE_FROM_CONTROLLED_INPUT,
35
+ range: initialValue,
36
+ });
37
+ },
38
+ [initialValue], // eslint-disable-line react-hooks/exhaustive-deps -- only run this when initialValue changes
39
+ );
40
+
41
+ return [range, dispatchRange] as const;
42
+ }
43
+
44
+ export const SetRangeActionType = {
45
+ SET_MIN: 'setMin',
46
+ SET_MAX: 'setMax',
47
+ SET_VALUE_FROM_CONTROLLED_INPUT: 'setValueFromControlledInput',
48
+ DISPATCHED_EVENT: 'dispatchedEvent',
49
+ } as const;
50
+
51
+ type SetRangeAction =
52
+ | {
53
+ type: typeof SetRangeActionType.SET_MIN | typeof SetRangeActionType.SET_MAX;
54
+ value: string;
55
+ }
56
+ | {
57
+ type: typeof SetRangeActionType.SET_VALUE_FROM_CONTROLLED_INPUT;
58
+ range: NumberRange;
59
+ }
60
+ | {
61
+ type: typeof SetRangeActionType.DISPATCHED_EVENT;
62
+ };
63
+
64
+ function rangeReducer(currentState: RangeState, action: SetRangeAction) {
65
+ const { min, max } = currentState.inputState;
66
+
67
+ switch (action.type) {
68
+ case SetRangeActionType.SET_MIN:
69
+ return addRange({ min: action.value, max });
70
+ case SetRangeActionType.SET_MAX:
71
+ return addRange({ min, max: action.value });
72
+
73
+ case SetRangeActionType.SET_VALUE_FROM_CONTROLLED_INPUT:
74
+ return addRange({
75
+ min: action.range.min?.toString() ?? '',
76
+ max: action.range.max?.toString() ?? '',
77
+ });
78
+
79
+ case SetRangeActionType.DISPATCHED_EVENT:
80
+ return {
81
+ ...currentState,
82
+ wasDispatched: true,
83
+ };
84
+ }
85
+ }
86
+
87
+ function addRange(inputState: InputState): RangeState {
88
+ const parsedMin = parseRangeValue(inputState.min);
89
+ const parsedMax = parseRangeValue(inputState.max);
90
+
91
+ const range = {
92
+ min: parsedMin.value,
93
+ max: parsedMax.value,
94
+ };
95
+
96
+ const isValidRange = parsedMin.valid && parsedMax.valid && isValid(range);
97
+
98
+ return {
99
+ inputState,
100
+ range: {
101
+ ...range,
102
+ isValidRange,
103
+ },
104
+ wasDispatched: false,
105
+ };
106
+ }
107
+
108
+ function parseRangeValue(inputValue: string) {
109
+ const trimmedInput = inputValue.trim();
110
+
111
+ if (trimmedInput === '') {
112
+ return {
113
+ valid: true,
114
+ value: undefined,
115
+ };
116
+ }
117
+
118
+ const value = Number(trimmedInput);
119
+ if (!Number.isFinite(value)) {
120
+ return {
121
+ valid: false,
122
+ value: undefined,
123
+ };
124
+ }
125
+
126
+ return {
127
+ valid: true,
128
+ value,
129
+ };
130
+ }
131
+
132
+ function isValid(range: NumberRange) {
133
+ if (range.min === undefined || range.max === undefined) {
134
+ return true;
135
+ }
136
+ return range.min <= range.max;
137
+ }
@@ -1,6 +1,6 @@
1
1
  // colorblind friendly colors taken from https://personal.sron.nl/~pault/
2
2
 
3
- const ColorsRGB = {
3
+ export const ColorsRGB = {
4
4
  indigo: [51, 34, 136],
5
5
  green: [17, 119, 51],
6
6
  cyan: [136, 204, 238],
@@ -1,8 +1,10 @@
1
+ import { gsEventNames } from '../../utils/gsEventNames';
2
+
1
3
  type LapisTextFilter = Record<string, string | undefined>;
2
4
 
3
5
  export class TextFilterChangedEvent extends CustomEvent<LapisTextFilter> {
4
6
  constructor(detail: LapisTextFilter) {
5
- super('gs-text-filter-changed', {
7
+ super(gsEventNames.textFilterChanged, {
6
8
  detail,
7
9
  bubbles: true,
8
10
  composed: true,
@@ -5,6 +5,7 @@ import data from './__mockData__/aggregated_hosts.json';
5
5
  import { TextFilter, type TextFilterProps } from './text-filter';
6
6
  import { previewHandles } from '../../../.storybook/preview';
7
7
  import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
8
+ import { gsEventNames } from '../../utils/gsEventNames';
8
9
  import { LapisUrlContextProvider } from '../LapisUrlContext';
9
10
  import { expectInvalidAttributesErrorMessage } from '../shared/stories/expectErrorMessage';
10
11
 
@@ -13,7 +14,7 @@ const meta: Meta<TextFilterProps> = {
13
14
  component: TextFilter,
14
15
  parameters: {
15
16
  actions: {
16
- handles: ['gs-text-filter-changed', ...previewHandles],
17
+ handles: [gsEventNames.textFilterChanged, ...previewHandles],
17
18
  },
18
19
  fetchMock: {
19
20
  mocks: [
@@ -93,7 +94,7 @@ export const RemoveInitialValue: StoryObj<TextFilterProps> = {
93
94
 
94
95
  const changedListenerMock = fn();
95
96
  await step('Setup event listener mock', () => {
96
- canvasElement.addEventListener('gs-text-filter-changed', changedListenerMock);
97
+ canvasElement.addEventListener(gsEventNames.textFilterChanged, changedListenerMock);
97
98
  });
98
99
 
99
100
  await waitFor(async () => {
@@ -136,7 +137,7 @@ export const KeepsPartialInputInInputField: StoryObj<TextFilterProps> = {
136
137
 
137
138
  const changedListenerMock = fn();
138
139
  await step('Setup event listener mock', () => {
139
- canvasElement.addEventListener('gs-text-filter-changed', changedListenerMock);
140
+ canvasElement.addEventListener(gsEventNames.textFilterChanged, changedListenerMock);
140
141
  });
141
142
  const inputField = () => canvas.getByPlaceholderText('Enter a host name', { exact: false });
142
143
  async function typeAndBlur(input: string) {
@@ -38,3 +38,5 @@ export { LineageFilterChangedEvent } from './preact/lineageFilter/LineageFilterC
38
38
  export { TextFilterChangedEvent } from './preact/textFilter/TextFilterChangedEvent';
39
39
 
40
40
  export type { MutationAnnotations, MutationAnnotation } from './web-components/mutation-annotations-context';
41
+
42
+ export { gsEventNames } from './utils/gsEventNames';
@@ -0,0 +1,11 @@
1
+ export const gsEventNames = {
2
+ error: 'gs-error',
3
+ dateRangeFilterChanged: 'gs-date-range-filter-changed',
4
+ dateRangeOptionChanged: 'gs-date-range-option-changed',
5
+ mutationFilterChanged: 'gs-mutation-filter-changed',
6
+ lineageFilterChanged: 'gs-lineage-filter-changed',
7
+ locationChanged: 'gs-location-changed',
8
+ textFilterChanged: 'gs-text-filter-changed',
9
+ numberRangeFilterChanged: 'gs-number-range-filter-changed',
10
+ numberRangeValueChanged: 'gs-number-range-value-changed',
11
+ } as const;
@@ -10,6 +10,7 @@ import './gs-date-range-filter';
10
10
  import '../gs-app';
11
11
  import { toYYYYMMDD } from '../../preact/dateRangeFilter/dateConversion';
12
12
  import { dateRangeOptionPresets } from '../../preact/dateRangeFilter/dateRangeOption';
13
+ import { gsEventNames } from '../../utils/gsEventNames';
13
14
  import { withinShadowRoot } from '../withinShadowRoot.story';
14
15
 
15
16
  const codeExample = String.raw`
@@ -29,7 +30,7 @@ const meta: Meta<Required<DateRangeFilterProps>> = {
29
30
  component: 'gs-date-range-filter',
30
31
  parameters: withComponentDocs({
31
32
  actions: {
32
- handles: ['gs-date-range-filter-changed', 'gs-date-range-option-changed', ...previewHandles],
33
+ handles: [gsEventNames.dateRangeFilterChanged, gsEventNames.dateRangeOptionChanged, ...previewHandles],
33
34
  },
34
35
  fetchMock: {},
35
36
  componentDocs: {
@@ -144,8 +145,8 @@ export const FiresEvents: StoryObj<Required<DateRangeFilterProps>> = {
144
145
  const filterChangedListenerMock = fn();
145
146
  const optionChangedListenerMock = fn();
146
147
  await step('Setup event listener mock', () => {
147
- canvasElement.addEventListener('gs-date-range-filter-changed', filterChangedListenerMock);
148
- canvasElement.addEventListener('gs-date-range-option-changed', optionChangedListenerMock);
148
+ canvasElement.addEventListener(gsEventNames.dateRangeFilterChanged, filterChangedListenerMock);
149
+ canvasElement.addEventListener(gsEventNames.dateRangeOptionChanged, optionChangedListenerMock);
149
150
  });
150
151
 
151
152
  await step('Expect last 6 months to be selected', async () => {
@@ -5,6 +5,7 @@ import type { DetailedHTMLProps, HTMLAttributes } from 'react';
5
5
 
6
6
  import { DateRangeFilter, type DateRangeFilterProps } from '../../preact/dateRangeFilter/date-range-filter';
7
7
  import { type DateRangeOptionChangedEvent } from '../../preact/dateRangeFilter/dateRangeOption';
8
+ import { type gsEventNames } from '../../utils/gsEventNames';
8
9
  import { type Equals, type Expect } from '../../utils/typeAssertions';
9
10
  import { PreactLitAdapter } from '../PreactLitAdapter';
10
11
 
@@ -143,8 +144,8 @@ declare global {
143
144
  }
144
145
 
145
146
  interface HTMLElementEventMap {
146
- 'gs-date-range-filter-changed': CustomEvent<Record<string, string>>;
147
- 'gs-date-range-option-changed': DateRangeOptionChangedEvent;
147
+ [gsEventNames.dateRangeFilterChanged]: CustomEvent<Record<string, string>>;
148
+ [gsEventNames.dateRangeOptionChanged]: DateRangeOptionChangedEvent;
148
149
  }
149
150
  }
150
151
 
@@ -9,6 +9,7 @@ import '../gs-app';
9
9
  import './gs-lineage-filter';
10
10
  import aggregatedData from '../../preact/lineageFilter/__mockData__/aggregated.json';
11
11
  import { type LineageFilterProps } from '../../preact/lineageFilter/lineage-filter';
12
+ import { gsEventNames } from '../../utils/gsEventNames';
12
13
  import { withinShadowRoot } from '../withinShadowRoot.story';
13
14
 
14
15
  const codeExample = String.raw`
@@ -25,7 +26,7 @@ const meta: Meta<Required<LineageFilterProps>> = {
25
26
  component: 'gs-lineage-filter',
26
27
  parameters: withComponentDocs({
27
28
  actions: {
28
- handles: ['gs-lineage-filter-changed', ...previewHandles],
29
+ handles: [gsEventNames.lineageFilterChanged, ...previewHandles],
29
30
  },
30
31
  fetchMock: {
31
32
  mocks: [
@@ -182,7 +183,7 @@ export const FiresEvent: StoryObj<Required<LineageFilterProps>> = {
182
183
  const inputField = () => canvas.getByPlaceholderText('Enter a lineage');
183
184
  const listenerMock = fn();
184
185
  await step('Setup event listener mock', () => {
185
- canvasElement.addEventListener('gs-lineage-filter-changed', listenerMock);
186
+ canvasElement.addEventListener(gsEventNames.lineageFilterChanged, listenerMock);
186
187
  });
187
188
 
188
189
  await step('wait until data is loaded', async () => {
@@ -3,6 +3,7 @@ import type { DetailedHTMLProps, HTMLAttributes } from 'react';
3
3
 
4
4
  import { type LineageFilterChangedEvent } from '../../preact/lineageFilter/LineageFilterChangedEvent';
5
5
  import { LineageFilter, type LineageFilterProps } from '../../preact/lineageFilter/lineage-filter';
6
+ import { type gsEventNames } from '../../utils/gsEventNames';
6
7
  import type { Equals, Expect } from '../../utils/typeAssertions';
7
8
  import { PreactLitAdapter } from '../PreactLitAdapter';
8
9
 
@@ -91,7 +92,7 @@ declare global {
91
92
  }
92
93
 
93
94
  interface HTMLElementEventMap {
94
- 'gs-lineage-filter-changed': LineageFilterChangedEvent;
95
+ [gsEventNames.lineageFilterChanged]: LineageFilterChangedEvent;
95
96
  }
96
97
  }
97
98
 
@@ -10,6 +10,7 @@ import '../gs-app';
10
10
  import './gs-location-filter';
11
11
  import data from '../../preact/locationFilter/__mockData__/aggregated.json';
12
12
  import { type LocationFilterProps } from '../../preact/locationFilter/location-filter';
13
+ import { gsEventNames } from '../../utils/gsEventNames';
13
14
  import { withinShadowRoot } from '../withinShadowRoot.story';
14
15
 
15
16
  const codeExample = String.raw`
@@ -26,7 +27,7 @@ const meta: Meta = {
26
27
  component: 'gs-location-filter',
27
28
  parameters: withComponentDocs({
28
29
  actions: {
29
- handles: ['gs-location-changed', ...previewHandles],
30
+ handles: [gsEventNames.locationChanged, ...previewHandles],
30
31
  },
31
32
  componentDocs: {
32
33
  opensShadowDom: true,
@@ -189,7 +190,7 @@ export const FiresEvent: StoryObj<LocationFilterProps> = {
189
190
 
190
191
  const listenerMock = fn();
191
192
  await step('Setup event listener mock', () => {
192
- canvasElement.addEventListener('gs-location-changed', listenerMock);
193
+ canvasElement.addEventListener(gsEventNames.locationChanged, listenerMock);
193
194
  });
194
195
 
195
196
  await step('wait until data is loaded', async () => {
@@ -3,6 +3,7 @@ import type { DetailedHTMLProps, HTMLAttributes } from 'react';
3
3
 
4
4
  import { type LocationChangedEvent } from '../../preact/locationFilter/LocationChangedEvent';
5
5
  import { LocationFilter, type LocationFilterProps } from '../../preact/locationFilter/location-filter';
6
+ import { type gsEventNames } from '../../utils/gsEventNames';
6
7
  import type { Equals, Expect } from '../../utils/typeAssertions';
7
8
  import { PreactLitAdapter } from '../PreactLitAdapter';
8
9
 
@@ -95,7 +96,7 @@ declare global {
95
96
  }
96
97
 
97
98
  interface HTMLElementEventMap {
98
- 'gs-location-changed': LocationChangedEvent;
99
+ [gsEventNames.locationChanged]: LocationChangedEvent;
99
100
  }
100
101
  }
101
102
 
@@ -7,6 +7,7 @@ import { previewHandles } from '../../../.storybook/preview';
7
7
  import { LAPIS_URL, REFERENCE_GENOME_ENDPOINT } from '../../constants';
8
8
  import '../gs-app';
9
9
  import { type MutationFilterProps } from '../../preact/mutationFilter/mutation-filter';
10
+ import { gsEventNames } from '../../utils/gsEventNames';
10
11
  import { withinShadowRoot } from '../withinShadowRoot.story';
11
12
  import './gs-mutation-filter';
12
13
 
@@ -21,7 +22,7 @@ const meta: Meta<MutationFilterProps> = {
21
22
  component: 'gs-mutation-filter',
22
23
  parameters: withComponentDocs({
23
24
  actions: {
24
- handles: ['gs-mutation-filter-changed', ...previewHandles],
25
+ handles: [gsEventNames.mutationFilterChanged, ...previewHandles],
25
26
  },
26
27
  fetchMock: {},
27
28
  componentDocs: {
@@ -73,7 +74,7 @@ export const FiresFilterChangedEvent: StoryObj<MutationFilterProps> = {
73
74
  const inputField = () => canvas.getByPlaceholderText('Enter a mutation', { exact: false });
74
75
  const listenerMock = fn();
75
76
  await step('Setup event listener mock', () => {
76
- canvasElement.addEventListener('gs-mutation-filter-changed', listenerMock);
77
+ canvasElement.addEventListener(gsEventNames.mutationFilterChanged, listenerMock);
77
78
  });
78
79
 
79
80
  await step('wait until data is loaded', async () => {
@@ -4,6 +4,7 @@ import type { DetailedHTMLProps, HTMLAttributes } from 'react';
4
4
  import { ReferenceGenomesAwaiter } from '../../preact/components/ReferenceGenomesAwaiter';
5
5
  import { MutationFilter, type MutationFilterProps } from '../../preact/mutationFilter/mutation-filter';
6
6
  import type { MutationsFilter } from '../../types';
7
+ import { type gsEventNames } from '../../utils/gsEventNames';
7
8
  import type { Equals, Expect } from '../../utils/typeAssertions';
8
9
  import { PreactLitAdapter } from '../PreactLitAdapter';
9
10
 
@@ -95,7 +96,7 @@ declare global {
95
96
  }
96
97
 
97
98
  interface HTMLElementEventMap {
98
- 'gs-mutation-filter-changed': CustomEvent<MutationsFilter>;
99
+ [gsEventNames.mutationFilterChanged]: CustomEvent<MutationsFilter>;
99
100
  }
100
101
  }
101
102
 
@@ -0,0 +1,27 @@
1
+ import { describe, expectTypeOf, test } from 'vitest';
2
+
3
+ import { NumberRangeFilterComponent } from './gs-number-range-filter';
4
+ import { type NumberRangeFilterProps } from '../../preact/numberRangeFilter/number-range-filter';
5
+
6
+ describe('gs-number-range-filter types', () => {
7
+ test('should match', ({}) => {
8
+ expectTypeOf(NumberRangeFilterComponent.prototype)
9
+ .toHaveProperty('value')
10
+ .toEqualTypeOf<NumberRangeFilterProps['value']>();
11
+ expectTypeOf(NumberRangeFilterComponent.prototype)
12
+ .toHaveProperty('lapisField')
13
+ .toEqualTypeOf<NumberRangeFilterProps['lapisField']>();
14
+ expectTypeOf(NumberRangeFilterComponent.prototype)
15
+ .toHaveProperty('sliderMin')
16
+ .toEqualTypeOf<NumberRangeFilterProps['sliderMin']>();
17
+ expectTypeOf(NumberRangeFilterComponent.prototype)
18
+ .toHaveProperty('sliderMax')
19
+ .toEqualTypeOf<NumberRangeFilterProps['sliderMax']>();
20
+ expectTypeOf(NumberRangeFilterComponent.prototype)
21
+ .toHaveProperty('sliderStep')
22
+ .toEqualTypeOf<NumberRangeFilterProps['sliderStep']>();
23
+ expectTypeOf(NumberRangeFilterComponent.prototype)
24
+ .toHaveProperty('width')
25
+ .toEqualTypeOf<NumberRangeFilterProps['width']>();
26
+ });
27
+ });
@@ -0,0 +1,96 @@
1
+ import '../gs-app';
2
+ import './gs-number-range-filter';
3
+
4
+ import { type Meta, type StoryObj } from '@storybook/web-components';
5
+ import { html } from 'lit';
6
+
7
+ import { withComponentDocs } from '../../../.storybook/ComponentDocsBlock';
8
+ import { LAPIS_URL } from '../../constants';
9
+ import { type NumberRangeFilterProps } from '../../preact/numberRangeFilter/number-range-filter';
10
+ import { gsEventNames } from '../../utils/gsEventNames';
11
+
12
+ const codeExample = String.raw`
13
+ <gs-number-range-filter
14
+ value='{"ageFrom": 10, "ageTo": 90}'
15
+ lapisField="age"
16
+ sliderMin="0"
17
+ sliderMax="100"
18
+ sliderStep="1"
19
+ width="50%"
20
+ ></gs-number-range-filter>`;
21
+
22
+ const meta: Meta<NumberRangeFilterProps> = {
23
+ title: 'Input/Number range filter',
24
+ component: 'gs-number-range-filter',
25
+ parameters: withComponentDocs({
26
+ actions: {
27
+ handles: [gsEventNames.numberRangeFilterChanged, gsEventNames.numberRangeValueChanged],
28
+ },
29
+ componentDocs: {
30
+ opensShadowDom: true,
31
+ expectsChildren: false,
32
+ codeExample,
33
+ },
34
+ }),
35
+ tags: ['autodocs'],
36
+ argTypes: {
37
+ value: {
38
+ control: {
39
+ type: 'object',
40
+ },
41
+ },
42
+ lapisField: {
43
+ control: {
44
+ type: 'text',
45
+ },
46
+ },
47
+ sliderMin: {
48
+ control: {
49
+ type: 'number',
50
+ },
51
+ },
52
+ sliderMax: {
53
+ control: {
54
+ type: 'number',
55
+ },
56
+ },
57
+ sliderStep: {
58
+ control: {
59
+ type: 'number',
60
+ },
61
+ },
62
+ width: {
63
+ control: {
64
+ type: 'text',
65
+ },
66
+ },
67
+ },
68
+ };
69
+
70
+ export default meta;
71
+
72
+ export const Default: StoryObj<NumberRangeFilterProps> = {
73
+ render: (args) => {
74
+ return html`
75
+ <gs-app lapis="${LAPIS_URL}">
76
+ <gs-number-range-filter
77
+ .value=${args.value}
78
+ .lapisField=${args.lapisField}
79
+ .sliderMin=${args.sliderMin}
80
+ .sliderMax=${args.sliderMax}
81
+ .sliderStep=${args.sliderStep}
82
+ .width=${args.width}
83
+ >
84
+ </gs-number-range-filter>
85
+ </gs-app>
86
+ `;
87
+ },
88
+ args: {
89
+ lapisField: 'age',
90
+ value: { min: 10, max: 90 },
91
+ sliderMin: 0,
92
+ sliderMax: 100,
93
+ sliderStep: 0.1,
94
+ width: '100%',
95
+ },
96
+ };
@@ -0,0 +1,148 @@
1
+ import { customElement, property } from 'lit/decorators.js';
2
+ import type { DetailedHTMLProps, HTMLAttributes } from 'react';
3
+
4
+ import {
5
+ type NumberRangeFilterChangedEvent,
6
+ type NumberRangeValueChangedEvent,
7
+ } from '../../preact/numberRangeFilter/NumberRangeFilterChangedEvent';
8
+ import { NumberRangeFilter } from '../../preact/numberRangeFilter/number-range-filter';
9
+ import { type gsEventNames } from '../../utils/gsEventNames';
10
+ import { PreactLitAdapter } from '../PreactLitAdapter';
11
+
12
+ /**
13
+ *
14
+ * ## Context
15
+ *
16
+ * This component lets the user specify filters for a `float` or `int` field in LAPIS.
17
+ * It consists of two text input fields and a slider.
18
+ *
19
+ * The slider must be restricted to a certain range.
20
+ * Users can however still enter values outside of this range in the text input fields.
21
+ *
22
+ * @fires {CustomEvent<Record<string, string | undefined>>} gs-number-range-filter-changed
23
+ * Fired when the slider is released,
24
+ * `onBlur` on the input fields after the user has typed a valid range in the input fields,
25
+ * or when one of the input fields is cleared.
26
+ * The `details` of this event contain an object with `${lapisField}From` and `${lapisField}To` as keys.
27
+ * The values are the numbers from the input fields or `undefined` if the input field is empty:
28
+ * ```
29
+ * {
30
+ * [`${lapisField}From`]: number | undefined
31
+ * [`${lapisField}To`]: number | undefined
32
+ * }
33
+ * ```
34
+ * Example:
35
+ * ```
36
+ * {
37
+ * ageFrom: 18,
38
+ * ageTo: undefined
39
+ * }
40
+ * ```
41
+ *
42
+ * @fires {CustomEvent<Record<string, string | undefined>>} gs-number-range-value-changed
43
+ * Similar to the `gs-number-range-filter-changed` event,
44
+ * but contains an `event.detail` that has a fixed format:
45
+ * ```
46
+ * {
47
+ * min: number | undefined
48
+ * max: number | undefined
49
+ * }
50
+ * ```
51
+ * This event should be used when you want to control this component externally.
52
+ * The `event.detail` can be used as the value of the component.
53
+ * Example:
54
+ * ```
55
+ * {
56
+ * min: 18,
57
+ * max: undefined
58
+ * }
59
+ * ```
60
+ */
61
+ @customElement('gs-number-range-filter')
62
+ export class NumberRangeFilterComponent extends PreactLitAdapter {
63
+ /**
64
+ * The value to use for this number filter.
65
+ *
66
+ * Must be of the form:
67
+ * ```
68
+ * {
69
+ * [`${lapisField}From`]: number | undefined
70
+ * [`${lapisField}To`]: number | undefined
71
+ * }
72
+ * ```
73
+ *
74
+ * This is the same format that the `gs-number-value-changed` event will emit.
75
+ */
76
+ @property({ type: Object })
77
+ value: { min?: number; max?: number } = {};
78
+
79
+ /**
80
+ * Required.
81
+ *
82
+ * The LAPIS field name to use for this text filter.
83
+ * The field must exist on this LAPIS instance.
84
+ */
85
+ @property()
86
+ lapisField = '';
87
+
88
+ /**
89
+ * The `min` value to use for the slider.
90
+ */
91
+ @property({ type: Number })
92
+ sliderMin = 0;
93
+
94
+ /**
95
+ * The `max` value to use for the slider.
96
+ */
97
+ @property({ type: Number })
98
+ sliderMax = 100;
99
+
100
+ /**
101
+ * The `step` value to use for the slider.
102
+ *
103
+ * This attribute has no effect on the text input.
104
+ */
105
+ @property({ type: Number })
106
+ sliderStep = 1;
107
+
108
+ /**
109
+ * The width of the component.
110
+ *
111
+ * Visit https://genspectrum.github.io/dashboard-components/?path=/docs/concepts-size-of-components--docs for more information.
112
+ */
113
+ @property({ type: String })
114
+ width: string = '100%';
115
+
116
+ override render() {
117
+ return (
118
+ <NumberRangeFilter
119
+ value={this.value}
120
+ lapisField={this.lapisField}
121
+ sliderMin={this.sliderMin}
122
+ sliderMax={this.sliderMax}
123
+ sliderStep={this.sliderStep}
124
+ width={this.width}
125
+ />
126
+ );
127
+ }
128
+ }
129
+
130
+ declare global {
131
+ interface HTMLElementTagNameMap {
132
+ 'gs-number-range-filter': NumberRangeFilterComponent;
133
+ }
134
+
135
+ interface HTMLElementEventMap {
136
+ [gsEventNames.numberRangeFilterChanged]: NumberRangeFilterChangedEvent;
137
+ [gsEventNames.numberRangeValueChanged]: NumberRangeValueChangedEvent;
138
+ }
139
+ }
140
+
141
+ declare global {
142
+ // eslint-disable-next-line @typescript-eslint/no-namespace
143
+ namespace JSX {
144
+ interface IntrinsicElements {
145
+ 'gs-number-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
146
+ }
147
+ }
148
+ }