Skip to content

Commit ddb362b

Browse files
committed
Share app position/dimensions between various hooks that modify it
1 parent 236c3d0 commit ddb362b

5 files changed

Lines changed: 138 additions & 85 deletions

File tree

src/components/BorderedApp/BorderedApp.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import React, { useRef } from "react";
2-
import useDragToMove from "../../hooks/useDragToMove";
32
import useWindowManagerStore from "../../stores/windowManagerStore";
43

54
import "./BorderedApp.scss";
6-
import useDragToResize, { Dimensions } from "../../hooks/useDragToResize";
5+
import { Dimensions } from "../../hooks/useDragToResize";
76
import BorderedAppMenu, {
87
BorderedAppMenuItemProps,
98
} from "./BorderedAppMenu/BorderedAppMenu";
10-
import useWindowMinMax from "../../hooks/useWindowMinMax";
9+
import usePositionableElement from "../../hooks/usePositionableElement";
1110

1211
interface BorderedAppProps {
1312
title: string;
@@ -45,7 +44,7 @@ function BorderedApp({
4544
const resizeSERef = useRef<HTMLDivElement>(null);
4645
const resizeSWRef = useRef<HTMLDivElement>(null);
4746

48-
useDragToResize({
47+
const { clickToMoveRef, maximize, minimize } = usePositionableElement({
4948
elementRef: appRef,
5049
resizeERef,
5150
resizeNRef,
@@ -58,12 +57,6 @@ function BorderedApp({
5857
minDimensions,
5958
});
6059

61-
const [clickRef] = useDragToMove({
62-
moveRef: appRef,
63-
});
64-
65-
const { minimize, maximize } = useWindowMinMax(appRef);
66-
6760
function onClickClose() {
6861
winMan.closeWindow(type, id);
6962
}
@@ -83,7 +76,7 @@ function BorderedApp({
8376
<div className="bordered-app__edge-e" ref={resizeERef} />
8477
<div
8578
className="bordered-app__title-bar"
86-
ref={clickRef}
79+
ref={clickToMoveRef}
8780
onDoubleClick={maximize}
8881
>
8982
<div className="bordered-app__window-menus-wrapper">

src/hooks/useDragToMove.ts

Lines changed: 17 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import React, {
2+
MutableRefObject,
23
RefCallback,
34
useCallback,
45
useEffect,
56
useRef,
67
useState,
78
} from "react";
9+
import { Rect } from "./useDragToResize";
810

911
// https://stackoverflow.com/a/39192992/6236042
1012

@@ -16,52 +18,28 @@ function hasStyle(value: unknown): value is StyledElement {
1618
return !!value && typeof value === "object" && "style" in value;
1719
}
1820

19-
const id = (x: { x: number; y: number }) => x;
20-
2121
interface UseDraggableProps {
22-
onDrag?: (x: { x: number; y: number }) => {
23-
x: number;
24-
y: number;
25-
};
2622
/**
2723
* A reference to the element that can be used to move the draggable element.
2824
* If omitted, clicking anywhere on the entire draggable element will
2925
* allow you to move it.
3026
*/
3127
moveRef?: React.RefObject<HTMLElement> | null;
28+
29+
elementRect: MutableRefObject<Partial<Rect>>;
3230
}
3331

3432
// complex logic should be a hook, not a component
3533
const useDragToMove = ({
36-
onDrag = id,
3734
moveRef = null,
35+
elementRect,
3836
}: UseDraggableProps): [React.RefCallback<HTMLElement>, boolean] => {
39-
// this state doesn't change often, so it's fine
4037
const [pressed, setPressed] = useState(false);
41-
42-
// do not store position in useState! even if you useEffect on
43-
// it and update `transform` CSS property, React still rerenders
44-
// on every state change, and it LAGS
45-
const position = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
4638
const ref = useRef<HTMLElement | null>(null);
4739

48-
// we've moved the code into the hook, and it would be weird to
49-
// return `ref` and `handleMouseDown` to be set on the same element
50-
// why not just do the job on our own here and use a function-ref
51-
// to subscribe to `mousedown` too? it would go like this:
5240
const unsubscribe = useRef<VoidFunction>();
5341
const legacyRef: RefCallback<HTMLElement> = useCallback(
5442
(elem) => {
55-
// in a production version of this code I'd use a
56-
// `useComposeRef` hook to compose function-ref and object-ref
57-
// into one ref, and then would return it. combining
58-
// hooks in this way by hand is error-prone
59-
60-
// then I'd also split out the rest of this function into a
61-
// separate hook to be called like this:
62-
// const legacyRef = useDomEvent('mousedown');
63-
// const combinedRef = useCombinedRef(ref, legacyRef);
64-
// return [combinedRef, pressed];
6543
ref.current = elem;
6644
if (unsubscribe.current) {
6745
unsubscribe.current;
@@ -85,37 +63,26 @@ const useDragToMove = ({
8563
);
8664

8765
useEffect(() => {
88-
// why subscribe in a `useEffect`? because we want to subscribe
89-
// to mousemove only when pressed, otherwise it will lag even
90-
// when you're not dragging
9166
if (!pressed) {
9267
return;
9368
}
9469

95-
// updating the page without any throttling is a bad idea
96-
// requestAnimationFrame-based throttle would probably be fine,
97-
// but be aware that naive implementation might make element
98-
// lag 1 frame behind cursor, and it will appear to be lagging
99-
// even at 60 FPS
10070
const handleMouseMove = throttle(
10171
(event: { movementX: number; movementY: number }) => {
102-
// needed for TypeScript anyway
103-
if (!ref.current || !position.current) {
72+
if (!ref.current || !elementRect.current) {
10473
return;
10574
}
106-
const pos = position.current;
107-
// it's important to save it into variable here,
108-
// otherwise we might capture reference to an element
109-
// that was long gone. not really sure what's correct
110-
// behavior for a case when you've been scrolling, and
111-
// the target element was replaced. probably some formulae
112-
// needed to handle that case. TODO
75+
76+
elementRect.current.left =
77+
(elementRect.current.left ?? 0) + event.movementX;
78+
79+
elementRect.current.top =
80+
(elementRect.current.top ?? 0) + event.movementY;
81+
11382
const elem = moveRef?.current ?? ref.current;
114-
position.current = onDrag({
115-
x: pos.x + event.movementX,
116-
y: pos.y + event.movementY,
117-
});
118-
elem.style.transform = `translate(${position.current.x}px, ${position.current.y}px)`;
83+
84+
elem.style.left = `${elementRect.current.left}px`;
85+
elem.style.top = `${elementRect.current.top}px`;
11986
}
12087
);
12188
const handleMouseUp = (e: MouseEvent) => {
@@ -138,7 +105,7 @@ const useDragToMove = ({
138105
// if `onDrag` wasn't defined with `useCallback`, we'd have to
139106
// resubscribe to 2 DOM events here, not to say it would mess
140107
// with `throttle` and reset its internal timer
141-
}, [pressed, onDrag, moveRef]);
108+
}, [pressed, moveRef, elementRect]);
142109

143110
// actually it makes sense to return an array only when
144111
// you expect that on the caller side all of the fields

src/hooks/useDragToResize.ts

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useRef } from "react";
1+
import { MutableRefObject, useEffect } from "react";
22

33
// reference: https://stackoverflow.com/questions/62436814/react-drag-corner-of-element-to-resize-contents
44

@@ -25,6 +25,7 @@ type MouseMovementHandler = (
2525

2626
interface UseDragToResizeProps {
2727
elementRef: React.RefObject<HTMLElement>;
28+
elementRect: MutableRefObject<Partial<Rect>>;
2829
minDimensions: Dimensions;
2930
resizeNRef?: React.RefObject<HTMLElement> | null;
3031
resizeNERef?: React.RefObject<HTMLElement> | null;
@@ -47,28 +48,8 @@ function useDragToResize({
4748
resizeSERef,
4849
resizeSWRef,
4950
minDimensions,
51+
elementRect,
5052
}: UseDragToResizeProps): void {
51-
const elementRect = useRef<Partial<Rect>>({
52-
width: undefined,
53-
height: undefined,
54-
left: undefined,
55-
top: undefined,
56-
});
57-
58-
// Since we dont know the size that the element should initialize to,
59-
// we need to grab the sizes from the element rect
60-
useEffect(() => {
61-
if (elementRef.current) {
62-
const rect = elementRef.current?.getBoundingClientRect();
63-
elementRect.current = {
64-
height: rect.height,
65-
width: rect.width,
66-
left: rect.left,
67-
top: rect.top,
68-
};
69-
}
70-
}, [elementRef]);
71-
7253
useEffect(() => {
7354
/**
7455
* Whenever a mousedown event happens on any of the resize refs,
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { useEffect, useRef } from "react";
2+
import useDragToResize, { Dimensions, Rect } from "./useDragToResize";
3+
import useDragToMove from "./useDragToMove";
4+
import useWindowMinMax from "./useWindowMinMax";
5+
6+
interface UsePositionableElementProps {
7+
elementRef: React.RefObject<HTMLElement>;
8+
minDimensions: Dimensions;
9+
resizeNRef?: React.RefObject<HTMLElement> | null;
10+
resizeNERef?: React.RefObject<HTMLElement> | null;
11+
resizeERef?: React.RefObject<HTMLElement> | null;
12+
resizeSERef?: React.RefObject<HTMLElement> | null;
13+
resizeSRef?: React.RefObject<HTMLElement> | null;
14+
resizeSWRef?: React.RefObject<HTMLElement> | null;
15+
resizeWRef?: React.RefObject<HTMLElement> | null;
16+
resizeNWRef?: React.RefObject<HTMLElement> | null;
17+
}
18+
19+
/**
20+
* Allows an element to be moved and resized.
21+
*
22+
* Essentially wraps around a bunch of other hooks,
23+
* holding the elements position in a single shared ref.
24+
*/
25+
function usePositionableElement({
26+
elementRef,
27+
minDimensions,
28+
resizeNRef,
29+
resizeNERef,
30+
resizeERef,
31+
resizeSERef,
32+
resizeSRef,
33+
resizeSWRef,
34+
resizeWRef,
35+
resizeNWRef,
36+
}: UsePositionableElementProps) {
37+
// Hold a single ref for the elements rect,
38+
// so we dont have to call getBoundingClientRect every
39+
// time we need to make a change to the position/dimensions.
40+
const elementRect = useRef<Partial<Rect>>({
41+
width: undefined,
42+
height: undefined,
43+
left: undefined,
44+
top: undefined,
45+
});
46+
47+
// Since we dont know the size that the element should initialize to,
48+
// we need to grab the sizes from the element rect
49+
useEffect(() => {
50+
if (elementRef.current) {
51+
const rect = elementRef.current?.getBoundingClientRect();
52+
elementRect.current = {
53+
height: rect.height,
54+
width: rect.width,
55+
left: rect.left,
56+
top: rect.top,
57+
};
58+
}
59+
}, [elementRef]);
60+
61+
// The resize hook allows the app to be resized
62+
// by dragging the corners or edges of the element.
63+
// It needs to know the elementRect, so any changes
64+
// it makes can be shared with the other hooks.
65+
useDragToResize({
66+
elementRef,
67+
elementRect,
68+
minDimensions,
69+
resizeNRef,
70+
resizeNERef,
71+
resizeERef,
72+
resizeSERef,
73+
resizeSRef,
74+
resizeSWRef,
75+
resizeWRef,
76+
resizeNWRef,
77+
});
78+
79+
// The move hook allows the app to be moved when
80+
// the element that has the clickToMoveRef is clicked and dragged.
81+
// Again, it needs to know the elementRect for the same reason mentioned above.
82+
const [clickToMoveRef] = useDragToMove({
83+
moveRef: elementRef,
84+
elementRect,
85+
});
86+
87+
// Allows the the element to be minimized or maximized.
88+
// Again, it needs to know the elementRect for the same reason mentioned above.
89+
const { maximize, minimize } = useWindowMinMax({
90+
windowRef: elementRef,
91+
windowRect: elementRect,
92+
});
93+
94+
return {
95+
clickToMoveRef,
96+
minimize,
97+
maximize,
98+
};
99+
}
100+
101+
export default usePositionableElement;

src/hooks/useWindowMinMax.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,32 @@
1+
import { MutableRefObject } from "react";
12
import useWindowManagerStore from "../stores/windowManagerStore";
3+
import { Rect } from "./useDragToResize";
24

35
function toPx(value: number) {
46
return `${value}px`;
57
}
6-
function useWindowMinMax(windowRef: React.RefObject<HTMLElement>) {
8+
9+
interface UseWindowMinMaxProps {
10+
windowRef: React.RefObject<HTMLElement>;
11+
windowRect: MutableRefObject<Partial<Rect>>;
12+
}
13+
function useWindowMinMax({ windowRect, windowRef }: UseWindowMinMaxProps) {
714
const winMan = useWindowManagerStore();
815

916
function maximize() {
1017
const boundary = winMan.contentRef.current?.getBoundingClientRect();
1118
const window = windowRef.current;
1219
if (!boundary || !window) return;
1320

21+
windowRect.current.top = boundary.y;
22+
windowRect.current.left = boundary.x;
23+
windowRect.current.width = boundary.width;
24+
windowRect.current.height = boundary.height;
25+
1426
window.style.top = toPx(boundary.y);
1527
window.style.left = toPx(boundary.x);
1628
window.style.width = toPx(boundary.width);
1729
window.style.height = toPx(boundary.height);
18-
window.style.transform = "";
1930
}
2031

2132
function minimize() {

0 commit comments

Comments
 (0)