feat(org-chart): add core cards and fixed-layout integration
This commit is contained in:
1 parent
d18e3c3380
commit
61c6f8d1f1
161 files changed
+13044
-1
No files matched your search
@@ -0,0 +1,105 @@
|
||||
import type { Employee } from '../data/index.js';
|
||||
import type { OrgChartEmployeeClickContext } from './types.js';
|
||||
|
||||
const statusNames = { active: '재직', dispatched: '파견', onLeave: '휴직' } as const;
|
||||
|
||||
export interface EmployeeInfoDialog {
|
||||
show(employee: Employee, context: OrgChartEmployeeClickContext): void;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
/** The native modal lives in the unscaled shell and enters the browser's top
|
||||
* layer. It keeps the Core shared by HTML and React, including focus handling. */
|
||||
export function createEmployeeInfoDialog(
|
||||
shell: HTMLElement, signal: AbortSignal, prefix: string, readOnly: boolean,
|
||||
): EmployeeInfoDialog {
|
||||
const document = shell.ownerDocument;
|
||||
const dialog = document.createElement('dialog');
|
||||
dialog.className = 'hm-orgchart-employee-dialog';
|
||||
dialog.dataset.component = 'EmployeeInfoDialog';
|
||||
dialog.setAttribute('aria-modal', 'true');
|
||||
const header = document.createElement('div');
|
||||
header.className = 'hm-orgchart-employee-dialog-header';
|
||||
const title = document.createElement('h2');
|
||||
title.id = `${prefix}-employee-dialog-title`;
|
||||
title.textContent = '직원 정보';
|
||||
dialog.setAttribute('aria-labelledby', title.id);
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.type = 'button';
|
||||
closeButton.className = 'hm-orgchart-employee-dialog-close';
|
||||
closeButton.textContent = '닫기';
|
||||
closeButton.setAttribute('aria-label', '직원 정보 닫기');
|
||||
const fields = document.createElement('dl');
|
||||
fields.className = 'hm-orgchart-employee-dialog-fields';
|
||||
header.append(title, closeButton);
|
||||
dialog.append(header, fields);
|
||||
shell.append(dialog);
|
||||
let trigger: HTMLElement | undefined;
|
||||
|
||||
function restoreFocus(): void {
|
||||
const previous = trigger;
|
||||
// close() restores synchronously, while the native close event is queued.
|
||||
// Only a real activation owns a return target: a second close notification
|
||||
// (or routine render cleanup before the dialog ever opens) must not focus
|
||||
// the shell and displace the card's restored focus.
|
||||
if (!previous) return;
|
||||
trigger = undefined;
|
||||
if (signal.aborted || !shell.isConnected) return;
|
||||
(previous?.isConnected ? previous : shell).focus({ preventScroll: true });
|
||||
}
|
||||
function close(): void {
|
||||
if (dialog.open) dialog.close();
|
||||
restoreFocus();
|
||||
}
|
||||
closeButton.addEventListener('click', close, { signal });
|
||||
dialog.addEventListener('cancel', event => {
|
||||
event.preventDefault();
|
||||
close();
|
||||
}, { signal });
|
||||
dialog.addEventListener('close', () => { if (!dialog.open) restoreFocus(); }, { signal });
|
||||
dialog.addEventListener('keydown', event => {
|
||||
if (event.key !== 'Tab') return;
|
||||
const focusable = [...dialog.querySelectorAll<HTMLElement>('button:not(:disabled), a[href], [tabindex="0"]')];
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
const active = (dialog.getRootNode() as ShadowRoot).activeElement;
|
||||
if ((event.shiftKey && active === first) || (!event.shiftKey && active === last) || !dialog.contains(active)) {
|
||||
event.preventDefault();
|
||||
(event.shiftKey ? last : first)?.focus();
|
||||
}
|
||||
}, { signal });
|
||||
signal.addEventListener('abort', () => {
|
||||
trigger = undefined;
|
||||
if (dialog.open) dialog.close();
|
||||
dialog.remove();
|
||||
}, { once: true });
|
||||
|
||||
return {
|
||||
close,
|
||||
show(employee, context) {
|
||||
if (signal.aborted) return;
|
||||
// Never synthesize contact details, dates or other personal information.
|
||||
const entries: [string, string | null | undefined][] = [
|
||||
['이름', readOnly ? context.trigger.querySelector('.hm-orgchart-person-name')?.textContent ?? employee.name : employee.name],
|
||||
['직위', employee.position],
|
||||
['소속', context.organization?.name],
|
||||
['직책', employee.jobTitle],
|
||||
['관계사', context.affiliation?.name],
|
||||
['근무 상태', statusNames[employee.workStatus]],
|
||||
];
|
||||
fields.replaceChildren();
|
||||
for (const [label, value] of entries) {
|
||||
if (!value) continue;
|
||||
const term = document.createElement('dt');
|
||||
term.textContent = label;
|
||||
const detail = document.createElement('dd');
|
||||
detail.textContent = value;
|
||||
fields.append(term, detail);
|
||||
}
|
||||
dialog.dataset.employeeId = employee.id;
|
||||
trigger = context.trigger;
|
||||
if (!dialog.open) dialog.showModal();
|
||||
closeButton.focus({ preventScroll: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user