///|
pub(all) enum RadioGroupOrientation {
RadioVertical
RadioHorizontal
} derive(Debug, Eq)
///|
const RadioGroupVerticalStyle : String = "display:grid;width:100%;gap:0.75rem"
///|
const RadioGroupHorizontalStyle : String = "display:flex;width:100%;flex-wrap:wrap;align-items:center;gap:0.75rem"
///|
const RadioGroupItemBaseStyle : String = "position:relative;display:inline-grid;width:1rem;height:1rem;flex-shrink:0;place-content:center;--rui-control-base-bg:var(--rui-input-background,transparent);--rui-control-base-fg:var(--rui-primary-foreground,oklch(0.985 0 0));--rui-control-base-border:var(--rui-input,oklch(0.922 0 0));--rui-control-base-shadow:0 1px 2px rgb(0 0 0 / 0.05);border:1px solid transparent;border-color:var(--rui-control-border,var(--rui-control-base-border,var(--rui-input,oklch(0.922 0 0))));border-radius:9999px;margin:0;padding:0;background:var(--rui-control-bg,var(--rui-control-base-bg,var(--rui-input-background,transparent)));color:var(--rui-control-fg,var(--rui-control-base-fg,var(--rui-primary-foreground,oklch(0.985 0 0))));box-shadow:var(--rui-control-shadow,var(--rui-control-base-shadow,0 1px 2px rgb(0 0 0 / 0.05)));cursor:pointer;appearance:none;-webkit-appearance:none;outline-offset:2px;vertical-align:middle"
///|
const RadioGroupItemCheckedStyle : String = "--rui-control-base-border:var(--rui-input,oklch(0.922 0 0));--rui-control-base-bg:radial-gradient(circle at center,var(--rui-primary,oklch(0.205 0 0)) 0 0.25rem,transparent 0.27rem),var(--rui-input-background,transparent);--rui-control-base-fg:var(--rui-primary,oklch(0.205 0 0))"
///|
const RadioGroupItemUncheckedStyle : String = "--rui-control-base-border:var(--rui-input,oklch(0.922 0 0));--rui-control-base-bg:var(--rui-input-background,transparent)"
///|
const RadioGroupItemInvalidStyle : String = "border-color:var(--rui-destructive-border,var(--rui-destructive,oklch(0.577 0.245 27.325)));box-shadow:0 0 0 3px var(--rui-destructive-ring,oklch(0.577 0.245 27.325 / 0.2))"
///|
fn radio_group_orientation_value(orientation : RadioGroupOrientation) -> String {
match orientation {
RadioVertical => "vertical"
RadioHorizontal => "horizontal"
}
}
///|
fn radio_group_orientation_style(orientation : RadioGroupOrientation) -> String {
match orientation {
RadioVertical => RadioGroupVerticalStyle
RadioHorizontal => RadioGroupHorizontalStyle
}
}
///|
fn radio_group_item_state_style(checked : Bool) -> String {
if checked {
RadioGroupItemCheckedStyle
} else {
RadioGroupItemUncheckedStyle
}
}
///|
fn radio_group_item_invalid_style(invalid : Bool) -> String {
if invalid {
RadioGroupItemInvalidStyle
} else {
""
}
}
///|
#cfg(target="js")
fn radio_group_move_and_select(event : @dom.KeyboardEvent) -> Bool {
if event.alt_key() ||
event.ctrl_key() ||
event.meta_key() ||
event.shift_key() {
return false
}
guard ui_roving_context(
event, "[data-slot=\"radio-group-item\"]", "[data-slot=\"radio-group\"]", "[data-slot=\"radio-group-item\"]",
)
is Some(context) else {
return false
}
guard ui_roving_target(
context,
event.key(),
"both",
ui_element_is_rtl(context.root),
)
is Some(target) else {
return false
}
ui_focus_element_without_scroll(target)
ui_click_element(target)
true
}
///|
#cfg(target="js")
fn radio_group_bind_keyboard(attrs : @html.Attrs) -> Unit {
ignore(
attrs.on_keydown(event => {
if radio_group_move_and_select(event) {
event.prevent_default()
}
@cmd.none
}),
)
}
///|
#cfg(not(target="js"))
fn radio_group_bind_keyboard(attrs : @html.Attrs) -> Unit {
ignore(attrs)
}
///|
/// Lay out native radio inputs as a Vega-style radio group.
///
/// Each item remains a real ``, so same-name items retain
/// the browser's arrow-key navigation and form behavior.
pub fn[C : @html.IsChildren] radio_group_root(
orientation? : RadioGroupOrientation = RadioVertical,
id? : String,
class? : String,
title? : String,
aria_label? : String,
attrs? : @html.Attrs,
style? : Array[String] = [],
children : C,
) -> @html.Html {
let element_attrs = ui_attrs(attrs)
.data_set("slot", "radio-group")
.data_set("orientation", radio_group_orientation_value(orientation))
.role("radiogroup")
if aria_label is Some(label) {
ignore(element_attrs.aria_label(label))
}
@html.div(
style=ui_styles(
[UiBoxSizing, UiFontSans, radio_group_orientation_style(orientation)],
style,
),
id?,
class?,
title?,
attrs=element_attrs,
children,
)
}
///|
/// Render one native radio item for the incremental group.
///
/// Pass the same `name` and `on_value_change` to sibling items. The callback
/// receives the activated item's `value`.
fn render_radio_group_item(
value~ : String,
name? : String,
checked? : Bool = false,
disabled? : Bool = false,
required? : Bool,
invalid? : Bool = false,
id? : String,
class? : String,
title? : String,
aria_label? : String,
on_value_change? : @cmd.Emit[String],
attrs? : @html.Attrs,
style? : Array[String] = [],
) -> @html.Html {
let state = if checked { "checked" } else { "unchecked" }
let control_attrs = ui_attrs(attrs)
.data_set("slot", "radio-group-item")
.data_set("state", state)
.data_set(state, "")
if disabled {
ignore(control_attrs.disabled(true).data_set("disabled", ""))
}
if invalid {
ignore(control_attrs.aria_invalid("true").data_set("invalid", ""))
}
if aria_label is Some(label) {
ignore(control_attrs.aria_label(label))
}
radio_group_bind_keyboard(control_attrs)
let on_change = on_value_change
@html.input(
input_type=@html.Radio,
name?,
value~,
checked~,
style=ui_styles(
[
UiBoxSizing,
UiTransition,
RadioGroupItemBaseStyle,
radio_group_item_state_style(checked),
radio_group_item_invalid_style(invalid),
ui_disabled_visual_style(disabled),
],
style,
),
id?,
class?,
title?,
required?,
on_change?,
attrs=control_attrs,
)
}
///|
/// Opaque state shared by `radio_group` and its items.
struct RadioGroupScope {
value : String
name : String?
disabled : Bool
required : Bool
invalid : Bool
emit : @cmd.Emit[String]
}
///|
/// Render one compound group state for the incremental wrapper.
fn[C : @html.IsChildren] render_radio_group(
value~ : String,
name? : String,
disabled? : Bool = false,
required? : Bool = false,
invalid? : Bool = false,
orientation? : RadioGroupOrientation = RadioVertical,
on_value_change? : @cmd.Emit[String],
id? : String,
class? : String,
title? : String,
aria_label? : String,
attrs? : @html.Attrs,
style? : Array[String] = [],
children : (RadioGroupScope) -> C,
) -> @html.Html {
let emit = @cmd.Emit(next => {
if on_value_change is Some(notify) {
notify(next)
} else {
@cmd.none
}
})
let scope = { value, name, disabled, required, invalid, emit }
let root_attrs = ui_attrs(attrs).data_set("value", value)
if disabled {
ignore(root_attrs.aria_disabled("true").data_set("disabled", ""))
}
if invalid {
ignore(root_attrs.aria_invalid("true").data_set("invalid", ""))
}
radio_group_root(
orientation~,
id?,
class?,
title?,
aria_label?,
attrs=root_attrs,
style~,
children(scope),
)
}
///|
/// Build a black-box radio group with component-local incremental selection.
#cfg(target="js")
pub fn[C : @html.IsChildren] radio_group(
default_value? : String = "",
name? : String,
disabled? : Bool = false,
required? : Bool = false,
invalid? : Bool = false,
orientation? : RadioGroupOrientation = RadioVertical,
on_value_change? : @cmd.Emit[String],
id? : String,
class? : String,
title? : String,
aria_label? : String,
attrs? : @html.Attrs,
style? : Array[String] = [],
children : (RadioGroupScope) -> C,
) -> @rabbita.Val[@html.Html] {
let (value, set_value) = @rabbita.create_variable(default_value)
value.view(value => {
render_radio_group(
value~,
name?,
disabled~,
required~,
invalid~,
orientation~,
on_value_change=@cmd.Emit(next => {
let update = set_value(_ => next)
if on_value_change is Some(notify) {
@cmd.batch([update, notify(next)])
} else {
update
}
}),
id?,
class?,
title?,
aria_label?,
attrs?,
style~,
children,
)
})
}
///|
#cfg(not(target="js"))
pub fn[C : @html.IsChildren] radio_group(
default_value? : String = "",
name? : String,
disabled? : Bool = false,
required? : Bool = false,
invalid? : Bool = false,
orientation? : RadioGroupOrientation = RadioVertical,
on_value_change? : @cmd.Emit[String],
id? : String,
class? : String,
title? : String,
aria_label? : String,
attrs? : @html.Attrs,
style? : Array[String] = [],
children : (RadioGroupScope) -> C,
) -> @rabbita.Val[@html.Html] {
ignore(on_value_change)
@rabbita.Val::constant(
render_radio_group(
value=default_value,
name?,
disabled~,
required~,
invalid~,
orientation~,
id?,
class?,
title?,
aria_label?,
attrs?,
style~,
children,
),
)
}
///|
/// Render one item bound to the local state owned by `radio_group`.
pub fn radio_group_item(
scope~ : RadioGroupScope,
value~ : String,
disabled? : Bool = false,
required? : Bool = false,
invalid? : Bool = false,
id? : String,
class? : String,
title? : String,
aria_label? : String,
attrs? : @html.Attrs,
style? : Array[String] = [],
) -> @html.Html {
let disabled = scope.disabled || disabled
let name = scope.name
render_radio_group_item(
value~,
name?,
checked=scope.value == value,
disabled~,
required=scope.required || required,
invalid=scope.invalid || invalid,
id?,
class?,
title?,
aria_label?,
on_value_change=@cmd.Emit(_ => (scope.emit)(value)),
attrs?,
style~,
)
}