fix(ui): Escape closes exactly one popup, topmost first (#535) - #790
Open
NotYuSheng wants to merge 1 commit into
Open
fix(ui): Escape closes exactly one popup, topmost first (#535)#790NotYuSheng wants to merge 1 commit into
NotYuSheng wants to merge 1 commit into
Conversation
Escape handling had grown one ad-hoc `document` keydown listener per hand-rolled overlay, none of which knew about the others or about react-bootstrap's own listener. With two layers on screen a single press either closed the wrong one or closed both — EntityDetailModal, for instance, swallowed Escape with `stopImmediatePropagation` whenever no nested *IP* panel was open, so the role help / evidence explainer / add-evidence modals opened from inside it took the whole panel down with them. Replaces those listeners with a shared coordinator (`useEscapeLayer`): - one capture-phase listener dispatches Escape to the topmost registered layer, ranked by painted stacking order (z-index path, then document order) with registration order as the tiebreak; - it yields when an SGDS modal is stacked above the layer, so react-bootstrap closes that modal itself, and stops propagation when it does handle the key so no modal below closes in the same press; - an open info popover wins outright: every OverlayTrigger here is click + rootClose, which dismisses on Escape *keyup*, so swallowing the keydown leaves the modal underneath alone. Also closes gaps the audit turned up: the cluster side panel only responded to Escape in fullscreen, and the geo-source popovers and the node-colour explainer did not respond at all. The page/dialog fullscreen handlers lose their hand-rolled "close this modal first" cascades — the stack now orders them. Every SGDS Modal was inventoried: all close on Escape via `onHide` except the four that block dismissal on purpose (upload in progress, merge in progress, snapshot save, pcap removal). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #535.
What was wrong
Escape handling was one ad-hoc
documentkeydown listener per hand-rolled overlay, each unaware of the others and of react-bootstrap's own listener. With two layers on screen a press closed the wrong one, or two at once:EntityDetailModalswallowed Escape withstopImmediatePropagationunless a nested IP panel was open — so the role-help, evidence-explainer and add-evidence modals opened from inside it were closed together with the whole panel.ConversationPageclosed the conversation modal on every Escape, on top of the modal's ownonHide.What this does
New shared coordinator
useEscapeLayer(frontend/src/utils/useEscapeLayer.ts), one capture-phase listener for all hand-rolled popups:OverlayTriggers here are click +rootClose, which dismiss on Escape keyup, so swallowing the keydown dismisses the popover and spares the modal beneath it. A root-level guard inMainLayoutkeeps that rule working on pages with no layer of their own.Audit results
SGDS
Modals — all close on Escape viaonHide. Four block dismissal on purpose and are left as-is: upload in progress (keyboard={!isUploading}), merge in progress (keyboard={false}), snapshot save while busy, pcap removal while deleting.Hand-rolled popups — now on the shared stack:
EntityDetailModal(+ its nested self),ConversationTracerModal, the cluster side panel, both geo-source popovers, the node-colour-priority explainer, and the four CSS-fullscreen modes (diagram + heatmap on the analysis page, diagram + heatmap in the monitor dialog).Gaps this closes: the cluster side panel only answered Escape while fullscreen; the geo-source popovers and the node-colour explainer did not answer it at all (the latter also gained
role="dialog").Deliberately still not dismissible: the report-generation progress overlays on Analysis/Compare.
Testing
useEscapeLayer.test.tsx) covering topmost-only dispatch, unwinding a nested stack one press at a time, yielding to a modal above, not closing a modal below, the nested-stacking-context case, and the popover rule. Full suite: 641 passing.docker compose up -d --build):🤖 Generated with Claude Code