Skip to content

Commit 418f697

Browse files
authored
Merge pull request #140 from AnthonyGress/dev
scroll fixes 1.3.0
2 parents 8f59c13 + 8e129a4 commit 418f697

File tree

18 files changed

+220
-58
lines changed

18 files changed

+220
-58
lines changed

backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"license": "ISC",
2323
"description": "",
2424
"dependencies": {
25-
"@loganmarchione/homelab-svg-assets": "^0.4.12",
25+
"@loganmarchione/homelab-svg-assets": "^0.4.13",
2626
"axios": "^1.8.3",
2727
"bcrypt": "^5.1.1",
2828
"cookie-parser": "^1.4.7",

frontend/src/App.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,24 +69,25 @@ export const App = () => {
6969
}
7070
}, [config?.title]);
7171

72-
// Global hotkey listener for Ctrl+0-9 / Cmd+0-9 to switch pages
72+
// Global hotkey listener for Ctrl+1-9 / Cmd+1-9 to switch pages
7373
useEffect(() => {
7474
const handleKeyDown = (event: KeyboardEvent) => {
7575
// Check for Ctrl+Number (Windows/Linux) or Cmd+Number (Mac)
76-
if ((event.ctrlKey || event.metaKey) && (event.key >= '0' && event.key <= '9')) {
76+
// Skip 0 to allow default browser behavior (zoom reset)
77+
if ((event.ctrlKey || event.metaKey) && (event.key >= '1' && event.key <= '9')) {
7778
event.preventDefault();
7879
event.stopPropagation(); // Prevent other listeners from interfering
7980

8081
const keyNumber = parseInt(event.key, 10);
8182

82-
if (keyNumber === 0) {
83-
// Cmd+0 goes to Settings page
83+
if (keyNumber === 9) {
84+
// Cmd+9 goes to Settings page
8485
navigate('/settings');
8586
} else if (keyNumber === 1) {
8687
// Cmd+1 always goes to Home page
8788
navigate('/');
8889
} else {
89-
// Cmd+2+ goes to custom pages (pages[0], pages[1], etc.)
90+
// Cmd+2-8 goes to custom pages (pages[0], pages[1], etc.)
9091
const pageIndex = keyNumber - 2;
9192

9293
if (pages && pages.length > pageIndex) {

frontend/src/api/dash-api.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import axios from 'axios';
22
import { StatusCodes } from 'http-status-codes';
3+
import shortid from 'shortid';
34

45
import { BACKEND_URL } from '../constants/constants';
56
import { Config, DashboardItem, Icon, UploadImageResponse } from '../types';
@@ -2203,9 +2204,6 @@ export class DashApi {
22032204
}
22042205

22052206
public static async createNote(note: { title: string; content: string; fontSize?: string }): Promise<any> {
2206-
// Import shortid dynamically to avoid issues
2207-
const shortid = await import('shortid');
2208-
22092207
try {
22102208
const noteWithId = {
22112209
...note,
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { Menu as MuiMenu, MenuProps as MuiMenuProps, Select as MuiSelect, SelectProps as MuiSelectProps } from '@mui/material';
2+
import React, { useEffect, useRef } from 'react';
3+
4+
import { lockScroll } from '../../utils/scroll-utils';
5+
6+
// Custom Menu component with scroll lock
7+
export const Menu: React.FC<MuiMenuProps> = ({ open, children, ...props }) => {
8+
const unlockScrollRef = useRef<(() => void) | null>(null);
9+
10+
useEffect(() => {
11+
if (open) {
12+
// Apply scroll lock when menu opens
13+
if (!unlockScrollRef.current) {
14+
unlockScrollRef.current = lockScroll();
15+
}
16+
} else {
17+
// Delay unlocking scroll to allow menu close transition to complete
18+
if (unlockScrollRef.current) {
19+
const unlock = unlockScrollRef.current;
20+
unlockScrollRef.current = null;
21+
22+
setTimeout(() => {
23+
unlock();
24+
}, 150);
25+
}
26+
}
27+
}, [open]);
28+
29+
// Cleanup on unmount
30+
useEffect(() => {
31+
return () => {
32+
if (unlockScrollRef.current) {
33+
unlockScrollRef.current();
34+
unlockScrollRef.current = null;
35+
}
36+
};
37+
}, []);
38+
39+
return (
40+
<MuiMenu
41+
open={open}
42+
disableScrollLock={true}
43+
{...props}
44+
>
45+
{children}
46+
</MuiMenu>
47+
);
48+
};
49+
50+
// Custom Select component with scroll lock
51+
export const Select: React.FC<MuiSelectProps> = ({ open, children, ...props }) => {
52+
const unlockScrollRef = useRef<(() => void) | null>(null);
53+
54+
useEffect(() => {
55+
if (open) {
56+
// Apply scroll lock when select opens
57+
if (!unlockScrollRef.current) {
58+
unlockScrollRef.current = lockScroll();
59+
}
60+
} else {
61+
// Delay unlocking scroll to allow select close transition to complete
62+
if (unlockScrollRef.current) {
63+
const unlock = unlockScrollRef.current;
64+
unlockScrollRef.current = null;
65+
66+
setTimeout(() => {
67+
unlock();
68+
}, 150);
69+
}
70+
}
71+
}, [open]);
72+
73+
// Cleanup on unmount
74+
useEffect(() => {
75+
return () => {
76+
if (unlockScrollRef.current) {
77+
unlockScrollRef.current();
78+
unlockScrollRef.current = null;
79+
}
80+
};
81+
}, []);
82+
83+
return (
84+
<MuiSelect
85+
open={open}
86+
MenuProps={{
87+
disableScrollLock: true,
88+
}}
89+
{...props}
90+
>
91+
{children}
92+
</MuiSelect>
93+
);
94+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Export custom MUI components with scroll lock functionality
2+
export { Menu, Select } from './ScrollLockComponents';

frontend/src/components/dashboard/DashboardGrid.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { DashboardItem, DOWNLOAD_CLIENT_TYPE, ITEM_TYPE, TORRENT_CLIENT_TYPE } f
2323
import { AddEditForm } from '../forms/AddEditForm/AddEditForm';
2424
import { CenteredModal } from '../modals/CenteredModal';
2525
import { ConfirmationOptions, PopupManager } from '../modals/PopupManager';
26+
import { ToastManager } from '../toast/ToastManager';
2627
import { BlankAppShortcut } from './base-items/apps/BlankAppShortcut';
2728
import { BlankWidget } from './base-items/widgets/BlankWidget';
2829
import { SortableAppShortcut } from './sortable-items/apps/SortableAppShortcut';
@@ -448,7 +449,6 @@ export const DashboardGrid: React.FC = () => {
448449
saveLayout(updatedLayout);
449450

450451
// Show success toast
451-
const { ToastManager } = await import('../toast/ToastManager');
452452
ToastManager.success(`${itemName} deleted successfully`);
453453
}
454454
};

frontend/src/components/dashboard/base-items/widgets/AdGuardWidget/AdGuardWidget.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Box, Button, CircularProgress, Grid2 as Grid, Menu, MenuItem, Paper, Typography } from '@mui/material';
1+
import { Box, Button, CircularProgress, Grid2 as Grid, MenuItem, Paper, Typography } from '@mui/material';
22
import { useCallback, useEffect, useRef, useState } from 'react';
33
import { FaGlobe, FaList, FaPercentage } from 'react-icons/fa';
44
import { FaShield } from 'react-icons/fa6';
@@ -8,6 +8,7 @@ import { DashApi } from '../../../../../api/dash-api';
88
import { BACKEND_URL } from '../../../../../constants/constants';
99
import { useAppContext } from '../../../../../context/useAppContext';
1010
import { formatNumber } from '../../../../../utils/utils';
11+
import { Menu } from '../../../../custom-mui';
1112

1213
// Define our own Timeout type based on setTimeout's return type
1314
type TimeoutId = ReturnType<typeof setTimeout>;

frontend/src/components/dashboard/base-items/widgets/DownloadClientWidget.tsx

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ArrowDownward, ArrowUpward, CheckCircle, Delete, Download, MoreVert, Pause, PlayArrow, Stop, Upload, Warning } from '@mui/icons-material';
2-
import { Box, Button, CardContent, CircularProgress, Grid, IconButton, LinearProgress, Link, Menu, MenuItem, TextField, Tooltip, Typography, useMediaQuery } from '@mui/material';
2+
import { Box, Button, CardContent, CircularProgress, Grid, IconButton, LinearProgress, Link, MenuItem, TextField, Tooltip, Typography, useMediaQuery } from '@mui/material';
33
import React, { useState } from 'react';
44

55
import { PopupManager } from '../../../../components/modals/PopupManager';
@@ -8,6 +8,7 @@ import { DUAL_WIDGET_CONTAINER_HEIGHT } from '../../../../constants/widget-dimen
88
import { useAppContext } from '../../../../context/useAppContext';
99
import { theme } from '../../../../theme/theme';
1010
import { TORRENT_CLIENT_TYPE } from '../../../../types';
11+
import { Menu } from '../../../custom-mui';
1112

1213
export type DownloadClientStats = {
1314
dl_info_speed: number;
@@ -139,15 +140,18 @@ interface DownloadItemProps {
139140
const DownloadItem: React.FC<DownloadItemProps> = ({ torrent, clientName, isAdmin, onResume, onPause, onDelete }) => {
140141
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
141142
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
143+
const [menuOpen, setMenuOpen] = useState(false);
142144
const [isActionLoading, setIsActionLoading] = useState(false);
143145
const { editMode } = useAppContext();
144146

145147
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
146148
setMenuAnchorEl(event.currentTarget);
149+
setMenuOpen(true);
147150
};
148151

149152
const handleMenuClose = () => {
150153
setMenuAnchorEl(null);
154+
setMenuOpen(false);
151155
};
152156

153157
const handleResume = async () => {
@@ -182,23 +186,10 @@ const DownloadItem: React.FC<DownloadItemProps> = ({ torrent, clientName, isAdmi
182186
if (onDelete) {
183187
handleMenuClose();
184188

185-
// Import Swal directly for this custom case
186-
const Swal = (await import('sweetalert2')).default;
187-
188-
Swal.fire({
189+
PopupManager.threeButtonDialog({
189190
title: `Remove "${torrent.name}"?`,
190-
icon: 'error',
191-
showDenyButton: true,
192-
showCancelButton: true,
193-
confirmButtonText: 'Delete Files',
194-
confirmButtonColor: theme.palette.error.main,
195-
denyButtonText: 'Keep Files',
196-
denyButtonColor: theme.palette.info.main,
197-
cancelButtonText: 'Cancel',
198-
reverseButtons: true,
199-
focusDeny: true
200-
}).then(async (result) => {
201-
if (result.isConfirmed) {
191+
confirmText: 'Delete Files',
192+
confirmAction: async () => {
202193
// Delete torrent and files
203194
setIsActionLoading(true);
204195
try {
@@ -208,7 +199,9 @@ const DownloadItem: React.FC<DownloadItemProps> = ({ torrent, clientName, isAdmi
208199
} finally {
209200
setIsActionLoading(false);
210201
}
211-
} else if (result.isDenied) {
202+
},
203+
denyText: 'Keep Files',
204+
denyAction: async () => {
212205
// Delete torrent only, keep files
213206
setIsActionLoading(true);
214207
try {
@@ -219,7 +212,6 @@ const DownloadItem: React.FC<DownloadItemProps> = ({ torrent, clientName, isAdmi
219212
setIsActionLoading(false);
220213
}
221214
}
222-
// If result.dismiss, do nothing (cancel)
223215
});
224216
}
225217
};
@@ -307,7 +299,7 @@ const DownloadItem: React.FC<DownloadItemProps> = ({ torrent, clientName, isAdmi
307299
)}
308300
<Menu
309301
anchorEl={menuAnchorEl}
310-
open={Boolean(menuAnchorEl)}
302+
open={menuOpen}
311303
onClose={handleMenuClose}
312304
anchorOrigin={{
313305
vertical: 'bottom',

frontend/src/components/dashboard/base-items/widgets/EditMenu.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import MoreVertIcon from '@mui/icons-material/MoreVert';
2-
import { IconButton, Menu, MenuItem } from '@mui/material';
2+
import { IconButton, MenuItem } from '@mui/material';
33
import React, { useState } from 'react';
44
import { FaHome } from 'react-icons/fa';
55
import { FaArrowRight, FaCopy, FaFile, FaHouse, FaPenToSquare, FaTrash, FaTrashCan } from 'react-icons/fa6';
66

77
import { useAppContext } from '../../../../context/useAppContext';
8+
import { Menu } from '../../../custom-mui';
89

910
type EditMenuProps = {
1011
editMode: boolean;

frontend/src/components/dashboard/base-items/widgets/NotesWidget/MarkdownToolbar.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ import {
88
FormatQuote,
99
Link
1010
} from '@mui/icons-material';
11-
import { Box, FormControl, IconButton, MenuItem, Select, Tooltip } from '@mui/material';
12-
import React from 'react';
11+
import { Box, FormControl, IconButton, MenuItem, Tooltip } from '@mui/material';
12+
import React, { useState } from 'react';
1313
import { GoHeading } from 'react-icons/go';
1414

1515
import { FONT_SIZE_SELECT_OPTIONS } from '../../../../../constants/font-sizes';
16+
import { Select } from '../../../../custom-mui';
1617

1718
interface MarkdownToolbarProps {
1819
onFormat: (type: string, prefix?: string, suffix?: string) => void;
@@ -28,6 +29,8 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
2829
onFontSizeChange,
2930
hideFontSize = false
3031
}) => {
32+
const [isSelectOpen, setIsSelectOpen] = useState(false);
33+
3134
const iconButtonStyle = {
3235
color: 'rgba(255,255,255,0.7)',
3336
'&:hover': { color: 'white' },
@@ -59,7 +62,10 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
5962
<FormControl size='small' sx={{ minWidth: 55, mr: 1 }}>
6063
<Select
6164
value={fontSize}
62-
onChange={(e) => onFontSizeChange(e.target.value)}
65+
open={isSelectOpen}
66+
onOpen={() => setIsSelectOpen(true)}
67+
onClose={() => setIsSelectOpen(false)}
68+
onChange={(e) => onFontSizeChange(e.target.value as string)}
6369
sx={{
6470
color: 'rgba(255,255,255,0.7)',
6571
fontSize: '0.75rem',

0 commit comments

Comments
 (0)