Files
Rivertalk/assets/js/ui/components/radio_group.js
T
2026-02-12 17:10:03 -06:00

220 lines
6.0 KiB
JavaScript

// saladui/components/radio_group.js
import Component from "../core/component";
import SaladUI from "../index";
import Collection from "../core/collection";
class RadioGroupComponent extends Component {
constructor(el, hookContext) {
super(el, { hookContext, ignoreItems: false });
// Initialize properties
this.items = this.getAllParts("item");
// Set keyboard navigation defaults
this.config.preventDefaultKeys = [
"ArrowLeft",
"ArrowRight",
"ArrowUp",
"ArrowDown",
"Home",
"End",
];
// Initialize collection manager for radio items
this.initializeCollection();
}
getComponentConfig() {
return {
stateMachine: {
idle: {
enter: "onIdleEnter",
transitions: {
valueChanged: "idle",
},
},
},
events: {
idle: {
keyMap: {
ArrowLeft: () => this.navigateItem("prev"),
ArrowRight: () => this.navigateItem("next"),
ArrowUp: () => this.navigateItem("prev"),
ArrowDown: () => this.navigateItem("next"),
Home: () => this.navigateItem("first"),
End: () => this.navigateItem("last"),
},
mouseMap: {
item: {
click: "handleItemClick",
},
},
},
},
ariaConfig: {
root: {
all: {
role: "radiogroup",
},
},
item: {
all: {
role: "radio",
},
},
},
};
}
initializeCollection() {
this.collection = new Collection({
type: "single",
defaultValue: this.options.initialValue,
getItemValue: (item) => item.getAttribute("data-value"),
isItemDisabled: (item) => item.getAttribute("data-disabled") === "true",
});
// Register items with the collection
this.items.forEach((item) => {
this.collection.add(item);
// Set initial ID if not present
if (!item.id) {
const value = item.getAttribute("data-value");
item.id = `${this.el.id}-item-${value}`;
}
});
// Initialize UI state
this.updateItemStates();
}
handleItemClick(event) {
const item = event.currentTarget;
if (item.getAttribute("data-disabled") === "true") return;
this.selectItem(item);
}
selectItem(item) {
const value = item.getAttribute("data-value");
const previousValue = this.collection.getValue();
// Get the collection item
const collectionItem = this.collection.getItemByInstance(item);
// Only proceed if we have a valid item and it's not already selected
if (collectionItem && value !== previousValue) {
// Transition the state machine to apply any state-specific behavior
this.transition("valueChanged", { value, previousValue });
this.collection.select(collectionItem);
this.updateItemStates();
// Emit value changed event
this.pushEvent("value-changed", {
value,
previousValue,
});
}
}
updateItemStates() {
const selectedValue = this.collection.getValue();
// Loop over collection items instead of DOM elements directly
this.collection.items.forEach((collectionItem) => {
const item = collectionItem.instance;
const value = collectionItem.value;
const isSelected = value === selectedValue;
const isDisabled = item.getAttribute("data-disabled") === "true";
// Update visual state
item.setAttribute("data-state", isSelected ? "checked" : "unchecked");
// Update ARIA attributes
item.setAttribute("aria-checked", isSelected.toString());
// Update tabindex for keyboard navigation
item.setAttribute("tabindex", isSelected ? "0" : "-1");
// Update native radio input if present
const input = item.querySelector('input[type="radio"]');
if (input) {
input.checked = isSelected;
input.disabled = isDisabled;
// Ensure name attribute is set for form submission
if (!input.name && this.options.name) {
input.name = this.options.name;
}
}
});
}
navigateItem(direction) {
const currentValue = this.collection.getValue();
const currentItem = this.collection.getItemByValue(currentValue);
// Get next item based on direction
const nextItem = this.collection.getItem(direction, currentItem);
if (!nextItem) return;
// Focus the item
if (typeof nextItem.instance.focus === "function") {
nextItem.instance.focus();
} else if (nextItem.instance) {
// Focus the item element directly
nextItem.instance.focus();
}
// Automatically select the focused item
this.selectItem(nextItem.instance);
}
onIdleEnter() {
// If no item is selected, make first enabled item focusable
if (!this.collection.getValue()) {
const firstItem = this.collection.getItem("first");
if (firstItem && firstItem.instance) {
firstItem.instance.setAttribute("tabindex", "0");
}
}
}
// Handle focus management for the entire group
setupComponentEvents() {
super.setupComponentEvents();
this.el.addEventListener("focus", (e) => {
// Only handle focus if the group itself was focused (not a child)
if (e.target === this.el) {
const selectedValue = this.collection.getValue();
if (selectedValue) {
// Focus the selected item
const selectedItem = this.collection.getItemByValue(selectedValue);
if (selectedItem && selectedItem.instance) {
selectedItem.instance.focus();
}
} else {
// Focus the first enabled item if none is selected
const firstItem = this.collection.getItem("first");
if (firstItem && firstItem.instance) {
firstItem.instance.focus();
}
}
}
});
}
// Clean up when the component is destroyed
beforeDestroy() {
this.collection = null;
}
}
// Register the component
SaladUI.register("radio-group", RadioGroupComponent);
export default RadioGroupComponent;