@axinom/mosaic-ui 0.66.0-rc.8 → 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.
Files changed (146) hide show
  1. package/dist/components/DynamicDataList/DynamicListHeader/DynamicListHeader.d.ts.map +1 -1
  2. package/dist/components/FieldSelection/FieldSelection.d.ts.map +1 -1
  3. package/dist/components/Filters/Filter/Filter.d.ts.map +1 -1
  4. package/dist/components/Filters/Filters.model.d.ts +5 -0
  5. package/dist/components/Filters/Filters.model.d.ts.map +1 -1
  6. package/dist/components/Filters/SelectionTypes/DateTimeFilter/DateTimeFilter.d.ts +2 -0
  7. package/dist/components/Filters/SelectionTypes/DateTimeFilter/DateTimeFilter.d.ts.map +1 -1
  8. package/dist/components/Filters/SelectionTypes/FreeTextFilter/FreeTextFilter.d.ts +2 -0
  9. package/dist/components/Filters/SelectionTypes/FreeTextFilter/FreeTextFilter.d.ts.map +1 -1
  10. package/dist/components/Filters/SelectionTypes/MultiOptionFilter/MultiOptionFilter.d.ts +2 -0
  11. package/dist/components/Filters/SelectionTypes/MultiOptionFilter/MultiOptionFilter.d.ts.map +1 -1
  12. package/dist/components/Filters/SelectionTypes/NumericTextFilter/NumericTextFilter.d.ts +2 -0
  13. package/dist/components/Filters/SelectionTypes/NumericTextFilter/NumericTextFilter.d.ts.map +1 -1
  14. package/dist/components/Filters/SelectionTypes/OptionsFilter/OptionsFilter.d.ts +2 -0
  15. package/dist/components/Filters/SelectionTypes/OptionsFilter/OptionsFilter.d.ts.map +1 -1
  16. package/dist/components/Filters/SelectionTypes/SearcheableOptionsFilter/SearcheableOptionsFilter.d.ts +2 -0
  17. package/dist/components/Filters/SelectionTypes/SearcheableOptionsFilter/SearcheableOptionsFilter.d.ts.map +1 -1
  18. package/dist/components/FormElements/Radio/Radio.d.ts.map +1 -1
  19. package/dist/components/FormElements/ToggleButton/ToggleButton.d.ts.map +1 -1
  20. package/dist/components/Hub/Tile/Tile.d.ts.map +1 -1
  21. package/dist/components/Icons/Icons.d.ts +4 -9
  22. package/dist/components/Icons/Icons.d.ts.map +1 -1
  23. package/dist/components/LandingPageTiles/TileLarge/TileLarge.d.ts.map +1 -1
  24. package/dist/components/LandingPageTiles/TileSmall/TileSmall.d.ts.map +1 -1
  25. package/dist/components/List/ListCheckBox/ListCheckBox.d.ts.map +1 -1
  26. package/dist/components/List/ListHeader/ColumnLabel/ColumnLabel.d.ts.map +1 -1
  27. package/dist/components/List/ListHeader/ListHeader.d.ts.map +1 -1
  28. package/dist/components/List/ListRow/ListRow.d.ts.map +1 -1
  29. package/dist/components/List/ListRow/ListRowCell/ListRowCell.d.ts +15 -0
  30. package/dist/components/List/ListRow/ListRowCell/ListRowCell.d.ts.map +1 -0
  31. package/dist/components/List/ListRow/ListRowCell/renderData.d.ts +9 -0
  32. package/dist/components/List/ListRow/ListRowCell/renderData.d.ts.map +1 -0
  33. package/dist/components/List/ListRow/Renderers/TagsRenderer/TagsRenderer.d.ts.map +1 -1
  34. package/dist/components/Loaders/ImageLoader/ImageLoader.d.ts.map +1 -1
  35. package/dist/components/PageHeader/PageHeaderAction/PageHeaderAction.d.ts.map +1 -1
  36. package/dist/components/Utils/Postgraphile/CreateConnectionRenderer.d.ts +1 -1
  37. package/dist/components/Utils/Postgraphile/CreateConnectionRenderer.d.ts.map +1 -1
  38. package/dist/components/VisualElements/ImgElement.d.ts +50 -0
  39. package/dist/components/VisualElements/ImgElement.d.ts.map +1 -0
  40. package/dist/components/VisualElements/SvgElement.d.ts +14 -0
  41. package/dist/components/VisualElements/SvgElement.d.ts.map +1 -0
  42. package/dist/components/VisualElements/index.d.ts +3 -0
  43. package/dist/components/VisualElements/index.d.ts.map +1 -0
  44. package/dist/components/index.d.ts +1 -0
  45. package/dist/components/index.d.ts.map +1 -1
  46. package/dist/helpers/idleCallbackHelpers.d.ts +42 -0
  47. package/dist/helpers/idleCallbackHelpers.d.ts.map +1 -0
  48. package/dist/helpers/index.d.ts +1 -0
  49. package/dist/helpers/index.d.ts.map +1 -1
  50. package/dist/hooks/useResize/ResizeIndicator.d.ts +8 -0
  51. package/dist/hooks/useResize/ResizeIndicator.d.ts.map +1 -0
  52. package/dist/hooks/useResize/useResize.d.ts +5 -2
  53. package/dist/hooks/useResize/useResize.d.ts.map +1 -1
  54. package/dist/index.d.ts +1 -0
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.es.js +4 -4
  57. package/dist/index.es.js.map +1 -1
  58. package/dist/index.js +4 -4
  59. package/dist/index.js.map +1 -1
  60. package/package.json +2 -2
  61. package/src/components/Accordion/Accordion.scss +1 -1
  62. package/src/components/Accordion/AccordionItem/AccordionItem.scss +2 -2
  63. package/src/components/Buttons/Button/Button.scss +5 -5
  64. package/src/components/Buttons/CompositeButton/CompositeButton.scss +7 -7
  65. package/src/components/Buttons/TextButton/TextButton.scss +6 -6
  66. package/src/components/ConfirmDialog/ConfirmDialog.scss +1 -1
  67. package/src/components/DateTime/DatePicker/DatePicker.scss +12 -12
  68. package/src/components/DateTime/TimePicker/ScrollColumn/ScrollColumn.scss +7 -7
  69. package/src/components/DateTime/TimePicker/TimePicker.scss +1 -1
  70. package/src/components/DynamicDataList/DynamicListHeader/DynamicListHeader.scss +2 -2
  71. package/src/components/DynamicDataList/DynamicListHeader/DynamicListHeader.spec.tsx +2 -0
  72. package/src/components/DynamicDataList/DynamicListHeader/DynamicListHeader.tsx +62 -50
  73. package/src/components/DynamicDataList/DynamicListRow/DynamicListRow.scss +2 -2
  74. package/src/components/FieldSelection/FieldSelection.scss +4 -0
  75. package/src/components/FieldSelection/FieldSelection.tsx +1 -0
  76. package/src/components/Filters/Filter/Filter.scss +34 -15
  77. package/src/components/Filters/Filter/Filter.spec.tsx +1 -1
  78. package/src/components/Filters/Filter/Filter.tsx +46 -34
  79. package/src/components/Filters/Filters.model.ts +6 -0
  80. package/src/components/Filters/SelectionTypes/DateTimeFilter/DateTimeFilter.tsx +5 -0
  81. package/src/components/Filters/SelectionTypes/FreeTextFilter/FreeTextFilter.tsx +4 -0
  82. package/src/components/Filters/SelectionTypes/MultiOptionFilter/MultiOptionFilter.scss +1 -1
  83. package/src/components/Filters/SelectionTypes/MultiOptionFilter/MultiOptionFilter.tsx +9 -1
  84. package/src/components/Filters/SelectionTypes/NumericTextFilter/NumericTextFilter.tsx +5 -0
  85. package/src/components/Filters/SelectionTypes/OptionsFilter/OptionsFilter.scss +6 -10
  86. package/src/components/Filters/SelectionTypes/OptionsFilter/OptionsFilter.tsx +8 -0
  87. package/src/components/Filters/SelectionTypes/SearcheableOptionsFilter/SearcheableOptionsFilter.tsx +6 -1
  88. package/src/components/FormElements/Checkbox/Checkbox.scss +4 -4
  89. package/src/components/FormElements/CustomTags/CustomTags.scss +1 -1
  90. package/src/components/FormElements/FileUploadControl/FileUploadControl.scss +4 -4
  91. package/src/components/FormElements/Radio/Radio.scss +5 -5
  92. package/src/components/FormElements/Radio/Radio.tsx +3 -2
  93. package/src/components/FormElements/Select/Select.scss +11 -6
  94. package/src/components/FormElements/ToggleButton/ToggleButton.scss +7 -7
  95. package/src/components/FormElements/ToggleButton/ToggleButton.tsx +32 -27
  96. package/src/components/Hub/Hub.stories.tsx +3 -2
  97. package/src/components/Hub/Tile/Tile.spec.tsx +7 -2
  98. package/src/components/Hub/Tile/Tile.tsx +2 -1
  99. package/src/components/Icons/Icons.scss +1 -0
  100. package/src/components/Icons/Icons.spec.tsx +90 -41
  101. package/src/components/Icons/Icons.tsx +357 -765
  102. package/src/components/InfoTooltip/InfoTooltip.scss +1 -1
  103. package/src/components/InlineMenu/InlineMenu.scss +2 -2
  104. package/src/components/LandingPageTiles/LandingPageTiles.stories.tsx +3 -2
  105. package/src/components/LandingPageTiles/TileLarge/TileLarge.spec.tsx +5 -1
  106. package/src/components/LandingPageTiles/TileLarge/TileLarge.tsx +2 -1
  107. package/src/components/LandingPageTiles/TileSmall/TileSmall.spec.tsx +7 -2
  108. package/src/components/LandingPageTiles/TileSmall/TileSmall.tsx +2 -1
  109. package/src/components/List/List.scss +4 -4
  110. package/src/components/List/ListCheckBox/ListCheckBox.tsx +1 -0
  111. package/src/components/List/ListHeader/ColumnLabel/ColumnLabel.spec.tsx +6 -6
  112. package/src/components/List/ListHeader/ColumnLabel/ColumnLabel.tsx +10 -13
  113. package/src/components/List/ListHeader/ListHeader.scss +2 -3
  114. package/src/components/List/ListHeader/ListHeader.spec.tsx +2 -0
  115. package/src/components/List/ListHeader/ListHeader.tsx +57 -51
  116. package/src/components/List/ListRow/ListRow.scss +1 -28
  117. package/src/components/List/ListRow/ListRow.spec.tsx +10 -8
  118. package/src/components/List/ListRow/ListRow.tsx +20 -152
  119. package/src/components/List/ListRow/ListRowCell/ListRowCell.scss +26 -0
  120. package/src/components/List/ListRow/ListRowCell/ListRowCell.spec.tsx +491 -0
  121. package/src/components/List/ListRow/ListRowCell/ListRowCell.tsx +57 -0
  122. package/src/components/List/ListRow/ListRowCell/renderData.tsx +124 -0
  123. package/src/components/List/ListRow/Renderers/TagsRenderer/TagsRenderer.spec.tsx +191 -80
  124. package/src/components/List/ListRow/Renderers/TagsRenderer/TagsRenderer.tsx +63 -34
  125. package/src/components/Loaders/ImageLoader/ImageLoader.spec.tsx +13 -14
  126. package/src/components/Loaders/ImageLoader/ImageLoader.tsx +5 -3
  127. package/src/components/PageHeader/PageHeader.scss +1 -1
  128. package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.tsx +13 -2
  129. package/src/components/Tabs/Tab/CustomTab.scss +4 -4
  130. package/src/components/Tabs/TabList/CustomTabList.scss +2 -2
  131. package/src/components/Utils/Postgraphile/CreateConnectionRenderer.spec.ts +48 -12
  132. package/src/components/Utils/Postgraphile/CreateConnectionRenderer.tsx +5 -4
  133. package/src/components/VisualElements/ImgElement.spec.tsx +92 -0
  134. package/src/components/VisualElements/ImgElement.tsx +72 -0
  135. package/src/components/VisualElements/SvgElement.spec.tsx +160 -0
  136. package/src/components/VisualElements/SvgElement.tsx +40 -0
  137. package/src/components/VisualElements/index.ts +7 -0
  138. package/src/components/index.ts +1 -0
  139. package/src/helpers/idleCallbackHelpers.ts +66 -0
  140. package/src/helpers/index.ts +5 -0
  141. package/src/hooks/useResize/ResizeIndicator.scss +7 -0
  142. package/src/hooks/useResize/ResizeIndicator.tsx +39 -0
  143. package/src/hooks/useResize/{useResize.ts → useResize.tsx} +38 -6
  144. package/src/index.ts +2 -0
  145. package/src/styles/branding.scss +89 -0
  146. 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
- // Mock ResizeObserver
7
- global.ResizeObserver = jest.fn().mockImplementation(() => ({
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 RendererWrapper: React.FC<RendererWrapperProps> = ({ value }) => {
13
+ const TagsRendererWrapper: React.FC<TagsRendererWrapperProps> = ({ value }) => {
19
14
  return <>{TagsRenderer(value)}</>;
20
15
  };
21
16
 
22
- const defaultProps: RendererWrapperProps = {
23
- value: [],
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
- // Reset mocks
29
- jest.clearAllMocks();
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
- it('renders the component without crashing', () => {
33
- const { container } = render(<RendererWrapper value={['test']} />);
34
- expect(container.firstChild).toBeInTheDocument();
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
- it('renders tags when array is provided', () => {
38
- const mockTags = ['tag1', 'tag2', 'tag3'];
88
+ describe('rendering', () => {
89
+ it('should render empty container when value is empty array', () => {
90
+ render(<TagsRendererWrapper value={[]} />);
39
91
 
40
- // Mock container width measurement
41
- Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
42
- configurable: true,
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(<RendererWrapper {...defaultProps} value={mockTags} />);
97
+ it('should render container with title attribute containing all tags', () => {
98
+ const tags = ['tag1', 'tag2', 'tag3'];
47
99
 
48
- // Since we're mocking ResizeObserver, simulate the callback
49
- const resizeObserverCallback = (global.ResizeObserver as jest.Mock).mock
50
- .calls[0]?.[0];
100
+ render(<TagsRendererWrapper value={tags} />);
51
101
 
52
- if (resizeObserverCallback) {
53
- resizeObserverCallback([]);
54
- }
102
+ const container = screen.getByTestId('tags-container');
103
+ expect(container).toHaveAttribute('title', 'tag1, tag2, tag3');
104
+ });
55
105
 
56
- // Check that container exists
57
- const container = screen.getByTestId('tags-container');
58
- expect(container).toBeInTheDocument();
59
- });
106
+ it('should render tags when array is provided', () => {
107
+ const tags = ['tag1', 'tag2', 'tag3'];
60
108
 
61
- it('displays overflow indicator when configured', () => {
62
- const mockTags = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5'];
109
+ render(<TagsRendererWrapper value={tags} />);
63
110
 
64
- // Mock container and tag measurements to force overflow
65
- Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
66
- configurable: true,
67
- value: 100, // Small container width to force overflow
111
+ const container = screen.getByTestId('tags-container');
112
+ expect(container).toBeInTheDocument();
113
+
114
+ triggerResizeObserver();
115
+
116
+ expect(mockScheduleIdleCallback).toHaveBeenCalled();
68
117
  });
69
118
 
70
- // Mock document.createElement to control tag width measurements
71
- const originalCreateElement = document.createElement;
72
- document.createElement = jest.fn().mockImplementation((tagName) => {
73
- const element = originalCreateElement.call(document, tagName);
74
- if (tagName === 'div') {
75
- Object.defineProperty(element, 'offsetWidth', {
76
- value: 50, // Each tag is 50px wide
77
- });
78
- }
79
- return element;
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
- render(<RendererWrapper {...defaultProps} value={mockTags} />);
139
+ describe('overflow behavior', () => {
140
+ it('should handle overflow with small container width', () => {
141
+ const tags = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5'];
83
142
 
84
- // Simulate ResizeObserver callback
85
- const resizeObserverCallback = (global.ResizeObserver as jest.Mock).mock
86
- .calls[0]?.[0];
143
+ Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
144
+ configurable: true,
145
+ writable: true,
146
+ value: 100,
147
+ });
87
148
 
88
- if (resizeObserverCallback) {
89
- resizeObserverCallback([]);
90
- }
149
+ render(<TagsRendererWrapper value={tags} />);
150
+
151
+ const container = screen.getByTestId('tags-container');
152
+ expect(container).toBeInTheDocument();
91
153
 
92
- // Test passes if no errors occur - the overflow logic depends on real DOM measurements
93
- expect(screen.getByTestId('tags-container')).toBeInTheDocument();
154
+ triggerResizeObserver();
94
155
 
95
- // Restore
96
- document.createElement = originalCreateElement;
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
- it('handles overflow behavior', () => {
100
- const mockTags = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5'];
193
+ describe('ResizeObserver integration', () => {
194
+ it('should observe container on mount', () => {
195
+ render(<TagsRendererWrapper value={['tag1', 'tag2']} />);
101
196
 
102
- render(<RendererWrapper {...defaultProps} value={mockTags} />);
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
- // Test that the component renders without errors
105
- const container = screen.getByTestId('tags-container');
106
- expect(container).toBeInTheDocument();
207
+ expect(mockScheduleIdleCallback).toHaveBeenCalled();
208
+ });
107
209
  });
108
210
 
109
- it('cleans up ResizeObserver on unmount', () => {
110
- const mockDisconnect = jest.fn();
111
- (global.ResizeObserver as jest.Mock).mockImplementation(() => ({
112
- observe: jest.fn(),
113
- unobserve: jest.fn(),
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
- const { unmount } = render(
118
- <RendererWrapper {...defaultProps} value={['tag1', 'tag2']} />,
119
- );
217
+ expect(mockResizeObserver.disconnect).not.toHaveBeenCalled();
120
218
 
121
- unmount();
219
+ unmount();
122
220
 
123
- expect(mockDisconnect).toHaveBeenCalled();
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 resizeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
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
- // Measure actual tag widths using hidden temporary elements in the actual container
27
- const tagWidths: number[] = [];
28
- const tempElements: HTMLDivElement[] = [];
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 (resizeTimeoutRef.current) {
125
- clearTimeout(resizeTimeoutRef.current);
139
+ if (idleCallbackRef.current) {
140
+ cancelScheduledCallback(idleCallbackRef.current);
126
141
  }
127
- resizeTimeoutRef.current = setTimeout(() => {
142
+ idleCallbackRef.current = scheduleIdleCallback(() => {
128
143
  calculateVisibleItems();
129
- }, 100);
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 (resizeTimeoutRef.current) {
137
- clearTimeout(resizeTimeoutRef.current);
152
+ if (idleCallbackRef.current) {
153
+ cancelScheduledCallback(idleCallbackRef.current);
138
154
  }
139
155
  resizeObserver.disconnect();
140
156
  };
141
- }, []); // Empty deps - only runs once
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 = shallow(
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 = shallow(<ImageLoader imgSrc="" />);
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 = shallow(<ImageLoader imgSrc="" />);
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 = shallow(<ImageLoader imgSrc={mockUrl} onLoad={spy} />);
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 = shallow(
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 = shallow(<ImageLoader imgSrc="" />);
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 = shallow(
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 = shallow(<ImageLoader imgSrc={mockUrl} onError={spy} />);
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 = shallow(
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
- <img
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
- <img
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
@@ -38,7 +38,7 @@
38
38
 
39
39
  margin-top: 12px;
40
40
 
41
- color: $gray;
41
+ color: var(--ax-neutral);
42
42
  font-size: 16px;
43
43
  }
44
44
 
@@ -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
- <img src={icon} alt={imgAlt} />
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
- <img src={headerIcon} alt={imgAlt} />
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
  )}