fix use dimensions hook (#905)

This commit is contained in:
Mike Sawka 2024-09-30 12:19:29 -07:00 committed by GitHub
parent 2f5351e8ca
commit cacde1b0ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 120 additions and 147 deletions

View File

@ -11,7 +11,7 @@ import {
Input, Input,
} from "@/app/block/blockutil"; } from "@/app/block/blockutil";
import { Button } from "@/app/element/button"; import { Button } from "@/app/element/button";
import { useWidth } from "@/app/hook/useWidth"; import { useDimensionsWithCallbackRef } from "@/app/hook/useDimensions";
import { TypeAheadModal } from "@/app/modals/typeaheadmodal"; import { TypeAheadModal } from "@/app/modals/typeaheadmodal";
import { ContextMenuModel } from "@/app/store/contextmenu"; import { ContextMenuModel } from "@/app/store/contextmenu";
import { import {
@ -294,8 +294,8 @@ const ConnStatusOverlay = React.memo(
const connName = blockData.meta?.connection; const connName = blockData.meta?.connection;
const connStatus = jotai.useAtomValue(getConnStatusAtom(connName)); const connStatus = jotai.useAtomValue(getConnStatusAtom(connName));
const isLayoutMode = jotai.useAtomValue(atoms.controlShiftDelayAtom); const isLayoutMode = jotai.useAtomValue(atoms.controlShiftDelayAtom);
const overlayRef = React.useRef<HTMLDivElement>(null); const [overlayRefCallback, _, domRect] = useDimensionsWithCallbackRef(30);
const width = useWidth(overlayRef); const width = domRect?.width;
const [showError, setShowError] = React.useState(false); const [showError, setShowError] = React.useState(false);
const blockNum = jotai.useAtomValue(nodeModel.blockNum); const blockNum = jotai.useAtomValue(nodeModel.blockNum);
@ -334,7 +334,7 @@ const ConnStatusOverlay = React.memo(
} }
return ( return (
<div className="connstatus-overlay" ref={overlayRef}> <div className="connstatus-overlay" ref={overlayRefCallback}>
<div className="connstatus-content"> <div className="connstatus-content">
<div className={clsx("connstatus-status-icon-wrapper", { "has-error": showError })}> <div className={clsx("connstatus-status-icon-wrapper", { "has-error": showError })}>
{showIcon && <i className="fa-solid fa-triangle-exclamation"></i>} {showIcon && <i className="fa-solid fa-triangle-exclamation"></i>}

View File

@ -1,12 +1,11 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { useHeight } from "@/app/hook/useHeight";
import { useWidth } from "@/app/hook/useWidth";
import clsx from "clsx"; import clsx from "clsx";
import React, { memo, useEffect, useLayoutEffect, useRef, useState } from "react"; import React, { memo, useEffect, useLayoutEffect, useRef, useState } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions";
import "./menu.less"; import "./menu.less";
type MenuItem = { type MenuItem = {
@ -143,9 +142,9 @@ const Menu = memo(
const [position, setPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 }); const [position, setPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 });
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
const subMenuRefs = useRef<{ [key: string]: React.RefObject<HTMLDivElement> }>({}); const subMenuRefs = useRef<{ [key: string]: React.RefObject<HTMLDivElement> }>({});
const domRect = useDimensionsWithExistingRef(scopeRef, 30);
const width = useWidth(scopeRef); const width = domRect?.width ?? 0;
const height = useHeight(scopeRef); const height = domRect?.height ?? 0;
items.forEach((_, idx) => { items.forEach((_, idx) => {
const key = `${idx}`; const key = `${idx}`;

View File

@ -1,67 +1,99 @@
import useResizeObserver from "@react-hook/resize-observer"; import * as React from "react";
import { useCallback, useRef, useState } from "react"; import { useCallback, useState } from "react";
import { debounce } from "throttle-debounce"; import { debounce } from "throttle-debounce";
/** // returns a callback ref, a ref object (that is set from the callback), and the width
* Get the current dimensions for the specified element, and whether it is currently changing size. Update when the element resizes. // pass debounceMs of null to not debounce
* @param ref The reference to the element to observe. export function useDimensionsWithCallbackRef<T extends HTMLElement>(
* @param delay The debounce delay to use for updating the dimensions. debounceMs: number = null
* @returns The dimensions of the element, and direction in which the dimensions are changing. ): [(node: T) => void, React.RefObject<T>, DOMRectReadOnly] {
*/ const [domRect, setDomRect] = useState<DOMRectReadOnly>(null);
const useDimensions = (ref: React.RefObject<HTMLElement>, delay = 0) => { const [htmlElem, setHtmlElem] = useState<T>(null);
const [dimensions, setDimensions] = useState<{ const rszObjRef = React.useRef<ResizeObserver>(null);
height: number | null; const oldHtmlElem = React.useRef<T>(null);
width: number | null; const ref = React.useRef<T>(null);
widthDirection?: string; const refCallback = useCallback((node: T) => {
heightDirection?: string; setHtmlElem(node);
}>({ ref.current = node;
height: null,
width: null,
});
const previousDimensions = useRef<{ height: number | null; width: number | null }>({
height: null,
width: null,
});
const updateDimensions = useCallback((entry: ResizeObserverEntry) => {
const parentHeight = entry.contentRect.height;
const parentWidth = entry.contentRect.width;
let widthDirection = "";
let heightDirection = "";
if (previousDimensions.current.width !== null && previousDimensions.current.height !== null) {
if (parentWidth > previousDimensions.current.width) {
widthDirection = "expanding";
} else if (parentWidth < previousDimensions.current.width) {
widthDirection = "shrinking";
} else {
widthDirection = "unchanged";
}
if (parentHeight > previousDimensions.current.height) {
heightDirection = "expanding";
} else if (parentHeight < previousDimensions.current.height) {
heightDirection = "shrinking";
} else {
heightDirection = "unchanged";
}
}
previousDimensions.current = { height: parentHeight, width: parentWidth };
setDimensions({ height: parentHeight, width: parentWidth, widthDirection, heightDirection });
}, []); }, []);
const setDomRectDebounced = React.useCallback(debounceMs == null ? setDomRect : debounce(debounceMs, setDomRect), [
const fUpdateDimensions = useCallback(delay > 0 ? debounce(delay, updateDimensions) : updateDimensions, [ debounceMs,
updateDimensions, setDomRect,
delay,
]); ]);
React.useEffect(() => {
if (!rszObjRef.current) {
rszObjRef.current = new ResizeObserver((entries) => {
for (const entry of entries) {
if (domRect == null) {
setDomRect(entry.contentRect);
} else {
setDomRectDebounced(entry.contentRect);
}
}
});
}
if (htmlElem) {
rszObjRef.current.observe(htmlElem);
oldHtmlElem.current = htmlElem;
}
return () => {
if (oldHtmlElem.current) {
rszObjRef.current?.unobserve(oldHtmlElem.current);
oldHtmlElem.current = null;
}
};
}, [htmlElem]);
React.useEffect(() => {
return () => {
rszObjRef.current?.disconnect();
};
}, []);
return [refCallback, ref, domRect];
}
useResizeObserver(ref, fUpdateDimensions); // will not react to ref changes
// pass debounceMs of null to not debounce
return dimensions; export function useDimensionsWithExistingRef<T extends HTMLElement>(
}; ref: React.RefObject<T>,
debounceMs: number = null
export { useDimensions }; ): DOMRectReadOnly {
const [domRect, setDomRect] = useState<DOMRectReadOnly>(null);
const rszObjRef = React.useRef<ResizeObserver>(null);
const oldHtmlElem = React.useRef<T>(null);
const setDomRectDebounced = React.useCallback(debounceMs == null ? setDomRect : debounce(debounceMs, setDomRect), [
debounceMs,
setDomRect,
]);
React.useEffect(() => {
if (!rszObjRef.current) {
rszObjRef.current = new ResizeObserver((entries) => {
for (const entry of entries) {
if (domRect == null) {
setDomRect(entry.contentRect);
} else {
setDomRectDebounced(entry.contentRect);
}
}
});
}
if (ref.current) {
rszObjRef.current.observe(ref.current);
oldHtmlElem.current = ref.current;
}
return () => {
if (oldHtmlElem.current) {
rszObjRef.current?.unobserve(oldHtmlElem.current);
oldHtmlElem.current = null;
}
};
}, [ref.current]);
React.useEffect(() => {
return () => {
rszObjRef.current?.disconnect();
};
}, []);
if (ref.current != null) {
return ref.current.getBoundingClientRect();
}
return null;
}

View File

@ -1,28 +0,0 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import useResizeObserver from "@react-hook/resize-observer";
import { useCallback, useState } from "react";
import { debounce } from "throttle-debounce";
/**
* Get the height of the specified element and update it when the element resizes.
* @param ref The reference to the element to observe.
* @param delay The debounce delay to use for updating the height.
* @returns The current height of the element, or null if the element is not yet mounted.
*/
const useHeight = (ref: React.RefObject<HTMLElement>, delay = 0) => {
const [height, setHeight] = useState<number | null>(null);
const updateHeight = useCallback((entry: ResizeObserverEntry) => {
setHeight(entry.contentRect.height);
}, []);
const fUpdateHeight = useCallback(delay > 0 ? debounce(delay, updateHeight) : updateHeight, [updateHeight, delay]);
useResizeObserver(ref, fUpdateHeight);
return height;
};
export { useHeight };

View File

@ -1,28 +0,0 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import useResizeObserver from "@react-hook/resize-observer";
import { useCallback, useState } from "react";
import { debounce } from "throttle-debounce";
/**
* Get the width of the specified element and update it when the element resizes.
* @param ref The reference to the element to observe.
* @param delay The debounce delay to use for updating the width.
* @returns The current width of the element, or null if the element is not yet mounted.
*/
const useWidth = (ref: React.RefObject<HTMLElement>, delay = 0) => {
const [width, setWidth] = useState<number | null>(null);
const updateWidth = useCallback((entry: ResizeObserverEntry) => {
setWidth(entry.contentRect.width);
}, []);
const fUpdateWidth = useCallback(delay > 0 ? debounce(delay, updateWidth) : updateWidth, [updateWidth, delay]);
useResizeObserver(ref, fUpdateWidth);
return width;
};
export { useWidth };

View File

@ -3,7 +3,7 @@
import { Input } from "@/app/element/input"; import { Input } from "@/app/element/input";
import { InputDecoration } from "@/app/element/inputdecoration"; import { InputDecoration } from "@/app/element/inputdecoration";
import { useDimensions } from "@/app/hook/useDimensions"; import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions";
import { makeIconClass } from "@/util/util"; import { makeIconClass } from "@/util/util";
import clsx from "clsx"; import clsx from "clsx";
import React, { forwardRef, useLayoutEffect, useRef } from "react"; import React, { forwardRef, useLayoutEffect, useRef } from "react";
@ -99,7 +99,9 @@ const TypeAheadModal = ({
autoFocus, autoFocus,
selectIndex, selectIndex,
}: TypeAheadModalProps) => { }: TypeAheadModalProps) => {
const { width, height } = useDimensions(blockRef); const domRect = useDimensionsWithExistingRef(blockRef, 30);
const width = domRect?.width ?? 0;
const height = domRect?.height ?? 0;
const modalRef = useRef<HTMLDivElement>(null); const modalRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLDivElement>(null); const inputRef = useRef<HTMLDivElement>(null);
const realInputRef = useRef<HTMLInputElement>(null); const realInputRef = useRef<HTMLInputElement>(null);

View File

@ -1,8 +1,6 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { useHeight } from "@/app/hook/useHeight";
import { useWidth } from "@/app/hook/useWidth";
import { getConnStatusAtom, globalStore, WOS } from "@/store/global"; import { getConnStatusAtom, globalStore, WOS } from "@/store/global";
import * as util from "@/util/util"; import * as util from "@/util/util";
import * as Plot from "@observablehq/plot"; import * as Plot from "@observablehq/plot";
@ -11,6 +9,7 @@ import * as htl from "htl";
import * as jotai from "jotai"; import * as jotai from "jotai";
import * as React from "react"; import * as React from "react";
import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions";
import { waveEventSubscribe } from "@/app/store/wps"; import { waveEventSubscribe } from "@/app/store/wps";
import { RpcApi } from "@/app/store/wshclientapi"; import { RpcApi } from "@/app/store/wshclientapi";
import { WindowRpcClient } from "@/app/store/wshrpcutil"; import { WindowRpcClient } from "@/app/store/wshrpcutil";
@ -231,8 +230,9 @@ function CpuPlotView({ model, blockId }: CpuPlotViewProps) {
const CpuPlotViewInner = React.memo(({ model }: CpuPlotViewProps) => { const CpuPlotViewInner = React.memo(({ model }: CpuPlotViewProps) => {
const containerRef = React.useRef<HTMLInputElement>(); const containerRef = React.useRef<HTMLInputElement>();
const plotData = jotai.useAtomValue(model.dataAtom); const plotData = jotai.useAtomValue(model.dataAtom);
const parentHeight = useHeight(containerRef); const domRect = useDimensionsWithExistingRef(containerRef, 30);
const parentWidth = useWidth(containerRef); const parentHeight = domRect?.height ?? 0;
const parentWidth = domRect?.width ?? 0;
const yvals = jotai.useAtomValue(model.metrics); const yvals = jotai.useAtomValue(model.metrics);
React.useEffect(() => { React.useEffect(() => {

View File

@ -1,7 +1,6 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { useHeight } from "@/app/hook/useHeight";
import { useTableNav } from "@table-nav/react"; import { useTableNav } from "@table-nav/react";
import { import {
createColumnHelper, createColumnHelper,
@ -14,6 +13,7 @@ import { clsx } from "clsx";
import Papa from "papaparse"; import Papa from "papaparse";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions";
import "./csvview.less"; import "./csvview.less";
const MAX_DATA_SIZE = 10 * 1024 * 1024; // 10MB in bytes const MAX_DATA_SIZE = 10 * 1024 * 1024; // 10MB in bytes
@ -54,7 +54,8 @@ const CSVView = ({ parentRef, filename, content }: CSVViewProps) => {
const [tableLoaded, setTableLoaded] = useState(false); const [tableLoaded, setTableLoaded] = useState(false);
const { listeners } = useTableNav(); const { listeners } = useTableNav();
const parentHeight = useHeight(parentRef); const domRect = useDimensionsWithExistingRef(parentRef, 30);
const parentHeight = domRect?.height ?? 0;
const cacheKey = `${filename}`; const cacheKey = `${filename}`;
csvCacheRef.current.set(cacheKey, content); csvCacheRef.current.set(cacheKey, content);

View File

@ -1,7 +1,6 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { useHeight } from "@/app/hook/useHeight";
import { ContextMenuModel } from "@/app/store/contextmenu"; import { ContextMenuModel } from "@/app/store/contextmenu";
import { atoms, createBlock, getApi } from "@/app/store/global"; import { atoms, createBlock, getApi } from "@/app/store/global";
import type { PreviewModel } from "@/app/view/preview/preview"; import type { PreviewModel } from "@/app/view/preview/preview";
@ -27,6 +26,7 @@ import { quote as shellQuote } from "shell-quote";
import { OverlayScrollbars } from "overlayscrollbars"; import { OverlayScrollbars } from "overlayscrollbars";
import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions";
import "./directorypreview.less"; import "./directorypreview.less";
interface DirectoryTableProps { interface DirectoryTableProps {
@ -352,8 +352,8 @@ function TableBody({
const warningBoxRef = useRef<HTMLDivElement>(null); const warningBoxRef = useRef<HTMLDivElement>(null);
const osInstanceRef = useRef<OverlayScrollbars>(null); const osInstanceRef = useRef<OverlayScrollbars>(null);
const rowRefs = useRef<HTMLDivElement[]>([]); const rowRefs = useRef<HTMLDivElement[]>([]);
const domRect = useDimensionsWithExistingRef(parentRef, 30);
const parentHeight = useHeight(parentRef); const parentHeight = domRect?.height ?? 0;
const conn = jotai.useAtomValue(model.connection); const conn = jotai.useAtomValue(model.connection);
useEffect(() => { useEffect(() => {
@ -363,7 +363,6 @@ function TableBody({
const warningBoxHeight = warningBoxRef.current?.offsetHeight ?? 0; const warningBoxHeight = warningBoxRef.current?.offsetHeight ?? 0;
const maxHeightLessHeader = parentHeight - warningBoxHeight; const maxHeightLessHeader = parentHeight - warningBoxHeight;
const tbodyHeight = Math.min(maxHeightLessHeader, fullTBodyHeight); const tbodyHeight = Math.min(maxHeightLessHeader, fullTBodyHeight);
setBodyHeight(tbodyHeight); setBodyHeight(tbodyHeight);
} }
}, [data, parentHeight]); }, [data, parentHeight]);

View File

@ -55,6 +55,7 @@
background-color: rgb(from var(--highlight-bg-color) r g b / 0.1); background-color: rgb(from var(--highlight-bg-color) r g b / 0.1);
margin-right: auto; margin-right: auto;
padding: 10px; padding: 10px;
max-width: 85%;
.markdown { .markdown {
width: 100%; width: 100%;
@ -71,6 +72,7 @@
&.chat-msg-user { &.chat-msg-user {
margin-left: auto; margin-left: auto;
padding: 10px; padding: 10px;
max-width: 85%;
background-color: rgb(from var(--accent-color) r g b / 0.15); background-color: rgb(from var(--accent-color) r g b / 0.15);
} }

View File

@ -4,7 +4,6 @@
import { Button } from "@/app/element/button"; import { Button } from "@/app/element/button";
import { Markdown } from "@/app/element/markdown"; import { Markdown } from "@/app/element/markdown";
import { TypingIndicator } from "@/app/element/typingindicator"; import { TypingIndicator } from "@/app/element/typingindicator";
import { useDimensions } from "@/app/hook/useDimensions";
import { RpcApi } from "@/app/store/wshclientapi"; import { RpcApi } from "@/app/store/wshclientapi";
import { WindowRpcClient } from "@/app/store/wshrpcutil"; import { WindowRpcClient } from "@/app/store/wshrpcutil";
import { atoms, fetchWaveFile, globalStore, WOS } from "@/store/global"; import { atoms, fetchWaveFile, globalStore, WOS } from "@/store/global";
@ -260,10 +259,7 @@ const ChatItem = ({ chatItem }: ChatItemProps) => {
<i className="fa-sharp fa-solid fa-sparkles"></i> <i className="fa-sharp fa-solid fa-sparkles"></i>
</div> </div>
</div> </div>
<div <div className="chat-msg chat-msg-assistant">
className="chat-msg chat-msg-assistant"
style={{ maxWidth: "calc(var(--aichat-msg-width) * 1px)" }}
>
<Markdown text={text} /> <Markdown text={text} />
</div> </div>
</> </>
@ -278,7 +274,7 @@ const ChatItem = ({ chatItem }: ChatItemProps) => {
} }
return ( return (
<> <>
<div className="chat-msg chat-msg-user" style={{ maxWidth: "calc(var(--aichat-msg-width) * 1px)" }}> <div className="chat-msg chat-msg-user">
<Markdown className="msg-text" text={text} /> <Markdown className="msg-text" text={text} />
</div> </div>
</> </>
@ -439,10 +435,8 @@ const WaveAi = ({ model }: { model: WaveAiModel; blockId: string }) => {
const [selectedBlockIdx, setSelectedBlockIdx] = useState<number | null>(null); const [selectedBlockIdx, setSelectedBlockIdx] = useState<number | null>(null);
const termFontSize: number = 14; const termFontSize: number = 14;
const windowDims = useDimensions(chatWindowRef);
const msgWidths = {}; const msgWidths = {};
const locked = useAtomValue(model.locked); const locked = useAtomValue(model.locked);
msgWidths["--aichat-msg-width"] = windowDims.width * 0.85;
// a weird workaround to initialize ansynchronously // a weird workaround to initialize ansynchronously
useEffect(() => { useEffect(() => {