From 7045496a39f34e9cfe59e8eaeeb5298a191231c5 Mon Sep 17 00:00:00 2001 From: Eva Date: Mon, 28 Nov 2022 08:44:19 +0100 Subject: [PATCH] :sparkles: Improve dashboard accessibility --- .../styles/main/partials/dashboard-grid.scss | 16 +- .../main/partials/dashboard-header.scss | 2 +- .../main/partials/dashboard-sidebar.scss | 24 +- .../styles/main/partials/dashboard.scss | 30 +- .../styles/main/partials/dropdown.scss | 4 + frontend/resources/templates/index.mustache | 2 +- frontend/resources/templates/render.mustache | 2 +- frontend/src/app/main/ui/auth/login.cljs | 12 +- .../app/main/ui/auth/recovery_request.cljs | 4 +- frontend/src/app/main/ui/auth/register.cljs | 12 +- .../app/main/ui/components/button_link.cljs | 2 +- .../app/main/ui/components/dropdown_menu.cljs | 121 +++++ .../app/main/ui/components/file_uploader.cljs | 3 +- .../src/app/main/ui/components/forms.cljs | 4 +- frontend/src/app/main/ui/components/link.cljs | 19 +- frontend/src/app/main/ui/dashboard.cljs | 23 +- .../src/app/main/ui/dashboard/comments.cljs | 15 +- frontend/src/app/main/ui/dashboard/files.cljs | 33 +- frontend/src/app/main/ui/dashboard/fonts.cljs | 7 +- frontend/src/app/main/ui/dashboard/grid.cljs | 144 +++--- .../src/app/main/ui/dashboard/import.cljs | 2 +- .../src/app/main/ui/dashboard/libraries.cljs | 2 +- .../app/main/ui/dashboard/placeholder.cljs | 4 +- .../src/app/main/ui/dashboard/projects.cljs | 64 ++- .../src/app/main/ui/dashboard/search.cljs | 2 +- .../src/app/main/ui/dashboard/sidebar.cljs | 478 ++++++++++++++---- .../src/app/main/ui/settings/sidebar.cljs | 9 +- frontend/src/app/util/keyboard.cljs | 4 + 28 files changed, 761 insertions(+), 283 deletions(-) create mode 100644 frontend/src/app/main/ui/components/dropdown_menu.cljs diff --git a/frontend/resources/styles/main/partials/dashboard-grid.scss b/frontend/resources/styles/main/partials/dashboard-grid.scss index 367531338..5400c271d 100644 --- a/frontend/resources/styles/main/partials/dashboard-grid.scss +++ b/frontend/resources/styles/main/partials/dashboard-grid.scss @@ -9,6 +9,7 @@ height: 100%; overflow: hidden; overflow-y: auto; + margin-bottom: 0; .grid-row { display: grid; @@ -26,6 +27,9 @@ margin: $size-3 $size-4 $size-4 $size-2; position: relative; text-align: center; + a { + width: 100%; + } @media #{$bp-max-1366} { height: 200px; flex: 1 0 230px; @@ -193,13 +197,15 @@ &.project-th { background-color: $color-white; - &:hover { + &:hover, + &:focus, + &:focus-within { .project-th-actions { opacity: 1; } } - &.selected { + .selected { .grid-item-th { border: 2px solid $color-primary; } @@ -241,7 +247,8 @@ width: 18px; } - &:hover { + &:hover, + &:focus { > svg { fill: $color-primary-dark; } @@ -312,7 +319,7 @@ background-repeat: no-repeat; border-top-left-radius: $br-small; border-top-right-radius: $br-small; - height: 70%; + height: 230px; max-height: 160px; overflow: hidden; position: relative; @@ -430,6 +437,7 @@ background-color: rgba(227, 227, 227, 0.3); padding: 13px; margin-right: 13px; + height: 230px; &.loader { justify-items: center; } diff --git a/frontend/resources/styles/main/partials/dashboard-header.scss b/frontend/resources/styles/main/partials/dashboard-header.scss index 51acbfa88..9df42505b 100644 --- a/frontend/resources/styles/main/partials/dashboard-header.scss +++ b/frontend/resources/styles/main/partials/dashboard-header.scss @@ -75,6 +75,7 @@ .dashboard-title { display: flex; align-items: center; + margin-left: 13px; h1 { color: $color-black; @@ -83,7 +84,6 @@ font-size: $fs22; font-weight: 600; z-index: 10; - margin-left: 13px; } .context-menu.is-open { diff --git a/frontend/resources/styles/main/partials/dashboard-sidebar.scss b/frontend/resources/styles/main/partials/dashboard-sidebar.scss index 6f8265ede..50f053316 100644 --- a/frontend/resources/styles/main/partials/dashboard-sidebar.scss +++ b/frontend/resources/styles/main/partials/dashboard-sidebar.scss @@ -7,13 +7,10 @@ .dashboard-sidebar { background-color: $color-white; z-index: 1; - - .sidebar-inside { - display: flex; - flex-direction: column; - height: 100%; - padding-top: $size-2; - } + display: flex; + flex-direction: column; + height: 100%; + padding-top: $size-2; .sidebar-content { display: flex; @@ -67,7 +64,8 @@ justify-content: center; align-items: center; cursor: pointer; - + background-color: transparent; + border: none; svg { width: 15px; height: 13px; @@ -78,15 +76,18 @@ .current-team { cursor: pointer; display: flex; + align-items: center; flex-grow: 1; font-size: $fs14; padding: 0px 10px; + background-color: transparent; + border: none; } .team-name { flex-grow: 1; display: flex; - height: 48px; + height: 40px; align-items: center; &.action { @@ -151,7 +152,7 @@ .switch-icon { display: flex; align-items: center; - + justify-content: center; svg { width: 10px; height: 10px; @@ -200,6 +201,9 @@ display: flex; flex-shrink: 0; padding: $size-2; + a { + width: 100%; + } svg { fill: $color-black; diff --git a/frontend/resources/styles/main/partials/dashboard.scss b/frontend/resources/styles/main/partials/dashboard.scss index f18499919..f574d4baf 100644 --- a/frontend/resources/styles/main/partials/dashboard.scss +++ b/frontend/resources/styles/main/partials/dashboard.scss @@ -72,7 +72,7 @@ border-radius: 8px; min-height: 211px; - .img { + .thumbnail { border-top-left-radius: 6px; border-bottom-left-radius: 6px; padding: 30px; @@ -133,15 +133,10 @@ display: none; width: 0; } - .text { - .info { - // font-size: $fs12; - } - } } } .walkthrough { - .img { + .thumbnail { background-image: url("/images/walkthrough-cover.png"); background-position: center; background-repeat: no-repeat; @@ -149,7 +144,7 @@ } } .tutorial { - .img { + .thumbnail { background-image: url("/images/hands-on-tutorial.png"); background-position: center; background-repeat: no-repeat; @@ -258,7 +253,8 @@ } .project-actions { - display: none; + display: flex; + opacity: 0; margin-left: $size-6; .btn-small { @@ -273,6 +269,8 @@ display: flex; align-items: center; margin-right: 14px; + background-color: transparent; + border: none; svg { width: 15px; height: 15px; @@ -286,9 +284,12 @@ } } } - &:hover { + + &:hover, + &:focus, + &:focus-within { .project-actions { - display: flex; + opacity: 1; } } } @@ -439,7 +440,8 @@ width: 100%; text-align: right; height: 56px; - div { + button { + border: none; cursor: pointer; height: 58px; display: inline-block; @@ -518,11 +520,13 @@ .card-container { width: 275px; - height: 100%; margin-top: 20px; display: inline-block; text-align: center; vertical-align: top; + background-color: transparent; + border: none; + padding: 0; } .template-card { diff --git a/frontend/resources/styles/main/partials/dropdown.scss b/frontend/resources/styles/main/partials/dropdown.scss index 8a2c354e5..9b21ab220 100644 --- a/frontend/resources/styles/main/partials/dropdown.scss +++ b/frontend/resources/styles/main/partials/dropdown.scss @@ -38,6 +38,10 @@ &:hover { background-color: $color-primary-lighter; } + + &:focus { + border: 1px black solid; + } } &.with-check { diff --git a/frontend/resources/templates/index.mustache b/frontend/resources/templates/index.mustache index ba8c3c054..da7083dd3 100644 --- a/frontend/resources/templates/index.mustache +++ b/frontend/resources/templates/index.mustache @@ -37,7 +37,7 @@ {{>../public/images/sprites/symbol/icons.svg}} {{>../public/images/sprites/symbol/cursors.svg}} -
+
{{# manifest}} diff --git a/frontend/resources/templates/render.mustache b/frontend/resources/templates/render.mustache index fa0c9b24c..ed1fd7c37 100644 --- a/frontend/resources/templates/render.mustache +++ b/frontend/resources/templates/render.mustache @@ -17,7 +17,7 @@ {{/manifest}} -
+
{{# manifest}} diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs index baac56f6f..33b5be8e6 100644 --- a/frontend/src/app/main/ui/auth/login.cljs +++ b/frontend/src/app/main/ui/auth/login.cljs @@ -250,20 +250,20 @@ (when (contains? @cf/flags :login) [:div.link-entry [:& lk/link {:action #(st/emit! (rt/nav :auth-recovery-request)) - :name (tr "auth.forgot-password") - :data-test "forgot-password"}]]) + :data-test "forgot-password"} + (tr "auth.forgot-password")]]) (when (contains? @cf/flags :registration) [:div.link-entry [:span (tr "auth.register") " "] [:& lk/link {:action #(st/emit! (rt/nav :auth-register {} params)) - :name (tr "auth.register-submit") - :data-test "register-submit"}]])] + :data-test "register-submit"} + (tr "auth.register-submit")]])] (when (contains? @cf/flags :demo-users) [:div.links.demo [:div.link-entry [:span (tr "auth.create-demo-profile") " "] [:& lk/link {:action #(st/emit! (du/create-demo-profile)) - :name (tr "auth.create-demo-account") - :data-test "demo-account-link"}]]])]]) + :data-test "demo-account-link"} + (tr "auth.create-demo-account")]]])]]) diff --git a/frontend/src/app/main/ui/auth/recovery_request.cljs b/frontend/src/app/main/ui/auth/recovery_request.cljs index 6356c5de2..37e92b226 100644 --- a/frontend/src/app/main/ui/auth/recovery_request.cljs +++ b/frontend/src/app/main/ui/auth/recovery_request.cljs @@ -101,5 +101,5 @@ [:div.links [:div.link-entry [:& lk/link {:action go-back - :name (tr "labels.go-back") - :data-test "go-back-link"}]]]]])) + :data-test "go-back-link"} + (tr "labels.go-back")]]]]])) diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index 38d9bf3ca..16206ab94 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -167,14 +167,14 @@ [:span (tr "auth.already-have-account") " "] [:& lk/link {:action #(st/emit! (rt/nav :auth-login {} params)) - :name (tr "auth.login-here") - :data-test "login-here-link"}]] + :data-test "login-here-link"} + (tr "auth.login-here")]] (when (contains? @cf/flags :demo-users) [:div.link-entry [:span (tr "auth.create-demo-profile") " "] - [:& lk/link {:action #(st/emit! (du/create-demo-profile)) - :name (tr "auth.create-demo-account")}]])]]) + [:& lk/link {:action #(st/emit! (du/create-demo-profile))} + (tr "auth.create-demo-account")]])]]) ;; --- PAGE: register validation @@ -271,8 +271,8 @@ [:div.links [:div.link-entry - [:& lk/link {:action #(st/emit! (rt/nav :auth-register {} {})) - :name (tr "labels.go-back")}]]]]) + [:& lk/link {:action #(st/emit! (rt/nav :auth-register {} {}))} + (tr "labels.go-back")]]]]) (mf/defc register-success-page [{:keys [params] :as props}] diff --git a/frontend/src/app/main/ui/components/button_link.cljs b/frontend/src/app/main/ui/components/button_link.cljs index 66cd2c395..a600e7524 100644 --- a/frontend/src/app/main/ui/components/button_link.cljs +++ b/frontend/src/app/main/ui/components/button_link.cljs @@ -12,7 +12,7 @@ (mf/defc button-link [{:keys [action icon name klass]}] [:a.btn-primary.btn-large.button-link {:class klass - :tabindex "0" + :tab-index "0" :on-click action :on-key-down (fn [event] (when (kbd/enter? event) diff --git a/frontend/src/app/main/ui/components/dropdown_menu.cljs b/frontend/src/app/main/ui/components/dropdown_menu.cljs new file mode 100644 index 000000000..0123bec65 --- /dev/null +++ b/frontend/src/app/main/ui/components/dropdown_menu.cljs @@ -0,0 +1,121 @@ +;; 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.dropdown-menu + (:require + [app.common.data :as d] + [app.config :as cfg] + [app.util.dom :as dom] + [app.util.globals :as globals] + [app.util.keyboard :as kbd] + [goog.events :as events] + [goog.object :as gobj] + [rumext.v2 :as mf]) + (:import goog.events.EventType)) + +(mf/defc dropdown-menu-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 "klass") + 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 dropdown-menu' + {::mf/wrap-props false} + [props] + (let [children (gobj/get props "children") + on-close (gobj/get props "on-close") + ref (gobj/get props "container") + ids (gobj/get props "ids") + list-class (gobj/get props "list-class") + + on-click + (fn [event] + (let [target (dom/get-target event) + + ;; MacOS ctrl+click sends two events: context-menu and click. + ;; In order to not have two handlings we ignore ctrl+click for this platform + mac-ctrl-click? (and (cfg/check-platform? :macos) (kbd/ctrl? event))] + (when (and (not mac-ctrl-click?) + (not (.-data-no-close ^js target))) + (if ref + (let [parent (mf/ref-val ref)] + (when-not (or (not parent) (.contains parent target)) + (on-close))) + (on-close))))) + + on-keyup + (fn [event] + (when (kbd/esc? event) + (on-close))) + + on-key-down + (fn [event] + (let [first-id (dom/get-element (first ids)) + first-element (dom/get-element first-id) + len (count ids)] + + (when (kbd/home? event) + (when first-element + (dom/focus! first-element))) + + (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 (kbd/tab? event) + (on-close)))) + + on-mount + (fn [] + (let [keys [(events/listen globals/document EventType.CLICK on-click) + (events/listen globals/document EventType.CONTEXTMENU on-click) + (events/listen globals/document EventType.KEYUP on-keyup) + (events/listen globals/document EventType.KEYDOWN on-key-down)]] + #(doseq [key keys] + (events/unlistenByKey key))))] + + (mf/use-effect on-mount) + [:ul {:class list-class + :role "menu"} + children])) + +(mf/defc dropdown-menu + {::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") + + (when (gobj/get props "show") + (mf/element dropdown-menu' props))) diff --git a/frontend/src/app/main/ui/components/file_uploader.cljs b/frontend/src/app/main/ui/components/file_uploader.cljs index 3587e17fe..71af00d4e 100644 --- a/frontend/src/app/main/ui/components/file_uploader.cljs +++ b/frontend/src/app/main/ui/components/file_uploader.cljs @@ -39,5 +39,6 @@ :type "file" :ref input-ref :on-change on-files-selected - :data-test data-test}]])) + :data-test data-test + :aria-label "uploader"}]])) diff --git a/frontend/src/app/main/ui/components/forms.cljs b/frontend/src/app/main/ui/components/forms.cljs index 176f99d78..f25358719 100644 --- a/frontend/src/app/main/ui/components/forms.cljs +++ b/frontend/src/app/main/ui/components/forms.cljs @@ -104,7 +104,7 @@ :placeholder label :on-change on-change :type @type' - :tabindex "0") + :tab-index "0") (cond-> (and value is-checkbox?) (assoc :default-checked value)) (cond-> (and touched? (:message error)) (assoc "aria-invalid" "true" "aria-describedby" (dm/str "error-" input-name))) @@ -224,7 +224,7 @@ {:name "submit" :class (when (or (not (:valid @form)) (true? disabled)) "btn-disabled") :disabled (or (not (:valid @form)) (true? disabled)) - :tabindex "0" + :tab-index "0" :on-click on-click :on-key-down (fn [event] (when (kbd/enter? event) diff --git a/frontend/src/app/main/ui/components/link.cljs b/frontend/src/app/main/ui/components/link.cljs index 2b9c19184..3e845628e 100644 --- a/frontend/src/app/main/ui/components/link.cljs +++ b/frontend/src/app/main/ui/components/link.cljs @@ -9,12 +9,13 @@ [app.util.keyboard :as kbd] [rumext.v2 :as mf])) -(mf/defc link [{:keys [action name klass data-test]}] - [:a {:on-click action - :klass klass - :on-key-down (fn [event] - (when (kbd/enter? event) - (action event))) - :tabindex "0" - :data-test data-test} - name]) +(mf/defc link [{:keys [action klass data-test keyboard-action children]}] + (let [keyboard-action (or keyboard-action action)] + [:a {:on-click action + :class klass + :on-key-down (fn [event] + (when (kbd/enter? event) + (keyboard-action event))) + :tab-index "0" + :data-test data-test} + [:* children]])) diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index 646859c9f..4cc988689 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -145,15 +145,20 @@ [:div.dashboard-templates-section {:class (when collapsed "collapsed")} [:div.title - [:div {:on-click toggle-collapse} + [:button {:tab-index "0" + :on-click 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")}} (for [num-item (range (count templates)) :let [item (nth templates num-item)]] - [:div.card-container {:id (str/concat "card-container-" num-item) - :key (:id item) - :on-click #(import-template 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)))} [:div.template-card [:div.img-container [:img {:src (:thumbnail-uri item) @@ -163,14 +168,16 @@ [:div.card-container [:div.template-card [:div.img-container - [:a {:href "https://penpot.app/libraries-templates.html" :target "_blank" :on-click handle-template-link} + [:a {:tab-index "0" + :href "https://penpot.app/libraries-templates.html" :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) - [:div.button.left {:on-click move-left} i/go-prev]) + [:button.button.left {:on-click move-left} i/go-prev]) (when more-cards - [:div.button.right {:on-click move-right} i/go-next])])) + [:button.button.right {:on-click move-right + :aria-label (tr "labels.next")} i/go-next])])) (mf/defc dashboard-content [{:keys [team projects project section search-term profile] :as props}] @@ -285,7 +292,7 @@ ;; components on team change. Many components assumes that the ;; team is already set so don't put the team into mf/deps. (when team - [:section.dashboard-layout {:key (:id team)} + [:main.dashboard-layout {:key (:id team)} [:& sidebar {:team team :projects projects diff --git a/frontend/src/app/main/ui/dashboard/comments.cljs b/frontend/src/app/main/ui/dashboard/comments.cljs index eaf699ad9..c03518fb9 100644 --- a/frontend/src/app/main/ui/dashboard/comments.cljs +++ b/frontend/src/app/main/ui/dashboard/comments.cljs @@ -16,6 +16,7 @@ [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] + [app.util.keyboard :as kbd] [potok.core :as ptk] [rumext.v2 :as mf])) @@ -54,7 +55,11 @@ [:div.dashboard-comments-section [:div.button - {:on-click show-dropdown + {:tab-index "0" + :on-click show-dropdown + :on-key-down (fn [event] + (when (kbd/enter? event) + (show-dropdown event))) :data-test "open-comments" :class (dom/classnames :open @show-dropdown? :unread (boolean (seq tgroups)))} @@ -64,7 +69,13 @@ [:div.dropdown.comments-section.comment-threads-section. [:div.header [:h3 (tr "labels.comments")] - [:span.close {:on-click hide-dropdown} i/close]] + [:span.close {:tab-index (if @show-dropdown? + "0" + "-1") + :on-click hide-dropdown + :on-key-down (fn [event] + (when (kbd/enter? event) + (hide-dropdown event)))} i/close]] [:hr] diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs index 747192a7c..712183d96 100644 --- a/frontend/src/app/main/ui/dashboard/files.cljs +++ b/frontend/src/app/main/ui/dashboard/files.cljs @@ -17,6 +17,7 @@ [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.webapi :as wapi] [beicon.core :as rx] [cuerdas.core :as str] @@ -63,7 +64,7 @@ [:header.dashboard-header (if (:is-default project) - [:div.dashboard-title + [:div.dashboard-title#dashboard-drafts-title [:h1 (tr "labels.drafts")]] (if (:edition @local) @@ -75,7 +76,9 @@ (with-meta {::ev/origin "project"})))) (swap! local assoc :edition false)))}] [:div.dashboard-title - [:h1 {:on-double-click on-edit :data-test "project-title"} + [:h1 {:on-double-click on-edit + :data-test "project-title" + :id (:id project)} (:name project)]])) [:& project-menu {:project project @@ -87,19 +90,35 @@ :on-import on-import}] [:div.dashboard-header-actions - [:a.btn-secondary.btn-small {:on-click on-create-click :data-test "new-file"} + [:a.btn-secondary.btn-small + {:tab-index "0" + :on-click on-create-click + :data-test "new-file" + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-create-click event)))} (tr "dashboard.new-file")] (when-not (:is-default project) - [:div.icon.pin-icon.tooltip.tooltip-bottom - {:class (when (:is-pinned project) "active") - :on-click toggle-pin :alt (tr "dashboard.pin-unpin")} + [:button.icon.pin-icon.tooltip.tooltip-bottom + {:tab-index "0" + :class (when (:is-pinned project) "active") + :on-click toggle-pin + :alt (tr "dashboard.pin-unpin") + :on-key-down (fn [event] + (when (kbd/enter? event) + (toggle-pin event)))} (if (:is-pinned project) i/pin-fill i/pin)]) [:div.icon.tooltip.tooltip-bottom-left - {:on-click on-menu-click :alt (tr "dashboard.options")} + {:tab-index "0" + :on-click on-menu-click + :alt (tr "dashboard.options") + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-menu-click event)))} i/actions]]])) (mf/defc files-section diff --git a/frontend/src/app/main/ui/dashboard/fonts.cljs b/frontend/src/app/main/ui/dashboard/fonts.cljs index 80e05287b..ffc657de6 100644 --- a/frontend/src/app/main/ui/dashboard/fonts.cljs +++ b/frontend/src/app/main/ui/dashboard/fonts.cljs @@ -51,7 +51,7 @@ (use-set-page-title team section) [:header.dashboard-header - [:div.dashboard-title + [:div.dashboard-title#dashboard-fonts-title [:h1 (tr "labels.fonts")]] [:nav #_[:ul @@ -134,8 +134,9 @@ [:& i18n/tr-html {:tag-name "span" :label "dashboard.fonts.hero-text2"}]]]] - [:div.btn-primary - {:on-click on-click} + [:button.btn-primary + {:on-click on-click + :tab-index "0"} [:span (tr "labels.add-custom-font")] [:& file-uploader {:input-id "font-upload" :accept cm/str-font-types diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index 79e9193f3..194b09e3a 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -192,7 +192,7 @@ on-menu-close (mf/use-fn - #(swap! local assoc :menu-open false)) + #(swap! local assoc :menu-open false)) on-select (fn [event] @@ -215,31 +215,31 @@ on-drag-start (mf/use-fn - (mf/deps selected-files) - (fn [event] - (let [offset (dom/get-offset-position (.-nativeEvent event)) + (mf/deps selected-files) + (fn [event] + (let [offset (dom/get-offset-position (.-nativeEvent event)) - select-current? (not (contains? selected-files (:id file))) + select-current? (not (contains? selected-files (:id file))) - item-el (mf/ref-val node-ref) - counter-el (create-counter-element item-el - (if select-current? - 1 - (count selected-files)))] - (when select-current? - (st/emit! (dd/clear-selected-files)) - (st/emit! (dd/toggle-file-select file))) + item-el (mf/ref-val node-ref) + counter-el (create-counter-element item-el + (if select-current? + 1 + (count selected-files)))] + (when select-current? + (st/emit! (dd/clear-selected-files)) + (st/emit! (dd/toggle-file-select file))) - (dnd/set-data! event "penpot/files" "dummy") - (dnd/set-allowed-effect! event "move") + (dnd/set-data! event "penpot/files" "dummy") + (dnd/set-allowed-effect! event "move") ;; set-drag-image requires that the element is rendered and ;; visible to the user at the moment of creating the ghost ;; image (to make a snapshot), but you may remove it right ;; afterwards, in the next render cycle. - (dom/append-child! item-el counter-el) - (dnd/set-drag-image! event item-el (:x offset) (:y offset)) - (ts/raf #(.removeChild ^js item-el counter-el))))) + (dom/append-child! item-el counter-el) + (dnd/set-drag-image! event item-el (:x offset) (:y offset)) + (ts/raf #(.removeChild ^js item-el counter-el))))) on-menu-click (mf/use-fn @@ -276,44 +276,60 @@ (when (and (not selected?) (:menu-open @local)) (swap! local assoc :menu-open false))) - [:div.grid-item.project-th - {:class (dom/classnames :selected selected? - :library library-view?) - :ref node-ref - :draggable true - :on-click on-select - :on-double-click on-navigate - :on-drag-start on-drag-start - :on-context-menu on-menu-click} + [:li.grid-item.project-th + [:a + {:tab-index "0" + :class (dom/classnames :selected selected? + :library library-view?) + :ref node-ref + :draggable true + :on-click on-select + :on-key-down (fn [event] + (dom/stop-propagation event) + (when (kbd/enter? event) + (on-navigate event)) + (when (kbd/shift? 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 + )) + :on-double-click on-navigate + :on-drag-start on-drag-start + :on-context-menu on-menu-click} - [:div.overlay] - (if library-view? - [:& grid-item-library {:file file}] - [:& grid-item-thumbnail {:file file}]) - (when (and (:is-shared file) (not library-view?)) - [:div.item-badge i/library]) - [:div.item-info - (if (:edition @local) - [:& inline-edition {:content (:name file) - :on-end edit}] - [:h3 (:name file)]) - [:& grid-item-metadata {:modified-at (:modified-at file)}]] - [:div.project-th-actions {:class (dom/classnames - :force-display (:menu-open @local))} - [:div.project-th-icon.menu - {:ref menu-ref - :on-click on-menu-click} - i/actions - (when selected? - [:& file-menu {:files (vals selected-files) - :show? (:menu-open @local) - :left (+ 24 (:x (:menu-pos @local))) - :top (:y (:menu-pos @local)) - :navigate? navigate? - :on-edit on-edit - :on-menu-close on-menu-close - :origin origin - :dashboard-local dashboard-local}])]]])) + [:div.overlay] + (if library-view? + [:& grid-item-library {:file file}] + [:& grid-item-thumbnail {:file file}]) + (when (and (:is-shared file) (not library-view?)) + [:div.item-badge i/library]) + [:div.item-info + (if (:edition @local) + [:& inline-edition {:content (:name file) + :on-end edit}] + [:h3 (:name file)]) + [:& grid-item-metadata {:modified-at (:modified-at file)}]] + [:div.project-th-actions {:class (dom/classnames + :force-display (:menu-open @local))} + [:div.project-th-icon.menu + {:tab-index "0" + :ref menu-ref + :on-click on-menu-click + :on-key-down (fn [event] + (when (kbd/enter? event) + (prn "entro en on-menu-click") + (on-menu-click event)))} + i/actions + (when selected? + [:& file-menu {:files (vals selected-files) + :show? (:menu-open @local) + :left (+ 24 (:x (:menu-pos @local))) + :top (:y (:menu-pos @local)) + :navigate? navigate? + :on-edit on-edit + :on-menu-close on-menu-close + :origin origin + :dashboard-local dashboard-local}])]]]] + )) (mf/defc grid @@ -361,7 +377,7 @@ (reset! dragging? false) (import-files (.-files (.-dataTransfer e))))))] - [:section.dashboard-grid + [:div.dashboard-grid {:on-drag-enter on-drag-enter :on-drag-over on-drag-over :on-drag-leave on-drag-leave @@ -372,11 +388,11 @@ [:& loading-placeholder] (seq files) - [:div.grid-row + [:ul.grid-row {:style {:grid-template-columns (str "repeat(" limit ", 1fr)")}} (when @dragging? - [:div.grid-item]) + [:li.grid-item]) (for [item files] [:& grid-item @@ -396,11 +412,11 @@ [{:keys [files selected-files dragging? limit] :as props}] (let [elements limit limit (if dragging? (dec limit) limit)] - [:div.grid-row.no-wrap + [:ul.grid-row.no-wrap {:style {:grid-template-columns (dm/str "repeat(" elements ", 1fr)")}} (when dragging? - [:div.grid-item.dragged]) + [:li.grid-item.dragged]) (for [item (take limit files)] [:& grid-item {:id (:id item) @@ -481,10 +497,10 @@ mdata {:on-success on-drop-success}] (st/emit! (dd/move-files (with-meta data mdata))))))))] - [:section.dashboard-grid {:on-drag-enter on-drag-enter - :on-drag-over on-drag-over - :on-drag-leave on-drag-leave - :on-drop on-drop} + [:div.dashboard-grid {:on-drag-enter on-drag-enter + :on-drag-over on-drag-over + :on-drag-leave on-drag-leave + :on-drop on-drop} (cond (nil? files) [:& loading-placeholder] diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs index ec333749b..66a481d1d 100644 --- a/frontend/src/app/main/ui/dashboard/import.cljs +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -51,7 +51,7 @@ [{:keys [project-id on-finish-import]} external-ref] (let [on-file-selected (use-import-file project-id on-finish-import)] - [:form.import-file + [:form.import-file {:aria-hidden "true"} [:& file-uploader {:accept ".penpot,.zip" :multi true :ref external-ref diff --git a/frontend/src/app/main/ui/dashboard/libraries.cljs b/frontend/src/app/main/ui/dashboard/libraries.cljs index 73f33447e..cb49dfa58 100644 --- a/frontend/src/app/main/ui/dashboard/libraries.cljs +++ b/frontend/src/app/main/ui/dashboard/libraries.cljs @@ -71,7 +71,7 @@ [:* [:header.dashboard-header {:ref rowref} - [:div.dashboard-title + [:div.dashboard-title#dashboard-libraries-title [:h1 (tr "dashboard.libraries-title")]]] [:section.dashboard-container.no-bg.dashboard-shared [:& grid {:files files diff --git a/frontend/src/app/main/ui/dashboard/placeholder.cljs b/frontend/src/app/main/ui/dashboard/placeholder.cljs index 1f093d7d7..9b22d6330 100644 --- a/frontend/src/app/main/ui/dashboard/placeholder.cljs +++ b/frontend/src/app/main/ui/dashboard/placeholder.cljs @@ -19,9 +19,9 @@ (create-fn "dashboard:empty-folder-placeholder")))] (cond (true? dragging?) - [:div.grid-row.no-wrap + [:ul.grid-row.no-wrap {:style {:grid-template-columns (str "repeat(" limit ", 1fr)")}} - [:div.grid-item]] + [:li.grid-item]] (= :libraries origin) [:div.grid-empty-placeholder.libs {:data-test "empty-placeholder"} diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index 41636bba0..30aed2456 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -21,6 +21,7 @@ [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.router :as rt] [app.util.time :as dt] [app.util.webapi :as wapi] @@ -35,9 +36,9 @@ [] (let [on-click (mf/use-fn #(st/emit! (dd/create-project)))] [:header.dashboard-header - [:div.dashboard-title + [:div.dashboard-title#dashboard-projects-title [:h1 (tr "dashboard.projects-title")]] - [:a.btn-secondary.btn-small + [:button.btn-secondary.btn-small {:on-click on-click :data-test "new-project-button"} (tr "dashboard.new-project")]])) @@ -112,11 +113,11 @@ (swap! state #(assoc % :status :importing)) (st/emit! (with-meta (dd/clone-template (with-meta params mdata)) {::ev/origin "get-started-hero-block"})))))] - [:div.tutorial - [:div.img] + [:article.tutorial + [:div.thumbnail] [:div.text - [:div.title (tr "dasboard.tutorial-hero.title")] - [:div.info (tr "dasboard.tutorial-hero.info")] + [:h2.title (tr "dasboard.tutorial-hero.title")] + [:p.info (tr "dasboard.tutorial-hero.info")] [:button.btn-primary.action {:on-click download-tutorial} (case (:status @state) :waiting (tr "dasboard.tutorial-hero.start") @@ -136,12 +137,15 @@ (st/emit! (ptk/event ::ev/event {::ev/name "show-walkthrough" ::ev/origin "get-started-hero-block" :section "dashboard"})))] - [:div.walkthrough - [:div.img] + [:article.walkthrough + [:div.thumbnail] [:div.text - [:div.title (tr "dasboard.walkthrough-hero.title")] - [:div.info (tr "dasboard.walkthrough-hero.info")] - [:a.btn-primary.action {:href " https://design.penpot.app/walkthrough" :target "_blank" :on-click handle-walkthrough-link} + [:h2.title (tr "dasboard.walkthrough-hero.title")] + [:p.info (tr "dasboard.walkthrough-hero.info")] + [:a.btn-primary.action + {:href " https://design.penpot.app/walkthrough" + :target "_blank" + :on-click handle-walkthrough-link} (tr "dasboard.walkthrough-hero.start")]] [:button.close {:on-click close-walkthrough @@ -165,8 +169,8 @@ width (mf/use-state nil) rowref (mf/use-ref) itemsize (if (>= @width 1030) - 280 - 230) + 280 + 230) ratio (if (some? @width) (/ @width itemsize) 0) nitems (mth/floor ratio) @@ -256,9 +260,9 @@ (vreset! mnt? false) (rx/dispose! sub)))) - [:div.dashboard-project-row + [:article.dashboard-project-row {:class (when first? "first")} - [:div.project {:ref rowref} + [:header.project {:ref rowref} [:div.project-name-wrapper (if (:edition? @local) [:& inline-edition {:content (:name project) @@ -285,23 +289,39 @@ [:span.recent-files-row-title-info (str ", " time)])) [:div.project-actions (when-not (:is-default project) - [:span.pin-icon.tooltip.tooltip-bottom + [:button.pin-icon.tooltip.tooltip-bottom {:class (when (:is-pinned project) "active") - :on-click toggle-pin :alt (tr "dashboard.pin-unpin")} + :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 i/pin)]) - [:a.btn-secondary.btn-small.tooltip.tooltip-bottom + [:button.btn-secondary.btn-small.tooltip.tooltip-bottom {:on-click on-create-click :alt (tr "dashboard.new-file") - :data-test "project-new-file"} + :aria-label (tr "dashboard.new-file") + :data-test "project-new-file" + :tab-index "0" + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-create-click event)))} i/close] - [:a.btn-secondary.btn-small.tooltip.tooltip-bottom + [:button.btn-secondary.btn-small.tooltip.tooltip-bottom {:on-click on-menu-click :alt (tr "dashboard.options") - :data-test "project-options"} + :aria-label (tr "dashboard.options") + :data-test "project-options" + :tab-index "0" + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-menu-click event)))} i/actions]]] (when (and (> limit 0) @@ -387,7 +407,7 @@ [:& interface-walkthrough {:close-walkthrough close-walkthrough}])]) - [:section.dashboard-container.no-bg + [:div.dashboard-container.no-bg (for [{:keys [id] :as project} projects] (let [files (when recent-map (->> (vals recent-map) diff --git a/frontend/src/app/main/ui/dashboard/search.cljs b/frontend/src/app/main/ui/dashboard/search.cljs index 865b52575..8d5bf9fb3 100644 --- a/frontend/src/app/main/ui/dashboard/search.cljs +++ b/frontend/src/app/main/ui/dashboard/search.cljs @@ -64,7 +64,7 @@ (rx/dispose! sub))))) [:* [:header.dashboard-header - [:div.dashboard-title + [:div.dashboard-title#dashboard-search-title [:h1 (tr "dashboard.title-search")]]] [:section.dashboard-container.search.no-bg {:ref rowref} diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 320eada9b..0b4e574f5 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -17,7 +17,8 @@ [app.main.data.users :as du] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item]] + [app.main.ui.components.link :refer [link]] [app.main.ui.dashboard.comments :refer [comments-section]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.dashboard.project-menu :refer [project-menu]] @@ -29,6 +30,7 @@ [app.util.keyboard :as kbd] [app.util.object :as obj] [app.util.router :as rt] + [app.util.timers :as ts] [beicon.core :as rx] [cljs.spec.alpha :as s] [goog.functions :as f] @@ -54,6 +56,20 @@ (fn [] (st/emit! (dd/go-to-files (:id item))))) + on-key-down + (mf/use-callback + (mf/deps item) + (fn [event] + (when (kbd/enter? event) + (st/emit! (dd/go-to-files (:id item)) + (ts/schedule-on-idle + (fn [] + (let [project-title (dom/get-element (str (:id item)))] + (when project-title + (dom/set-attribute! project-title "tabindex" "0") + (dom/focus! project-title) + (dom/set-attribute! project-title "tabindex" "-1"))))))))) + on-menu-click (mf/use-callback (fn [event] @@ -79,25 +95,25 @@ on-drag-enter (mf/use-callback - (mf/deps selected-project) - (fn [e] - (when (dnd/has-type? e "penpot/files") - (dom/prevent-default e) - (when-not (dnd/from-child? e) - (when (not= selected-project (:id item)) - (swap! local assoc :dragging? true)))))) + (mf/deps selected-project) + (fn [e] + (when (dnd/has-type? e "penpot/files") + (dom/prevent-default e) + (when-not (dnd/from-child? e) + (when (not= selected-project (:id item)) + (swap! local assoc :dragging? true)))))) on-drag-over (mf/use-callback - (fn [e] - (when (dnd/has-type? e "penpot/files") - (dom/prevent-default e)))) + (fn [e] + (when (dnd/has-type? e "penpot/files") + (dom/prevent-default e)))) on-drag-leave (mf/use-callback - (fn [e] - (when-not (dnd/from-child? e) - (swap! local assoc :dragging? false)))) + (fn [e] + (when-not (dnd/from-child? e) + (swap! local assoc :dragging? false)))) on-drop-success (mf/use-callback @@ -117,9 +133,11 @@ (st/emit! (dd/move-files (with-meta data mdata)))))))] [:* - [:li {:class (if selected? "current" - (when (:dragging? @local) "dragging")) + [:li {:tab-index "0" + :class (if selected? "current" + (when (:dragging? @local) "dragging")) :on-click on-click + :on-key-down on-key-down :on-double-click on-edit-open :on-context-menu on-menu-click :on-drag-enter on-drag-enter @@ -143,15 +161,6 @@ focused? (mf/use-state false) emit! (mf/use-memo #(f/debounce st/emit! 500)) - on-search-focus - (mf/use-callback - (mf/deps team-id) - (fn [event] - (reset! focused? true) - (let [value (dom/get-target-val event)] - (dom/select-text! (dom/get-target event)) - (emit! (dd/go-to-search value))))) - on-search-blur (mf/use-callback (fn [_] @@ -167,16 +176,25 @@ on-clear-click (mf/use-callback (mf/deps team-id) - (fn [_] + (fn [e] (let [search-input (dom/get-element "search-input")] (dom/clean-value! search-input) (dom/focus! search-input) - (emit! (dd/go-to-search))))) + (emit! (dd/go-to-search)) + (dom/prevent-default e) + (dom/stop-propagation e)))) on-key-press (mf/use-callback (fn [e] (when (kbd/enter? e) + (ts/schedule-on-idle + (fn [] + (let [search-title (dom/get-element (str "dashboard-search-title"))] + (when search-title + (dom/set-attribute! search-title "tabindex" "0") + (dom/focus! search-title) + (dom/set-attribute! search-title "tabindex" "-1"))))) (dom/prevent-default e) (dom/stop-propagation e))))] @@ -185,10 +203,11 @@ {:key "images-search-box" :id "search-input" :type "text" + :aria-label (tr "dashboard.search-placeholder") :placeholder (tr "dashboard.search-placeholder") :default-value search-term :auto-complete "off" - :on-focus on-search-focus + ;; :on-focus on-search-focus :on-blur on-search-blur :on-change on-search-change :on-key-press on-key-press @@ -196,18 +215,20 @@ (if (or @focused? (seq search-term)) [:div.clear-search - {:on-click on-clear-click} + {:tab-index "0" + :on-click on-clear-click + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-clear-click event)))} i/close] [:div.search {:on-click on-clear-click} i/search])])) -(mf/defc teams-selector-dropdown - [{:keys [team profile] :as props}] - (let [teams (mf/deref refs/teams) - - on-create-clicked +(mf/defc teams-selector-dropdown-items + [{:keys [team profile teams] :as props}] + (let [on-create-clicked (mf/use-callback #(st/emit! (modal/show :team-form {}))) @@ -216,24 +237,40 @@ (fn [team-id] (st/emit! (dd/go-to-projects team-id))))] - [:ul.dropdown.teams-dropdown - [:li.team-name {:on-click (partial team-selected (:default-team-id profile))} + [:* + [:& dropdown-menu-item {:on-click (partial team-selected (:default-team-id profile)) + :on-key-down (fn [event] + (when (kbd/enter? event) + (team-selected (:default-team-id profile) event))) + :id "teams-selector-default-team" + :klass "team-name"} [:span.team-icon i/logo-icon] [:span.team-text (tr "dashboard.your-penpot")] (when (= (:default-team-id profile) (:id team)) [:span.icon i/tick])] (for [team-item (remove :is-default (vals teams))] - [:li.team-name {:on-click (partial team-selected (:id team-item)) - :key (dm/str (:id team-item))} + [:& dropdown-menu-item {:on-click (partial team-selected (:id team-item)) + :on-key-down (fn [event] + (when (kbd/enter? event) + (team-selected (:id team-item) event))) + :id (str "teams-selector-" (:id team-item)) + :klass "team-name" + :key (dm/str (:id team-item))} [:span.team-icon [:img {:src (cf/resolve-team-photo-url team-item) :alt (:name team-item)}]] [:span.team-text {:title (:name team-item)} (:name team-item)] (when (= (:id team-item) (:id team)) [:span.icon i/tick])]) - [:hr] - [:li.team-name.action {:on-click on-create-clicked :data-test "create-new-team"} + [:hr {:role "separator"}] + [:& dropdown-menu-item {:on-click on-create-clicked + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-create-clicked event))) + :id "teams-selector-create-team" + :klass "team-name action" + :key "teams-selector-create-team"} [:span.team-icon.new-team i/close] [:span.team-text (tr "dashboard.create-new-team")]]])) @@ -288,11 +325,11 @@ on-leave-clicked #(st/emit! (modal/show - {:type :confirm - :title (tr "modals.leave-confirm.title") - :message (tr "modals.leave-confirm.message") - :accept-label (tr "modals.leave-confirm.accept") - :on-accept leave-fn})) + {:type :confirm + :title (tr "modals.leave-confirm.title") + :message (tr "modals.leave-confirm.message") + :accept-label (tr "modals.leave-confirm.accept") + :on-accept leave-fn})) on-leave-as-owner-clicked (fn [] @@ -305,55 +342,145 @@ leave-and-close #(st/emit! (modal/show - {:type :confirm - :title (tr "modals.leave-confirm.title") - :message (tr "modals.leave-and-close-confirm.message" (:name team)) - :scd-message (tr "modals.leave-and-close-confirm.hint") - :accept-label (tr "modals.leave-confirm.accept") - :on-accept delete-fn})) + {:type :confirm + :title (tr "modals.leave-confirm.title") + :message (tr "modals.leave-and-close-confirm.message" (:name team)) + :scd-message (tr "modals.leave-and-close-confirm.hint") + :accept-label (tr "modals.leave-confirm.accept") + :on-accept delete-fn})) on-delete-clicked #(st/emit! - (modal/show - {:type :confirm - :title (tr "modals.delete-team-confirm.title") - :message (tr "modals.delete-team-confirm.message") - :accept-label (tr "modals.delete-team-confirm.accept") - :on-accept delete-fn}))] + (modal/show + {:type :confirm + :title (tr "modals.delete-team-confirm.title") + :message (tr "modals.delete-team-confirm.message") + :accept-label (tr "modals.delete-team-confirm.accept") + :on-accept delete-fn}))] + + [:* + [:& dropdown-menu-item {:on-click go-members + :on-key-down (fn [event] + (when (kbd/enter? event) + (go-members))) + :id "teams-options-members" + :key "teams-options-members" + :data-test "team-members"} + (tr "labels.members")] + [:& dropdown-menu-item {:on-click go-invitations + :on-key-down (fn [event] + (when (kbd/enter? event) + (go-invitations))) + :id "teams-options-invitations" + :key "teams-options-invitations" + :data-test "team-invitations"} + (tr "labels.invitations")] - [:ul.dropdown.options-dropdown - [:li {:on-click go-members :data-test "team-members"} (tr "labels.members")] - [:li {:on-click go-invitations :data-test "team-invitations"} (tr "labels.invitations")] (when (contains? @cf/flags :webhooks) - [:li {:on-click go-webhooks :data-test "team-webhooks"} (tr "labels.webhooks")]) - [:li {:on-click go-settings :data-test "team-settings"} (tr "labels.settings")] + [:& dropdown-menu-item {:on-click go-webhooks + :on-key-down (fn [event] + (when (kbd/enter? event) + (go-webhooks))) + :id "teams-options-webhooks" + :key "teams-options-webhooks"} + (tr "labels.webhooks")]) + + [:& dropdown-menu-item {:on-click go-settings + :on-key-down (fn [event] + (when (kbd/enter? event) + (go-settings))) + :id "teams-options-settings" + :key "teams-options-settings" + :data-test "team-settings"} + (tr "labels.settings")] + [:hr] (when can-rename? - [:li {:on-click on-rename-clicked :data-test "rename-team"} (tr "labels.rename")]) + [:& dropdown-menu-item {:on-click on-rename-clicked + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-rename-clicked))) + :id "teams-options-rename" + :key "teams-options-rename" + :data-test "rename-team"} + (tr "labels.rename")]) (cond (= (count members) 1) - [:li {:on-click leave-and-close} (tr "dashboard.leave-team")] + [:& dropdown-menu-item {:on-click leave-and-close + :on-key-down (fn [event] + (when (kbd/enter? event) + (leave-and-close))) + :id "teams-options-leave-team" + :key "teams-options-leave-team"} + (tr "dashboard.leave-team")] + (get-in team [:permissions :is-owner]) - [:li {:on-click on-leave-as-owner-clicked :data-test "leave-team"} (tr "dashboard.leave-team")] + [:& dropdown-menu-item {:on-click on-leave-as-owner-clicked + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-leave-as-owner-clicked))) + :id "teams-options-leave-team" + :key "teams-options-leave-team" + :data-test "leave-team"} + (tr "dashboard.leave-team")] (> (count members) 1) - [:li {:on-click on-leave-clicked} (tr "dashboard.leave-team")]) + [:& dropdown-menu-item {:on-click on-leave-clicked + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-leave-clicked))) + :id "teams-options-leave-team" + :key "teams-options-leave-team"} + (tr "dashboard.leave-team")]) (when (get-in team [:permissions :is-owner]) - [:li.warning {:on-click on-delete-clicked :data-test "delete-team"} (tr "dashboard.delete-team")])])) + [:& dropdown-menu-item {:on-click on-delete-clicked + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-delete-clicked))) + :id "teams-options-delete-team" + :key "teams-options-delete-team" + :klass "warning" + :data-test "delete-team"} + (tr "dashboard.delete-team")])])) (mf/defc sidebar-team-switch [{:keys [team profile] :as props}] - (let [show-team-opts-ddwn? (mf/use-state false) - show-teams-ddwn? (mf/use-state false)] + (let [teams (mf/deref refs/teams) + teams-without-default (into {} (filter (fn [[_ v]] (= false (:is-default v))) teams)) + team-ids (map #(str "teams-selector-" %) (keys teams-without-default)) + ids (concat ["teams-selector-default-team"] team-ids ["teams-selector-create-team"]) + show-team-opts-ddwn? (mf/use-state false) + show-teams-ddwn? (mf/use-state false) + can-rename? (or (get-in team [:permissions :is-owner]) (get-in team [:permissions :is-admin])) + options-ids ["teams-options-members" + "teams-options-invitations" + (when (contains? @cf/flags :webhooks) + "teams-options-webhooks") + "teams-options-settings" + (when can-rename? + "teams-options-rename") + "teams-options-leave-team" + (when (get-in team [:permissions :is-owner]) + "teams-options-delete-team")]] [:div.sidebar-team-switch [:div.switch-content - [:div.current-team {:on-click #(reset! show-teams-ddwn? true)} + [:button.current-team {:tab-index "0" + :on-click #(reset! show-teams-ddwn? true) + :on-key-down (fn [event] + (when (or (kbd/space? event) (kbd/enter? event)) + (dom/prevent-default event) + (reset! show-teams-ddwn? true) + (ts/schedule-on-idle + (fn [] + (let [first-element (dom/get-element (first ids))] + (when first-element + (dom/focus! first-element)))))))} (if (:is-default team) [:div.team-name [:span.team-icon i/logo-icon] @@ -368,17 +495,33 @@ i/arrow-down]] (when-not (:is-default team) - [:div.switch-options {:on-click #(reset! show-team-opts-ddwn? true)} + [:button.switch-options {:on-click #(reset! show-team-opts-ddwn? true) + :tab-index "0" + :on-key-down (fn [event] + (when (or (kbd/space? event) (kbd/enter? event)) + (dom/prevent-default event) + (reset! show-team-opts-ddwn? true) + (ts/schedule-on-idle + (fn [] + (let [first-element (dom/get-element (first options-ids))] + (when first-element + (dom/focus! first-element)))))))} i/actions])] ;; Teams Dropdown - [:& dropdown {:show @show-teams-ddwn? - :on-close #(reset! show-teams-ddwn? false)} - [:& teams-selector-dropdown {:team team - :profile profile}]] + [:& dropdown-menu {:show @show-teams-ddwn? + :on-close #(reset! show-teams-ddwn? false) + :ids ids + :list-class "dropdown teams-dropdown"} + [:& teams-selector-dropdown-items {:ids ids + :team team + :profile profile + :teams teams}]] - [:& dropdown {:show @show-team-opts-ddwn? - :on-close #(reset! show-team-opts-ddwn? false)} + [:& dropdown-menu {:show @show-team-opts-ddwn? + :on-close #(reset! show-team-opts-ddwn? false) + :ids options-ids + :list-class "dropdown options-dropdown"} [:& team-options-dropdown {:team team :profile profile}]]])) @@ -400,11 +543,34 @@ (mf/deps team) #(st/emit! (rt/nav :dashboard-projects {:team-id (:id team)}))) + go-projects-with-key + (mf/use-callback + (mf/deps team) + #(st/emit! (rt/nav :dashboard-projects {:team-id (:id team)}) + (ts/schedule-on-idle + (fn [] + (let [projects-title (dom/get-element "dashboard-projects-title")] + (when projects-title + (dom/set-attribute! projects-title "tabindex" "0") + (dom/focus! projects-title) + (dom/set-attribute! projects-title "tabindex" "-1"))))))) + go-fonts (mf/use-callback (mf/deps team) #(st/emit! (rt/nav :dashboard-fonts {:team-id (:id team)}))) + go-fonts-with-key + (mf/use-callback + (mf/deps team) + #(st/emit! (rt/nav :dashboard-fonts {:team-id (:id team)}) + (ts/schedule-on-idle + (fn [] + (let [font-title (dom/get-element "dashboard-fonts-title")] + (when font-title + (dom/set-attribute! font-title "tabindex" "0") + (dom/focus! font-title) + (dom/set-attribute! font-title "tabindex" "-1"))))))) go-drafts (mf/use-callback (mf/deps team default-project-id) @@ -412,11 +578,36 @@ (st/emit! (rt/nav :dashboard-files {:team-id (:id team) :project-id default-project-id})))) + + go-drafts-with-key + (mf/use-callback + (mf/deps team default-project-id) + #(st/emit! (rt/nav :dashboard-files {:team-id (:id team) + :project-id default-project-id}) + (ts/schedule-on-idle + (fn [] + (let [drafts-title (dom/get-element "dashboard-drafts-title")] + (when drafts-title + (dom/set-attribute! drafts-title "tabindex" "0") + (dom/focus! drafts-title) + (dom/set-attribute! drafts-title "tabindex" "-1"))))))) + go-libs (mf/use-callback (mf/deps team) #(st/emit! (rt/nav :dashboard-libraries {:team-id (:id team)}))) + go-libs-with-key + (mf/use-callback + (mf/deps team) + #(st/emit! (rt/nav :dashboard-libraries {:team-id (:id team)}) + (ts/schedule-on-idle + (fn [] + (let [libs-title (dom/get-element "dashboard-libraries-title")] + (when libs-title + (dom/set-attribute! libs-title "tabindex" "0") + (dom/focus! libs-title) + (dom/set-attribute! libs-title "tabindex" "-1"))))))) pinned-projects (->> (vals projects) (remove :is-default) @@ -430,28 +621,32 @@ [:div.sidebar-content-section [:ul.sidebar-nav.no-overflow [:li.recent-projects - {:on-click go-projects - :class-name (when projects? "current")} - [:span.element-title (tr "labels.projects")]] + {:class-name (when projects? "current")} + [:& link {:action go-projects + :keyboard-action go-projects-with-key} + [:span.element-title (tr "labels.projects")]]] - [:li {:on-click go-drafts - :class-name (when drafts? "current")} - [:span.element-title (tr "labels.drafts")]] + [:li {:class-name (when drafts? "current")} + [:& link {:action go-drafts + :keyboard-action go-drafts-with-key} + [:span.element-title (tr "labels.drafts")]]] - [:li {:on-click go-libs - :class-name (when libs? "current")} - [:span.element-title (tr "labels.shared-libraries")]]]] + [:li {:class-name (when libs? "current")} + [:& link {:action go-libs + :keyboard-action go-libs-with-key} + [:span.element-title (tr "labels.shared-libraries")]]]]] [:hr] [:div.sidebar-content-section [:ul.sidebar-nav.no-overflow - [:li - {:on-click go-fonts - :data-test "fonts" - :class-name (when fonts? "current")} - [:span.element-title (tr "labels.fonts")]]]] + [:li {:class-name (when fonts? "current")} + + [:& link {:action go-fonts + :keyboard-action go-fonts-with-key + :data-test "fonts"} + [:span.element-title (tr "labels.fonts")]]]]] [:hr] [:div.sidebar-content-section {:data-test "pinned-projects"} @@ -492,42 +687,106 @@ (st/emit! (modal/show {:type :release-notes :version version}))))))] [:div.profile-section - [:div.profile {:on-click #(reset! show true) + [:div.profile {:tab-index "0" + :on-click #(reset! show true) + :on-key-down (fn [event] + (when (kbd/enter? event) + (reset! show true))) :data-test "profile-btn"} [:img {:src photo :alt (:fullname profile)}] [:span (:fullname profile)]] - [:& dropdown {:on-close #(reset! show false) - :show @show} + [:& dropdown-menu {:on-close #(reset! show false) + :show @show} [:ul.dropdown - [:li {:on-click (partial on-click :settings-profile) + [:li {:tab-index (if show + "0" + "-1") + :on-click (partial on-click :settings-profile) + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-click :settings-profile event))) :data-test "profile-profile-opt"} [:span.text (tr "labels.your-account")]] - [:li.separator {:on-click #(dom/open-new-window "https://help.penpot.app") + [:li.separator {:tab-index (if show + "0" + "-1") + :on-click #(dom/open-new-window "https://help.penpot.app") + :on-key-down (fn [event] + (when (kbd/enter? event) + (dom/open-new-window "https://help.penpot.app"))) :data-test "help-center-profile-opt"} [:span.text (tr "labels.help-center")]] - [:li {:on-click #(dom/open-new-window "https://community.penpot.app")} + [:li {:tab-index (if show + "0" + "-1") + :on-click #(dom/open-new-window "https://community.penpot.app") + :on-key-down (fn [event] + (when (kbd/enter? event) + (dom/open-new-window "https://community.penpot.app")))} [:span.text (tr "labels.community")]] - [:li {:on-click #(dom/open-new-window "https://www.youtube.com/c/Penpot")} + [:li {:tab-index (if show + "0" + "-1") + :on-click #(dom/open-new-window "https://www.youtube.com/c/Penpot") + :on-key-down (fn [event] + (when (kbd/enter? event) + (dom/open-new-window "https://www.youtube.com/c/Penpot")))} [:span.text (tr "labels.tutorials")]] - [:li {:on-click show-release-notes} + [:li {:tab-index (if show + "0" + "-1") + :on-click show-release-notes + :on-key-down (fn [event] + (when (kbd/enter? event) + (show-release-notes)))} [:span (tr "labels.release-notes")]] - [:li.separator {:on-click #(dom/open-new-window "https://penpot.app/libraries-templates.html") + [:li.separator {:tab-index (if show + "0" + "-1") + :on-click #(dom/open-new-window "https://penpot.app/libraries-templates.html") + :on-key-down (fn [event] + (when (kbd/enter? event) + (dom/open-new-window "https://penpot.app/libraries-templates.html"))) :data-test "libraries-templates-profile-opt"} [:span.text (tr "labels.libraries-and-templates")]] - [:li {:on-click #(dom/open-new-window "https://github.com/penpot/penpot")} + [:li {:tab-index (if show + "0" + "-1") + :on-click #(dom/open-new-window "https://github.com/penpot/penpot") + :on-key-down (fn [event] + (when (kbd/enter? event) + (dom/open-new-window "https://github.com/penpot/penpot")))} [:span (tr "labels.github-repo")]] - [:li {:on-click #(dom/open-new-window "https://penpot.app/terms.html")} + [:li {:tab-index (if show + "0" + "-1") + :on-click #(dom/open-new-window "https://penpot.app/terms.html") + :on-key-down (fn [event] + (when (kbd/enter? event) + (dom/open-new-window "https://penpot.app/terms.html")))} [:span (tr "auth.terms-of-service")]] (when (contains? @cf/flags :user-feedback) - [:li.separator {:on-click (partial on-click :settings-feedback) + [:li.separator {:tab-index (if show + "0" + "-1") + :on-click (partial on-click :settings-feedback) + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-click :settings-feedback event))) :data-test "feedback-profile-opt"} [:span.text (tr "labels.give-feedback")]]) - [:li.separator {:on-click #(on-click (du/logout) %) + [:li.separator {:tab-index (if show + "0" + "-1") + :on-click #(on-click (du/logout) %) + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-click (du/logout) event))) :data-test "logout-profile-opt"} [:span.icon i/exit] [:span.text (tr "labels.logout")]]]] @@ -542,9 +801,8 @@ [props] (let [team (obj/get props "team") profile (obj/get props "profile")] - [:div.dashboard-sidebar - [:div.sidebar-inside - [:> sidebar-content props] - [:& profile-section - {:profile profile - :team team}]]])) + [:nav.dashboard-sidebar + [:> sidebar-content props] + [:& profile-section + {:profile profile + :team team}]])) diff --git a/frontend/src/app/main/ui/settings/sidebar.cljs b/frontend/src/app/main/ui/settings/sidebar.cljs index 8a48644f6..5ea4316be 100644 --- a/frontend/src/app/main/ui/settings/sidebar.cljs +++ b/frontend/src/app/main/ui/settings/sidebar.cljs @@ -101,8 +101,7 @@ {::mf/wrap [mf/memo]} [{:keys [profile locale section]}] [:div.dashboard-sidebar.settings - [:div.sidebar-inside - [:& sidebar-content {:profile profile - :section section}] - [:& profile-section {:profile profile - :locale locale}]]]) + [:& sidebar-content {:profile profile + :section section}] + [:& profile-section {:profile profile + :locale locale}]]) diff --git a/frontend/src/app/util/keyboard.cljs b/frontend/src/app/util/keyboard.cljs index daed45f69..09d5dd72b 100644 --- a/frontend/src/app/util/keyboard.cljs +++ b/frontend/src/app/util/keyboard.cljs @@ -40,11 +40,15 @@ (def space? (is-key? " ")) (def up-arrow? (is-key? "ArrowUp")) (def down-arrow? (is-key? "ArrowDown")) +(def left-arrow? (is-key? "ArrowLeft")) +(def right-arrow? (is-key? "ArrowRight")) (def alt-key? (is-key? "Alt")) (def ctrl-key? (is-key? "Control")) (def meta-key? (is-key? "Meta")) (def comma? (is-key? ",")) (def backspace? (is-key? "Backspace")) +(def home? (is-key? "Home")) +(def tab? (is-key? "Tab")) (defn editing? [e] (.-editing ^js e))