@axinom/mosaic-ui 0.65.3 → 0.65.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/List/ListRow/Renderers/TagsRenderer/TagsRenderer.d.ts.map +1 -1
- package/dist/index.es.js +1 -1
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Explorer/Explorer.stories.tsx +17 -0
- package/src/components/List/ListRow/Renderers/TagsRenderer/TagsRenderer.spec.tsx +0 -28
- package/src/components/List/ListRow/Renderers/TagsRenderer/TagsRenderer.tsx +96 -71
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axinom/mosaic-ui",
|
|
3
|
-
"version": "0.65.
|
|
3
|
+
"version": "0.65.4",
|
|
4
4
|
"description": "UI components for building Axinom Mosaic applications",
|
|
5
5
|
"author": "Axinom",
|
|
6
6
|
"license": "PROPRIETARY",
|
|
@@ -112,5 +112,5 @@
|
|
|
112
112
|
"publishConfig": {
|
|
113
113
|
"access": "public"
|
|
114
114
|
},
|
|
115
|
-
"gitHead": "
|
|
115
|
+
"gitHead": "ac37c5be8b4c739425573c41be818692020f1f25"
|
|
116
116
|
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from '../FormStation';
|
|
22
22
|
import { IconName } from '../Icons';
|
|
23
23
|
import { ListSelectMode } from '../List';
|
|
24
|
+
import { createConnectionRenderer } from '../Utils';
|
|
24
25
|
import { generateBulkEditMutation } from './BulkEdit/GenerateMutation';
|
|
25
26
|
import { Explorer } from './Explorer';
|
|
26
27
|
import { QuickEditContext } from './QuickEdit/QuickEditContext';
|
|
@@ -34,6 +35,7 @@ interface ExplorerStoryData {
|
|
|
34
35
|
title: string;
|
|
35
36
|
date?: Date;
|
|
36
37
|
desc: string;
|
|
38
|
+
tags?: { nodes: { name: string }[] };
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
type ExplorerStoryType = typeof Explorer<ExplorerStoryData>;
|
|
@@ -120,6 +122,14 @@ const generateData = (
|
|
|
120
122
|
title: `${usePrefix ? `Index ${index}: ` : ''}${faker.random.words(
|
|
121
123
|
faker.datatype.number({ min: 1, max: 3 }),
|
|
122
124
|
)}`,
|
|
125
|
+
tags: {
|
|
126
|
+
nodes: Array.from(
|
|
127
|
+
{ length: faker.datatype.number({ min: 0, max: 10 }) },
|
|
128
|
+
() => ({
|
|
129
|
+
name: faker.random.word(),
|
|
130
|
+
}),
|
|
131
|
+
),
|
|
132
|
+
},
|
|
123
133
|
};
|
|
124
134
|
});
|
|
125
135
|
|
|
@@ -140,6 +150,13 @@ export const Default: StoryObj<ExplorerStoryType> = {
|
|
|
140
150
|
propertyName: 'title',
|
|
141
151
|
label: 'Title',
|
|
142
152
|
},
|
|
153
|
+
{
|
|
154
|
+
propertyName: 'tags',
|
|
155
|
+
label: 'Tags',
|
|
156
|
+
render: createConnectionRenderer<{ nodes: { name: string }[] }>(
|
|
157
|
+
(tag) => tag.name,
|
|
158
|
+
),
|
|
159
|
+
},
|
|
143
160
|
{
|
|
144
161
|
propertyName: 'date',
|
|
145
162
|
label: 'Date',
|
|
@@ -34,27 +34,6 @@ describe('TagsRenderer', () => {
|
|
|
34
34
|
expect(container.firstChild).toBeInTheDocument();
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
it('returns empty content when value is not an array', () => {
|
|
38
|
-
const { container } = render(
|
|
39
|
-
<RendererWrapper {...defaultProps} value="not-an-array" />,
|
|
40
|
-
);
|
|
41
|
-
expect(container.firstChild).toBeNull();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('returns empty content when value is null', () => {
|
|
45
|
-
const { container } = render(
|
|
46
|
-
<RendererWrapper {...defaultProps} value={null} />,
|
|
47
|
-
);
|
|
48
|
-
expect(container.firstChild).toBeNull();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('returns empty content when value is undefined', () => {
|
|
52
|
-
const { container } = render(
|
|
53
|
-
<RendererWrapper {...defaultProps} value={undefined} />,
|
|
54
|
-
);
|
|
55
|
-
expect(container.firstChild).toBeNull();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
37
|
it('renders tags when array is provided', () => {
|
|
59
38
|
const mockTags = ['tag1', 'tag2', 'tag3'];
|
|
60
39
|
|
|
@@ -127,13 +106,6 @@ describe('TagsRenderer', () => {
|
|
|
127
106
|
expect(container).toBeInTheDocument();
|
|
128
107
|
});
|
|
129
108
|
|
|
130
|
-
it('handles empty array', () => {
|
|
131
|
-
const { container } = render(
|
|
132
|
-
<RendererWrapper {...defaultProps} value={[]} />,
|
|
133
|
-
);
|
|
134
|
-
expect(container.firstChild).toBeNull();
|
|
135
|
-
});
|
|
136
|
-
|
|
137
109
|
it('cleans up ResizeObserver on unmount', () => {
|
|
138
110
|
const mockDisconnect = jest.fn();
|
|
139
111
|
(global.ResizeObserver as jest.Mock).mockImplementation(() => ({
|
|
@@ -6,91 +6,120 @@ export const TagsRenderer = (val: unknown): JSX.Element => {
|
|
|
6
6
|
const resizeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
7
7
|
const [visibleCount, setVisibleCount] = useState<number>(0);
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const prevValRef = useRef<string[]>([]);
|
|
10
|
+
const valRef = useRef(val);
|
|
11
|
+
valRef.current = val; // Update on every render
|
|
12
|
+
|
|
13
|
+
// Shared calculation function that uses valRef for current value
|
|
14
|
+
const calculateVisibleItems = (): void => {
|
|
15
|
+
const currentVal = valRef.current;
|
|
16
|
+
const container = containerRef.current;
|
|
17
|
+
|
|
18
|
+
if (!container || !Array.isArray(currentVal) || currentVal.length === 0) {
|
|
12
19
|
return;
|
|
13
20
|
}
|
|
14
21
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
const containerWidth = container.offsetWidth;
|
|
23
|
+
const gap = 5; // Gap between items from SCSS
|
|
24
|
+
const overflowIndicatorWidth = 60; // Approximate width for "... +X"
|
|
25
|
+
|
|
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
|
+
}
|
|
20
46
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const tagWidths
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
tempElements.push(tempTag);
|
|
39
|
-
tagWidths.push(tempTag.offsetWidth);
|
|
40
|
-
});
|
|
41
|
-
} finally {
|
|
42
|
-
// Clean up temporary elements
|
|
43
|
-
tempElements.forEach((el) => container.removeChild(el));
|
|
47
|
+
// Calculate how many items can fit
|
|
48
|
+
let totalWidth = 0;
|
|
49
|
+
let newVisibleCount = 0;
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < (currentVal as string[]).length; i++) {
|
|
52
|
+
const itemWidth = tagWidths[i] + (i > 0 ? gap : 0);
|
|
53
|
+
const wouldNeedOverflow = i < (currentVal as string[]).length - 1;
|
|
54
|
+
const requiredWidth =
|
|
55
|
+
totalWidth +
|
|
56
|
+
itemWidth +
|
|
57
|
+
(wouldNeedOverflow ? overflowIndicatorWidth + gap : 0);
|
|
58
|
+
|
|
59
|
+
if (requiredWidth <= containerWidth) {
|
|
60
|
+
totalWidth += itemWidth;
|
|
61
|
+
newVisibleCount++;
|
|
62
|
+
} else {
|
|
63
|
+
break;
|
|
44
64
|
}
|
|
65
|
+
}
|
|
45
66
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
67
|
+
// If we can't fit all items, reserve space for overflow indicator
|
|
68
|
+
if (
|
|
69
|
+
newVisibleCount < (currentVal as string[]).length &&
|
|
70
|
+
newVisibleCount > 0
|
|
71
|
+
) {
|
|
72
|
+
// Re-calculate with overflow indicator space reserved
|
|
73
|
+
totalWidth = 0;
|
|
74
|
+
newVisibleCount = 0;
|
|
49
75
|
|
|
50
|
-
for (let i = 0; i <
|
|
76
|
+
for (let i = 0; i < (currentVal as string[]).length; i++) {
|
|
51
77
|
const itemWidth = tagWidths[i] + (i > 0 ? gap : 0);
|
|
52
|
-
const wouldNeedOverflow = i < val.length - 1;
|
|
53
78
|
const requiredWidth =
|
|
54
|
-
totalWidth +
|
|
55
|
-
itemWidth +
|
|
56
|
-
(wouldNeedOverflow ? overflowIndicatorWidth + gap : 0);
|
|
79
|
+
totalWidth + itemWidth + overflowIndicatorWidth + gap;
|
|
57
80
|
|
|
58
|
-
if (
|
|
81
|
+
if (
|
|
82
|
+
requiredWidth <= containerWidth &&
|
|
83
|
+
i < (currentVal as string[]).length - 1
|
|
84
|
+
) {
|
|
59
85
|
totalWidth += itemWidth;
|
|
60
86
|
newVisibleCount++;
|
|
61
87
|
} else {
|
|
62
88
|
break;
|
|
63
89
|
}
|
|
64
90
|
}
|
|
91
|
+
}
|
|
65
92
|
|
|
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
|
-
}
|
|
93
|
+
// Update state with the new visible count
|
|
94
|
+
setVisibleCount(newVisibleCount);
|
|
95
|
+
};
|
|
85
96
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
97
|
+
// Effect 1: Handle tag changes (runs when val changes)
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!Array.isArray(val) || val.length === 0) {
|
|
100
|
+
setVisibleCount(0);
|
|
101
|
+
prevValRef.current = [];
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check if array contents actually changed
|
|
106
|
+
const valArray = val as string[];
|
|
107
|
+
const hasChanged =
|
|
108
|
+
valArray.length !== prevValRef.current.length ||
|
|
109
|
+
valArray.some((item, index) => item !== prevValRef.current[index]);
|
|
110
|
+
|
|
111
|
+
if (hasChanged) {
|
|
112
|
+
calculateVisibleItems();
|
|
113
|
+
prevValRef.current = [...valArray];
|
|
114
|
+
}
|
|
115
|
+
}, [val]);
|
|
89
116
|
|
|
90
|
-
|
|
91
|
-
|
|
117
|
+
// Effect 2: Set up ResizeObserver (runs once on mount)
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (!containerRef.current) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
92
122
|
|
|
93
|
-
// Debounced resize handler
|
|
94
123
|
const debouncedCalculate = (): void => {
|
|
95
124
|
if (resizeTimeoutRef.current) {
|
|
96
125
|
clearTimeout(resizeTimeoutRef.current);
|
|
@@ -109,13 +138,9 @@ export const TagsRenderer = (val: unknown): JSX.Element => {
|
|
|
109
138
|
}
|
|
110
139
|
resizeObserver.disconnect();
|
|
111
140
|
};
|
|
112
|
-
}, [
|
|
113
|
-
|
|
114
|
-
if (!Array.isArray(val) || val.length === 0) {
|
|
115
|
-
return <></>;
|
|
116
|
-
}
|
|
141
|
+
}, []); // Empty deps - only runs once
|
|
117
142
|
|
|
118
|
-
const stringArray = val as string[];
|
|
143
|
+
const stringArray = Array.isArray(val) ? (val as string[]) : [];
|
|
119
144
|
const visibleItems = stringArray.slice(0, visibleCount);
|
|
120
145
|
const hiddenItems = stringArray.slice(visibleCount);
|
|
121
146
|
|