From 8fc7db4b65da190a82c968825094b09c2b775447 Mon Sep 17 00:00:00 2001 From: Ptrhnk Date: Wed, 11 Dec 2024 16:15:21 +0000 Subject: [PATCH] 2444 slow middle box resizing (#2471) * hook for callbackDebounce and resize observer * use hook everywhere, remove external resize observer lib * back to previous statement list debounce handling but with custom observer hook * debounced resize for statement list except annotator, remove disableUserSelect from redux and css by js instead --- packages/client/package.json | 1 - packages/client/pnpm-lock.yaml | 23 +------ packages/client/src/Theme/global.ts | 6 +- packages/client/src/app.tsx | 8 +-- .../src/components/advanced/Page/Page.tsx | 5 +- .../PanelSeparator/PanelSeparator.tsx | 7 +-- packages/client/src/hooks/index.tsx | 16 ++++- .../client/src/hooks/useDebouncedCallback.tsx | 33 ++++++++++ .../client/src/hooks/useResizeObserver.tsx | 60 +++++++++++++++++++ .../Documents/DocumentRow/DocumentRow.tsx | 2 +- .../EntitySearchBox/EntitySearchBox.tsx | 8 +-- .../StatementsListBox/StatementListBox.tsx | 13 ++-- .../StatementListTextAnnotator.tsx | 5 +- .../layout/disableUserSelectSlice.tsx | 16 ----- packages/client/src/redux/store.tsx | 2 - 15 files changed, 130 insertions(+), 75 deletions(-) create mode 100644 packages/client/src/hooks/useDebouncedCallback.tsx create mode 100644 packages/client/src/hooks/useResizeObserver.tsx delete mode 100644 packages/client/src/redux/features/layout/disableUserSelectSlice.tsx diff --git a/packages/client/package.json b/packages/client/package.json index 53bcbe362..cdd128a24 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -97,7 +97,6 @@ "ts-loader": "^9.5.1", "typescript": "^5.5.4", "typescript-plugin-styled-components": "^3.0.0", - "use-resize-observer": "^9.1.0", "webpack": "^5.94.0", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.0.4", diff --git a/packages/client/pnpm-lock.yaml b/packages/client/pnpm-lock.yaml index 97d7d341f..782652a51 100644 --- a/packages/client/pnpm-lock.yaml +++ b/packages/client/pnpm-lock.yaml @@ -225,9 +225,6 @@ importers: typescript-plugin-styled-components: specifier: ^3.0.0 version: 3.0.0(typescript@5.5.4) - use-resize-observer: - specifier: ^9.1.0 - version: 9.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) webpack: specifier: ^5.94.0 version: 5.94.0(webpack-cli@5.1.4) @@ -1200,9 +1197,6 @@ packages: peerDependencies: tslib: '2' - '@juggle/resize-observer@3.4.0': - resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} - '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} @@ -1935,7 +1929,7 @@ packages: annotator@file:../annotator: resolution: {directory: ../annotator, type: directory} - engines: {pnpm: '>=8.15.0'} + engines: {pnpm: ^9.0.0} anser@1.4.10: resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==} @@ -2800,6 +2794,7 @@ packages: eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true espree@9.6.1: @@ -5267,12 +5262,6 @@ packages: '@types/react': optional: true - use-resize-observer@9.1.0: - resolution: {integrity: sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==} - peerDependencies: - react: 16.8.0 - 18 - react-dom: 16.8.0 - 18 - use-sync-external-store@1.2.2: resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} peerDependencies: @@ -6879,8 +6868,6 @@ snapshots: dependencies: tslib: 2.7.0 - '@juggle/resize-observer@3.4.0': {} - '@leichtgewicht/ip-codec@2.0.5': {} '@nodelib/fs.scandir@2.1.5': @@ -11744,12 +11731,6 @@ snapshots: optionalDependencies: '@types/react': 18.3.5 - use-resize-observer@9.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - '@juggle/resize-observer': 3.4.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - use-sync-external-store@1.2.2(react@18.3.1): dependencies: react: 18.3.1 diff --git a/packages/client/src/Theme/global.ts b/packages/client/src/Theme/global.ts index 9ea25631b..68973b37d 100644 --- a/packages/client/src/Theme/global.ts +++ b/packages/client/src/Theme/global.ts @@ -5,7 +5,6 @@ import { ThemeType } from "./theme"; interface GlobalStyle { theme: ThemeType; - disableUserSelect?: boolean; } const GlobalStyle = createGlobalStyle` html { @@ -32,9 +31,8 @@ const GlobalStyle = createGlobalStyle` -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } - *:not(input,textarea) { - user-select: ${({ disableUserSelect }) => - disableUserSelect ? "none" : "auto"}; + .no-select { + user-select: none; } h1 { font-size: ${({ theme }) => theme.fontSize["4xl"]}; diff --git a/packages/client/src/app.tsx b/packages/client/src/app.tsx index 0f2f1d508..8c04bc33e 100644 --- a/packages/client/src/app.tsx +++ b/packages/client/src/app.tsx @@ -86,9 +86,6 @@ const queryClient = new QueryClient({ }); export const App: React.FC = () => { const dispatch = useAppDispatch(); - const disableUserSelect = useAppSelector( - (state) => state.layout.disableUserSelect - ); const selectedThemeId: InterfaceEnums.Theme = useAppSelector( (state) => state.theme ); @@ -179,10 +176,7 @@ export const App: React.FC = () => { - +
{/* fontSize zooms query devtools to normal size */} diff --git a/packages/client/src/components/advanced/Page/Page.tsx b/packages/client/src/components/advanced/Page/Page.tsx index 55c975d5d..06c119915 100644 --- a/packages/client/src/components/advanced/Page/Page.tsx +++ b/packages/client/src/components/advanced/Page/Page.tsx @@ -12,7 +12,6 @@ import useKeypress from "hooks/useKeyPress"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useLocation, useNavigate } from "react-router"; import { Id, toast } from "react-toastify"; -import { setDisableUserSelect } from "redux/features/layout/disableUserSelectSlice"; import { setPing } from "redux/features/pingSlice"; import { setLastClickedIndex } from "redux/features/statementList/lastClickedIndexSlice"; import { setUsername } from "redux/features/usernameSlice"; @@ -106,8 +105,8 @@ export const Page: React.FC = ({ children }) => { const [tempLocation, setTempLocation] = useState(false); - useKeypress("Shift", () => dispatch(setDisableUserSelect(true))); - useKeyLift("Shift", () => dispatch(setDisableUserSelect(false))); + useKeypress("Shift", () => document.body.classList.add("no-select")); + useKeyLift("Shift", () => document.body.classList.remove("no-select")); useQuery({ queryKey: ["ping"], diff --git a/packages/client/src/components/advanced/PanelSeparator/PanelSeparator.tsx b/packages/client/src/components/advanced/PanelSeparator/PanelSeparator.tsx index f31d9b7f5..20f005f39 100644 --- a/packages/client/src/components/advanced/PanelSeparator/PanelSeparator.tsx +++ b/packages/client/src/components/advanced/PanelSeparator/PanelSeparator.tsx @@ -1,6 +1,5 @@ -import React, { useEffect, useState } from "react"; import { useSpring } from "@react-spring/web"; -import { setDisableUserSelect } from "redux/features/layout/disableUserSelectSlice"; +import React, { useEffect, useState } from "react"; import { setPanelWidths } from "redux/features/layout/panelWidthsSlice"; import { setSeparatorXPosition } from "redux/features/layout/separatorXPositionSlice"; import { useAppDispatch, useAppSelector } from "redux/hooks"; @@ -71,7 +70,7 @@ export const PanelSeparator: React.FC = ({}) => { const onMouseDown = (e: React.MouseEvent) => { setSeparatorXTempPosition(e.clientX); setDragging(true); - dispatch(setDisableUserSelect(true)); + document.body.classList.add("no-select"); }; const onMove = (clientX: number) => { @@ -98,7 +97,7 @@ export const PanelSeparator: React.FC = ({}) => { const onMouseUp = () => { setDragging(false); - dispatch(setDisableUserSelect(false)); + document.body.classList.remove("no-select"); }; useEffect(() => { diff --git a/packages/client/src/hooks/index.tsx b/packages/client/src/hooks/index.tsx index 8f119d0a4..669f175c7 100644 --- a/packages/client/src/hooks/index.tsx +++ b/packages/client/src/hooks/index.tsx @@ -1,5 +1,19 @@ +import { useContainerDimensions } from "./useContainerDimensions"; import useDebounce from "./useDebounce"; +import useDebouncedCallback from "./useDebouncedCallback"; +import useKeyLift from "./useKeyLift"; import useKeyPress from "./useKeyPress"; +import { useResizeObserver } from "./useResizeObserver"; import { useSearchParams } from "./useSearchParamsContext"; +import { useWindowSize } from "./useWindowSize"; -export { useDebounce, useKeyPress, useSearchParams }; +export { + useDebounce, + useKeyPress, + useKeyLift, + useContainerDimensions, + useSearchParams, + useDebouncedCallback, + useResizeObserver, + useWindowSize, +}; diff --git a/packages/client/src/hooks/useDebouncedCallback.tsx b/packages/client/src/hooks/useDebouncedCallback.tsx new file mode 100644 index 000000000..31e57b8fa --- /dev/null +++ b/packages/client/src/hooks/useDebouncedCallback.tsx @@ -0,0 +1,33 @@ +import { useCallback, useRef } from "react"; + +const useDebouncedCallback = void>( + callback: T, + delay: number +): T => { + const timeoutRef = useRef(); + + const debouncedCallback = useCallback( + (...args: Parameters) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + timeoutRef.current = window.setTimeout(() => { + callback(...args); + }, delay); + }, + [callback, delay] + ); + + const cleanup = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }, []); + + useCallback(() => cleanup, [cleanup]); + + return debouncedCallback as T; +}; + +export default useDebouncedCallback; diff --git a/packages/client/src/hooks/useResizeObserver.tsx b/packages/client/src/hooks/useResizeObserver.tsx new file mode 100644 index 000000000..22eee1a80 --- /dev/null +++ b/packages/client/src/hooks/useResizeObserver.tsx @@ -0,0 +1,60 @@ +import { useDebouncedCallback } from "hooks"; +import { useLayoutEffect, useState, useRef } from "react"; + +interface UseResizeObserverOptions { + debounceDelay?: number; +} + +interface Size { + width: number | undefined; + height: number | undefined; +} + +export const useResizeObserver = ({ + debounceDelay = 0, +}: UseResizeObserverOptions = {}) => { + const ref = useRef(null); + const animationFrameRef = useRef(null); + const [size, setSize] = useState({ + width: undefined, + height: undefined, + }); + + const debouncedCallback = useDebouncedCallback((value: Size) => { + setSize(value); + }, debounceDelay); + + useLayoutEffect(() => { + const element = ref.current; + if (!element || typeof ResizeObserver === "undefined") { + return; + } + + const resizeObserver = new ResizeObserver((entries) => { + animationFrameRef.current = window.requestAnimationFrame(() => { + if (!Array.isArray(entries) || entries.length === 0) { + return; + } + + const entry = entries[0]; + const { width, height } = entry.contentRect; + + debouncedCallback({ + width: Math.round(width), + height: Math.round(height), + }); + }); + }); + + resizeObserver.observe(element); + + return () => { + if (animationFrameRef.current !== null) { + window.cancelAnimationFrame(animationFrameRef.current); + } + resizeObserver.disconnect(); + }; + }, [debouncedCallback]); + + return { ref, ...size }; +}; diff --git a/packages/client/src/pages/Documents/DocumentRow/DocumentRow.tsx b/packages/client/src/pages/Documents/DocumentRow/DocumentRow.tsx index 385f4a82b..8ed78135b 100644 --- a/packages/client/src/pages/Documents/DocumentRow/DocumentRow.tsx +++ b/packages/client/src/pages/Documents/DocumentRow/DocumentRow.tsx @@ -18,7 +18,6 @@ import React, { } from "react"; import { FaSave, FaTrash } from "react-icons/fa"; import { RiFileEditFill } from "react-icons/ri"; -import useResizeObserver from "use-resize-observer"; import { StyledCount, StyledCountTag, @@ -28,6 +27,7 @@ import { } from "../DocumentsPageStyles"; import theme from "Theme/theme"; import { EntityColors } from "types"; +import { useResizeObserver } from "hooks"; interface DocumentRow { document: IResponseDocument; diff --git a/packages/client/src/pages/Main/containers/EntitySearchBox/EntitySearchBox.tsx b/packages/client/src/pages/Main/containers/EntitySearchBox/EntitySearchBox.tsx index 43f602b21..fae6632a8 100644 --- a/packages/client/src/pages/Main/containers/EntitySearchBox/EntitySearchBox.tsx +++ b/packages/client/src/pages/Main/containers/EntitySearchBox/EntitySearchBox.tsx @@ -14,14 +14,13 @@ import Dropdown, { EntitySuggester, EntityTag, } from "components/advanced"; -import { useDebounce, useSearchParams } from "hooks"; +import { useDebounce, useResizeObserver, useSearchParams } from "hooks"; import React, { useEffect, useMemo, useState } from "react"; import { CgOptions } from "react-icons/cg"; import { FaPlus } from "react-icons/fa"; import { IoMdArrowDropdownCircle } from "react-icons/io"; import { RiCloseFill } from "react-icons/ri"; import { DropdownItem } from "types"; -import useResizeObserver from "use-resize-observer"; import { StyledAdvancedOptions, StyledAdvancedOptionsSign, @@ -79,9 +78,8 @@ export const EntitySearchBox: React.FC = () => { const [searchData, setSearchData] = useState(initValues); const debouncedValues = useDebounce(searchData, debounceTime); - const { ref: resultRef, height = 0 } = useResizeObserver(); - - const debouncedResultsHeight = useDebounce(height, 20); + const { ref: resultRef, height: debouncedResultsHeight = 0 } = + useResizeObserver(); const statusOptionSelected: EntityEnums.Status = useMemo(() => { if (!!searchData.status) { diff --git a/packages/client/src/pages/Main/containers/StatementsListBox/StatementListBox.tsx b/packages/client/src/pages/Main/containers/StatementsListBox/StatementListBox.tsx index 2b7f96cde..fca7c496e 100644 --- a/packages/client/src/pages/Main/containers/StatementsListBox/StatementListBox.tsx +++ b/packages/client/src/pages/Main/containers/StatementsListBox/StatementListBox.tsx @@ -11,7 +11,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import api from "api"; import { CustomScrollbar, Loader, Submit, ToastWithLink } from "components"; import { CStatement, CTerritory } from "constructors"; -import { useDebounce, useSearchParams } from "hooks"; +import { useDebounce, useResizeObserver, useSearchParams } from "hooks"; import React, { useEffect, useMemo, useState } from "react"; import { BsInfoCircle } from "react-icons/bs"; import { toast } from "react-toastify"; @@ -22,7 +22,6 @@ import { setRowsExpanded } from "redux/features/statementList/rowsExpandedSlice" import { useAppDispatch, useAppSelector } from "redux/hooks"; import { COLLAPSED_TABLE_WIDTH } from "Theme/constants"; import { EntitiesDeleteSuccessResponse, StatementListDisplayMode } from "types"; -import useResizeObserver from "use-resize-observer"; import { StatementListHeader } from "./StatementListHeader/StatementListHeader"; import { StatementListTable } from "./StatementListTable/StatementListTable"; import { StatementListTextAnnotator } from "./StatementListTextAnnotator/StatementListTextAnnotator"; @@ -511,9 +510,9 @@ export const StatementListBox: React.FC = () => { ref: contentRef, height: contentHeight = 0, width: contentWidth = 0, - } = useResizeObserver(); - - const debouncedWidth = useDebounce(contentWidth, 100); + } = useResizeObserver({ + debounceDelay: displayMode === StatementListDisplayMode.LIST ? 50 : 0, + }); const [storedAnnotatorResourceId, setStoredAnnotatorResourceId] = useState< string | false @@ -548,9 +547,9 @@ export const StatementListBox: React.FC = () => { const width = useMemo( () => displayMode === StatementListDisplayMode.LIST - ? debouncedWidth + ? contentWidth : COLLAPSED_TABLE_WIDTH, - [displayMode, debouncedWidth] + [displayMode, contentWidth] ); return ( diff --git a/packages/client/src/pages/Main/containers/StatementsListBox/StatementListTextAnnotator/StatementListTextAnnotator.tsx b/packages/client/src/pages/Main/containers/StatementsListBox/StatementListTextAnnotator/StatementListTextAnnotator.tsx index 542ce90bc..1f2cd0f6f 100644 --- a/packages/client/src/pages/Main/containers/StatementsListBox/StatementListTextAnnotator/StatementListTextAnnotator.tsx +++ b/packages/client/src/pages/Main/containers/StatementsListBox/StatementListTextAnnotator/StatementListTextAnnotator.tsx @@ -15,10 +15,9 @@ import { TbAnchorOff } from "react-icons/tb"; import { TiDocumentText } from "react-icons/ti"; import { ThemeContext } from "styled-components"; import { COLLAPSED_TABLE_WIDTH } from "Theme/constants"; -import useResizeObserver from "use-resize-observer"; import { StyledInfoText } from "../StatementListHeader/StatementListHeaderStyles"; import { entitiesDict } from "@shared/dictionaries/entity"; -import { useDebounce } from "hooks"; +import { useDebounce, useResizeObserver } from "hooks"; interface StatementListTextAnnotator { statements: IResponseStatement[]; @@ -252,7 +251,7 @@ export const StatementListTextAnnotator: React.FC< }, [selectedDocument, territoryId]); const { ref: selectorRef, height: selectorHeight = 0 } = - useResizeObserver(); + useResizeObserver({ debounceDelay: 0 }); const themeContext = useContext(ThemeContext); diff --git a/packages/client/src/redux/features/layout/disableUserSelectSlice.tsx b/packages/client/src/redux/features/layout/disableUserSelectSlice.tsx deleted file mode 100644 index 74f05c745..000000000 --- a/packages/client/src/redux/features/layout/disableUserSelectSlice.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; - -const initialState: boolean = false; - -const disableUserSelectSlice = createSlice({ - name: "disableUserSelect", - initialState: initialState, - reducers: { - setDisableUserSelect: (state: boolean, action: PayloadAction) => - (state = action.payload), - }, -}); - -export const { setDisableUserSelect } = disableUserSelectSlice.actions; - -export default disableUserSelectSlice.reducer; diff --git a/packages/client/src/redux/store.tsx b/packages/client/src/redux/store.tsx index 9e5e9d971..329afcdb8 100644 --- a/packages/client/src/redux/store.tsx +++ b/packages/client/src/redux/store.tsx @@ -16,7 +16,6 @@ import treeInitializeSlice from "./features/territoryTree/treeInitializeSlice"; import usernameSlice from "./features/usernameSlice"; import contentHeightSlice from "./features/layout/contentHeightSlice"; import statementListOpenedSlice from "./features/layout/statementListOpenedSlice"; -import disableUserSelectSlice from "./features/layout/disableUserSelectSlice"; import lastClickedIndexSlice from "./features/statementList/lastClickedIndexSlice"; import disableStatementListScrollSlice from "./features/statementList/disableStatementListScrollSlice"; import disableTreeScrollSlice from "./features/territoryTree/disableTreeScrollSlice"; @@ -63,7 +62,6 @@ const store: Store = configureStore({ fourthPanelExpanded: fourthPanelExpandedSlice, fourthPanelBoxesOpened: fourthPanelBoxesOpenedSlice, statementListOpened: statementListOpenedSlice, - disableUserSelect: disableUserSelectSlice, }), }, });