mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-19 23:41:18 +02:00
Move portal to shared ui lib
This commit is contained in:
90
packages/ui/src/components/Overlay.tsx
Normal file
90
packages/ui/src/components/Overlay.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import classNames from 'classnames';
|
||||
import { FocusTrap } from 'focus-trap-react';
|
||||
import * as m from 'motion/react-m';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { Portal } from './Portal';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
portalName: string;
|
||||
open: boolean;
|
||||
onClose?: () => void;
|
||||
zIndex?: keyof typeof zIndexes;
|
||||
variant?: 'default' | 'transparent';
|
||||
noBackdrop?: boolean;
|
||||
}
|
||||
|
||||
const zIndexes: Record<number, string> = {
|
||||
10: 'z-10',
|
||||
20: 'z-20',
|
||||
30: 'z-30',
|
||||
40: 'z-40',
|
||||
50: 'z-50',
|
||||
};
|
||||
|
||||
export function Overlay({
|
||||
variant = 'default',
|
||||
zIndex = 30,
|
||||
open,
|
||||
onClose,
|
||||
portalName,
|
||||
noBackdrop,
|
||||
children,
|
||||
}: Props) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
if (noBackdrop) {
|
||||
return (
|
||||
<Portal name={portalName}>
|
||||
{open && (
|
||||
<FocusTrap focusTrapOptions={{ clickOutsideDeactivates: true }}>
|
||||
{/* NOTE: <div> wrapper is required for some reason, or FocusTrap complains */}
|
||||
<div>{children}</div>
|
||||
</FocusTrap>
|
||||
)}
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal name={portalName}>
|
||||
{open && (
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
// Allow outside click so we can click things like toasts
|
||||
allowOutsideClick: true,
|
||||
delayInitialFocus: true,
|
||||
checkCanFocusTrap: async () => {
|
||||
// Not sure why delayInitialFocus: true doesn't help, but having this no-op promise
|
||||
// seems to be required to make things work.
|
||||
},
|
||||
}}
|
||||
>
|
||||
<m.div
|
||||
ref={containerRef}
|
||||
className={classNames('fixed inset-0', zIndexes[zIndex])}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
>
|
||||
<div
|
||||
aria-hidden
|
||||
onClick={onClose}
|
||||
className={classNames(
|
||||
'absolute inset-0',
|
||||
variant === 'default' && 'bg-backdrop backdrop-blur-sm',
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Show the draggable region at the top */}
|
||||
{/* TODO: Figure out tauri drag region and also make clickable still */}
|
||||
{variant === 'default' && (
|
||||
<div data-tauri-drag-region className="absolute top-0 left-0 h-md right-0" />
|
||||
)}
|
||||
{children}
|
||||
</m.div>
|
||||
</FocusTrap>
|
||||
)}
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
13
packages/ui/src/components/Portal.tsx
Normal file
13
packages/ui/src/components/Portal.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { usePortal } from '../hooks/usePortal';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function Portal({ children, name }: Props) {
|
||||
const portal = usePortal(name);
|
||||
return createPortal(children, portal);
|
||||
}
|
||||
@@ -13,6 +13,8 @@ interface Props {
|
||||
onWidthChange: (width: number) => void;
|
||||
hidden?: boolean;
|
||||
onHiddenChange?: (hidden: boolean) => void;
|
||||
floating?: boolean;
|
||||
floatingWidth?: number;
|
||||
defaultWidth?: number;
|
||||
minWidth?: number;
|
||||
className?: string;
|
||||
@@ -25,6 +27,8 @@ export function SidebarLayout({
|
||||
onWidthChange,
|
||||
hidden = false,
|
||||
onHiddenChange,
|
||||
floating = false,
|
||||
floatingWidth = 320,
|
||||
defaultWidth = 250,
|
||||
minWidth = 50,
|
||||
className,
|
||||
@@ -75,6 +79,28 @@ export function SidebarLayout({
|
||||
onWidthChange(defaultWidth);
|
||||
}, [onWidthChange, defaultWidth]);
|
||||
|
||||
if (floating) {
|
||||
return (
|
||||
<div className={classNames(className, 'relative w-full h-full overflow-hidden')}>
|
||||
{children}
|
||||
{!hidden && (
|
||||
<>
|
||||
<div
|
||||
className="absolute inset-0 bg-black/50 z-20 transition-opacity"
|
||||
onClick={() => onHiddenChange?.(true)}
|
||||
/>
|
||||
<div
|
||||
style={{ width: floatingWidth }}
|
||||
className="absolute top-0 left-0 bottom-0 z-20 animate-slide-in-left"
|
||||
>
|
||||
{sidebar}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={styles}
|
||||
|
||||
26
packages/ui/src/hooks/usePortal.ts
Normal file
26
packages/ui/src/hooks/usePortal.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useRef } from 'react';
|
||||
|
||||
const PORTAL_CONTAINER_ID = 'react-portal';
|
||||
|
||||
export function usePortal(name: string) {
|
||||
const ref = useRef(getOrCreatePortal(name));
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
function getOrCreatePortal(name: string) {
|
||||
let portalContainer = document.getElementById(PORTAL_CONTAINER_ID);
|
||||
if (!portalContainer) {
|
||||
portalContainer = document.createElement('div');
|
||||
portalContainer.id = PORTAL_CONTAINER_ID;
|
||||
document.body.appendChild(portalContainer);
|
||||
}
|
||||
|
||||
let existing = portalContainer.querySelector(`:scope > [data-portal-name="${name}"]`);
|
||||
if (!existing) {
|
||||
const el = document.createElement('div');
|
||||
el.setAttribute('data-portal-name', name);
|
||||
portalContainer.appendChild(el);
|
||||
existing = el;
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
@@ -28,3 +28,6 @@ export type { SplitLayoutLayout, SlotProps } from "./components/SplitLayout";
|
||||
export { Table, TableBody, TableHead, TableRow, TableCell, TruncatedWideTableCell, TableHeaderCell } from "./components/Table";
|
||||
export { clamp } from "./lib/clamp";
|
||||
export { useContainerSize } from "./hooks/useContainerSize";
|
||||
export { Overlay } from "./components/Overlay";
|
||||
export { Portal } from "./components/Portal";
|
||||
export { usePortal } from "./hooks/usePortal";
|
||||
|
||||
Reference in New Issue
Block a user