@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.
Files changed (117) hide show
  1. package/README.md +1 -0
  2. package/package.json +30 -0
  3. package/src/Component.css +2 -0
  4. package/src/Component.tsx +20 -0
  5. package/src/DraggableLayout.css +18 -0
  6. package/src/DraggableLayout.tsx +29 -0
  7. package/src/__tests__/flexbox-utils.spec.js +90 -0
  8. package/src/action-buttons/action-buttons.css +12 -0
  9. package/src/action-buttons/action-buttons.tsx +30 -0
  10. package/src/action-buttons/index.ts +1 -0
  11. package/src/chest-of-drawers/Chest.css +36 -0
  12. package/src/chest-of-drawers/Chest.tsx +42 -0
  13. package/src/chest-of-drawers/Drawer.css +153 -0
  14. package/src/chest-of-drawers/Drawer.tsx +118 -0
  15. package/src/chest-of-drawers/index.ts +2 -0
  16. package/src/common-types.ts +9 -0
  17. package/src/debug.ts +16 -0
  18. package/src/dialog/Dialog.css +16 -0
  19. package/src/dialog/Dialog.tsx +59 -0
  20. package/src/dialog/index.ts +1 -0
  21. package/src/drag-drop/BoxModel.ts +546 -0
  22. package/src/drag-drop/DragState.ts +222 -0
  23. package/src/drag-drop/Draggable.ts +282 -0
  24. package/src/drag-drop/DropMenu.css +70 -0
  25. package/src/drag-drop/DropMenu.tsx +68 -0
  26. package/src/drag-drop/DropTarget.ts +392 -0
  27. package/src/drag-drop/DropTargetRenderer.css +40 -0
  28. package/src/drag-drop/DropTargetRenderer.tsx +284 -0
  29. package/src/drag-drop/dragDropTypes.ts +49 -0
  30. package/src/drag-drop/index.ts +4 -0
  31. package/src/editable-label/EditableLabel.css +28 -0
  32. package/src/editable-label/EditableLabel.tsx +99 -0
  33. package/src/editable-label/index.ts +1 -0
  34. package/src/flexbox/Flexbox.css +45 -0
  35. package/src/flexbox/Flexbox.tsx +70 -0
  36. package/src/flexbox/FlexboxLayout.jsx +26 -0
  37. package/src/flexbox/FluidGrid.css +134 -0
  38. package/src/flexbox/FluidGrid.tsx +84 -0
  39. package/src/flexbox/FluidGridLayout.tsx +10 -0
  40. package/src/flexbox/Splitter.css +140 -0
  41. package/src/flexbox/Splitter.tsx +135 -0
  42. package/src/flexbox/flexbox-utils.ts +128 -0
  43. package/src/flexbox/flexboxTypes.ts +63 -0
  44. package/src/flexbox/index.ts +4 -0
  45. package/src/flexbox/useResponsiveSizing.ts +85 -0
  46. package/src/flexbox/useSplitterResizing.ts +272 -0
  47. package/src/index.ts +20 -0
  48. package/src/layout-action.ts +21 -0
  49. package/src/layout-header/ActionButton.tsx +23 -0
  50. package/src/layout-header/Header.css +8 -0
  51. package/src/layout-header/Header.tsx +222 -0
  52. package/src/layout-header/index.ts +1 -0
  53. package/src/layout-provider/LayoutProvider.tsx +160 -0
  54. package/src/layout-provider/LayoutProviderContext.ts +17 -0
  55. package/src/layout-provider/index.ts +2 -0
  56. package/src/layout-provider/useLayoutDragDrop.ts +241 -0
  57. package/src/layout-reducer/flexUtils.ts +281 -0
  58. package/src/layout-reducer/index.ts +4 -0
  59. package/src/layout-reducer/insert-layout-element.ts +365 -0
  60. package/src/layout-reducer/layout-reducer.ts +255 -0
  61. package/src/layout-reducer/layoutTypes.ts +151 -0
  62. package/src/layout-reducer/layoutUtils.ts +302 -0
  63. package/src/layout-reducer/remove-layout-element.ts +240 -0
  64. package/src/layout-reducer/replace-layout-element.ts +118 -0
  65. package/src/layout-reducer/resize-flex-children.ts +56 -0
  66. package/src/layout-reducer/wrap-layout-element.ts +317 -0
  67. package/src/layout-view/View.css +58 -0
  68. package/src/layout-view/View.tsx +149 -0
  69. package/src/layout-view/ViewContext.ts +31 -0
  70. package/src/layout-view/index.ts +4 -0
  71. package/src/layout-view/useView.tsx +104 -0
  72. package/src/layout-view/useViewActionDispatcher.ts +133 -0
  73. package/src/layout-view/useViewResize.ts +53 -0
  74. package/src/layout-view/viewTypes.ts +37 -0
  75. package/src/palette/Palette.css +37 -0
  76. package/src/palette/Palette.tsx +140 -0
  77. package/src/palette/PaletteUitk.css +9 -0
  78. package/src/palette/PaletteUitk.tsx +79 -0
  79. package/src/palette/index.ts +2 -0
  80. package/src/placeholder/Placeholder.css +10 -0
  81. package/src/placeholder/Placeholder.tsx +39 -0
  82. package/src/placeholder/index.ts +1 -0
  83. package/src/registry/ComponentRegistry.ts +35 -0
  84. package/src/registry/index.ts +1 -0
  85. package/src/responsive/OverflowMenu.css +31 -0
  86. package/src/responsive/OverflowMenu.jsx +56 -0
  87. package/src/responsive/breakpoints.ts +48 -0
  88. package/src/responsive/index.ts +4 -0
  89. package/src/responsive/measureMinimumNodeSize.ts +23 -0
  90. package/src/responsive/overflowUtils.js +14 -0
  91. package/src/responsive/use-breakpoints.ts +100 -0
  92. package/src/responsive/useOverflowObserver.ts +606 -0
  93. package/src/responsive/useResizeObserver.ts +154 -0
  94. package/src/responsive/utils.ts +37 -0
  95. package/src/stack/Stack.css +39 -0
  96. package/src/stack/Stack.tsx +160 -0
  97. package/src/stack/StackLayout.tsx +137 -0
  98. package/src/stack/index.ts +3 -0
  99. package/src/stack/stackTypes.ts +19 -0
  100. package/src/tabs/TabPanel.css +12 -0
  101. package/src/tabs/TabPanel.tsx +17 -0
  102. package/src/tabs/index.ts +1 -0
  103. package/src/tools/config-wrapper/ConfigWrapper.jsx +53 -0
  104. package/src/tools/config-wrapper/index.js +1 -0
  105. package/src/tools/devtools-box/layout-configurator.css +112 -0
  106. package/src/tools/devtools-box/layout-configurator.jsx +369 -0
  107. package/src/tools/devtools-tree/layout-tree-viewer.css +15 -0
  108. package/src/tools/devtools-tree/layout-tree-viewer.jsx +36 -0
  109. package/src/tools/index.js +3 -0
  110. package/src/use-persistent-state.ts +115 -0
  111. package/src/utils/componentFromLayout.tsx +30 -0
  112. package/src/utils/index.ts +6 -0
  113. package/src/utils/pathUtils.ts +294 -0
  114. package/src/utils/propUtils.ts +24 -0
  115. package/src/utils/refUtils.ts +16 -0
  116. package/src/utils/styleUtils.ts +14 -0
  117. 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
+ }