@axinom/mosaic-ui 0.66.0-rc.2 → 0.66.0-rc.20
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/Explorer/BulkEdit/FormFieldsConfigConverter.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/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.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/DynamicDataList/DynamicListHeader/DynamicListHeader.spec.tsx +2 -0
- package/src/components/DynamicDataList/DynamicListHeader/DynamicListHeader.tsx +62 -50
- package/src/components/Explorer/BulkEdit/FormFieldsConfigConverter.tsx +5 -21
- package/src/components/Explorer/Explorer.stories.tsx +17 -0
- 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 +6 -1
- package/src/components/Filters/SelectionTypes/FreeTextFilter/FreeTextFilter.tsx +4 -0
- 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/Radio/Radio.tsx +3 -2
- package/src/components/FormElements/Select/Select.scss +11 -6
- 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 +1 -1
- 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/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 +0 -1
- 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 +0 -27
- 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.scss +2 -1
- package/src/components/List/ListRow/Renderers/TagsRenderer/TagsRenderer.spec.tsx +187 -104
- package/src/components/List/ListRow/Renderers/TagsRenderer/TagsRenderer.tsx +134 -80
- package/src/components/Loaders/ImageLoader/ImageLoader.spec.tsx +13 -14
- package/src/components/Loaders/ImageLoader/ImageLoader.tsx +5 -3
- package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.tsx +13 -2
- package/src/components/Utils/Postgraphile/CreateConnectionRenderer.spec.ts +22 -75
- 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/styles/variables.scss +7 -6
|
@@ -1,153 +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
|
-
|
|
30
|
-
|
|
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
|
+
});
|
|
31
69
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
70
|
+
// Mock offsetWidth for container
|
|
71
|
+
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
|
|
72
|
+
configurable: true,
|
|
73
|
+
writable: true,
|
|
74
|
+
value: 300,
|
|
75
|
+
});
|
|
35
76
|
});
|
|
36
77
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
);
|
|
41
|
-
|
|
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();
|
|
42
86
|
});
|
|
43
87
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
<
|
|
47
|
-
);
|
|
48
|
-
expect(container.firstChild).toBeNull();
|
|
49
|
-
});
|
|
88
|
+
describe('rendering', () => {
|
|
89
|
+
it('should render empty container when value is empty array', () => {
|
|
90
|
+
render(<TagsRendererWrapper value={[]} />);
|
|
50
91
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
);
|
|
55
|
-
expect(container.firstChild).toBeNull();
|
|
56
|
-
});
|
|
92
|
+
const container = screen.getByTestId('tags-container');
|
|
93
|
+
expect(container).toBeInTheDocument();
|
|
94
|
+
expect(container).toBeEmptyDOMElement();
|
|
95
|
+
});
|
|
57
96
|
|
|
58
|
-
|
|
59
|
-
|
|
97
|
+
it('should render container with title attribute containing all tags', () => {
|
|
98
|
+
const tags = ['tag1', 'tag2', 'tag3'];
|
|
60
99
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
100
|
+
render(<TagsRendererWrapper value={tags} />);
|
|
101
|
+
|
|
102
|
+
const container = screen.getByTestId('tags-container');
|
|
103
|
+
expect(container).toHaveAttribute('title', 'tag1, tag2, tag3');
|
|
65
104
|
});
|
|
66
105
|
|
|
67
|
-
render(
|
|
106
|
+
it('should render tags when array is provided', () => {
|
|
107
|
+
const tags = ['tag1', 'tag2', 'tag3'];
|
|
68
108
|
|
|
69
|
-
|
|
70
|
-
const resizeObserverCallback = (global.ResizeObserver as jest.Mock).mock
|
|
71
|
-
.calls[0]?.[0];
|
|
109
|
+
render(<TagsRendererWrapper value={tags} />);
|
|
72
110
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
111
|
+
const container = screen.getByTestId('tags-container');
|
|
112
|
+
expect(container).toBeInTheDocument();
|
|
76
113
|
|
|
77
|
-
|
|
78
|
-
const container = screen.getByTestId('tags-container');
|
|
79
|
-
expect(container).toBeInTheDocument();
|
|
80
|
-
});
|
|
114
|
+
triggerResizeObserver();
|
|
81
115
|
|
|
82
|
-
|
|
83
|
-
|
|
116
|
+
expect(mockScheduleIdleCallback).toHaveBeenCalled();
|
|
117
|
+
});
|
|
84
118
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
+
});
|
|
89
136
|
});
|
|
137
|
+
});
|
|
90
138
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
139
|
+
describe('overflow behavior', () => {
|
|
140
|
+
it('should handle overflow with small container width', () => {
|
|
141
|
+
const tags = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5'];
|
|
142
|
+
|
|
143
|
+
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
|
|
144
|
+
configurable: true,
|
|
145
|
+
writable: true,
|
|
146
|
+
value: 100,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
render(<TagsRendererWrapper value={tags} />);
|
|
150
|
+
|
|
151
|
+
const container = screen.getByTestId('tags-container');
|
|
152
|
+
expect(container).toBeInTheDocument();
|
|
153
|
+
|
|
154
|
+
triggerResizeObserver();
|
|
155
|
+
|
|
156
|
+
expect(mockScheduleIdleCallback).toHaveBeenCalled();
|
|
101
157
|
});
|
|
102
158
|
|
|
103
|
-
|
|
159
|
+
it('should display overflow indicator when not all tags fit', async () => {
|
|
160
|
+
const tags = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5'];
|
|
104
161
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
162
|
+
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
|
|
163
|
+
configurable: true,
|
|
164
|
+
writable: true,
|
|
165
|
+
value: 150,
|
|
166
|
+
});
|
|
108
167
|
|
|
109
|
-
|
|
110
|
-
resizeObserverCallback([]);
|
|
111
|
-
}
|
|
168
|
+
render(<TagsRendererWrapper value={tags} />);
|
|
112
169
|
|
|
113
|
-
|
|
114
|
-
expect(screen.getByTestId('tags-container')).toBeInTheDocument();
|
|
170
|
+
triggerResizeObserver();
|
|
115
171
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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={[]} />);
|
|
119
180
|
|
|
120
|
-
|
|
121
|
-
|
|
181
|
+
const container = screen.getByTestId('tags-container');
|
|
182
|
+
expect(container).toBeInTheDocument();
|
|
183
|
+
});
|
|
122
184
|
|
|
123
|
-
|
|
185
|
+
it('should handle non-array values gracefully', () => {
|
|
186
|
+
render(<TagsRendererWrapper value={null} />);
|
|
124
187
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
188
|
+
const container = screen.getByTestId('tags-container');
|
|
189
|
+
expect(container).toBeInTheDocument();
|
|
190
|
+
});
|
|
128
191
|
});
|
|
129
192
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
<
|
|
133
|
-
|
|
134
|
-
|
|
193
|
+
describe('ResizeObserver integration', () => {
|
|
194
|
+
it('should observe container on mount', () => {
|
|
195
|
+
render(<TagsRendererWrapper value={['tag1', 'tag2']} />);
|
|
196
|
+
|
|
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();
|
|
206
|
+
|
|
207
|
+
expect(mockScheduleIdleCallback).toHaveBeenCalled();
|
|
208
|
+
});
|
|
135
209
|
});
|
|
136
210
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
disconnect: mockDisconnect,
|
|
143
|
-
}));
|
|
211
|
+
describe('cleanup', () => {
|
|
212
|
+
it('should disconnect ResizeObserver on unmount', () => {
|
|
213
|
+
const { unmount } = render(
|
|
214
|
+
<TagsRendererWrapper value={['tag1', 'tag2']} />,
|
|
215
|
+
);
|
|
144
216
|
|
|
145
|
-
|
|
146
|
-
<RendererWrapper {...defaultProps} value={['tag1', 'tag2']} />,
|
|
147
|
-
);
|
|
217
|
+
expect(mockResizeObserver.disconnect).not.toHaveBeenCalled();
|
|
148
218
|
|
|
149
|
-
|
|
219
|
+
unmount();
|
|
150
220
|
|
|
151
|
-
|
|
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
|
+
});
|
|
152
235
|
});
|
|
153
236
|
});
|
|
@@ -1,121 +1,175 @@
|
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
const prevValRef = useRef<string[]>([]);
|
|
14
|
+
const valRef = useRef(val);
|
|
15
|
+
valRef.current = val; // Update on every render
|
|
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;
|
|
13
22
|
}
|
|
14
23
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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;
|
|
20
37
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
return textWidth + padding;
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
// Shared calculation function that uses valRef for current value
|
|
42
|
+
const calculateVisibleItems = useCallback((): void => {
|
|
43
|
+
const currentVal = valRef.current;
|
|
44
|
+
const container = containerRef.current;
|
|
45
|
+
|
|
46
|
+
if (!container || !Array.isArray(currentVal) || currentVal.length === 0) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const containerWidth = container.offsetWidth;
|
|
51
|
+
const gap = 5; // Gap between items from SCSS
|
|
52
|
+
const overflowIndicatorWidth = 60; // Approximate width for "... +X"
|
|
53
|
+
|
|
54
|
+
// Estimate tag widths based on text content
|
|
55
|
+
const tagWidths: number[] = (currentVal as string[]).map((item) =>
|
|
56
|
+
estimateTagWidth(item),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Calculate how many items can fit
|
|
60
|
+
let totalWidth = 0;
|
|
61
|
+
let newVisibleCount = 0;
|
|
62
|
+
|
|
63
|
+
for (let i = 0; i < (currentVal as string[]).length; i++) {
|
|
64
|
+
const itemWidth = tagWidths[i] + (i > 0 ? gap : 0);
|
|
65
|
+
const wouldNeedOverflow = i < (currentVal as string[]).length - 1;
|
|
66
|
+
const requiredWidth =
|
|
67
|
+
totalWidth +
|
|
68
|
+
itemWidth +
|
|
69
|
+
(wouldNeedOverflow ? overflowIndicatorWidth + gap : 0);
|
|
70
|
+
|
|
71
|
+
if (requiredWidth <= containerWidth) {
|
|
72
|
+
totalWidth += itemWidth;
|
|
73
|
+
newVisibleCount++;
|
|
74
|
+
} else {
|
|
75
|
+
break;
|
|
44
76
|
}
|
|
77
|
+
}
|
|
45
78
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
79
|
+
// If we can't fit all items, reserve space for overflow indicator
|
|
80
|
+
if (
|
|
81
|
+
newVisibleCount < (currentVal as string[]).length &&
|
|
82
|
+
newVisibleCount > 0
|
|
83
|
+
) {
|
|
84
|
+
// Re-calculate with overflow indicator space reserved
|
|
85
|
+
totalWidth = 0;
|
|
86
|
+
newVisibleCount = 0;
|
|
49
87
|
|
|
50
|
-
for (let i = 0; i <
|
|
88
|
+
for (let i = 0; i < (currentVal as string[]).length; i++) {
|
|
51
89
|
const itemWidth = tagWidths[i] + (i > 0 ? gap : 0);
|
|
52
|
-
const wouldNeedOverflow = i < val.length - 1;
|
|
53
90
|
const requiredWidth =
|
|
54
|
-
totalWidth +
|
|
55
|
-
itemWidth +
|
|
56
|
-
(wouldNeedOverflow ? overflowIndicatorWidth + gap : 0);
|
|
91
|
+
totalWidth + itemWidth + overflowIndicatorWidth + gap;
|
|
57
92
|
|
|
58
|
-
if (
|
|
93
|
+
if (
|
|
94
|
+
requiredWidth <= containerWidth &&
|
|
95
|
+
i < (currentVal as string[]).length - 1
|
|
96
|
+
) {
|
|
59
97
|
totalWidth += itemWidth;
|
|
60
98
|
newVisibleCount++;
|
|
61
99
|
} else {
|
|
62
100
|
break;
|
|
63
101
|
}
|
|
64
102
|
}
|
|
103
|
+
}
|
|
65
104
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
totalWidth = 0;
|
|
70
|
-
newVisibleCount = 0;
|
|
71
|
-
|
|
72
|
-
for (let i = 0; i < val.length; i++) {
|
|
73
|
-
const itemWidth = tagWidths[i] + (i > 0 ? gap : 0);
|
|
74
|
-
const requiredWidth =
|
|
75
|
-
totalWidth + itemWidth + overflowIndicatorWidth + gap;
|
|
76
|
-
|
|
77
|
-
if (requiredWidth <= containerWidth && i < val.length - 1) {
|
|
78
|
-
totalWidth += itemWidth;
|
|
79
|
-
newVisibleCount++;
|
|
80
|
-
} else {
|
|
81
|
-
break;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
105
|
+
// Update state with the new visible count
|
|
106
|
+
setVisibleCount(newVisibleCount);
|
|
107
|
+
}, [estimateTagWidth]);
|
|
85
108
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
109
|
+
// Effect 1: Handle tag changes (runs when val changes)
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!Array.isArray(val) || val.length === 0) {
|
|
112
|
+
setVisibleCount(0);
|
|
113
|
+
prevValRef.current = [];
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
89
116
|
|
|
90
|
-
//
|
|
91
|
-
|
|
117
|
+
// Check if array contents actually changed
|
|
118
|
+
const valArray = val as string[];
|
|
119
|
+
const hasChanged =
|
|
120
|
+
valArray.length !== prevValRef.current.length ||
|
|
121
|
+
valArray.some((item, index) => item !== prevValRef.current[index]);
|
|
122
|
+
|
|
123
|
+
if (hasChanged) {
|
|
124
|
+
calculateVisibleItems();
|
|
125
|
+
prevValRef.current = [...valArray];
|
|
126
|
+
}
|
|
127
|
+
}, [val, calculateVisibleItems]);
|
|
128
|
+
|
|
129
|
+
// Ref to store the idle callback ID
|
|
130
|
+
const idleCallbackRef = useRef<IdleCallbackHandle | null>(null);
|
|
131
|
+
|
|
132
|
+
// // Effect 2: Set up ResizeObserver (runs once on mount)
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (!containerRef.current) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
92
137
|
|
|
93
|
-
// Debounced resize handler
|
|
94
138
|
const debouncedCalculate = (): void => {
|
|
95
|
-
if (
|
|
96
|
-
|
|
139
|
+
if (idleCallbackRef.current) {
|
|
140
|
+
cancelScheduledCallback(idleCallbackRef.current);
|
|
97
141
|
}
|
|
98
|
-
|
|
142
|
+
idleCallbackRef.current = scheduleIdleCallback(() => {
|
|
99
143
|
calculateVisibleItems();
|
|
100
|
-
}
|
|
144
|
+
});
|
|
101
145
|
};
|
|
102
146
|
|
|
147
|
+
// const resizeObserver = new ResizeObserver(calculateVisibleItems);
|
|
103
148
|
const resizeObserver = new ResizeObserver(debouncedCalculate);
|
|
104
149
|
resizeObserver.observe(containerRef.current);
|
|
105
150
|
|
|
106
151
|
return () => {
|
|
107
|
-
if (
|
|
108
|
-
|
|
152
|
+
if (idleCallbackRef.current) {
|
|
153
|
+
cancelScheduledCallback(idleCallbackRef.current);
|
|
109
154
|
}
|
|
110
155
|
resizeObserver.disconnect();
|
|
111
156
|
};
|
|
112
|
-
}, [
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
157
|
+
}, [calculateVisibleItems]);
|
|
158
|
+
|
|
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
|
+
);
|
|
116
171
|
}
|
|
117
172
|
|
|
118
|
-
const stringArray = val as string[];
|
|
119
173
|
const visibleItems = stringArray.slice(0, visibleCount);
|
|
120
174
|
const hiddenItems = stringArray.slice(visibleCount);
|
|
121
175
|
|