@vuu-ui/vuu-layout 0.0.27
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/README.md +1 -0
- package/package.json +30 -0
- package/src/Component.css +2 -0
- package/src/Component.tsx +20 -0
- package/src/DraggableLayout.css +18 -0
- package/src/DraggableLayout.tsx +29 -0
- package/src/__tests__/flexbox-utils.spec.js +90 -0
- package/src/action-buttons/action-buttons.css +12 -0
- package/src/action-buttons/action-buttons.tsx +30 -0
- package/src/action-buttons/index.ts +1 -0
- package/src/chest-of-drawers/Chest.css +36 -0
- package/src/chest-of-drawers/Chest.tsx +42 -0
- package/src/chest-of-drawers/Drawer.css +153 -0
- package/src/chest-of-drawers/Drawer.tsx +118 -0
- package/src/chest-of-drawers/index.ts +2 -0
- package/src/common-types.ts +9 -0
- package/src/debug.ts +16 -0
- package/src/dialog/Dialog.css +16 -0
- package/src/dialog/Dialog.tsx +59 -0
- package/src/dialog/index.ts +1 -0
- package/src/drag-drop/BoxModel.ts +546 -0
- package/src/drag-drop/DragState.ts +222 -0
- package/src/drag-drop/Draggable.ts +282 -0
- package/src/drag-drop/DropMenu.css +70 -0
- package/src/drag-drop/DropMenu.tsx +68 -0
- package/src/drag-drop/DropTarget.ts +392 -0
- package/src/drag-drop/DropTargetRenderer.css +40 -0
- package/src/drag-drop/DropTargetRenderer.tsx +284 -0
- package/src/drag-drop/dragDropTypes.ts +49 -0
- package/src/drag-drop/index.ts +4 -0
- package/src/editable-label/EditableLabel.css +28 -0
- package/src/editable-label/EditableLabel.tsx +99 -0
- package/src/editable-label/index.ts +1 -0
- package/src/flexbox/Flexbox.css +45 -0
- package/src/flexbox/Flexbox.tsx +70 -0
- package/src/flexbox/FlexboxLayout.jsx +26 -0
- package/src/flexbox/FluidGrid.css +134 -0
- package/src/flexbox/FluidGrid.tsx +84 -0
- package/src/flexbox/FluidGridLayout.tsx +10 -0
- package/src/flexbox/Splitter.css +140 -0
- package/src/flexbox/Splitter.tsx +135 -0
- package/src/flexbox/flexbox-utils.ts +128 -0
- package/src/flexbox/flexboxTypes.ts +63 -0
- package/src/flexbox/index.ts +4 -0
- package/src/flexbox/useResponsiveSizing.ts +85 -0
- package/src/flexbox/useSplitterResizing.ts +272 -0
- package/src/index.ts +20 -0
- package/src/layout-action.ts +21 -0
- package/src/layout-header/ActionButton.tsx +23 -0
- package/src/layout-header/Header.css +8 -0
- package/src/layout-header/Header.tsx +222 -0
- package/src/layout-header/index.ts +1 -0
- package/src/layout-provider/LayoutProvider.tsx +160 -0
- package/src/layout-provider/LayoutProviderContext.ts +17 -0
- package/src/layout-provider/index.ts +2 -0
- package/src/layout-provider/useLayoutDragDrop.ts +241 -0
- package/src/layout-reducer/flexUtils.ts +281 -0
- package/src/layout-reducer/index.ts +4 -0
- package/src/layout-reducer/insert-layout-element.ts +365 -0
- package/src/layout-reducer/layout-reducer.ts +255 -0
- package/src/layout-reducer/layoutTypes.ts +151 -0
- package/src/layout-reducer/layoutUtils.ts +302 -0
- package/src/layout-reducer/remove-layout-element.ts +240 -0
- package/src/layout-reducer/replace-layout-element.ts +118 -0
- package/src/layout-reducer/resize-flex-children.ts +56 -0
- package/src/layout-reducer/wrap-layout-element.ts +317 -0
- package/src/layout-view/View.css +58 -0
- package/src/layout-view/View.tsx +149 -0
- package/src/layout-view/ViewContext.ts +31 -0
- package/src/layout-view/index.ts +4 -0
- package/src/layout-view/useView.tsx +104 -0
- package/src/layout-view/useViewActionDispatcher.ts +133 -0
- package/src/layout-view/useViewResize.ts +53 -0
- package/src/layout-view/viewTypes.ts +37 -0
- package/src/palette/Palette.css +37 -0
- package/src/palette/Palette.tsx +140 -0
- package/src/palette/PaletteUitk.css +9 -0
- package/src/palette/PaletteUitk.tsx +79 -0
- package/src/palette/index.ts +2 -0
- package/src/placeholder/Placeholder.css +10 -0
- package/src/placeholder/Placeholder.tsx +39 -0
- package/src/placeholder/index.ts +1 -0
- package/src/registry/ComponentRegistry.ts +35 -0
- package/src/registry/index.ts +1 -0
- package/src/responsive/OverflowMenu.css +31 -0
- package/src/responsive/OverflowMenu.jsx +56 -0
- package/src/responsive/breakpoints.ts +48 -0
- package/src/responsive/index.ts +4 -0
- package/src/responsive/measureMinimumNodeSize.ts +23 -0
- package/src/responsive/overflowUtils.js +14 -0
- package/src/responsive/use-breakpoints.ts +100 -0
- package/src/responsive/useOverflowObserver.ts +606 -0
- package/src/responsive/useResizeObserver.ts +154 -0
- package/src/responsive/utils.ts +37 -0
- package/src/stack/Stack.css +39 -0
- package/src/stack/Stack.tsx +160 -0
- package/src/stack/StackLayout.tsx +137 -0
- package/src/stack/index.ts +3 -0
- package/src/stack/stackTypes.ts +19 -0
- package/src/tabs/TabPanel.css +12 -0
- package/src/tabs/TabPanel.tsx +17 -0
- package/src/tabs/index.ts +1 -0
- package/src/tools/config-wrapper/ConfigWrapper.jsx +53 -0
- package/src/tools/config-wrapper/index.js +1 -0
- package/src/tools/devtools-box/layout-configurator.css +112 -0
- package/src/tools/devtools-box/layout-configurator.jsx +369 -0
- package/src/tools/devtools-tree/layout-tree-viewer.css +15 -0
- package/src/tools/devtools-tree/layout-tree-viewer.jsx +36 -0
- package/src/tools/index.js +3 -0
- package/src/use-persistent-state.ts +115 -0
- package/src/utils/componentFromLayout.tsx +30 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/pathUtils.ts +294 -0
- package/src/utils/propUtils.ts +24 -0
- package/src/utils/refUtils.ts +16 -0
- package/src/utils/styleUtils.ts +14 -0
- package/src/utils/typeOf.ts +22 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
import { useResizeObserver } from "./useResizeObserver";
|
|
3
|
+
import { measureMinimumNodeSize } from "./measureMinimumNodeSize";
|
|
4
|
+
|
|
5
|
+
const MONITORED_DIMENSIONS = {
|
|
6
|
+
horizontal: ["width", "scrollHeight"],
|
|
7
|
+
vertical: ["height", "scrollWidth"],
|
|
8
|
+
none: [],
|
|
9
|
+
};
|
|
10
|
+
const NO_OVERFLOW_INDICATOR = {};
|
|
11
|
+
const NO_DATA = {};
|
|
12
|
+
|
|
13
|
+
const UNCOLLAPSED_DYNAMIC_ITEMS =
|
|
14
|
+
'[data-collapsible="dynamic"]:not([data-collapsed="true"]):not([data-collapsing="true"])';
|
|
15
|
+
|
|
16
|
+
const addAll = (sum: number, m: any) => sum + m.size;
|
|
17
|
+
const addAllExceptOverflowIndicator = (sum: number, m: any) =>
|
|
18
|
+
sum + (m.isOverflowIndicator ? 0 : m.size);
|
|
19
|
+
|
|
20
|
+
// There should be no collapsible items here that are not already collapsed
|
|
21
|
+
// otherwise we would be collapsing, not overflowing
|
|
22
|
+
const lastOverflowableItem = (arr) => {
|
|
23
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
24
|
+
const item = arr[i];
|
|
25
|
+
// TODO should we support a no-overflow attribute (maybe a priority 0)
|
|
26
|
+
// to prevent an item from overflowing ?
|
|
27
|
+
// TODO when all collapsible items are collapsed and we start overflowing,
|
|
28
|
+
// should we leave collapsed items to last in the overflow priority ?
|
|
29
|
+
if (!item.isOverflowIndicator) {
|
|
30
|
+
return item;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
};
|
|
35
|
+
const OVERFLOWING = 1000;
|
|
36
|
+
const collapsedOnly = (status) => status > 0 && status < 1000;
|
|
37
|
+
const includesOverflow = (status) => status >= OVERFLOWING;
|
|
38
|
+
const lastListItem = (listRef) => listRef.current[listRef.current.length - 1];
|
|
39
|
+
|
|
40
|
+
const newlyCollapsed = (visibleItems) =>
|
|
41
|
+
visibleItems.some((item) => item.collapsed && item.fullWidth === null);
|
|
42
|
+
|
|
43
|
+
const hasUncollapsedDynamicItems = (containerRef) =>
|
|
44
|
+
containerRef.current.querySelector(UNCOLLAPSED_DYNAMIC_ITEMS) !== null;
|
|
45
|
+
|
|
46
|
+
const moveOverflowItem = (fromStack, toStack) => {
|
|
47
|
+
const item = lastOverflowableItem(fromStack.current);
|
|
48
|
+
if (item) {
|
|
49
|
+
fromStack.current = fromStack.current.filter((i) => i !== item);
|
|
50
|
+
toStack.current = toStack.current.concat(item);
|
|
51
|
+
return item;
|
|
52
|
+
} else {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const byDescendingPriority = (m1, m2) => {
|
|
58
|
+
let result = m1.priority - m2.priority;
|
|
59
|
+
if (result === 0) {
|
|
60
|
+
result = m1.index - m2.index;
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const getOverflowIndicator = (visibleRef) =>
|
|
66
|
+
visibleRef.current.find((item) => item.isOverflowIndicator);
|
|
67
|
+
|
|
68
|
+
const Dimensions = {
|
|
69
|
+
horizontal: {
|
|
70
|
+
size: "clientWidth",
|
|
71
|
+
depth: "clientHeight",
|
|
72
|
+
scrollDepth: "scrollHeight",
|
|
73
|
+
},
|
|
74
|
+
vertical: {
|
|
75
|
+
size: "clientHeight",
|
|
76
|
+
depth: "clientWidth",
|
|
77
|
+
scrollDepth: "scrollWidth",
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const measureContainerOverflow = (
|
|
82
|
+
{ current: innerEl },
|
|
83
|
+
orientation = "horizontal"
|
|
84
|
+
) => {
|
|
85
|
+
const dim = Dimensions[orientation];
|
|
86
|
+
const { [dim.depth]: containerDepth } = innerEl.parentNode;
|
|
87
|
+
const { [dim.scrollDepth]: scrollDepth, [dim.size]: contentSize } = innerEl;
|
|
88
|
+
const isOverflowing = containerDepth < scrollDepth;
|
|
89
|
+
return [isOverflowing, contentSize, containerDepth];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const useOverflowStatus = () => {
|
|
93
|
+
const [, forceUpdate] = useState(null);
|
|
94
|
+
// TODO make this easier to understand by storing the overflow and
|
|
95
|
+
// collapse status as separate reference count fields
|
|
96
|
+
const [overflowing, _setOverflowing] = useState(0);
|
|
97
|
+
const overflowingRef = useRef(0);
|
|
98
|
+
const setOverflowing = useCallback(
|
|
99
|
+
(value) => {
|
|
100
|
+
_setOverflowing((overflowingRef.current = value));
|
|
101
|
+
},
|
|
102
|
+
[_setOverflowing]
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const updateOverflowStatus = useCallback(
|
|
106
|
+
(value, force) => {
|
|
107
|
+
if (Math.abs(value) === OVERFLOWING) {
|
|
108
|
+
if (value > 0 && !includesOverflow(overflowingRef.current)) {
|
|
109
|
+
setOverflowing(overflowingRef.current + value);
|
|
110
|
+
} else if (value < 0 && includesOverflow(overflowingRef.current)) {
|
|
111
|
+
setOverflowing(overflowingRef.current + value);
|
|
112
|
+
} else {
|
|
113
|
+
forceUpdate({});
|
|
114
|
+
}
|
|
115
|
+
} else if (value !== 0) {
|
|
116
|
+
setOverflowing(overflowingRef.current + value);
|
|
117
|
+
} else if (force) {
|
|
118
|
+
forceUpdate({});
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
[forceUpdate, overflowingRef, setOverflowing]
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
return [overflowingRef, overflowing, updateOverflowStatus];
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const measureChildNodes = ({ current: innerEl }, dimension) => {
|
|
128
|
+
const measurements = Array.from(innerEl.childNodes).reduce(
|
|
129
|
+
(list, node: Node) => {
|
|
130
|
+
const {
|
|
131
|
+
collapsible,
|
|
132
|
+
collapsed,
|
|
133
|
+
collapsing,
|
|
134
|
+
index,
|
|
135
|
+
priority = "1",
|
|
136
|
+
overflowIndicator,
|
|
137
|
+
overflowed,
|
|
138
|
+
} = node?.dataset ?? NO_DATA;
|
|
139
|
+
if (index) {
|
|
140
|
+
const size = measureMinimumNodeSize(node, dimension);
|
|
141
|
+
if (overflowed) {
|
|
142
|
+
delete node.dataset.overflowed;
|
|
143
|
+
}
|
|
144
|
+
list.push({
|
|
145
|
+
collapsible,
|
|
146
|
+
collapsed: collapsible ? collapsed === "true" : undefined,
|
|
147
|
+
collapsing,
|
|
148
|
+
// only to be populated in case of collapse
|
|
149
|
+
// TODO check the role of this - especially the way we check it in useEffect
|
|
150
|
+
// to detect collapse
|
|
151
|
+
fullSize: null,
|
|
152
|
+
index: parseInt(index, 10),
|
|
153
|
+
isOverflowIndicator: overflowIndicator,
|
|
154
|
+
label: node.title || node.innerText,
|
|
155
|
+
priority: parseInt(priority, 10),
|
|
156
|
+
size,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return list;
|
|
160
|
+
},
|
|
161
|
+
[]
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
return measurements.sort(byDescendingPriority);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const getElementForItem = (ref, item) =>
|
|
168
|
+
ref.current.querySelector(`:scope > [data-idx='${item.index}']`);
|
|
169
|
+
|
|
170
|
+
// value could be anything which might require a re-evaluation. In the case of tabs
|
|
171
|
+
// we might have selected an overflowed tab. Can we make this more efficient, only
|
|
172
|
+
// needs action if an overflowed item re-enters the visible section
|
|
173
|
+
export function useOverflowObserver(orientation = "horizontal", label = "") {
|
|
174
|
+
const ref = useRef(null);
|
|
175
|
+
const [overflowingRef, overflowing, updateOverflowStatus] =
|
|
176
|
+
useOverflowStatus();
|
|
177
|
+
// const [, forceUpdate] = useState();
|
|
178
|
+
const visibleRef = useRef([]);
|
|
179
|
+
const overflowedRef = useRef([]);
|
|
180
|
+
const collapsedRef = useRef([]);
|
|
181
|
+
const collapsingRef = useRef(false);
|
|
182
|
+
const rootDepthRef = useRef(null);
|
|
183
|
+
const containerSizeRef = useRef(null);
|
|
184
|
+
const horizontalRef = useRef(orientation === "horizontal");
|
|
185
|
+
const overflowIndicatorSizeRef = useRef(36); // should default by density
|
|
186
|
+
const minSizeRef = useRef(0);
|
|
187
|
+
|
|
188
|
+
const setContainerMinSize = useCallback(
|
|
189
|
+
(size) => {
|
|
190
|
+
const isHorizontal = horizontalRef.current;
|
|
191
|
+
if (size === undefined) {
|
|
192
|
+
const dimension = isHorizontal ? "width" : "height";
|
|
193
|
+
({ [dimension]: size } = ref.current.getBoundingClientRect());
|
|
194
|
+
}
|
|
195
|
+
minSizeRef.current = size;
|
|
196
|
+
const styleDimension = isHorizontal ? "minWidth" : "minHeight";
|
|
197
|
+
ref.current.style[styleDimension] = size + "px";
|
|
198
|
+
},
|
|
199
|
+
[ref]
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const markOverflowingItems = useCallback(
|
|
203
|
+
(visibleContentSize, containerSize) => {
|
|
204
|
+
let result = 0;
|
|
205
|
+
// First pass, see if there is a collapsible item we can collapse. We won't
|
|
206
|
+
// know how much space this frees up until the thing has re-rendered, so this
|
|
207
|
+
// may kick off a chain of renders and remeasures if there are multiple collapsible
|
|
208
|
+
// items and each yields only a part of the shrinkage we need to apply.
|
|
209
|
+
// That's the worst case scenario.
|
|
210
|
+
if (
|
|
211
|
+
visibleRef.current.some((item) => item.collapsible && !item.collapsed)
|
|
212
|
+
) {
|
|
213
|
+
for (let i = visibleRef.current.length - 1; i >= 0; i--) {
|
|
214
|
+
const item = visibleRef.current[i];
|
|
215
|
+
if (item.collapsible === "instant" && !item.collapsed) {
|
|
216
|
+
item.collapsed = true;
|
|
217
|
+
const target = getElementForItem(ref, item);
|
|
218
|
+
target.dataset.collapsed = true;
|
|
219
|
+
collapsedRef.current.push(item);
|
|
220
|
+
// We only ever collapse 1 item at a time. We now need to wait for
|
|
221
|
+
// it to render, so we can re-measure and determine how much space
|
|
222
|
+
// this has saved.
|
|
223
|
+
return 1;
|
|
224
|
+
} else if (
|
|
225
|
+
item.collapsible === "dynamic" &&
|
|
226
|
+
!item.collapsed &&
|
|
227
|
+
!item.collapsing
|
|
228
|
+
) {
|
|
229
|
+
item.collapsing = true;
|
|
230
|
+
const target = getElementForItem(ref, item);
|
|
231
|
+
target.dataset.collapsing = true;
|
|
232
|
+
collapsedRef.current.push(item);
|
|
233
|
+
ref.current.dataset.collapsing = true;
|
|
234
|
+
// We only ever collapse 1 item at a time. We now need to wait for
|
|
235
|
+
// it to render, so we can re-measure and determine how much space
|
|
236
|
+
// this has saved.
|
|
237
|
+
return 1;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// If no collapsible items, movin items from visible to overflowed queues
|
|
243
|
+
while (visibleContentSize > containerSize) {
|
|
244
|
+
const overflowedItem = moveOverflowItem(visibleRef, overflowedRef);
|
|
245
|
+
if (overflowedItem === null) {
|
|
246
|
+
// unable to overflow, all items are collapsed, this is our minimum width,
|
|
247
|
+
// enforce it ...
|
|
248
|
+
// TODO what if density changes
|
|
249
|
+
//TODO probably not right, now we overflow even collapsed items, min width should be
|
|
250
|
+
// overflow indicator width plus width of any non-overflowable items
|
|
251
|
+
setContainerMinSize(visibleContentSize);
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
visibleContentSize -= overflowedItem.size;
|
|
255
|
+
const target = getElementForItem(ref, overflowedItem);
|
|
256
|
+
target.dataset.overflowed = true;
|
|
257
|
+
result = OVERFLOWING;
|
|
258
|
+
}
|
|
259
|
+
return result;
|
|
260
|
+
},
|
|
261
|
+
[setContainerMinSize]
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const removeOverflowIfSpaceAllows = useCallback(
|
|
265
|
+
(containerSize) => {
|
|
266
|
+
let result = 0;
|
|
267
|
+
// TODO calculate this without using fullWidth if we have OVERFLOW
|
|
268
|
+
// Need a loop here where we first remove OVERFLOW, then potentially remove
|
|
269
|
+
// COLLAPSE too
|
|
270
|
+
// We want to re-introduce overflowed items before we start to restore collapsed items
|
|
271
|
+
// When we are dealing with overflowed items, we just use the current width of collapsed items.
|
|
272
|
+
let visibleContentSize = visibleRef.current.reduce(
|
|
273
|
+
addAllExceptOverflowIndicator,
|
|
274
|
+
0
|
|
275
|
+
);
|
|
276
|
+
let diff = containerSize - visibleContentSize;
|
|
277
|
+
|
|
278
|
+
if (collapsedOnly(overflowingRef.current)) {
|
|
279
|
+
// find the next collapsed item, see how much extra space it would
|
|
280
|
+
// occupy if restored. If we have enough space, restore it.
|
|
281
|
+
while (collapsedRef.current.length) {
|
|
282
|
+
const item = lastListItem(collapsedRef);
|
|
283
|
+
const itemDiff = item.fullSize - item.size;
|
|
284
|
+
if (diff >= itemDiff) {
|
|
285
|
+
item.collapsed = false;
|
|
286
|
+
item.size = item.fullSize;
|
|
287
|
+
// Be careful before setting this to null, check the code in useEffect
|
|
288
|
+
delete item.fullSize;
|
|
289
|
+
const target = getElementForItem(ref, item);
|
|
290
|
+
collapsedRef.current.pop();
|
|
291
|
+
delete target.dataset.collapsed;
|
|
292
|
+
diff = diff - itemDiff;
|
|
293
|
+
result += 1;
|
|
294
|
+
} else {
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return result;
|
|
299
|
+
} else {
|
|
300
|
+
while (overflowedRef.current.length > 0) {
|
|
301
|
+
const { size: nextSize } = lastListItem(overflowedRef);
|
|
302
|
+
|
|
303
|
+
if (diff >= nextSize) {
|
|
304
|
+
const { size: overflowSize = 0 } =
|
|
305
|
+
getOverflowIndicator(visibleRef) || NO_OVERFLOW_INDICATOR;
|
|
306
|
+
// we can only ignore the width of overflow Indicator if either there is only one remaining
|
|
307
|
+
// overflow item (so overflowIndicator will be removed) or diff is big enough to accommodate
|
|
308
|
+
// the overflow Ind.
|
|
309
|
+
if (
|
|
310
|
+
overflowedRef.current.length === 1 ||
|
|
311
|
+
diff >= nextSize + overflowSize
|
|
312
|
+
) {
|
|
313
|
+
const overflowedItem = moveOverflowItem(
|
|
314
|
+
overflowedRef,
|
|
315
|
+
visibleRef
|
|
316
|
+
);
|
|
317
|
+
visibleContentSize += overflowedItem.size;
|
|
318
|
+
const target = getElementForItem(ref, overflowedItem);
|
|
319
|
+
delete target.dataset.overflowed;
|
|
320
|
+
diff = diff - overflowedItem.size;
|
|
321
|
+
result = OVERFLOWING;
|
|
322
|
+
} else {
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// DOn't return OVERFLOWING unless there is no more overflow
|
|
331
|
+
return result;
|
|
332
|
+
},
|
|
333
|
+
[overflowingRef]
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const initializeDynamicContent = useCallback(() => {
|
|
337
|
+
let renderedSize = visibleRef.current.reduce(addAll, 0);
|
|
338
|
+
let diff = renderedSize - containerSizeRef.current;
|
|
339
|
+
for (let i = visibleRef.current.length - 1; i >= 0; i--) {
|
|
340
|
+
const item = visibleRef.current[i];
|
|
341
|
+
if (item.collapsible && !item.collapsed) {
|
|
342
|
+
const target = getElementForItem(ref, item);
|
|
343
|
+
// TODO where do we derive min width 28 + 8
|
|
344
|
+
if (diff > item.size - 36) {
|
|
345
|
+
// We really want to know if it has reached min-width, but we will have to
|
|
346
|
+
// wait for it to render
|
|
347
|
+
target.dataset.collapsed = item.collapsed = true;
|
|
348
|
+
diff -= item.size;
|
|
349
|
+
} else {
|
|
350
|
+
target.dataset.collapsing = item.collapsing = true;
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}, [containerSizeRef, ref, visibleRef]);
|
|
356
|
+
|
|
357
|
+
const collapseCollapsingItem = useCallback(
|
|
358
|
+
(item, target) => {
|
|
359
|
+
target.dataset.collapsing = item.collapsing = false;
|
|
360
|
+
target.dataset.collapsed = item.collapsed = true;
|
|
361
|
+
|
|
362
|
+
const rest = visibleRef.current.filter(
|
|
363
|
+
({ collapsible, collapsed }) => collapsible === "dynamic" && !collapsed
|
|
364
|
+
);
|
|
365
|
+
const last = rest.pop();
|
|
366
|
+
if (last) {
|
|
367
|
+
const lastTarget = getElementForItem(ref, last);
|
|
368
|
+
lastTarget.dataset.collapsing = last.collapsing = true;
|
|
369
|
+
} else {
|
|
370
|
+
// Set minSize to current measured size
|
|
371
|
+
// TODO check that this makes sense...suspect it doesn't
|
|
372
|
+
setContainerMinSize();
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
[setContainerMinSize]
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
const restoreCollapsingItem = useCallback((item, target) => {
|
|
379
|
+
target.dataset.collapsing = item.collapsing = false;
|
|
380
|
+
// we might have an opportunity to switch the next collapsed item to
|
|
381
|
+
// collapsing here. If we don't do this, it will ge handled in the next resize
|
|
382
|
+
}, []);
|
|
383
|
+
|
|
384
|
+
const checkDynamicContent = useCallback(
|
|
385
|
+
(containerHasGrown) => {
|
|
386
|
+
// The order must matter here
|
|
387
|
+
const collapsingItem = visibleRef.current.find(
|
|
388
|
+
({ collapsible, collapsing }) => collapsible === "dynamic" && collapsing
|
|
389
|
+
);
|
|
390
|
+
const collapsedItem = visibleRef.current.find(
|
|
391
|
+
({ collapsible, collapsed }) => collapsible === "dynamic" && collapsed
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
if (collapsingItem === undefined && collapsedItem === undefined) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (collapsingItem === undefined) {
|
|
399
|
+
const target = getElementForItem(ref, collapsedItem);
|
|
400
|
+
target.dataset.collapsed = collapsedItem.collapsed = false;
|
|
401
|
+
target.dataset.collapsing = collapsedItem.collapsing = true;
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const target = getElementForItem(ref, collapsingItem);
|
|
406
|
+
const dimension = horizontalRef.current ? "width" : "height";
|
|
407
|
+
|
|
408
|
+
if (containerHasGrown && collapsedItem) {
|
|
409
|
+
const size = measureMinimumNodeSize(target, dimension);
|
|
410
|
+
// We don't restore a collapsing item unless there is at least one collapsed item
|
|
411
|
+
if (collapsedItem && size === collapsingItem.size) {
|
|
412
|
+
restoreCollapsingItem(collapsingItem, target);
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
// Note we are going to compare width with minWidth. Margin is ignored here, so we
|
|
416
|
+
// use getBoundingClientRect rather than measureNode
|
|
417
|
+
const { [dimension]: size } = target.getBoundingClientRect();
|
|
418
|
+
const style = getComputedStyle(target);
|
|
419
|
+
const minSize = parseInt(style.getPropertyValue(`min-${dimension}`));
|
|
420
|
+
if (size === minSize) {
|
|
421
|
+
collapseCollapsingItem(collapsingItem, target);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
[collapseCollapsingItem, restoreCollapsingItem]
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const resetMeasurements = useCallback(() => {
|
|
429
|
+
const [isOverflowing, innerContainerSize, rootContainerDepth] =
|
|
430
|
+
measureContainerOverflow(ref, orientation);
|
|
431
|
+
|
|
432
|
+
containerSizeRef.current = innerContainerSize;
|
|
433
|
+
rootDepthRef.current = rootContainerDepth;
|
|
434
|
+
|
|
435
|
+
const hasDynamicItems = hasUncollapsedDynamicItems(ref);
|
|
436
|
+
|
|
437
|
+
if (hasDynamicItems || isOverflowing) {
|
|
438
|
+
const dimension = horizontalRef.current ? "width" : "height";
|
|
439
|
+
const measurements = measureChildNodes(ref, dimension);
|
|
440
|
+
visibleRef.current = measurements;
|
|
441
|
+
overflowedRef.current = [];
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (hasDynamicItems) {
|
|
445
|
+
// if we don't have overflow, but we do have dynamic collapse items, we need to monitor resize events
|
|
446
|
+
// to determine when the collapsing item reaches min-width. At which point it becomes collapsed, and
|
|
447
|
+
// the next dynanic collapse item assumes collapsing status
|
|
448
|
+
collapsingRef.current = true;
|
|
449
|
+
ref.current.dataset.collapsing = true;
|
|
450
|
+
|
|
451
|
+
if (isOverflowing) {
|
|
452
|
+
// We will only encounter this scenario first-time in. Once we initialize for dynamic content,
|
|
453
|
+
// there will be no more overflow (unless we decide to re-enable overflow once all dynamic
|
|
454
|
+
// items are collapsed ).
|
|
455
|
+
initializeDynamicContent();
|
|
456
|
+
} else {
|
|
457
|
+
const collapsingItem = lastListItem(visibleRef);
|
|
458
|
+
const element = getElementForItem(ref, collapsingItem);
|
|
459
|
+
element.dataset.collapsing = collapsingItem.collapsing = true;
|
|
460
|
+
}
|
|
461
|
+
} else if (isOverflowing) {
|
|
462
|
+
// We may already have an overflowIndicator here, if caller is Tabstrip
|
|
463
|
+
let renderedSize = visibleRef.current.reduce(
|
|
464
|
+
addAllExceptOverflowIndicator,
|
|
465
|
+
0
|
|
466
|
+
);
|
|
467
|
+
const result = markOverflowingItems(
|
|
468
|
+
renderedSize,
|
|
469
|
+
innerContainerSize - overflowIndicatorSizeRef.current
|
|
470
|
+
);
|
|
471
|
+
updateOverflowStatus(+result);
|
|
472
|
+
}
|
|
473
|
+
}, [
|
|
474
|
+
initializeDynamicContent,
|
|
475
|
+
markOverflowingItems,
|
|
476
|
+
orientation,
|
|
477
|
+
updateOverflowStatus,
|
|
478
|
+
]);
|
|
479
|
+
|
|
480
|
+
const resizeHandler = useCallback(
|
|
481
|
+
({
|
|
482
|
+
scrollHeight,
|
|
483
|
+
height = scrollHeight,
|
|
484
|
+
scrollWidth,
|
|
485
|
+
width = scrollWidth,
|
|
486
|
+
}) => {
|
|
487
|
+
const [size, depth] = horizontalRef.current
|
|
488
|
+
? [width, height]
|
|
489
|
+
: [height, width];
|
|
490
|
+
|
|
491
|
+
const wasFullSize = overflowingRef.current === 0;
|
|
492
|
+
const overflowDetected = depth > rootDepthRef.current;
|
|
493
|
+
const containerHasGrown = size > containerSizeRef.current;
|
|
494
|
+
|
|
495
|
+
containerSizeRef.current = size;
|
|
496
|
+
|
|
497
|
+
if (containerHasGrown && size === minSizeRef.current) {
|
|
498
|
+
// ignore
|
|
499
|
+
} else if (collapsingRef.current) {
|
|
500
|
+
checkDynamicContent(containerHasGrown);
|
|
501
|
+
} else if (!wasFullSize && containerHasGrown) {
|
|
502
|
+
const result = removeOverflowIfSpaceAllows(size);
|
|
503
|
+
// Don't remove the overflowing status if there are remaining overflowed item(s).
|
|
504
|
+
// Unlike collapsed items, overflowed is not a reference count.
|
|
505
|
+
if (result !== OVERFLOWING || overflowedRef.current.length === 0) {
|
|
506
|
+
updateOverflowStatus(-result);
|
|
507
|
+
} else if (result === OVERFLOWING) {
|
|
508
|
+
updateOverflowStatus(0, true);
|
|
509
|
+
}
|
|
510
|
+
} else if (wasFullSize && overflowDetected) {
|
|
511
|
+
// TODO if client is not using an overflow indicator, there is nothing to do here,
|
|
512
|
+
// just let nature take its course. How do we know this ?
|
|
513
|
+
// This is when we need to add width to measurements we are tracking
|
|
514
|
+
resetMeasurements();
|
|
515
|
+
} else if (!wasFullSize && overflowDetected) {
|
|
516
|
+
// we're still overflowing
|
|
517
|
+
let renderedSize = visibleRef.current.reduce(addAll, 0);
|
|
518
|
+
if (size < renderedSize) {
|
|
519
|
+
const result = markOverflowingItems(renderedSize, size);
|
|
520
|
+
updateOverflowStatus(+result);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
[
|
|
525
|
+
checkDynamicContent,
|
|
526
|
+
removeOverflowIfSpaceAllows,
|
|
527
|
+
resetMeasurements,
|
|
528
|
+
markOverflowingItems,
|
|
529
|
+
overflowingRef,
|
|
530
|
+
updateOverflowStatus,
|
|
531
|
+
]
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
useLayoutEffect(() => {
|
|
535
|
+
const dimension = horizontalRef.current ? "width" : "height";
|
|
536
|
+
if (newlyCollapsed(visibleRef.current)) {
|
|
537
|
+
// These are in reverse priority order, so last collapsed will always be first
|
|
538
|
+
const [collapsedItem] = visibleRef.current.filter(
|
|
539
|
+
(item) => item.collapsed
|
|
540
|
+
);
|
|
541
|
+
if (collapsedItem.fullSize === null) {
|
|
542
|
+
const target = getElementForItem(ref, collapsedItem);
|
|
543
|
+
if (target) {
|
|
544
|
+
const collapsedSize = measureMinimumNodeSize(target, dimension);
|
|
545
|
+
collapsedItem.fullSize = collapsedItem.size;
|
|
546
|
+
collapsedItem.size = collapsedSize;
|
|
547
|
+
// is the difference between collapsed size and original size enough ?
|
|
548
|
+
// TODO we repeat this code a lot, factoer it out
|
|
549
|
+
const renderedSize = visibleRef.current.reduce(addAll, 0);
|
|
550
|
+
if (renderedSize > containerSizeRef.current) {
|
|
551
|
+
const strategy = markOverflowingItems(
|
|
552
|
+
renderedSize,
|
|
553
|
+
containerSizeRef.current - overflowIndicatorSizeRef.current
|
|
554
|
+
);
|
|
555
|
+
updateOverflowStatus(+strategy);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
} else if (includesOverflow(overflowing)) {
|
|
560
|
+
const target = ref.current.querySelector(
|
|
561
|
+
`:scope > [data-overflow-indicator='true']`
|
|
562
|
+
);
|
|
563
|
+
if (target) {
|
|
564
|
+
const { index, priority = "1" } = target?.dataset ?? NO_DATA;
|
|
565
|
+
const item = {
|
|
566
|
+
index: parseInt(index, 10),
|
|
567
|
+
isOverflowIndicator: true,
|
|
568
|
+
priority: parseInt(priority, 10),
|
|
569
|
+
label: target.innerText,
|
|
570
|
+
size: measureMinimumNodeSize(target, dimension),
|
|
571
|
+
};
|
|
572
|
+
overflowIndicatorSizeRef.current = item.size;
|
|
573
|
+
visibleRef.current = visibleRef.current
|
|
574
|
+
.concat(item)
|
|
575
|
+
.sort(byDescendingPriority);
|
|
576
|
+
}
|
|
577
|
+
} else if (getOverflowIndicator(visibleRef)) {
|
|
578
|
+
visibleRef.current = visibleRef.current.filter(
|
|
579
|
+
(item) => !item.isOverflowIndicator
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
}, [
|
|
583
|
+
markOverflowingItems,
|
|
584
|
+
overflowing,
|
|
585
|
+
ref,
|
|
586
|
+
updateOverflowStatus,
|
|
587
|
+
visibleRef,
|
|
588
|
+
]);
|
|
589
|
+
|
|
590
|
+
// Measurement occurs post-render, by necessity, need to trigger a render
|
|
591
|
+
useLayoutEffect(() => {
|
|
592
|
+
async function measure() {
|
|
593
|
+
await document.fonts.ready;
|
|
594
|
+
if (ref.current !== null) {
|
|
595
|
+
resetMeasurements();
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (orientation !== "none") {
|
|
599
|
+
measure();
|
|
600
|
+
}
|
|
601
|
+
}, [label, orientation, resetMeasurements]);
|
|
602
|
+
|
|
603
|
+
useResizeObserver(ref, MONITORED_DIMENSIONS[orientation], resizeHandler);
|
|
604
|
+
|
|
605
|
+
return [ref, overflowedRef.current, collapsedRef.current, resetMeasurements];
|
|
606
|
+
}
|