mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
🐛 Fix radio-buttons component in the DS (#8820)
This commit is contained in:
parent
87bb1b8e74
commit
04f6307c69
@ -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?}]]))]))
|
||||
@ -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.
|
||||
|
||||
<Canvas of={RadioButtons.Default} />
|
||||
|
||||
@ -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.
|
||||
|
||||
<Canvas of={RadioButtons.WithIcons} />
|
||||
|
||||
```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
|
||||
@ -20,6 +20,11 @@
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
outline: $b-1 solid var(--color-background-quaternary);
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
|
||||
@ -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 }) => <RadioButtons {...args} />,
|
||||
render: (args) => {
|
||||
const [selected, setSelected] = React.useState(args.selected);
|
||||
|
||||
return (
|
||||
<RadioButtons
|
||||
{...args}
|
||||
selected={selected}
|
||||
onChange={(value, event) => {
|
||||
setSelected(value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Default = {};
|
||||
@ -80,3 +101,9 @@ export const WithIcons = {
|
||||
options: optionsIcon,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithOptionDisabled = {
|
||||
args: {
|
||||
options: optionsDisabled,
|
||||
},
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user