mirror of
https://github.com/penpot/penpot.git
synced 2025-06-04 07:01:39 +02:00
🐛 Fix context menu event handling issues (#5917)
* 💄 Change call convention for dashboard grid component * 🎉 Add helper component for easy portal to document * 🐛 Fix context menu event handling issues With this commit, the behavior of context menu and scroll is changed to: close menu on scroll instead of disabling all pointer events while menu is open. The previous behavior causes a second event of context menu open a native browser context menu instead of penpot menu.
This commit is contained in:
parent
dda9f62504
commit
24cb1728b0
8 changed files with 108 additions and 84 deletions
|
@ -105,10 +105,10 @@ test("Multiple elements in context", async ({ page }) => {
|
||||||
|
|
||||||
await button.click({ button: "right" });
|
await button.click({ button: "right" });
|
||||||
|
|
||||||
await expect(button.getByTestId("duplicate-multi")).toBeVisible();
|
await expect(page.getByTestId("duplicate-multi")).toBeVisible();
|
||||||
await expect(button.getByTestId("file-move-multi")).toBeVisible();
|
await expect(page.getByTestId("file-move-multi")).toBeVisible();
|
||||||
await expect(button.getByTestId("file-binary-export-multi")).toBeVisible();
|
await expect(page.getByTestId("file-binary-export-multi")).toBeVisible();
|
||||||
await expect(button.getByTestId("file-delete-multi")).toBeVisible();
|
await expect(page.getByTestId("file-delete-multi")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("User has create file button", async ({ page }) => {
|
test("User has create file button", async ({ page }) => {
|
||||||
|
|
15
frontend/src/app/main/ui/components/portal.cljs
Normal file
15
frontend/src/app/main/ui/components/portal.cljs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.main.ui.components.portal
|
||||||
|
(:require
|
||||||
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
|
(mf/defc portal-on-document*
|
||||||
|
[{:keys [children]}]
|
||||||
|
(mf/portal
|
||||||
|
(mf/html [:* children])
|
||||||
|
(.-body js/document)))
|
|
@ -59,9 +59,6 @@
|
||||||
|
|
||||||
permissions (:permissions team)
|
permissions (:permissions team)
|
||||||
|
|
||||||
dashboard-local (mf/deref refs/dashboard-local)
|
|
||||||
file-menu-open? (:menu-open dashboard-local)
|
|
||||||
|
|
||||||
default-project-id
|
default-project-id
|
||||||
(get default-project :id)
|
(get default-project :id)
|
||||||
|
|
||||||
|
@ -87,7 +84,6 @@
|
||||||
(mf/use-effect on-resize)
|
(mf/use-effect on-resize)
|
||||||
|
|
||||||
[:div {:class (stl/css :dashboard-content)
|
[:div {:class (stl/css :dashboard-content)
|
||||||
:style {:pointer-events (when file-menu-open? "none")}
|
|
||||||
:on-click clear-selected-fn
|
:on-click clear-selected-fn
|
||||||
:ref container}
|
:ref container}
|
||||||
(case section
|
(case section
|
||||||
|
|
|
@ -55,8 +55,8 @@
|
||||||
projects))
|
projects))
|
||||||
|
|
||||||
(mf/defc file-menu*
|
(mf/defc file-menu*
|
||||||
{::mf/props :obj}
|
|
||||||
[{:keys [files on-edit on-menu-close top left navigate origin parent-id can-edit]}]
|
[{:keys [files on-edit on-menu-close top left navigate origin parent-id can-edit]}]
|
||||||
|
|
||||||
(assert (seq files) "missing `files` prop")
|
(assert (seq files) "missing `files` prop")
|
||||||
(assert (fn? on-edit) "missing `on-edit` prop")
|
(assert (fn? on-edit) "missing `on-edit` prop")
|
||||||
(assert (fn? on-menu-close) "missing `on-menu-close` prop")
|
(assert (fn? on-menu-close) "missing `on-menu-close` prop")
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
[app.main.data.project :as dpj]
|
[app.main.data.project :as dpj]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.dashboard.grid :refer [grid]]
|
[app.main.ui.dashboard.grid :refer [grid*]]
|
||||||
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
|
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
|
||||||
[app.main.ui.dashboard.pin-button :refer [pin-button*]]
|
[app.main.ui.dashboard.pin-button :refer [pin-button*]]
|
||||||
[app.main.ui.dashboard.project-menu :refer [project-menu*]]
|
[app.main.ui.dashboard.project-menu :refer [project-menu*]]
|
||||||
|
@ -198,7 +198,7 @@
|
||||||
:subtitle (if is-draft-proyect
|
:subtitle (if is-draft-proyect
|
||||||
(tr "dashboard.empty-placeholder-drafts-subtitle")
|
(tr "dashboard.empty-placeholder-drafts-subtitle")
|
||||||
(tr "dashboard.empty-placeholder-files-subtitle"))}]
|
(tr "dashboard.empty-placeholder-files-subtitle"))}]
|
||||||
[:& grid {:project project
|
[:> grid* {:project project
|
||||||
:files files
|
:files files
|
||||||
:selected-files selected-files
|
:selected-files selected-files
|
||||||
:can-edit can-edit?
|
:can-edit can-edit?
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
[app.main.repo :as rp]
|
[app.main.repo :as rp]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.components.color-bullet :as bc]
|
[app.main.ui.components.color-bullet :as bc]
|
||||||
|
[app.main.ui.components.portal :refer [portal-on-document*]]
|
||||||
[app.main.ui.dashboard.file-menu :refer [file-menu*]]
|
[app.main.ui.dashboard.file-menu :refer [file-menu*]]
|
||||||
[app.main.ui.dashboard.import :refer [use-import-file]]
|
[app.main.ui.dashboard.import :refer [use-import-file]]
|
||||||
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
|
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
|
||||||
|
@ -241,29 +242,37 @@
|
||||||
counter-el))
|
counter-el))
|
||||||
|
|
||||||
(mf/defc grid-item*
|
(mf/defc grid-item*
|
||||||
{::mf/props :obj}
|
|
||||||
[{:keys [file origin can-edit selected-files]}]
|
[{:keys [file origin can-edit selected-files]}]
|
||||||
(let [file-id (:id file)
|
(let [file-id (get file :id)
|
||||||
|
state (mf/deref refs/dashboard-local)
|
||||||
|
|
||||||
is-library-view (= origin :libraries)
|
menu-pos
|
||||||
|
(get state :menu-pos)
|
||||||
|
|
||||||
dashboard-local (mf/deref refs/dashboard-local)
|
menu-open?
|
||||||
file-menu-open? (:menu-open dashboard-local)
|
(and (get state :menu-open)
|
||||||
|
(= file-id (:file-id state)))
|
||||||
|
|
||||||
selected? (contains? selected-files file-id)
|
selected?
|
||||||
|
(contains? selected-files file-id)
|
||||||
|
|
||||||
|
selected-num
|
||||||
|
(count selected-files)
|
||||||
|
|
||||||
node-ref (mf/use-ref)
|
node-ref (mf/use-ref)
|
||||||
menu-ref (mf/use-ref)
|
menu-ref (mf/use-ref)
|
||||||
|
|
||||||
|
is-library-view?
|
||||||
|
(= origin :libraries)
|
||||||
|
|
||||||
on-menu-close
|
on-menu-close
|
||||||
(mf/use-fn
|
(mf/use-fn #(st/emit! (dd/hide-file-menu)))
|
||||||
(fn [_]
|
|
||||||
(st/emit! (dd/hide-file-menu))))
|
|
||||||
|
|
||||||
on-select
|
on-select
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
(mf/deps selected? selected-num)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(when (or (not selected?) (> (count selected-files) 1))
|
(when (or (not selected?) (> selected-num 1))
|
||||||
(dom/stop-propagation event)
|
(dom/stop-propagation event)
|
||||||
(let [shift? (kbd/shift? event)]
|
(let [shift? (kbd/shift? event)]
|
||||||
(when-not shift?
|
(when-not shift?
|
||||||
|
@ -281,41 +290,36 @@
|
||||||
|
|
||||||
on-drag-start
|
on-drag-start
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps selected-files can-edit)
|
(mf/deps selected? selected-num)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(st/emit! (dd/hide-file-menu))
|
(st/emit! (dd/hide-file-menu))
|
||||||
(when can-edit
|
(when can-edit
|
||||||
(let [offset (dom/get-offset-position (dom/event->native-event event))
|
(let [offset (dom/get-offset-position (dom/event->native-event event))
|
||||||
|
|
||||||
select-current? (not (contains? selected-files (:id file)))
|
|
||||||
|
|
||||||
item-el (mf/ref-val node-ref)
|
item-el (mf/ref-val node-ref)
|
||||||
counter-el (create-counter-element
|
counter-el (create-counter-element item-el
|
||||||
item-el
|
(if (not selected?)
|
||||||
(if select-current?
|
|
||||||
1
|
1
|
||||||
(count selected-files)))]
|
selected-num))]
|
||||||
(when select-current?
|
(when (not selected?)
|
||||||
(st/emit! (dd/clear-selected-files))
|
(st/emit! (dd/clear-selected-files))
|
||||||
(st/emit! (dd/toggle-file-select file)))
|
(st/emit! (dd/toggle-file-select file)))
|
||||||
|
|
||||||
(dnd/set-data! event "penpot/files" "dummy")
|
(dnd/set-data! event "penpot/files" "dummy")
|
||||||
(dnd/set-allowed-effect! event "move")
|
(dnd/set-allowed-effect! event "move")
|
||||||
|
|
||||||
;; set-drag-image requires that the element is rendered and
|
;; set-drag-image requires that the element is rendered
|
||||||
;; visible to the user at the moment of creating the ghost
|
;; and visible to the user at the moment of creating the
|
||||||
;; image (to make a snapshot), but you may remove it right
|
;; ghost image (to make a snapshot), but you may remove
|
||||||
;; afterwards, in the next render cycle.
|
;; it right afterwards, in the next render cycle.
|
||||||
(dom/append-child! item-el counter-el)
|
(dom/append-child! item-el counter-el)
|
||||||
(dnd/set-drag-image! event item-el (:x offset) (:y offset))
|
(dnd/set-drag-image! event item-el (:x offset) (:y offset))
|
||||||
(ts/raf #(.removeChild ^js item-el counter-el))))))
|
(ts/raf #(dom/remove-child! item-el counter-el))))))
|
||||||
|
|
||||||
on-menu-click
|
on-menu-click
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps file selected?)
|
(mf/deps file selected?)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(dom/stop-propagation event)
|
(dom/stop-propagation event)
|
||||||
(dom/prevent-default event)
|
|
||||||
|
|
||||||
(when-not selected?
|
(when-not selected?
|
||||||
(when-not (kbd/shift? event)
|
(when-not (kbd/shift? event)
|
||||||
|
@ -339,12 +343,10 @@
|
||||||
|
|
||||||
on-context-menu
|
on-context-menu
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps on-menu-click is-library-view)
|
(mf/deps on-menu-click)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(dom/stop-propagation event)
|
|
||||||
(dom/prevent-default event)
|
(dom/prevent-default event)
|
||||||
(when-not is-library-view
|
(on-menu-click event)))
|
||||||
(on-menu-click event))))
|
|
||||||
|
|
||||||
edit
|
edit
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
@ -362,8 +364,8 @@
|
||||||
(dom/stop-propagation event)
|
(dom/stop-propagation event)
|
||||||
(st/emit! (dd/start-edit-file-name file-id))))
|
(st/emit! (dd/start-edit-file-name file-id))))
|
||||||
|
|
||||||
handle-key-down
|
on-key-down
|
||||||
(mf/use-callback
|
(mf/use-fn
|
||||||
(mf/deps on-navigate on-select)
|
(mf/deps on-navigate on-select)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(dom/stop-propagation event)
|
(dom/stop-propagation event)
|
||||||
|
@ -371,63 +373,70 @@
|
||||||
(on-navigate event))
|
(on-navigate event))
|
||||||
(when (kbd/shift? event)
|
(when (kbd/shift? event)
|
||||||
(when (or (kbd/down-arrow? event) (kbd/left-arrow? event) (kbd/up-arrow? event) (kbd/right-arrow? event))
|
(when (or (kbd/down-arrow? event) (kbd/left-arrow? event) (kbd/up-arrow? event) (kbd/right-arrow? event))
|
||||||
(on-select event)) ;; TODO Fix this
|
;; TODO Fix this
|
||||||
)))]
|
(on-select event)))))
|
||||||
|
|
||||||
|
on-menu-key-down
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps on-menu-click)
|
||||||
|
(fn [event]
|
||||||
|
(when (kbd/enter? event)
|
||||||
|
(dom/stop-propagation event)
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(on-menu-click event))))]
|
||||||
|
|
||||||
[:li {:class (stl/css-case :grid-item true
|
[:li {:class (stl/css-case :grid-item true
|
||||||
:project-th true
|
:project-th true
|
||||||
:library is-library-view)}
|
:library is-library-view?)}
|
||||||
[:div
|
[:div
|
||||||
{:class (stl/css-case :selected selected?
|
{:class (stl/css-case :selected selected?
|
||||||
:library is-library-view)
|
:library is-library-view?)
|
||||||
:ref node-ref
|
:ref node-ref
|
||||||
:role "button"
|
:role "button"
|
||||||
:title (:name file)
|
:title (:name file)
|
||||||
:draggable (dm/str can-edit)
|
:draggable (dm/str can-edit)
|
||||||
:on-click on-select
|
:on-click on-select
|
||||||
:on-key-down handle-key-down
|
:on-key-down on-key-down
|
||||||
:on-double-click on-navigate
|
:on-double-click on-navigate
|
||||||
:on-drag-start on-drag-start
|
:on-drag-start on-drag-start
|
||||||
:on-context-menu on-context-menu}
|
:on-context-menu on-context-menu}
|
||||||
|
|
||||||
[:div {:class (stl/css :overlay)}]
|
[:div {:class (stl/css :overlay)}]
|
||||||
|
|
||||||
(if ^boolean is-library-view
|
(if ^boolean is-library-view?
|
||||||
[:> grid-item-library* {:file file}]
|
[:> grid-item-library* {:file file}]
|
||||||
[:> grid-item-thumbnail* {:file file :can-edit can-edit}])
|
[:> grid-item-thumbnail* {:file file :can-edit can-edit}])
|
||||||
|
|
||||||
(when (and (:is-shared file) (not is-library-view))
|
(when (and (:is-shared file) (not is-library-view?))
|
||||||
[:div {:class (stl/css :item-badge)} i/library])
|
[:div {:class (stl/css :item-badge)} i/library])
|
||||||
|
|
||||||
[:div {:class (stl/css :info-wrapper)}
|
[:div {:class (stl/css :info-wrapper)}
|
||||||
[:div {:class (stl/css :item-info)}
|
[:div {:class (stl/css :item-info)}
|
||||||
(if (and (= file-id (:file-id dashboard-local)) (:edition dashboard-local))
|
(if (and (= file-id (:file-id state)) (:edition state))
|
||||||
[:& inline-edition {:content (:name file)
|
[:& inline-edition {:content (:name file)
|
||||||
:on-end edit}]
|
:on-end edit}]
|
||||||
[:h3 (:name file)])
|
[:h3 (:name file)])
|
||||||
[:& grid-item-metadata {:modified-at (:modified-at file)}]]
|
[:& grid-item-metadata {:modified-at (:modified-at file)}]]
|
||||||
|
|
||||||
[:div {:class (stl/css-case :project-th-actions true :force-display (:menu-open dashboard-local))}
|
[:div {:class (stl/css-case :project-th-actions true :force-display menu-open?)}
|
||||||
[:div
|
[:div
|
||||||
{:class (stl/css :project-th-icon :menu)
|
{:class (stl/css :project-th-icon :menu)
|
||||||
:tab-index "0"
|
:tab-index "0"
|
||||||
:role "button"
|
:role "button"
|
||||||
:aria-label (tr "dashboard.options")
|
:aria-label (tr "dashboard.options")
|
||||||
:ref menu-ref
|
:ref menu-ref
|
||||||
:id (str file-id "-action-menu")
|
:id (dm/str file-id "-action-menu")
|
||||||
:on-click on-menu-click
|
:on-click on-menu-click
|
||||||
:on-key-down (fn [event]
|
:on-key-down on-menu-key-down}
|
||||||
(when (kbd/enter? event)
|
|
||||||
(dom/stop-propagation event)
|
|
||||||
(on-menu-click event)))}
|
|
||||||
menu-icon
|
menu-icon
|
||||||
(when (and selected? file-menu-open?)
|
(when (and selected? menu-open?)
|
||||||
;; When the menu is open we disable events in the dashboard. We need to force pointer events
|
;; When the menu is open we disable events in the dashboard. We need to force pointer events
|
||||||
;; so the menu can be handled
|
;; so the menu can be handled
|
||||||
[:div {:style {:pointer-events "all"}}
|
[:> portal-on-document* {}
|
||||||
[:> file-menu* {:files (vals selected-files)
|
[:> file-menu* {:files (vals selected-files)
|
||||||
:left (+ 24 (:x (:menu-pos dashboard-local)))
|
:left (+ 24 (:x menu-pos))
|
||||||
:top (:y (:menu-pos dashboard-local))
|
:top (:y menu-pos)
|
||||||
:can-edit can-edit
|
:can-edit can-edit
|
||||||
:navigate true
|
:navigate true
|
||||||
:on-edit on-edit
|
:on-edit on-edit
|
||||||
|
@ -435,7 +444,7 @@
|
||||||
:origin origin
|
:origin origin
|
||||||
:parent-id (dm/str file-id "-action-menu")}]])]]]]]))
|
:parent-id (dm/str file-id "-action-menu")}]])]]]]]))
|
||||||
|
|
||||||
(mf/defc grid
|
(mf/defc grid*
|
||||||
{::mf/props :obj}
|
{::mf/props :obj}
|
||||||
[{:keys [files project origin limit create-fn can-edit selected-files]}]
|
[{:keys [files project origin limit create-fn can-edit selected-files]}]
|
||||||
(let [dragging? (mf/use-state false)
|
(let [dragging? (mf/use-state false)
|
||||||
|
@ -455,6 +464,9 @@
|
||||||
import-files
|
import-files
|
||||||
(use-import-file project-id on-finish-import)
|
(use-import-file project-id on-finish-import)
|
||||||
|
|
||||||
|
on-scroll
|
||||||
|
(mf/use-fn #(st/emit! (dd/hide-file-menu)))
|
||||||
|
|
||||||
on-drag-enter
|
on-drag-enter
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn [e]
|
(fn [e]
|
||||||
|
@ -496,6 +508,7 @@
|
||||||
:on-drag-over on-drag-over
|
:on-drag-over on-drag-over
|
||||||
:on-drag-leave on-drag-leave
|
:on-drag-leave on-drag-leave
|
||||||
:on-drop on-drop
|
:on-drop on-drop
|
||||||
|
:on-scroll on-scroll
|
||||||
:ref node-ref}
|
:ref node-ref}
|
||||||
(cond
|
(cond
|
||||||
(nil? files)
|
(nil? files)
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
[app.main.data.team :as dtm]
|
[app.main.data.team :as dtm]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.dashboard.grid :refer [grid]]
|
[app.main.ui.dashboard.grid :refer [grid*]]
|
||||||
[app.main.ui.hooks :as hooks]
|
[app.main.ui.hooks :as hooks]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
|
@ -67,7 +67,7 @@
|
||||||
|
|
||||||
[:section {:class (stl/css :dashboard-container :no-bg :dashboard-shared)
|
[:section {:class (stl/css :dashboard-container :no-bg :dashboard-shared)
|
||||||
:ref rowref}
|
:ref rowref}
|
||||||
[:& grid {:files files
|
[:> grid* {:files files
|
||||||
:selected-files selected-files
|
:selected-files selected-files
|
||||||
:project default-project
|
:project default-project
|
||||||
:origin :libraries
|
:origin :libraries
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
[app.main.data.dashboard :as dd]
|
[app.main.data.dashboard :as dd]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.dashboard.grid :refer [grid]]
|
[app.main.ui.dashboard.grid :refer [grid*]]
|
||||||
[app.main.ui.hooks :as hooks]
|
[app.main.ui.hooks :as hooks]
|
||||||
[app.main.ui.icons :as i]
|
[app.main.ui.icons :as i]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
|
@ -77,7 +77,7 @@
|
||||||
[:div {:class (stl/css :text)} (tr "dashboard.no-matches-for" search-term)]]
|
[:div {:class (stl/css :text)} (tr "dashboard.no-matches-for" search-term)]]
|
||||||
|
|
||||||
:else
|
:else
|
||||||
[:& grid {:files result
|
[:> grid* {:files result
|
||||||
:selected-files selected
|
:selected-files selected
|
||||||
:origin :search
|
:origin :search
|
||||||
:limit limit}])]]))
|
:limit limit}])]]))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue