0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-15 08:21:40 -05:00

Merge pull request #2952 from penpot/eva-ally-context-3

Add new accessibility functionality to the dashboard
This commit is contained in:
Aitor 2023-02-20 13:30:42 +01:00 committed by GitHub
commit 96ce475206
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 625 additions and 145 deletions

View file

@ -64,6 +64,10 @@ a {
}
}
button {
font-family: "worksans", sans-serif;
}
p {
font-size: $fs12;
margin-bottom: 1rem;

View file

@ -73,6 +73,40 @@
form {
margin: 2rem 0 0.5rem 0;
.accept-terms-and-privacy-wrapper {
position: relative;
.input-checkbox {
margin-bottom: 0;
}
.input-checkbox input[type="checkbox"] {
position: absolute;
display: block;
width: 20px;
height: 20px;
opacity: 0;
top: 22px;
}
label {
margin-left: 40px;
}
label:before {
position: absolute;
top: 15px;
left: -36px;
}
label:after {
position: absolute;
top: 15px;
left: -33px;
}
.input-checkbox input[type="checkbox"]:focus {
opacity: 100%;
}
.auth-links {
margin-left: 40px;
font-size: 0.75rem;
}
}
}
}

View file

@ -27,10 +27,15 @@
margin: $size-3 $size-4 $size-4 $size-2;
position: relative;
text-align: center;
a {
a,
button {
width: 100%;
font-weight: normal;
}
button {
background-color: transparent;
border: none;
}
@media #{$bp-max-1366} {
height: 200px;
flex: 1 0 230px;

View file

@ -183,6 +183,7 @@
.dashboard-project-row {
margin-bottom: $size-5;
position: relative;
.project {
align-items: center;
@ -211,6 +212,8 @@
font-size: $fs14;
justify-content: space-between;
cursor: pointer;
background-color: transparent;
border: none;
.placeholder-icon {
transform: rotate(-90deg);
margin-left: 10px;
@ -297,6 +300,35 @@
opacity: 1;
}
}
.show-more {
align-items: center;
color: $color-gray-30;
display: flex;
font-size: $fs14;
justify-content: space-between;
cursor: pointer;
background-color: transparent;
border: none;
position: absolute;
top: 9px;
right: 53px;
.placeholder-icon {
transform: rotate(-90deg);
margin-left: 10px;
svg {
height: 14px;
width: 14px;
fill: $color-gray-30;
}
}
&:hover {
color: $color-primary-dark;
svg {
fill: $color-primary-dark;
}
}
}
}
.recent-files-row-title-info {

View file

@ -245,16 +245,16 @@
:type "text"}]]
(when (contains? @cf/flags :terms-and-privacy-checkbox)
[:div.fields-row
[:div.fields-row.input-visible.accept-terms-and-privacy-wrapper
[:& fm/input {:name :accept-terms-and-privacy
:class "check-primary"
:type "checkbox"}
[:span
(tr "auth.terms-privacy-agreement")
[:div
[:a {:href "https://penpot.app/terms" :target "_blank"} (tr "auth.terms-of-service")]
[:span ",\u00A0"]
[:a {:href "https://penpot.app/privacy" :target "_blank"} (tr "auth.privacy-policy")]]]]])
(tr "auth.terms-privacy-agreement")]]
[:div.auth-links
[:a {:href "https://penpot.app/terms" :target "_blank"} (tr "auth.terms-of-service")]
[:span ",\u00A0"]
[:a {:href "https://penpot.app/privacy" :target "_blank"} (tr "auth.privacy-policy")]]])
[:& fm/submit-button
{:label (tr "auth.register-submit")

View file

@ -57,8 +57,8 @@
{node-height :height node-width :width} bounding_rect
{window-height :height window-width :width} window_size
target-offset-y (if (> (+ top node-height) window-height)
(- node-height)
0)
(- node-height)
0)
target-offset-x (if (> (+ left node-width) window-width)
(- node-width)
0)]
@ -85,9 +85,9 @@
props (obj/merge props #js {:on-close on-local-close})]
(mf/use-effect
(mf/deps options)
#(swap! local assoc :levels [{:parent-option nil
:options options}]))
(mf/deps options)
#(swap! local assoc :levels [{:parent-option nil
:options options}]))
(when (and open? (some? (:levels @local)))
[:> dropdown' props

View file

@ -0,0 +1,260 @@
;; 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.context-menu-a11y
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.main.refs :as refs]
[app.main.ui.components.dropdown :refer [dropdown']]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.object :as obj]
[app.util.timers :as tm]
[goog.object :as gobj]
[rumext.v2 :as mf]))
(defn generate-ids-group
[options parent-name]
(let [ids (->> options
(map :id)
(filter some?))]
(if parent-name
(cons "go-back-sub-option" ids)
ids)))
(mf/defc context-menu-a11y-item
{::mf/wrap-props false}
[props]
(let [children (gobj/get props "children")
on-click (gobj/get props "on-click")
on-key-down (gobj/get props "on-key-down")
id (gobj/get props "id")
klass (gobj/get props "klass")
key (gobj/get props "key")
data-test (gobj/get props "data-test")]
[:li {:id id
:class klass
:tab-index "0"
:on-key-down on-key-down
:on-click on-click
:key key
:role "menuitem"
:data-test data-test}
children]))
(mf/defc context-menu-a11y'
{::mf/wrap-props false}
[props]
(assert (fn? (gobj/get props "on-close")) "missing `on-close` prop")
(assert (boolean? (gobj/get props "show")) "missing `show` prop")
(assert (vector? (gobj/get props "options")) "missing `options` prop")
(let [open? (gobj/get props "show")
on-close (gobj/get props "on-close")
options (gobj/get props "options")
is-selectable (gobj/get props "selectable")
selected (gobj/get props "selected")
top (gobj/get props "top" 0)
left (gobj/get props "left" 0)
fixed? (gobj/get props "fixed?" false)
min-width? (gobj/get props "min-width?" false)
origin (gobj/get props "origin")
route (mf/deref refs/route)
in-dashboard? (= :dashboard-projects (:name (:data route)))
local (mf/use-state {:offset-y 0
:offset-x 0
:levels nil})
on-local-close
(mf/use-callback
(fn []
(swap! local assoc :levels [{:parent-option nil
:options options}])
(on-close)))
props (obj/merge props #js {:on-close on-local-close})
ids (generate-ids-group (:options (last (:levels @local))) (:parent-option (last (:levels @local))))
check-menu-offscreen
(mf/use-callback
(mf/deps top (:offset-y @local) left (:offset-x @local))
(fn [node]
(when (some? node)
(let [bounding_rect (dom/get-bounding-rect node)
window_size (dom/get-window-size)
{node-height :height node-width :width} bounding_rect
{window-height :height window-width :width} window_size
target-offset-y (if (> (+ top node-height) window-height)
(- node-height)
0)
target-offset-x (if (> (+ left node-width) window-width)
(- node-width)
0)]
(when (or (not= target-offset-y (:offset-y @local)) (not= target-offset-x (:offset-x @local)))
(swap! local assoc :offset-y target-offset-y :offset-x target-offset-x))))))
enter-submenu
(mf/use-callback
(mf/deps options)
(fn [option-name sub-options]
(fn [event]
(dom/stop-propagation event)
(swap! local update :levels
conj {:parent-option option-name
:options sub-options}))))
exit-submenu
(mf/use-callback
(fn [event]
(dom/stop-propagation event)
(swap! local update :levels pop)))
on-key-down
(fn [options-original parent-original]
(fn [event]
(let [ids (generate-ids-group options-original parent-original)
first-id (dom/get-element (first ids))
first-element (dom/get-element first-id)
len (count ids)
parent (dom/get-target event)
parent-id (dom/get-attribute parent "id")
option (first (filter #(= parent-id (:id %)) options-original))
sub-options (:sub-options option)
has-suboptions? (some? (:sub-options option))
option-handler (:option-handler option)
is-back-option (= "go-back-sub-option" parent-id)]
(when (kbd/home? event)
(when first-element
(dom/focus! first-element)))
(when (kbd/enter? event)
(if is-back-option
(exit-submenu event)
(if has-suboptions?
(do
(dom/stop-propagation event)
(swap! local update :levels
conj {:parent-option (:option-name option)
:options sub-options}))
(do
(dom/stop-propagation event)
(option-handler event)))))
(when (and is-back-option
(kbd/left-arrow? event))
(exit-submenu event))
(when (and has-suboptions? (kbd/right-arrow? event))
(dom/stop-propagation event)
(swap! local update :levels
conj {:parent-option (:option-name option)
:options sub-options}))
(when (kbd/up-arrow? event)
(let [actual-selected (dom/get-active)
actual-id (dom/get-attribute actual-selected "id")
actual-index (d/index-of ids actual-id)
previous-id (if (= 0 actual-index)
(last ids)
(nth ids (- actual-index 1)))]
(dom/focus! (dom/get-element previous-id))))
(when (kbd/down-arrow? event)
(let [actual-selected (dom/get-active)
actual-id (dom/get-attribute actual-selected "id")
actual-index (d/index-of ids actual-id)
next-id (if (= (- len 1) actual-index)
(first ids)
(nth ids (+ 1 actual-index)))]
(dom/focus! (dom/get-element next-id))))
(when (or (kbd/esc? event) (kbd/tab? event))
(on-close)
(dom/focus! (dom/get-element origin))))))]
(mf/with-effect [options]
(swap! local assoc :levels [{:parent-option nil
:options options}]))
(mf/with-effect [ids]
(tm/schedule-on-idle
(dom/focus! (dom/get-element (first ids)))))
(when (and open? (some? (:levels @local)))
[:> dropdown' props
(let [level (-> @local :levels peek)
original-options (:options level)
parent-original (:parent-option level)]
[:div.context-menu {:class (dom/classnames :is-open open?
:fixed fixed?
:is-selectable is-selectable)
:style {:top (+ top (:offset-y @local))
:left (+ left (:offset-x @local))}
:on-key-down (on-key-down original-options parent-original)}
(let [level (-> @local :levels peek)]
[:ul.context-menu-items {:class (dom/classnames :min-width min-width?)
:role "menu"
:ref check-menu-offscreen}
(when-let [parent-option (:parent-option level)]
[:*
[:& context-menu-a11y-item
{:id "go-back-sub-option"
:tab-index "0"
:on-key-down (fn [event]
(dom/prevent-default event))}
[:div.context-menu-action.submenu-back
{:data-no-close true
:on-click exit-submenu}
[:span i/arrow-slide]
parent-option]]
[:li.separator]])
(for [[index option] (d/enumerate (:options level))]
(let [option-name (:option-name option)
id (:id option)
sub-options (:sub-options option)
option-handler (:option-handler option)
data-test (:data-test option)]
(when option-name
(if (= option-name :separator)
[:li.separator {:key (dm/str "context-item-" index)}]
[:& context-menu-a11y-item
{:id id
:class (dom/classnames :is-selected (and selected (= option-name selected)))
:key (dm/str "context-item-" index)
:tab-index "0"
:on-key-down (fn [event]
(dom/prevent-default event))}
(if-not sub-options
[:a.context-menu-action {:on-click #(do (dom/stop-propagation %)
(on-close)
(option-handler %))
:data-test data-test}
(if (and in-dashboard? (= option-name "Default"))
(tr "dashboard.default-team-name")
option-name)]
[:a.context-menu-action.submenu
{:data-no-close true
:on-click (enter-submenu option-name sub-options)
:data-test data-test}
option-name
[:span i/arrow-slide]])]))))])])])))
(mf/defc context-menu-a11y
{::mf/wrap-props false}
[props]
(assert (fn? (gobj/get props "on-close")) "missing `on-close` prop")
(assert (boolean? (gobj/get props "show")) "missing `show` prop")
(assert (vector? (gobj/get props "options")) "missing `options` prop")
(when (gobj/get props "show")
(mf/element context-menu-a11y' props)))

View file

@ -25,7 +25,7 @@
on-key-down (gobj/get props "on-key-down")
id (gobj/get props "id")
klass (gobj/get props "klass")
key (gobj/get props "klass")
key (gobj/get props "key")
data-test (gobj/get props "data-test")]
[:li {:id id
:class klass
@ -45,7 +45,7 @@
ref (gobj/get props "container")
ids (gobj/get props "ids")
list-class (gobj/get props "list-class")
ids (filter some? ids)
on-click
(fn [event]
(let [target (dom/get-target event)

View file

@ -8,6 +8,7 @@
(:require
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.math :as mth]
[app.common.spec :as us]
[app.main.data.dashboard :as dd]
[app.main.data.dashboard.shortcuts :as sc]
@ -83,6 +84,10 @@
container-size (* (+ 2 num-cards) card-width)
;; We need space for num-cards plus the libraries&templates link
more-cards (> (+ @card-offset (* (+ 1 num-cards) card-width)) content-width)
visible-card-count (mth/floor (/ content-width 275))
left-moves (/ @card-offset -275)
first-visible-card left-moves
last-visible-card (+ (- visible-card-count 1) left-moves)
content-ref (mf/use-ref)
toggle-collapse
@ -146,38 +151,78 @@
[:div.dashboard-templates-section {:class (when collapsed "collapsed")}
[:div.title
[:button {:tab-index "0"
:on-click toggle-collapse}
:on-click toggle-collapse
:on-key-down (fn [event]
(when (kbd/enter? event)
(dom/stop-propagation event)
(dom/prevent-default event)
(toggle-collapse)))}
[:span (tr "dashboard.libraries-and-templates")]
[:span.icon (if collapsed i/arrow-up i/arrow-down)]]]
[:div.content {:ref content-ref
:style {:left @card-offset :width (str container-size "px")}}
:style {:left @card-offset :width (str container-size "px")}}
(for [num-item (range (count templates)) :let [item (nth templates num-item)]]
[:a.card-container {:tab-index "0"
:id (str/concat "card-container-" num-item)
:key (:id item)
:on-click #(import-template item)
:on-key-down (fn [event]
(when (kbd/enter? event)
(import-template item)))}
(let [is-visible? (and (>= num-item first-visible-card) (<= num-item last-visible-card))]
[:a.card-container {:tab-index (if (or (not is-visible?) collapsed)
"-1"
"0")
:id (str/concat "card-container-" num-item)
:key (:id item)
:on-click #(import-template item)
:on-key-down (fn [event]
(when (kbd/enter? event)
(dom/stop-propagation event)
(import-template item)))}
[:div.template-card
[:div.img-container
[:img {:src (:thumbnail-uri item)
:alt (:name item)}]]
[:div.card-name [:span (:name item)] [:span.icon i/download]]]]))
(let [is-visible? (and (>= num-cards first-visible-card) (<= num-cards last-visible-card))]
[:div.card-container
[:div.template-card
[:div.img-container
[:img {:src (:thumbnail-uri item)
:alt (:name item)}]]
[:div.card-name [:span (:name item)] [:span.icon i/download]]]])
[:div.card-container
[:div.template-card
[:div.img-container
[:a {:tab-index "0"
:href "https://penpot.app/libraries-templates" :target "_blank" :on-click handle-template-link}
[:div.template-link
[:div.template-link-title (tr "dashboard.libraries-and-templates")]
[:div.template-link-text (tr "dashboard.libraries-and-templates.explore")]]]]]]]
(when (< @card-offset 0)
[:button.button.left {:on-click move-left} i/go-prev])
[:a {:id (str/concat "card-container-" num-cards)
:tab-index (if (or (not is-visible?) collapsed)
"-1"
"0")
:href "https://penpot.app/libraries-templates.html"
:target "_blank"
:on-click handle-template-link
:on-key-down (fn [event]
(when (kbd/enter? event)
(dom/stop-propagation event)
(handle-template-link)))}
[:div.template-link
[:div.template-link-title (tr "dashboard.libraries-and-templates")]
[:div.template-link-text (tr "dashboard.libraries-and-templates.explore")]]]]]])]
(when (< @card-offset 0)
[:button.button.left {:tab-index (if collapsed
"-1"
"0")
:on-click move-left
:on-key-down (fn [event]
(when (kbd/enter? event)
(dom/stop-propagation event)
(move-left)
(let [first-element (dom/get-element (str/concat "card-container-" first-visible-card))]
(when first-element
(dom/focus! first-element)))))} i/go-prev])
(when more-cards
[:button.button.right {:on-click move-right
:aria-label (tr "labels.next")} i/go-next])]))
[:button.button.right {:tab-index (if collapsed
"-1"
"0")
:on-click move-right
:aria-label (tr "labels.next")
:on-key-down (fn [event]
(when (kbd/enter? event)
(dom/stop-propagation event)
(move-right)
(let [last-element (dom/get-element (str/concat "card-container-" last-visible-card))]
(when last-element
(dom/focus! last-element)))))} i/go-next])]))
(mf/defc dashboard-content
[{:keys [team projects project section search-term profile] :as props}]
@ -277,6 +322,7 @@
(let [events [(events/listen goog/global EventType.KEYDOWN
(fn [event]
(when (kbd/enter? event)
(dom/stop-propagation event)
(st/emit! (dd/open-selected-file)))))]]
(fn []
(doseq [key events]

View file

@ -12,7 +12,7 @@
[app.main.data.modal :as modal]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.ui.components.context-menu :refer [context-menu]]
[app.main.ui.components.context-menu-a11y :refer [context-menu-a11y]]
[app.main.ui.context :as ctx]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
@ -27,6 +27,10 @@
(tr "labels.drafts")
(:name project)))
(defn get-project-id
[project]
(str (:id project)))
(defn get-team-name
[team]
(if (:is-default team)
@ -49,7 +53,7 @@
projects))
(mf/defc file-menu
[{:keys [files show? on-edit on-menu-close top left navigate? origin] :as props}]
[{:keys [files show? on-edit on-menu-close top left navigate? origin parent-id] :as props}]
(assert (seq files) "missing `files` prop")
(assert (boolean? show?) "missing `show?` prop")
(assert (fn? on-edit) "missing `on-edit` prop")
@ -65,13 +69,10 @@
current-team-id (mf/use-ctx ctx/current-team-id)
teams (mf/use-state nil)
current-team (get @teams current-team-id)
other-teams (remove #(= (:id %) current-team-id) (vals @teams))
current-projects (remove #(= (:id %) (:project-id file))
(:projects current-team))
on-new-tab
(fn [_]
(let [path-params {:project-id (:project-id file)
@ -211,53 +212,101 @@
(rx/map group-by-team)
(rx/subs #(when (mf/ref-val mounted-ref)
(reset! teams %)))))))
(when current-team
(let [sub-options (conj (vec (for [project current-projects]
[(get-project-name project)
(on-move (:id current-team)
(:id project))]))
(when (seq other-teams)
[(tr "dashboard.move-to-other-team") nil
(for [team other-teams]
[(get-team-name team) nil
(for [sub-project (:projects team)]
[(get-project-name sub-project)
(on-move (:id team)
(:id sub-project))])])
"move-to-other-team"]))
(let [sub-options (concat (vec (for [project current-projects]
{:option-name (get-project-name project)
:id (get-project-id project)
:option-handler (on-move (:id current-team)
(:id project))}))
(when (seq other-teams)
[{:option-name (tr "dashboard.move-to-other-team")
:id "move-to-other-team"
:sub-options
(for [team other-teams]
{:option-name (get-team-name team)
:id (get-project-id team)
:sub-options
(for [sub-project (:projects team)]
{:option-name (get-project-name sub-project)
:id (get-project-id sub-project)
:option-handler (on-move (:id team)
(:id sub-project))})})}]))
options (if multi?
[[(tr "dashboard.duplicate-multi" file-count) on-duplicate nil "duplicate-multi"]
[{:option-name (tr "dashboard.duplicate-multi" file-count)
:id "file-duplicate-multi"
:option-handler on-duplicate
:data-test "duplicate-multi"}
(when (or (seq current-projects) (seq other-teams))
[(tr "dashboard.move-to-multi" file-count) nil sub-options "move-to-multi"])
[(tr "dashboard.export-binary-multi" file-count) on-export-binary-files]
[(tr "dashboard.export-standard-multi" file-count) on-export-standard-files]
{:option-name (tr "dashboard.move-to-multi" file-count)
:id "file-move-multi"
:sub-options sub-options
:data-test "move-to-multi"})
{:option-name (tr "dashboard.export-binary-multi" file-count)
:id "file-binari-export-multi"
:option-handler on-export-binary-files}
{:option-name (tr "dashboard.export-standard-multi" file-count)
:id "file-standard-export-multi"
:option-handler on-export-standard-files}
(when (:is-shared file)
[(tr "labels.unpublish-multi-files" file-count) on-del-shared nil "file-del-shared"])
{:option-name (tr "labels.unpublish-multi-files" file-count)
:id "file-unpublish-multi"
:option-handler on-del-shared
:data-test "file-del-shared"})
(when (not is-lib-page?)
[:separator]
[(tr "labels.delete-multi-files" file-count) on-delete nil "delete-multi-files"])]
{:option-name :separator}
{:option-name (tr "labels.delete-multi-files" file-count)
:id "file-delete-multi"
:option-handler on-delete
:data-test "delete-multi-files"})]
[[(tr "dashboard.open-in-new-tab") on-new-tab]
[(tr "labels.rename") on-edit nil "file-rename"]
[(tr "dashboard.duplicate") on-duplicate nil "file-duplicate"]
[{:option-name (tr "dashboard.open-in-new-tab")
:id "file-open-new-tab"
:option-handler on-new-tab}
{:option-name (tr "labels.rename")
:id "file-rename"
:option-handler on-edit
:data-test "file-rename"}
{:option-name (tr "dashboard.duplicate")
:id "file-duplicate"
:option-handler on-duplicate
:data-test "file-duplicate"}
(when (and (not is-lib-page?) (or (seq current-projects) (seq other-teams)))
[(tr "dashboard.move-to") nil sub-options "file-move-to"])
{:option-name (tr "dashboard.move-to")
:id "file-move-to"
:sub-options sub-options
:data-test "file-move-to"})
(if (:is-shared file)
[(tr "dashboard.unpublish-shared") on-del-shared nil "file-del-shared"]
[(tr "dashboard.add-shared") on-add-shared nil "file-add-shared"])
[:separator]
[(tr "dashboard.download-binary-file") on-export-binary-files nil "download-binary-file"]
[(tr "dashboard.download-standard-file") on-export-standard-files nil "download-standard-file"]
{:option-name (tr "dashboard.unpublish-shared")
:id "file-del-shared"
:option-handler on-del-shared
:data-test "file-del-shared"}
{:option-name (tr "dashboard.add-shared")
:id "file-add-shared"
:option-handler on-add-shared
:data-test "file-add-shared"})
{:option-name :separator}
{:option-name (tr "dashboard.download-binary-file")
:id "file-download-binary"
:option-handler on-export-binary-files
:data-test "download-binary-file"}
{:option-name (tr "dashboard.download-standard-file")
:id "file-download-standard"
:option-handler on-export-standard-files
:data-test "download-standard-file"}
(when (not is-lib-page?)
[:separator]
[(tr "labels.delete") on-delete nil "file-delete"])])]
{:option-name :separator}
{:option-name (tr "labels.delete")
:id "file-delete"
:option-handler on-delete
:data-test "file-delete"})])]
[:& context-menu {:on-close on-menu-close
:show show?
:fixed? (or (not= top 0) (not= left 0))
:min-width? true
:top top
:left left
:options options}]))))
[:& context-menu-a11y {:on-close on-menu-close
:show show?
:fixed? (or (not= top 0) (not= left 0))
:min-width? true
:top top
:left left
:options options
:origin parent-id}]))))

View file

@ -39,7 +39,7 @@
on-menu-click
(mf/use-fn
(fn [event]
(let [position (dom/get-client-position event)]
(let [position (dom/get-client-position event)]
(dom/prevent-default event)
(swap! local assoc :menu-open true :menu-pos position))))

View file

@ -8,6 +8,7 @@
(:require
[app.common.data.macros :as dm]
[app.common.files.features :as ffeat]
[app.common.geom.point :as gpt]
[app.common.logging :as log]
[app.main.data.dashboard :as dd]
[app.main.data.messages :as msg]
@ -251,7 +252,14 @@
(st/emit! (dd/clear-selected-files)))
(st/emit! (dd/toggle-file-select file)))
(let [position (dom/get-client-position event)]
(let [client-position (dom/get-client-position event)
position (if (and (nil? (:y client-position)) (nil? (:x client-position)))
(let [target-element (dom/get-target event)
points (dom/get-bounding-rect target-element)
y (:top points)
x (:left points)]
(gpt/point x y))
client-position)]
(swap! local assoc
:menu-open true
:menu-pos position))))
@ -277,7 +285,7 @@
(swap! local assoc :menu-open false)))
[:li.grid-item.project-th
[:a
[:button
{:tab-index "0"
:class (dom/classnames :selected selected?
:library library-view?)
@ -314,9 +322,11 @@
[:div.project-th-icon.menu
{:tab-index "0"
:ref menu-ref
:id (str file-id "-action-menu")
:on-click on-menu-click
:on-key-down (fn [event]
(when (kbd/enter? event)
(dom/stop-propagation event)
(on-menu-click event)))}
i/actions
(when selected?
@ -328,8 +338,8 @@
:on-edit on-edit
:on-menu-close on-menu-close
:origin origin
:dashboard-local dashboard-local}])]]]]]))
:dashboard-local dashboard-local
:parent-id (str file-id "-action-menu")}])]]]]]))
(mf/defc grid
[{:keys [files project origin limit library-view? create-fn] :as props}]

View file

@ -12,7 +12,7 @@
[app.main.data.modal :as modal]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.context-menu :refer [context-menu]]
[app.main.ui.components.context-menu-a11y :refer [context-menu-a11y]]
[app.main.ui.context :as ctx]
[app.main.ui.dashboard.import :as udi]
[app.util.dom :as dom]
@ -67,7 +67,7 @@
(let [data {:id (:id project) :team-id team-id}
mdata {:on-success #(on-move-success team-id)}]
#(st/emit! (dm/success (tr "dashboard.success-move-project"))
(dd/move-project (with-meta data mdata)))))
(dd/move-project (with-meta data mdata)))))
delete-fn
(fn [_]
@ -77,12 +77,12 @@
on-delete
#(st/emit!
(modal/show
{:type :confirm
:title (tr "modals.delete-project-confirm.title")
:message (tr "modals.delete-project-confirm.message")
:accept-label (tr "modals.delete-project-confirm.accept")
:on-accept delete-fn}))
(modal/show
{:type :confirm
:title (tr "modals.delete-project-confirm.title")
:message (tr "modals.delete-project-confirm.message")
:accept-label (tr "modals.delete-project-confirm.accept")
:on-accept delete-fn}))
file-input (mf/use-ref nil)
@ -94,34 +94,54 @@
on-finish-import
(mf/use-callback
(fn []
(when (fn? on-import) (on-import))))]
(when (fn? on-import) (on-import))))
options [(when-not (:is-default project)
{:option-name (tr "labels.rename")
:id "project-menu-rename"
:option-handler on-edit
:data-test "project-rename"})
(when-not (:is-default project)
{:option-name (tr "dashboard.duplicate")
:id "project-menu-duplicated"
:option-handler on-duplicate
:data-test "project-duplicate"})
(when-not (:is-default project)
{:option-name (tr "dashboard.pin-unpin")
:id "project-menu-pin"
:option-handler toggle-pin})
(when (and (seq teams) (not (:is-default project)))
{:option-name (tr "dashboard.move-to")
:id "project-menu-move-to"
:sub-options (for [team teams]
{:option-name (:name team)
:id (:name team)
:option-handler (on-move (:id team))})
:data-test "project-move-to"})
(when (some? on-import)
{:option-name (tr "dashboard.import")
:id "project-menu-import"
:option-handler on-import-files
:data-test "file-import"})
(when-not (:is-default project)
{:option-name :separator})
(when-not (:is-default project)
{:option-name (tr "labels.delete")
:id "project-menu-delete"
:option-handler on-delete
:data-test "project-delete"})]]
[:*
[:& udi/import-form {:ref file-input
:project-id (:id project)
:on-finish-import on-finish-import}]
[:& context-menu
[:& context-menu-a11y
{:on-close on-menu-close
:show show?
:fixed? (or (not= top 0) (not= left 0))
:min-width? true
:top top
:left left
:options [(when-not (:is-default project)
[(tr "labels.rename") on-edit nil "project-rename"])
(when-not (:is-default project)
[(tr "dashboard.duplicate") on-duplicate nil "project-duplicate"])
(when-not (:is-default project)
[(tr "dashboard.pin-unpin") toggle-pin])
(when (and (seq teams) (not (:is-default project)))
[(tr "dashboard.move-to") nil
(for [team teams]
[(:name team) (on-move (:id team))])
"project-move-to"])
(when (some? on-import)
[(tr "dashboard.import") on-import-files nil "file-import"])
(when-not (:is-default project)
[:separator])
(when-not (:is-default project)
[(tr "labels.delete") on-delete nil "project-delete"])]}]]))
:options options}]]))

View file

@ -7,6 +7,7 @@
(ns app.main.ui.dashboard.projects
(:require
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.common.math :as mth]
[app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
@ -142,7 +143,7 @@
[:div.text
[:h2.title (tr "dasboard.walkthrough-hero.title")]
[:p.info (tr "dasboard.walkthrough-hero.info")]
[:a.btn-primary.action
[:a.btn-primary.action
{:href " https://design.penpot.app/walkthrough"
:target "_blank"
:on-click handle-walkthrough-link}
@ -187,13 +188,23 @@
toggle-pin
(mf/use-fn
(mf/deps project)
#(st/emit! (dd/toggle-project-pin project)))
(fn [event]
(dom/stop-propagation event)
(st/emit! (dd/toggle-project-pin project))))
on-menu-click
(mf/use-fn
(fn [event]
(let [position (dom/get-client-position event)]
(dom/prevent-default event)
(dom/prevent-default event)
(let [client-position (dom/get-client-position event)
position (if (and (nil? (:y client-position)) (nil? (:x client-position)))
(let [target-element (dom/get-target event)
points (dom/get-bounding-rect target-element)
y (:top points)
x (:left points)]
(gpt/point x y))
client-position)]
(swap! local assoc
:menu-open true
:menu-pos position))))
@ -276,7 +287,7 @@
[:& project-menu
{:project project
:show? (:menu-open @local)
:left (:x (:menu-pos @local))
:left (+ 24 (:x (:menu-pos @local)))
:top (:y (:menu-pos @local))
:on-edit on-edit-open
:on-menu-close on-menu-close
@ -291,12 +302,9 @@
(when-not (:is-default project)
[:button.pin-icon.tooltip.tooltip-bottom
{:class (when (:is-pinned project) "active")
:on-click toggle-pin
:on-click toggle-pin
:alt (tr "dashboard.pin-unpin")
:aria-label (tr "dashboard.pin-unpin")
:on-key-down (fn [event]
(when (kbd/enter? event)
(toggle-pin event)))
:tab-index "0"}
(if (:is-pinned project)
i/pin-fill
@ -321,22 +329,27 @@
:tab-index "0"
:on-key-down (fn [event]
(when (kbd/enter? event)
(dom/stop-propagation event)
(on-menu-click event)))}
i/actions]]]
(when (and (> limit 0)
(> file-count limit))
[:div.show-more {:on-click on-nav}
[:div.placeholder-label
(tr "dashboard.show-all-files")]
[:div.placeholder-icon i/arrow-down]])]
i/actions]]]]
[:& line-grid
{:project project
:team team
:files files
:create-fn create-file
:limit limit}]]))
:limit limit}]
(when (and (> limit 0)
(> file-count limit))
[:button.show-more {:on-click on-nav
:tab-index "0"
:on-key-down (fn [event]
(when (kbd/enter? event)
(on-nav)))}
[:div.placeholder-label
(tr "dashboard.show-all-files")]
[:div.placeholder-icon i/arrow-down]])]))
(def recent-files-ref

View file

@ -147,7 +147,7 @@
[^js node selector]
(loop [current node]
(if (or (nil? current) (.matches current selector) )
(if (or (nil? current) (.matches current selector))
current
(recur (.-parentElement current)))))
@ -175,10 +175,10 @@
(defn get-scroll-position
[^js event]
(when (some? event)
{:scroll-height (.-scrollHeight event)
:scroll-left (.-scrollLeft event)
:scroll-top (.-scrollTop event)
:scroll-width (.-scrollWidth event)}))
{:scroll-height (.-scrollHeight event)
:scroll-left (.-scrollLeft event)
:scroll-top (.-scrollTop event)
:scroll-width (.-scrollWidth event)}))
(def get-target-val (comp get-value get-target))
@ -309,6 +309,13 @@
(when (some? el)
(.querySelectorAll el selector))))
(defn get-element-offset-position
[^js node]
(when (some? node)
(let [x (.-offsetTop node)
y (.-offsetLeft node)]
(gpt/point x y))))
(defn get-client-position
[^js event]
(let [x (.-clientX event)
@ -567,7 +574,7 @@
(let [extension (cm/mtype->extension mtype)
opts {:suggestedName (str filename "." extension)
:types [{:description description
:accept { mtype [(str "." extension)]}}]}]
:accept {mtype [(str "." extension)]}}]}]
(-> (p/let [file-system (.showSaveFilePicker globals/window (clj->js opts))
writable (.createWritable file-system)
@ -576,9 +583,9 @@
_ (.write writable blob)]
(.close writable))
(p/catch
#(when-not (and (= (type %) js/DOMException)
(= (.-name %) "AbortError"))
(trigger-download-uri filename mtype uri)))))
#(when-not (and (= (type %) js/DOMException)
(= (.-name %) "AbortError"))
(trigger-download-uri filename mtype uri)))))
(trigger-download-uri filename mtype uri)))
@ -609,9 +616,9 @@
(defn animate!
([item keyframes duration] (animate! item keyframes duration nil))
([item keyframes duration onfinish]
(let [animation (.animate item keyframes duration)]
(when onfinish
(set! (.-onfinish animation) onfinish)))))
(let [animation (.animate item keyframes duration)]
(when onfinish
(set! (.-onfinish animation) onfinish)))))
(defn is-child?
[^js node ^js candidate]