From f9edafa800752fef25602dcaf84a0e1e080daa17 Mon Sep 17 00:00:00 2001 From: Hector Villarreal Date: Tue, 3 Nov 2026 01:54:50 -0600 Subject: [PATCH] #2120: a linked cell's menu edits, opens or removes its link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Right-clicking a cell with a link only offered "Link…", whose dialog was titled Insert Hyperlink, so changing a link wasn't obvious. As in Excel, the cell menu of a linked cell now offers Edit Hyperlink… (the same dialog, filled with the link and titled Edit Hyperlink), Open Hyperlink (as clicking the link: a reference selects its cell, a web or e-mail address asks first) and Remove Hyperlink (edit.clearHyperlinks). A cell without a link keeps "Link…". Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs.rs | 2 ++ crates/ui-egui/src/grid/menus.rs | 16 +++++++++ .../tests/it/context_menu_stays_open.rs | 33 +++++++++++++++++++ docs/ui-parity.md | 3 +- 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 33be245a5..ec61c5c56 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -564,6 +564,8 @@ impl Dialog { d.values.insert("text".into(), json!(text)); d.values.insert("tooltip".into(), json!(link.tooltip)); d.original_link_text = Some(text); + // As in Excel, the dialog on a linked cell edits that link. + d.title = "Edit Hyperlink".into(); } d } diff --git a/crates/ui-egui/src/grid/menus.rs b/crates/ui-egui/src/grid/menus.rs index b05a04bc7..0e7872334 100644 --- a/crates/ui-egui/src/grid/menus.rs +++ b/crates/ui-egui/src/grid/menus.rs @@ -129,6 +129,9 @@ pub(super) fn context_menu(app: &mut SheetApp, ui: &mut egui::Ui) { egui::Frame::popup(ui.style()).show(ui, |ui| { ui.set_min_width(200.0); let insert_copied = insert_copied_item(app); + // On a cell with a link Excel offers to edit, open or remove it (#2120). + let link = + app.session.active().and_then(|d| d.wb.active().and_then(|sh| sh.hyperlinks.get(&d.selection.active)).map(|l| l.target.clone())); let mut items: Vec<(&str, &str)> = vec![ ("Cut", "edit.cut"), ("Copy", "edit.copy"), @@ -154,6 +157,14 @@ pub(super) fn context_menu(app: &mut SheetApp, ui: &mut egui::Ui) { if let Some(item) = insert_copied { items.insert(6, item); } + if link.is_some() + && let Some(i) = items.iter().position(|(_, id)| *id == "ui:insertLink") + { + items.splice( + i..=i, + [("Edit Hyperlink…", "ui:insertLink"), ("Open Hyperlink", "ui:openLink"), ("Remove Hyperlink", "edit.clearHyperlinks")], + ); + } if show_details_offered(app) && let Some(i) = items.iter().position(|(_, id)| *id == "data.sortDescending") { @@ -178,6 +189,11 @@ pub(super) fn context_menu(app: &mut SheetApp, ui: &mut egui::Ui) { "ui:newNote" => app.open_dialog("comment", json!({"threaded": false})), "ui:defineName" => app.open_dialog("defineName", json!({})), "ui:insertLink" => app.open_dialog("insertLink", json!({})), + "ui:openLink" => { + if let Some(target) = &link { + super::interact::follow_link(app, target); + } + } "ui:pickList" => app.open_dialog("pickList", json!({})), "ui:filterValue" => { if let Some(d) = app.session.active() { diff --git a/crates/ui-egui/tests/it/context_menu_stays_open.rs b/crates/ui-egui/tests/it/context_menu_stays_open.rs index c48c659d0..365c28c92 100644 --- a/crates/ui-egui/tests/it/context_menu_stays_open.rs +++ b/crates/ui-egui/tests/it/context_menu_stays_open.rs @@ -100,3 +100,36 @@ fn a_quick_right_click_between_pixels_keeps_the_menu_open_on_retina() { h.run_steps(4); assert!(h.state().grid.context_menu.is_some(), "the menu stays open"); } + +#[test] +fn a_linked_cell_offers_to_edit_open_and_remove_its_link() { + // #2120: the menu only offered "Link…", and its dialog was titled Insert Hyperlink. + use egui_kittest::kittest::Queryable as _; + let mut h = harness(); + h.state_mut().session.execute("insert.link", serde_json::json!({"cell": "B2", "target": "#Sheet1!D9", "text": "Go"})).unwrap(); + h.run_steps(2); + quick_right_click(&mut h, "B2"); + assert!(h.query_by_label("Edit Hyperlink…").is_some() && h.query_by_label("Open Hyperlink").is_some(), "linked cell menu"); + assert!(h.query_by_label("Link…").is_none()); + // Open Hyperlink follows it (an internal reference selects its cell). + h.get_by_label("Open Hyperlink").click(); + h.run_steps(4); + assert_eq!(h.state().session.active().unwrap().selection.active, CellRef::parse("D9").unwrap()); + // Edit Hyperlink opens the link's own dialog. + quick_right_click(&mut h, "B2"); + h.get_by_label("Edit Hyperlink…").click(); + h.run_steps(4); + let d = h.state().dialog.as_ref().expect("the link dialog"); + assert_eq!(d.title, "Edit Hyperlink"); + assert_eq!(d.values.get("target").and_then(|v| v.as_str()), Some("#Sheet1!D9")); + h.state_mut().dialog = None; + h.run_steps(2); + // Remove Hyperlink takes the link away; a cell without one offers Link… again. + quick_right_click(&mut h, "B2"); + h.get_by_label("Remove Hyperlink").click(); + h.run_steps(4); + let sh = h.state().session.active().unwrap().wb.active().unwrap().clone(); + assert!(!sh.hyperlinks.contains_key(&CellRef::parse("B2").unwrap())); + quick_right_click(&mut h, "B2"); + assert!(h.query_by_label("Link…").is_some() && h.query_by_label("Edit Hyperlink…").is_none()); +} diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 125182d27..e34aac280 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-02 · **Change:** minor (RefEdit reference boxes in dialogs: Goal Seek, Solver and Data Analysis use them, the arrow keys select while a dialog is collapsed and F4 cycles a reference's anchors; earlier: Python cells: Python mode in the formula bar; earlier: Insert › Equation and the equation editor; earlier: every floating object selects, resizes from eight handles and deletes with the keyboard; earlier: Formulas › Error Checking › Trace Error; trace arrows stay through a zoom (#3097); earlier: Page Setup sets the page order, Down, then over or Over, then down, which printing, the print preview, Page Break Preview and Page Layout view follow; earlier: Formulas › Function Library as labelled buttons with category menus; Recently Used and the Insert Function categories; Esc leaves a File-page text field before closing the view; earlier: checkbox cells print as boxes; Ctrl+arrow and Ctrl+Shift+arrow while pointing a formula reference; editing a hyperlink loads its existing fields; AutoSum over a selected row puts its total to the right; ribbon catalog count 276 / 292) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (a linked cell's menu edits, opens or removes the link; earlier: RefEdit reference boxes in dialogs: Goal Seek, Solver and Data Analysis use them, the arrow keys select while a dialog is collapsed and F4 cycles a reference's anchors; earlier: Python cells: Python mode in the formula bar; earlier: Insert › Equation and the equation editor; earlier: every floating object selects, resizes from eight handles and deletes with the keyboard; earlier: Formulas › Error Checking › Trace Error; trace arrows stay through a zoom (#3097); earlier: Page Setup sets the page order, Down, then over or Over, then down, which printing, the print preview, Page Break Preview and Page Layout view follow; earlier: Formulas › Function Library as labelled buttons with category menus; Recently Used and the Insert Function categories; Esc leaves a File-page text field before closing the view; earlier: checkbox cells print as boxes; Ctrl+arrow and Ctrl+Shift+arrow while pointing a formula reference; editing a hyperlink loads its existing fields; AutoSum over a selected row puts its total to the right; ribbon catalog count 276 / 292) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 Interaction, controls and feel, against Excel. Part of [target-app-parity.md](target-app-parity.md). **UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured @@ -237,6 +237,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12. | Date | Change | Summary | |---|---|---| +| 2026-11-03 | minor | Cell context menu on a cell with a link: Edit Hyperlink… (the link dialog, titled Edit Hyperlink and filled with the link), Open Hyperlink and Remove Hyperlink, as in Excel, instead of Link… (#2120) | | 2026-11-02 | minor | RefEdit reference boxes in dialogs: pick a range on the grid, collapse the dialog while selecting; Goal Seek, Solver (with the Model Range of Load/Save) and Data Analysis use them; while collapsed the arrow keys move the selection and Shift+arrow extends it, F4 cycles a reference's anchors, a click elsewhere keeps the box picking, a dialog opened over a picking one puts the selection back, OK stays on the sheet its command showed, and scripted cancel and confirm end a pick like the buttons; the dialog's sheet is followed through the recorded undo steps, so a sheet deleted and recreated under its name is not taken for it and an undo of the deletion brings it back; a box the dialog stops showing is no longer picked into | | 2026-11-02 | minor | Python mode edits a cell's code in the formula bar's code editor (Ctrl+Alt+Shift+P, `=PY` then Tab, editing a Python cell), with the PY badge beside fx; its keys apply in input order, also several in one frame, and Ctrl+Enter commits through `python.set` (empty code clears the cell), keeping the code and showing the command's message when the code is refused | | 2026-11-02 | minor | Insert › Symbols: Equation split button (access keys N E), a gallery of examples, and an equation editor with quick-insert buttons, the error underlined at its position and a live preview; double-clicking an equation edits it |