diff --git a/frontend/src/app/main/ui/ds/controls/switcher.cljs b/frontend/src/app/main/ui/ds/controls/switcher.cljs new file mode 100644 index 0000000000..1efd0b98b2 --- /dev/null +++ b/frontend/src/app/main/ui/ds/controls/switcher.cljs @@ -0,0 +1,14 @@ +;; 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.controls.switcher + (:require + [app.common.data.macros :as dm] + [app.main.ui.ds.controls.switcher.switcher :as impl])) + +(dm/export impl/switcher*) + + diff --git a/frontend/src/app/main/ui/ds/controls/switcher/switcher.cljs b/frontend/src/app/main/ui/ds/controls/switcher/switcher.cljs index d009b0d422..fe2e0bf304 100644 --- a/frontend/src/app/main/ui/ds/controls/switcher/switcher.cljs +++ b/frontend/src/app/main/ui/ds/controls/switcher/switcher.cljs @@ -8,7 +8,9 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] - [app.common.data.macros :as dm] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [app.util.keyboard :as kbd] [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -22,85 +24,88 @@ [:disabled {:optional true} :boolean] [:size {:optional true} [:enum "sm" "md" "lg"]] [:aria-label {:optional true} [:maybe :string]] - [:class {:optional true} :string] - [:data-testid {:optional true} :string]]) + [:class {:optional true} :string]]) (mf/defc switcher* {::mf/forward-ref true ::mf/schema schema:switcher} [{:keys [id label checked default-checked on-change disabled size aria-label class] :rest props} ref] (let [id (or id (mf/use-id)) - size (keyword (d/nilv size "md")) + size (d/nilv size "md") disabled (d/nilv disabled false) - + + ;; TODO: review which one is better ;; Internal state for uncontrolled mode internal-checked* (mf/use-state (d/nilv default-checked false)) internal-checked (deref internal-checked*) - + ;; Determine if controlled or uncontrolled controlled? (some? checked) current-checked (if controlled? checked internal-checked) - + ;; Toggle handler - handle-toggle (mf/use-fn - (mf/deps controlled? current-checked on-change internal-checked*) - (fn [event] - (when-not disabled - (let [new-checked (not current-checked)] - (when-not controlled? - (reset! internal-checked* new-checked)) - (when on-change - (on-change new-checked event)))))) - + handle-toggle + (mf/use-fn + (mf/deps controlled? current-checked on-change internal-checked* disabled) + (fn [event] + (when-not disabled + (let [new-checked (not current-checked)] + (when-not controlled? + (reset! internal-checked* new-checked)) + (when on-change + (on-change new-checked event)))))) + ;; Keyboard events - handle-keydown (mf/use-fn - (mf/deps handle-toggle) - (fn [event] - (when (or (= (.-key event) " ") (= (.-key event) "Enter")) - (.preventDefault event) - (handle-toggle event)))) - + 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)))) + ;; Label click handler - handle-label-click (mf/use-fn - (mf/deps handle-toggle) - (fn [event] - (.preventDefault event) - (handle-toggle event))) - + handle-label-click + (mf/use-fn + (mf/deps handle-toggle) + (fn [event] + (dom/prevent-default event) + (handle-toggle event))) + has-label (not (str/blank? label)) effective-aria-label (if has-label - (or aria-label label) - "Toggle switch")] - - [:div {:class (dm/str class " " (stl/css-case :switcher-wrapper true)) - :data-testid (.-data-testid props)} + (or aria-label label) + ;; TODO: Add translation string + ;; (tr "linea de traduccion") + "Toggle switch") + + props (mf/spread-props props {:id id + :ref ref + :role "switch" + :tabIndex (if disabled -1 0) + :aria-checked current-checked + :aria-disabled disabled + :aria-label effective-aria-label + :class (stl/css-case :switcher true + :is-checked current-checked + :is-disabled disabled + :switcher--sm (= size "sm") + :switcher--md (= size "md") + :switcher--lg (= size "lg")) + :on-click handle-toggle + :on-key-down handle-keydown})] + + [:div {:class [class (stl/css :switcher-wrapper)]} (when has-label [:label {:for id :class (stl/css-case :switcher-label true :is-disabled disabled) :on-click handle-label-click} label]) - [:div {:id id - :ref ref - :role "switch" - :tabIndex (if disabled -1 0) - :aria-checked current-checked - :aria-disabled disabled - :aria-label effective-aria-label - :class (stl/css-case :switcher true - :is-checked current-checked - :is-disabled disabled - :switcher--sm (= size :sm) - :switcher--md (= size :md) - :switcher--lg (= size :lg)) - :on-click handle-toggle - :on-key-down handle-keydown} + [:> :div props [:div {:class (stl/css-case :switcher-track true :is-checked current-checked :is-disabled disabled)} [:div {:class (stl/css-case :switcher-thumb true :is-checked current-checked :is-disabled disabled)}]]]])) - -;; Export as default -(def switcher switcher*) diff --git a/frontend/src/app/main/ui/ds/controls/switcher/switcher.mdx b/frontend/src/app/main/ui/ds/controls/switcher/switcher.mdx index eb86bcd177..2008ab495f 100644 --- a/frontend/src/app/main/ui/ds/controls/switcher/switcher.mdx +++ b/frontend/src/app/main/ui/ds/controls/switcher/switcher.mdx @@ -5,7 +5,7 @@ Copyright (c) KALEIDOS INC */ } import { Canvas, Meta } from '@storybook/blocks'; -import * as SwitcherStories from "./switcher.stories"; +import * as Switcher from "./switcher.stories"; @@ -13,7 +13,7 @@ import * as SwitcherStories from "./switcher.stories"; The `switcher*` component is a toggle control that allows users to switch between two states (on/off). It provides both controlled and uncontrolled modes, making it suitable for various use cases across the interface. - + ## Anatomy @@ -59,7 +59,7 @@ The switcher component consists of three main parts: (reset! checked new-checked))}]) ``` - + ### Different Sizes @@ -71,8 +71,6 @@ The switcher supports three size variants: `"sm"`, `"md"` (default), and `"lg"`. [:> switcher* {:size "lg" :label "Large"}] ``` - - ### Disabled State Disabled switchers are non-interactive and visually distinct. You can control the disabled state using the `disabled` prop in the controls panel. @@ -92,7 +90,7 @@ When no visible label is provided, use `aria-label` for accessibility. :default-checked false}] ``` - + ## Design Tokens @@ -138,4 +136,4 @@ The switcher component uses the following design system tokens: ## Interactive Example - + diff --git a/frontend/src/app/main/ui/ds/controls/switcher/switcher.stories.jsx b/frontend/src/app/main/ui/ds/controls/switcher/switcher.stories.jsx index 6a7a727cd7..fb494e8b41 100644 --- a/frontend/src/app/main/ui/ds/controls/switcher/switcher.stories.jsx +++ b/frontend/src/app/main/ui/ds/controls/switcher/switcher.stories.jsx @@ -11,7 +11,7 @@ const { Switcher } = Components; export default { title: "Controls/Switcher", - component: Components.Switcher, + component: Switcher, argTypes: { checked: { control: { type: "boolean" }, @@ -44,16 +44,15 @@ export default { }, }, args: { - label: "Enable notifications", disabled: false, size: "md", defaultChecked: false, }, parameters: { - controls: { exclude: ["id", "class", "dataTestid"] }, + controls: { exclude: ["id", "class", "dataTestid", "on-change"] }, }, render: ({ onChange, ...args }) => ( - + ), };