@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.
- package/package.json +10 -13
- package/src/Component.css +0 -0
- package/src/Component.tsx +20 -0
- package/src/DraggableLayout.css +18 -0
- package/src/DraggableLayout.tsx +26 -0
- package/src/__tests__/flexbox-utils.spec.js +90 -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 +159 -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/drag-drop/BoxModel.ts +551 -0
- package/src/drag-drop/DragState.ts +219 -0
- package/src/drag-drop/Draggable.ts +282 -0
- package/src/drag-drop/DropMenu.css +71 -0
- package/src/drag-drop/DropMenu.tsx +61 -0
- package/src/drag-drop/DropTarget.ts +393 -0
- package/src/drag-drop/DropTargetRenderer.css +40 -0
- package/src/drag-drop/DropTargetRenderer.tsx +277 -0
- package/src/drag-drop/dragDropTypes.ts +47 -0
- package/src/drag-drop/index.ts +5 -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.tsx +28 -0
- package/src/flexbox/FluidGrid.css +134 -0
- package/src/flexbox/FluidGrid.tsx +82 -0
- package/src/flexbox/FluidGridLayout.tsx +9 -0
- package/src/flexbox/Splitter.css +140 -0
- package/src/flexbox/Splitter.tsx +127 -0
- package/src/flexbox/flexbox-utils.ts +128 -0
- package/src/flexbox/flexboxTypes.ts +68 -0
- package/src/flexbox/index.ts +5 -0
- package/src/flexbox/useResponsiveSizing.ts +82 -0
- package/src/flexbox/useSplitterResizing.ts +270 -0
- package/src/index.ts +19 -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 +216 -0
- package/src/layout-header/index.ts +1 -0
- package/src/layout-provider/LayoutProvider.tsx +161 -0
- package/src/layout-provider/LayoutProviderContext.ts +17 -0
- package/src/layout-provider/index.ts +3 -0
- package/src/layout-provider/useLayoutDragDrop.ts +210 -0
- package/src/layout-reducer/flexUtils.ts +276 -0
- package/src/layout-reducer/index.ts +5 -0
- package/src/layout-reducer/insert-layout-element.ts +365 -0
- package/src/layout-reducer/layout-reducer.ts +237 -0
- package/src/layout-reducer/layoutTypes.ts +159 -0
- package/src/layout-reducer/layoutUtils.ts +288 -0
- package/src/layout-reducer/remove-layout-element.ts +226 -0
- package/src/layout-reducer/replace-layout-element.ts +113 -0
- package/src/layout-reducer/resize-flex-children.ts +55 -0
- package/src/layout-reducer/wrap-layout-element.ts +307 -0
- package/src/layout-view/View.css +61 -0
- package/src/layout-view/View.tsx +143 -0
- package/src/layout-view/ViewContext.ts +30 -0
- package/src/layout-view/index.ts +5 -0
- package/src/layout-view/useView.tsx +104 -0
- package/src/layout-view/useViewActionDispatcher.ts +123 -0
- package/src/layout-view/useViewResize.ts +53 -0
- package/src/layout-view/viewTypes.ts +35 -0
- package/src/palette/Palette.css +33 -0
- package/src/palette/Palette.tsx +140 -0
- package/src/palette/PaletteSalt.css +9 -0
- package/src/palette/PaletteSalt.tsx +79 -0
- package/src/palette/index.ts +3 -0
- package/src/placeholder/Placeholder.css +10 -0
- package/src/placeholder/Placeholder.tsx +38 -0
- package/src/placeholder/index.ts +1 -0
- package/src/registry/ComponentRegistry.ts +44 -0
- package/src/registry/index.ts +1 -0
- package/src/responsive/breakpoints.ts +62 -0
- package/src/responsive/index.ts +3 -0
- package/src/responsive/measureMinimumNodeSize.ts +23 -0
- package/src/responsive/overflowUtils.js +14 -0
- package/src/responsive/use-breakpoints.ts +101 -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 +173 -0
- package/src/stack/StackLayout.tsx +119 -0
- package/src/stack/index.ts +4 -0
- package/src/stack/stackTypes.ts +22 -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.tsx +55 -0
- package/src/tools/config-wrapper/index.ts +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.ts +4 -0
- package/src/use-persistent-state.ts +112 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/pathUtils.ts +283 -0
- package/src/utils/propUtils.ts +26 -0
- package/src/utils/refUtils.ts +16 -0
- package/src/utils/styleUtils.ts +13 -0
- package/src/utils/typeOf.ts +25 -0
- package/tsconfig-emit-types.json +11 -0
- package/LICENSE +0 -201
- package/cjs/index.js +0 -20
- package/cjs/index.js.map +0 -7
- package/esm/index.js +0 -20
- package/esm/index.js.map +0 -7
- package/index.css +0 -2
- 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,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,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,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
|
+
}
|