///|
/// APG Switch Pattern
/// https://www.w3.org/WAI/ARIA/apg/patterns/switch/
///
/// A switch is an input widget for binary "on" or "off" values.
/// Similar to checkbox but semantically different (on/off vs checked/unchecked).
///
/// Important: The label should NOT change when state changes.
///
/// Keyboard Interaction:
/// - Space: Toggle the switch state
/// - Enter: Toggle the switch state (optional)
///
/// ARIA:
/// - role="switch"
/// - aria-checked: "true" (on) / "false" (off)
///|
/// Create an accessible switch.
///
/// Parameters:
/// - on: Whether the switch is on
/// - disabled: Whether disabled
/// - aria_label: Accessible label (should NOT change with state)
/// - aria_labelledby: ID of labelling element
/// - on_click: Click handler
/// - on_keydown: Keydown handler (for Space/Enter toggle)
/// - children: Switch label content
pub fn[E] switch(
on : Bool,
disabled? : Bool,
aria_label? : String,
aria_labelledby? : String,
on_click? : @luna.EventHandler[E],
on_keydown? : @luna.EventHandler[E],
children : Array[@luna.Node[E, String]],
) -> @luna.Node[E, String] {
let attrs : Array[(String, @luna.Attr[E, String])] = [
("role", @luna.attr_static("switch")),
("aria-checked", @luna.attr_static(if on { "true" } else { "false" })),
("tabindex", @luna.attr_static("0")),
]
if disabled is Some(true) {
attrs.push(("aria-disabled", @luna.attr_static("true")))
}
if aria_label is Some(label) {
attrs.push(("aria-label", @luna.attr_static(label)))
}
if aria_labelledby is Some(id) {
attrs.push(("aria-labelledby", @luna.attr_static(id)))
}
if on_click is Some(handler) {
attrs.push(("click", @luna.attr_handler(handler)))
}
if on_keydown is Some(handler) {
attrs.push(("keydown", @luna.attr_handler(handler)))
}
// Build switch with track, thumb, icon structure
let track = @luna.h("span", [("class", @luna.attr_static("switch-track"))]) <| [
@luna.h("span", [("class", @luna.attr_static("switch-icon"))]) <| [
@luna.text("✓"),
],
@luna.h("span", [("class", @luna.attr_static("switch-thumb"))]) <| [],
]
let label = @luna.h("span", [("class", @luna.attr_static("switch-label"))]) <| children
@luna.h("button", attrs) <| [
track, label,
]
}
///|
/// Create a dynamic switch with signal-based state.
pub fn[E] switch_dyn(
on : @resource.Signal[Bool],
disabled? : Bool,
aria_label? : String,
on_click? : @luna.EventHandler[E],
on_keydown? : @luna.EventHandler[E],
children : Array[@luna.Node[E, String]],
) -> @luna.Node[E, String] {
let attrs : Array[(String, @luna.Attr[E, String])] = [
("role", @luna.attr_static("switch")),
(
"aria-checked",
@luna.attr_dynamic(fn() { if on.get() { "true" } else { "false" } }),
),
("tabindex", @luna.attr_static("0")),
]
if disabled is Some(true) {
attrs.push(("aria-disabled", @luna.attr_static("true")))
}
if aria_label is Some(label) {
attrs.push(("aria-label", @luna.attr_static(label)))
}
if on_click is Some(handler) {
attrs.push(("click", @luna.attr_handler(handler)))
}
if on_keydown is Some(handler) {
attrs.push(("keydown", @luna.attr_handler(handler)))
}
// Build switch with track, thumb, icon structure
let track = @luna.h("span", [("class", @luna.attr_static("switch-track"))]) <| [
@luna.h("span", [("class", @luna.attr_static("switch-icon"))]) <| [
@luna.text("✓"),
],
@luna.h("span", [("class", @luna.attr_static("switch-thumb"))]) <| [],
]
let label = @luna.h("span", [("class", @luna.attr_static("switch-label"))]) <| children
@luna.h("button", attrs) <| [
track, label,
]
}
///|
/// Create a switch with computed state (accepts getter function).
/// Useful for derived switch states.
pub fn[E] switch_computed(
on : () -> Bool,
disabled? : Bool,
aria_label? : String,
on_click? : @luna.EventHandler[E],
on_keydown? : @luna.EventHandler[E],
children : Array[@luna.Node[E, String]],
) -> @luna.Node[E, String] {
let attrs : Array[(String, @luna.Attr[E, String])] = [
("role", @luna.attr_static("switch")),
("aria-checked", @luna.attr_dynamic(fn() { bool_to_aria(on()) })),
("tabindex", @luna.attr_static("0")),
]
if disabled is Some(true) {
attrs.push(("aria-disabled", @luna.attr_static("true")))
}
if aria_label is Some(label) {
attrs.push(("aria-label", @luna.attr_static(label)))
}
if on_click is Some(handler) {
attrs.push(("click", @luna.attr_handler(handler)))
}
if on_keydown is Some(handler) {
attrs.push(("keydown", @luna.attr_handler(handler)))
}
// Build switch with track, thumb, icon structure
let track = @luna.h("span", [("class", @luna.attr_static("switch-track"))]) <| [
@luna.h("span", [("class", @luna.attr_static("switch-icon"))]) <| [
@luna.text("✓"),
],
@luna.h("span", [("class", @luna.attr_static("switch-thumb"))]) <| [],
]
let label = @luna.h("span", [("class", @luna.attr_static("switch-label"))]) <| children
@luna.h("button", attrs) <| [
track, label,
]
}
///|
/// Create a switch using native checkbox with role="switch".
pub fn[E] switch_native(
name : String,
on : Bool,
disabled? : Bool,
id? : String,
children : Array[@luna.Node[E, String]],
) -> @luna.Node[E, String] {
let input_id = id.unwrap_or("switch-" + name)
let input_attrs : Array[(String, @luna.Attr[E, String])] = [
("type", @luna.attr_static("checkbox")),
("role", @luna.attr_static("switch")),
("name", @luna.attr_static(name)),
("id", @luna.attr_static(input_id)),
]
if on {
input_attrs.push(("checked", @luna.attr_static("")))
}
if disabled is Some(true) {
input_attrs.push(("disabled", @luna.attr_static("")))
}
let label_attrs : Array[(String, @luna.Attr[E, String])] = [
("for", @luna.attr_static(input_id)),
]
@luna.fragment <| [
@luna.h("input", input_attrs) <| [],
@luna.h("label", label_attrs) <| children,
]
}
///|
/// Create a switch group.
pub fn[E] switch_group(
aria_label? : String,
children : Array[@luna.Node[E, String]],
) -> @luna.Node[E, String] {
let attrs : Array[(String, @luna.Attr[E, String])] = [
("role", @luna.attr_static("group")),
]
if aria_label is Some(label) {
attrs.push(("aria-label", @luna.attr_static(label)))
}
@luna.h("div", attrs) <| children
}
///|
/// Create a keyboard handler for switch that supports Space and Enter.
///
/// APG requires:
/// - Space: Toggle switch state
/// - Enter: Toggle switch state
///
/// Parameters:
/// - on_toggle: Called when switch should toggle
pub fn make_switch_handler(
on_toggle : () -> Unit,
) -> @luna.EventHandler[@js.Any] {
@luna.handler(fn(e : @js.Any) {
let key : String = e._get("key").cast()
match key {
" " | "Enter" => {
let _ = e._call("preventDefault", [])
on_toggle()
}
_ => ()
}
})
}
///|
/// Create a complete interactive switch with built-in keyboard support.
///
/// Features:
/// - Space and Enter toggle state
/// - Click toggles state
/// - Proper ARIA attributes
///
/// Parameters:
/// - on: Signal controlling switch state
/// - disabled: Whether disabled
/// - aria_label: Accessible label
/// - children: Switch label content
pub fn switch_interactive(
on : @resource.Signal[Bool],
disabled? : Bool,
aria_label? : String,
children : Array[@luna.Node[@js.Any, String]],
) -> @luna.Node[@js.Any, String] {
let is_disabled = disabled.unwrap_or(false)
let toggle = fn() { if !is_disabled { on.set(!on.get()) } }
let keydown_handler = make_switch_handler(toggle)
let click_handler = @luna.handler(fn(_) { toggle() })
let attrs : Array[(String, @luna.Attr[@js.Any, String])] = [
("role", @luna.attr_static("switch")),
(
"aria-checked",
@luna.attr_dynamic(fn() { if on.get() { "true" } else { "false" } }),
),
("tabindex", @luna.attr_static("0")),
("keydown", @luna.attr_handler(keydown_handler)),
("click", @luna.attr_handler(click_handler)),
]
if is_disabled {
attrs.push(("aria-disabled", @luna.attr_static("true")))
}
if aria_label is Some(label) {
attrs.push(("aria-label", @luna.attr_static(label)))
}
// Build switch with track, thumb, icon structure
let track = @luna.h("span", [("class", @luna.attr_static("switch-track"))]) <| [
@luna.h("span", [("class", @luna.attr_static("switch-icon"))]) <| [
@luna.text("✓"),
],
@luna.h("span", [("class", @luna.attr_static("switch-thumb"))]) <| [],
]
let label = @luna.h("span", [("class", @luna.attr_static("switch-label"))]) <| children
@luna.h("button", attrs) <| [
track, label,
]
}