///|
/// APG Listbox Pattern
/// https://www.w3.org/WAI/ARIA/apg/patterns/listbox/
///
/// A listbox presents a list of options and allows selecting one or more.
///
/// Keyboard Interaction:
/// - Down Arrow: Move focus to next option
/// - Up Arrow: Move focus to previous option
/// - Home: Move focus to first option
/// - End: Move focus to last option
/// - Space: Toggle selection (multi-select)
/// - Type-ahead: Focus option starting with typed characters
///
/// ARIA:
/// - role="listbox" with aria-multiselectable (if multi-select)
/// - role="option" with aria-selected
///|
/// Listbox option data
pub(all) struct ListboxOption {
value : String
label : String
disabled : Bool
}
///|
/// Create a listbox option
pub fn listbox_option(
value : String,
label : String,
disabled? : Bool,
) -> ListboxOption {
{ value, label, disabled: disabled.unwrap_or(false) }
}
///|
/// Create a listbox container.
///
/// Parameters:
/// - aria_label: Accessible label
/// - aria_labelledby: ID of labelling element
/// - multiselectable: Whether multiple options can be selected
/// - on_keydown: Keydown handler (for arrow key navigation, type-ahead)
/// - children: Option elements
pub fn[E] listbox(
aria_label? : String,
aria_labelledby? : String,
multiselectable? : Bool,
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("listbox")),
("tabindex", @luna.attr_static("0")),
]
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 multiselectable is Some(true) {
attrs.push(("aria-multiselectable", @luna.attr_static("true")))
}
if on_keydown is Some(handler) {
attrs.push(("keydown", @luna.attr_handler(handler)))
}
@luna.h("div", attrs, children)
}
///|
/// Create a listbox option.
///
/// Parameters:
/// - value: Option value
/// - selected: Whether selected
/// - disabled: Whether disabled
/// - children: Option content
pub fn[E] option(
value : String,
selected : Bool,
disabled? : Bool,
children : Array[@luna.Node[E, String]],
) -> @luna.Node[E, String] {
let attrs : Array[(String, @luna.Attr[E, String])] = [
("role", @luna.attr_static("option")),
(
"aria-selected",
@luna.attr_static(if selected { "true" } else { "false" }),
),
("data-value", @luna.attr_static(value)),
]
if disabled is Some(true) {
attrs.push(("aria-disabled", @luna.attr_static("true")))
}
@luna.h("div", attrs, children)
}
///|
/// Create a dynamic option with signal-based selection.
pub fn[E] option_dyn(
value : String,
selected_values : @resource.Signal[Array[String]],
disabled? : Bool,
children : Array[@luna.Node[E, String]],
) -> @luna.Node[E, String] {
let attrs : Array[(String, @luna.Attr[E, String])] = [
("role", @luna.attr_static("option")),
(
"aria-selected",
@luna.attr_dynamic(fn() {
if selected_values.get().contains(value) {
"true"
} else {
"false"
}
}),
),
("data-value", @luna.attr_static(value)),
]
if disabled is Some(true) {
attrs.push(("aria-disabled", @luna.attr_static("true")))
}
@luna.h("div", attrs, children)
}
///|
/// Create an option group.
pub fn[E] optgroup(
aria_label : String,
children : Array[@luna.Node[E, String]],
) -> @luna.Node[E, String] {
@luna.h(
"div",
[
("role", @luna.attr_static("group")),
("aria-label", @luna.attr_static(aria_label)),
],
children,
)
}
///|
/// Build a complete listbox from options.
pub fn[E] listbox_from_options(
options : Array[ListboxOption],
selected_value : String,
aria_label? : String,
) -> @luna.Node[E, String] {
let option_nodes : Array[@luna.Node[E, String]] = []
for opt in options {
let is_selected = opt.value == selected_value
option_nodes.push(
option(opt.value, is_selected, disabled=opt.disabled, [
@luna.text(opt.label),
]),
)
}
listbox(aria_label?, option_nodes)
}
///|
/// Build a multi-select listbox.
pub fn[E] listbox_multi(
options : Array[ListboxOption],
selected_values : Array[String],
aria_label? : String,
) -> @luna.Node[E, String] {
let option_nodes : Array[@luna.Node[E, String]] = []
for opt in options {
let is_selected = selected_values.contains(opt.value)
option_nodes.push(
option(opt.value, is_selected, disabled=opt.disabled, [
@luna.text(opt.label),
]),
)
}
listbox(aria_label?, multiselectable=true, option_nodes)
}