@streetui/renderer 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -178,6 +178,20 @@ declare function wireSignalBindings(ctx: RenderContext, graphNode: GraphNode, in
178
178
  * eager build factory (it only ever renders 0..1 branch, so eager is fine).
179
179
  */
180
180
  declare function wireReactiveList(ctx: RenderContext, graphNode: GraphNode, instance: NodeInstance, el: Element): void;
181
+ /**
182
+ * Attach overlay focus/keyboard behavior to a mounted portal. Server-safe: on
183
+ * the server `dom.body()` is null so this returns immediately (SSR emits inert
184
+ * markup, no focus concept). A plain portal has no `__overlay__` descriptor, so
185
+ * this also returns immediately — the behavior is purely additive.
186
+ *
187
+ * The panel is mounted/unmounted by the portal's inner `when(open, …)`, whose
188
+ * signal subscription is registered *before* this one (the conditional child is
189
+ * mounted earlier in the portal branch). Signal subscribers fire synchronously
190
+ * in subscription order, so on open→true the panel DOM exists before we move
191
+ * focus into it, and on open→false the panel is torn down before we restore
192
+ * focus. All listeners are tracked on the instance and torn down on unmount.
193
+ */
194
+ declare function wireOverlayBehavior(ctx: RenderContext, graphNode: GraphNode, instance: NodeInstance, target: Element): void;
181
195
 
182
196
  /**
183
197
  * Patch — targeted DOM updates driven by signal changes.
@@ -431,4 +445,4 @@ declare function renderToString(compiled: CompiledApplication, options?: RenderT
431
445
 
432
446
  declare function resolveTag(type: SemanticNodeType): string;
433
447
 
434
- export { type HydrationDiagnostic, type HydrationDiagnosticSink, type HydrationMismatchType, type MountFn, NodeInstance, type PlanEntry, type ReconcileResult, type RenderContext, type RenderToStringOptions, STATE_MARKER_ATTR, StreetRenderHandle, StreetRendererImpl, type StreetRendererOptions, applyNodeProps, applyProp, buttonUpdate, consoleHydrationDiagnosticSink, createHydrationDiagnosticCollector, createRenderContext, createRenderer, formatHydrationDiagnostic, headingUpdate, hydrateGraph, inputUpdate, mountGraph, mountNode, patchNode, patchProp, readState, reconcileChildren, reconcileChildrenByPlan, renderToString, resolveTag, serializeState, textUpdate, wireEvents, wireReactiveList, wireSignalBindings };
448
+ export { type HydrationDiagnostic, type HydrationDiagnosticSink, type HydrationMismatchType, type MountFn, NodeInstance, type PlanEntry, type ReconcileResult, type RenderContext, type RenderToStringOptions, STATE_MARKER_ATTR, StreetRenderHandle, StreetRendererImpl, type StreetRendererOptions, applyNodeProps, applyProp, buttonUpdate, consoleHydrationDiagnosticSink, createHydrationDiagnosticCollector, createRenderContext, createRenderer, formatHydrationDiagnostic, headingUpdate, hydrateGraph, inputUpdate, mountGraph, mountNode, patchNode, patchProp, readState, reconcileChildren, reconcileChildrenByPlan, renderToString, resolveTag, serializeState, textUpdate, wireEvents, wireOverlayBehavior, wireReactiveList, wireSignalBindings };
package/dist/index.js CHANGED
@@ -138,6 +138,16 @@ function wireEvents(dom, graph, node, element, instance) {
138
138
  }
139
139
  }
140
140
 
141
+ // src/mount.ts
142
+ import {
143
+ focusInitial,
144
+ trapFocus,
145
+ containFocus,
146
+ onEscape,
147
+ saveFocus,
148
+ restoreFocus
149
+ } from "@streetui/dom";
150
+
141
151
  // src/tag-map.ts
142
152
  var TAG_MAP = {
143
153
  application: "div",
@@ -156,7 +166,12 @@ var TAG_MAP = {
156
166
  component: "div",
157
167
  slot: "div",
158
168
  fragment: "div",
159
- "reactive-list": "ul"
169
+ "reactive-list": "ul",
170
+ // A portal renders as a neutral inline anchor <div> at its declaration site;
171
+ // its children are relocated to a document.body container on the browser
172
+ // (see the portal branch in mount.ts). On the server (no body) it renders
173
+ // inline, so the anchor tag is what SSR/hydration positionally match on.
174
+ portal: "div"
160
175
  };
161
176
  function resolveTag(type) {
162
177
  return TAG_MAP[type] ?? "div";
@@ -568,6 +583,28 @@ function mountNode(ctx, graphNode, parentDom) {
568
583
  wireReactiveList(ctx, graphNode, instance2, el2);
569
584
  return instance2;
570
585
  }
586
+ if (graphNode.type === "portal") {
587
+ const anchor = dom.createElement(resolveTag("portal"));
588
+ dom.setAttribute(anchor, "data-streetui-portal", "");
589
+ applyNodeProps(ctx, graphNode, anchor);
590
+ const instance2 = new NodeInstance(graphNode, anchor);
591
+ ctx.instances.set(graphNode.id, instance2);
592
+ const body = dom.body();
593
+ let target = anchor;
594
+ if (body !== null) {
595
+ const portalContainer = dom.createElement("div");
596
+ dom.setAttribute(portalContainer, "data-streetui-portal-container", "");
597
+ dom.appendChild(body, portalContainer);
598
+ instance2.trackCleanup(() => dom.removeChild(body, portalContainer));
599
+ target = portalContainer;
600
+ }
601
+ for (const child of graphNode.children) {
602
+ instance2.addChild(mountNode(ctx, child, target));
603
+ }
604
+ dom.appendChild(parentDom, anchor);
605
+ wireOverlayBehavior(ctx, graphNode, instance2, target);
606
+ return instance2;
607
+ }
571
608
  const tag = resolveTag(graphNode.type);
572
609
  const el = dom.createElement(tag);
573
610
  applyNodeProps(ctx, graphNode, el);
@@ -717,6 +754,44 @@ function forgetInstance(ctx, instance) {
717
754
  ctx.instances.delete(instance.graphNode.id);
718
755
  for (const child of instance.children) forgetInstance(ctx, child);
719
756
  }
757
+ function wireOverlayBehavior(ctx, graphNode, instance, target) {
758
+ const { dom, graph } = ctx;
759
+ if (dom.body() === null) return;
760
+ const descFn = graph.getHandler(`__overlay__${graphNode.id}`);
761
+ if (descFn === void 0) return;
762
+ const desc = descFn();
763
+ const openSig = desc.open;
764
+ if (openSig === void 0 || typeof openSig.subscribe !== "function") return;
765
+ let active = [];
766
+ let saved = null;
767
+ const teardown = () => {
768
+ for (const fn of active) fn();
769
+ active = [];
770
+ };
771
+ const onOpenChange = (isOpen) => {
772
+ if (isOpen) {
773
+ if (desc.restoreFocus) saved = saveFocus(dom);
774
+ if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
775
+ if (desc.modal) {
776
+ active.push(trapFocus(dom, target));
777
+ active.push(containFocus(dom, target));
778
+ }
779
+ if (desc.closeOnEscape && desc.onClose !== void 0) {
780
+ active.push(onEscape(dom, target, desc.onClose));
781
+ }
782
+ } else {
783
+ teardown();
784
+ if (desc.restoreFocus && saved !== null) {
785
+ restoreFocus(dom, saved);
786
+ saved = null;
787
+ }
788
+ }
789
+ };
790
+ const unsub = openSig.subscribe(onOpenChange);
791
+ instance.trackCleanup(unsub);
792
+ instance.trackCleanup(teardown);
793
+ if (openSig.peek() === true) onOpenChange(true);
794
+ }
720
795
 
721
796
  // src/renderer.ts
722
797
  import { BrowserDOMAdapter } from "@streetui/dom";
@@ -822,6 +897,25 @@ function hydrateNode(ctx, graphNode, domNode, path) {
822
897
  wireReactiveList(ctx, graphNode, instance, domNode);
823
898
  return instance;
824
899
  }
900
+ case "portal": {
901
+ const instance = new NodeInstance(graphNode, domNode);
902
+ ctx.instances.set(graphNode.id, instance);
903
+ const body = dom.body();
904
+ let target = domNode;
905
+ if (body !== null) {
906
+ const portalContainer = dom.createElement("div");
907
+ dom.setAttribute(portalContainer, "data-streetui-portal-container", "");
908
+ for (const child of dom.childNodes(domNode)) {
909
+ dom.appendChild(portalContainer, child);
910
+ }
911
+ dom.appendChild(body, portalContainer);
912
+ instance.trackCleanup(() => dom.removeChild(body, portalContainer));
913
+ target = portalContainer;
914
+ }
915
+ hydrateChildren(ctx, graphNode, instance, target, path);
916
+ wireOverlayBehavior(ctx, graphNode, instance, target);
917
+ return instance;
918
+ }
825
919
  default: {
826
920
  const instance = new NodeInstance(graphNode, domNode);
827
921
  ctx.instances.set(graphNode.id, instance);
@@ -1129,6 +1223,7 @@ export {
1129
1223
  serializeState,
1130
1224
  textUpdate,
1131
1225
  wireEvents,
1226
+ wireOverlayBehavior,
1132
1227
  wireReactiveList,
1133
1228
  wireSignalBindings
1134
1229
  };