mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-21 14:19:39 +02:00
More composable forms (#989)
* Make forms more composable, opt-in to form builder * Remove unused method * Simpler money input controls * Add in new form styling to imports * Lint fixes * Small tweak of multi select styles
This commit is contained in:
parent
47523f64c2
commit
e51806b98b
50 changed files with 268 additions and 488 deletions
|
@ -4,17 +4,23 @@ import { CurrenciesService } from "services/currencies_service";
|
|||
// Connects to data-controller="money-field"
|
||||
// when currency select change, update the input value with the correct placeholder and step
|
||||
export default class extends Controller {
|
||||
static targets = ["amount", "currency"];
|
||||
static targets = ["amount", "currency", "symbol"];
|
||||
|
||||
handleCurrencyChange() {
|
||||
const selectedCurrency = event.target.value;
|
||||
handleCurrencyChange(e) {
|
||||
const selectedCurrency = e.target.value;
|
||||
this.updateAmount(selectedCurrency);
|
||||
}
|
||||
|
||||
updateAmount(currency) {
|
||||
(new CurrenciesService).get(currency).then((data) => {
|
||||
this.amountTarget.placeholder = data.placeholder;
|
||||
this.amountTarget.step = data.step;
|
||||
(new CurrenciesService).get(currency).then((currency) => {
|
||||
console.log(currency)
|
||||
this.amountTarget.step = currency.step;
|
||||
|
||||
if (isFinite(this.amountTarget.value)) {
|
||||
this.amountTarget.value = parseFloat(this.amountTarget.value).toFixed(currency.default_precision)
|
||||
}
|
||||
|
||||
this.symbolTarget.innerText = currency.symbol;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,179 +0,0 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue