mirror of
https://github.com/penpot/penpot.git
synced 2026-07-21 21:47:50 +00:00
🐛 Fix dashboard user menu submenu not closing on hover out (#10639)
The dashboard profile menu submenus (Help & Learning, Community & Contributions, About Penpot) opened on pointer enter but nothing closed them when the pointer left the option, leaving a stale submenu visible until the whole menu closed. Close the open submenu when the pointer leaves an expandable option or its submenu, with a 200ms grace period (same approach as the workspace context menu) so the submenu survives the pointer crossing the gap between the parent menu and the floating submenu. Keyboard navigation is unchanged and now covered by tests. Closes #10549 AI-assisted-by: claude-fable-5 Signed-off-by: Akshit Nassa <nassaakshit@gmail.com> Co-authored-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
parent
3469867cf5
commit
89551b2415
@ -28,6 +28,92 @@ test("Navigate to penpot changelog from profile menu", async ({ page }) => {
|
||||
);
|
||||
});
|
||||
|
||||
test("Submenu closes when hovering a menu option without submenu", async ({
|
||||
page,
|
||||
}) => {
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.goToDashboard();
|
||||
|
||||
await dashboardPage.openProfileMenu();
|
||||
await page.getByText("About Penpot").hover();
|
||||
|
||||
const changelogSubmenuItem = page.getByText("Penpot Changelog");
|
||||
await expect(changelogSubmenuItem).toBeVisible();
|
||||
|
||||
await dashboardPage.userProfileOption.hover();
|
||||
await expect(changelogSubmenuItem).toBeHidden();
|
||||
});
|
||||
|
||||
test("Submenu stays open while moving the pointer into it", async ({
|
||||
page,
|
||||
}) => {
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.goToDashboard();
|
||||
|
||||
await dashboardPage.openProfileMenu();
|
||||
const aboutPenpotItem = page.getByText("About Penpot");
|
||||
await aboutPenpotItem.hover();
|
||||
|
||||
const changelogSubmenuItem = page.getByText("Penpot Changelog");
|
||||
await expect(changelogSubmenuItem).toBeVisible();
|
||||
|
||||
// Walk the pointer from the parent option into the submenu the way a
|
||||
// real user does — gradually, crossing the gap between the two menus.
|
||||
const from = await aboutPenpotItem.boundingBox();
|
||||
const to = await changelogSubmenuItem.boundingBox();
|
||||
await page.mouse.move(from.x + from.width / 2, from.y + from.height / 2);
|
||||
await page.mouse.move(to.x + to.width / 2, to.y + to.height / 2, {
|
||||
steps: 20,
|
||||
});
|
||||
|
||||
await expect(changelogSubmenuItem).toBeVisible();
|
||||
});
|
||||
|
||||
test("Submenu closes when the pointer leaves the menu entirely", async ({
|
||||
page,
|
||||
}) => {
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.goToDashboard();
|
||||
|
||||
await dashboardPage.openProfileMenu();
|
||||
await page.getByText("About Penpot").hover();
|
||||
|
||||
const changelogSubmenuItem = page.getByText("Penpot Changelog");
|
||||
await expect(changelogSubmenuItem).toBeVisible();
|
||||
|
||||
await dashboardPage.mainHeading.hover();
|
||||
await expect(changelogSubmenuItem).toBeHidden();
|
||||
});
|
||||
|
||||
test("Hovering another expandable option switches submenus", async ({
|
||||
page,
|
||||
}) => {
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.goToDashboard();
|
||||
|
||||
await dashboardPage.openProfileMenu();
|
||||
await page.getByText("Help & Learning").hover();
|
||||
|
||||
const helpCenterSubmenuItem = page.getByText("Help Center");
|
||||
await expect(helpCenterSubmenuItem).toBeVisible();
|
||||
|
||||
await page.getByText("About Penpot").hover();
|
||||
await expect(page.getByText("Penpot Changelog")).toBeVisible();
|
||||
await expect(helpCenterSubmenuItem).toBeHidden();
|
||||
});
|
||||
|
||||
test("Submenu opens with keyboard navigation", async ({ page }) => {
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.goToDashboard();
|
||||
|
||||
await dashboardPage.openProfileMenu();
|
||||
await dashboardPage.sidebarMenu
|
||||
.getByRole("menuitem", { name: "Help & Learning" })
|
||||
.press("Enter");
|
||||
|
||||
await expect(page.getByText("Help Center")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Opens release notes from current version from profile menu", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
|
||||
(mf/defc internal-dropdown-menu*
|
||||
{::mf/private true}
|
||||
[{:keys [on-close children class id]}]
|
||||
[{:keys [on-close children class id on-pointer-enter on-pointer-leave]}]
|
||||
|
||||
(assert (fn? on-close) "missing `on-close` prop")
|
||||
|
||||
@ -106,7 +106,12 @@
|
||||
#(doseq [key keys]
|
||||
(events/unlistenByKey key))))
|
||||
|
||||
[:ul {:class class :role "menu" :ref container} children]))
|
||||
[:ul {:class class
|
||||
:role "menu"
|
||||
:ref container
|
||||
:on-pointer-enter on-pointer-enter
|
||||
:on-pointer-leave on-pointer-leave}
|
||||
children]))
|
||||
|
||||
(mf/defc dropdown-menu*
|
||||
[{:keys [show] :as props}]
|
||||
|
||||
@ -1133,7 +1133,7 @@
|
||||
|
||||
(mf/defc help-learning-menu*
|
||||
{::mf/private true}
|
||||
[{:keys [on-close on-click]}]
|
||||
[{:keys [on-close on-click on-pointer-enter on-pointer-leave]}]
|
||||
(let [handle-click-url
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
@ -1150,7 +1150,9 @@
|
||||
|
||||
[:> dropdown-menu* {:show true
|
||||
:class (stl/css :sub-menu :help-learning)
|
||||
:on-close on-close}
|
||||
:on-close on-close
|
||||
:on-pointer-enter on-pointer-enter
|
||||
:on-pointer-leave on-pointer-leave}
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:data-url "https://help.penpot.app"
|
||||
@ -1177,7 +1179,7 @@
|
||||
|
||||
(mf/defc community-contributions-menu*
|
||||
{::mf/private true}
|
||||
[{:keys [on-close]}]
|
||||
[{:keys [on-close on-pointer-enter on-pointer-leave]}]
|
||||
(let [handle-click-url
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
@ -1191,7 +1193,9 @@
|
||||
|
||||
[:> dropdown-menu* {:show true
|
||||
:class (stl/css :sub-menu :community)
|
||||
:on-close on-close}
|
||||
:on-close on-close
|
||||
:on-pointer-enter on-pointer-enter
|
||||
:on-pointer-leave on-pointer-leave}
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:data-url "https://github.com/penpot/penpot"
|
||||
@ -1207,7 +1211,7 @@
|
||||
|
||||
(mf/defc about-penpot-menu*
|
||||
{::mf/private true}
|
||||
[{:keys [on-close]}]
|
||||
[{:keys [on-close on-pointer-enter on-pointer-leave]}]
|
||||
(let [version cf/version
|
||||
show-release-notes
|
||||
(mf/use-fn
|
||||
@ -1230,7 +1234,9 @@
|
||||
|
||||
[:> dropdown-menu* {:show true
|
||||
:class (stl/css :sub-menu :about)
|
||||
:on-close on-close}
|
||||
:on-close on-close
|
||||
:on-pointer-enter on-pointer-enter
|
||||
:on-pointer-leave on-pointer-leave}
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click show-release-notes}
|
||||
@ -1255,6 +1261,11 @@
|
||||
show-profile-menu? (deref show-profile-menu*)
|
||||
sub-menu* (mf/use-state false)
|
||||
sub-menu (deref sub-menu*)
|
||||
|
||||
;; Tracks whether the pointer is over an expandable option or
|
||||
;; its floating submenu, so the submenu survives the gap
|
||||
;; between them while the pointer travels across.
|
||||
hovering?* (mf/use-ref false)
|
||||
version (:base cf/version)
|
||||
|
||||
close-sub-menu
|
||||
@ -1320,6 +1331,24 @@
|
||||
(keyword))]
|
||||
(reset! sub-menu* menu))))
|
||||
|
||||
on-menu-pointer-enter
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(mf/set-ref-val! hovering?* true)
|
||||
(on-menu-click event)))
|
||||
|
||||
on-menu-pointer-leave
|
||||
(mf/use-fn
|
||||
(fn [_]
|
||||
(mf/set-ref-val! hovering?* false)
|
||||
(ts/schedule 200 #(when-not (mf/ref-val hovering?*)
|
||||
(reset! sub-menu* nil)))))
|
||||
|
||||
on-sub-menu-pointer-enter
|
||||
(mf/use-fn
|
||||
(fn [_]
|
||||
(mf/set-ref-val! hovering?* true)))
|
||||
|
||||
on-power-up-click
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
@ -1393,7 +1422,8 @@
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-menu-click event)))
|
||||
:on-pointer-enter on-menu-click
|
||||
:on-pointer-enter on-menu-pointer-enter
|
||||
:on-pointer-leave on-menu-pointer-leave
|
||||
:data-testid "help-learning"
|
||||
:id "help-learning"}
|
||||
[:span {:class (stl/css :item-name)} (tr "labels.help-learning")]
|
||||
@ -1404,7 +1434,8 @@
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-menu-click event)))
|
||||
:on-pointer-enter on-menu-click
|
||||
:on-pointer-enter on-menu-pointer-enter
|
||||
:on-pointer-leave on-menu-pointer-leave
|
||||
:data-testid "community-contributions"
|
||||
:id "community-contributions"}
|
||||
[:span {:class (stl/css :item-name)} (tr "labels.community-contributions")]
|
||||
@ -1415,7 +1446,8 @@
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-menu-click event)))
|
||||
:on-pointer-enter on-menu-click
|
||||
:on-pointer-enter on-menu-pointer-enter
|
||||
:on-pointer-leave on-menu-pointer-leave
|
||||
:data-testid "about-penpot"
|
||||
:id "about-penpot"}
|
||||
|
||||
@ -1440,13 +1472,20 @@
|
||||
(when show-profile-menu?
|
||||
(case sub-menu
|
||||
:help-learning
|
||||
[:> help-learning-menu* {:on-close close-sub-menu :on-click on-click}]
|
||||
[:> help-learning-menu* {:on-close close-sub-menu
|
||||
:on-click on-click
|
||||
:on-pointer-enter on-sub-menu-pointer-enter
|
||||
:on-pointer-leave on-menu-pointer-leave}]
|
||||
|
||||
:community-contributions
|
||||
[:> community-contributions-menu* {:on-close close-sub-menu}]
|
||||
[:> community-contributions-menu* {:on-close close-sub-menu
|
||||
:on-pointer-enter on-sub-menu-pointer-enter
|
||||
:on-pointer-leave on-menu-pointer-leave}]
|
||||
|
||||
:about-penpot
|
||||
[:> about-penpot-menu* {:on-close close-sub-menu}]
|
||||
[:> about-penpot-menu* {:on-close close-sub-menu
|
||||
:on-pointer-enter on-sub-menu-pointer-enter
|
||||
:on-pointer-leave on-menu-pointer-leave}]
|
||||
nil))]))
|
||||
|
||||
(mf/defc sidebar*
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user