mirror of
https://github.com/we-promise/sure.git
synced 2026-04-07 14:31:25 +00:00
132 lines
3.3 KiB
JavaScript
132 lines
3.3 KiB
JavaScript
import {
|
|
autoUpdate,
|
|
computePosition,
|
|
offset,
|
|
shift,
|
|
} from "@floating-ui/dom";
|
|
import { Controller } from "@hotwired/stimulus";
|
|
|
|
/**
|
|
* A "menu" can contain arbitrary content including non-clickable items, links, buttons, and forms.
|
|
*/
|
|
export default class extends Controller {
|
|
static targets = ["button", "content"];
|
|
|
|
static values = {
|
|
show: Boolean,
|
|
placement: { type: String, default: "bottom-end" },
|
|
offset: { type: Number, default: 6 },
|
|
};
|
|
|
|
connect() {
|
|
this.show = this.showValue;
|
|
this.boundUpdate = this.update.bind(this);
|
|
this.addEventListeners();
|
|
this.startAutoUpdate();
|
|
}
|
|
|
|
disconnect() {
|
|
this.removeEventListeners();
|
|
this.stopAutoUpdate();
|
|
this.close();
|
|
}
|
|
|
|
addEventListeners() {
|
|
this.buttonTarget.addEventListener("click", this.toggle);
|
|
this.element.addEventListener("keydown", this.handleKeydown);
|
|
document.addEventListener("click", this.handleOutsideClick);
|
|
document.addEventListener("turbo:load", this.handleTurboLoad);
|
|
}
|
|
|
|
removeEventListeners() {
|
|
this.buttonTarget.removeEventListener("click", this.toggle);
|
|
this.element.removeEventListener("keydown", this.handleKeydown);
|
|
document.removeEventListener("click", this.handleOutsideClick);
|
|
document.removeEventListener("turbo:load", this.handleTurboLoad);
|
|
}
|
|
|
|
handleTurboLoad = () => {
|
|
if (!this.show) this.close();
|
|
};
|
|
|
|
handleOutsideClick = (event) => {
|
|
if (this.show && !this.element.contains(event.target)) this.close();
|
|
};
|
|
|
|
handleKeydown = (event) => {
|
|
if (event.key === "Escape") {
|
|
this.close();
|
|
this.buttonTarget.focus();
|
|
}
|
|
};
|
|
|
|
toggle = () => {
|
|
this.show = !this.show;
|
|
this.contentTarget.classList.toggle("hidden", !this.show);
|
|
if (this.show) {
|
|
this.update();
|
|
this.focusFirstElement();
|
|
}
|
|
};
|
|
|
|
close() {
|
|
this.show = false;
|
|
this.contentTarget.classList.add("hidden");
|
|
}
|
|
|
|
focusFirstElement() {
|
|
const focusableElements =
|
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
const firstFocusableElement =
|
|
this.contentTarget.querySelectorAll(focusableElements)[0];
|
|
if (firstFocusableElement) {
|
|
firstFocusableElement.focus({ preventScroll: true });
|
|
}
|
|
}
|
|
|
|
startAutoUpdate() {
|
|
if (!this._cleanup) {
|
|
this._cleanup = autoUpdate(
|
|
this.buttonTarget,
|
|
this.contentTarget,
|
|
this.boundUpdate,
|
|
);
|
|
}
|
|
}
|
|
|
|
stopAutoUpdate() {
|
|
if (this._cleanup) {
|
|
this._cleanup();
|
|
this._cleanup = null;
|
|
}
|
|
}
|
|
|
|
update() {
|
|
if (!this.buttonTarget || !this.contentTarget) return;
|
|
|
|
const isSmallScreen = !window.matchMedia("(min-width: 768px)").matches;
|
|
|
|
computePosition(this.buttonTarget, this.contentTarget, {
|
|
placement: isSmallScreen ? "bottom" : this.placementValue,
|
|
middleware: [offset(this.offsetValue), shift({ padding: 5 })],
|
|
strategy: "fixed",
|
|
}).then(({ x, y }) => {
|
|
if (isSmallScreen) {
|
|
Object.assign(this.contentTarget.style, {
|
|
position: "fixed",
|
|
left: "0px",
|
|
width: "100vw",
|
|
top: `${y}px`,
|
|
});
|
|
} else {
|
|
Object.assign(this.contentTarget.style, {
|
|
position: "fixed",
|
|
left: `${x}px`,
|
|
top: `${y}px`,
|
|
width: "",
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|