Created
October 10, 2022 18:50
-
-
Save ryanflorence/071da98fdff24a044a611c838e78858e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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