refactor: Integrate PortalDropdown component into PageHeaderActions for improved dropdown management and layout consistency
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 17m4s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 17m4s
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
|||||||
IconPlayerPlay,
|
IconPlayerPlay,
|
||||||
IconBolt
|
IconBolt
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
import PortalDropdown from './PortalDropdown.jsx';
|
||||||
|
|
||||||
function PageHeaderActions({
|
function PageHeaderActions({
|
||||||
loading = false,
|
loading = false,
|
||||||
@@ -39,17 +40,19 @@ function PageHeaderActions({
|
|||||||
<IconPlayerPlay className="me-1" /> Фоновое обновление
|
<IconPlayerPlay className="me-1" /> Фоновое обновление
|
||||||
</button>
|
</button>
|
||||||
{(onBackgroundUpdate && onOnlineUpdate) && (
|
{(onBackgroundUpdate && onOnlineUpdate) && (
|
||||||
<>
|
<PortalDropdown
|
||||||
<button className="btn btn-outline-success dropdown-toggle dropdown-toggle-split" type="button" data-bs-toggle="dropdown" data-bs-container="body" aria-expanded="false" aria-label="Другие варианты обновления"></button>
|
buttonClassName="btn btn-outline-success dropdown-toggle dropdown-toggle-split"
|
||||||
<div className="dropdown-menu">
|
buttonProps={{ 'aria-label': 'Другие варианты обновления' }}
|
||||||
<button className="dropdown-item" type="button" onClick={onBackgroundUpdate}>
|
align="right"
|
||||||
<IconPlayerPlay className="me-1" /> Фоновое обновление
|
buttonContent={null}
|
||||||
</button>
|
>
|
||||||
<button className="dropdown-item" type="button" onClick={onOnlineUpdate}>
|
<button className="dropdown-item" type="button" onClick={onBackgroundUpdate}>
|
||||||
<IconBolt className="me-1" /> Online-обновление
|
<IconPlayerPlay className="me-1" /> Фоновое обновление
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<button className="dropdown-item" type="button" onClick={onOnlineUpdate}>
|
||||||
</>
|
<IconBolt className="me-1" /> Online-обновление
|
||||||
|
</button>
|
||||||
|
</PortalDropdown>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -63,10 +66,12 @@ function PageHeaderActions({
|
|||||||
)}
|
)}
|
||||||
{(onImport || onExport) && (
|
{(onImport || onExport) && (
|
||||||
<div className="dropdown">
|
<div className="dropdown">
|
||||||
<button className="btn btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" data-bs-container="body" aria-expanded="false" title="Импорт/Экспорт">
|
<PortalDropdown
|
||||||
<IconDownload className="me-1" /> Импорт/Экспорт
|
buttonClassName="btn btn-outline-primary dropdown-toggle"
|
||||||
</button>
|
buttonProps={{ title: 'Импорт/Экспорт' }}
|
||||||
<div className="dropdown-menu">
|
align="left"
|
||||||
|
buttonContent={<><IconDownload className="me-1" /> Импорт/Экспорт</>}
|
||||||
|
>
|
||||||
{onImport && (
|
{onImport && (
|
||||||
<button className="dropdown-item" type="button" onClick={onImport}>
|
<button className="dropdown-item" type="button" onClick={onImport}>
|
||||||
<IconUpload className="me-1" /> Импорт
|
<IconUpload className="me-1" /> Импорт
|
||||||
@@ -77,7 +82,7 @@ function PageHeaderActions({
|
|||||||
<IconDownload className="me-1" /> Экспорт
|
<IconDownload className="me-1" /> Экспорт
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</PortalDropdown>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{onClear && (
|
{onClear && (
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
{...buttonProps}
|
||||||
|
ref={buttonRef}
|
||||||
|
type="button"
|
||||||
|
className={buttonClassName}
|
||||||
|
onClick={(e) => { e.preventDefault(); setOpen((v) => !v); }}
|
||||||
|
aria-expanded={open ? 'true' : 'false'}
|
||||||
|
>
|
||||||
|
{buttonContent}
|
||||||
|
</button>
|
||||||
|
{open && body && createPortal(
|
||||||
|
<div
|
||||||
|
ref={menuRef}
|
||||||
|
style={{ position: 'absolute', top: `${coords.top}px`, left: `${coords.left}px`, zIndex: 10000, minWidth: `${coords.minWidth}px` }}
|
||||||
|
>
|
||||||
|
<div className={menuClassName} style={{ display: 'block' }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
body
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PortalDropdown;
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user