Repository: penpot/penpot
File: frontend/src/app/main/ui/ds/controls/switch.cljs (ClojureScript)

Code before:

(ns app.main.ui.ds.controls.switch
  (:require-macros
   [app.main.style :as stl])
  (:require
   [app.common.data :as d]
   [app.util.dom :as dom]
   [app.util.keyboard :as kbd]
   [cuerdas.core :as str]
   [rumext.v2 :as mf]))

(def ^:private schema:switch
  [:map
   [:id {:optional true} :string]
   [:class {:optional true} :string]
   [:label {:optional true} [:maybe :string]]
   [:aria-label {:optional true} [:maybe :string]]
   [:default-checked {:optional true} :boolean]
   [:on-change {:optional true} [:maybe fn?]]
   [:disabled {:optional true} :boolean]])

(mf/defc switch*
  {::mf/schema schema:switch}
  [{:keys [id class label aria-label default-checked on-change disabled] :rest props} ref]
  (let [ref         (or ref (mf/use-ref))
        id          (or id (mf/use-id))

        is-checked* (mf/use-state (d/nilv default-checked false))
        is-checked  (deref is-checked*)

        disabled    (d/nilv disabled false)

        has-label   (not (str/blank? label))

        handle-toggle
        (mf/use-fn
         (mf/deps on-change is-checked* disabled)
         (fn []
           (when-not disabled
             (let [new-checked (not is-checked)]
               (reset! is-checked* new-checked)
               (when on-change
                 (on-change new-checked))))))

        handle-keydown
        (mf/use-fn
         (mf/deps handle-toggle)
         (fn [event]
           (when (or (kbd/space? event) (kbd/enter? event))
             (dom/prevent-default event)
             (handle-toggle event))))

Review comment (refactor):
React hooks can't be under conditionals, I suggest split this in two steps. Example: 

<img width="500" height="77" alt="Image" src="https://github.com/user-attachments/assets/edbb025c-dfd4-438a-abdc-f229be51d9db" />

Code after:

(ns app.main.ui.ds.controls.switch
  (:require-macros
   [app.main.style :as stl])
  (:require
   [app.common.data :as d]
   [app.util.dom :as dom]
   [app.util.keyboard :as kbd]
   [cuerdas.core :as str]
   [rumext.v2 :as mf]))

(def ^:private schema:switch
  [:map
   [:id {:optional true} :string]
   [:class {:optional true} :string]
   [:label {:optional true} [:maybe :string]]
   [:aria-label {:optional true} [:maybe :string]]
   [:default-checked {:optional true} [:maybe :boolean]]
   [:on-change {:optional true} [:maybe fn?]]
   [:disabled {:optional true} :boolean]])

(mf/defc switch*
  {::mf/schema schema:switch}
  [{:keys [id class label aria-label default-checked on-change disabled] :rest props} ref]
  (let [checked*   (mf/use-state default-checked)
        checked?   (deref checked*)

        disabled?  (d/nilv disabled false)

        has-label? (not (str/blank? label))

        handle-toggle
        (mf/use-fn
         (mf/deps on-change checked? disabled?)
         #(when-not disabled?
            (let [updated-checked? (not checked?)]
              (reset! checked* updated-checked?)
              (when on-change
                (on-change updated-checked?)))))

        handle-keydown
        (mf/use-fn
         (mf/deps handle-toggle)
         (fn [event]
           (dom/prevent-default event)
           (when-not disabled?
             (when (or (kbd/space? event) (kbd/enter? event))
               (handle-toggle event)))))

        props
        (mf/spread-props props {:ref ref