@axinom/mosaic-ui 0.66.0-rc.9 → 0.66.0
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.
- package/dist/components/DynamicDataList/DynamicListHeader/DynamicListHeader.d.ts.map +1 -1
- package/dist/components/FieldSelection/FieldSelection.d.ts.map +1 -1
- package/dist/components/Filters/Filter/Filter.d.ts.map +1 -1
- package/dist/components/Filters/Filters.model.d.ts +5 -0
- package/dist/components/Filters/Filters.model.d.ts.map +1 -1
- package/dist/components/Filters/SelectionTypes/DateTimeFilter/DateTimeFilter.d.ts +2 -0
- package/dist/components/Filters/SelectionTypes/DateTimeFilter/DateTimeFilter.d.ts.map +1 -1
- package/dist/components/Filters/SelectionTypes/FreeTextFilter/FreeTextFilter.d.ts +2 -0
- package/dist/components/Filters/SelectionTypes/FreeTextFilter/FreeTextFilter.d.ts.map +1 -1
- package/dist/components/Filters/SelectionTypes/MultiOptionFilter/MultiOptionFilter.d.ts +2 -0
- package/dist/components/Filters/SelectionTypes/MultiOptionFilter/MultiOptionFilter.d.ts.map +1 -1
- package/dist/components/Filters/SelectionTypes/NumericTextFilter/NumericTextFilter.d.ts +2 -0
- package/dist/components/Filters/SelectionTypes/NumericTextFilter/NumericTextFilter.d.ts.map +1 -1
- package/dist/components/Filters/SelectionTypes/OptionsFilter/OptionsFilter.d.ts +2 -0
- package/dist/components/Filters/SelectionTypes/OptionsFilter/OptionsFilter.d.ts.map +1 -1
- package/dist/components/Filters/SelectionTypes/SearcheableOptionsFilter/SearcheableOptionsFilter.d.ts +2 -0
- package/dist/components/Filters/SelectionTypes/SearcheableOptionsFilter/SearcheableOptionsFilter.d.ts.map +1 -1
- package/dist/components/FormElements/Radio/Radio.d.ts.map +1 -1
- package/dist/components/FormElements/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/components/Hub/Tile/Tile.d.ts.map +1 -1
- package/dist/components/Icons/Icons.d.ts +4 -9
- package/dist/components/Icons/Icons.d.ts.map +1 -1
- package/dist/components/LandingPageTiles/TileLarge/TileLarge.d.ts.map +1 -1
- package/dist/components/LandingPageTiles/TileSmall/TileSmall.d.ts.map +1 -1
- package/dist/components/List/ListCheckBox/ListCheckBox.d.ts.map +1 -1
- package/dist/components/List/ListHeader/ColumnLabel/ColumnLabel.d.ts.map +1 -1
- package/dist/components/List/ListHeader/ListHeader.d.ts.map +1 -1
- package/dist/components/List/ListRow/ListRow.d.ts.map +1 -1
- package/dist/components/List/ListRow/ListRowCell/ListRowCell.d.ts +15 -0
- package/dist/components/List/ListRow/ListRowCell/ListRowCell.d.ts.map +1 -0
- package/dist/components/List/ListRow/ListRowCell/renderData.d.ts +9 -0
- package/dist/components/List/ListRow/ListRowCell/renderData.d.ts.map +1 -0
- package/dist/components/List/ListRow/Renderers/TagsRenderer/TagsRenderer.d.ts.map +1 -1
- package/dist/components/Loaders/ImageLoader/ImageLoader.d.ts.map +1 -1
- package/dist/components/PageHeader/PageHeaderAction/PageHeaderAction.d.ts.map +1 -1
- package/dist/components/Utils/Postgraphile/CreateConnectionRenderer.d.ts +1 -1
- package/dist/components/Utils/Postgraphile/CreateConnectionRenderer.d.ts.map +1 -1
- package/dist/components/VisualElements/ImgElement.d.ts +50 -0
- package/dist/components/VisualElements/ImgElement.d.ts.map +1 -0
- package/dist/components/VisualElements/SvgElement.d.ts +14 -0
- package/dist/components/VisualElements/SvgElement.d.ts.map +1 -0
- package/dist/components/VisualElements/index.d.ts +3 -0
- package/dist/components/VisualElements/index.d.ts.map +1 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/helpers/idleCallbackHelpers.d.ts +42 -0
- package/dist/helpers/idleCallbackHelpers.d.ts.map +1 -0
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/hooks/useResize/ResizeIndicator.d.ts +8 -0
- package/dist/hooks/useResize/ResizeIndicator.d.ts.map +1 -0
- package/dist/hooks/useResize/useResize.d.ts +5 -2
- package/dist/hooks/useResize/useResize.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.es.js +4 -4
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Accordion/Accordion.scss +1 -1
- package/src/components/Accordion/AccordionItem/AccordionItem.scss +2 -2
- package/src/components/Buttons/Button/Button.scss +5 -5
- package/src/components/Buttons/CompositeButton/CompositeButton.scss +7 -7
- package/src/components/Buttons/TextButton/TextButton.scss +6 -6
- package/src/components/ConfirmDialog/ConfirmDialog.scss +1 -1
- package/src/components/DateTime/DatePicker/DatePicker.scss +12 -12
- package/src/components/DateTime/TimePicker/ScrollColumn/ScrollColumn.scss +7 -7
- package/src/components/DateTime/TimePicker/TimePicker.scss +1 -1
- package/src/components/DynamicDataList/DynamicListHeader/DynamicListHeader.scss +2 -2
- package/src/components/DynamicDataList/DynamicListHeader/DynamicListHeader.spec.tsx +2 -0
- package/src/components/DynamicDataList/DynamicListHeader/DynamicListHeader.tsx +62 -50
- package/src/components/DynamicDataList/DynamicListRow/DynamicListRow.scss +2 -2
- package/src/components/FieldSelection/FieldSelection.scss +4 -0
- package/src/components/FieldSelection/FieldSelection.tsx +1 -0
- package/src/components/Filters/Filter/Filter.scss +34 -15
- package/src/components/Filters/Filter/Filter.spec.tsx +1 -1
- package/src/components/Filters/Filter/Filter.tsx +46 -34
- package/src/components/Filters/Filters.model.ts +6 -0
- package/src/components/Filters/SelectionTypes/DateTimeFilter/DateTimeFilter.tsx +5 -0
- package/src/components/Filters/SelectionTypes/FreeTextFilter/FreeTextFilter.tsx +4 -0
- package/src/components/Filters/SelectionTypes/MultiOptionFilter/MultiOptionFilter.scss +1 -1
- package/src/components/Filters/SelectionTypes/MultiOptionFilter/MultiOptionFilter.tsx +9 -1
- package/src/components/Filters/SelectionTypes/NumericTextFilter/NumericTextFilter.tsx +5 -0
- package/src/components/Filters/SelectionTypes/OptionsFilter/OptionsFilter.scss +6 -10
- package/src/components/Filters/SelectionTypes/OptionsFilter/OptionsFilter.tsx +8 -0
- package/src/components/Filters/SelectionTypes/SearcheableOptionsFilter/SearcheableOptionsFilter.tsx +6 -1
- package/src/components/FormElements/Checkbox/Checkbox.scss +4 -4
- package/src/components/FormElements/CustomTags/CustomTags.scss +1 -1
- package/src/components/FormElements/FileUploadControl/FileUploadControl.scss +4 -4
- package/src/components/FormElements/Radio/Radio.scss +5 -5
- package/src/components/FormElements/Radio/Radio.tsx +3 -2
- package/src/components/FormElements/Select/Select.scss +11 -6
- package/src/components/FormElements/ToggleButton/ToggleButton.scss +7 -7
- package/src/components/FormElements/ToggleButton/ToggleButton.tsx +32 -27
- package/src/components/Hub/Hub.stories.tsx +3 -2
- package/src/components/Hub/Tile/Tile.spec.tsx +7 -2
- package/src/components/Hub/Tile/Tile.tsx +2 -1
- package/src/components/Icons/Icons.scss +1 -0
- package/src/components/Icons/Icons.spec.tsx +90 -41
- package/src/components/Icons/Icons.tsx +357 -765
- package/src/components/InfoTooltip/InfoTooltip.scss +1 -1
- package/src/components/InlineMenu/InlineMenu.scss +2 -2
- package/src/components/LandingPageTiles/LandingPageTiles.stories.tsx +3 -2
- package/src/components/LandingPageTiles/TileLarge/TileLarge.spec.tsx +5 -1
- package/src/components/LandingPageTiles/TileLarge/TileLarge.tsx +2 -1
- package/src/components/LandingPageTiles/TileSmall/TileSmall.spec.tsx +7 -2
- package/src/components/LandingPageTiles/TileSmall/TileSmall.tsx +2 -1
- package/src/components/List/List.scss +4 -4
- package/src/components/List/ListCheckBox/ListCheckBox.tsx +1 -0
- package/src/components/List/ListHeader/ColumnLabel/ColumnLabel.spec.tsx +6 -6
- package/src/components/List/ListHeader/ColumnLabel/ColumnLabel.tsx +10 -13
- package/src/components/List/ListHeader/ListHeader.scss +2 -3
- package/src/components/List/ListHeader/ListHeader.spec.tsx +2 -0
- package/src/components/List/ListHeader/ListHeader.tsx +57 -51
- package/src/components/List/ListRow/ListRow.scss +1 -28
- package/src/components/List/ListRow/ListRow.spec.tsx +10 -8
- package/src/components/List/ListRow/ListRow.tsx +20 -152
- package/src/components/List/ListRow/ListRowCell/ListRowCell.scss +26 -0
- package/src/components/List/ListRow/ListRowCell/ListRowCell.spec.tsx +491 -0
- package/src/components/List/ListRow/ListRowCell/ListRowCell.tsx +57 -0
- package/src/components/List/ListRow/ListRowCell/renderData.tsx +124 -0
- package/src/components/List/ListRow/Renderers/TagsRenderer/TagsRenderer.spec.tsx +191 -80
- package/src/components/List/ListRow/Renderers/TagsRenderer/TagsRenderer.tsx +63 -34
- package/src/components/Loaders/ImageLoader/ImageLoader.spec.tsx +13 -14
- package/src/components/Loaders/ImageLoader/ImageLoader.tsx +5 -3
- package/src/components/PageHeader/PageHeader.scss +1 -1
- package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.tsx +13 -2
- package/src/components/Tabs/Tab/CustomTab.scss +4 -4
- package/src/components/Tabs/TabList/CustomTabList.scss +2 -2
- package/src/components/Utils/Postgraphile/CreateConnectionRenderer.spec.ts +48 -12
- package/src/components/Utils/Postgraphile/CreateConnectionRenderer.tsx +5 -4
- package/src/components/VisualElements/ImgElement.spec.tsx +92 -0
- package/src/components/VisualElements/ImgElement.tsx +72 -0
- package/src/components/VisualElements/SvgElement.spec.tsx +160 -0
- package/src/components/VisualElements/SvgElement.tsx +40 -0
- package/src/components/VisualElements/index.ts +7 -0
- package/src/components/index.ts +1 -0
- package/src/helpers/idleCallbackHelpers.ts +66 -0
- package/src/helpers/index.ts +5 -0
- package/src/hooks/useResize/ResizeIndicator.scss +7 -0
- package/src/hooks/useResize/ResizeIndicator.tsx +39 -0
- package/src/hooks/useResize/{useResize.ts → useResize.tsx} +38 -6
- package/src/index.ts +2 -0
- package/src/styles/branding.scss +89 -0
- package/src/styles/variables.scss +245 -187
|
@@ -1,125 +1,236 @@
|
|
|
1
1
|
import '@testing-library/jest-dom';
|
|
2
|
-
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { act, render, screen, waitFor } from '@testing-library/react';
|
|
3
3
|
import React from 'react';
|
|
4
|
+
import * as idleCallbackHelpers from '../../../../../helpers/idleCallbackHelpers';
|
|
5
|
+
import { noop } from '../../../../../helpers/utils';
|
|
4
6
|
import { TagsRenderer } from './TagsRenderer';
|
|
5
7
|
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
observe: jest.fn(),
|
|
9
|
-
unobserve: jest.fn(),
|
|
10
|
-
disconnect: jest.fn(),
|
|
11
|
-
}));
|
|
12
|
-
|
|
13
|
-
interface RendererWrapperProps {
|
|
14
|
-
/** Column data */
|
|
8
|
+
// Wrapper component to properly render TagsRenderer
|
|
9
|
+
interface TagsRendererWrapperProps {
|
|
15
10
|
value: unknown;
|
|
16
11
|
}
|
|
17
12
|
|
|
18
|
-
const
|
|
13
|
+
const TagsRendererWrapper: React.FC<TagsRendererWrapperProps> = ({ value }) => {
|
|
19
14
|
return <>{TagsRenderer(value)}</>;
|
|
20
15
|
};
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
// Helper to trigger ResizeObserver callback
|
|
18
|
+
const triggerResizeObserver = (): void => {
|
|
19
|
+
const resizeObserverCallback = (global.ResizeObserver as jest.Mock).mock
|
|
20
|
+
.calls[0]?.[0];
|
|
21
|
+
if (resizeObserverCallback) {
|
|
22
|
+
act(() => {
|
|
23
|
+
resizeObserverCallback([]);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
describe('TagsRenderer', () => {
|
|
29
|
+
let mockResizeObserver: {
|
|
30
|
+
observe: jest.Mock;
|
|
31
|
+
unobserve: jest.Mock;
|
|
32
|
+
disconnect: jest.Mock;
|
|
33
|
+
};
|
|
34
|
+
let mockScheduleIdleCallback: jest.SpyInstance;
|
|
35
|
+
let mockCancelScheduledCallback: jest.SpyInstance;
|
|
36
|
+
|
|
27
37
|
beforeEach(() => {
|
|
28
|
-
//
|
|
29
|
-
|
|
38
|
+
// Setup ResizeObserver mock
|
|
39
|
+
mockResizeObserver = {
|
|
40
|
+
observe: jest.fn(),
|
|
41
|
+
unobserve: jest.fn(),
|
|
42
|
+
disconnect: jest.fn(),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
global.ResizeObserver = jest
|
|
46
|
+
.fn()
|
|
47
|
+
.mockImplementation(() => mockResizeObserver);
|
|
48
|
+
|
|
49
|
+
// Setup scheduleIdleCallback mock
|
|
50
|
+
mockScheduleIdleCallback = jest
|
|
51
|
+
.spyOn(idleCallbackHelpers, 'scheduleIdleCallback')
|
|
52
|
+
.mockImplementation((callback) => {
|
|
53
|
+
callback({
|
|
54
|
+
didTimeout: false,
|
|
55
|
+
timeRemaining: () => 50,
|
|
56
|
+
} as IdleDeadline);
|
|
57
|
+
return 1;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Setup cancelScheduledCallback mock
|
|
61
|
+
mockCancelScheduledCallback = jest
|
|
62
|
+
.spyOn(idleCallbackHelpers, 'cancelScheduledCallback')
|
|
63
|
+
.mockImplementation(noop);
|
|
64
|
+
|
|
65
|
+
// Mock window.getComputedStyle for font size calculations
|
|
66
|
+
window.getComputedStyle = jest.fn().mockReturnValue({
|
|
67
|
+
fontSize: '14px',
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Mock offsetWidth for container
|
|
71
|
+
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
|
|
72
|
+
configurable: true,
|
|
73
|
+
writable: true,
|
|
74
|
+
value: 300,
|
|
75
|
+
});
|
|
30
76
|
});
|
|
31
77
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
// Don't use jest.clearAllMocks() as it removes mock implementations
|
|
80
|
+
// Instead, manually clear just the mock call history
|
|
81
|
+
mockResizeObserver.observe.mockClear();
|
|
82
|
+
mockResizeObserver.unobserve.mockClear();
|
|
83
|
+
mockResizeObserver.disconnect.mockClear();
|
|
84
|
+
mockScheduleIdleCallback.mockClear();
|
|
85
|
+
mockCancelScheduledCallback.mockClear();
|
|
35
86
|
});
|
|
36
87
|
|
|
37
|
-
|
|
38
|
-
|
|
88
|
+
describe('rendering', () => {
|
|
89
|
+
it('should render empty container when value is empty array', () => {
|
|
90
|
+
render(<TagsRendererWrapper value={[]} />);
|
|
39
91
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
value: 300, // Assume container width of 300px
|
|
92
|
+
const container = screen.getByTestId('tags-container');
|
|
93
|
+
expect(container).toBeInTheDocument();
|
|
94
|
+
expect(container).toBeEmptyDOMElement();
|
|
44
95
|
});
|
|
45
96
|
|
|
46
|
-
render(
|
|
97
|
+
it('should render container with title attribute containing all tags', () => {
|
|
98
|
+
const tags = ['tag1', 'tag2', 'tag3'];
|
|
47
99
|
|
|
48
|
-
|
|
49
|
-
const resizeObserverCallback = (global.ResizeObserver as jest.Mock).mock
|
|
50
|
-
.calls[0]?.[0];
|
|
100
|
+
render(<TagsRendererWrapper value={tags} />);
|
|
51
101
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
102
|
+
const container = screen.getByTestId('tags-container');
|
|
103
|
+
expect(container).toHaveAttribute('title', 'tag1, tag2, tag3');
|
|
104
|
+
});
|
|
55
105
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
expect(container).toBeInTheDocument();
|
|
59
|
-
});
|
|
106
|
+
it('should render tags when array is provided', () => {
|
|
107
|
+
const tags = ['tag1', 'tag2', 'tag3'];
|
|
60
108
|
|
|
61
|
-
|
|
62
|
-
const mockTags = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5'];
|
|
109
|
+
render(<TagsRendererWrapper value={tags} />);
|
|
63
110
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
111
|
+
const container = screen.getByTestId('tags-container');
|
|
112
|
+
expect(container).toBeInTheDocument();
|
|
113
|
+
|
|
114
|
+
triggerResizeObserver();
|
|
115
|
+
|
|
116
|
+
expect(mockScheduleIdleCallback).toHaveBeenCalled();
|
|
68
117
|
});
|
|
69
118
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
119
|
+
it('should render tags with proper text content', async () => {
|
|
120
|
+
const tags = ['JavaScript', 'React', 'Testing'];
|
|
121
|
+
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
|
|
122
|
+
configurable: true,
|
|
123
|
+
writable: true,
|
|
124
|
+
value: 500,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
render(<TagsRendererWrapper value={tags} />);
|
|
128
|
+
|
|
129
|
+
triggerResizeObserver();
|
|
130
|
+
|
|
131
|
+
await waitFor(() => {
|
|
132
|
+
expect(screen.getByText('JavaScript')).toBeInTheDocument();
|
|
133
|
+
expect(screen.getByText('React')).toBeInTheDocument();
|
|
134
|
+
expect(screen.getByText('Testing')).toBeInTheDocument();
|
|
135
|
+
});
|
|
80
136
|
});
|
|
137
|
+
});
|
|
81
138
|
|
|
82
|
-
|
|
139
|
+
describe('overflow behavior', () => {
|
|
140
|
+
it('should handle overflow with small container width', () => {
|
|
141
|
+
const tags = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5'];
|
|
83
142
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
143
|
+
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
|
|
144
|
+
configurable: true,
|
|
145
|
+
writable: true,
|
|
146
|
+
value: 100,
|
|
147
|
+
});
|
|
87
148
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
149
|
+
render(<TagsRendererWrapper value={tags} />);
|
|
150
|
+
|
|
151
|
+
const container = screen.getByTestId('tags-container');
|
|
152
|
+
expect(container).toBeInTheDocument();
|
|
91
153
|
|
|
92
|
-
|
|
93
|
-
expect(screen.getByTestId('tags-container')).toBeInTheDocument();
|
|
154
|
+
triggerResizeObserver();
|
|
94
155
|
|
|
95
|
-
|
|
96
|
-
|
|
156
|
+
expect(mockScheduleIdleCallback).toHaveBeenCalled();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should display overflow indicator when not all tags fit', async () => {
|
|
160
|
+
const tags = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5'];
|
|
161
|
+
|
|
162
|
+
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
|
|
163
|
+
configurable: true,
|
|
164
|
+
writable: true,
|
|
165
|
+
value: 150,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
render(<TagsRendererWrapper value={tags} />);
|
|
169
|
+
|
|
170
|
+
triggerResizeObserver();
|
|
171
|
+
|
|
172
|
+
// The overflow indicator may or may not appear depending on calculations
|
|
173
|
+
// This test verifies the component handles the scenario without errors
|
|
174
|
+
const container = screen.getByTestId('tags-container');
|
|
175
|
+
expect(container).toBeInTheDocument();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should handle empty tag list gracefully', () => {
|
|
179
|
+
render(<TagsRendererWrapper value={[]} />);
|
|
180
|
+
|
|
181
|
+
const container = screen.getByTestId('tags-container');
|
|
182
|
+
expect(container).toBeInTheDocument();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should handle non-array values gracefully', () => {
|
|
186
|
+
render(<TagsRendererWrapper value={null} />);
|
|
187
|
+
|
|
188
|
+
const container = screen.getByTestId('tags-container');
|
|
189
|
+
expect(container).toBeInTheDocument();
|
|
190
|
+
});
|
|
97
191
|
});
|
|
98
192
|
|
|
99
|
-
|
|
100
|
-
|
|
193
|
+
describe('ResizeObserver integration', () => {
|
|
194
|
+
it('should observe container on mount', () => {
|
|
195
|
+
render(<TagsRendererWrapper value={['tag1', 'tag2']} />);
|
|
101
196
|
|
|
102
|
-
|
|
197
|
+
expect(mockResizeObserver.observe).toHaveBeenCalledTimes(1);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should trigger calculations when ResizeObserver fires', () => {
|
|
201
|
+
render(<TagsRendererWrapper value={['tag1', 'tag2', 'tag3']} />);
|
|
202
|
+
|
|
203
|
+
expect(mockScheduleIdleCallback).not.toHaveBeenCalled();
|
|
204
|
+
|
|
205
|
+
triggerResizeObserver();
|
|
103
206
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
expect(container).toBeInTheDocument();
|
|
207
|
+
expect(mockScheduleIdleCallback).toHaveBeenCalled();
|
|
208
|
+
});
|
|
107
209
|
});
|
|
108
210
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
disconnect: mockDisconnect,
|
|
115
|
-
}));
|
|
211
|
+
describe('cleanup', () => {
|
|
212
|
+
it('should disconnect ResizeObserver on unmount', () => {
|
|
213
|
+
const { unmount } = render(
|
|
214
|
+
<TagsRendererWrapper value={['tag1', 'tag2']} />,
|
|
215
|
+
);
|
|
116
216
|
|
|
117
|
-
|
|
118
|
-
<RendererWrapper {...defaultProps} value={['tag1', 'tag2']} />,
|
|
119
|
-
);
|
|
217
|
+
expect(mockResizeObserver.disconnect).not.toHaveBeenCalled();
|
|
120
218
|
|
|
121
|
-
|
|
219
|
+
unmount();
|
|
122
220
|
|
|
123
|
-
|
|
221
|
+
expect(mockResizeObserver.disconnect).toHaveBeenCalledTimes(1);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should cancel idle callback on unmount if pending', () => {
|
|
225
|
+
const { unmount } = render(
|
|
226
|
+
<TagsRendererWrapper value={['tag1', 'tag2']} />,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
triggerResizeObserver();
|
|
230
|
+
|
|
231
|
+
unmount();
|
|
232
|
+
|
|
233
|
+
expect(mockCancelScheduledCallback).toHaveBeenCalled();
|
|
234
|
+
});
|
|
124
235
|
});
|
|
125
236
|
});
|
|
@@ -1,17 +1,45 @@
|
|
|
1
|
-
import React, { useEffect, useRef, useState } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
cancelScheduledCallback,
|
|
4
|
+
scheduleIdleCallback,
|
|
5
|
+
type IdleCallbackHandle,
|
|
6
|
+
} from '../../../../../helpers';
|
|
2
7
|
import classes from './TagsRenderer.scss';
|
|
3
8
|
|
|
4
9
|
export const TagsRenderer = (val: unknown): JSX.Element => {
|
|
5
10
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
6
|
-
const
|
|
7
|
-
const [visibleCount, setVisibleCount] = useState<number>(0);
|
|
11
|
+
const [visibleCount, setVisibleCount] = useState<number | null>(null);
|
|
8
12
|
|
|
9
13
|
const prevValRef = useRef<string[]>([]);
|
|
10
14
|
const valRef = useRef(val);
|
|
11
15
|
valRef.current = val; // Update on every render
|
|
12
16
|
|
|
17
|
+
// Estimate tag width based on text content without DOM manipulation
|
|
18
|
+
const estimateTagWidth = useCallback((text: string): number => {
|
|
19
|
+
const container = containerRef.current;
|
|
20
|
+
if (!container) {
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Get computed font size from the container
|
|
25
|
+
const computedStyle = window.getComputedStyle(container);
|
|
26
|
+
const fontSize = parseFloat(computedStyle.fontSize) || 14;
|
|
27
|
+
|
|
28
|
+
// Average character width is approximately 0.6 of font size for typical fonts
|
|
29
|
+
// This is a reasonable approximation for most monospace and sans-serif fonts
|
|
30
|
+
const avgCharWidth = fontSize * 0.55;
|
|
31
|
+
|
|
32
|
+
// Tag padding is 5px on each side (10px total) from SCSS
|
|
33
|
+
const padding = 10;
|
|
34
|
+
|
|
35
|
+
// Estimate text width
|
|
36
|
+
const textWidth = text.length * avgCharWidth;
|
|
37
|
+
|
|
38
|
+
return textWidth + padding;
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
13
41
|
// Shared calculation function that uses valRef for current value
|
|
14
|
-
const calculateVisibleItems = (): void => {
|
|
42
|
+
const calculateVisibleItems = useCallback((): void => {
|
|
15
43
|
const currentVal = valRef.current;
|
|
16
44
|
const container = containerRef.current;
|
|
17
45
|
|
|
@@ -23,26 +51,10 @@ export const TagsRenderer = (val: unknown): JSX.Element => {
|
|
|
23
51
|
const gap = 5; // Gap between items from SCSS
|
|
24
52
|
const overflowIndicatorWidth = 60; // Approximate width for "... +X"
|
|
25
53
|
|
|
26
|
-
//
|
|
27
|
-
const tagWidths: number[] = []
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
(currentVal as string[]).forEach((item) => {
|
|
32
|
-
const tempTag = document.createElement('div');
|
|
33
|
-
tempTag.className = classes.tag;
|
|
34
|
-
tempTag.textContent = item;
|
|
35
|
-
tempTag.style.position = 'absolute';
|
|
36
|
-
tempTag.style.visibility = 'hidden';
|
|
37
|
-
tempTag.style.pointerEvents = 'none';
|
|
38
|
-
container.appendChild(tempTag);
|
|
39
|
-
tempElements.push(tempTag);
|
|
40
|
-
tagWidths.push(tempTag.offsetWidth);
|
|
41
|
-
});
|
|
42
|
-
} finally {
|
|
43
|
-
// Clean up temporary elements
|
|
44
|
-
tempElements.forEach((el) => container.removeChild(el));
|
|
45
|
-
}
|
|
54
|
+
// Estimate tag widths based on text content
|
|
55
|
+
const tagWidths: number[] = (currentVal as string[]).map((item) =>
|
|
56
|
+
estimateTagWidth(item),
|
|
57
|
+
);
|
|
46
58
|
|
|
47
59
|
// Calculate how many items can fit
|
|
48
60
|
let totalWidth = 0;
|
|
@@ -92,7 +104,7 @@ export const TagsRenderer = (val: unknown): JSX.Element => {
|
|
|
92
104
|
|
|
93
105
|
// Update state with the new visible count
|
|
94
106
|
setVisibleCount(newVisibleCount);
|
|
95
|
-
};
|
|
107
|
+
}, [estimateTagWidth]);
|
|
96
108
|
|
|
97
109
|
// Effect 1: Handle tag changes (runs when val changes)
|
|
98
110
|
useEffect(() => {
|
|
@@ -112,35 +124,52 @@ export const TagsRenderer = (val: unknown): JSX.Element => {
|
|
|
112
124
|
calculateVisibleItems();
|
|
113
125
|
prevValRef.current = [...valArray];
|
|
114
126
|
}
|
|
115
|
-
}, [val]);
|
|
127
|
+
}, [val, calculateVisibleItems]);
|
|
128
|
+
|
|
129
|
+
// Ref to store the idle callback ID
|
|
130
|
+
const idleCallbackRef = useRef<IdleCallbackHandle | null>(null);
|
|
116
131
|
|
|
117
|
-
// Effect 2: Set up ResizeObserver (runs once on mount)
|
|
132
|
+
// // Effect 2: Set up ResizeObserver (runs once on mount)
|
|
118
133
|
useEffect(() => {
|
|
119
134
|
if (!containerRef.current) {
|
|
120
135
|
return;
|
|
121
136
|
}
|
|
122
137
|
|
|
123
138
|
const debouncedCalculate = (): void => {
|
|
124
|
-
if (
|
|
125
|
-
|
|
139
|
+
if (idleCallbackRef.current) {
|
|
140
|
+
cancelScheduledCallback(idleCallbackRef.current);
|
|
126
141
|
}
|
|
127
|
-
|
|
142
|
+
idleCallbackRef.current = scheduleIdleCallback(() => {
|
|
128
143
|
calculateVisibleItems();
|
|
129
|
-
}
|
|
144
|
+
});
|
|
130
145
|
};
|
|
131
146
|
|
|
147
|
+
// const resizeObserver = new ResizeObserver(calculateVisibleItems);
|
|
132
148
|
const resizeObserver = new ResizeObserver(debouncedCalculate);
|
|
133
149
|
resizeObserver.observe(containerRef.current);
|
|
134
150
|
|
|
135
151
|
return () => {
|
|
136
|
-
if (
|
|
137
|
-
|
|
152
|
+
if (idleCallbackRef.current) {
|
|
153
|
+
cancelScheduledCallback(idleCallbackRef.current);
|
|
138
154
|
}
|
|
139
155
|
resizeObserver.disconnect();
|
|
140
156
|
};
|
|
141
|
-
}, []);
|
|
157
|
+
}, [calculateVisibleItems]);
|
|
142
158
|
|
|
143
159
|
const stringArray = Array.isArray(val) ? (val as string[]) : [];
|
|
160
|
+
|
|
161
|
+
// Don't render items until we've calculated the visible count
|
|
162
|
+
if (visibleCount === null) {
|
|
163
|
+
return (
|
|
164
|
+
<div
|
|
165
|
+
ref={containerRef}
|
|
166
|
+
className={classes.container}
|
|
167
|
+
data-testid="tags-container"
|
|
168
|
+
title={stringArray.join(', ')}
|
|
169
|
+
/>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
144
173
|
const visibleItems = stringArray.slice(0, visibleCount);
|
|
145
174
|
const hiddenItems = stringArray.slice(visibleCount);
|
|
146
175
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
|
-
import { shallow } from 'enzyme';
|
|
2
|
+
import { mount, shallow } from 'enzyme';
|
|
3
3
|
import React, { ReactNode, SyntheticEvent } from 'react';
|
|
4
4
|
import ContentLoader from 'react-content-loader';
|
|
5
5
|
import { act } from 'react-dom/test-utils';
|
|
@@ -18,7 +18,7 @@ describe('ImageLoader', () => {
|
|
|
18
18
|
const mockWidth = 'test-width';
|
|
19
19
|
const mockAlt = 'test-alt';
|
|
20
20
|
|
|
21
|
-
const wrapper =
|
|
21
|
+
const wrapper = mount(
|
|
22
22
|
<ImageLoader
|
|
23
23
|
imgSrc={mockSrc}
|
|
24
24
|
alt={mockAlt}
|
|
@@ -39,7 +39,7 @@ describe('ImageLoader', () => {
|
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
it('renders the loading animation while an image is loading', () => {
|
|
42
|
-
const wrapper =
|
|
42
|
+
const wrapper = mount(<ImageLoader imgSrc="" />);
|
|
43
43
|
|
|
44
44
|
const loader = wrapper.find(ContentLoader);
|
|
45
45
|
const imgDisplay = wrapper.find('img').prop('style')?.display;
|
|
@@ -49,9 +49,10 @@ describe('ImageLoader', () => {
|
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
it('renders the image after loading has completed', () => {
|
|
52
|
-
const wrapper =
|
|
52
|
+
const wrapper = mount(<ImageLoader imgSrc="" />);
|
|
53
53
|
|
|
54
54
|
wrapper.find('img').prop('onLoad')!({} as SyntheticEvent);
|
|
55
|
+
wrapper.update();
|
|
55
56
|
|
|
56
57
|
const loader = wrapper.find(ContentLoader);
|
|
57
58
|
const imgDisplay = wrapper.find('img').prop('style')?.display;
|
|
@@ -63,7 +64,7 @@ describe('ImageLoader', () => {
|
|
|
63
64
|
it('emits onLoad callback with img src after successful load', () => {
|
|
64
65
|
const spy = jest.fn();
|
|
65
66
|
const mockUrl = 'mock-url';
|
|
66
|
-
const wrapper =
|
|
67
|
+
const wrapper = mount(<ImageLoader imgSrc={mockUrl} onLoad={spy} />);
|
|
67
68
|
|
|
68
69
|
wrapper.find('img').prop('onLoad')!({} as SyntheticEvent);
|
|
69
70
|
|
|
@@ -77,9 +78,7 @@ describe('ImageLoader', () => {
|
|
|
77
78
|
};
|
|
78
79
|
const spy = jest.fn();
|
|
79
80
|
const mockUrl = 'mock-url';
|
|
80
|
-
const wrapper =
|
|
81
|
-
<ImageLoader imgSrc={mockUrl} onImageClick={spy} />,
|
|
82
|
-
);
|
|
81
|
+
const wrapper = mount(<ImageLoader imgSrc={mockUrl} onImageClick={spy} />);
|
|
83
82
|
|
|
84
83
|
const image = wrapper.find('img');
|
|
85
84
|
|
|
@@ -93,9 +92,10 @@ describe('ImageLoader', () => {
|
|
|
93
92
|
});
|
|
94
93
|
|
|
95
94
|
it('renders the fallback background color when loading has failed', () => {
|
|
96
|
-
const wrapper =
|
|
95
|
+
const wrapper = mount(<ImageLoader imgSrc="" />);
|
|
97
96
|
|
|
98
97
|
wrapper.find('img').prop('onError')!({} as SyntheticEvent);
|
|
98
|
+
wrapper.update();
|
|
99
99
|
|
|
100
100
|
const loader = wrapper.find(ContentLoader);
|
|
101
101
|
const fallbackContainer = wrapper.find('.container').at(1);
|
|
@@ -107,11 +107,12 @@ describe('ImageLoader', () => {
|
|
|
107
107
|
|
|
108
108
|
it('renders a fallback image when loading has failed', () => {
|
|
109
109
|
const mockFallbackUrl = 'mock-url';
|
|
110
|
-
const wrapper =
|
|
110
|
+
const wrapper = mount(
|
|
111
111
|
<ImageLoader imgSrc="" fallbackSrc={mockFallbackUrl} />,
|
|
112
112
|
);
|
|
113
113
|
|
|
114
114
|
wrapper.find('img').prop('onError')!({} as SyntheticEvent);
|
|
115
|
+
wrapper.update();
|
|
115
116
|
|
|
116
117
|
const loader = wrapper.find(ContentLoader);
|
|
117
118
|
const images = wrapper.find('img');
|
|
@@ -127,7 +128,7 @@ describe('ImageLoader', () => {
|
|
|
127
128
|
it('emits onError callback with img src after load failure', () => {
|
|
128
129
|
const spy = jest.fn();
|
|
129
130
|
const mockUrl = 'mock-url';
|
|
130
|
-
const wrapper =
|
|
131
|
+
const wrapper = mount(<ImageLoader imgSrc={mockUrl} onError={spy} />);
|
|
131
132
|
|
|
132
133
|
wrapper.find('img').prop('onError')!({} as SyntheticEvent);
|
|
133
134
|
|
|
@@ -142,9 +143,7 @@ describe('ImageLoader', () => {
|
|
|
142
143
|
<circle r="50" />
|
|
143
144
|
</svg>
|
|
144
145
|
);
|
|
145
|
-
const wrapper =
|
|
146
|
-
<ImageLoader imgSrc="" loadingSkeleton={mockSVG} />,
|
|
147
|
-
);
|
|
146
|
+
const wrapper = mount(<ImageLoader imgSrc="" loadingSkeleton={mockSVG} />);
|
|
148
147
|
|
|
149
148
|
const svg = wrapper.find(`#${mockId}`);
|
|
150
149
|
|
|
@@ -2,6 +2,7 @@ import clsx from 'clsx';
|
|
|
2
2
|
import React, { ReactNode, useCallback, useEffect, useState } from 'react';
|
|
3
3
|
import ContentLoader from 'react-content-loader';
|
|
4
4
|
import { noop } from '../../../helpers/utils';
|
|
5
|
+
import { ImgElement } from '../../VisualElements/ImgElement';
|
|
5
6
|
import { SquareOutlineSkeleton } from '../skeletons';
|
|
6
7
|
import classes from './ImageLoader.scss';
|
|
7
8
|
|
|
@@ -140,7 +141,7 @@ export const ImageLoader: React.FC<ImageLoaderProps> = ({
|
|
|
140
141
|
)}
|
|
141
142
|
<div className={classes.imageContainer}>
|
|
142
143
|
{state !== ImageLoaderState.Failed && imgSrc !== undefined && (
|
|
143
|
-
<
|
|
144
|
+
<ImgElement
|
|
144
145
|
src={imgSrc}
|
|
145
146
|
height={imgHeight}
|
|
146
147
|
width={imgWidth}
|
|
@@ -149,7 +150,7 @@ export const ImageLoader: React.FC<ImageLoaderProps> = ({
|
|
|
149
150
|
objectFit: 'contain',
|
|
150
151
|
maxWidth: '100%',
|
|
151
152
|
}}
|
|
152
|
-
alt={alt}
|
|
153
|
+
alt={alt ? alt : 'Loaded content image'}
|
|
153
154
|
onLoad={onLoadHandler}
|
|
154
155
|
onError={onErrorHandler}
|
|
155
156
|
onClick={onImageClick}
|
|
@@ -159,11 +160,12 @@ export const ImageLoader: React.FC<ImageLoaderProps> = ({
|
|
|
159
160
|
)}
|
|
160
161
|
{state === ImageLoaderState.Failed &&
|
|
161
162
|
(fallbackSrc ? (
|
|
162
|
-
<
|
|
163
|
+
<ImgElement
|
|
163
164
|
className={classes.fallBackImage}
|
|
164
165
|
src={String(fallbackSrc)}
|
|
165
166
|
height={imgHeight}
|
|
166
167
|
width={imgWidth}
|
|
168
|
+
alt="Image unavailable"
|
|
167
169
|
/>
|
|
168
170
|
) : (
|
|
169
171
|
<div
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
useConfirmationDelay,
|
|
10
10
|
} from '../../ConfirmDialog';
|
|
11
11
|
import { IconName, Icons } from '../../Icons';
|
|
12
|
+
import { ImgElement } from '../../VisualElements';
|
|
12
13
|
import {
|
|
13
14
|
PageHeaderActionProps,
|
|
14
15
|
PageHeaderActionType,
|
|
@@ -142,7 +143,12 @@ const PageHeaderJSAction: React.FC<PageHeaderJsActionProps> = ({
|
|
|
142
143
|
<div className={classes.icon}>
|
|
143
144
|
{!confirmation &&
|
|
144
145
|
(typeof icon === 'string' ? (
|
|
145
|
-
<
|
|
146
|
+
<ImgElement
|
|
147
|
+
src={icon}
|
|
148
|
+
{...(imgAlt
|
|
149
|
+
? { alt: imgAlt ?? `${label} icon` }
|
|
150
|
+
: { decorative: true })}
|
|
151
|
+
/>
|
|
146
152
|
) : (
|
|
147
153
|
<Icons icon={icon} className={classes.pageHeaderActionsIcons} />
|
|
148
154
|
))}
|
|
@@ -208,7 +214,12 @@ const PageHeaderNavigationAction: React.FC<PageHeaderNavigationActionProps> = ({
|
|
|
208
214
|
>
|
|
209
215
|
<div className={classes.icon}>
|
|
210
216
|
{typeof headerIcon === 'string' ? (
|
|
211
|
-
<
|
|
217
|
+
<ImgElement
|
|
218
|
+
src={headerIcon}
|
|
219
|
+
{...(imgAlt
|
|
220
|
+
? { alt: imgAlt ?? `${label} icon` }
|
|
221
|
+
: { decorative: true })}
|
|
222
|
+
/>
|
|
212
223
|
) : (
|
|
213
224
|
<Icons icon={headerIcon} className={classes.pageHeaderActionsIcons} />
|
|
214
225
|
)}
|