M7: a grouped component set's background takes no connection either (#2161 review)

A set in a group has no top-level frame, so a drop on its background fell through to frame_at
and made a Navigate to the section around it. target_at now returns nothing for any point
inside the source's set that misses a sibling variant, as Figma does; red-first UI test with the
set in a group in a section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 checked the finding and wrote
This commit is contained in:
Trevor Brown
2026-11-09 09:38:54 -05:00
co-authored by Claude Opus 5.5
parent 5c50025270
commit 36cd59dada
2 changed files with 31 additions and 0 deletions
@@ -171,6 +171,11 @@ fn target_at(app: &FigApp, doc: &Document, page: NodeId, source: NodeId, p: Pos2
if let Some(w) = siblings.into_iter().rev().find(|w| doc.world_bounds(*w).is_some_and(|b| b.contains(pt))) {
return (w != v).then_some(Target::ChangeTo(w));
}
// The set's own background: nothing, as in Figma, whatever holds the set. (A set in a
// group has no top-level frame, so the fallback below would offer a section around it.)
if doc.world_bounds(set).is_some_and(|b| b.contains(pt)) {
return None;
}
}
frame_at(app, doc, page, p).filter(|f| doc.top_level_frame(source) != Some(*f)).map(Target::Navigate)
}
@@ -28,6 +28,11 @@ struct Setup {
}
fn setup() -> Setup {
setup_in(false)
}
/// `grouped`: the set is put in a group, inside a section around everything.
fn setup_in(grouped: bool) -> Setup {
let mut s = Session::new();
let mut variant = |name: &str, x: f64| {
let f = id(&s.execute("frame.create", &json!({"x": x, "y": 0, "width": 100, "height": 80, "name": name})).unwrap());
@@ -38,6 +43,12 @@ fn setup() -> Setup {
let layer = id(&s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 30, "height": 20, "parent": default})).unwrap());
let outside = id(&s.execute("frame.create", &json!({"x": 0, "y": 500, "width": 200, "height": 150, "name": "Outside"})).unwrap());
let set = id(&s.execute("component.combineVariants", &json!({"ids": [default, hover]})).unwrap());
if grouped {
let group = id(&s.execute("object.group", &json!({"ids": [set]})).unwrap());
let section = id(&s.execute("node.create", &json!({"type": "section", "x": -200, "y": -200, "width": 1000, "height": 1000})).unwrap());
s.execute("node.reparent", &json!({"ids": [group], "parent": section})).unwrap();
assert_eq!(s.doc().unwrap().top_level_frame(default), None, "a set in a group has no top-level frame");
}
let mut u = Ui::new(s);
u.app.ui.right_tab = RightTab::Prototype;
Setup { u, set, default, hover, layer, outside }
@@ -114,6 +125,21 @@ fn dropped_on_the_sets_background_or_its_own_variant_it_adds_nothing() {
let _ = t.set;
}
/// A set in a group in a section (CodeRabbit, #2161): its background still gives nothing, not a
/// Navigate to the section, and a sibling still gives Change to.
#[test]
fn in_a_grouped_set_the_background_adds_nothing_and_a_sibling_changes_to() {
let mut t = setup_in(true);
let (a, b) = (screen_centre(&t.u.app, t.default), screen_centre(&t.u.app, t.hover));
let gap = pos2((a.x + b.x) / 2.0, a.y);
let src = t.default;
drag(&mut t, src, gap);
assert!(interactions(&t, t.default).is_empty(), "{:?}", interactions(&t, t.default));
let to = screen_centre(&t.u.app, t.hover);
drag(&mut t, src, to);
assert_change_to(&t, t.default, t.hover);
}
#[test]
fn a_variant_dropped_on_a_frame_outside_the_set_still_navigates() {
let mut t = setup();