♻️ Improve and add radio buttons to the DS

This commit is contained in:
Luis de Dios
2025-12-22 00:07:59 +01:00
parent 047483a70a
commit 590ec50c24
8 changed files with 376 additions and 28 deletions

View File

@@ -12,6 +12,7 @@
[app.main.ui.ds.controls.combobox :refer [combobox*]]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
[app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]]
[app.main.ui.ds.controls.select :refer [select*]]
[app.main.ui.ds.controls.switch :refer [switch*]]
[app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]]
@@ -63,6 +64,7 @@
:Select select*
:Switch switch*
:Checkbox checkbox*
:RadioButtons radio-buttons*
:Combobox combobox*
:Text text*
:TabSwitcher tab-switcher*

View File

@@ -0,0 +1,101 @@
;; 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.radio-buttons
(:require-macros
[app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon-list]]
[app.util.dom :as dom]
[rumext.v2 :as mf]
[rumext.v2.util :as mfu]))
(def ^:private schema:radio-button
[:map
[:id :string]
[:icon {:optional true}
[:and :string [:fn #(contains? icon-list %)]]]
[:label :string]
[:value :string]
[:disabled {:optional true} :boolean]])
(def ^:private schema:radio-buttons
[:map
[:class {:optional true} :string]
[:expanded {:optional true} :boolean]
[:name {:optional true} :string]
[:selected {:optional true} :string]
[:options [:vector {:min 2} schema:radio-button]]
[:on-change {:optional true} fn?]])
(mf/defc radio-buttons*
{::mf/schema schema:radio-buttons}
[{:keys [class expanded name selected options on-change] :rest props}]
(let [options (if (array? options)
(mfu/bean options)
options)
handle-click
(mf/use-fn
(fn [event]
(let [target (dom/get-target event)
label (dom/get-parent-with-data target "label")]
(dom/prevent-default event)
(dom/stop-propagation event)
(dom/click label))))
handle-change
(mf/use-fn
(mf/deps selected on-change)
(fn [event]
(let [input (dom/get-target event)
value (dom/get-target-val event)]
(when (fn? on-change)
(on-change value event))
(dom/blur! input))))
props
(mf/spread-props props {:key (dm/str name "-" selected)
:class [class (stl/css-case :wrapper true
:expanded expanded)]})]
[:> :div props
(for [[idx {:keys [id value label icon disabled]}] (d/enumerate options)]
(let [checked? (= selected value)]
[:label {:key idx
:html-for id
:data-label true
:data-testid id
:class (stl/css-case :label true
:expanded expanded)}
(if (some? icon)
[:> icon-button* {:variant "secondary"
:on-click handle-click
:aria-pressed checked?
:aria-label label
:icon icon
:disabled disabled}]
[:> button* {:variant "secondary"
:on-click handle-click
:aria-pressed checked?
:class (stl/css-case :button true
:expanded expanded)
:disabled disabled}
label])
[:input {:id id
:class (stl/css :input)
:on-change handle-change
:type "radio"
:name name
:disabled disabled
:value value
:default-checked checked?}]]))]))

View File

@@ -0,0 +1,87 @@
{ /* 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 */ }
import { Canvas, Meta } from '@storybook/addon-docs/blocks';
import * as RadioButtons from "./radio_buttons.stories";
<Meta title="Controls/Radio Buttons" />
# Radio Buttons
The `radio-buttons*` component allows users to switch between two or more options that are mutually exclusive.
## Variants
Radio buttons with text only. The label will be the text of the button.
<Canvas of={RadioButtons.Default} />
```clj
[:> radio-buttons* {:selected "left"
:on-change handle-change
:name "alignment"
:expanded false
:options [{:id "align-left"
:label "Left"
:value "left"}
{:id "align-center"
:label "Center"
:value "center"}
{:id "align-right"
:label "Right"
:value "right"}]}]
```
Radio buttons with icons only. In this case, the label will act as the tooltip of each button.
<Canvas of={RadioButtons.WithIcons} />
```clj
(ns app.main.ui.foo
(:require
[app.main.ui.ds.foundations.assets.icon :as i]))
[:> radio-buttons* {:selected "left"
:on-change handle-change
:name "alignment"
:expanded false
:options [{:id "align-left"
:icon i/text-align-left
:label "Left align"
:value "left"}
{:id "align-center"
:icon i/text-align-center
:label "Center align"
:value "center"}
{:id "align-right"
:icon i/text-align-right
:label "Right align"
:value "right"}]}]
```
In both cases, the selected value must match the value of the selected option.
## Anatomy
Under the hood, each option is represented by
- a button, which is the visible and clickable element. It may be either an icon button or a text button.
- a radio input, which is not visible but retains the current state of the option.
A radio group is defined by giving each of radio buttons in the group the same name. Once a radio group is established,
selecting any radio button in that group automatically deselects any currently-selected radio button in the same group.
## Usage Guidelines
### When to Use
- For multiple choice settings that take effect immediately.
- In preference panels and configuration screens.
### When Not to Use
- For boolean settings (use switch or checkbox instead).
- For actions that require confirmation (use buttons instead).
- For temporary states that need explicit "Apply" action.

View File

@@ -0,0 +1,40 @@
// 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
@use "ds/_borders.scss" as *;
@use "ds/spacing.scss" as *;
.wrapper {
display: flex;
justify-content: flex-start;
align-items: center;
width: fit-content;
border-radius: $br-8;
background-color: var(--color-background-tertiary);
gap: var(--sp-xs);
&.expanded {
width: 100%;
}
}
.label {
&.expanded {
display: flex;
flex: 1 1 0;
}
}
.button {
&.expanded {
justify-content: center;
flex-grow: 1;
}
}
.input {
display: none;
}

View File

@@ -0,0 +1,67 @@
// 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
import * as React from "react";
import Components from "@target/components";
const { RadioButtons } = Components;
const options = [
{id: "left", label: "Left", value: "left" },
{id: "center", label: "Center", value: "center" },
{id: "right", label: "Right", value: "right" },
];
const optionsIcon = [
{id: "left", label: "Left align", value: "left", icon: "text-align-left" },
{id: "center", label: "Center align", value: "center", icon: "text-align-center" },
{id: "right", label: "Right align", value: "right", icon: "text-align-right" },
];
export default {
title: "Controls/Radio Buttons",
component: RadioButtons,
argTypes: {
name: {
control: { type: "text" },
description: "Whether the checkbox is checked",
},
selected: {
control: { type: "select" },
options: ["left", "center", "right"],
description: "Whether the checkbox is checked",
},
expanded: {
control: { type: "boolean" },
description: "Whether the checkbox is checked",
},
disabled: {
control: { type: "boolean" },
description: "Whether the checkbox is disabled",
},
},
args: {
name: "alignment",
selected: "left",
expanded: false,
options: options,
disabled: false,
},
parameters: {
controls: {
exclude: ["options", "on-change"],
},
},
render: ({ ...args }) => <RadioButtons {...args} />,
};
export const Default = {};
export const WithIcons = {
args: {
options: optionsIcon,
},
};

View File

@@ -17,13 +17,13 @@
[app.main.data.workspace.interactions :as dwi]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.components.select :refer [select]]
[app.main.ui.components.title-bar :refer [title-bar*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.checkbox :refer [checkbox*]]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
[app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
[app.util.dom :as dom]
@@ -582,41 +582,44 @@
:options animation-opts
:on-change change-animation-type}]]]
;; Direction
;; Way
(when (ctsi/has-way? interaction)
[:div {:class (stl/css :interaction-row)}
[:div {:class (stl/css :interaction-row-radio)}
[:& radio-buttons {:selected (d/name way)
:on-change change-way
:name "animation-way"}
[:& radio-button {:value "in"
:id "animation-way-in"}]
[:& radio-button {:id "animation-way-out"
:value "out"}]]]])
[:> radio-buttons* {:selected (d/name way)
:on-change change-way
:name "animation-way"
:expanded true
:options [{:id "animation-way-in"
:label (tr "workspace.options.interaction-animation-direction-in")
:value "in"}
{:id "animation-way-out"
:label (tr "workspace.options.interaction-animation-direction-out")
:value "out"}]}]]])
;; Direction
(when (ctsi/has-direction? interaction)
[:div {:class (stl/css :interaction-row)}
[:div {:class (stl/css :interaction-row-radio)}
[:& radio-buttons {:selected (d/name direction)
:on-change change-direction
:name "animation-direction"}
[:& radio-button {:icon i/row
:icon-class (stl/css :right)
:value "right"
:id "animation-right"}]
[:& radio-button {:icon i/row-reverse
:icon-class (stl/css :left)
:id "animation-left"
:value "left"}]
[:& radio-button {:icon i/column
:icon-class (stl/css :down)
:id "animation-down"
:value "down"}]
[:& radio-button {:icon i/column-reverse
:icon-class (stl/css :up)
:id "animation-up"
:value "up"}]]]])
[:> radio-buttons* {:selected (d/name direction)
:on-change change-direction
:name "animation-direction"
:options [{:id "animation-right"
:icon i/row
:label (tr "workspace.options.interaction-animation-direction-right")
:value "right"}
{:id "animation-left"
:icon i/row-reverse
:label (tr "workspace.options.interaction-animation-direction-left")
:value "left"}
{:id "animation-down"
:icon i/column
:label (tr "workspace.options.interaction-animation-direction-down")
:value "down"}
{:id "animation-up"
:icon i/column-reverse
:label (tr "workspace.options.interaction-animation-direction-up")
:value "up"}]}]]])
;; Duration
(when (ctsi/has-duration? interaction)

View File

@@ -6161,6 +6161,30 @@ msgstr "After delay"
msgid "workspace.options.interaction-animation"
msgstr "Animation"
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:609
msgid "workspace.options.interaction-animation-direction-right"
msgstr "Right"
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:613
msgid "workspace.options.interaction-animation-direction-left"
msgstr "Left"
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:617
msgid "workspace.options.interaction-animation-direction-down"
msgstr "Down"
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:621
msgid "workspace.options.interaction-animation-direction-up"
msgstr "Up"
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:595
msgid "workspace.options.interaction-animation-direction-in"
msgstr "In"
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:599
msgid "workspace.options.interaction-animation-direction-out"
msgstr "Out"
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:413
msgid "workspace.options.interaction-animation-dissolve"
msgstr "Dissolve"

View File

@@ -6114,6 +6114,30 @@ msgstr "Tiempo"
msgid "workspace.options.interaction-animation"
msgstr "Animación"
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:609
msgid "workspace.options.interaction-animation-direction-right"
msgstr "Derecha"
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:613
msgid "workspace.options.interaction-animation-direction-left"
msgstr "Izquierda"
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:617
msgid "workspace.options.interaction-animation-direction-down"
msgstr "Abajo"
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:621
msgid "workspace.options.interaction-animation-direction-up"
msgstr "Arriba"
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:595
msgid "workspace.options.interaction-animation-direction-in"
msgstr "Dentro"
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:599
msgid "workspace.options.interaction-animation-direction-out"
msgstr "Fuera"
#: src/app/main/ui/workspace/sidebar/options/menus/interactions.cljs:413
msgid "workspace.options.interaction-animation-dissolve"
msgstr "Disolver"