@bodil/dom 0.1.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/component.d.ts +227 -9
  2. package/dist/component.js +185 -38
  3. package/dist/component.js.map +1 -1
  4. package/dist/css.d.ts +4 -0
  5. package/dist/css.js +4 -0
  6. package/dist/css.js.map +1 -1
  7. package/dist/decorators/attribute.d.ts +15 -15
  8. package/dist/decorators/attribute.js +28 -9
  9. package/dist/decorators/attribute.js.map +1 -1
  10. package/dist/decorators/attribute.test.js +58 -6
  11. package/dist/decorators/attribute.test.js.map +1 -1
  12. package/dist/decorators/connect.d.ts +2 -0
  13. package/dist/decorators/connect.js.map +1 -1
  14. package/dist/decorators/connect.test.js +1 -1
  15. package/dist/decorators/connect.test.js.map +1 -1
  16. package/dist/decorators/reactive.d.ts +1 -1
  17. package/dist/decorators/reactive.js +1 -1
  18. package/dist/decorators/reactive.js.map +1 -1
  19. package/dist/decorators/reactive.test.js +1 -1
  20. package/dist/decorators/reactive.test.js.map +1 -1
  21. package/dist/decorators/require.test.js +1 -1
  22. package/dist/decorators/require.test.js.map +1 -1
  23. package/dist/dom.d.ts +43 -1
  24. package/dist/dom.js +63 -15
  25. package/dist/dom.js.map +1 -1
  26. package/dist/dom.test.d.ts +1 -0
  27. package/dist/dom.test.js +20 -0
  28. package/dist/dom.test.js.map +1 -0
  29. package/dist/emitter.d.ts +8 -0
  30. package/dist/emitter.js.map +1 -1
  31. package/dist/event.d.ts +4 -0
  32. package/dist/event.js.map +1 -1
  33. package/dist/geometry.d.ts +7 -0
  34. package/dist/geometry.js +4 -0
  35. package/dist/geometry.js.map +1 -1
  36. package/dist/index.d.ts +2 -1
  37. package/dist/index.js +2 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/signal.d.ts +12 -3
  40. package/dist/signal.js +7 -1
  41. package/dist/signal.js.map +1 -1
  42. package/dist/signal.test.js +2 -2
  43. package/dist/signal.test.js.map +1 -1
  44. package/dist/test.d.ts +17 -0
  45. package/dist/test.js +34 -0
  46. package/dist/test.js.map +1 -0
  47. package/package.json +27 -19
  48. package/src/component.ts +289 -53
  49. package/src/css.ts +5 -0
  50. package/src/decorators/attribute.test.ts +41 -14
  51. package/src/decorators/attribute.ts +57 -29
  52. package/src/decorators/reactive.test.ts +1 -1
  53. package/src/decorators/reactive.ts +4 -4
  54. package/src/decorators/require.test.ts +1 -1
  55. package/src/dom.test.ts +23 -0
  56. package/src/dom.ts +87 -15
  57. package/src/emitter.ts +8 -0
  58. package/src/event.ts +4 -0
  59. package/src/geometry.ts +8 -0
  60. package/src/index.ts +2 -1
  61. package/src/signal.test.ts +2 -2
  62. package/src/signal.ts +13 -2
  63. package/src/test.ts +38 -0
  64. package/src/decorators/connect.test.ts +0 -119
  65. package/src/decorators/connect.ts +0 -85
package/src/component.ts CHANGED
@@ -1,4 +1,9 @@
1
- import { isIterable, isNullish } from "@bodil/core/assert";
1
+ /**
2
+ * Web component base class.
3
+ * @module
4
+ */
5
+
6
+ import { isNullish, present } from "@bodil/core/assert";
2
7
  import { DisposableContext, toDisposable, type Disposifiable } from "@bodil/core/disposable";
3
8
  import { Signal } from "@bodil/signal";
4
9
  import {
@@ -9,6 +14,8 @@ import {
9
14
  unsafeCSS,
10
15
  type CSSResult,
11
16
  type CSSResultOrNative,
17
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
18
+ type html,
12
19
  type ReactiveController,
13
20
  type ReactiveControllerHost,
14
21
  type RenderOptions,
@@ -22,10 +29,9 @@ import {
22
29
  toAttribute,
23
30
  type AttributeConfig,
24
31
  } from "./decorators/attribute";
25
- import { connectedJobs } from "./decorators/connect";
26
32
  import { reactiveFields, signalForObject } from "./decorators/reactive";
27
33
  import { requiredProperties } from "./decorators/require";
28
- import { childElements } from "./dom";
34
+ import { findDescendants } from "./dom";
29
35
  import { EmitterElement } from "./emitter";
30
36
  import { eventListener } from "./event";
31
37
  import { scheduler } from "./scheduler";
@@ -47,12 +53,6 @@ export {
47
53
  type AttributeGetterSetterOptions,
48
54
  type AttributeType,
49
55
  } from "./decorators/attribute";
50
- export {
51
- connect,
52
- connectEffect,
53
- type ConnectFunction,
54
- type ConnectFunctionReturnValue,
55
- } from "./decorators/connect";
56
56
  export { reactive } from "./decorators/reactive";
57
57
  export { require } from "./decorators/require";
58
58
  export { EmitterElement } from "./emitter";
@@ -67,6 +67,8 @@ export type UpdateConfig = {
67
67
 
68
68
  const finalised = Symbol("finalised");
69
69
 
70
+ export type Disposables = Disposifiable | Iterable<Disposifiable | undefined> | undefined | void;
71
+
70
72
  export type Deps = Array<typeof HTMLElement>;
71
73
 
72
74
  export type CSSStyleSpec = CSSResult | CSSStyleSheet | string;
@@ -77,6 +79,10 @@ function processCSSStyleSpec(spec: CSSStyleSpec) {
77
79
  return getCompatibleStyle(typeof spec === "string" ? unsafeCSS(spec) : spec);
78
80
  }
79
81
 
82
+ /**
83
+ * Error thrown when a {@link Component.render} method causes infinite
84
+ * re-renders.
85
+ */
80
86
  export class ComponentUpdateLoopError extends Error {
81
87
  constructor(message?: string, options?: ErrorOptions) {
82
88
  super(message, options);
@@ -84,14 +90,49 @@ export class ComponentUpdateLoopError extends Error {
84
90
  }
85
91
  }
86
92
 
93
+ /**
94
+ * Base class for web components.
95
+ */
87
96
  export abstract class Component
88
97
  extends EmitterElement
89
98
  implements ReactiveControllerHost, Disposable
90
99
  {
100
+ /**
101
+ * Declare the web components this element will be using.
102
+ *
103
+ * This is a convenient place to make sure these web components will be
104
+ * defined when your component is rendered. It has no effect otherwise.
105
+ *
106
+ * @example
107
+ * class MyComponent extends Component {
108
+ * static deps: Deps = [ MySubcomponent, MyOtherSubcomponent ];
109
+ * }
110
+ */
91
111
  static deps: Deps = [];
112
+
113
+ /**
114
+ * Declare CSS style sheets which will be installed for this web component.
115
+ *
116
+ * Style sheets can be either {@link CSSStyleSheet} objects,
117
+ * Lit template {@link CSSResult}s, or strings.
118
+ *
119
+ * If a superclass also declares the `styles` property, this list will be
120
+ * appended to the superclass's list.
121
+ *
122
+ * @example
123
+ * class MyComponent extends Component {
124
+ * static styles = css`
125
+ * :host {
126
+ * border: 2px solid red;
127
+ * }
128
+ * `;
129
+ * }
130
+ */
92
131
  static styles?: CSSStyleSpecDeclaration;
132
+
93
133
  static shadowRootOptions: ShadowRootInit = { mode: "open" };
94
- static initialisers = new Set<() => void>();
134
+
135
+ private static initialisers = new Set<() => void>();
95
136
 
96
137
  /** @ignore */
97
138
  protected static attributeConfig = new Map<string, AttributeConfig>();
@@ -106,6 +147,7 @@ export abstract class Component
106
147
 
107
148
  readonly #controllers = new Set<ReactiveController>();
108
149
  readonly #connectedContext = new DisposableContext();
150
+ readonly #finalCleanupContext = new DisposableContext();
109
151
  #dirty = false;
110
152
  #isUpdatePending = false;
111
153
  #updateSignal?: Signal.Computed<void>;
@@ -120,29 +162,59 @@ export abstract class Component
120
162
 
121
163
  readonly renderRoot: ShadowRoot | HTMLElement = this.createRenderRoot();
122
164
 
165
+ /**
166
+ * True if this component had scheduled an update which has not yet started.
167
+ */
123
168
  get isUpdatePending(): boolean {
124
169
  return this.#isUpdatePending;
125
170
  }
171
+
126
172
  get updateComplete(): Promise<boolean> {
127
173
  return this.#updateResult.promise;
128
174
  }
175
+
176
+ /**
177
+ * A {@link Promise} which resolves when this component has completed its
178
+ * first update and considers itself stabilised, according to the
179
+ * {@link Component.stabilise} method.
180
+ *
181
+ * By default, a component considers itself stabilised when all of its
182
+ * children which are also {@link Component}s are reporting as stabilised.
183
+ */
129
184
  get hasStabilised(): Promise<void> {
130
185
  return this.#stabilised.promise;
131
186
  }
132
187
 
133
- #abortController = new AbortController();
188
+ /**
189
+ * An {@link AbortSignal} which will trigger when this component is
190
+ * disconnected from the DOM.
191
+ *
192
+ * This can be passed along to asynchronous tasks such as {@link fetch}
193
+ * initiated by this component, which will then be aborted automatically if
194
+ * the component is removed from the DOM.
195
+ */
134
196
  get abortSignal(): AbortSignal {
135
197
  return this.#abortController.signal;
136
198
  }
199
+ #abortController = new AbortController();
137
200
 
138
- static addInitialiser(init: () => void) {
201
+ /**
202
+ * Register a function which will be executed whenever an instance of this
203
+ * class is constructed.
204
+ */
205
+ static addInitialiser(init: (this: Component) => void) {
139
206
  if (!Object.hasOwn(this, "initialisers")) {
140
207
  this.initialisers = new Set();
141
208
  }
142
209
  this.initialisers.add(init);
143
210
  }
144
211
 
145
- static get observedAttributes(): Array<string> {
212
+ /**
213
+ * A list of the attributes this element has declared.
214
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#responding_to_attribute_changes
215
+ * @internal
216
+ */
217
+ static get observedAttributes(): ReadonlyArray<string> {
146
218
  this.finalise();
147
219
  return this.attributeConfig.keys().toArray();
148
220
  }
@@ -181,6 +253,7 @@ export abstract class Component
181
253
  }
182
254
  }
183
255
 
256
+ /** @ignore */
184
257
  constructor() {
185
258
  super();
186
259
 
@@ -190,7 +263,7 @@ export abstract class Component
190
263
  const fields = this.constructor[Symbol.metadata]?.[reactiveFields] ?? [];
191
264
  for (const field of fields as Iterable<string | symbol>) {
192
265
  const sig = signalForObject(this, field, () =>
193
- Signal(undefined, { equals: Object.is }),
266
+ Signal.from(undefined, { equals: Object.is }),
194
267
  ) as Signal.State<unknown>;
195
268
  Object.defineProperty(this, field, {
196
269
  get() {
@@ -230,20 +303,12 @@ export abstract class Component
230
303
  }
231
304
  }
232
305
 
233
- protected connectedCallback() {
306
+ private connectedCallback() {
234
307
  this.#connectedContext.dispose();
235
308
  this.#controllers.forEach((c) => c.hostConnected?.());
236
309
  this.#rootPart?.setConnected(true);
237
310
 
238
- for (const job of connectedJobs(this)) {
239
- const result = job.call(this);
240
- const items = isNullish(result)
241
- ? Iterator.from<Disposifiable>([])
242
- : isIterable(result)
243
- ? Iterator.from(result)
244
- : Iterator.from([result]);
245
- items.filter((i) => i !== undefined).forEach(this.useWhileConnected.bind(this));
246
- }
311
+ this.connected();
247
312
 
248
313
  this.useWhileConnected(
249
314
  Signal.effect(() => {
@@ -261,12 +326,13 @@ export abstract class Component
261
326
  this.requestUpdate();
262
327
  }
263
328
 
264
- protected disconnectedCallback() {
329
+ private disconnectedCallback() {
265
330
  if (this.#updateSignal !== undefined) {
266
331
  this.#updateSignalWatcher.unwatch(this.#updateSignal);
267
332
  this.#updateSignal = undefined;
268
333
  }
269
334
  this.#isUpdatePending = false;
335
+ this.disconnected();
270
336
  this.#abortController.abort(
271
337
  new DOMException(`${this.tagName} element disconnected`, "AbortError"),
272
338
  );
@@ -276,6 +342,9 @@ export abstract class Component
276
342
  this.#connectedContext.dispose();
277
343
  }
278
344
 
345
+ /**
346
+ * @internal
347
+ */
279
348
  setAttributeQuietly(name: string, value: string | null) {
280
349
  this.#ignoreAttributeUpdates++;
281
350
  if (value === null) {
@@ -286,6 +355,9 @@ export abstract class Component
286
355
  this.#ignoreAttributeUpdates--;
287
356
  }
288
357
 
358
+ /**
359
+ * @internal
360
+ */
289
361
  protected attributeChangedCallback(
290
362
  name: string,
291
363
  old: string | null,
@@ -297,15 +369,23 @@ export abstract class Component
297
369
  const attrConfig = (this.constructor as typeof Component).attributeConfig.get(name);
298
370
  if (attrConfig !== undefined) {
299
371
  const value = fromAttribute(valueString, attrConfig.type);
300
- this[attrConfig.property as keyof this] = value as any;
372
+ try {
373
+ this[attrConfig.property as keyof this] = value as any;
374
+ // Silently swallow any write errors.
375
+ } catch (_e) {}
301
376
  }
302
377
  }
303
378
 
304
- $signal<K extends keyof this & string, V extends this[K]>(prop: K): Signal<V> {
379
+ /**
380
+ * Typedoc does *not* like this type signature. TODO figure out how to
381
+ * document it properly.
382
+ * @ignore
383
+ */
384
+ $signal<K extends keyof this & string, V extends this[K]>(prop: K): Signal.Computed<V> {
305
385
  const _value = this[prop];
306
386
  return signalForObject(this, prop, () => {
307
387
  throw new TypeError(`Object has no reactive property ${JSON.stringify(prop)}`);
308
- }) as Signal<V>;
388
+ }) as Signal.Computed<V>;
309
389
  }
310
390
 
311
391
  addController(controller: ReactiveController) {
@@ -319,6 +399,16 @@ export abstract class Component
319
399
  this.#controllers.delete(controller);
320
400
  }
321
401
 
402
+ /**
403
+ * Create the component's render root.
404
+ *
405
+ * By default, this creates a {@link ShadowRoot} and attaches it to the
406
+ * component.
407
+ *
408
+ * If you don't want to use a shadow DOM, you can override this method to
409
+ * just `return this`, which causes the component's contents to render as
410
+ * direct children of the component itself.
411
+ */
322
412
  protected createRenderRoot(): HTMLElement | ShadowRoot {
323
413
  const renderRoot =
324
414
  this.shadowRoot ??
@@ -328,6 +418,9 @@ export abstract class Component
328
418
  return renderRoot;
329
419
  }
330
420
 
421
+ /**
422
+ * Ask this component to update itself.
423
+ */
331
424
  requestUpdate(opts?: UpdateConfig) {
332
425
  this.#dirty = true;
333
426
 
@@ -341,7 +434,9 @@ export abstract class Component
341
434
  }
342
435
  }
343
436
 
344
- #hasRequiredProperties?: Signal.Computed<boolean>;
437
+ /**
438
+ * @internal
439
+ */
345
440
  hasRequiredProperties(): Signal.Computed<boolean> {
346
441
  if (this.#hasRequiredProperties === undefined) {
347
442
  const requiredProperties = (this.constructor as typeof Component).requiredProperties;
@@ -349,9 +444,7 @@ export abstract class Component
349
444
  () => {
350
445
  return requiredProperties.size === 0
351
446
  ? true
352
- : requiredProperties
353
- .keys()
354
- .every((key) => (this as any)[key] !== undefined);
447
+ : requiredProperties.keys().every((key) => !isNullish((this as any)[key]));
355
448
  },
356
449
  // every signal update is relevant, even if the signal value doesn't
357
450
  // change, because the required values probably did.
@@ -360,7 +453,9 @@ export abstract class Component
360
453
  }
361
454
  return this.#hasRequiredProperties;
362
455
  }
456
+ #hasRequiredProperties?: Signal.Computed<boolean>;
363
457
 
458
+ /** @internal */
364
459
  protected async performUpdate() {
365
460
  if (!this.isConnected || !this.isUpdatePending) {
366
461
  return;
@@ -418,6 +513,7 @@ export abstract class Component
418
513
  }
419
514
  }
420
515
 
516
+ /** @internal */
421
517
  protected update() {
422
518
  if (this.#updateSignal !== undefined) {
423
519
  this.#updateSignalWatcher.unwatch(this.#updateSignal);
@@ -430,17 +526,23 @@ export abstract class Component
430
526
  this.#updateSignal.get();
431
527
  }
432
528
 
529
+ /**
530
+ * Return an iterator over this component's children which are also
531
+ * {@link Component}s.
532
+ */
433
533
  protected findChildComponents(
434
534
  root: Element | ShadowRoot = this.renderRoot,
435
535
  ): IteratorObject<Component> {
436
- let all: Array<Element> = childElements(root).toArray();
437
- const top = [...all];
438
- for (const el of top) {
439
- all = [...all, ...this.findChildComponents(el)];
440
- }
441
- return Iterator.from(all).filter((el) => el instanceof Component);
536
+ return findDescendants(root, (el) => el instanceof Component);
442
537
  }
443
538
 
539
+ /**
540
+ * Return a {@link Promise} which resolves when this component considers
541
+ * itself to have stabilised.
542
+ *
543
+ * The default implementation waits for any children which are also
544
+ * {@link Component}s to report that they have also stabilised.
545
+ */
444
546
  protected async stabilise(): Promise<void> {
445
547
  // wait for children to stabilise
446
548
  const children = this.findChildComponents();
@@ -448,41 +550,112 @@ export abstract class Component
448
550
  await Promise.all(children.map((child) => child.hasStabilised)).catch(console.error);
449
551
  }
450
552
 
553
+ /**
554
+ * This lifecycle callback is called each time this component is connected
555
+ * to the DOM.
556
+ */
557
+ protected connected(): void {
558
+ //
559
+ }
560
+
561
+ /**
562
+ * This lifecycle callback is called each time this component is
563
+ * disconnected from the DOM.
564
+ */
565
+ protected disconnected(): void {
566
+ //
567
+ }
568
+
569
+ /**
570
+ * This lifecycle callback is called each time an update has completed.
571
+ */
451
572
  protected updated(): void {
452
- // called when an update has completed
573
+ //
453
574
  }
454
575
 
576
+ /**
577
+ * This lifecycle callback is called after the component's first update has
578
+ * completed, and before {@link Component.updated}.
579
+ */
455
580
  protected firstUpdated() {
456
- // called when the first update has completed, and before
457
- // {@link Component.updated}.
581
+ //
458
582
  }
459
583
 
584
+ /**
585
+ * This lifecycle callback is called when the component considers itself
586
+ * stabilised after its first update.
587
+ *
588
+ * @see {@link Component.hasStabilised}
589
+ */
460
590
  protected stabilised() {
461
- // called when the component has stabilised after its first update
591
+ //
462
592
  }
463
593
 
594
+ /**
595
+ * This lifecycle callback is called every time a property decorated with
596
+ * the @{@link require} decorator has changed, but only when every property
597
+ * marked as such is not `undefined`.
598
+ */
464
599
  protected initialised() {
465
- // called when a property marked `@require` has changed and every
466
- // such property is non-`undefined`.
600
+ //
467
601
  }
468
602
 
603
+ /**
604
+ * This lifecycle callback is called the first time every property decorated
605
+ * with @{@link require} has been defined, and before
606
+ * {@link Component.initialised}.
607
+ */
469
608
  protected firstInitialised() {
470
- // called the first time all properties marked `@require` become
471
- // defined, and before {@link Component.initialised}.
609
+ //
472
610
  }
473
611
 
612
+ /**
613
+ * Render the component's contents.
614
+ *
615
+ * This function should return a Lit [renderable
616
+ * value](https://lit.dev/docs/templates/expressions/#child-expressions),
617
+ * usually an {@link html} template.
618
+ *
619
+ * @example
620
+ * class MyComponent extends Component {
621
+ * protected override render() {
622
+ * return html`
623
+ * <h1>Hello Joe!</h1>
624
+ * `;
625
+ * }
626
+ * }
627
+ */
474
628
  protected render(): unknown {
475
629
  return nothing;
476
630
  }
477
631
 
478
- [Symbol.dispose]() {
632
+ [Symbol.dispose](): void {
479
633
  for (const child of this.findChildComponents()) {
480
634
  child[Symbol.dispose]();
481
635
  }
636
+ this.remove();
482
637
  this.disconnectedCallback();
483
638
  (this.#rootPart as any)?._$clear?.();
484
639
  this.#rootPart = undefined;
485
- this.remove();
640
+ this.#finalCleanupContext.dispose();
641
+ }
642
+
643
+ /**
644
+ * Register a {@link Disposable} for automatic disposal when this component
645
+ * is disposed.
646
+ */
647
+ use(disposifiable: undefined): undefined;
648
+ use(disposifiable: null): null;
649
+ use(disposifiable: Disposifiable): Disposable;
650
+ use(disposifiable: Disposifiable | undefined): Disposable | undefined;
651
+ use(disposifiable: Disposifiable | null | undefined): Disposable | null | undefined;
652
+ use(disposifiable: Disposifiable | null | undefined): Disposable | null | undefined {
653
+ if (isNullish(disposifiable)) {
654
+ return disposifiable;
655
+ }
656
+ const disposable = toDisposable(disposifiable);
657
+ this.#finalCleanupContext.use(disposable);
658
+ return disposable;
486
659
  }
487
660
 
488
661
  /**
@@ -501,6 +674,11 @@ export abstract class Component
501
674
  * this.useWhileConnected(this.on("click", this.handleClick.bind(this)));
502
675
  * }
503
676
  */
677
+ useWhileConnected(
678
+ disposifiable: Disposifiable,
679
+ secondDisposable: Disposifiable,
680
+ ...disposifiables: Array<Disposifiable>
681
+ ): Array<Disposable>;
504
682
  useWhileConnected(disposifiable: undefined): undefined;
505
683
  useWhileConnected(disposifiable: null): null;
506
684
  useWhileConnected(disposifiable: Disposifiable): Disposable;
@@ -510,7 +688,15 @@ export abstract class Component
510
688
  ): Disposable | null | undefined;
511
689
  useWhileConnected(
512
690
  disposifiable: Disposifiable | null | undefined,
513
- ): Disposable | null | undefined {
691
+ ...disposifiables: Array<Disposifiable>
692
+ ): Array<Disposable> | Disposable | null | undefined {
693
+ if (disposifiables.length > 0) {
694
+ return [disposifiable, ...disposifiables].map((d) => {
695
+ const disposable = toDisposable(present(d));
696
+ this.#connectedContext.use(disposable);
697
+ return disposable;
698
+ });
699
+ }
514
700
  if (isNullish(disposifiable)) {
515
701
  return disposifiable;
516
702
  }
@@ -541,6 +727,27 @@ export abstract class Component
541
727
  return this.shadowRoot?.activeElement ?? this.querySelector(":focus");
542
728
  }
543
729
 
730
+ /**
731
+ * Find the first element in the component's {@link Component.renderRoot}
732
+ * matching the provided CSS selector.
733
+ *
734
+ * If you need to query the component's direct child elements instead, use
735
+ * {@link Component.querySlot}.
736
+ *
737
+ * If you were looking for Lit's `@query` decorator, use this as a getter
738
+ * instead, as in the example below.
739
+ *
740
+ * @example
741
+ * class MyComponent extends Component {
742
+ * get button(): HTMLButtonElement {
743
+ * return this.query("button");
744
+ * }
745
+ *
746
+ * protected override render() {
747
+ * return html`<button>I am a button</button>`;
748
+ * }
749
+ * }
750
+ */
544
751
  query<El extends keyof HTMLElementTagNameMap>(selector: El): HTMLElementTagNameMap[El] | null;
545
752
  query<El extends keyof SVGElementTagNameMap>(selector: El): SVGElementTagNameMap[El] | null;
546
753
  query<El extends keyof MathMLElementTagNameMap>(
@@ -555,6 +762,30 @@ export abstract class Component
555
762
  return this.renderRoot.querySelector(selector);
556
763
  }
557
764
 
765
+ /**
766
+ * Find all elements in the component's {@link Component.renderRoot}
767
+ * matching the provided CSS selector.
768
+ *
769
+ * If you need to query the component's direct child elements instead, use
770
+ * {@link Component.querySlot}.
771
+ *
772
+ * If you were looking for Lit's `@queryAll` decorator, use this as a getter
773
+ * instead, as in the example below.
774
+ *
775
+ * @example
776
+ * class MyComponent extends Component {
777
+ * get buttons(): NodeListOf<HTMLButtonElement> {
778
+ * return this.queryAll("button");
779
+ * }
780
+ *
781
+ * protected override render() {
782
+ * return html`
783
+ * <button>I am a button</button>
784
+ * <button>I am also a button</button>
785
+ * `;
786
+ * }
787
+ * }
788
+ */
558
789
  queryAll<El extends keyof HTMLElementTagNameMap>(
559
790
  selector: El,
560
791
  ): NodeListOf<HTMLElementTagNameMap[El]>;
@@ -573,6 +804,13 @@ export abstract class Component
573
804
  return this.renderRoot.querySelectorAll(selector);
574
805
  }
575
806
 
807
+ /**
808
+ * Find the elements attached to a given slot on this component, according
809
+ * to the provided {@link QuerySlotOptions}.
810
+ *
811
+ * If you include `reactive: true` in your query, the result will be a
812
+ * signal which updates with the contents of the slot.
813
+ */
576
814
  querySlot(
577
815
  options: QuerySlotOptions & { nodes: true; reactive: true },
578
816
  ): Signal.Computed<Array<Node>>;
@@ -600,11 +838,9 @@ export abstract class Component
600
838
  querySlot<El extends keyof MathMLElementTagNameMap>(
601
839
  options: QuerySlotOptions & { selector: El },
602
840
  ): Array<MathMLElementTagNameMap[El]>;
603
- /** @deprecated */
604
841
  querySlot<El extends keyof HTMLElementDeprecatedTagNameMap>(
605
842
  options: QuerySlotOptions & { reactive: true; selector: El },
606
843
  ): Signal.Computed<Array<HTMLElementDeprecatedTagNameMap[El]>>;
607
- /** @deprecated */
608
844
  querySlot<El extends keyof HTMLElementDeprecatedTagNameMap>(
609
845
  options: QuerySlotOptions & { selector: El },
610
846
  ): Array<HTMLElementDeprecatedTagNameMap[El]>;
@@ -637,9 +873,9 @@ export abstract class Component
637
873
  return query(slotEl);
638
874
  }
639
875
  const sig = getOrSetSignal(this, query, () => {
640
- const sig = Signal(query(slotEl), { equals: listsEqual });
876
+ const sig = Signal.from(query(slotEl), { equals: listsEqual });
641
877
  this.addController(new SlotChangeController(this, slot, sig, query));
642
- return sig.readOnly();
878
+ return sig.readOnly;
643
879
  });
644
880
  return sig;
645
881
  }
package/src/css.ts CHANGED
@@ -1,3 +1,8 @@
1
+ /**
2
+ * CSS manipulation tools.
3
+ * @module
4
+ */
5
+
1
6
  import { assert } from "@bodil/core/assert";
2
7
  import { None, Option, Some } from "@bodil/opt";
3
8