♻️ Replace layer tabs component with the new tab switcher component

This commit is contained in:
Eva Marco 2024-08-20 16:53:56 +02:00
parent 1782837a38
commit 3df9c88bb7
6 changed files with 236 additions and 99 deletions

View file

@ -9,8 +9,8 @@
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.main.style :as stl]) [app.main.style :as stl])
(:require (:require
[app.common.data :as d] [app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]]
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list] :as i] [app.util.array :as array]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[app.util.object :as obj] [app.util.object :as obj]
@ -19,45 +19,48 @@
(mf/defc tab* (mf/defc tab*
{::mf/props :obj {::mf/props :obj
::mf/private true} ::mf/private true}
[{:keys [selected icon label aria-label id tab-ref] :rest props}] [{:keys [selected icon label aria-label id on-ref] :rest props}]
(let [class (stl/css-case :tab true (let [class (stl/css-case :tab true
:selected selected) :selected selected)
props (mf/spread-props props {:class class props (mf/spread-props props
:role "tab" {:class class
:aria-selected selected :role "tab"
:title (or label aria-label) :aria-selected selected
:tab-index (if selected nil -1) :title (or label aria-label)
:ref tab-ref :tab-index (if selected nil -1)
:data-id id})] :ref (fn [node]
(on-ref node id))
:data-id id})]
[:> "li" {} [:li
[:> "button" props [:> :button props
(when icon (when (some? icon)
[:> icon* [:> icon*
{:id icon {:id icon
:aria-hidden (when label true) :aria-hidden (when label true)
:aria-label (when (not label) aria-label)}]) :aria-label (when (not label) aria-label)}])
(when label (when (string? label)
[:span {:class (stl/css-case :tab-text true [:span {:class (stl/css-case :tab-text true
:tab-text-and-icon icon)} label])]])) :tab-text-and-icon icon)}
label])]]))
(mf/defc tab-nav* (mf/defc tab-nav*
{::mf/props :obj {::mf/props :obj
::mf/private true} ::mf/private true}
[{:keys [tabs-refs tabs selected on-click button-position action-button] :rest props}] [{:keys [on-ref tabs selected on-click button-position action-button] :rest props}]
(let [class (stl/css-case :tab-nav true (let [class (stl/css-case :tab-nav true
:tab-nav-start (= "start" button-position) :tab-nav-start (= "start" button-position)
:tab-nav-end (= "end" button-position)) :tab-nav-end (= "end" button-position))
props (mf/spread-props props {:class (stl/css :tab-list) props (mf/spread-props props
:role "tablist" {:class (stl/css :tab-list)
:aria-orientation "horizontal"})] :role "tablist"
[:> "nav" {:class class} :aria-orientation "horizontal"})]
[:nav {:class class}
(when (= button-position "start") (when (= button-position "start")
action-button) action-button)
[:> "ul" props [:> "ul" props
(for [[index element] (map-indexed vector tabs)] (for [element ^js tabs]
(let [icon (obj/get element "icon") (let [icon (obj/get element "icon")
label (obj/get element "label") label (obj/get element "label")
aria-label (obj/get element "aria-label") aria-label (obj/get element "aria-label")
@ -67,24 +70,14 @@
:key (dm/str "tab-" id) :key (dm/str "tab-" id)
:label label :label label
:aria-label aria-label :aria-label aria-label
:selected (= index selected) :selected (= id selected)
:on-click on-click :on-click on-click
:id id :on-ref on-ref
:tab-ref (nth tabs-refs index)}]))] :id id}]))]
(when (= button-position "end") (when (= button-position "end")
action-button)])) action-button)]))
(mf/defc tab-panel*
{::mf/props :obj
::mf/private true}
[{:keys [children name] :rest props}]
(let [props (mf/spread-props props {:class (stl/css :tab-panel)
:aria-labelledby name
:role "tabpanel"})]
[:> "section" props
children]))
(defn- valid-tabs? (defn- valid-tabs?
[tabs] [tabs]
(every? (fn [tab] (every? (fn [tab]
@ -96,10 +89,24 @@
(not (and aria-label (or (nil? icon) label)))))) (not (and aria-label (or (nil? icon) label))))))
(seq tabs))) (seq tabs)))
(def ^:private positions (set '("start" "end"))) (def ^:private positions
#{"start" "end"})
(defn- valid-button-position? [position button] (defn- valid-button-position?
(or (nil? position) (and (contains? positions position) (some? button)))) [position button]
(or (nil? position)
(and (contains? positions position)
(some? button))))
(defn- get-tab
[tabs id]
(or (array/find #(= id (obj/get % "id")) tabs)
(aget tabs 0)))
(defn- get-selected-tab-id
[tabs default]
(let [tab (get-tab tabs default)]
(obj/get tab "id")))
(mf/defc tab-switcher* (mf/defc tab-switcher*
{::mf/props :obj} {::mf/props :obj}
@ -107,54 +114,83 @@
;; TODO: Use a schema to assert the tabs prop -> https://tree.taiga.io/project/penpot/task/8521 ;; TODO: Use a schema to assert the tabs prop -> https://tree.taiga.io/project/penpot/task/8521
(assert (valid-tabs? tabs) "unexpected props for tab-switcher") (assert (valid-tabs? tabs) "unexpected props for tab-switcher")
(assert (valid-button-position? action-button-position action-button) "invalid action-button-position") (assert (valid-button-position? action-button-position action-button) "invalid action-button-position")
(let [tab-ids (mapv #(obj/get % "id") tabs)
active-tab-index* (mf/use-state (or (d/index-of tab-ids default-selected) 0)) (let [selected* (mf/use-state #(get-selected-tab-id tabs default-selected))
active-tab-index (deref active-tab-index*) selected (deref selected*)
tabs-refs (mapv (fn [_] (mf/use-ref)) tabs) tabs-nodes-refs (mf/use-ref nil)
tabs-ref (mf/use-ref nil)
active-tab (nth tabs active-tab-index) on-click
panel-content (obj/get active-tab "content")
handle-click
(mf/use-fn (mf/use-fn
(mf/deps on-change-tab tab-ids) (mf/deps on-change-tab)
(fn [event] (fn [event]
(let [id (dom/get-data (dom/get-current-target event) "id") (let [node (dom/get-current-target event)
index (d/index-of tab-ids id)] id (dom/get-data node "id")]
(reset! active-tab-index* index) (reset! selected* id)
(when (fn? on-change-tab) (when (fn? on-change-tab)
(on-change-tab id))))) (on-change-tab id)))))
on-ref
(mf/use-fn
(fn [node id]
(let [refs (or (mf/ref-val tabs-nodes-refs) #js {})
refs (if node
(obj/set! refs id node)
(obj/unset! refs id))]
(mf/set-ref-val! tabs-nodes-refs refs))))
on-key-down on-key-down
(mf/use-fn (mf/use-fn
(mf/deps tabs-refs active-tab-index) (mf/deps selected)
(fn [event] (fn [event]
(let [len (count tabs-refs) (let [tabs (mf/ref-val tabs-ref)
index (cond len (alength tabs)
(kbd/home? event) 0 sel? #(= selected (obj/get % "id"))
(kbd/left-arrow? event) (mod (- active-tab-index 1) len) id (cond
(kbd/right-arrow? event) (mod (+ active-tab-index 1) len))] (kbd/home? event)
(when index (let [tab (aget tabs 0)]
(reset! active-tab-index* index) (obj/get tab "id"))
(dom/focus! (mf/ref-val (nth tabs-refs index)))))))
(kbd/left-arrow? event)
(let [index (array/find-index sel? tabs)
index (mod (- index 1) len)
tab (aget tabs index)]
(obj/get tab "id"))
(kbd/right-arrow? event)
(let [index (array/find-index sel? tabs)
index (mod (+ index 1) len)
tab (aget tabs index)]
(obj/get tab "id")))]
(when (some? id)
(reset! selected* id)
(let [nodes (mf/ref-val tabs-nodes-refs)
node (obj/get nodes id)]
(dom/focus! node))))))
class (dm/str class " " (stl/css :tabs)) class (dm/str class " " (stl/css :tabs))
props (mf/spread-props props {:class class props (mf/spread-props props {:class class
:on-key-down on-key-down})] :on-key-down on-key-down})]
[:> "article" props (mf/with-effect [tabs]
[:> "div" {:class (stl/css :padding-wrapper)} (mf/set-ref-val! tabs-ref tabs))
[:> :article props
[:div {:class (stl/css :padding-wrapper)}
[:> tab-nav* {:button-position action-button-position [:> tab-nav* {:button-position action-button-position
:action-button action-button :action-button action-button
:tabs tabs :tabs tabs
:selected active-tab-index :on-ref on-ref
:on-click handle-click :selected selected
:tabs-refs tabs-refs}]] :on-click on-click}]]
[:> tab-panel* {:tab-index 0}
panel-content]]))
[:section {:class (stl/css :tab-panel)
:tab-index 0
:role "tabpanel"}
(let [active-tab (get-tab tabs selected)]
(obj/get active-tab "content"))]]))

View file

@ -11,8 +11,9 @@
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
[app.main.ui.context :as muc] [app.main.ui.context :as muc]
[app.main.ui.ds.foundations.assets.icon :refer [icon*]]
[app.main.ui.ds.tab-switcher :refer [tab-switcher*]]
[app.main.ui.hooks.resize :refer [use-resize-hook]] [app.main.ui.hooks.resize :refer [use-resize-hook]]
[app.main.ui.workspace.comments :refer [comments-sidebar]] [app.main.ui.workspace.comments :refer [comments-sidebar]]
[app.main.ui.workspace.left-header :refer [left-header]] [app.main.ui.workspace.left-header :refer [left-header]]
@ -31,6 +32,17 @@
;; --- Left Sidebar (Component) ;; --- Left Sidebar (Component)
(mf/defc collapse-button
{::mf/wrap [mf/memo]
::mf/wrap-props false}
[{:keys [on-click] :as props}]
;; NOTE: This custom button may be replace by an action button when this variant is designed
[:button {:class (stl/css :collapse-sidebar-button)
:on-click on-click}
[:& icon* {:id "arrow"
:size "s"
:aria-label (tr "workspace.sidebar.collapse")}]])
(mf/defc left-sidebar (mf/defc left-sidebar
{::mf/wrap [mf/memo] {::mf/wrap [mf/memo]
::mf/wrap-props false} ::mf/wrap-props false}
@ -60,7 +72,49 @@
(mf/use-fn #(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar))) (mf/use-fn #(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)))
on-tab-change on-tab-change
(mf/use-fn #(st/emit! (dw/go-to-layout %)))] (mf/use-fn #(st/emit! (dw/go-to-layout (keyword %))))
tabs (if ^boolean mode-inspect?
#js [#js {:label (tr "workspace.sidebar.layers")
:id "layers"
:content (mf/html [:article {:class (stl/css :layers-tab)
:style #js {"--height" (str size-pages "px")}}
[:& sitemap {:layout layout
:toggle-pages toggle-pages
:show-pages? @show-pages?
:size size-pages}]
(when @show-pages?
[:div {:class (stl/css :resize-area-horiz)
:on-pointer-down on-pointer-down-pages
:on-lost-pointer-capture on-lost-pointer-capture-pages
:on-pointer-move on-pointer-move-pages}])
[:& layers-toolbox {:size-parent size
:size size-pages}]])}]
#js [#js {:label (tr "workspace.sidebar.layers")
:id "layers"
:content (mf/html [:article {:class (stl/css :layers-tab)
:style #js {"--height" (str size-pages "px")}}
[:& sitemap {:layout layout
:toggle-pages toggle-pages
:show-pages? @show-pages?
:size size-pages}]
(when @show-pages?
[:div {:class (stl/css :resize-area-horiz)
:on-pointer-down on-pointer-down-pages
:on-lost-pointer-capture on-lost-pointer-capture-pages
:on-pointer-move on-pointer-move-pages}])
[:& layers-toolbox {:size-parent size
:size size-pages}]])}
#js {:label (tr "workspace.toolbar.assets")
:id "assets"
:content (mf/html [:& assets-toolbox {:size (- size 58)}])}])]
[:& (mf/provider muc/sidebar) {:value :left} [:& (mf/provider muc/sidebar) {:value :left}
[:aside {:ref parent-ref [:aside {:ref parent-ref
@ -89,36 +143,43 @@
:else :else
[:div {:class (stl/css :settings-bar-content)} [:div {:class (stl/css :settings-bar-content)}
[:& tab-container [:> tab-switcher* {:tabs tabs
{:on-change-tab on-tab-change :default-selected (dm/str section)
:selected section :on-change-tab on-tab-change
:collapsable true :class (stl/css :left-sidebar-tabs)
:handle-collapse handle-collapse :action-button-position "start"
:header-class (stl/css :tab-spacing)} :action-button (mf/html [:& collapse-button {:on-click handle-collapse}])}]
[:& tab-element {:id :layers #_[:& tab-container
:title (tr "workspace.sidebar.layers")} {:on-change-tab on-tab-change
[:article {:class (stl/css :layers-tab) :selected section
:style #js {"--height" (str size-pages "px")}} :collapsable true
:handle-collapse handle-collapse
:header-class (stl/css :tab-spacing)}
[:& sitemap {:layout layout [:& tab-element {:id :layers
:toggle-pages toggle-pages :title (tr "workspace.sidebar.layers")}
:show-pages? @show-pages? [:article {:class (stl/css :layers-tab)
:size size-pages}] :style #js {"--height" (str size-pages "px")}}
(when @show-pages? [:& sitemap {:layout layout
[:div {:class (stl/css :resize-area-horiz) :toggle-pages toggle-pages
:on-pointer-down on-pointer-down-pages :show-pages? @show-pages?
:on-lost-pointer-capture on-lost-pointer-capture-pages :size size-pages}]
:on-pointer-move on-pointer-move-pages}])
[:& layers-toolbox {:size-parent size (when @show-pages?
:size size-pages}]]] [:div {:class (stl/css :resize-area-horiz)
:on-pointer-down on-pointer-down-pages
:on-lost-pointer-capture on-lost-pointer-capture-pages
:on-pointer-move on-pointer-move-pages}])
(when-not ^boolean mode-inspect? [:& layers-toolbox {:size-parent size
[:& tab-element {:id :assets :size size-pages}]]]
:title (tr "workspace.toolbar.assets")}
[:& assets-toolbox {:size (- size 58)}]])]])]])) (when-not ^boolean mode-inspect?
[:& tab-element {:id :assets
:title (tr "workspace.toolbar.assets")}
[:& assets-toolbox {:size (- size 58)}]])]])]]))
;; --- Right Sidebar (Component) ;; --- Right Sidebar (Component)

View file

@ -89,3 +89,22 @@ $width-settings-bar-max: $s-500;
border-bottom: $s-2 solid var(--resize-area-border-color); border-bottom: $s-2 solid var(--resize-area-border-color);
cursor: ns-resize; cursor: ns-resize;
} }
.left-sidebar-tabs {
--tabs-nav-padding-inline-start: var(--sp-m);
--tabs-nav-padding-inline-end: var(--sp-m);
}
.collapse-sidebar-button {
--collapse-icon-color: var(--color-foreground-secondary);
@include flexCenter;
@include buttonStyle;
height: 100%;
width: $s-24;
border-radius: $br-5;
color: var(--collapse-icon-color);
transform: rotate(180deg);
&:hover {
--collapse-icon-color: var(--color-foreground-primary);
}
}

View file

@ -9,7 +9,7 @@
(:require (:require
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.icons :as i] [app.main.ui.ds.foundations.assets.icon :refer [icon*]]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
@ -22,6 +22,8 @@
:class (stl/css :collapsed-sidebar)} :class (stl/css :collapsed-sidebar)}
[:div {:class (stl/css :collapsed-title)} [:div {:class (stl/css :collapsed-title)}
[:button {:class (stl/css :collapsed-button) [:button {:class (stl/css :collapsed-button)
:on-click on-click :title (tr "workspace.sidebar.expand")
:aria-label (tr "workspace.sidebar.expand")} :on-click on-click}
i/arrow]]])) [:& icon* {:id "arrow"
:size "s"
:aria-label (tr "workspace.sidebar.expand")}]]]]))

View file

@ -14,10 +14,11 @@
padding: $s-4; padding: $s-4;
border-radius: $br-8; border-radius: $br-8;
background: var(--color-background-primary); background: var(--color-background-primary);
margin-inline-start: var(--sp-m);
} }
.collapsed-title { .collapsed-title {
@include flexCenter; @include flexCenter;
height: $s-32; height: $s-36;
width: $s-24; width: $s-24;
border-radius: $br-8; border-radius: $br-8;
background: var(--color-background-secondary); background: var(--color-background-secondary);

View file

@ -6,7 +6,9 @@
(ns app.util.array (ns app.util.array
"A collection of helpers for work with javascript arrays." "A collection of helpers for work with javascript arrays."
(:refer-clojure :exclude [conj! conj filter])) (:refer-clojure :exclude [conj! conj filter map reduce find])
(:require
[cljs.core :as c]))
(defn conj (defn conj
"A conj like function for js arrays." "A conj like function for js arrays."
@ -49,3 +51,19 @@
"A specific filter for js arrays." "A specific filter for js arrays."
[pred ^js/Array o] [pred ^js/Array o]
(.filter o pred)) (.filter o pred))
(defn map
[f a]
(.map ^js/Array a f))
(defn reduce
[f init val]
(.reduce ^js/Array val f init))
(defn find-index
[f v]
(.findIndex ^js/Array v f))
(defn find
[f v]
(.find ^js/Array v f))