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,
|
||||
IconBolt
|
||||
} from '@tabler/icons-react';
|
||||
import PortalDropdown from './PortalDropdown.jsx';
|
||||
|
||||
function PageHeaderActions({
|
||||
loading = false,
|
||||
@@ -39,17 +40,19 @@ function PageHeaderActions({
|
||||
<IconPlayerPlay className="me-1" /> Фоновое обновление
|
||||
</button>
|
||||
{(onBackgroundUpdate && onOnlineUpdate) && (
|
||||
<>
|
||||
<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>
|
||||
<div className="dropdown-menu">
|
||||
<button className="dropdown-item" type="button" onClick={onBackgroundUpdate}>
|
||||
<IconPlayerPlay className="me-1" /> Фоновое обновление
|
||||
</button>
|
||||
<button className="dropdown-item" type="button" onClick={onOnlineUpdate}>
|
||||
<IconBolt className="me-1" /> Online-обновление
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
<PortalDropdown
|
||||
buttonClassName="btn btn-outline-success dropdown-toggle dropdown-toggle-split"
|
||||
buttonProps={{ 'aria-label': 'Другие варианты обновления' }}
|
||||
align="right"
|
||||
buttonContent={null}
|
||||
>
|
||||
<button className="dropdown-item" type="button" onClick={onBackgroundUpdate}>
|
||||
<IconPlayerPlay className="me-1" /> Фоновое обновление
|
||||
</button>
|
||||
<button className="dropdown-item" type="button" onClick={onOnlineUpdate}>
|
||||
<IconBolt className="me-1" /> Online-обновление
|
||||
</button>
|
||||
</PortalDropdown>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -63,10 +66,12 @@ function PageHeaderActions({
|
||||
)}
|
||||
{(onImport || onExport) && (
|
||||
<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="Импорт/Экспорт">
|
||||
<IconDownload className="me-1" /> Импорт/Экспорт
|
||||
</button>
|
||||
<div className="dropdown-menu">
|
||||
<PortalDropdown
|
||||
buttonClassName="btn btn-outline-primary dropdown-toggle"
|
||||
buttonProps={{ title: 'Импорт/Экспорт' }}
|
||||
align="left"
|
||||
buttonContent={<><IconDownload className="me-1" /> Импорт/Экспорт</>}
|
||||
>
|
||||
{onImport && (
|
||||
<button className="dropdown-item" type="button" onClick={onImport}>
|
||||
<IconUpload className="me-1" /> Импорт
|
||||
@@ -77,7 +82,7 @@ function PageHeaderActions({
|
||||
<IconDownload className="me-1" /> Экспорт
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</PortalDropdown>
|
||||
</div>
|
||||
)}
|
||||
{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