diff --git a/frontend/src/components/PageHeaderActions.jsx b/frontend/src/components/PageHeaderActions.jsx
index 54765a3..cb59bbc 100644
--- a/frontend/src/components/PageHeaderActions.jsx
+++ b/frontend/src/components/PageHeaderActions.jsx
@@ -6,6 +6,7 @@ import {
IconPlayerPlay,
IconBolt
} from '@tabler/icons-react';
+import PortalDropdown from './PortalDropdown.jsx';
function PageHeaderActions({
loading = false,
@@ -39,17 +40,19 @@ function PageHeaderActions({
Фоновое обновление
{(onBackgroundUpdate && onOnlineUpdate) && (
- <>
-
-
-
-
-
- >
+
+
+
+
)}
)}
@@ -63,10 +66,12 @@ function PageHeaderActions({
)}
{(onImport || onExport) && (
-
-
+
Импорт/Экспорт>}
+ >
{onImport && (
)}
-
+
)}
{onClear && (
diff --git a/frontend/src/components/PortalDropdown.jsx b/frontend/src/components/PortalDropdown.jsx
new file mode 100644
index 0000000..1b98581
--- /dev/null
+++ b/frontend/src/components/PortalDropdown.jsx
@@ -0,0 +1,95 @@
+import { useEffect, useLayoutEffect, useRef, useState } from 'react';
+import { createPortal } from 'react-dom';
+
+function getBody() {
+ if (typeof document === 'undefined') return null;
+ return document.body;
+}
+
+function PortalDropdown({
+ buttonClassName = 'btn btn-outline-secondary dropdown-toggle',
+ buttonProps = {},
+ align = 'left',
+ children,
+ buttonContent,
+ menuClassName = 'dropdown-menu show',
+}) {
+ const buttonRef = useRef(null);
+ const menuRef = useRef(null);
+ const [open, setOpen] = useState(false);
+ const [coords, setCoords] = useState({ top: 0, left: 0, minWidth: 0 });
+
+ const updatePosition = () => {
+ const btn = buttonRef.current;
+ if (!btn) return;
+ const rect = btn.getBoundingClientRect();
+ const minWidth = rect.width; // make menu at least as wide as button
+ let left = rect.left;
+ if (align === 'right') {
+ const menu = menuRef.current;
+ const menuWidth = menu ? menu.offsetWidth : 0;
+ left = Math.max(0, rect.right - (menuWidth || rect.width));
+ }
+ setCoords({ top: rect.bottom + window.scrollY, left: left + window.scrollX, minWidth });
+ };
+
+ useLayoutEffect(() => {
+ if (open) updatePosition();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [open]);
+
+ useEffect(() => {
+ if (!open) return;
+ const onScroll = () => updatePosition();
+ const onResize = () => updatePosition();
+ const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
+ const onClickOutside = (e) => {
+ if (!menuRef.current || !buttonRef.current) return;
+ if (!menuRef.current.contains(e.target) && !buttonRef.current.contains(e.target)) {
+ setOpen(false);
+ }
+ };
+ window.addEventListener('scroll', onScroll, true);
+ window.addEventListener('resize', onResize);
+ document.addEventListener('keydown', onKey);
+ document.addEventListener('mousedown', onClickOutside);
+ return () => {
+ window.removeEventListener('scroll', onScroll, true);
+ window.removeEventListener('resize', onResize);
+ document.removeEventListener('keydown', onKey);
+ document.removeEventListener('mousedown', onClickOutside);
+ };
+ }, [open]);
+
+ const body = getBody();
+
+ return (
+ <>
+
+ {open && body && createPortal(
+ ,
+ body
+ )}
+ >
+ );
+}
+
+export default PortalDropdown;
+
+