///|
const CheckboxBaseStyle : String = "display:inline-flex;width:1rem;height:1rem;flex-shrink:0;align-items:center;justify-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:calc(var(--rui-radius,0.625rem) - 0.375rem);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;user-select:none;appearance:none;-webkit-appearance:none;vertical-align:middle;outline-offset:2px"
///|
const CheckboxCheckedStyle : String = "--rui-control-base-border:var(--rui-primary,oklch(0.205 0 0));--rui-control-base-bg:var(--rui-primary,oklch(0.205 0 0))"
///|
const CheckboxUncheckedStyle : String = "--rui-control-base-border:var(--rui-input,oklch(0.922 0 0));--rui-control-base-bg:var(--rui-input-background,transparent)"
///|
const CheckboxInvalidStyle : 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))"
///|
const CheckboxReadOnlyStyle : String = "cursor:default"
///|
const CheckboxIndicatorStyle : String = "display:block;box-sizing:border-box;flex-shrink:0;pointer-events:none;transition:opacity 100ms ease,transform 100ms ease"
///|
fn checkbox_state(checked : Bool, indeterminate : Bool) -> (String, String) {
if indeterminate {
("mixed", "indeterminate")
} else if checked {
("true", "checked")
} else {
("false", "unchecked")
}
}
///|
fn checkbox_state_style(checked : Bool, indeterminate : Bool) -> String {
if checked || indeterminate {
CheckboxCheckedStyle
} else {
CheckboxUncheckedStyle
}
}
///|
fn checkbox_read_only_style(read_only : Bool) -> String {
if read_only {
CheckboxReadOnlyStyle
} else {
""
}
}
///|
fn checkbox_invalid_style(invalid : Bool) -> String {
if invalid {
CheckboxInvalidStyle
} else {
""
}
}
///|
fn checkbox_indicator_state_style(
checked : Bool,
indeterminate : Bool,
) -> String {
if indeterminate {
"width:0.5rem;height:0.125rem;border-radius:9999px;background:currentColor;opacity:1;transform:scale(1)"
} else if checked {
"width:0.375rem;height:0.625rem;border-right:0.125rem solid currentColor;border-bottom:0.125rem solid currentColor;opacity:1;transform:translateY(-0.0625rem) rotate(45deg)"
} else {
"width:0.375rem;height:0.625rem;opacity:0;transform:scale(0.75)"
}
}
///|
#cfg(target="js")
fn checkbox_bind_keyboard_attrs(attrs : @html.Attrs) -> Unit {
ignore(
attrs.on_keydown(event => {
if event.key() == "Enter" {
event.prevent_default()
}
@cmd.none
}),
)
}
///|
#cfg(not(target="js"))
fn checkbox_bind_keyboard_attrs(attrs : @html.Attrs) -> Unit {
ignore(attrs)
}
///|
/// Render one checkbox state for the incremental wrapper.
///
/// Mouse activation and Space request the next value through
/// `on_checked_change`. Enter's native button activation is cancelled so it
/// intentionally does not change this checkbox.
/// The component is not a form control and does not create a hidden input.
fn render_checkbox(
checked? : Bool = false,
indeterminate? : Bool = false,
disabled? : Bool = false,
read_only? : Bool = false,
invalid? : Bool = false,
id? : String,
class? : String,
title? : String,
aria_label? : String,
on_checked_change? : @cmd.Emit[Bool],
attrs? : @html.Attrs,
style? : Array[String] = [],
) -> @html.Html {
let (aria_checked, data_state) = checkbox_state(checked, indeterminate)
let next_checked = !checked
let control_attrs = ui_attrs(attrs).data_set("slot", "checkbox")
ignore(control_attrs.role("checkbox"))
ignore(control_attrs.aria_checked(aria_checked))
ignore(control_attrs.aria_disabled(ui_bool(disabled)))
ignore(control_attrs.aria_readonly(ui_bool(read_only)))
ignore(control_attrs.data_set("state", data_state))
if indeterminate {
ignore(control_attrs.data_set("indeterminate", ""))
} else if checked {
ignore(control_attrs.data_set("checked", ""))
} else {
ignore(control_attrs.data_set("unchecked", ""))
}
if disabled {
ignore(control_attrs.data_set("disabled", ""))
}
if read_only {
ignore(control_attrs.data_set("readonly", ""))
}
if invalid {
ignore(control_attrs.aria_invalid("true").data_set("invalid", ""))
}
if aria_label is Some(label) {
ignore(control_attrs.aria_label(label))
}
checkbox_bind_keyboard_attrs(control_attrs)
if !disabled && !read_only && on_checked_change is Some(emit) {
ignore(control_attrs.on_click(_ => emit(next_checked)))
}
let indicator_attrs = @html.Attrs::build()
.data_set("slot", "checkbox-indicator")
.data_set("state", data_state)
if indeterminate {
ignore(indicator_attrs.data_set("indeterminate", ""))
} else if checked {
ignore(indicator_attrs.data_set("checked", ""))
} else {
ignore(indicator_attrs.data_set("unchecked", ""))
}
ignore(indicator_attrs.aria_hidden("true"))
let indicator = @html.span(
style=[
UiBoxSizing,
CheckboxIndicatorStyle,
checkbox_indicator_state_style(checked, indeterminate),
],
attrs=indicator_attrs,
"",
)
@html.button(
style=ui_styles(
[
UiBoxSizing,
UiFontSans,
UiTextRendering,
UiTransition,
CheckboxBaseStyle,
checkbox_state_style(checked, indeterminate),
checkbox_read_only_style(read_only),
checkbox_invalid_style(invalid),
ui_disabled_visual_style(disabled),
],
style,
),
id?,
class?,
title?,
type_="button",
disabled~,
attrs=control_attrs,
indicator,
)
}
///|
priv struct CheckboxModel {
checked : Bool
indeterminate : Bool
} derive(Eq)
///|
priv enum CheckboxMsg {
CheckboxChange(Bool)
}
///|
/// Build a black-box checkbox with component-local incremental state.
#cfg(target="js")
pub fn checkbox(
default_checked? : Bool = false,
default_indeterminate? : Bool = false,
disabled? : Bool = false,
read_only? : Bool = false,
invalid? : Bool = false,
id? : String,
class? : String,
title? : String,
aria_label? : String,
on_checked_change? : @cmd.Emit[Bool],
attrs? : @html.Attrs,
style? : Array[String] = [],
) -> @rabbita.Val[@html.Html] {
let initial = {
checked: default_checked,
indeterminate: default_indeterminate,
}
let (model, emit) = @rabbita.create_state(initial, update=fn(_, msg, _) {
let next = match msg {
CheckboxChange(checked) => { checked, indeterminate: false }
}
let command = if on_checked_change is Some(notify) {
notify(next.checked)
} else {
@cmd.none
}
(next, command)
})
model.view(model => {
render_checkbox(
checked=model.checked,
indeterminate=model.indeterminate,
disabled~,
read_only~,
invalid~,
id?,
class?,
title?,
aria_label?,
on_checked_change=@cmd.Emit(checked => emit(CheckboxChange(checked))),
attrs?,
style~,
)
})
}
///|
/// Native/SSR fallback for `checkbox`: render the initial state as a constant
/// incremental value.
#cfg(not(target="js"))
pub fn checkbox(
default_checked? : Bool = false,
default_indeterminate? : Bool = false,
disabled? : Bool = false,
read_only? : Bool = false,
invalid? : Bool = false,
id? : String,
class? : String,
title? : String,
aria_label? : String,
on_checked_change? : @cmd.Emit[Bool],
attrs? : @html.Attrs,
style? : Array[String] = [],
) -> @rabbita.Val[@html.Html] {
ignore(on_checked_change)
@rabbita.Val::constant(
render_checkbox(
checked=default_checked,
indeterminate=default_indeterminate,
disabled~,
read_only~,
invalid~,
id?,
class?,
title?,
aria_label?,
attrs?,
style~,
),
)
}