mirror of
https://github.com/penpot/penpot.git
synced 2025-04-15 08:21:40 -05:00
✨ Add new accessibility functionalities to dashboard
This commit is contained in:
parent
9e190d9810
commit
fcb8b15ef2
15 changed files with 625 additions and 145 deletions
|
@ -64,6 +64,10 @@ a {
|
|||
}
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: "worksans", sans-serif;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: $fs12;
|
||||
margin-bottom: 1rem;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
260
frontend/src/app/main/ui/components/context_menu_a11y.cljs
Normal file
260
frontend/src/app/main/ui/components/context_menu_a11y.cljs
Normal 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)))
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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}]))))
|
||||
|
|
|
@ -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))))
|
||||
|
||||
|
|
|
@ -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}]
|
||||
|
|
|
@ -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}]]))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Add table
Reference in a new issue