220 lines
6.0 KiB
JavaScript
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;
|