@vuu-ui/vuu-layout 0.5.14 → 0.5.15

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 (114) hide show
  1. package/package.json +10 -13
  2. package/src/Component.css +0 -0
  3. package/src/Component.tsx +20 -0
  4. package/src/DraggableLayout.css +18 -0
  5. package/src/DraggableLayout.tsx +26 -0
  6. package/src/__tests__/flexbox-utils.spec.js +90 -0
  7. package/src/chest-of-drawers/Chest.css +36 -0
  8. package/src/chest-of-drawers/Chest.tsx +42 -0
  9. package/src/chest-of-drawers/Drawer.css +159 -0
  10. package/src/chest-of-drawers/Drawer.tsx +118 -0
  11. package/src/chest-of-drawers/index.ts +2 -0
  12. package/src/common-types.ts +9 -0
  13. package/src/debug.ts +16 -0
  14. package/src/drag-drop/BoxModel.ts +551 -0
  15. package/src/drag-drop/DragState.ts +219 -0
  16. package/src/drag-drop/Draggable.ts +282 -0
  17. package/src/drag-drop/DropMenu.css +71 -0
  18. package/src/drag-drop/DropMenu.tsx +61 -0
  19. package/src/drag-drop/DropTarget.ts +393 -0
  20. package/src/drag-drop/DropTargetRenderer.css +40 -0
  21. package/src/drag-drop/DropTargetRenderer.tsx +277 -0
  22. package/src/drag-drop/dragDropTypes.ts +47 -0
  23. package/src/drag-drop/index.ts +5 -0
  24. package/src/editable-label/EditableLabel.css +28 -0
  25. package/src/editable-label/EditableLabel.tsx +99 -0
  26. package/src/editable-label/index.ts +1 -0
  27. package/src/flexbox/Flexbox.css +45 -0
  28. package/src/flexbox/Flexbox.tsx +70 -0
  29. package/src/flexbox/FlexboxLayout.tsx +28 -0
  30. package/src/flexbox/FluidGrid.css +134 -0
  31. package/src/flexbox/FluidGrid.tsx +82 -0
  32. package/src/flexbox/FluidGridLayout.tsx +9 -0
  33. package/src/flexbox/Splitter.css +140 -0
  34. package/src/flexbox/Splitter.tsx +127 -0
  35. package/src/flexbox/flexbox-utils.ts +128 -0
  36. package/src/flexbox/flexboxTypes.ts +68 -0
  37. package/src/flexbox/index.ts +5 -0
  38. package/src/flexbox/useResponsiveSizing.ts +82 -0
  39. package/src/flexbox/useSplitterResizing.ts +270 -0
  40. package/src/index.ts +19 -0
  41. package/src/layout-action.ts +21 -0
  42. package/src/layout-header/ActionButton.tsx +23 -0
  43. package/src/layout-header/Header.css +8 -0
  44. package/src/layout-header/Header.tsx +216 -0
  45. package/src/layout-header/index.ts +1 -0
  46. package/src/layout-provider/LayoutProvider.tsx +161 -0
  47. package/src/layout-provider/LayoutProviderContext.ts +17 -0
  48. package/src/layout-provider/index.ts +3 -0
  49. package/src/layout-provider/useLayoutDragDrop.ts +210 -0
  50. package/src/layout-reducer/flexUtils.ts +276 -0
  51. package/src/layout-reducer/index.ts +5 -0
  52. package/src/layout-reducer/insert-layout-element.ts +365 -0
  53. package/src/layout-reducer/layout-reducer.ts +237 -0
  54. package/src/layout-reducer/layoutTypes.ts +159 -0
  55. package/src/layout-reducer/layoutUtils.ts +288 -0
  56. package/src/layout-reducer/remove-layout-element.ts +226 -0
  57. package/src/layout-reducer/replace-layout-element.ts +113 -0
  58. package/src/layout-reducer/resize-flex-children.ts +55 -0
  59. package/src/layout-reducer/wrap-layout-element.ts +307 -0
  60. package/src/layout-view/View.css +61 -0
  61. package/src/layout-view/View.tsx +143 -0
  62. package/src/layout-view/ViewContext.ts +30 -0
  63. package/src/layout-view/index.ts +5 -0
  64. package/src/layout-view/useView.tsx +104 -0
  65. package/src/layout-view/useViewActionDispatcher.ts +123 -0
  66. package/src/layout-view/useViewResize.ts +53 -0
  67. package/src/layout-view/viewTypes.ts +35 -0
  68. package/src/palette/Palette.css +33 -0
  69. package/src/palette/Palette.tsx +140 -0
  70. package/src/palette/PaletteSalt.css +9 -0
  71. package/src/palette/PaletteSalt.tsx +79 -0
  72. package/src/palette/index.ts +3 -0
  73. package/src/placeholder/Placeholder.css +10 -0
  74. package/src/placeholder/Placeholder.tsx +38 -0
  75. package/src/placeholder/index.ts +1 -0
  76. package/src/registry/ComponentRegistry.ts +44 -0
  77. package/src/registry/index.ts +1 -0
  78. package/src/responsive/breakpoints.ts +62 -0
  79. package/src/responsive/index.ts +3 -0
  80. package/src/responsive/measureMinimumNodeSize.ts +23 -0
  81. package/src/responsive/overflowUtils.js +14 -0
  82. package/src/responsive/use-breakpoints.ts +101 -0
  83. package/src/responsive/useResizeObserver.ts +154 -0
  84. package/src/responsive/utils.ts +37 -0
  85. package/src/stack/Stack.css +39 -0
  86. package/src/stack/Stack.tsx +173 -0
  87. package/src/stack/StackLayout.tsx +119 -0
  88. package/src/stack/index.ts +4 -0
  89. package/src/stack/stackTypes.ts +22 -0
  90. package/src/tabs/TabPanel.css +12 -0
  91. package/src/tabs/TabPanel.tsx +17 -0
  92. package/src/tabs/index.ts +1 -0
  93. package/src/tools/config-wrapper/ConfigWrapper.tsx +55 -0
  94. package/src/tools/config-wrapper/index.ts +1 -0
  95. package/src/tools/devtools-box/layout-configurator.css +112 -0
  96. package/src/tools/devtools-box/layout-configurator.jsx +369 -0
  97. package/src/tools/devtools-tree/layout-tree-viewer.css +15 -0
  98. package/src/tools/devtools-tree/layout-tree-viewer.jsx +36 -0
  99. package/src/tools/index.ts +4 -0
  100. package/src/use-persistent-state.ts +112 -0
  101. package/src/utils/index.ts +5 -0
  102. package/src/utils/pathUtils.ts +283 -0
  103. package/src/utils/propUtils.ts +26 -0
  104. package/src/utils/refUtils.ts +16 -0
  105. package/src/utils/styleUtils.ts +13 -0
  106. package/src/utils/typeOf.ts +25 -0
  107. package/tsconfig-emit-types.json +11 -0
  108. package/LICENSE +0 -201
  109. package/cjs/index.js +0 -20
  110. package/cjs/index.js.map +0 -7
  111. package/esm/index.js +0 -20
  112. package/esm/index.js.map +0 -7
  113. package/index.css +0 -2
  114. package/index.css.map +0 -7
@@ -0,0 +1,140 @@
1
+ import { uuid } from "@vuu-ui/vuu-utils";
2
+ import { List, ListItem, ListItemProps } from "@heswell/salt-lab";
3
+ import cx from "classnames";
4
+ import {
5
+ cloneElement,
6
+ HTMLAttributes,
7
+ memo,
8
+ MouseEvent,
9
+ ReactElement,
10
+ } from "react";
11
+ import { useLayoutProviderDispatch } from "../layout-provider";
12
+ import { View } from "../layout-view";
13
+ import { registerComponent } from "../registry/ComponentRegistry";
14
+
15
+ import "./Palette.css";
16
+
17
+ const clonePaletteItem = (paletteItem: HTMLElement) => {
18
+ const dolly = paletteItem.cloneNode(true) as HTMLElement;
19
+ dolly.id = "";
20
+ delete dolly.dataset.idx;
21
+ return dolly;
22
+ };
23
+
24
+ export interface PaletteItemProps extends ListItemProps {
25
+ children: ReactElement;
26
+ closeable?: boolean;
27
+ header?: boolean;
28
+ idx?: number;
29
+ resize?: "defer";
30
+ resizeable?: boolean;
31
+ }
32
+
33
+ export const PaletteItem = memo(
34
+ ({
35
+ className,
36
+ children: component,
37
+ idx,
38
+ resizeable,
39
+ header,
40
+ closeable,
41
+ ...props
42
+ }: PaletteItemProps) => {
43
+ return (
44
+ <ListItem
45
+ className={cx("vuuPaletteItem", className)}
46
+ data-draggable
47
+ {...props}
48
+ />
49
+ );
50
+ }
51
+ );
52
+
53
+ PaletteItem.displayName = "PaletteItem";
54
+
55
+ export interface PaletteProps
56
+ extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
57
+ children: ReactElement[];
58
+ orientation: "horizontal" | "vertical";
59
+ selection?: string;
60
+ }
61
+
62
+ export const Palette = ({
63
+ children,
64
+ className,
65
+ orientation = "horizontal",
66
+ ...props
67
+ }: PaletteProps) => {
68
+ const dispatch = useLayoutProviderDispatch();
69
+ const classBase = "vuuPalette";
70
+
71
+ function handleMouseDown(evt: MouseEvent) {
72
+ const target = evt.target as HTMLElement;
73
+ const listItemElement = target.closest(".vuuPaletteItem") as HTMLElement;
74
+ const idx = parseInt(listItemElement.dataset.idx ?? "-1");
75
+ if (idx !== -1) {
76
+ console.log({
77
+ children,
78
+ idx,
79
+ listItemElement,
80
+ });
81
+ }
82
+ const {
83
+ props: { caption, children: payload, template, ...props },
84
+ } = children[idx];
85
+ const { height, left, top, width } =
86
+ listItemElement.getBoundingClientRect();
87
+ const id = uuid();
88
+ const identifiers = { id, key: id };
89
+ const component = template ? (
90
+ payload
91
+ ) : (
92
+ <View {...identifiers} {...props} title={props.label}>
93
+ {payload}
94
+ </View>
95
+ );
96
+
97
+ dispatch({
98
+ dragRect: {
99
+ left,
100
+ top,
101
+ right: left + width,
102
+ bottom: top + 150,
103
+ width,
104
+ height,
105
+ },
106
+ dragElement: clonePaletteItem(listItemElement),
107
+ evt: evt.nativeEvent,
108
+ instructions: {
109
+ DoNotRemove: true,
110
+ DoNotTransform: true,
111
+ RemoveDraggableOnDragEnd: true,
112
+ dragThreshold: 10,
113
+ },
114
+ path: "*",
115
+ payload: component,
116
+ type: "drag-start",
117
+ });
118
+ }
119
+
120
+ return (
121
+ <List
122
+ {...props}
123
+ borderless
124
+ className={cx(classBase, className, `${classBase}-${orientation}`)}
125
+ maxHeight={800}
126
+ selected={null}
127
+ >
128
+ {children.map((child, idx) =>
129
+ child.type === PaletteItem
130
+ ? cloneElement(child, {
131
+ key: idx,
132
+ onMouseDown: handleMouseDown,
133
+ })
134
+ : child
135
+ )}
136
+ </List>
137
+ );
138
+ };
139
+
140
+ registerComponent("Palette", Palette, "view");
@@ -0,0 +1,9 @@
1
+ .vuuPalette {
2
+ --list-item-header-bg: inherit;
3
+ --list-item-header-color: inherit;
4
+ --list-item-padding: 0 6px 0 24px;
5
+ --list-item-header-twisty-color: black;
6
+ --list-item-header-twisty-left: 3px;
7
+ --list-item-header-twisty-right: auto;
8
+ }
9
+
@@ -0,0 +1,79 @@
1
+ import { uuid } from "@vuu-ui/vuu-utils";
2
+ import { List, ListItem, ListItemProps, ListProps } from "@heswell/salt-lab";
3
+ import cx from "classnames";
4
+ import { MouseEvent, ReactElement } from "react";
5
+ import { useLayoutProviderDispatch } from "../layout-provider";
6
+ import { View } from "../layout-view";
7
+ import { registerComponent } from "../registry/ComponentRegistry";
8
+
9
+ import "./PaletteSalt.css";
10
+
11
+ const classBase = "vuuPalette";
12
+
13
+ export interface PaletteListItemProps extends ListItemProps {
14
+ children: ReactElement;
15
+ ViewProps: {
16
+ header?: boolean;
17
+ closeable?: boolean;
18
+ resizeable?: boolean;
19
+ };
20
+ template: boolean;
21
+ }
22
+
23
+ export const PaletteListItem = (props: PaletteListItemProps) => {
24
+ const { children, ViewProps, label, onMouseDown, template, ...restProps } =
25
+ props;
26
+ const dispatch = useLayoutProviderDispatch();
27
+
28
+ const handleMouseDown = (evt: MouseEvent<HTMLDivElement>) => {
29
+ const { left, top, width } = evt.currentTarget.getBoundingClientRect();
30
+ const id = uuid();
31
+ const identifiers = { id, key: id };
32
+ const component = template ? (
33
+ children
34
+ ) : (
35
+ <View {...identifiers} {...ViewProps} title={props.label}>
36
+ {children}
37
+ </View>
38
+ );
39
+
40
+ dispatch({
41
+ type: "drag-start",
42
+ evt: evt.nativeEvent,
43
+ path: "*",
44
+ payload: component,
45
+ instructions: {
46
+ DoNotRemove: true,
47
+ DoNotTransform: true,
48
+ RemoveDraggableOnDragEnd: true,
49
+ dragThreshold: 10,
50
+ },
51
+ dragRect: {
52
+ left,
53
+ top,
54
+ right: left + width,
55
+ bottom: top + 150,
56
+ width,
57
+ height: 100,
58
+ },
59
+ });
60
+ };
61
+ return (
62
+ <ListItem onMouseDown={handleMouseDown} {...restProps}>
63
+ {label}
64
+ </ListItem>
65
+ );
66
+ };
67
+
68
+ export const PaletteSalt = ({ className, ...props }: ListProps) => {
69
+ return (
70
+ <List
71
+ {...props}
72
+ className={cx(classBase, className)}
73
+ height="100%"
74
+ selectionStrategy="none"
75
+ />
76
+ );
77
+ };
78
+
79
+ registerComponent("PaletteSalt", PaletteSalt, "view");
@@ -0,0 +1,3 @@
1
+ export * from "./Palette";
2
+ export * from "./PaletteSalt";
3
+
@@ -0,0 +1,10 @@
1
+ .vuuPlaceholder {
2
+ flex-basis: 0;
3
+ flex-grow: 1;
4
+ flex-shrink: 1;
5
+ }
6
+
7
+ .vuuPlaceholder-shim {
8
+ flex-grow: 0;
9
+ flex-shrink: 0;
10
+ }
@@ -0,0 +1,38 @@
1
+ import cx from "classnames";
2
+ import { HTMLAttributes } from "react";
3
+ import { registerComponent } from "../registry/ComponentRegistry";
4
+
5
+ import "./Placeholder.css";
6
+
7
+ const classBase = "vuuPlaceholder";
8
+
9
+ export interface PlaceholderProps extends HTMLAttributes<HTMLDivElement> {
10
+ closeable?: boolean;
11
+ flexFill?: boolean;
12
+ resizeable?: boolean;
13
+ shim?: boolean;
14
+ }
15
+
16
+ export const Placeholder = ({
17
+ className,
18
+ closeable,
19
+ flexFill,
20
+ resizeable,
21
+ shim,
22
+ ...props
23
+ }: PlaceholderProps) => {
24
+ return (
25
+ <div
26
+ className={cx(classBase, className, {
27
+ [`${classBase}-shim`]: shim,
28
+ })}
29
+ {...props}
30
+ data-placeholder
31
+ data-resizeable
32
+ >
33
+ </div>
34
+ );
35
+ };
36
+
37
+ Placeholder.displayName = "Placeholder";
38
+ registerComponent("Placeholder", Placeholder);
@@ -0,0 +1 @@
1
+ export * from './Placeholder';
@@ -0,0 +1,44 @@
1
+ import { FunctionComponent } from "react";
2
+
3
+ const _containers: { [key: string]: boolean } = {};
4
+ const _views: { [key: string]: boolean } = {};
5
+
6
+ export type layoutComponentType = "component" | "container" | "view";
7
+
8
+ export interface ComponentWithId {
9
+ id: string;
10
+ [key: string]: unknown;
11
+ }
12
+
13
+ export const ComponentRegistry: {
14
+ [key: string]: FunctionComponent<ComponentWithId>;
15
+ } = {};
16
+
17
+ export function isContainer(componentType: string) {
18
+ return _containers[componentType] === true;
19
+ }
20
+
21
+ export function isView(componentType: string) {
22
+ return _views[componentType] === true;
23
+ }
24
+
25
+ export const isLayoutComponent = (type: string) =>
26
+ isContainer(type) || isView(type);
27
+
28
+ export const isRegistered = (className: string) =>
29
+ !!ComponentRegistry[className];
30
+
31
+ export function registerComponent(
32
+ componentName: string,
33
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
34
+ component: FunctionComponent<any>,
35
+ type: layoutComponentType = "component"
36
+ ) {
37
+ ComponentRegistry[componentName] = component;
38
+
39
+ if (type === "container") {
40
+ _containers[componentName] = true;
41
+ } else if (type === "view") {
42
+ _views[componentName] = true;
43
+ }
44
+ }
@@ -0,0 +1 @@
1
+ export * from './ComponentRegistry';
@@ -0,0 +1,62 @@
1
+ // should we have some global; defaults ?
2
+
3
+ import { BreakPointsProp } from "../flexbox/flexboxTypes";
4
+
5
+ export type BreakPointRamp = [string, number, number];
6
+
7
+ function breakpointReader(
8
+ themeName: string,
9
+ defaultBreakpoints?: BreakPointsProp
10
+ ) {
11
+ //TODO ownerDocument
12
+ const themeRoot = document.body.querySelector(`.${themeName}`);
13
+ const handler = {
14
+ get: function (style: CSSStyleDeclaration, stopName: string) {
15
+ const val = style.getPropertyValue(
16
+ // lets assume we have the following naming convention
17
+ `--${themeName}-breakpoint-${stopName}`
18
+ );
19
+ return val ? parseInt(val) : undefined;
20
+ },
21
+ };
22
+
23
+ return themeRoot
24
+ ? new Proxy(getComputedStyle(themeRoot), handler)
25
+ : defaultBreakpoints ?? {};
26
+ }
27
+
28
+ const byDescendingStopSize = (
29
+ [, s1]: [string, number],
30
+ [, s2]: [string, number]
31
+ ) => s2 - s1;
32
+
33
+ // These are assumed to be min-width (aka mobile-first) stops, we could take a
34
+ // paramneter to support max-width as well ?
35
+ // return [stopName, minWidth, maxWidth]
36
+ export const breakpointRamp = (
37
+ breakpoints: BreakPointsProp
38
+ ): BreakPointRamp[] =>
39
+ Object.entries(breakpoints)
40
+ .sort(byDescendingStopSize)
41
+ .map(([name, value], i, all) => [
42
+ name,
43
+ value,
44
+ i < all.length - 1 ? all[i + 1][1] : 9999,
45
+ ]);
46
+
47
+ let documentBreakpoints: BreakPointRamp[] | null = null;
48
+
49
+ const loadBreakpoints = (themeName = "salt") => {
50
+ // TODO would be nice to read these breakpoint labels from a css variable to
51
+ // avoid hard-coding them here ?
52
+ const { xs, sm, md, lg, xl } = breakpointReader(themeName) as BreakPointsProp;
53
+ return breakpointRamp({ xs, sm, md, lg, xl });
54
+ };
55
+
56
+ //TODO support multiple themes loaded
57
+ export const getBreakPoints = (themeName?: string) => {
58
+ if (documentBreakpoints === null) {
59
+ documentBreakpoints = loadBreakpoints(themeName);
60
+ }
61
+ return documentBreakpoints;
62
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./use-breakpoints";
2
+ export * from "./useResizeObserver";
3
+ export * from "./utils";
@@ -0,0 +1,23 @@
1
+ const LEFT_RIGHT = ['left', 'right'];
2
+ const TOP_BOTTOM = ['top', 'bottom'];
3
+
4
+ export function measureMinimumNodeSize(node: HTMLElement, dimension: 'width' | 'height' = 'width') {
5
+ const { [dimension]: size } = node.getBoundingClientRect();
6
+ const { padRight = false, padLeft = false } = node.dataset;
7
+ const style = getComputedStyle(node);
8
+ const [start, end] = dimension === 'width' ? LEFT_RIGHT : TOP_BOTTOM;
9
+ const marginStart = padLeft ? 0 : parseInt(style.getPropertyValue(`margin-${start}`), 10);
10
+ const marginEnd = padRight ? 0 : parseInt(style.getPropertyValue(`margin-${end}`), 10);
11
+
12
+ let minWidth = size;
13
+ const flexShrink = parseInt(style.getPropertyValue('flex-shrink'), 10);
14
+ if (flexShrink > 0) {
15
+ const flexBasis = parseInt(style.getPropertyValue('flex-basis'), 10);
16
+ // TODO what about percentage values ?
17
+ if (!isNaN(flexBasis)) {
18
+ minWidth = flexBasis;
19
+ }
20
+ }
21
+
22
+ return marginStart + minWidth + marginEnd;
23
+ }
@@ -0,0 +1,14 @@
1
+ export const getOverflowedItems = (containerRef, height = 64) => {
2
+ const elements = Array.from(containerRef.current.childNodes);
3
+ const firstOverflowIdx = findFirstOverflow(elements, height);
4
+ return [elements.slice(0, firstOverflowIdx), elements.slice(firstOverflowIdx)];
5
+ };
6
+
7
+ export const findFirstOverflow = (elements, height) => {
8
+ for (let i = 0; i < elements.length; i++) {
9
+ if (elements[i].offsetTop >= height) {
10
+ return i;
11
+ }
12
+ }
13
+ return -1;
14
+ };
@@ -0,0 +1,101 @@
1
+ import { RefObject, useCallback, useEffect, useRef, useState } from "react";
2
+ import { useResizeObserver } from "./useResizeObserver";
3
+ import {
4
+ BreakPointRamp,
5
+ breakpointRamp,
6
+ getBreakPoints as getDocumentBreakpoints,
7
+ } from "./breakpoints";
8
+ import { BreakPoint, BreakPointsProp } from "../flexbox/flexboxTypes";
9
+
10
+ const EMPTY_ARRAY: BreakPoint[] = [];
11
+
12
+ export interface BreakpointsHookProps {
13
+ breakPoints?: BreakPointsProp;
14
+ smallerThan?: string;
15
+ }
16
+
17
+ // TODO how do we cater for smallerThan/greaterThan breakpoints
18
+ export const useBreakpoints = (
19
+ { breakPoints: breakPointsProp, smallerThan }: BreakpointsHookProps,
20
+ ref: RefObject<HTMLElement>
21
+ ) => {
22
+ const [breakpointMatch, setBreakpointmatch] = useState(
23
+ smallerThan ? false : "lg"
24
+ );
25
+ const bodyRef = useRef(document.body);
26
+ const breakPointsRef = useRef<BreakPointRamp[]>(
27
+ breakPointsProp ? breakpointRamp(breakPointsProp) : getDocumentBreakpoints()
28
+ );
29
+
30
+ // TODO how do we identify the default
31
+ const sizeRef = useRef("lg");
32
+
33
+ const stopFromMinWidth = useCallback(
34
+ (w) => {
35
+ if (breakPointsRef.current) {
36
+ for (const [name, size] of breakPointsRef.current) {
37
+ if (w >= size) {
38
+ return name;
39
+ }
40
+ }
41
+ }
42
+ },
43
+ [breakPointsRef]
44
+ );
45
+
46
+ const matchSizeAgainstBreakpoints = useCallback(
47
+ (width) => {
48
+ if (smallerThan) {
49
+ const breakPointRamp = breakPointsRef.current.find(
50
+ ([name]: BreakPointRamp) => name === smallerThan
51
+ );
52
+ if (breakPointRamp) {
53
+ const [, , maxValue] = breakPointRamp;
54
+ return width < maxValue;
55
+ }
56
+ } else {
57
+ return stopFromMinWidth(width);
58
+ }
59
+ // is this right ?
60
+ return width;
61
+ },
62
+ [smallerThan, stopFromMinWidth]
63
+ );
64
+
65
+ // TODO need to make the dimension a config
66
+ useResizeObserver(
67
+ ref || bodyRef,
68
+ breakPointsRef.current ? ["width"] : EMPTY_ARRAY,
69
+ ({ width: measuredWidth }: { width?: number }) => {
70
+ const result = matchSizeAgainstBreakpoints(measuredWidth);
71
+ if (result !== sizeRef.current) {
72
+ sizeRef.current = result;
73
+ setBreakpointmatch(result);
74
+ }
75
+ },
76
+ true
77
+ );
78
+
79
+ useEffect(() => {
80
+ const target = ref || bodyRef;
81
+ if (target.current) {
82
+ const prevSize = sizeRef.current;
83
+ if (breakPointsRef.current) {
84
+ // We're measuring here when the resizeObserver has also measured
85
+ // There isn't a convenient way to get the Resizeobserver to
86
+ // notify initial size - that's not really its job, unless we
87
+ // set a flag ?
88
+ const { clientWidth } = target.current;
89
+ const result = matchSizeAgainstBreakpoints(clientWidth);
90
+ sizeRef.current = result;
91
+ // If initial size of ref does not match the default, notify client after render
92
+ if (result !== prevSize) {
93
+ setBreakpointmatch(result);
94
+ }
95
+ }
96
+ }
97
+ }, [setBreakpointmatch, matchSizeAgainstBreakpoints, ref]);
98
+
99
+ // No, just ass the class directly to the ref, no need to render
100
+ return breakpointMatch;
101
+ };
@@ -0,0 +1,154 @@
1
+ /* eslint-disable no-restricted-syntax */
2
+ import { useCallback, useLayoutEffect, useRef, RefObject } from 'react';
3
+ export const WidthHeight = ['height', 'width'];
4
+ export const HeightOnly = ['height'];
5
+ export const WidthOnly = ['width'];
6
+
7
+ export type measurements<T = string | number> = {
8
+ height?: T;
9
+ scrollHeight?: T;
10
+ scrollWidth?: T;
11
+ width?: T;
12
+ };
13
+ type measuredDimension = keyof measurements<number>;
14
+
15
+ export type ResizeHandler = (measurements: measurements<number>) => void;
16
+
17
+ type observedDetails = {
18
+ onResize?: ResizeHandler;
19
+ measurements: measurements<number>;
20
+ };
21
+ const observedMap = new WeakMap<HTMLElement, observedDetails>();
22
+
23
+ const getTargetSize = (
24
+ element: HTMLElement,
25
+ contentRect: DOMRectReadOnly,
26
+ dimension: measuredDimension
27
+ ): number => {
28
+ switch (dimension) {
29
+ case 'height':
30
+ return contentRect.height;
31
+ case 'scrollHeight':
32
+ return element.scrollHeight;
33
+ case 'scrollWidth':
34
+ return element.scrollWidth;
35
+ case 'width':
36
+ return contentRect.width;
37
+ default:
38
+ return 0;
39
+ }
40
+ };
41
+
42
+ const resizeObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => {
43
+ for (const entry of entries) {
44
+ const { target, contentRect } = entry;
45
+ const observedTarget = observedMap.get(target as HTMLElement);
46
+ if (observedTarget) {
47
+ const { onResize, measurements } = observedTarget;
48
+ let sizeChanged = false;
49
+ for (const [dimension, size] of Object.entries(measurements)) {
50
+ const newSize = getTargetSize(
51
+ target as HTMLElement,
52
+ contentRect,
53
+ dimension as measuredDimension
54
+ );
55
+ if (newSize !== size) {
56
+ sizeChanged = true;
57
+ measurements[dimension as measuredDimension] = newSize;
58
+ }
59
+ }
60
+ if (sizeChanged) {
61
+ onResize && onResize(measurements);
62
+ }
63
+ }
64
+ }
65
+ });
66
+
67
+ // TODO use an optional lag (default to false) to ask to fire onResize
68
+ // with initial size
69
+ // Note asking for scrollHeight alone will not trigger onResize, this is only triggered by height,
70
+ // with scrollHeight returned as an auxilliary value
71
+ export function useResizeObserver(
72
+ ref: RefObject<Element | HTMLElement | null>,
73
+ dimensions: string[],
74
+ onResize: ResizeHandler,
75
+ reportInitialSize = false
76
+ ): void {
77
+ const dimensionsRef = useRef(dimensions);
78
+ const measure = useCallback((target: HTMLElement): measurements<number> => {
79
+ const rect = target.getBoundingClientRect();
80
+ return dimensionsRef.current.reduce((map: { [key: string]: number }, dim) => {
81
+ map[dim] = getTargetSize(target, rect, dim as measuredDimension);
82
+ return map;
83
+ }, {});
84
+ }, []);
85
+
86
+ // TODO use ref to store resizeHandler here
87
+ // resize handler registered with REsizeObserver will never change
88
+ // use ref to store user onResize callback here
89
+ // resizeHandler will call user callback.current
90
+
91
+ // Keep this effect separate in case user inadvertently passes different
92
+ // dimensions or callback instance each time - we only ever want to
93
+ // initiate new observation when ref changes.
94
+ useLayoutEffect(() => {
95
+ const target = ref.current as HTMLElement;
96
+ let cleanedUp = false;
97
+
98
+ async function registerObserver() {
99
+ // Create the map entry immediately. useEffect may fire below
100
+ // before fonts are ready and attempt to update entry
101
+ observedMap.set(target, { measurements: {} as measurements<number> });
102
+ cleanedUp = false;
103
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
104
+ const { fonts } = document as any;
105
+ if (fonts) {
106
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
107
+ await fonts.ready;
108
+ }
109
+ if (!cleanedUp) {
110
+ const observedTarget = observedMap.get(target);
111
+ if (observedTarget) {
112
+ const measurements = measure(target);
113
+ observedTarget.measurements = measurements;
114
+ resizeObserver.observe(target);
115
+ if (reportInitialSize) {
116
+ onResize(measurements);
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ if (target) {
123
+ // TODO might we want multiple callers to attach a listener to the same element ?
124
+ if (observedMap.has(target)) {
125
+ throw Error('useResizeObserver attemping to observe same element twice');
126
+ }
127
+ void registerObserver();
128
+ }
129
+ return () => {
130
+ if (target && observedMap.has(target)) {
131
+ resizeObserver.unobserve(target);
132
+ observedMap.delete(target);
133
+ cleanedUp = true;
134
+ }
135
+ };
136
+ }, [ref, measure]);
137
+
138
+ useLayoutEffect(() => {
139
+ const target = ref.current as HTMLElement;
140
+ const record = observedMap.get(target);
141
+ if (record) {
142
+ if (dimensionsRef.current !== dimensions) {
143
+ dimensionsRef.current = dimensions;
144
+ const measurements = measure(target);
145
+ record.measurements = measurements;
146
+ }
147
+ // Might not have changed, but no harm ...
148
+ record.onResize = onResize;
149
+ }
150
+ }, [dimensions, measure, ref, onResize]);
151
+
152
+ // TODO might be a good idea to ref and return the current measurememnts. That way, derived hooks
153
+ // e.g useBreakpoints don't have to measure and client cn make onResize callback simpler
154
+ }