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:
parent
1782837a38
commit
3df9c88bb7
6 changed files with 236 additions and 99 deletions
|
@ -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"))]]))
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")}]]]]))
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Add table
Reference in a new issue