enhancement
major
minor
major
minor
major
#29534
TL Views tile-stack: frames get no definite height, and a frame that is uncovered again is rebuilt from scratch
Problem
Two defects of <tile-stack> (com.top_logic.layout.view.tiles), observed in an application whose main sections are drill-down stacks (a ticket list pushing a ticket page, a project list pushing a tabbed project page).
1. A frame has no definite height
The client control (TLTileStack.tsx) renders <div class="tlTileStack" style="width:100%; height:100%">, but the height never resolves: the fill chain of the layout CSS is a :has() list - .tlStack:has(> .tlPanel--fill), .tlStack:has(> .tlSplitPanel) { flex: 1; min-height: 0 } - and .tlTileStack is not in it, so the enclosing <stack> (the usual host of a <tile-stack> beside its <slot-content> breadcrumb) sizes to content, and so does everything inside the frame. Measured: a <panel fill="true"> with a table inside a frame is 199 px high in a 1000 px viewport; a tabbed project page 286 px. Tables in frames therefore never fill the viewport nor virtualize, and the page scrolls as a whole instead of the table body.
A :has() list that enumerates the child types allowed to grow is the wrong mechanism anyway: every new container element has to be added to it, and a container nested inside a container it does not name (here: tile-stack inside stack) breaks the chain silently.
2. An uncovered frame is rebuilt
ReactTileStackControl renders the top frame only: on every path change it builds the top frame's view anew and calls cleanupTree() on the previous child. Popping back to a frame that stayed on the stack (breadcrumb click) therefore instantiates that frame from scratch: its tab bar returns to the first tab, table selections and scroll positions are gone, a form in edit mode is discarded. The TileFrame keeps only the view reference, the label and the captured parameter values, so nothing of the frame's interactive state survives being covered.
3. A dropped frame was disposed inside the notification that dropped it
Found while fixing 2: the path listener called cleanupTree() on the replaced frame synchronously. A channel notifies a snapshot of its listeners, so a control of the disposed subtree that listens on the same path channel - a nested breadcrumb, a channel bound to the path - is torn down while the notification is still delivering to it. ChannelNotificationScope.afterNotification exists for exactly this and was not used.
Solution
- A fill contract that propagates through containers, replacing the :has() list (react-src/bridge/fill.ts): a control that fills carries the single class tlFill (flex: 1; min-height: 0), and a container hosting a filling child fills in turn, up to the first box that is bounded anyway - the viewport-high mount point, a tab-content region, a scrolling panel body. The chain is a property of the path through the control tree, so it is decided there: useFill(fills) reports a fixed decision (<panel fill="true">), useFillHost() makes a container follow its children (stack, tile stack, inset, deck pane) or always fill (split panel, tab bar, app shell), and <FillBarrier> ends the chain at a region that is bounded on its own (a panel body, a tab content area) or laid out apart from the page (a dialog, a window, a drawer). A container that declares nothing stays opaque, so no request leaks past it.
- Covered frames are kept, not disposed. The stack holds one frame per position - the initial view, then the frames of the path - and displays the one at the end of the path; the frames it covers keep their control tree and their layout box (taken out of the flow at the size of the stack, invisible), so returning to a frame shows it as the user left it: selected tab, selected table row, values being edited and scroll offsets included. A path change keeps the frames of the longest common prefix and disposes the ones it drops, so a path reconstructed from a URL (#29530) keeps the frames it names (TileFrame compares by view, label and params). The hiding sits on a wrapper the stack renders itself, because a frame's content renders its own root element and a style set on that from outside is overwritten on its next render. Frames are addressed as frame[<position>] for scripted tests, as a deck pane addresses its panes.
- A dropped frame is disposed after the notification has unwound (ChannelNotificationScope.afterNotification).
- Regression coverage in com.top_logic.demo.react (tiles demo): the overview frame is a <panel fill="true">, so its table fills the viewport and virtualizes; the detail frame has two tabs, the second one pushing a further detail frame, so that the state a covered frame keeps is observable - the selected tab, the row selected in it and the values being edited are still there after coming back.
- Documentation of both in docs/faq/react-view-layer.md.