1
0
Fork 0
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:
Zach Gollwitzer 2024-07-16 14:08:24 -04:00 committed by GitHub
parent 47523f64c2
commit e51806b98b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 268 additions and 488 deletions

View file

@ -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;
});
}
}

View file

@ -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();
}
}
}