Initial Commit
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
// 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;
|
||||
Reference in New Issue
Block a user