Files
HMDesign/packages/org-chart-react/src/HMEmployeeDialog.tsx
T

75 lines
3.8 KiB
TypeScript

"use client";
import { useLayoutEffect, useRef } from 'react';
import {
Avatar, AvatarFallback, AvatarImage, Button, Dialog, DialogBody, DialogClose,
DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
} from '@hm-design/ui';
import type { OrgChartEmployeeSelection, OrgChartEmployeePopupProps } from './types.js';
const statusNames = { active: '재직', dispatched: '파견', onLeave: '휴직' } as const;
/** Explicit React-only adapter. The Core and default entry stay UI-independent. */
export default function HMEmployeeDialog({ selection, onClose }: OrgChartEmployeePopupProps) {
const returnTarget = useRef<OrgChartEmployeeSelection | null>(null);
const returnRoot = useRef<ShadowRoot | null>(null);
const returnContainer = useRef<HTMLElement | null>(null);
useLayoutEffect(() => {
if (!selection) return;
returnTarget.current = selection;
const root = selection.context.trigger.getRootNode();
const Shadow = selection.context.trigger.ownerDocument.defaultView?.ShadowRoot;
returnRoot.current = Shadow && root instanceof Shadow ? root : null;
returnContainer.current = returnRoot.current?.host.parentElement ?? null;
}, [selection]);
function restoreFocus(event: Event) {
event.preventDefault();
const previous = returnTarget.current;
returnTarget.current = null;
if (!previous) return;
const { trigger } = previous.context;
// A mode/selection render can replace the card while the Portal is open.
// Retain its ShadowRoot before replacement and resolve the same employee.
const shadow = returnRoot.current;
const currentRoot = shadow?.host.isConnected ? shadow : returnContainer.current?.firstElementChild?.shadowRoot;
const replacement = [...currentRoot?.querySelectorAll<HTMLElement>('.hm-orgchart-employee') ?? []]
.find(card => card.dataset.employeeId === previous.employee.id);
const target = trigger.isConnected ? trigger : replacement
?? currentRoot?.querySelector<HTMLElement>('.hm-orgchart-shell');
target?.focus({ preventScroll: true });
}
const displayed = selection ?? returnTarget.current;
const employee = displayed?.employee;
const context = displayed?.context;
const image = employee?.profileImage
?? context?.trigger.querySelector<HTMLImageElement>('.hm-orgchart-avatar img')?.src;
const fields: [string, string | null | undefined][] = employee ? [
['이름', employee.name], ['직위', employee.position],
['소속 회사', context?.affiliation?.name], ['소속 조직', context?.organization?.name],
['직책', employee.jobTitle], ['근무 상태', statusNames[employee.workStatus]],
] : [];
return <Dialog open={selection !== null} onOpenChange={open => { if (!open) onClose(); }}>
<DialogContent onCloseAutoFocus={restoreFocus} data-component="HMOrgChartEmployeeDialog" data-employee-id={employee?.id}>
<DialogHeader>
<DialogTitle>직원 정보</DialogTitle>
<DialogDescription>조직도에서 선택한 직원의 정보를 확인합니다.</DialogDescription>
</DialogHeader>
<DialogBody>
<Avatar size="lg">
{image ? <AvatarImage src={image} alt="" referrerPolicy="no-referrer" /> : null}
<AvatarFallback>{employee?.name.slice(0, 1)}</AvatarFallback>
</Avatar>
<dl style={{ display: 'grid', gap: '0.75rem', margin: '1rem 0 0' }}>{fields.filter(([, value]) => Boolean(value)).map(([label, value]) => <div key={label} style={{ display: 'grid', gridTemplateColumns: '5rem minmax(0, 1fr)', gap: '0.75rem' }}>
<dt style={{ fontWeight: 500 }}>{label}</dt><dd style={{ margin: 0, minWidth: 0, overflowWrap: 'anywhere' }}>{value}</dd>
</div>)}</dl>
</DialogBody>
<DialogFooter>
<DialogClose asChild><Button variant="cancel" appearance="solid">닫기</Button></DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>;
}