mirror of
https://github.com/penpot/penpot.git
synced 2025-06-13 02:11:42 +02: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
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue