///|
/// APG Slider Pattern
/// https://www.w3.org/WAI/ARIA/apg/patterns/slider/
///
/// A slider is an input where the user selects a value from within a given range.
///
/// Keyboard Interaction:
/// - Right/Up Arrow: Increase by one step
/// - Left/Down Arrow: Decrease by one step
/// - Home: Set to minimum
/// - End: Set to maximum
/// - Page Up: Increase by large step (optional)
/// - Page Down: Decrease by large step (optional)
///
/// ARIA:
/// - role="slider"
/// - aria-valuenow, aria-valuemin, aria-valuemax
/// - aria-valuetext (optional, for human-readable value)
/// - aria-orientation (default: horizontal)
///|
/// Slider orientation
pub(all) enum SliderOrientation {
Horizontal
Vertical
}
///|
/// Create an accessible slider.
///
/// Parameters:
/// - value: Current value
/// - min: Minimum value (default: 0)
/// - max: Maximum value (default: 100)
/// - step: Step increment (optional, for data attribute)
/// - orientation: Slider orientation
/// - aria_label: Accessible label
/// - aria_labelledby: ID of labelling element
/// - aria_valuetext: Human-readable value
/// - on_keydown: Keydown handler (for arrow key value changes)
/// - children: Visual slider content
pub fn[E] slider(
value : Double,
min? : Double,
max? : Double,
step? : Double,
orientation? : SliderOrientation,
aria_label? : String,
aria_labelledby? : String,
aria_valuetext? : String,
on_keydown? : @luna.EventHandler[E],
children : Array[@luna.Node[E, String]],
) -> @luna.Node[E, String] {
let min_val = min.unwrap_or(0.0)
let max_val = max.unwrap_or(100.0)
let attrs : Array[(String, @luna.Attr[E, String])] = [
("role", @luna.attr_static("slider")),
("tabindex", @luna.attr_static("0")),
("aria-valuenow", @luna.attr_static(value.to_string())),
("aria-valuemin", @luna.attr_static(min_val.to_string())),
("aria-valuemax", @luna.attr_static(max_val.to_string())),
]
if step is Some(s) {
attrs.push(("data-step", @luna.attr_static(s.to_string())))
}
if orientation is Some(Vertical) {
attrs.push(("aria-orientation", @luna.attr_static("vertical")))
}
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 aria_valuetext is Some(text) {
attrs.push(("aria-valuetext", @luna.attr_static(text)))
}
if on_keydown is Some(handler) {
attrs.push(("keydown", @luna.attr_handler(handler)))
}
@luna.h("div", attrs, children)
}
///|
/// Create a dynamic slider with signal-based value.
pub fn[E] slider_dyn(
value : @resource.Signal[Double],
min? : Double,
max? : Double,
orientation? : SliderOrientation,
aria_label? : String,
aria_valuetext_fn? : (Double) -> String,
on_keydown? : @luna.EventHandler[E],
children : Array[@luna.Node[E, String]],
) -> @luna.Node[E, String] {
let min_val = min.unwrap_or(0.0)
let max_val = max.unwrap_or(100.0)
let attrs : Array[(String, @luna.Attr[E, String])] = [
("role", @luna.attr_static("slider")),
("tabindex", @luna.attr_static("0")),
("aria-valuenow", @luna.attr_dynamic(fn() { value.get().to_string() })),
("aria-valuemin", @luna.attr_static(min_val.to_string())),
("aria-valuemax", @luna.attr_static(max_val.to_string())),
]
if orientation is Some(Vertical) {
attrs.push(("aria-orientation", @luna.attr_static("vertical")))
}
if aria_label is Some(label) {
attrs.push(("aria-label", @luna.attr_static(label)))
}
if aria_valuetext_fn is Some(fn_) {
attrs.push(
("aria-valuetext", @luna.attr_dynamic(fn() { fn_(value.get()) })),
)
}
if on_keydown is Some(handler) {
attrs.push(("keydown", @luna.attr_handler(handler)))
}
@luna.h("div", attrs, children)
}
///|
/// Create a native HTML range input.
pub fn[E] slider_native(
name : String,
value : Double,
min? : Double,
max? : Double,
step? : Double,
aria_label? : String,
id? : String,
) -> @luna.Node[E, String] {
let min_val = min.unwrap_or(0.0)
let max_val = max.unwrap_or(100.0)
let input_id = id.unwrap_or("slider-" + name)
let attrs : Array[(String, @luna.Attr[E, String])] = [
("type", @luna.attr_static("range")),
("name", @luna.attr_static(name)),
("id", @luna.attr_static(input_id)),
("value", @luna.attr_static(value.to_string())),
("min", @luna.attr_static(min_val.to_string())),
("max", @luna.attr_static(max_val.to_string())),
]
if step is Some(s) {
attrs.push(("step", @luna.attr_static(s.to_string())))
}
if aria_label is Some(label) {
attrs.push(("aria-label", @luna.attr_static(label)))
}
@luna.h("input", attrs, [])
}
///|
/// Create a slider with full keyboard and mouse support.
/// Includes arrow key navigation, Home/End for min/max, PageUp/PageDown for large steps.
/// Also supports click-to-position for mouse interaction.
///
/// Parameters:
/// - value: Signal containing the current value
/// - min: Minimum value (default: 0)
/// - max: Maximum value (default: 100)
/// - step: Step increment (default: 1)
/// - large_step: Large step for PageUp/PageDown (default: step * 10)
/// - orientation: Slider orientation
/// - aria_label: Accessible label
/// - aria_valuetext_fn: Function to convert value to human-readable text
/// - children: Visual slider content
pub fn slider_interactive(
value : @resource.Signal[Double],
min? : Double,
max? : Double,
step? : Double,
large_step? : Double,
orientation? : SliderOrientation,
aria_label? : String,
aria_valuetext_fn? : (Double) -> String,
children : Array[@luna.Node[@js.Any, String]],
) -> @luna.Node[@js.Any, String] {
let min_val = min.unwrap_or(0.0)
let max_val = max.unwrap_or(100.0)
let step_val = step.unwrap_or(1.0)
let keydown_handler = make_slider_handler(
fn() { value.get() },
min_val,
max_val,
step_val,
fn(v) { value.set(v) },
large_step?,
)
let drag_handler = make_slider_drag_handler(min_val, max_val, step_val, fn(
v,
) {
value.set(v)
})
let attrs : Array[(String, @luna.Attr[@js.Any, String])] = [
("role", @luna.attr_static("slider")),
("tabindex", @luna.attr_static("0")),
("aria-valuenow", @luna.attr_dynamic(fn() { value.get().to_string() })),
("aria-valuemin", @luna.attr_static(min_val.to_string())),
("aria-valuemax", @luna.attr_static(max_val.to_string())),
("keydown", @luna.attr_handler(keydown_handler)),
("mousedown", @luna.attr_handler(drag_handler)),
(
"style",
@luna.attr_dynamic(fn() {
let v = value.get()
let range = max_val - min_val
let percent = if range > 0.0 {
(v - min_val) / range * 100.0
} else {
0.0
}
"--slider-percent:" + percent.to_string()
}),
),
]
if orientation is Some(Vertical) {
attrs.push(("aria-orientation", @luna.attr_static("vertical")))
}
if aria_label is Some(label) {
attrs.push(("aria-label", @luna.attr_static(label)))
}
if aria_valuetext_fn is Some(fn_) {
attrs.push(
("aria-valuetext", @luna.attr_dynamic(fn() { fn_(value.get()) })),
)
}
@luna.h("div", attrs, children)
}