mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 05:09:38 +02:00
* Fix dropdown issues and add dummy transaction category modal * Minor namings tweaks * Add search type * Use new menu controller * Complete basic transaction category inline CRUD actions * Fix lint error --------- Co-authored-by: Jakub Kottnauer <jk@jakubkottnauer.com>
179 lines
5.4 KiB
JavaScript
179 lines
5.4 KiB
JavaScript
import { Controller } from "@hotwired/stimulus";
|
|
|
|
/**
|
|
* A custom "select" element that follows accessibility patterns of a native select element.
|
|
*
|
|
* - If you need to display arbitrary content including non-clickable items, links, buttons, and forms, use the "popover" controller instead.
|
|
*/
|
|
export default class extends Controller {
|
|
static classes = ["active"];
|
|
static targets = ["option", "button", "list", "input", "buttonText"];
|
|
static values = { selected: String };
|
|
|
|
initialize() {
|
|
this.show = false;
|
|
|
|
const selectedElement = this.optionTargets.find(
|
|
(option) => option.dataset.value === this.selectedValue
|
|
);
|
|
if (selectedElement) {
|
|
this.updateAriaAttributesAndClasses(selectedElement);
|
|
this.syncButtonTextWithInput();
|
|
}
|
|
}
|
|
|
|
connect() {
|
|
this.syncButtonTextWithInput();
|
|
if (this.hasButtonTarget) {
|
|
this.buttonTarget.addEventListener("click", this.toggleList);
|
|
}
|
|
this.element.addEventListener("keydown", this.handleKeydown);
|
|
document.addEventListener("click", this.handleOutsideClick);
|
|
this.element.addEventListener("turbo:load", this.handleTurboLoad);
|
|
}
|
|
|
|
disconnect() {
|
|
this.element.removeEventListener("keydown", this.handleKeydown);
|
|
document.removeEventListener("click", this.handleOutsideClick);
|
|
this.element.removeEventListener("turbo:load", this.handleTurboLoad);
|
|
|
|
if (this.hasButtonTarget) {
|
|
this.buttonTarget.removeEventListener("click", this.toggleList);
|
|
}
|
|
}
|
|
|
|
selectedValueChanged() {
|
|
this.syncButtonTextWithInput();
|
|
}
|
|
|
|
handleOutsideClick = (event) => {
|
|
if (this.show && !this.element.contains(event.target)) {
|
|
this.close();
|
|
}
|
|
};
|
|
|
|
handleTurboLoad = () => {
|
|
this.close();
|
|
this.syncButtonTextWithInput();
|
|
};
|
|
|
|
handleKeydown = (event) => {
|
|
switch (event.key) {
|
|
case " ":
|
|
case "Enter":
|
|
event.preventDefault(); // Prevent the default action to avoid scrolling
|
|
if (
|
|
this.hasButtonTarget &&
|
|
document.activeElement === this.buttonTarget
|
|
) {
|
|
this.toggleList();
|
|
} else {
|
|
this.selectOption(event);
|
|
}
|
|
break;
|
|
case "ArrowDown":
|
|
event.preventDefault(); // Prevent the default action to avoid scrolling
|
|
this.focusNextOption();
|
|
break;
|
|
case "ArrowUp":
|
|
event.preventDefault(); // Prevent the default action to avoid scrolling
|
|
this.focusPreviousOption();
|
|
break;
|
|
case "Escape":
|
|
this.close();
|
|
if (this.hasButtonTarget) {
|
|
this.buttonTarget.focus(); // Bring focus back to the button
|
|
}
|
|
break;
|
|
case "Tab":
|
|
this.close();
|
|
break;
|
|
}
|
|
};
|
|
|
|
focusNextOption() {
|
|
this.focusOptionInDirection(1);
|
|
}
|
|
|
|
focusPreviousOption() {
|
|
this.focusOptionInDirection(-1);
|
|
}
|
|
|
|
focusOptionInDirection(direction) {
|
|
const currentFocusedIndex = this.optionTargets.findIndex(
|
|
(option) => option === document.activeElement
|
|
);
|
|
const optionsCount = this.optionTargets.length;
|
|
const nextIndex =
|
|
(currentFocusedIndex + direction + optionsCount) % optionsCount;
|
|
this.optionTargets[nextIndex].focus();
|
|
}
|
|
|
|
toggleList = () => {
|
|
if (!this.hasButtonTarget) return; // Ensure button target is present before toggling
|
|
|
|
this.show = !this.show;
|
|
this.listTarget.classList.toggle("hidden", !this.show);
|
|
this.buttonTarget.setAttribute("aria-expanded", this.show.toString());
|
|
|
|
if (this.show) {
|
|
// Focus the first option or the selected option when the list is shown
|
|
const selectedOption = this.optionTargets.find(
|
|
(option) => option.getAttribute("aria-selected") === "true"
|
|
);
|
|
(selectedOption || this.optionTargets[0]).focus();
|
|
}
|
|
};
|
|
|
|
close() {
|
|
if (this.hasButtonTarget) {
|
|
this.show = false;
|
|
this.listTarget.classList.add("hidden");
|
|
this.buttonTarget.setAttribute("aria-expanded", "false");
|
|
}
|
|
}
|
|
|
|
selectOption(event) {
|
|
const selectedOption =
|
|
event.type === "keydown" ? document.activeElement : event.currentTarget;
|
|
this.updateAriaAttributesAndClasses(selectedOption);
|
|
if (this.inputTarget.value !== selectedOption.getAttribute("data-value")) {
|
|
this.updateInputValueAndEmitEvent(selectedOption);
|
|
}
|
|
this.close(); // Close the list after selection
|
|
}
|
|
|
|
updateAriaAttributesAndClasses(selectedOption) {
|
|
this.optionTargets.forEach((option) => {
|
|
option.setAttribute("aria-selected", "false");
|
|
option.setAttribute("tabindex", "-1");
|
|
option.classList.remove(...this.activeClasses);
|
|
});
|
|
selectedOption.classList.add(...this.activeClasses);
|
|
selectedOption.setAttribute("aria-selected", "true");
|
|
selectedOption.focus();
|
|
}
|
|
|
|
updateInputValueAndEmitEvent(selectedOption) {
|
|
// Update the hidden input's value
|
|
const selectedValue = selectedOption.getAttribute("data-value");
|
|
this.inputTarget.value = selectedValue;
|
|
this.syncButtonTextWithInput();
|
|
|
|
// Emit an input event for auto-submit functionality
|
|
const inputEvent = new Event("input", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
});
|
|
this.inputTarget.dispatchEvent(inputEvent);
|
|
}
|
|
|
|
syncButtonTextWithInput() {
|
|
const matchingOption = this.optionTargets.find(
|
|
(option) => option.getAttribute("data-value") === this.inputTarget.value
|
|
);
|
|
if (matchingOption && this.hasButtonTextTarget) {
|
|
this.buttonTextTarget.textContent = matchingOption.textContent.trim();
|
|
}
|
|
}
|
|
}
|