From 0be8bc1cd56351ef6bfc8a7e1fdcc4241aff8815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benny=20Powers=20-=20=D7=A2=D7=9D=20=D7=99=D7=A9=D7=A8?= =?UTF-8?q?=D7=90=D7=9C=20=D7=97=D7=99!?= Date: Tue, 16 Apr 2024 16:06:48 +0300 Subject: [PATCH] fix!: context types (#1518) * fix(lib)!: context types https://github.com/webcomponents-cg/community-protocols/pull/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 https://github.com/lit/lit/pull/4614 and https://github.com/webcomponents-cg/community-protocols/pull/59/files * docs: create changeset --------- Co-authored-by: Steven Spriggs --- .changeset/light-rice-warn.md | 5 ++++ lib/context/color/consumer.ts | 7 +++-- lib/context/color/controller.ts | 46 +++++++++++++----------------- lib/context/color/provider.ts | 25 ++++++++-------- lib/context/event.ts | 46 ++++++++++++++---------------- lib/context/headings/consumer.ts | 18 ++++++------ lib/context/headings/controller.ts | 10 +++---- lib/context/headings/provider.ts | 25 +++++++--------- 8 files changed, 85 insertions(+), 97 deletions(-) create mode 100644 .changeset/light-rice-warn.md diff --git a/.changeset/light-rice-warn.md b/.changeset/light-rice-warn.md new file mode 100644 index 0000000000..2b1257a791 --- /dev/null +++ b/.changeset/light-rice-warn.md @@ -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) diff --git a/lib/context/color/consumer.ts b/lib/context/color/consumer.ts index 1ac81da188..8961cefe21 100644 --- a/lib/context/color/consumer.ts +++ b/lib/context/color/consumer.ts @@ -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 @@ -52,13 +52,14 @@ export class ColorContextConsumer< #override: ColorTheme | null = null; constructor(host: T, private options?: ColorContextConsumerOptions) { - 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; diff --git a/lib/context/color/controller.ts b/lib/context/color/controller.ts index d199d0c6ba..5048610818 100644 --- a/lib/context/color/controller.ts +++ b/lib/context/color/controller.ts @@ -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 { prefix?: string; @@ -28,12 +23,15 @@ export interface ColorContextOptions { * ```html * * -* +* * * * ``` */ -export const contextEvents = new Map>(); +export const contextEvents = new Map< + ReactiveElement, + ContextRequestEvent +>(); /** * Color context is derived from the `--context` css custom property, @@ -47,26 +45,22 @@ export const contextEvents = new Map 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(Symbol('rh-color-context')); - /** The context object which describes the host's colour context */ - protected context: Context; + /** 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) { - 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); + } } diff --git a/lib/context/color/provider.ts b/lib/context/color/provider.ts index 3820fb6746..d00a5f5a7d 100644 --- a/lib/context/color/provider.ts +++ b/lib/context/color/provider.ts @@ -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, @@ -85,9 +85,11 @@ export class ColorContextProvider< } constructor(host: T, options?: ColorContextProviderOptions) { - 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; @@ -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); @@ -129,12 +131,9 @@ export class ColorContextProvider< /** Was the context event fired requesting our colour-context context? */ #isColorContextEvent( - event: ContextEvent - ): event is ContextEvent> { - return ( - event.target !== this.host && - event.context.name === this.context.name - ); + event: ContextRequestEvent + ): event is ContextRequestEvent { + return event.target !== this.host && event.context === ColorContextController.context; } /** @@ -142,7 +141,7 @@ export class ColorContextProvider< * 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) { + async #onChildContextRequestEvent(event: ContextRequestEvent) { // only handle ContextEvents relevant to colour context if (this.#isColorContextEvent(event)) { // claim the context-request event for ourselves (required by context protocol) @@ -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); } } diff --git a/lib/context/event.ts b/lib/context/event.ts index 5101079b8c..7f0edff266 100644 --- a/lib/context/event.ts +++ b/lib/context/event.ts @@ -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 = { - name: string; - initialValue?: T; +export type Context = KeyType & { + __context__: ValueType; }; /** - * An unknown context typeU + * An unknown context type */ -export type UnknownContext = Context; +export type UnknownContext = Context; /** * A helper type which can extract a Context value type from a Context type */ -export type ContextType = T extends Context - ? Y +export type ContextType = T extends Context + ? V : never; /** * A function which creates a Context value object */ -export function createContext( - name: string, - initialValue?: T -): Readonly> { - return { - name, - initialValue, - }; +export function createContext( + key: unknown, +): Readonly> { + return key as Context; } /** @@ -42,7 +41,7 @@ export function createContext( */ export type ContextCallback = ( value: ValueType, - dispose?: () => void + unsubscribe?: () => void ) => void; /** @@ -51,15 +50,15 @@ export type ContextCallback = ( * 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 extends Event { +export class ContextRequestEvent extends Event { public constructor( public readonly context: T, public readonly callback: ContextCallback>, - public readonly multiple?: boolean + public readonly subscribe?: boolean ) { super('context-request', { bubbles: true, composed: true }); } @@ -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; + 'context-request': ContextRequestEvent>; } } - diff --git a/lib/context/headings/consumer.ts b/lib/context/headings/consumer.ts index 8e7c1e46f0..2d4fc84c9f 100644 --- a/lib/context/headings/consumer.ts +++ b/lib/context/headings/consumer.ts @@ -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; @@ -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>(this.context, e => - this.#contextCallback(e), true); + const { context } = HeadingLevelController; + const event = new ContextRequestEvent( + context, + e => this.#contextCallback(e), + true, + ); this.host.dispatchEvent(event); contextEvents.set(this.host, event); } diff --git a/lib/context/headings/controller.ts b/lib/context/headings/controller.ts index afcde307b1..398c7f59be 100644 --- a/lib/context/headings/controller.ts +++ b/lib/context/headings/controller.ts @@ -4,7 +4,7 @@ import { ifDefined } from 'lit/directives/if-defined.js'; import { createContext, - type ContextEvent, + type ContextRequestEvent, type UnknownContext, } from '../event.js'; @@ -41,24 +41,22 @@ export interface HeadingLevelContextOptions { * ```html * * - * + * * * * ``` */ -export const contextEvents = new Map>(); +export const contextEvents = new Map>(); /** * 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(Symbol('rh-heading-level-context')); public offset: number; - protected context = createContext(HeadingLevelController.CONTEXT); - #level = 1; get level(): number { return this.#level; } diff --git a/lib/context/headings/provider.ts b/lib/context/headings/provider.ts index 761f9233e2..69a902d124 100644 --- a/lib/context/headings/provider.ts +++ b/lib/context/headings/provider.ts @@ -1,8 +1,7 @@ import { contextEvents, HeadingLevelController } from './controller.js'; import { - ContextEvent, - type Context, + ContextRequestEvent, type UnknownContext, type ContextCallback, } from '../event.js'; @@ -36,8 +35,7 @@ export class HeadingLevelContextProvider extends HeadingLevelController { #callbacks = new Set>(); hostConnected() { - this.host.addEventListener('context-request', e => - this.#onChildContextEvent(e as ContextEvent)); + this.host.addEventListener('context-request', e => this.#onChildContextRequestEvent(e)); for (const [host, fired] of contextEvents) { host.dispatchEvent(fired); } @@ -63,18 +61,15 @@ export class HeadingLevelContextProvider extends HeadingLevelController { } /** Was the context event fired requesting our colour-context context? */ - #isHeadingContextEvent( - event: ContextEvent - ): event is ContextEvent> { - return ( - event.target !== this.host && - event.context.name === this.context.name - ); + #isHeadingContextRequestEvent( + event: ContextRequestEvent + ): event is ContextRequestEvent { + return event.target !== this.host && event.context === HeadingLevelController.context; } - async #onChildContextEvent(event: ContextEvent) { - // only handle ContextEvents relevant to colour context - if (this.#isHeadingContextEvent(event)) { + async #onChildContextRequestEvent(event: ContextRequestEvent) { + // only handle ContextRequestEvents relevant to colour context + if (this.#isHeadingContextRequestEvent(event)) { // claim the context-request event for ourselves (required by context protocol) event.stopPropagation(); @@ -82,7 +77,7 @@ export class HeadingLevelContextProvider extends HeadingLevelController { event.callback(this.level); // Cache the callback for future updates, if requested - if (event.multiple) { + if (event.subscribe) { this.#callbacks.add(event.callback); } }