From 3df9c88bb7bc6487d47a289199582f1f928128f8 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 20 Aug 2024 16:53:56 +0200 Subject: [PATCH] :recycle: Replace layer tabs component with the new tab switcher component --- frontend/src/app/main/ui/ds/tab_switcher.cljs | 168 +++++++++++------- .../src/app/main/ui/workspace/sidebar.cljs | 115 +++++++++--- .../src/app/main/ui/workspace/sidebar.scss | 19 ++ .../workspace/sidebar/collapsable_button.cljs | 10 +- .../workspace/sidebar/collapsable_button.scss | 3 +- frontend/src/app/util/array.cljs | 20 ++- 6 files changed, 236 insertions(+), 99 deletions(-) diff --git a/frontend/src/app/main/ui/ds/tab_switcher.cljs b/frontend/src/app/main/ui/ds/tab_switcher.cljs index 5a5d5218a..1235514f9 100644 --- a/frontend/src/app/main/ui/ds/tab_switcher.cljs +++ b/frontend/src/app/main/ui/ds/tab_switcher.cljs @@ -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"))]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar.cljs b/frontend/src/app/main/ui/workspace/sidebar.cljs index 11a189d4b..7b5b876e0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar.cljs @@ -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) diff --git a/frontend/src/app/main/ui/workspace/sidebar.scss b/frontend/src/app/main/ui/workspace/sidebar.scss index e73dc621a..af53021b1 100644 --- a/frontend/src/app/main/ui/workspace/sidebar.scss +++ b/frontend/src/app/main/ui/workspace/sidebar.scss @@ -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); + } +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.cljs b/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.cljs index 31c9b0af5..11adff3db 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.cljs @@ -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")}]]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.scss b/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.scss index 072a072a5..3f62cd24b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/collapsable_button.scss @@ -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); diff --git a/frontend/src/app/util/array.cljs b/frontend/src/app/util/array.cljs index 012b62c0a..1e56d99b3 100644 --- a/frontend/src/app/util/array.cljs +++ b/frontend/src/app/util/array.cljs @@ -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))