diff --git a/docs/components/066-menubar.md b/docs/components/066-menubar.md index 76ca596..ea969cd 100644 --- a/docs/components/066-menubar.md +++ b/docs/components/066-menubar.md @@ -74,3 +74,4 @@ if id, ok := res.Chosen(); ok && id == "wrap" { - 菜单打开期间,焦点保持在标题上,高亮项由组件管理。驱动菜单的按键注册在标题上,而 MyGo 的 active descendant 不公开,因此高亮变化时用 `c.Announce` 朗读当前项,并把它写入标题的 `Description`。 - 弹层打开时在 160 ms 内淡入,减少动画时直接显示。关闭时立即消失:弹层关闭后 MyGo 不再构建其元素,公开接口无法为已移除的浮层做淡出。 - 点击菜单内任意位置(包括禁用项)或用指针打开子菜单后,焦点回到所属标题,方向键、Enter、Escape 继续有效。 +- 菜单打开时用 Tab 或 Shift+Tab 离开菜单栏,菜单关闭,焦点留在 Tab 目标上;焦点在菜单栏内任意位置时 Escape 关闭菜单。 diff --git a/menubar.go b/menubar.go index 5a04be4..e1e6189 100644 --- a/menubar.go +++ b/menubar.go @@ -105,9 +105,17 @@ func Menubar(c *ui.Context, menus []MenubarMenu, opts MenubarOptions) MenubarRes titles[i] = t.Bounds() } }) + if m.st.open >= 0 && bar.Shortcut(0, ui.KeyEscape) { + m.close() + } if m.st.open >= 0 { m.overlay(titles) } + // Presses in the menu refocus the title, so lost focus means Tab left the menubar. + if m.st.open >= 0 && m.st.focusTo < 0 && !bar.FocusWithin() { + slog.Debug("mujica: menubar close on focus out") + m.st.open, m.st.hl = -1, nil + } // Focus stays on the title, so the highlighted item is read out. if name := m.current(); m.moved && name != "" { c.Announce(name) diff --git a/menubar_panel.go b/menubar_panel.go index cecba9c..7a35ae4 100644 --- a/menubar_panel.go +++ b/menubar_panel.go @@ -20,6 +20,9 @@ func (m *menubarRun) overlay(titles []ui.Rect) { m.close() return } + if back.Pressed() { + m.focus(st.open) + } // Title hotspots above the backdrop switch menus as the pointer moves. for i, r := range titles { hot := ui.Box(c).Absolute().Left(r.X).Top(r.Y).Size(r.W, r.H).Cursor(ui.CursorPointer) @@ -31,6 +34,9 @@ func (m *menubarRun) overlay(titles []ui.Rect) { } return } + if hot.Pressed() { + m.focus(st.open) + } if hot.Hovered() && i != st.open { m.openMenu(i, false) } @@ -76,7 +82,7 @@ func (m *menubarRun) panel(l int, x, y float32, parent ui.Rect) (*ui.Element, *u y = max(4, wh-4-pb.H) } panel.Left(max(4, x)).Top(y) - if panel.Clicked() { + if panel.Clicked() || panel.Pressed() { m.focus(st.open) } var anchor *ui.Element diff --git a/menubar_test.go b/menubar_test.go index db09c7a..23ed84a 100644 --- a/menubar_test.go +++ b/menubar_test.go @@ -178,6 +178,38 @@ func TestMenubarKeysAfterClickInMenu(t *testing.T) { } } +func TestMenubarTabOutClosesMenu(t *testing.T) { + for _, back := range []bool{false, true} { + a := &menubarApp{} + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Fill().Children(func() { + Button(c, "Before", ButtonOptions{}) + Menubar(c, a.menus(), MenubarOptions{}) + Button(c, "After", ButtonOptions{}) + }) + }, 700, 500) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyDown) + if !tt.HasText("Quit") { + t.Fatal("menu did not open") + } + mods, want := ui.Modifiers(0), "After" + if back { + mods, want = ui.Shift, "Before" + } + tt.Key(mods, ui.KeyTab) + if tt.HasText("Quit") || !tt.Focused(want) { + t.Fatalf("shift=%v: menu open %v, focus on %s %v", back, tt.HasText("Quit"), want, tt.Focused(want)) + } + tt.Key(0, ui.KeyEscape) + if !tt.Focused(want) { + t.Fatalf("shift=%v: Escape moved focus off %s", back, want) + } + } +} + func TestMenubarLongMenuScrolls(t *testing.T) { var items []MenubarItem for i := range 30 {