diff --git a/package.json b/package.json index 590af1c6..7e940315 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@fontsource/roboto": "5.2.5", "@juggle/resize-observer": "3.4.0", "@loadable/component": "5.16.4", + "@mantine/hooks": "^7.17.5", "@mui/icons-material": "7.0.2", "@mui/material": "7.0.2", "@mui/system": "7.0.2", diff --git a/src/modules/backup/screens/Backup.tsx b/src/modules/backup/screens/Backup.tsx index 631629e1..27b9004f 100644 --- a/src/modules/backup/screens/Backup.tsx +++ b/src/modules/backup/screens/Backup.tsx @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useEffect, useLayoutEffect, useState } from 'react'; +import { useEffect, useLayoutEffect, useRef, useState } from 'react'; import List from '@mui/material/List'; import ListItemText from '@mui/material/ListItemText'; import { fromEvent } from 'file-selector'; @@ -23,6 +23,7 @@ import Button from '@mui/material/Button'; import ListItem from '@mui/material/ListItem'; import { Link } from 'react-router-dom'; import Stack from '@mui/material/Stack'; +import { useEventListener, useMergedRef, useWindowEvent } from '@mantine/hooks'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { makeToast } from '@/modules/core/utils/Toast.ts'; import { BackupRestoreState, ValidateBackupQuery } from '@/lib/graphql/generated/graphql.ts'; @@ -89,6 +90,8 @@ export function Backup() { const [, setTriggerReRender] = useState(0); + const inputRef = useRef(null); + const restoreProgress = (() => { if (!data?.restoreStatus) { return 0; @@ -193,40 +196,25 @@ export function Backup() { } }; - const dropHandler = async (e: Event) => { - e.preventDefault(); - const files = await fromEvent(e); - - submitBackup(files[0] as File); - }; - - const dragOverHandler = (e: Event) => { - e.preventDefault(); - }; - const closeInvalidBackupDialog = () => { setIsInvalidBackupDialogOpen(false); resetBackupState(); }; - useEffect(() => { - document.addEventListener('drop', dropHandler); - document.addEventListener('dragover', dragOverHandler); + useWindowEvent('drop', async (e) => { + e.preventDefault(); + const files = await fromEvent(e); - const handleFileSelection = async (event: Event) => { - const files = await fromEvent(event); - submitBackup(files[0] as File); - }; - - const input = document.getElementById('backup-file'); - input?.addEventListener('change', handleFileSelection); - - return () => { - document.removeEventListener('drop', dropHandler); - document.removeEventListener('dragover', dragOverHandler); - input?.removeEventListener('change', handleFileSelection); - }; - }, []); + submitBackup(files[0] as File); + }); + useWindowEvent('dragover', (e) => { + e.preventDefault(); + }); + const inputEventListenerRef = useEventListener('change', async (event) => { + const files = await fromEvent(event); + submitBackup(files[0] as File); + }); + const mergedInputRef = useMergedRef(inputRef, inputEventListenerRef); if (loading) { return ; @@ -253,10 +241,7 @@ export function Backup() { secondary={t('settings.backup.action.create.label.description')} /> - document.getElementById('backup-file')?.click()} - disabled={!!backupRestoreId} - > + inputRef.current?.click()} disabled={!!backupRestoreId}> - + {t('settings.backup.action.validate.dialog.title')} diff --git a/src/modules/core/components/AppbarSearch.tsx b/src/modules/core/components/AppbarSearch.tsx index c636b464..4c8dcd8a 100644 --- a/src/modules/core/components/AppbarSearch.tsx +++ b/src/modules/core/components/AppbarSearch.tsx @@ -6,13 +6,14 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import SearchIcon from '@mui/icons-material/Search'; import IconButton from '@mui/material/IconButton'; import { useQueryParam, StringParam } from 'use-query-params'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; import { useTheme } from '@mui/material/styles'; +import { useHotkeys } from 'react-hotkeys-hook'; import { CustomTooltip } from '@/modules/core/components/CustomTooltip.tsx'; import { SearchTextField } from '@/modules/core/components/inputs/SearchTextField.tsx'; @@ -75,20 +76,13 @@ export const AppbarSearch: React.FunctionComponent = (props) => { if (!searchString) updateSearchOpenState(false); }; - const handleKeyboardEvent = (e: KeyboardEvent) => { - if (e.key === 'F3' || (e.ctrlKey && e.key === 'f')) { - e.preventDefault(); + useHotkeys( + 'ctrl+f, F3', + () => { updateSearchOpenState(true); - } - }; - - useEffect(() => { - window.addEventListener('keydown', handleKeyboardEvent); - - return () => { - window.removeEventListener('keydown', handleKeyboardEvent); - }; - }, [handleKeyboardEvent]); + }, + { preventDefault: true }, + ); if (isOpen) { return ( diff --git a/src/modules/core/components/menu/NestedMenuItem.tsx b/src/modules/core/components/menu/NestedMenuItem.tsx index 01d50222..ba2be0a2 100644 --- a/src/modules/core/components/menu/NestedMenuItem.tsx +++ b/src/modules/core/components/menu/NestedMenuItem.tsx @@ -23,10 +23,8 @@ import { MouseEvent, ReactNode, RefAttributes, - useImperativeHandle, useRef, useState, - Ref, } from 'react'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; @@ -34,6 +32,7 @@ import Box from '@mui/material/Box'; import { OverridableComponent } from '@mui/material/OverridableComponent'; import { SvgIconTypeMap } from '@mui/material/SvgIcon'; +import { useMergedRef } from '@mantine/hooks'; import { IconMenuItem } from '@/modules/core/components/menu/IconMenuItem.tsx'; import { getOptionForDirection } from '@/modules/theme/services/ThemeCreator.ts'; import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx'; @@ -74,10 +73,10 @@ const NestedMenuItem = forwardRef((pr const { ref: containerRefProp, ...ContainerProps } = ContainerPropsProp; const menuItemRef = useRef(null); - useImperativeHandle(ref, () => menuItemRef.current!); + const mergedMenuItemRef = useMergedRef(ref, menuItemRef); const containerRef = useRef(null); - useImperativeHandle(containerRefProp as Ref, () => containerRef.current as HTMLElement); + const mergedContainerRef = useMergedRef(containerRefProp, containerRef); const menuContainerRef = useRef(null); @@ -184,7 +183,7 @@ const NestedMenuItem = forwardRef((pr return ( ((pr { }; }, [t, status, isQueueEmpty]); - useEffect(() => { - const ignoreError = (e: WindowEventMap['error']) => { - if ( - e.message === 'ResizeObserver loop completed with undelivered notifications.' || - e.message === 'ResizeObserver loop limit exceeded' - ) { - e.stopImmediatePropagation(); - } - }; - - // Virtuoso's resize observer can throw this error, - // which is caught by DnD and aborts dragging. - window.addEventListener('error', ignoreError); - - return () => window.removeEventListener('error', ignoreError); - }, []); + // Virtuoso's resize observer can throw this error, + // which is caught by DnD and aborts dragging. + useWindowEvent('error', (e) => { + if ( + e.message === 'ResizeObserver loop completed with undelivered notifications.' || + e.message === 'ResizeObserver loop limit exceeded' + ) { + e.stopImmediatePropagation(); + } + }); if (isLoading) { return ; diff --git a/src/modules/extension/screens/Extensions.tsx b/src/modules/extension/screens/Extensions.tsx index d68c5b4c..d38a4f10 100644 --- a/src/modules/extension/screens/Extensions.tsx +++ b/src/modules/extension/screens/Extensions.tsx @@ -16,6 +16,7 @@ import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; import { useTranslation } from 'react-i18next'; import { Link, useLocation, useNavigate } from 'react-router-dom'; +import { useWindowEvent } from '@mantine/hooks'; import { CustomTooltip } from '@/modules/core/components/CustomTooltip.tsx'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { AppbarSearch } from '@/modules/core/components/AppbarSearch.tsx'; @@ -242,25 +243,14 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) { }; }, [t, shownLangs, allLangs]); - useEffect(() => { - const dropHandler = async (e: Event) => { - e.preventDefault(); - const files = await fromEvent(e); - submitExternalExtension(files[0] as File); - }; - - const dragOverHandler = (e: Event) => { - e.preventDefault(); - }; - - document.addEventListener('drop', dropHandler); - document.addEventListener('dragover', dragOverHandler); - - return () => { - document.removeEventListener('drop', dropHandler); - document.removeEventListener('dragover', dragOverHandler); - }; - }, []); + useWindowEvent('drop', async (e) => { + e.preventDefault(); + const files = await fromEvent(e); + submitExternalExtension(files[0] as File); + }); + useWindowEvent('dragover', (e) => { + e.preventDefault(); + }); if (isLoading) { return ; diff --git a/src/modules/reader/components/viewer/ReaderViewer.tsx b/src/modules/reader/components/viewer/ReaderViewer.tsx index 22ff7813..0f0fae22 100644 --- a/src/modules/reader/components/viewer/ReaderViewer.tsx +++ b/src/modules/reader/components/viewer/ReaderViewer.tsx @@ -12,7 +12,6 @@ import { memo, useCallback, useEffect, - useImperativeHandle, useLayoutEffect, useMemo, useRef, @@ -21,6 +20,7 @@ import { import Stack from '@mui/material/Stack'; import { useTheme } from '@mui/material/styles'; import { useLocation } from 'react-router-dom'; +import { useMergedRef } from '@mantine/hooks'; import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; import { IReaderSettings, @@ -168,7 +168,7 @@ const BaseReaderViewer = forwardRef( }; const scrollElementRef = useRef(null); - useImperativeHandle(ref, () => scrollElementRef.current!); + const mergedRef = useMergedRef(ref, scrollElementRef); const isContinuousVerticalReadingModeActive = isContinuousVerticalReadingMode(readingMode); const isContinuousReadingModeActive = isContinuousReadingMode(readingMode); @@ -310,7 +310,7 @@ const BaseReaderViewer = forwardRef( return ( { const previousDimensionsRef = useRef({ width: window.innerWidth, height: window.innerHeight }); - useEffect(() => { - const handleResize = () => { - const { width, height } = previousDimensionsRef.current; - previousDimensionsRef.current = { width: window.innerWidth, height: window.innerHeight }; + const handleResize = useCallback(() => { + const { width, height } = previousDimensionsRef.current; + previousDimensionsRef.current = { width: window.innerWidth, height: window.innerHeight }; - if (!shouldPreserveOnResizeChange(readingMode, pageScaleMode, width, height)) { - return; - } + if (!shouldPreserveOnResizeChange(readingMode, pageScaleMode, width, height)) { + return; + } - setPageToScrollToIndex(pageIndex); - }; - - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); + setPageToScrollToIndex(pageIndex); }, [readingMode, pageScaleMode, pageIndex]); + + useWindowEvent('resize', handleResize); }; interface ScrollPreservationInfo { diff --git a/yarn.lock b/yarn.lock index eb2ff3da..1746bbc9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2564,6 +2564,11 @@ hoist-non-react-statics "^3.3.1" react-is "^16.12.0" +"@mantine/hooks@^7.17.5": + version "7.17.5" + resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-7.17.5.tgz#ae3c270161c4a156e3ca8dff1c7a73c4a3710c48" + integrity sha512-Q/3AHI1fjl+W7xQ3jEoMmSoTxLqxMI2gPfxIjd73OPmRpPenYWR1zk/diirXXm2t7JOrAbmpA3/O1gzmgqzc/Q== + "@mui/core-downloads-tracker@^7.0.2": version "7.0.2" resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-7.0.2.tgz#2e6dcaf5027a3957d37797b8dce5c15c78fd4b82"