///|
priv enum ToggleGroupMsg {
ToggleGroupValue(String)
}
///|
/// Opaque render scope owned by `toggle_group`.
struct ToggleGroupScope {
values : Array[String]
multiple : Bool
variant : ToggleVariant
size : ToggleSize
spacing : Int
emit : @cmd.Emit[ToggleGroupMsg]
tab_claimed : Ref[Bool]
}
///|
#cfg(target="js")
fn toggle_group_move_focus(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=\"toggle-group-item\"]", "[data-slot=\"toggle-group\"]",
"[data-slot=\"toggle-group-item\"]",
)
is Some(context) else {
return false
}
let orientation = context.root.get_attribute("data-orientation").unwrap_or("")
guard ui_roving_target(
context,
event.key(),
orientation,
ui_element_is_rtl(context.root),
)
is Some(target) else {
return false
}
for item in context.items {
ui_set_element_tab_index(
item,
if item.is_same_node(target.as_node()) {
0
} else {
-1
},
)
}
ui_focus_element_without_scroll(target)
true
}
///|
#cfg(target="js")
fn toggle_group_bind_keyboard(attrs : @html.Attrs) -> Unit {
ignore(
attrs.on_keydown(event => {
if toggle_group_move_focus(event) {
event.prevent_default()
}
@cmd.none
}),
)
}
///|
#cfg(not(target="js"))
fn toggle_group_bind_keyboard(attrs : @html.Attrs) -> Unit {
ignore(attrs)
}
///|
fn toggle_group_contains(values : Array[String], value : String) -> Bool {
for item in values {
if item == value {
return true
}
}
false
}
///|
fn toggle_group_next_values(
values : Array[String],
value : String,
multiple : Bool,
) -> Array[String] {
let selected = toggle_group_contains(values, value)
if !multiple {
if selected {
[]
} else {
[value]
}
} else {
let next : Array[String] = []
for item in values {
if item != value {
next.push(item)
}
}
if !selected {
next.push(value)
}
next
}
}
///|
fn[C : @html.IsChildren] render_toggle_group(
scope : ToggleGroupScope,
orientation : SeparatorOrientation,
variant : ToggleVariant,
size : ToggleSize,
spacing : Int,
aria_label : String?,
id : String?,
class : String?,
title : String?,
attrs : @html.Attrs?,
style : Array[String],
children : (ToggleGroupScope) -> C,
) -> @html.Html {
let orientation_value = separator_orientation_value(orientation)
let gap = spacing.to_double() / 4.0
let group_attrs = ui_attrs(attrs)
.role("group")
.aria_orientation(orientation_value)
.data_set("slot", "toggle-group")
.data_set("orientation", orientation_value)
.data_set("variant", toggle_variant_value(variant))
.data_set("size", toggle_size_value(size))
.data_set("spacing", "\{spacing}")
.data_set("multiple", ui_bool(scope.multiple))
if aria_label is Some(label) {
ignore(group_attrs.aria_label(label))
}
@html.div(
id?,
class?,
title?,
attrs=group_attrs,
style=ui_styles(
[
UiBoxSizing,
UiFontSans,
"--rui-toggle-group-gap:\{gap}rem;display:inline-flex;width:fit-content;align-items:center;gap:var(--rui-toggle-group-gap);border-radius:var(--rui-control-radius,calc(var(--rui-radius,0.625rem) - 0.125rem))",
if spacing == 0 && variant is Outline {
UiShadowXs
} else {
""
},
if orientation is Vertical {
"flex-direction:column;align-items:stretch"
} else {
"flex-direction:row"
},
],
style,
),
children(scope),
)
}
///|
/// Stateful shadcn-style toggle group. Selection is component-local and the
/// optional callback observes each committed value set.
#cfg(target="js")
pub fn[C : @html.IsChildren] toggle_group(
default_values? : Array[String] = [],
multiple? : Bool = false,
orientation? : SeparatorOrientation = Horizontal,
variant? : ToggleVariant = Default,
size? : ToggleSize = Default,
spacing? : Int = 0,
aria_label? : String,
on_value_change? : @cmd.Emit[Array[String]],
id? : String,
class? : String,
title? : String,
attrs? : @html.Attrs,
style? : Array[String] = [],
children : (ToggleGroupScope) -> C,
) -> @rabbita.Val[@html.Html] {
let (values, emit) = @rabbita.create_state(default_values, update=fn(
_,
msg,
values,
) {
let next = match msg {
ToggleGroupValue(value) =>
toggle_group_next_values(values, value, multiple)
}
let command = if on_value_change is Some(notify) {
notify(next)
} else {
@cmd.none
}
(next, command)
})
values.view(values => {
render_toggle_group(
{
values,
multiple,
variant,
size,
spacing,
emit,
tab_claimed: Ref(false),
},
orientation,
variant,
size,
spacing,
aria_label,
id,
class,
title,
attrs,
style,
children,
)
})
}
///|
#cfg(not(target="js"))
pub fn[C : @html.IsChildren] toggle_group(
default_values? : Array[String] = [],
multiple? : Bool = false,
orientation? : SeparatorOrientation = Horizontal,
variant? : ToggleVariant = Default,
size? : ToggleSize = Default,
spacing? : Int = 0,
aria_label? : String,
on_value_change? : @cmd.Emit[Array[String]],
id? : String,
class? : String,
title? : String,
attrs? : @html.Attrs,
style? : Array[String] = [],
children : (ToggleGroupScope) -> C,
) -> @rabbita.Val[@html.Html] {
ignore(on_value_change)
let emit = @cmd.Emit(_ => @cmd.none)
@rabbita.Val::constant(
render_toggle_group(
{
values: default_values,
multiple,
variant,
size,
spacing,
emit,
tab_claimed: Ref(false),
},
orientation,
variant,
size,
spacing,
aria_label,
id,
class,
title,
attrs,
style,
children,
),
)
}
///|
pub fn[C : @html.IsChildren] toggle_group_item(
scope~ : ToggleGroupScope,
value~ : String,
disabled? : Bool = false,
aria_label? : String,
id? : String,
class? : String,
title? : String,
attrs? : @html.Attrs,
style? : Array[String] = [],
children : C,
) -> @html.Html {
let pressed = toggle_group_contains(scope.values, value)
let item_attrs = ui_attrs(attrs)
.data_set("slot", "toggle-group-item")
.data_set("variant", toggle_variant_value(scope.variant))
.data_set("size", toggle_size_value(scope.size))
.data_set("spacing", "\{scope.spacing}")
.data_set("value", value)
.data_set("state", if pressed { "on" } else { "off" })
let tab_stop = !disabled &&
!scope.tab_claimed.val &&
(scope.values.length() == 0 || pressed)
if tab_stop {
scope.tab_claimed.val = true
}
ignore(item_attrs.tabindex(if tab_stop { 0 } else { -1 }))
toggle_group_bind_keyboard(item_attrs)
if aria_label is Some(label) {
ignore(item_attrs.aria_label(label))
}
render_toggle(
pressed~,
variant=scope.variant,
size=scope.size,
disabled~,
slot="toggle-group-item",
id?,
class?,
title?,
on_pressed_change=@cmd.Emit(_ => (scope.emit)(ToggleGroupValue(value))),
attrs=item_attrs,
style=ui_styles(
[
"width:auto;min-width:0;flex-shrink:0;--rui-toggle-padding-inline:0.75rem",
if scope.spacing == 0 {
"--rui-toggle-base-radius:0;--rui-toggle-base-shadow:none"
} else {
""
},
],
style,
),
children,
)
}