18 KiB
@hm-design/org-chart-react v0.1
React 19 lifecycle adapter for @hm-design/org-chart@0.1.0. The wrapper delegates organization DOM, engine work, validation, summaries, interactions and Shadow DOM styles to the existing Vanilla Core. Auto can optionally show employee information with the public HM Dialog in a separate React Portal. It has no navigation, page layout, fetch client or separate node design.
Usage
import { useMemo } from 'react';
import { JsonOrgChartDataSource } from '@hm-design/org-chart';
import { OrgChart } from '@hm-design/org-chart-react';
import type { OrgChartDataset, OrgChartState } from '@hm-design/org-chart-react';
import engineCssUrl from '@hm-design/org-chart/engine.css?url';
import coreCssUrl from '@hm-design/org-chart/style.css?url';
// Core P1 supplies standalone OrgChart design; no HM Theme/font import needed.
const stylesheetUrls = { engine: engineCssUrl, core: coreCssUrl };
export function OrganizationContent({ data }: { data: OrgChartDataset }) {
const dataSource = useMemo(() => new JsonOrgChartDataSource(data), [data]);
const onStateChange = (state: OrgChartState) => {
// Data/source/validation/style failures are state.status === 'error'.
// A ready state contains the Core's shared summary and dataVersion.
void state;
};
return (
<OrgChart
dataSource={dataSource}
stylesheetUrls={stylesheetUrls}
onStateChange={onStateChange}
onLifecycleError={error => { console.error('OrgChart lifecycle failed', error); }}
/>
);
}
The ?url imports above are Vite asset imports, not a universal browser feature. Other hosts resolve the two Core public CSS entries to actual deployment URLs. Importing CSS only into the document does not style the Core's Shadow DOM. Engine CSS is linked first, then standalone Core CSS. P1 owns its colors, typography and geometry without depending on HM Theme. Set public --hm-orgchart-* properties on the wrapper container to override only this instance. No HM common variables are redefined. The default entry has no runtime or declaration dependency on HM UI. For independent Tree/Summary integration, use the Core factories in a ref/effect without copying their UI.
Explicit popup adapters
Minimum React installation: @hm-design/org-chart, @hm-design/org-chart-react, React 19 and the host renderer (usually react-dom). HM UI, HM Theme and Tailwind are unnecessary for the default entry and Core popup. Core + Vanilla requires only @hm-design/org-chart and its public CSS URLs.
For the existing HM Dialog, add the optional @hm-design/ui peer (0.1.0, linked from the same HMDesign Git Submodule + npm Workspace; it is not installed from an npm registry) and its required peers, provide the host's real HM Theme/Tailwind CSS, and explicitly select both the entry and popup mode:
import { OrgChart } from '@hm-design/org-chart-react/hm-dialog';
// Existing dataSource, stylesheetUrls and all other Props are unchanged.
<OrgChart dataSource={dataSource} stylesheetUrls={stylesheetUrls}
layoutMode="auto" employeePopup="hm-dialog" />;
Migration from the previous HM popup setup: change the OrgChart import to /hm-dialog; keep employeePopup="hm-dialog". Installing UI alone never switches the popup. Even the HM entry defaults to the Core popup. The default entry can also use the exported HMEmployeeDialog as employeePopupAdapter explicitly.
For another design system, keep the default entry and implement a React component with the public OrgChartEmployeePopupProps contract:
import { OrgChart, type OrgChartEmployeePopupProps } from '@hm-design/org-chart-react';
// MyEmployeePopup receives { selection, onClose }; selection is null when closed.
// Read selection.employee/context without mutating them. Own accessible modal
// semantics, Escape/close behavior and focus restoration to context.trigger.
<OrgChart dataSource={dataSource} stylesheetUrls={stylesheetUrls} layoutMode="auto"
employeePopup="adapter" employeePopupAdapter={MyEmployeePopup} />;
Neither Core nor the dataset changes. Supplying an adapter alone does not enable it. An explicit onEmployeeClick still takes precedence. If an adapter mode is requested without an adapter, the wrapper retains the Core popup and reports a configuration error through onLifecycleError. Fixed continues to use its existing Core popup. Department Colors and Overview remain Storybook-only pages and have no new exports.
Public API
Default named runtime export: OrgChart. /hm-dialog exports the same OrgChart Props and HMEmployeeDialog adapter. Type exports: OrgChartProps, OrgChartEmployeeSelection, OrgChartEmployeePopupProps and the Core's public types, reexported as types only. Runtime helpers such as JsonOrgChartDataSource remain available from @hm-design/org-chart.
OrgChartProps reuses Omit<OrgChartOptions, 'container' | 'onStateChange'>:
- Required
dataSource: the same CoreOrgChartDataSourceinterface,load(options?: { signal?: AbortSignal }): Promise<unknown>. - Required
stylesheetUrls:{ engine: string; core: string }, the exact Core asset contract. - Optional
displayMode:'interactive'(default) or'readOnly', passed to the Core composition. Changing it recreates the Core handle. - Optional
layoutMode:'fixed'(default) or'auto', independent ofdisplayMode. Auto renders company forests, collapse and Zoom/Pan/Fit. P5-4 retains the current Expanded card presentation by user decision; external SVG matching is no longer required. Changing mode recreates the Core handle.
See Expanded usage and styling for company selection, top nodes, JSON sources, popup ownership and Auto-only CSS examples. Keyboard focus reveals offscreen controls at the current Zoom; group-title Hover feedback and drag/keyboard activation are implemented in the shared Core.
- Optional
companyId: legacy Auto fallback company preset (defaulthanmac). Explicit/inherited organization company identity takes priority; Fixed ignores company colors. Changing it recreates the handle. See Core README. - Optional
selectedCompanyIds: undefined selects all, [] selects none. Changes call Core selection on the existing instance, preserving per-company collapse state and source counts. - Optional
autoLayout:{ type: 'tb' | 'lr' | 'hybrid', hybridStartDepth: 2 | 3 | 4, initialVisibleDepth: 1 | 2 | 3 | 'all', hybridOrganizationIds?: readonly string[] }(each field optional). Changes use CoresetAutoLayouton the existing instance; TB/LR/Hybrid changes retain valid collapse state. Per-group hybrid IDs apply only to TB; clear them when selecting LR/global Hybrid. Omission follows Core defaults (TB, Hybrid depth 3, all visible). - Optional
interactions:{ collapse?: boolean, zoom?: boolean, pan?: boolean }, all enabled by default. Auto independently enables/disables group toggles, Zoom controls/wheel Zoom and drag Pan via CoresetInteractionson the same instance. Omitted fields are enabled, and removing the prop restores all three defaults. Existing collapse and viewport state remain; TB/LR/Hybrid has no interaction prerequisite. Fixed ignores this option and keeps its existing behavior. - Optional
employeePopup:'core'(default),'hm-dialog','adapter', or'none'. Applies only to Auto; Fixed retains its existing Core popup. HM Dialog requires the explicit/hm-dialogentry oremployeePopupAdapter={HMEmployeeDialog}, the optional@hm-design/ui@0.1.0peer and the consumer's real HM Theme/Tailwind CSS entry. An explicitonEmployeeClickalways takes precedence and suppresses either default popup. Host popup state closes on source/company changes, adapter replacement and successful movement. HM focus restoration still resolves the employee's replacement card in the original ShadowRoot after a layout render. Auto uses CoresetEmployeeClickHandlerto change popup ownership on the same instance, retaining company selection, layout, collapse and viewport state. - Optional
employeePopupAdapter: ReactComponentType<OrgChartEmployeePopupProps>. Receives the untouched employee/context selection andonClose; used only with an explicit adapter popup mode. No global registration or package-presence detection is performed. - Optional
companyTopNodes,employeeConnections,zoom: immutable Core configuration maps/range. Identity changes recreate the handle; memoize these objects. Names, positions and profile overrides affect only Auto presentation, not callback data. - Optional
organizationLinks: existing organization-ID-to-URL map, applied only to original 1Depth titles. Missing/blank values retain plain titles. Core validates HTTP(S)/relative URLs and rejects invalid URLs, other schemes and credentials. Treat the map as immutable and memoize it; a new map recreates the handle. - Optional
onEmployeeClick(employee, context): host-owned information UI, replacing the Core's built-in popup.contextcontains the existingorganization,affiliation, focusabletriggercard andoriginalEvent. It works in both modes and receives untouched Employee data. - Optional
onMove(result: OrgChartMoveResult): latest committed callback receives Auto employee/group move outcomes (moveddistinguishes success from rejection). Successful moves close an open HM employee Dialog, so its employee context cannot become stale. Changing this callback does not recreate or reload Core. Movement changes the Core's in-memory snapshot; no fixture or server is written. - Optional
aggregationPolicy: the Core'sApprovedAggregationPolicy. - Optional
classNameandstyle: applied only to React's outer container. - Optional
onStateChange(state: OrgChartState): latest committed callback receives actual Core state changes. - Optional
onLifecycleError(error: unknown): reports unexpected Core creation or rejected mount/reload calls and unexpected cleanup errors. Controlled data, source, validation, rendering and stylesheet failures use the Core'serrorstate instead.
The container is owned by React; its dedicated children are owned by Core. There are no React children, imperative handle, title, employee or engine manipulation Props. Titles, descriptions, presentation groups, organization order and metadata use the common data contract. An omitted lifecycle error callback leaves unexpected operation errors handled but does not produce a second error UI or console log. Consumer state callback errors follow the Core's isolation behavior; lifecycle error callback exceptions are also contained.
Without onEmployeeClick, employee cards follow employeePopup in Auto: Core read-only information dialog (default), public HM Dialog, or no popup. Fixed always retains the Core popup. Both dialogs use only existing employee/organization/affiliation fields and omit missing values. Core readOnly names match the displayed card while source data remains unchanged; HM Dialog shows source employee data. The native modal sits outside the scaled Stage and HM Dialog uses a React Portal; both support Close, ESC, focus trapping and return to the card. Group-link events remain separate; a visible name tooltip closes before popup activation. A custom callback owns its popup's accessibility and cleanup.
<OrgChart
dataSource={dataSource}
stylesheetUrls={stylesheetUrls}
displayMode="readOnly"
organizationLinks={configuredOrganizationLinks}
onEmployeeClick={(employee, context) => openEmployeeInformation(employee, context)}
/>
Both map and popup function above are supplied by the host; no real destination URL or employee information is embedded in the package. Omit the callback for the default popup.
Lifecycle and updates
P9.4 adds dragDropEnabled?: boolean (default false) for Auto. Pass true to
enable employee/group pointer, keyboard and public Core moves. Changing this prop
calls the shared Core setDragDropEnabled synchronously on the existing handle;
it neither remounts nor reloads the source. Switching OFF cancels active movement,
clears highlights and retains completed moves, popup, collapse and viewport state.
Pan/Zoom/Fit remain independent. Fixed ignores the prop.
Mount creates one new Core handle, subscribes to its state and calls mount(container). Mount already loads the source; the wrapper does not perform an additional initial reload. The wrapper does not synthesize idle, destroyed or empty callbacks. Current Core emits loading, then ready/error; employees [] is ready, organizations [] is error. An empty type member does not establish a supported Empty State.
A new dataSource object triggers the existing handle's reload(nextSource). This starts Core's latest revision and aborts or ignores older responses. Updating fields inside the same source object is not a reload signal. JsonOrgChartDataSource captures inline input at its construction; create a new source for a new immutable snapshot. Memoize sources rather than constructing one on every render.
Changing callback identity updates the latest committed callback without reload or recreation. In Auto, adding/removing onEmployeeClick or changing employeePopup updates Core's employee handler without replacing the handle. Fixed retains its original ownership policy: adding/removing onEmployeeClick recreates the handle. Abandoned concurrent renders do not update callbacks. Policy or organizationLinks identity changes, displayMode changes, or either stylesheet URL string value changes dispose the previous handle and create a new one because these settings have no Core setter. Replacing the stylesheet URL object with the same two strings does not recreate. Treat policy and link-map objects as immutable and memoize them; changing an object internally is not a configuration update signal.
Auto separates blank-space Pan from employee-card/group-title movement. Click continues to open employee information or toggle a group; employees and groups may move within or across companies. Employee moves update organizationId; group moves update parentId and retain the subtree and IDs. Within-company moves preserve existing affiliations, including dispatched employees. Cross-company employee moves additionally update affiliationId to the target company; cross-company group moves update the moved group's and every descendant's companyId and their employees' affiliationId. The target company controls colors and counts; leaders and company roots remain protected. Focus a card/title and press Alt+M to begin keyboard movement, arrow keys to choose a target, Enter to confirm, or ESC to cancel. Employee cards retain a keyboard focus entry point when employeePopup="none". Fixed has no movement editing.
Cleanup invalidates the wrapper generation, unsubscribes, calls Core's terminal destroy() and clears its handle reference. React Strict Mode's setup → cleanup → setup uses a fresh handle. Old generations cannot deliver state/lifecycle callbacks to the new instance; superseded operation rejection handlers are also ignored. Cleanup runs before replacement, leaving at most one active Core per wrapper container. The wrapper does not repeat engine disposal or DOM removal itself.
Auto retains valid organization collapse state on source/selection updates, fits reloads/selection and restores toggle focus. Fixed retains its previous regeneration policy. Source errors can recover by replacing the source; Core validation/summary/node behavior remains identical to Vanilla. Invalid runtime Props that prevent Core construction are lifecycle usage errors: fix them and remount the component (for example by changing its React key). No rendered fallback silently replaces an invalid configuration.
Committed callback publication, Core ownership and source updates use layout effects in that order. Cleanup/reload runs synchronously with React commit, so a pending old response cannot notify in a gap before passive cleanup. Server rendering produces only the outer container; SSR/hydration remains outside the verified scope.
Package boundary and verification limits
The build emits separate ESM dist/index.js and dist/hm-dialog.js entries, their shared UI-independent wrapper chunk, and TypeScript declarations. It preserves the "use client" boundary and leaves React (including react/jsx-runtime), @hm-design/org-chart, and optional @hm-design/ui external. Only the HM entry imports UI. React is a ^19.0.0 peer, matching the repository's existing React package convention; Core 0.1.0 is a runtime dependency. React DOM is supplied by the consuming app and is not imported by this package. Core must be built before wrapper typecheck/build in this workspace because dependency resolution uses its public declarations. Building all entries, including the HM adapter, also needs HM UI declarations in the maintainer workspace; installing the packed default entry does not.
From the repository root:
npm run typecheck --workspace @hm-design/org-chart-react
npm run build --workspace @hm-design/org-chart-react
Storybook is one React host, not proof of every consumer deployment. The adapter delegates Auto Pan/Zoom to Core; it adds no alternate layout, additional relation overlays or API client. SSR/hydration, full screen reader behavior, physical touch, complete heap/GC recovery, HM Dialog without its Theme, Workdesk reference visual approval and every deployment environment require their own evidence. Default/Core rendering requires no HM Theme. Public ESM import without a DOM does not establish SSR/hydration support. Consult the minimal-install verification report for actual packed-package consumer results.
AI coding tools
AI coding tools should read ai/AI_GUIDE.md first (shared HMDesign entry point). ai/AI_USAGE.md points to the OrgChart rules, API catalog and patterns shipped in @hm-design/org-chart/ai/. ai/ is included in the packed package.