"Inside one panel tree, z-index paints the popup but does not order the click"
▸ SYMPTOM
A popup, such as a dropdown menu, paints cleanly over the panel below it. Every item past the first is dead: the block underneath the menu takes the press instead. The first item usually works, so the control reads as sticky rather than broken. That tell hides the bug for a long time.
▸ CAUSE
Inside one panel tree, z-index decides what a popup looks like and decides nothing about what takes the click. The two orders come from two different sums.
- Hit order. A panel's rank among its siblings is
SiblingIndex + ZIndex, added into one number byPanel.GetRenderOrderIndex. The press goes to the highest-ranked panel that contains the point.PanelInput.CheckHovercompares only within each parent's own child list, and the last match found wins. - Paint order. The pixels come from an accumulated depth the batched renderer carries down the tree, adding each child's z-index to its parent's. So a
z-index: 5anywhere paints above everything at depth zero, whatever its ancestors do.
An absolutely positioned menu that overhangs a later sibling of its own row therefore looks perfect and is unclickable. The later sibling outranks the menu's row in hit order, so it wins the press even though the menu paints on top.
▸ FIX
Lift the ancestors, not the popup.
- Lift every element between the popup and the common parent of the panels it overhangs. The popup's own z-index cannot fix hit order across that boundary.
- Scope the lift to while-open. A permanent lift steals clicks from the modals above it.
- If the overhang leaves the component entirely, lift at the frame level. A list at the bottom of a side panel may need the frame to raise the row that the side panel and the overhung region share.
▸ WHY IT WORKS
Hit-testing ranks siblings inside each parent, so raising the popup alone never changes which sibling wins a press one level up. Raising the shared ancestor changes the SiblingIndex + ZIndex rank at the level where the two panels actually compete, which is the only level where the press is decided.
▸ DIAGNOSE IT WITH A HIT PROBE
Build a console command once per project that transcribes CheckHover. It finds a control by class, reports which panel a click at the control's center would really land on, and then sends the click to that winner rather than to the target. A dead control then stays dead under its own test harness. Any console door that sets state directly will pass on a control nobody can press, so route test clicks through the real hit path.
- Published