diff --git a/frontend/src/app/main/ui/ds/controls/radio_buttons.cljs b/frontend/src/app/main/ui/ds/controls/radio_buttons.cljs
index ea9dd6fff3..044fe87bc4 100644
--- a/frontend/src/app/main/ui/ds/controls/radio_buttons.cljs
+++ b/frontend/src/app/main/ui/ds/controls/radio_buttons.cljs
@@ -36,45 +36,55 @@
[:selected {:optional true}
[:maybe [:or :keyword :string]]]
[:allow-empty {:optional true} :boolean]
+ [:disabled {:optional true} :boolean]
[:options [:vector {:min 1} schema:radio-button]]
[:on-change {:optional true} fn?]])
(mf/defc radio-buttons*
{::mf/schema schema:radio-buttons}
- [{:keys [class variant extended name selected allow-empty options on-change] :rest props}]
+ [{:keys [class variant extended name selected allow-empty options on-change disabled] :rest props}]
(let [options (if (array? options)
(mfu/bean options)
options)
- type (if allow-empty "checkbox" "radio")
- variant (d/nilv variant "secondary")
+ type (if allow-empty "checkbox" "radio")
+ variant (d/nilv variant "secondary")
+ wrapper-disabled (d/nilv disabled false)
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))))
+ label (dom/get-parent-with-data target "label")
+ input (dom/query label "input")
+ disabled? (dom/get-attribute target "disabled")]
+ (when-not disabled?
+ (dom/click input)))))
handle-change
(mf/use-fn
- (mf/deps selected on-change)
+ (mf/deps selected on-change allow-empty)
(fn [event]
- (let [input (dom/get-target event)
- value (dom/get-target-val event)]
+ (let [input (dom/get-target event)
+ value (dom/get-target-val event)
+ selected-str (when selected (d/name selected))
+ new-value (if (and allow-empty (= value selected-str))
+ nil
+ value)]
(when (fn? on-change)
- (on-change value event))
+ (on-change new-value event))
(dom/blur! input))))
props
(mf/spread-props props {:key (dm/str name "-" selected)
:class [class (stl/css-case :wrapper true
+ :disabled disabled
:extended extended)]})]
[:> :div props
(for [[idx {:keys [id class value label icon disabled]}] (d/enumerate options)]
- (let [checked? (= selected value)]
+ (let [value-str (d/name value)
+ selected-str (when selected (d/name selected))
+ checked? (= selected-str value-str)]
[:label {:key idx
:html-for id
:data-label true
@@ -88,13 +98,13 @@
:aria-pressed checked?
:aria-label label
:icon icon
- :disabled disabled}]
+ :disabled (or disabled wrapper-disabled)}]
[:> button* {:variant variant
:on-click handle-click
:aria-pressed checked?
:class (stl/css-case :button true
:extended extended)
- :disabled disabled}
+ :disabled (or disabled wrapper-disabled)}
label])
[:input {:id id
@@ -102,6 +112,6 @@
:on-change handle-change
:type type
:name name
- :disabled disabled
+ :disabled (or disabled wrapper-disabled)
:value value
- :default-checked checked?}]]))]))
+ :checked checked?}]]))]))
\ No newline at end of file
diff --git a/frontend/src/app/main/ui/ds/controls/radio_buttons.mdx b/frontend/src/app/main/ui/ds/controls/radio_buttons.mdx
index 226319286a..5346d8751c 100644
--- a/frontend/src/app/main/ui/ds/controls/radio_buttons.mdx
+++ b/frontend/src/app/main/ui/ds/controls/radio_buttons.mdx
@@ -11,11 +11,17 @@ import * as RadioButtons from "./radio_buttons.stories";
# Radio Buttons
-The `radio-buttons*` component allows users to switch between two or more options that are mutually exclusive.
+The `radio-buttons*` component lets users select a single option from a set of mutually exclusive choices.
+
+It is designed for immediate selection changes, without requiring a confirmation step.
+
+---
## Variants
-Radio buttons with text only. The label will be the text of the button.
+### Text only
+
+Radio buttons using text labels. The label is displayed directly on each option.
@@ -34,12 +40,14 @@ Radio buttons with text only. The label will be the text of the button.
{:id "align-right"
:label "Right"
:value "right"}]}]
+
+ Icon only
```
-Radio buttons with icons only. In this case, the label will act as the tooltip of each button.
+### Icons only
+Radio buttons using icons instead of text labels. The label is used as tooltip and accessibility text.
-
```clj
(ns app.main.ui.foo
(:require
@@ -63,35 +71,58 @@ Radio buttons with icons only. In this case, the label will act as the tooltip o
:label "Right align"
:value "right"}]}]
```
+### Anatomy
-## Anatomy
+Each option is composed of:
-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 visible control (button or icon button)
+A hidden native input (radio or checkbox) that stores the state
-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.
+All options share the same name, forming a radio group. Selecting one option automatically deselects the previously selected one.
-The `selected` parameter should be set to the value of the option that is to be active. Otherwise, no option will be selected.
+## Behavior
-If the parameter `allow-empty` is enabled, then the component will work with checkboxes instead of radio buttons,
-and therefore the selected option can be deselected. However, it will still only be possible to select one option.
+### Selection
+The selected prop controls the active option
+It must match the value of one of the provided options
+If selected is nil, no option is selected
-The `extended` parameter allows the component to use all the available space from the parent and distribute it equally
-among all elements.
+### Allow empty
-Any option can be individually disabled using the `disabled` parameter.
+When allow-empty is enabled:
+
+The selected option can be deselected
+Only one option can still be active at a time
+This introduces toggle-like behavior over a single selection group
+
+### Extended
+
+When extended is enabled:
+
+The component expands to fill the width of its container
+Options are evenly distributed across available space
+
+### Disabled state
+The entire group can be disabled using the `:disabled` prop
+Individual options can also be disabled using `:disabled` inside each option
+Disabled options cannot be interacted with.
## Usage Guidelines
-### When to Use
+### When to use
+For settings where users must choose exactly one option
+For preference or configuration panels
+When changes should take effect immediately
-- For multiple choice settings that take effect immediately.
-- In preference panels and configuration screens.
+### When not to use
-### When Not to Use
+For boolean toggles → use a switch or checkbox
+For multiple selection → use checkboxes
+For actions requiring confirmation → use buttons or dialogs
+For workflows that require an explicit “Apply” step
-- For boolean settings (use switch or checkbox instead).
-- For actions that require confirmation (use buttons instead).
-- For temporary states that need explicit "Apply" action.
+### Notes
+
+This component is controlled: state must be managed externally via selected
+It does not manage internal state
+The on-change handler is called with the new value whenever selection changes
\ No newline at end of file
diff --git a/frontend/src/app/main/ui/ds/controls/radio_buttons.scss b/frontend/src/app/main/ui/ds/controls/radio_buttons.scss
index 05957025dc..56e53fae7e 100644
--- a/frontend/src/app/main/ui/ds/controls/radio_buttons.scss
+++ b/frontend/src/app/main/ui/ds/controls/radio_buttons.scss
@@ -20,6 +20,11 @@
width: 100%;
display: flex;
}
+
+ &.disabled {
+ outline: $b-1 solid var(--color-background-quaternary);
+ background-color: transparent;
+ }
}
.label {
diff --git a/frontend/src/app/main/ui/ds/controls/radio_buttons.stories.jsx b/frontend/src/app/main/ui/ds/controls/radio_buttons.stories.jsx
index 7133a1b961..157f83e465 100644
--- a/frontend/src/app/main/ui/ds/controls/radio_buttons.stories.jsx
+++ b/frontend/src/app/main/ui/ds/controls/radio_buttons.stories.jsx
@@ -15,6 +15,12 @@ const options = [
{ id: "right", label: "Right", value: "right" },
];
+const optionsDisabled = [
+ { id: "left", label: "Left", value: "left" },
+ { id: "center", label: "Center", value: "center", disabled: true },
+ { id: "right", label: "Right", value: "right" },
+];
+
const optionsIcon = [
{ id: "left", label: "Left align", value: "left", icon: "text-align-left" },
{
@@ -68,9 +74,24 @@ export default {
parameters: {
controls: {
exclude: ["options", "on-change"],
+ disabled: {
+ control: { type: "boolean" },
+ },
},
},
- render: ({ ...args }) => ,
+ render: (args) => {
+ const [selected, setSelected] = React.useState(args.selected);
+
+ return (
+ {
+ setSelected(value);
+ }}
+ />
+ );
+ },
};
export const Default = {};
@@ -80,3 +101,9 @@ export const WithIcons = {
options: optionsIcon,
},
};
+
+export const WithOptionDisabled = {
+ args: {
+ options: optionsDisabled,
+ },
+};