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}
|
[:selected {:optional true}
|
||||||
[:maybe [:or :keyword :string]]]
|
[:maybe [:or :keyword :string]]]
|
||||||
[:allow-empty {:optional true} :boolean]
|
[:allow-empty {:optional true} :boolean]
|
||||||
|
[:disabled {:optional true} :boolean]
|
||||||
[:options [:vector {:min 1} schema:radio-button]]
|
[:options [:vector {:min 1} schema:radio-button]]
|
||||||
[:on-change {:optional true} fn?]])
|
[:on-change {:optional true} fn?]])
|
||||||
|
|
||||||
(mf/defc radio-buttons*
|
(mf/defc radio-buttons*
|
||||||
{::mf/schema schema: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)
|
(let [options (if (array? options)
|
||||||
(mfu/bean options)
|
(mfu/bean options)
|
||||||
options)
|
options)
|
||||||
type (if allow-empty "checkbox" "radio")
|
type (if allow-empty "checkbox" "radio")
|
||||||
variant (d/nilv variant "secondary")
|
variant (d/nilv variant "secondary")
|
||||||
|
wrapper-disabled (d/nilv disabled false)
|
||||||
|
|
||||||
handle-click
|
handle-click
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(let [target (dom/get-target event)
|
(let [target (dom/get-target event)
|
||||||
label (dom/get-parent-with-data target "label")]
|
label (dom/get-parent-with-data target "label")
|
||||||
(dom/prevent-default event)
|
input (dom/query label "input")
|
||||||
(dom/stop-propagation event)
|
disabled? (dom/get-attribute target "disabled")]
|
||||||
(dom/click label))))
|
(when-not disabled?
|
||||||
|
(dom/click input)))))
|
||||||
|
|
||||||
handle-change
|
handle-change
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps selected on-change)
|
(mf/deps selected on-change allow-empty)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(let [input (dom/get-target event)
|
(let [input (dom/get-target event)
|
||||||
value (dom/get-target-val 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)
|
(when (fn? on-change)
|
||||||
(on-change value event))
|
(on-change new-value event))
|
||||||
(dom/blur! input))))
|
(dom/blur! input))))
|
||||||
|
|
||||||
props
|
props
|
||||||
(mf/spread-props props {:key (dm/str name "-" selected)
|
(mf/spread-props props {:key (dm/str name "-" selected)
|
||||||
:class [class (stl/css-case :wrapper true
|
:class [class (stl/css-case :wrapper true
|
||||||
|
:disabled disabled
|
||||||
:extended extended)]})]
|
:extended extended)]})]
|
||||||
|
|
||||||
[:> :div props
|
[:> :div props
|
||||||
(for [[idx {:keys [id class value label icon disabled]}] (d/enumerate options)]
|
(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
|
[:label {:key idx
|
||||||
:html-for id
|
:html-for id
|
||||||
:data-label true
|
:data-label true
|
||||||
@ -88,13 +98,13 @@
|
|||||||
:aria-pressed checked?
|
:aria-pressed checked?
|
||||||
:aria-label label
|
:aria-label label
|
||||||
:icon icon
|
:icon icon
|
||||||
:disabled disabled}]
|
:disabled (or disabled wrapper-disabled)}]
|
||||||
[:> button* {:variant variant
|
[:> button* {:variant variant
|
||||||
:on-click handle-click
|
:on-click handle-click
|
||||||
:aria-pressed checked?
|
:aria-pressed checked?
|
||||||
:class (stl/css-case :button true
|
:class (stl/css-case :button true
|
||||||
:extended extended)
|
:extended extended)
|
||||||
:disabled disabled}
|
:disabled (or disabled wrapper-disabled)}
|
||||||
label])
|
label])
|
||||||
|
|
||||||
[:input {:id id
|
[:input {:id id
|
||||||
@ -102,6 +112,6 @@
|
|||||||
:on-change handle-change
|
:on-change handle-change
|
||||||
:type type
|
:type type
|
||||||
:name name
|
:name name
|
||||||
:disabled disabled
|
:disabled (or disabled wrapper-disabled)
|
||||||
:value value
|
:value value
|
||||||
:default-checked checked?}]]))]))
|
:checked checked?}]]))]))
|
||||||
@ -11,11 +11,17 @@ import * as RadioButtons from "./radio_buttons.stories";
|
|||||||
|
|
||||||
# Radio Buttons
|
# 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
|
## 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} />
|
<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"
|
{:id "align-right"
|
||||||
:label "Right"
|
:label "Right"
|
||||||
:value "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} />
|
<Canvas of={RadioButtons.WithIcons} />
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
(ns app.main.ui.foo
|
(ns app.main.ui.foo
|
||||||
(:require
|
(:require
|
||||||
@ -63,35 +71,58 @@ Radio buttons with icons only. In this case, the label will act as the tooltip o
|
|||||||
:label "Right align"
|
:label "Right align"
|
||||||
:value "right"}]}]
|
:value "right"}]}]
|
||||||
```
|
```
|
||||||
|
### Anatomy
|
||||||
|
|
||||||
## Anatomy
|
Each option is composed of:
|
||||||
|
|
||||||
Under the hood, each option is represented by
|
A visible control (button or icon button)
|
||||||
- a button, which is the visible and clickable element. It may be either an icon button or a text button.
|
A hidden native input (radio or checkbox) that stores the state
|
||||||
- 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,
|
All options share the same name, forming a radio group. Selecting one option automatically deselects the previously selected one.
|
||||||
selecting any radio button in that group automatically deselects any currently-selected radio button in the same group.
|
|
||||||
|
|
||||||
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,
|
### Selection
|
||||||
and therefore the selected option can be deselected. However, it will still only be possible to select one option.
|
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
|
### Allow empty
|
||||||
among all elements.
|
|
||||||
|
|
||||||
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
|
## 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.
|
### When not to use
|
||||||
- In preference panels and configuration screens.
|
|
||||||
|
|
||||||
### 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).
|
### Notes
|
||||||
- For actions that require confirmation (use buttons instead).
|
|
||||||
- For temporary states that need explicit "Apply" action.
|
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%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
outline: $b-1 solid var(--color-background-quaternary);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
|
|||||||
@ -15,6 +15,12 @@ const options = [
|
|||||||
{ id: "right", label: "Right", value: "right" },
|
{ 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 = [
|
const optionsIcon = [
|
||||||
{ id: "left", label: "Left align", value: "left", icon: "text-align-left" },
|
{ id: "left", label: "Left align", value: "left", icon: "text-align-left" },
|
||||||
{
|
{
|
||||||
@ -68,9 +74,24 @@ export default {
|
|||||||
parameters: {
|
parameters: {
|
||||||
controls: {
|
controls: {
|
||||||
exclude: ["options", "on-change"],
|
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 = {};
|
export const Default = {};
|
||||||
@ -80,3 +101,9 @@ export const WithIcons = {
|
|||||||
options: optionsIcon,
|
options: optionsIcon,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const WithOptionDisabled = {
|
||||||
|
args: {
|
||||||
|
options: optionsDisabled,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user