0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-28 15:41:25 -05:00

♻️ 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.main.style :as stl])
(:require
[app.common.data :as d]
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list] :as i]
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]]
[app.util.array :as array]
[app.util.dom :as dom]
[app.util.keyboard :as kbd]
[app.util.object :as obj]
@ -19,45 +19,48 @@
(mf/defc tab*
{::mf/props :obj
::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
:selected selected)
props (mf/spread-props props {:class class
:role "tab"
:aria-selected selected
:title (or label aria-label)
:tab-index (if selected nil -1)
:ref tab-ref
:data-id id})]
props (mf/spread-props props
{:class class
:role "tab"
:aria-selected selected
:title (or label aria-label)
:tab-index (if selected nil -1)
:ref (fn [node]
(on-ref node id))
:data-id id})]
[:> "li" {}
[:> "button" props
(when icon
[:li
[:> :button props
(when (some? icon)
[:> icon*
{:id icon
:aria-hidden (when label true)
:aria-label (when (not label) aria-label)}])
(when label
(when (string? label)
[: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/props :obj
::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
:tab-nav-start (= "start" button-position)
:tab-nav-end (= "end" button-position))
props (mf/spread-props props {:class (stl/css :tab-list)
:role "tablist"
:aria-orientation "horizontal"})]
[:> "nav" {:class class}
props (mf/spread-props props
{:class (stl/css :tab-list)
:role "tablist"
:aria-orientation "horizontal"})]
[:nav {:class class}
(when (= button-position "start")
action-button)
[:> "ul" props
(for [[index element] (map-indexed vector tabs)]
(for [element ^js tabs]
(let [icon (obj/get element "icon")
label (obj/get element "label")
aria-label (obj/get element "aria-label")
@ -67,24 +70,14 @@
:key (dm/str "tab-" id)
:label label
:aria-label aria-label
:selected (= index selected)
:selected (= id selected)
:on-click on-click
:id id
:tab-ref (nth tabs-refs index)}]))]
:on-ref on-ref
:id id}]))]
(when (= button-position "end")
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?
[tabs]
(every? (fn [tab]
@ -96,10 +89,24 @@
(not (and aria-label (or (nil? icon) label))))))
(seq tabs)))
(def ^:private positions (set '("start" "end")))
(def ^:private positions
#{"start" "end"})
(defn- valid-button-position? [position button]
(or (nil? position) (and (contains? positions position) (some? button))))
(defn- valid-button-position?
[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/props :obj}
@ -107,54 +114,83 @@
;; 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-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))
active-tab-index (deref active-tab-index*)
(let [selected* (mf/use-state #(get-selected-tab-id tabs default-selected))
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)
panel-content (obj/get active-tab "content")
handle-click
on-click
(mf/use-fn
(mf/deps on-change-tab tab-ids)
(mf/deps on-change-tab)
(fn [event]
(let [id (dom/get-data (dom/get-current-target event) "id")
index (d/index-of tab-ids id)]
(reset! active-tab-index* index)
(let [node (dom/get-current-target event)
id (dom/get-data node "id")]
(reset! selected* id)
(when (fn? on-change-tab)
(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
(mf/use-fn
(mf/deps tabs-refs active-tab-index)
(mf/deps selected)
(fn [event]
(let [len (count tabs-refs)
index (cond
(kbd/home? event) 0
(kbd/left-arrow? event) (mod (- active-tab-index 1) len)
(kbd/right-arrow? event) (mod (+ active-tab-index 1) len))]
(when index
(reset! active-tab-index* index)
(dom/focus! (mf/ref-val (nth tabs-refs index)))))))
(let [tabs (mf/ref-val tabs-ref)
len (alength tabs)
sel? #(= selected (obj/get % "id"))
id (cond
(kbd/home? event)
(let [tab (aget tabs 0)]
(obj/get tab "id"))
(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))
props (mf/spread-props props {:class class
:on-key-down on-key-down})]
[:> "article" props
[:> "div" {:class (stl/css :padding-wrapper)}
(mf/with-effect [tabs]
(mf/set-ref-val! tabs-ref tabs))
[:> :article props
[:div {:class (stl/css :padding-wrapper)}
[:> tab-nav* {:button-position action-button-position
:action-button action-button
:tabs tabs
:selected active-tab-index
:on-click handle-click
:tabs-refs tabs-refs}]]
:on-ref on-ref
:selected selected
: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.refs :as refs]
[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.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.workspace.comments :refer [comments-sidebar]]
[app.main.ui.workspace.left-header :refer [left-header]]
@ -31,6 +32,17 @@
;; --- 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/wrap [mf/memo]
::mf/wrap-props false}
@ -60,7 +72,49 @@
(mf/use-fn #(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)))
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}
[:aside {:ref parent-ref
@ -89,36 +143,43 @@
:else
[:div {:class (stl/css :settings-bar-content)}
[:& tab-container
{:on-change-tab on-tab-change
:selected section
:collapsable true
:handle-collapse handle-collapse
:header-class (stl/css :tab-spacing)}
[:> tab-switcher* {:tabs tabs
:default-selected (dm/str section)
:on-change-tab on-tab-change
:class (stl/css :left-sidebar-tabs)
:action-button-position "start"
:action-button (mf/html [:& collapse-button {:on-click handle-collapse}])}]
[:& tab-element {:id :layers
:title (tr "workspace.sidebar.layers")}
[:article {:class (stl/css :layers-tab)
:style #js {"--height" (str size-pages "px")}}
#_[:& tab-container
{:on-change-tab on-tab-change
:selected section
:collapsable true
:handle-collapse handle-collapse
:header-class (stl/css :tab-spacing)}
[:& sitemap {:layout layout
:toggle-pages toggle-pages
:show-pages? @show-pages?
:size size-pages}]
[:& tab-element {:id :layers
:title (tr "workspace.sidebar.layers")}
[:article {:class (stl/css :layers-tab)
:style #js {"--height" (str size-pages "px")}}
(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}])
[:& sitemap {:layout layout
:toggle-pages toggle-pages
:show-pages? @show-pages?
:size size-pages}]
[:& layers-toolbox {:size-parent size
: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}])
(when-not ^boolean mode-inspect?
[:& tab-element {:id :assets
:title (tr "workspace.toolbar.assets")}
[:& assets-toolbox {:size (- size 58)}]])]])]]))
[:& layers-toolbox {:size-parent size
:size size-pages}]]]
(when-not ^boolean mode-inspect?
[:& tab-element {:id :assets
:title (tr "workspace.toolbar.assets")}
[:& assets-toolbox {:size (- size 58)}]])]])]]))
;; --- 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);
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
[app.main.data.workspace :as dw]
[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]]
[rumext.v2 :as mf]))
@ -22,6 +22,8 @@
:class (stl/css :collapsed-sidebar)}
[:div {:class (stl/css :collapsed-title)}
[:button {:class (stl/css :collapsed-button)
:on-click on-click
:aria-label (tr "workspace.sidebar.expand")}
i/arrow]]]))
:title (tr "workspace.sidebar.expand")
:on-click on-click}
[:& icon* {:id "arrow"
:size "s"
:aria-label (tr "workspace.sidebar.expand")}]]]]))

View file

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

View file

@ -6,7 +6,9 @@
(ns app.util.array
"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
"A conj like function for js arrays."
@ -49,3 +51,19 @@
"A specific filter for js arrays."
[pred ^js/Array o]
(.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))