UI styling, main workspace layout (#119)

This commit is contained in:
Danny McGee
2024-04-05 20:15:09 -04:00
committed by GitHub
parent c5a0250d9f
commit bfc638b644
57 changed files with 2004 additions and 694 deletions
+1
View File
@@ -1,3 +1,4 @@
export * from "./decorators";
export * from "./events";
export * from "./observe";
export * from "./types";
+79
View File
@@ -0,0 +1,79 @@
import { LitElement, PropertyDeclaration, PropertyValues, html, render } from "lit";
import { customElement, state } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js";
import { inject, provide } from "./decorators";
import { observe } from "./observe";
import { DynamicProvider, UniqueToken } from "./types";
interface CountProvider extends DynamicProvider<CountProvider> {
readonly count: number;
}
const COUNT_PROVIDER = UniqueToken.create<CountProvider>();
@customElement("test-count-provider")
@provide(COUNT_PROVIDER)
class TestCountProvider extends LitElement implements CountProvider {
@state() count = 0;
protected override update(changes: PropertyValues<this>): void {
if (changes.has("count")) {
const downstreamChanges: PropertyValues<CountProvider> = new Map();
downstreamChanges.set("count", changes.get("count"));
this.dispatchEvent(new CustomEvent("property-changes", {
detail: downstreamChanges,
}));
}
super.update(changes);
}
protected override render = () => html`
<slot></slot>
`;
}
@customElement("test-count-dependent")
class TestCountDependent extends LitElement {
@observe(["count"])
@inject(COUNT_PROVIDER)
countProvider!: CountProvider;
override requestUpdate(
name?: PropertyKey | undefined,
oldValue?: unknown,
options?: PropertyDeclaration<unknown, unknown> | undefined
): void {
super.requestUpdate(name, oldValue, options);
}
protected override update(changes: PropertyValues<this>): void {
this.setAttribute("count", this.countProvider?.count?.toString() ?? null);
super.update(changes);
}
}
describe("@observe", () => {
it("should work", async () => {
const providerRef = createRef<TestCountProvider>();
const dependentRef = createRef<TestCountDependent>();
render(html`
<test-count-provider ${ref(providerRef)}>
<test-count-dependent ${ref(dependentRef)}></test-count-dependent>
</test-count-provider>
`, document.body);
expect(providerRef.value).toBeDefined();
expect(dependentRef.value).toBeDefined();
while (!(await dependentRef.value!.updateComplete));
expect(dependentRef.value!.getAttribute("count")).toEqual("0");
providerRef.value!.count = 42;
while (!(await providerRef.value!.updateComplete));
while (!(await dependentRef.value!.updateComplete));
expect(dependentRef.value!.getAttribute("count")).toEqual("42");
});
});
+80
View File
@@ -0,0 +1,80 @@
import { LitElement, PropertyValues } from "lit";
import { appendRoutine, prependRoutine } from "../decorators/internal";
import { DynamicProvider } from "./types";
/**
* Observe the given property names of the decorated {@linkcode DynamicProvider},
* calling `requestUpdate` to re-render the component when one of those
* properties is updated in the provider.
*
* @example
* ```ts
* const COUNT_PROVIDER = UniqueToken.create<CountProvider>();
*
* interface CountProvider extends DynamicProvider<CountProvider> {
* readonly count: number;
* }
*
* \@customElement("sts-count-observer")
* export class CountObserverElement extends LitElement {
* \@observe(["count"])
* \@inject(COUNT_PROVIDER)
* _countProvider!: CountProvider;
*
* protected override render = () => html`
* <!--
* This component should correctly update whenever the injected
* `CountProvider`'s `count` property changes.
* -->
* The count is: ${this._countProvider.count}
* `;
* }
* ```
*/
export function observe<
T extends LitElement,
K extends keyof T,
P extends (T[K] & object),
>(observedProps: (keyof P)[])
: T[K] extends DynamicProvider<T[K]>
? ((proto: T, propName: K) => void)
: never
{
return ((proto: T, propName: K) => {
function onProviderChanges(
this: T,
{ detail: changes }: CustomEvent<PropertyValues<P>>,
): void {
// FIXME: Holy shit TypeScript, gg
if (observedProps.some(prop => changes.has(prop as never)))
this.requestUpdate();
}
const $onProviderChanges = Symbol("onProviderChanges");
type Decorated = T & {
[$onProviderChanges]: typeof onProviderChanges;
}
appendRoutine(proto, "connectedCallback", function (this: T) {
assertType<Decorated>(this);
this[$onProviderChanges] ??= onProviderChanges.bind(this);
const provider = this[propName];
assertType<DynamicProvider<P>>(provider);
provider?.addEventListener?.("property-changes", this[$onProviderChanges]);
});
prependRoutine(proto, "disconnectedCallback", function (this: T) {
assertType<Decorated>(this);
const provider = this[propName];
assertType<DynamicProvider<P>>(provider);
provider?.removeEventListener?.("property-changes", this[$onProviderChanges]);
});
}) as any
}
function assertType<T>(value: unknown): asserts value is T {}