@reshape-biotech/design-system 2.3.1 → 2.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 (60) hide show
  1. package/README.md +0 -3
  2. package/dist/components/activity/Activity.stories.svelte +8 -0
  3. package/dist/components/activity/Activity.test.d.ts +1 -0
  4. package/dist/components/activity/Activity.test.js +89 -0
  5. package/dist/components/avatar/Avatar.test.d.ts +1 -0
  6. package/dist/components/avatar/Avatar.test.js +55 -0
  7. package/dist/components/button/Button.test.d.ts +1 -0
  8. package/dist/components/button/Button.test.js +117 -0
  9. package/dist/components/button/Button.test.svelte +5 -0
  10. package/dist/components/button/Button.test.svelte.d.ts +19 -0
  11. package/dist/components/checkbox/Checkbox.test.d.ts +1 -0
  12. package/dist/components/checkbox/Checkbox.test.js +51 -0
  13. package/dist/components/graphs/matrix/Matrix.test.d.ts +1 -0
  14. package/dist/components/graphs/matrix/Matrix.test.js +256 -0
  15. package/dist/components/graphs/matrix/Matrix.test.svelte +49 -0
  16. package/dist/components/graphs/matrix/Matrix.test.svelte.d.ts +4 -0
  17. package/dist/components/graphs/utils/tooltipFormatter.d.ts +1 -0
  18. package/dist/components/graphs/utils/tooltipFormatter.js +18 -4
  19. package/dist/components/icons/index.d.ts +1 -6
  20. package/dist/components/icons/index.js +7 -0
  21. package/dist/components/input/Input.test.d.ts +1 -0
  22. package/dist/components/input/Input.test.js +35 -0
  23. package/dist/components/label/Label.test.d.ts +1 -0
  24. package/dist/components/label/Label.test.js +29 -0
  25. package/dist/components/manual-cfu-counter/ManualCFUCounter.svelte +1 -1
  26. package/dist/components/manual-cfu-counter/test/ManualCFUCounter.test.d.ts +1 -0
  27. package/dist/components/manual-cfu-counter/test/ManualCFUCounter.test.js +319 -0
  28. package/dist/components/modal/components/modal-overlay.svelte +1 -5
  29. package/dist/components/multi-cfu-counter/MultiCFUCounter.svelte +1 -1
  30. package/dist/components/multi-cfu-counter/test/MultiCFUCounter.test.d.ts +1 -0
  31. package/dist/components/multi-cfu-counter/test/MultiCFUCounter.test.js +320 -0
  32. package/dist/components/progress-circle/ProgressCircle.svelte +1 -1
  33. package/dist/components/select/components/SelectItem.svelte +2 -2
  34. package/dist/components/sjsf-wrappers/SjsfNumberInputWrapper.svelte +1 -1
  35. package/dist/components/sjsf-wrappers/SjsfNumberInputWrapper.svelte.d.ts +1 -1
  36. package/dist/components/sjsf-wrappers/SjsfSelectWidgetWrapper.svelte +77 -0
  37. package/dist/components/sjsf-wrappers/SjsfSelectWidgetWrapper.svelte.d.ts +3 -0
  38. package/dist/components/sjsf-wrappers/SjsfTextInputWrapper.svelte.d.ts +1 -1
  39. package/dist/components/sjsf-wrappers/sjsfCustomTheme.js +4 -0
  40. package/dist/components/slider/Slider.stories.svelte +5 -0
  41. package/dist/components/slider/Slider.svelte +7 -1
  42. package/dist/components/slider/Slider.svelte.d.ts +1 -0
  43. package/dist/components/stat-card/StatCard.test.d.ts +1 -0
  44. package/dist/components/stat-card/StatCard.test.js +54 -0
  45. package/dist/components/table/Table.test.d.ts +1 -0
  46. package/dist/components/table/Table.test.js +97 -0
  47. package/dist/components/table/Table.test.svelte +37 -0
  48. package/dist/components/table/Table.test.svelte.d.ts +12 -0
  49. package/dist/components/textarea/Textarea.test.d.ts +1 -0
  50. package/dist/components/textarea/Textarea.test.js +90 -0
  51. package/dist/components/toggle-icon-button/ToggleIconButton.test.d.ts +1 -0
  52. package/dist/components/toggle-icon-button/ToggleIconButton.test.js +100 -0
  53. package/dist/components/tooltip/Tooltip.test.d.ts +1 -0
  54. package/dist/components/tooltip/Tooltip.test.js +81 -0
  55. package/dist/notifications.d.ts +4 -1
  56. package/dist/tailwind.preset.d.ts +3 -0
  57. package/dist/tailwind.preset.js +1 -0
  58. package/dist/tokens.d.ts +8 -0
  59. package/dist/tokens.js +4 -0
  60. package/package.json +197 -197
@@ -0,0 +1,320 @@
1
+ import { render, fireEvent, waitFor, findByText, findByRole, getByTestId, } from '@testing-library/svelte';
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import { tick } from 'svelte';
4
+ import MultiCFUCounter from '../MultiCFUCounter.svelte';
5
+ import MultiCFUCounterTestWrapper from './MultiCFUCounterTestWrapper.svelte';
6
+ class MockImage {
7
+ width = 464;
8
+ height = 464;
9
+ src = '';
10
+ onload = null;
11
+ complete = false;
12
+ constructor() {
13
+ // Set a timeout to simulate async image loading
14
+ setTimeout(() => {
15
+ this.complete = true;
16
+ if (this.onload)
17
+ this.onload();
18
+ }, 0);
19
+ }
20
+ }
21
+ if (typeof global !== 'undefined') {
22
+ global.Image = MockImage;
23
+ // mock resizeobserver which isn't available in gh actions environment
24
+ class MockResizeObserver {
25
+ observe = vi.fn();
26
+ unobserve = vi.fn();
27
+ disconnect = vi.fn();
28
+ }
29
+ global.ResizeObserver = MockResizeObserver;
30
+ }
31
+ const simulateImageLoad = async () => {
32
+ await tick();
33
+ };
34
+ describe('ManualCFUCounter Component', () => {
35
+ beforeEach(() => {
36
+ vi.clearAllMocks();
37
+ });
38
+ it('renders with default props', async () => {
39
+ const { container } = render(MultiCFUCounter, {
40
+ props: {
41
+ imageUrl: 'test-image.jpg',
42
+ },
43
+ });
44
+ await simulateImageLoad();
45
+ const svg = container.querySelector('svg');
46
+ const image = container.querySelector('image');
47
+ const dotsGroup = container.querySelector('#dots');
48
+ expect(svg).toBeTruthy();
49
+ expect(image).toBeTruthy();
50
+ expect(dotsGroup).toBeTruthy();
51
+ const mainSvg = container.querySelector('svg[role="application"]');
52
+ expect(mainSvg).toBeTruthy();
53
+ expect(mainSvg?.getAttribute('role')).toBe('application');
54
+ expect(mainSvg?.getAttribute('aria-label')).toContain('CFU Counter');
55
+ });
56
+ it('displays action buttons when marks are present', async () => {
57
+ const { container } = render(MultiCFUCounter, {
58
+ props: {
59
+ imageUrl: 'test-image.jpg',
60
+ marks: [{ x: 10, y: 10 }],
61
+ activeMarkerName: 'Test Marker',
62
+ },
63
+ });
64
+ await simulateImageLoad();
65
+ const undoButton = await findByRole(container, 'button', { name: /Undo last mark/i });
66
+ const clearButton = await findByText(container, 'Clear all');
67
+ expect(undoButton).toBeTruthy();
68
+ expect(clearButton).toBeTruthy();
69
+ });
70
+ it('does not display action buttons when hideMarkers is true', async () => {
71
+ const { queryByAltText, queryByText } = render(MultiCFUCounter, {
72
+ props: {
73
+ imageUrl: 'test-image.jpg',
74
+ hideMarkers: true,
75
+ marks: [{ x: 10, y: 10 }],
76
+ },
77
+ });
78
+ await simulateImageLoad();
79
+ const undoButton = queryByAltText('Undo last mark');
80
+ const clearButton = queryByText('Clear all');
81
+ expect(undoButton).toBeFalsy();
82
+ expect(clearButton).toBeFalsy();
83
+ });
84
+ it('does not display action buttons when disabled is true', async () => {
85
+ const { queryByAltText, queryByText } = render(MultiCFUCounter, {
86
+ props: {
87
+ imageUrl: 'test-image.jpg',
88
+ disabled: true,
89
+ marks: [{ x: 10, y: 10 }],
90
+ },
91
+ });
92
+ await simulateImageLoad();
93
+ const undoButton = queryByAltText('Undo last mark');
94
+ const clearButton = queryByText('Clear all');
95
+ expect(undoButton).toBeFalsy();
96
+ expect(clearButton).toBeFalsy();
97
+ });
98
+ it('adds cursor-not-allowed class when disabled', async () => {
99
+ const { container } = render(MultiCFUCounter, {
100
+ props: {
101
+ imageUrl: 'test-image.jpg',
102
+ disabled: true,
103
+ },
104
+ });
105
+ await simulateImageLoad();
106
+ const svg = container.querySelector('svg');
107
+ expect(svg?.classList.contains('cursor-not-allowed')).toBe(true);
108
+ });
109
+ it('renders with initial marks provided via marks prop', async () => {
110
+ const initialMarksData = [
111
+ { x: 100, y: 100 },
112
+ { x: 200, y: 200 },
113
+ ];
114
+ const { container } = render(MultiCFUCounter, {
115
+ props: {
116
+ imageUrl: 'test-image.jpg',
117
+ marks: initialMarksData,
118
+ },
119
+ });
120
+ await simulateImageLoad();
121
+ await tick();
122
+ const marksCount = container.querySelector('[data-testid="marks-count"]');
123
+ expect(marksCount?.textContent).toBe('2');
124
+ const markers = container.querySelectorAll('[data-testid^="marker-"]');
125
+ expect(markers.length).toBe(2);
126
+ const circles = container.querySelectorAll('circle');
127
+ expect(circles.length).toBe(2);
128
+ expect(circles[0].getAttribute('cx')).toBe('100');
129
+ expect(circles[0].getAttribute('cy')).toBe('100');
130
+ expect(circles[1].getAttribute('cx')).toBe('200');
131
+ expect(circles[1].getAttribute('cy')).toBe('200');
132
+ });
133
+ it('simulates adding a marker via direct click', async () => {
134
+ const mockOnClick = vi.fn();
135
+ const { container } = render(MultiCFUCounter, {
136
+ props: {
137
+ imageUrl: 'test-image.jpg',
138
+ onclick: mockOnClick,
139
+ },
140
+ });
141
+ await simulateImageLoad();
142
+ const marksCountBefore = container.querySelector('[data-testid="marks-count"]');
143
+ expect(marksCountBefore?.textContent).toBe('0');
144
+ const svg = container.querySelector('svg');
145
+ expect(svg).toBeTruthy();
146
+ if (svg) {
147
+ const viewportElement = container.querySelector('#viewport');
148
+ expect(viewportElement).toBeTruthy();
149
+ if (viewportElement) {
150
+ await fireEvent.click(viewportElement, {
151
+ clientX: 100,
152
+ clientY: 100,
153
+ });
154
+ }
155
+ await waitFor(() => {
156
+ expect(mockOnClick).toHaveBeenCalledTimes(1);
157
+ });
158
+ const callArgs = mockOnClick.mock.calls[0];
159
+ const addedMarks = callArgs[1];
160
+ expect(addedMarks.length).toBe(1);
161
+ expect(addedMarks[0].x).toBeCloseTo(100);
162
+ expect(addedMarks[0].y).toBeCloseTo(100);
163
+ await waitFor(() => {
164
+ const marksCountAfter = container.querySelector('[data-testid="marks-count"]');
165
+ expect(marksCountAfter?.textContent).toBe('1');
166
+ });
167
+ const marker = container.querySelector('[data-testid="marker-1"]');
168
+ expect(marker).toBeTruthy();
169
+ }
170
+ });
171
+ it('simulates adding multiple markers via clicks', async () => {
172
+ const mockOnClick = vi.fn();
173
+ const { container } = render(MultiCFUCounter, {
174
+ props: {
175
+ imageUrl: 'test-image.jpg',
176
+ onclick: mockOnClick,
177
+ },
178
+ });
179
+ await simulateImageLoad();
180
+ let marksCount = container.querySelector('[data-testid="marks-count"]');
181
+ expect(marksCount?.textContent).toBe('0');
182
+ const svg = container.querySelector('svg');
183
+ expect(svg).toBeTruthy();
184
+ if (svg) {
185
+ const viewportElement = container.querySelector('#viewport');
186
+ expect(viewportElement).toBeTruthy();
187
+ if (viewportElement) {
188
+ await fireEvent.click(viewportElement, {
189
+ clientX: 100,
190
+ clientY: 100,
191
+ });
192
+ await waitFor(() => {
193
+ expect(mockOnClick).toHaveBeenCalledTimes(1);
194
+ });
195
+ let firstCallArgs = mockOnClick.mock.calls[0];
196
+ let firstMarks = firstCallArgs[1];
197
+ expect(firstMarks.length).toBe(1);
198
+ await fireEvent.click(viewportElement, {
199
+ clientX: 200,
200
+ clientY: 200,
201
+ });
202
+ }
203
+ await waitFor(() => {
204
+ expect(mockOnClick).toHaveBeenCalledTimes(2);
205
+ });
206
+ let secondCallArgs = mockOnClick.mock.calls[1];
207
+ let secondMarks = secondCallArgs[1];
208
+ expect(secondMarks.length).toBe(2);
209
+ const markers = container.querySelectorAll('[data-testid^="marker-"]');
210
+ expect(markers.length).toBe(2);
211
+ const circles = container.querySelectorAll('circle');
212
+ expect(circles.length).toBe(2);
213
+ expect(mockOnClick).toHaveBeenCalledTimes(2);
214
+ }
215
+ });
216
+ it('simulates clearing marks through the reset button', async () => {
217
+ const mockOnClick = vi.fn();
218
+ const initialMarksData = [
219
+ { x: 100, y: 100 },
220
+ { x: 200, y: 200 },
221
+ ];
222
+ const { container, component } = render(MultiCFUCounterTestWrapper, {
223
+ props: {
224
+ initialTestMarks: initialMarksData,
225
+ onclick: mockOnClick,
226
+ },
227
+ });
228
+ await simulateImageLoad();
229
+ await tick();
230
+ let marksCount = getByTestId(container, 'marks-count');
231
+ expect(marksCount?.textContent).toBe('2');
232
+ const clearButton = await findByText(container, 'Clear all');
233
+ expect(clearButton).toBeTruthy();
234
+ if (clearButton) {
235
+ await fireEvent.click(clearButton);
236
+ await tick();
237
+ const wrapperInstance = component;
238
+ const currentWrapperMarks = wrapperInstance.getCurrentMarks();
239
+ expect(currentWrapperMarks.length).toBe(0);
240
+ marksCount = getByTestId(container, 'wrapper-marks-count');
241
+ expect(marksCount?.textContent).toBe('0');
242
+ const innerMarksCount = getByTestId(container, 'marks-count');
243
+ expect(innerMarksCount?.textContent).toBe('0');
244
+ const markers = container.querySelectorAll('[data-testid^="marker-"]');
245
+ expect(markers.length).toBe(0);
246
+ expect(mockOnClick).not.toHaveBeenCalledWith();
247
+ }
248
+ });
249
+ it('properly constrains pan to prevent moving outside container', async () => {
250
+ const { container } = render(MultiCFUCounter, {
251
+ props: {
252
+ imageUrl: 'test-image.jpg',
253
+ },
254
+ });
255
+ await simulateImageLoad();
256
+ const viewport = container.querySelector('#viewport');
257
+ expect(viewport).toBeTruthy();
258
+ expect(viewport?.getAttribute('transform')).toBe('translate(0, 0) scale(1)');
259
+ });
260
+ it('properly sets up keyboard accessibility attributes', async () => {
261
+ const { container } = render(MultiCFUCounter, {
262
+ props: {
263
+ imageUrl: 'test-image.jpg',
264
+ },
265
+ });
266
+ await simulateImageLoad();
267
+ const svg = container.querySelector('svg[role="application"]');
268
+ expect(svg?.getAttribute('tabindex')).toBe('0');
269
+ expect(svg?.getAttribute('role')).toBe('application');
270
+ expect(svg?.getAttribute('aria-label')).toContain('CFU Counter');
271
+ });
272
+ it('simulates undoing a marker addition via the undo button', async () => {
273
+ const mockOnClick = vi.fn();
274
+ const initialMarksData = [{ x: 50, y: 50 }];
275
+ const { container, component } = render(MultiCFUCounterTestWrapper, {
276
+ props: {
277
+ initialTestMarks: initialMarksData,
278
+ onclick: mockOnClick,
279
+ },
280
+ });
281
+ await simulateImageLoad();
282
+ await tick();
283
+ let marksCount = getByTestId(container, 'marks-count');
284
+ expect(marksCount?.textContent).toBe('1');
285
+ const svg = container.querySelector('svg');
286
+ expect(svg).toBeTruthy();
287
+ if (svg) {
288
+ const viewportElement = container.querySelector('#viewport');
289
+ expect(viewportElement).toBeTruthy();
290
+ if (viewportElement) {
291
+ await fireEvent.click(viewportElement, { clientX: 100, clientY: 100 });
292
+ }
293
+ await tick();
294
+ const wrapperInstance = component;
295
+ const marksAfterClick = wrapperInstance.getCurrentMarks();
296
+ expect(marksAfterClick.length).toBe(2);
297
+ mockOnClick.mockClear();
298
+ }
299
+ const undoButton = await findByRole(container, 'button', { name: /Undo last mark/i });
300
+ expect(undoButton).toBeTruthy();
301
+ if (undoButton) {
302
+ await fireEvent.click(undoButton);
303
+ await tick();
304
+ const wrapperInstance = component;
305
+ const marksAfterUndo = wrapperInstance.getCurrentMarks();
306
+ expect(marksAfterUndo.length).toBe(1);
307
+ expect(marksAfterUndo[0].x).toBeCloseTo(50);
308
+ expect(marksAfterUndo[0].y).toBeCloseTo(50);
309
+ await waitFor(() => {
310
+ const innerMarksCount = getByTestId(container, 'marks-count');
311
+ expect(innerMarksCount?.textContent).toBe('1');
312
+ const wrapperMarksCount = getByTestId(container, 'wrapper-marks-count');
313
+ expect(wrapperMarksCount?.textContent).toBe('1');
314
+ });
315
+ const markers = container.querySelectorAll('[data-testid^="marker-"]');
316
+ expect(markers.length).toBe(1);
317
+ expect(mockOnClick).not.toHaveBeenCalled();
318
+ }
319
+ });
320
+ });
@@ -19,7 +19,7 @@
19
19
  const radius = size === 'md' ? 7 : 6;
20
20
 
21
21
  // Create the circle path
22
- const path = `M ${center} ${center - radius}
22
+ const path = `M ${center} ${center - radius}
23
23
  A ${radius} ${radius} 0 1 1 ${center - 0.01} ${center - radius}`;
24
24
  </script>
25
25
 
@@ -13,8 +13,8 @@
13
13
  let { class: className = '', children, ...restProps }: Props = $props();
14
14
 
15
15
  const baseClasses = `
16
- relative flex w-full h-auto gap-2 cursor-default select-none items-center justify-between rounded-lg p-3 text-sm outline-none
17
- focus:bg-neutral focus:text-accent-foreground
16
+ relative flex w-full h-auto gap-2 cursor-default select-none items-center justify-between rounded-lg p-3 text-sm outline-none
17
+ focus:bg-neutral focus:text-accent-foreground
18
18
  data-[disabled]:pointer-events-none data-[disabled]:opacity-50
19
19
  data-[highlighted]:bg-neutral data-[highlighted]:text-accent-foreground
20
20
  `;
@@ -86,7 +86,7 @@
86
86
  <Input
87
87
  bind:value={() => getStringValue(), (v) => setNumericValue(v)}
88
88
  placeholder={String(placeholder)}
89
- type={'number'}
89
+ type={'text'}
90
90
  inputmode={'decimal'}
91
91
  pattern={config?.schema?.type === 'integer' ? '-?\\d*' : '-?\\d*\\.?\\d*'}
92
92
  {readonly}
@@ -1,3 +1,3 @@
1
- declare const SjsfNumberInputWrapper: import("svelte").Component<ComponentProps, {}, "value">;
1
+ declare const SjsfNumberInputWrapper: import("svelte").Component<any, {}, "value">;
2
2
  type SjsfNumberInputWrapper = ReturnType<typeof SjsfNumberInputWrapper>;
3
3
  export default SjsfNumberInputWrapper;
@@ -0,0 +1,77 @@
1
+ <script lang="ts">
2
+ import { getFormContext, selectAttributes } from '@sjsf/form';
3
+ import type { ComponentProps } from '@sjsf/form';
4
+ import * as Select from '../select';
5
+
6
+ type Props = ComponentProps['selectWidget'];
7
+ let { handlers, value = $bindable(), options = [], config }: Props = $props();
8
+
9
+ const ctx = getFormContext();
10
+ const attributes = $derived(
11
+ selectAttributes(ctx, config, 'select', handlers, { style: 'flex-grow: 1' })
12
+ );
13
+
14
+ const isPrimitive = (v: unknown): v is string | number | boolean =>
15
+ ['string', 'number', 'boolean'].includes(typeof v);
16
+
17
+ let domValue = $derived.by(() => {
18
+ const primitiveMatch = options.find((o) => isPrimitive(o.value) && o.value === value);
19
+ if (primitiveMatch) return String(primitiveMatch.value);
20
+ const idx = options.findIndex((o) => o.value === value);
21
+ return idx >= 0 ? String(idx) : '';
22
+ });
23
+
24
+ const onSelectChange = (v: string | undefined) => {
25
+ if (v === undefined || v === '') {
26
+ value = undefined;
27
+ } else {
28
+ const primitiveMatch = options.find((o) => isPrimitive(o.value) && String(o.value) === v);
29
+ if (primitiveMatch) {
30
+ value = primitiveMatch.value;
31
+ } else {
32
+ const idx = Number(v);
33
+ if (!Number.isNaN(idx) && options[idx]) {
34
+ value = options[idx].value;
35
+ }
36
+ }
37
+ }
38
+ if (typeof (attributes as any).onchange === 'function') {
39
+ (attributes as any).onchange(new Event('change'));
40
+ }
41
+ };
42
+ </script>
43
+
44
+ <input
45
+ type="hidden"
46
+ name={String(attributes.name)}
47
+ id={String(attributes.id)}
48
+ value={domValue}
49
+ disabled={attributes.disabled}
50
+ />
51
+
52
+ <Select.Root value={domValue} type="single" items={[]} onValueChange={onSelectChange}>
53
+ <Select.Trigger
54
+ placeholder={attributes.placeholder ?? undefined}
55
+ displayValue={options.find((o) => isPrimitive(o.value) && String(o.value) === domValue)
56
+ ?.label || options.find((o, idx) => String(idx) === domValue && !isPrimitive(o.value))?.label}
57
+ disabled={attributes.disabled}
58
+ />
59
+ <Select.Portal>
60
+ <Select.Content>
61
+ {#if config.schema.default === undefined}
62
+ <Select.Item value="" label={attributes.placeholder ?? undefined} />
63
+ {/if}
64
+ {#each options as option, index (option.id)}
65
+ {#if isPrimitive(option.value)}
66
+ <Select.Item
67
+ value={String(option.value)}
68
+ label={option.label}
69
+ disabled={option.disabled}
70
+ />
71
+ {:else}
72
+ <Select.Item value={String(index)} label={option.label} disabled={option.disabled} />
73
+ {/if}
74
+ {/each}
75
+ </Select.Content>
76
+ </Select.Portal>
77
+ </Select.Root>
@@ -0,0 +1,3 @@
1
+ declare const SjsfSelectWidgetWrapper: import("svelte").Component<any, {}, "value">;
2
+ type SjsfSelectWidgetWrapper = ReturnType<typeof SjsfSelectWidgetWrapper>;
3
+ export default SjsfSelectWidgetWrapper;
@@ -1,3 +1,3 @@
1
- declare const SjsfTextInputWrapper: import("svelte").Component<ComponentProps, {}, "value">;
1
+ declare const SjsfTextInputWrapper: import("svelte").Component<any, {}, "value">;
2
2
  type SjsfTextInputWrapper = ReturnType<typeof SjsfTextInputWrapper>;
3
3
  export default SjsfTextInputWrapper;
@@ -2,7 +2,11 @@ import { theme as BasicTheme } from '@sjsf/basic-theme';
2
2
  import { overrideByRecord } from '@sjsf/form/lib/resolver';
3
3
  import SjsfNumberInputWrapper from './SjsfNumberInputWrapper.svelte';
4
4
  import SjsfTextInputWrapper from './SjsfTextInputWrapper.svelte';
5
+ import EnumField from '@sjsf/form/fields/extra-fields/enum.svelte';
6
+ import SjsfSelectWidgetWrapper from './SjsfSelectWidgetWrapper.svelte';
5
7
  export const sjsfCustomTheme = overrideByRecord(BasicTheme, {
6
8
  numberWidget: SjsfNumberInputWrapper,
7
9
  textWidget: SjsfTextInputWrapper,
10
+ enumField: EnumField,
11
+ selectWidget: SjsfSelectWidgetWrapper,
8
12
  });
@@ -35,3 +35,8 @@
35
35
  bufferColorClass="bg-danger-inverse"
36
36
  ></Slider>
37
37
  </Story>
38
+ <Story name="Disabled" asChild>
39
+ <div class="opacity-75">
40
+ <Slider value={2} max={4} buffer={2} disabled></Slider>
41
+ </div>
42
+ </Story>
@@ -14,6 +14,7 @@
14
14
  export let onMousedown: () => void = () => {};
15
15
  export let onMouseup: () => void = () => {};
16
16
  export let id: string | undefined = undefined;
17
+ export let disabled = false;
17
18
 
18
19
  $: if (value > max) {
19
20
  value = max;
@@ -29,7 +30,7 @@
29
30
  }
30
31
  </script>
31
32
 
32
- <div class={`relative flex h-10 items-center ${className}`}>
33
+ <div class={`relative flex h-10 items-center ${className} ${disabled ? 'opacity-40' : ''}`}>
33
34
  <div class="track-overlay"></div>
34
35
  <div
35
36
  class="pointer-events-none absolute h-2.5 rounded-full {bufferColorClass}"
@@ -62,6 +63,7 @@
62
63
  bind:value
63
64
  on:mousedown={onMousedown}
64
65
  on:mouseup={onMouseup}
66
+ {disabled}
65
67
  />
66
68
  <div class="thumb-overlay" style="left: {calculatePosition(value)} + 0.5rem)">
67
69
  <Icon iconName="CaretUpDown" class="rotate-90" />
@@ -124,4 +126,8 @@
124
126
  height: 0;
125
127
  border: none;
126
128
  }
129
+
130
+ input[type='range']:disabled {
131
+ cursor: not-allowed;
132
+ }
127
133
  </style>
@@ -24,6 +24,7 @@ declare const Slider: $$__sveltets_2_IsomorphicComponent<{
24
24
  onMousedown?: () => void;
25
25
  onMouseup?: () => void;
26
26
  id?: string | undefined;
27
+ disabled?: boolean;
27
28
  }, {
28
29
  [evt: string]: CustomEvent<any>;
29
30
  }, {}, {}, string>;
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom';
@@ -0,0 +1,54 @@
1
+ import { render, fireEvent, screen } from '@testing-library/svelte';
2
+ import { tick } from 'svelte';
3
+ import StatCard from './StatCard.svelte';
4
+ import '@testing-library/jest-dom';
5
+ import { describe, it, expect, vi } from 'vitest';
6
+ describe('StatCard', () => {
7
+ it('should focus the input field when the pencil edit icon is clicked', async () => {
8
+ render(StatCard, {
9
+ props: {
10
+ title: 'Test Stat',
11
+ value: 'Initial Value',
12
+ editable: true,
13
+ onsubmit: vi.fn(),
14
+ },
15
+ });
16
+ const pencilButton = screen.getByRole('button');
17
+ await fireEvent.click(pencilButton);
18
+ // Wait for Svelte's reactivity and the $effect with tick() to apply focus
19
+ await tick();
20
+ const inputField = screen.getByRole('textbox');
21
+ expect(inputField).toHaveFocus();
22
+ });
23
+ it('should focus the input field when the card body is clicked', async () => {
24
+ render(StatCard, {
25
+ props: {
26
+ title: 'Clickable Stat',
27
+ value: 'Initial Value',
28
+ editable: true,
29
+ onsubmit: vi.fn(),
30
+ },
31
+ });
32
+ const cardBody = screen.getByTestId('stat-card-body');
33
+ await fireEvent.click(cardBody);
34
+ await tick(); // Wait for Svelte's reactivity
35
+ const inputField = screen.getByRole('textbox');
36
+ expect(inputField).toHaveFocus();
37
+ });
38
+ it('should not focus the input field when the card body is clicked, out of editable state', async () => {
39
+ render(StatCard, {
40
+ props: {
41
+ title: 'Clickable Stat',
42
+ value: 'Initial Value',
43
+ editable: false,
44
+ onsubmit: vi.fn(),
45
+ },
46
+ });
47
+ const cardBody = screen.getByTestId('stat-card-body');
48
+ await fireEvent.click(cardBody);
49
+ await tick(); // Wait for Svelte's reactivity
50
+ // When not editable, the input field should not be rendered
51
+ const inputField = screen.queryByRole('textbox');
52
+ expect(inputField).toBeNull();
53
+ });
54
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,97 @@
1
+ import { render, screen } from '@testing-library/svelte';
2
+ import Table from './Table.test.svelte';
3
+ const users = [
4
+ {
5
+ name: 'John Doe',
6
+ age: 25,
7
+ role: 'admin',
8
+ },
9
+ {
10
+ name: 'Jane Smith',
11
+ age: 32,
12
+ role: 'member',
13
+ },
14
+ {
15
+ name: 'Michael Johnson',
16
+ age: 41,
17
+ role: 'deactivated',
18
+ },
19
+ {
20
+ name: 'Emily Brown',
21
+ age: 28,
22
+ role: 'member',
23
+ },
24
+ {
25
+ name: 'David Lee',
26
+ age: 37,
27
+ role: 'admin',
28
+ },
29
+ {
30
+ name: 'Sarah Wilson',
31
+ age: 29,
32
+ role: 'member',
33
+ },
34
+ {
35
+ name: 'Robert Taylor',
36
+ age: 45,
37
+ role: 'deactivated',
38
+ },
39
+ {
40
+ name: 'Lisa Anderson',
41
+ age: 33,
42
+ role: 'member',
43
+ },
44
+ {
45
+ name: 'James Martinez',
46
+ age: 39,
47
+ role: 'admin',
48
+ },
49
+ {
50
+ name: 'Jennifer Garcia',
51
+ age: 31,
52
+ role: 'deactivated',
53
+ },
54
+ ];
55
+ describe('Table Component', () => {
56
+ it('renders the table with correct headers', () => {
57
+ render(Table, { props: { users } });
58
+ expect(screen.getByText('Name')).toBeInTheDocument();
59
+ expect(screen.getByText('Age')).toBeInTheDocument();
60
+ expect(screen.getByText('Role')).toBeInTheDocument();
61
+ });
62
+ it('renders table rows with user data', () => {
63
+ render(Table, { props: { users } });
64
+ users.forEach((user) => {
65
+ expect(screen.getByTestId(`user-${user.name}`)).toBeInTheDocument();
66
+ expect(screen.getByTestId(`user-${user.name}-age`)).toBeInTheDocument();
67
+ expect(screen.getByTestId(`user-${user.name}-role`)).toBeInTheDocument();
68
+ });
69
+ });
70
+ it('applies disabled class to deactivated users', () => {
71
+ render(Table, { props: { users } });
72
+ const deactivatedUser = screen.getByText('Michael Johnson');
73
+ expect(deactivatedUser.closest('tr')).toHaveClass('disabled');
74
+ });
75
+ it('renders phone icon button for admin users', () => {
76
+ render(Table, { props: { users, userRole: 'admin' } });
77
+ const phoneIcon = screen.getAllByTestId('phone-icon');
78
+ expect(phoneIcon).toHaveLength(users.length);
79
+ });
80
+ it('does not render phone icon button for non-admin users', () => {
81
+ render(Table, { props: { users, userRole: 'member' } });
82
+ const phoneIcon = screen.queryAllByTestId('phone-icon');
83
+ expect(phoneIcon).toHaveLength(0);
84
+ });
85
+ it('applies disabled styles to td elements when disabled', () => {
86
+ render(Table, { props: { users, userRole: 'member' } });
87
+ const deactivatedUser = screen.getByText('Michael Johnson');
88
+ const tr = deactivatedUser.closest('tr');
89
+ if (tr) {
90
+ expect(tr.classList.contains('disabled')).toBe(true);
91
+ expect(tr.classList.contains('[&.disabled]:text-tertiary')).toBe(true);
92
+ }
93
+ else {
94
+ throw new Error('Tr element not found');
95
+ }
96
+ });
97
+ });