Skip to content

Commit

Permalink
fix!: context types (#1518)
Browse files Browse the repository at this point in the history
* fix(lib)!: context types

webcomponents-cg/community-protocols#59
made a breaking change to the way the context protocol works.
This commit brings our types in line with the new types on the protocol

* fix(context)!: adapt our contexts to new types

see lit/lit#4614 and https://github.com/webcomponents-cg/community-protocols/pull/59/files

* docs: create changeset

---------

Co-authored-by: Steven Spriggs <[email protected]>
  • Loading branch information
bennypowers and zeroedin authored Apr 16, 2024
1 parent b049b5c commit 0be8bc1
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 97 deletions.
5 changes: 5 additions & 0 deletions .changeset/light-rice-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rhds/elements": patch
---

Context: aligned context implementation with updated [protocol defintions](https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md#definitions)
7 changes: 4 additions & 3 deletions lib/context/color/consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
type ColorContextOptions
} from './controller.js';

import { ContextEvent } from '../event.js';
import { ContextRequestEvent } from '../event.js';

/**
* A Color theme is a context-specific restriction on the available color palettes
Expand Down Expand Up @@ -52,13 +52,14 @@ export class ColorContextConsumer<
#override: ColorTheme | null = null;

constructor(host: T, private options?: ColorContextConsumerOptions<T>) {
super(host, options);
super(host);
this.#propertyName = options?.propertyName ?? 'on' as keyof T;
}

/** When a consumer connects, it requests colour context from the closest provider. */
async hostConnected() {
const event = new ContextEvent(this.context, e => this.#contextCallback(e), true);
const { context } = ColorContextController;
const event = new ContextRequestEvent(context, e => this.#contextCallback(e), true);
this.#override = this.#propertyValue;
contextEvents.set(this.host, event);
await this.host.updateComplete;
Expand Down
46 changes: 20 additions & 26 deletions lib/context/color/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,9 @@ import type { ReactiveController, ReactiveElement } from 'lit';

import { StyleController } from '@patternfly/pfe-core/controllers/style-controller.js';

import {
createContext,
ContextEvent,
type Context,
type UnknownContext,
} from '../event.js';
import { createContext, type ContextRequestEvent } from '../event.js';

import CONTEXT_BASE_STYLES from './context-color.css';
import COLOR_CONTEXT_BASE_STYLES from './context-color.css';

export interface ColorContextOptions<T extends ReactiveElement> {
prefix?: string;
Expand All @@ -28,12 +23,15 @@ export interface ColorContextOptions<T extends ReactiveElement> {
* ```html
* <early-provider>
* <late-provider>
* <eager-consumer>
* <eager-consumer></eager-consumer>
* </late-provider>
* </early-provider>
* ```
*/
export const contextEvents = new Map<ReactiveElement, ContextEvent<UnknownContext>>();
export const contextEvents = new Map<
ReactiveElement,
ContextRequestEvent<typeof ColorContextController.context>
>();

/**
* Color context is derived from the `--context` css custom property,
Expand All @@ -47,26 +45,22 @@ export const contextEvents = new Map<ReactiveElement, ContextEvent<UnknownContex
export abstract class ColorContextController<
T extends ReactiveElement
> implements ReactiveController {
abstract update(next?: ColorTheme | null): void;
/** The context object which acts as the key for providers and consumers */
public static readonly context = createContext<ColorTheme | null>(Symbol('rh-color-context'));

/** The context object which describes the host's colour context */
protected context: Context<ColorTheme | null>;
/** The style controller which provides the necessary CSS. */
protected styleController: StyleController;

/** The style controller which provides the necessary CSS. */
protected styleController: StyleController;
/** The last-known color context on the host */
protected last: ColorTheme | null = null;

/** Prefix for colour context. Set this in Options to create a separate context */
protected prefix = 'rh-';
hostUpdate?(): void

/** The last-known color context on the host */
protected last: ColorTheme | null = null;
/** callback which updates the context value on consumers */
abstract update(next?: ColorTheme | null): void;

hostUpdate?(): void

constructor(protected host: T, options?: ColorContextOptions<T>) {
this.prefix = options?.prefix ?? 'rh';
this.context = createContext(`${this.prefix}-color-context`);
this.styleController = new StyleController(host, CONTEXT_BASE_STYLES);
host.addController(this);
}
constructor(protected host: T) {
this.styleController = new StyleController(host, COLOR_CONTEXT_BASE_STYLES);
host.addController(this);
}
}
25 changes: 12 additions & 13 deletions lib/context/color/provider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ReactiveController, ReactiveElement } from 'lit';
import type { Context, ContextCallback, ContextEvent, UnknownContext } from '../event.js';
import type { ContextCallback, ContextRequestEvent, UnknownContext } from '../event.js';

import {
contextEvents,
Expand Down Expand Up @@ -85,9 +85,11 @@ export class ColorContextProvider<
}

constructor(host: T, options?: ColorContextProviderOptions<T>) {
const { attribute = 'color-palette', ...rest } = options ?? {};
super(host, rest);
this.#consumer = new ColorContextConsumer(host, { callback: value => this.update(value) });
const { attribute = 'color-palette' } = options ?? {};
super(host);
this.#consumer = new ColorContextConsumer(host, {
callback: value => this.update(value),
});
this.#logger = new Logger(host);
this.#style = window.getComputedStyle(host);
this.#attribute = attribute;
Expand All @@ -102,7 +104,7 @@ export class ColorContextProvider<
* in case this context provider upgraded after and is closer to a given consumer.
*/
async hostConnected() {
this.host.addEventListener('context-request', e => this.#onChildContextEvent(e));
this.host.addEventListener('context-request', e => this.#onChildContextRequestEvent(e));
this.#mo.observe(this.host, { attributes: true, attributeFilter: [this.#attribute] });
for (const [host, fired] of contextEvents) {
host.dispatchEvent(fired);
Expand All @@ -129,20 +131,17 @@ export class ColorContextProvider<

/** Was the context event fired requesting our colour-context context? */
#isColorContextEvent(
event: ContextEvent<UnknownContext>
): event is ContextEvent<Context<ColorTheme | null>> {
return (
event.target !== this.host &&
event.context.name === this.context.name
);
event: ContextRequestEvent<UnknownContext>
): event is ContextRequestEvent<typeof ColorContextController.context> {
return event.target !== this.host && event.context === ColorContextController.context;
}

/**
* Provider part of context API
* When a child connects, claim its context-request event
* and add its callback to the Set of children if it requests multiple updates
*/
async #onChildContextEvent(event: ContextEvent<UnknownContext>) {
async #onChildContextRequestEvent(event: ContextRequestEvent<UnknownContext>) {
// only handle ContextEvents relevant to colour context
if (this.#isColorContextEvent(event)) {
// claim the context-request event for ourselves (required by context protocol)
Expand All @@ -152,7 +151,7 @@ export class ColorContextProvider<
event.callback(this.value);

// Cache the callback for future updates, if requested
if (event.multiple) {
if (event.subscribe) {
this.#callbacks.add(event.callback);
}
}
Expand Down
46 changes: 22 additions & 24 deletions lib/context/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,35 @@
*/

/**
* A Context object defines an optional initial value for a Context, as well as a name identifier for debugging purposes.
* A context key.
*
* A context key can be any type of object, including strings and symbols. The
* Context type brands the key type with the `__context__` property that
* carries the type of the value the context references.
*/
export type Context<T> = {
name: string;
initialValue?: T;
export type Context<KeyType, ValueType> = KeyType & {
__context__: ValueType;
};

/**
* An unknown context typeU
* An unknown context type
*/
export type UnknownContext = Context<unknown>;
export type UnknownContext = Context<unknown, unknown>;

/**
* A helper type which can extract a Context value type from a Context type
*/
export type ContextType<T extends UnknownContext> = T extends Context<infer Y>
? Y
export type ContextType<T extends UnknownContext> = T extends Context<infer _, infer V>
? V
: never;

/**
* A function which creates a Context value object
*/
export function createContext<T>(
name: string,
initialValue?: T
): Readonly<Context<T>> {
return {
name,
initialValue,
};
export function createContext<ValueType>(
key: unknown,
): Readonly<Context<typeof key, ValueType>> {
return key as Context<typeof key, ValueType>;
}

/**
Expand All @@ -42,7 +41,7 @@ export function createContext<T>(
*/
export type ContextCallback<ValueType> = (
value: ValueType,
dispose?: () => void
unsubscribe?: () => void
) => void;

/**
Expand All @@ -51,15 +50,15 @@ export type ContextCallback<ValueType> = (
* A provider should inspect the `context` property of the event to determine if it has a value that can
* satisfy the request, calling the `callback` with the requested value if so.
*
* If the requested context event contains a truthy `multiple` value, then a provider can call the callback
* multiple times if the value is changed, if this is the case the provider should pass a `dispose`
* method to the callback which requesters can invoke to indicate they no longer wish to receive these updates.
* If the requested context event contains a truthy `subscribe` value, then a provider can call the callback
* multiple times if the value is changed, if this is the case the provider should pass an `unsubscribe`
* function to the callback which requesters can invoke to indicate they no longer wish to receive these updates.
*/
export class ContextEvent<T extends UnknownContext> extends Event {
export class ContextRequestEvent<T extends UnknownContext> extends Event {
public constructor(
public readonly context: T,
public readonly callback: ContextCallback<ContextType<T>>,
public readonly multiple?: boolean
public readonly subscribe?: boolean
) {
super('context-request', { bubbles: true, composed: true });
}
Expand All @@ -71,7 +70,6 @@ declare global {
* A 'context-request' event can be emitted by any element which desires
* a context value to be injected by an external provider.
*/
'context-request': ContextEvent<UnknownContext>;
'context-request': ContextRequestEvent<Context<unknown, unknown>>;
}
}

18 changes: 8 additions & 10 deletions lib/context/headings/consumer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import {
ContextEvent,
type Context,
} from '../event.js';
import { ContextRequestEvent } from '../event.js';

import {
contextEvents,
HeadingLevelController,
} from './controller.js';
import { contextEvents, HeadingLevelController } from './controller.js';

export interface HeadingTemplateOptions {
id?: string;
Expand All @@ -23,8 +17,12 @@ export class HeadingLevelContextConsumer extends HeadingLevelController {

/** When a consumer connects, it requests context from the closest provider. */
hostConnected() {
const event = new ContextEvent<Context<number>>(this.context, e =>
this.#contextCallback(e), true);
const { context } = HeadingLevelController;
const event = new ContextRequestEvent<typeof context>(
context,
e => this.#contextCallback(e),
true,
);
this.host.dispatchEvent(event);
contextEvents.set(this.host, event);
}
Expand Down
10 changes: 4 additions & 6 deletions lib/context/headings/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ifDefined } from 'lit/directives/if-defined.js';

import {
createContext,
type ContextEvent,
type ContextRequestEvent,
type UnknownContext,
} from '../event.js';

Expand Down Expand Up @@ -41,24 +41,22 @@ export interface HeadingLevelContextOptions {
* ```html
* <early-provider>
* <late-provider>
* <eager-consumer>
* <eager-consumer></eager-consumer>
* </late-provider>
* </early-provider>
* ```
*/
export const contextEvents = new Map<ReactiveElement, ContextEvent<UnknownContext>>();
export const contextEvents = new Map<ReactiveElement, ContextRequestEvent<UnknownContext>>();

/**
* Determines which heading level immediately precedes the host element,
* and provides templates for shadow headings.
*/
export class HeadingLevelController implements ReactiveController {
static get CONTEXT() { return 'rh-heading-levels'; }
public static readonly context = createContext<number>(Symbol('rh-heading-level-context'));

public offset: number;

protected context = createContext<number>(HeadingLevelController.CONTEXT);

#level = 1;

get level(): number { return this.#level; }
Expand Down
25 changes: 10 additions & 15 deletions lib/context/headings/provider.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { contextEvents, HeadingLevelController } from './controller.js';

import {
ContextEvent,
type Context,
ContextRequestEvent,
type UnknownContext,
type ContextCallback,
} from '../event.js';
Expand Down Expand Up @@ -36,8 +35,7 @@ export class HeadingLevelContextProvider extends HeadingLevelController {
#callbacks = new Set<ContextCallback<number>>();

hostConnected() {
this.host.addEventListener('context-request', e =>
this.#onChildContextEvent(e as ContextEvent<UnknownContext>));
this.host.addEventListener('context-request', e => this.#onChildContextRequestEvent(e));
for (const [host, fired] of contextEvents) {
host.dispatchEvent(fired);
}
Expand All @@ -63,26 +61,23 @@ export class HeadingLevelContextProvider extends HeadingLevelController {
}

/** Was the context event fired requesting our colour-context context? */
#isHeadingContextEvent(
event: ContextEvent<UnknownContext>
): event is ContextEvent<Context<number>> {
return (
event.target !== this.host &&
event.context.name === this.context.name
);
#isHeadingContextRequestEvent(
event: ContextRequestEvent<UnknownContext>
): event is ContextRequestEvent<typeof HeadingLevelController.context> {
return event.target !== this.host && event.context === HeadingLevelController.context;
}

async #onChildContextEvent(event: ContextEvent<UnknownContext>) {
// only handle ContextEvents relevant to colour context
if (this.#isHeadingContextEvent(event)) {
async #onChildContextRequestEvent(event: ContextRequestEvent<UnknownContext>) {
// only handle ContextRequestEvents relevant to colour context
if (this.#isHeadingContextRequestEvent(event)) {
// claim the context-request event for ourselves (required by context protocol)
event.stopPropagation();

// Run the callback to initialize the child's value
event.callback(this.level);

// Cache the callback for future updates, if requested
if (event.multiple) {
if (event.subscribe) {
this.#callbacks.add(event.callback);
}
}
Expand Down

0 comments on commit 0be8bc1

Please sign in to comment.