mirror of
https://github.com/penpot/penpot.git
synced 2025-05-29 06:56:10 +02:00
200 lines
6.9 KiB
Clojure
200 lines
6.9 KiB
Clojure
;; 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.ds.tab-switcher
|
|
(:require-macros
|
|
[app.common.data.macros :as dm]
|
|
[app.main.style :as stl])
|
|
(:require
|
|
[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]
|
|
[rumext.v2 :as mf]))
|
|
|
|
(mf/defc tab*
|
|
{::mf/props :obj
|
|
::mf/private true}
|
|
[{: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 (fn [node]
|
|
(on-ref node id))
|
|
:data-id id
|
|
;; This prop is to be used for accessibility purposes only.
|
|
:id id})]
|
|
|
|
[:li
|
|
[:> :button props
|
|
(when (some? icon)
|
|
[:> icon*
|
|
{:id icon
|
|
:aria-hidden (when label true)
|
|
:aria-label (when (not label) aria-label)}])
|
|
(when (string? label)
|
|
[:span {:class (stl/css-case :tab-text true
|
|
:tab-text-and-icon icon)}
|
|
label])]]))
|
|
|
|
(mf/defc tab-nav*
|
|
{::mf/props :obj
|
|
::mf/private true}
|
|
[{: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}
|
|
(when (= button-position "start")
|
|
action-button)
|
|
|
|
[:> "ul" props
|
|
(for [element ^js tabs]
|
|
(let [icon (obj/get element "icon")
|
|
label (obj/get element "label")
|
|
aria-label (obj/get element "aria-label")
|
|
id (obj/get element "id")]
|
|
|
|
[:> tab* {:icon icon
|
|
:key (dm/str "tab-" id)
|
|
:label label
|
|
:aria-label aria-label
|
|
:selected (= id selected)
|
|
:on-click on-click
|
|
:on-ref on-ref
|
|
:id id}]))]
|
|
|
|
(when (= button-position "end")
|
|
action-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")))
|
|
|
|
(def ^:private schema:tab
|
|
[:and
|
|
[:map {:title "tab"}
|
|
[:icon {:optional true}
|
|
[:and :string [:fn #(contains? icon-list %)]]]
|
|
[:label {:optional true} :string]
|
|
[:aria-label {:optional true} :string]
|
|
[:content some?]]
|
|
[:fn {:error/message "invalid data: missing required props"}
|
|
(fn [tab]
|
|
(or (and (contains? tab :icon)
|
|
(or (contains? tab :label)
|
|
(contains? tab :aria-label)))
|
|
(contains? tab :label)))]])
|
|
|
|
(def ^:private schema:tab-switcher
|
|
[:map
|
|
[:class {:optional true} :string]
|
|
[:action-button-position {:optional true}
|
|
[:enum "start" "end"]]
|
|
[:default-selected {:optional true} :string]
|
|
[:tabs [:vector {:min 1} schema:tab]]])
|
|
|
|
(mf/defc tab-switcher*
|
|
{::mf/props :obj
|
|
::mf/schema schema:tab-switcher}
|
|
[{:keys [class tabs on-change-tab default-selected selected action-button-position action-button] :rest props}]
|
|
(let [selected* (mf/use-state #(or selected (get-selected-tab-id tabs default-selected)))
|
|
selected (or selected (deref selected*))
|
|
|
|
tabs-nodes-refs (mf/use-ref nil)
|
|
tabs-ref (mf/use-ref nil)
|
|
|
|
on-click
|
|
(mf/use-fn
|
|
(mf/deps on-change-tab)
|
|
(fn [event]
|
|
(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 selected)
|
|
(fn [event]
|
|
(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})]
|
|
|
|
(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
|
|
:on-ref on-ref
|
|
:selected selected
|
|
:on-key-down on-key-down
|
|
:on-click on-click}]]
|
|
|
|
(let [active-tab (get-tab tabs selected)
|
|
content (obj/get active-tab "content")
|
|
id (obj/get active-tab "id")]
|
|
[:section {:class (stl/css :tab-panel)
|
|
:tab-index 0
|
|
:role "tabpanel"
|
|
:aria-labelledby id}
|
|
content])]))
|