Skip to content

Instantly share code, notes, and snippets.

@ryanflorence
Created October 10, 2022 18:50
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ryanflorence/071da98fdff24a044a611c838e78858e to your computer and use it in GitHub Desktop.
Save ryanflorence/071da98fdff24a044a611c838e78858e to your computer and use it in GitHub Desktop.
// WIP, just finding all the boxes and glue, implementation is woefully incomplete
import type { DOMAttributes } from "react";
import { assign, createMachine, interpret } from "@xstate/fsm";
import invariant from "tiny-invariant";
type CustomElement<T> = Partial<
T & DOMAttributes<T> & { children: any; class: string }
>;
type MachineContext = { input?: HTMLInputElement; button?: HTMLButtonElement };
type MachineEvents =
| { type: "INIT"; input: HTMLInputElement; button: HTMLButtonElement }
| { type: "BUTTON_CLICK" }
| { type: "ESCAPE" }
| { type: "ARROW_DOWN" }
| { type: "ARROW_UP" }
| { type: "OUTER_INTERACTION" };
const machine = createMachine<MachineContext, MachineEvents>({
id: "amalgo-box",
initial: "idle",
states: {
idle: {
on: {
INIT: {
target: "closed",
actions: assign((_, event) => ({
input: event.input,
button: event.button,
})),
},
},
},
closed: {
entry: () => {
document.body.style.overflow = "";
},
on: {
BUTTON_CLICK: "open",
},
},
open: {
entry: ctx => {
requestAnimationFrame(() => {
ctx.input?.focus();
});
document.body.style.overflow = "hidden";
},
on: {
BUTTON_CLICK: "closed",
OUTER_INTERACTION: "closed",
ESCAPE: {
target: "closed",
actions: ctx => {
requestAnimationFrame(() => {
ctx.button?.focus();
});
},
},
},
},
},
});
class AmalgoBox extends HTMLElement {
context = interpret(machine);
connectedCallback() {
const service = (this.context = interpret(machine).start());
const input = this.querySelector("input");
const button = this.querySelector("button");
invariant(input, "need an <input />");
invariant(button, "need an <button />");
service.send({ type: "INIT", input, button });
service.subscribe(state => {
this.setAttribute("state", state.value);
if (state.value === "open") {
document.addEventListener("mousedown", this.outerEvent);
document.addEventListener("touchstart", this.outerEvent);
document.addEventListener("focusin", this.outerEvent);
document.addEventListener("keydown", this.keydownEvent);
} else if (state.value === "closed") {
document.removeEventListener("mousedown", this.outerEvent);
document.removeEventListener("touchstart", this.outerEvent);
document.removeEventListener("focusin", this.outerEvent);
document.removeEventListener("keydown", this.keydownEvent);
}
});
}
keydownEvent = (event: KeyboardEvent) => {
if (event.key === "Escape") {
this.context.send("ESCAPE");
}
};
outerEvent = (event: Event) => {
const interactedWithin =
event.target instanceof Node && this.contains(event.target);
if (!interactedWithin) {
this.context.send("OUTER_INTERACTION");
}
};
}
class AmalgoElement extends HTMLElement {
getContext() {
let parent = this.closest("amalgo-box") as AmalgoBox | undefined;
if (!parent) throw new Error("Must be child of <amalgo-box>");
return parent.context;
}
}
class Button extends AmalgoElement {
connectedCallback() {
let button = this.childNodes[0];
invariant(button instanceof HTMLButtonElement);
button.addEventListener("click", () => {
this.getContext().send("BUTTON_CLICK");
});
}
}
class Input extends AmalgoElement {}
class Popover extends AmalgoElement {
connectedCallback() {
this.getContext().subscribe(state => {
if (state.value === "closed") {
this.hidden = true;
} else if (state.value === "open") {
this.hidden = false;
}
});
}
}
class Menu extends AmalgoElement {}
class Option extends AmalgoElement {}
////////////////////////////////////////////////////////////////////////////////
declare global {
namespace JSX {
interface IntrinsicElements {
["amalgo-box"]: CustomElement<AmalgoBox>;
["amalgo-button"]: CustomElement<Button>;
["amalgo-input"]: CustomElement<Input>;
["amalgo-popover"]: CustomElement<Popover>;
["amalgo-menu"]: CustomElement<Menu>;
["amalgo-option"]: CustomElement<Option>;
}
}
}
window.customElements.define("amalgo-box", AmalgoBox);
window.customElements.define("amalgo-button", Button);
window.customElements.define("amalgo-input", Input);
window.customElements.define("amalgo-popover", Popover);
window.customElements.define("amalgo-menu", Menu);
window.customElements.define("amalgo-option", Option);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment