From cf54ae55b4cd2d9c9574d2b8ce7da25fbe1baeec Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Thu, 16 Jan 2025 09:02:12 +0200 Subject: [PATCH 1/4] fix(context): use @lit/context WIP --- elements/rh-card/demo/color-context.html | 20 +- elements/rh-card/demo/variants.html | 49 +-- eleventy.config.ts | 1 + lib/context/color/controller.ts | 43 +- lib/context/color/provider.ts | 52 ++- lib/context/event.ts | 76 ---- package-lock.json | 512 ++-------------------- package.json | 8 +- patches/@patternfly+pfe-core+4.0.4.patch | 3 +- patches/@patternfly+pfe-tools+4.0.1.patch | 2 +- 10 files changed, 95 insertions(+), 671 deletions(-) delete mode 100644 lib/context/event.ts diff --git a/elements/rh-card/demo/color-context.html b/elements/rh-card/demo/color-context.html index f7a56ae253..0563a8044a 100644 --- a/elements/rh-card/demo/color-context.html +++ b/elements/rh-card/demo/color-context.html @@ -6,9 +6,7 @@

Default card, slotted content and footer

sollicitudin mauris tincidunt. Pellentesque vel dapibus risus. Nullam aliquam felis orci, eget cursus mi lacinia quis. Vivamus at felis sem.

- - Call to action - + Call to action @@ -17,9 +15,7 @@

Slotted title, content, and footer

- - Call to action - + Call to action
@@ -38,9 +34,7 @@

Card with slotted image header. Full width image.

at felis sem.

- - Call to action - + Call to action
@@ -49,9 +43,7 @@

Custom header

Nullam eleifend elit sed est egestas, a sollicitudin mauris tincidunt. Pellentesque vel dapibus risus. Nullam aliquam felis orci, eget cursus mi lacinia quis. Vivamus at felis sem.

- - Call to action - + Call to action
@@ -60,9 +52,7 @@

Custom header

Nullam eleifend elit sed est egestas, a sollicitudin mauris tincidunt. Pellentesque vel dapibus risus. Nullam aliquam felis orci, eget cursus mi lacinia quis. Vivamus at felis sem.

- - Call to action - + Call to action
diff --git a/elements/rh-card/demo/variants.html b/elements/rh-card/demo/variants.html index 676ca915e0..a34b57e770 100644 --- a/elements/rh-card/demo/variants.html +++ b/elements/rh-card/demo/variants.html @@ -5,9 +5,7 @@

Default card, slotted content and footer

Nullam eleifend elit sed est egestas, a sollicitudin mauris tincidunt. Pellentesque vel dapibus risus. Nullam aliquam felis orci, eget cursus mi lacinia quis. Vivamus at felis sem.

- - Call to action - + Call to action @@ -16,9 +14,16 @@

Slotted title, content, and footer

Nullam eleifend elit sed est egestas, a sollicitudin mauris tincidunt. Pellentesque vel dapibus risus. Nullam aliquam felis orci, eget cursus mi lacinia quis. Vivamus at felis sem.

- - Call to action - + Call to action +
+ + +

Darkest color palette

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Nullam eleifend elit sed est egestas, a sollicitudin mauris + tincidunt. Pellentesque vel dapibus risus. Nullam aliquam + felis orci, eget cursus mi lacinia quis. Vivamus at felis sem.

+ Call to action
@@ -27,9 +32,7 @@

Lighter color palette

Nullam eleifend elit sed est egestas, a sollicitudin mauris tincidunt. Pellentesque vel dapibus risus. Nullam aliquam felis orci, eget cursus mi lacinia quis. Vivamus at felis sem.

- - Call to action - + Call to action
@@ -38,9 +41,7 @@

Lighter color palette and custom header

Nullam eleifend elit sed est egestas, a sollicitudin mauris tincidunt. Pellentesque vel dapibus risus. Nullam aliquam felis orci, eget cursus mi lacinia quis. Vivamus at felis sem.

- - Call to action - + Call to action
@@ -49,9 +50,7 @@

Custom header

Nullam eleifend elit sed est egestas, a sollicitudin mauris tincidunt. Pellentesque vel dapibus risus. Nullam aliquam felis orci, eget cursus mi lacinia quis. Vivamus at felis sem.

- - Call to action - + Call to action
@@ -60,9 +59,7 @@

Center aligned content, footer

Nullam eleifend elit sed est egestas, a sollicitudin mauris tincidunt. Pellentesque vel dapibus risus. Nullam aliquam felis orci, eget cursus mi lacinia quis. Vivamus at felis sem.

- - Call to action - + Call to action
@@ -71,9 +68,7 @@

End aligned content, footer

Nullam eleifend elit sed est egestas, a sollicitudin mauris tincidunt. Pellentesque vel dapibus risus. Nullam aliquam felis orci, eget cursus mi lacinia quis. Vivamus at felis sem.

- - Call to action - + Call to action
@@ -87,9 +82,7 @@

End aligned content, footer

Nullam eleifend elit sed est egestas, a sollicitudin mauris tincidunt. Pellentesque vel dapibus risus. Nullam aliquam felis orci, eget cursus mi lacinia quis. Vivamus at felis sem.

- - Call to action - + Call to action
@@ -103,9 +96,7 @@

End aligned content, footer

Nullam eleifend elit sed est egestas, a sollicitudin mauris tincidunt. Pellentesque vel dapibus risus. Nullam aliquam felis orci, eget cursus mi lacinia quis. Vivamus at felis sem.

- - Call to action - + Call to action
@@ -120,9 +111,7 @@

Card with slotted image header. Full width image.

Nullam eleifend elit sed est egestas, a sollicitudin mauris tincidunt. Pellentesque vel dapibus risus. Nullam aliquam felis orci, eget cursus mi lacinia quis. Vivamus at felis sem.

- - Call to action - + Call to action
diff --git a/eleventy.config.ts b/eleventy.config.ts index 7d10a776c3..c672d9b82f 100644 --- a/eleventy.config.ts +++ b/eleventy.config.ts @@ -208,6 +208,7 @@ export default async function(eleventyConfig: UserConfig) { 'elements/rh-code-block/rh-code-block.ts', 'elements/rh-table/rh-table.ts', 'elements/rh-accordion/rh-accordion.ts', + 'elements/rh-card/rh-card.ts', 'elements/rh-cta/rh-cta.ts', 'elements/rh-footer/rh-footer-universal.ts', 'elements/rh-skip-link/rh-skip-link.ts', diff --git a/lib/context/color/controller.ts b/lib/context/color/controller.ts index ad3c9063c5..f230b4fee0 100644 --- a/lib/context/color/controller.ts +++ b/lib/context/color/controller.ts @@ -1,42 +1,27 @@ import type { ColorTheme } from './consumer.js'; import type { CSSResult, ReactiveController } from 'lit'; +import type { ContextEvent, Context } from '@lit/context'; import { ReactiveElement } from 'lit'; import { StyleController } from '@patternfly/pfe-core/controllers/style-controller.js'; -import { createContext, type ContextRequestEvent, type UnknownContext } from '../event.js'; +import { createContextWithRoot } from '@patternfly/pfe-core/functions/context.js'; + +export type UnknownContext = Context; export interface ColorContextOptions { prefix?: string; propertyName?: keyof T; } -type ColorContext = typeof ColorContextController.context; +type ColorContext = typeof context; -/** -* Maps from consumer host elements to already-fired request events -* We hold these in memory in order to re-fire the events every time a new provider connects. -* This is a hedge against cases where an early-upgrading provider claims an early-upgrading -* consumer before a late-upgrading provider has a chance to register as the rightful provider -* @example Monkey-in-the-middle error -* In this example, we must re-fire the event from eager-consumer when late-provider -* upgrades, so as to ensure that late-provider claims it for itself -* ```html -* -* -* -* -* -* ``` -*/ -export const contextEvents = new Map< - ReactiveElement, - ContextRequestEvent ->(); +/** The context object which acts as the key for providers and consumers */ +export const context = createContextWithRoot('rh-color-context'); export const isColorContextEvent = - (event: ContextRequestEvent): event is ContextRequestEvent => + (event: ContextEvent): event is ContextEvent => event.context === ColorContextController.context; /** @@ -51,23 +36,11 @@ export const isColorContextEvent = export abstract class ColorContextController< T extends ReactiveElement > implements ReactiveController { - /** The context object which acts as the key for providers and consumers */ - public static readonly context = createContext(Symbol('rh-color-context')); - /** The last-known color context on the host */ protected last: ColorTheme | null = null; - hostUpdate?(): void; - - /** callback which updates the context value on consumers */ - abstract update(next?: ColorTheme | null): void; - constructor(protected host: T, styles: CSSStyleSheet | CSSResult) { new StyleController(host, styles); - if (this.host instanceof ReactiveElement) { - const Class = (this.host.constructor as typeof ReactiveElement); - Class.styles = [Class.styles].flat().filter(x => !!x).concat([styles]); - } host.addController(this); } } diff --git a/lib/context/color/provider.ts b/lib/context/color/provider.ts index d62dbec16b..37a47423d6 100644 --- a/lib/context/color/provider.ts +++ b/lib/context/color/provider.ts @@ -1,11 +1,13 @@ -import { isServer, type ReactiveController, type ReactiveElement } from 'lit'; -import type { ContextCallback, ContextRequestEvent, UnknownContext } from '../event.js'; +import type { ReactiveElement } from 'lit'; +import type { ContextCallback, ContextEvent } from '@lit/context'; -import { - contextEvents, - ColorContextController, - type ColorContextOptions, -} from './controller.js'; +import { ContextProvider } from '@lit/context'; + +import { context, type ColorContextOptions, type UnknownContext } from './controller.js'; + +import { isServer } from 'lit'; + +import { ColorContextController } from './controller.js'; import { ColorContextConsumer, type ColorTheme } from './consumer.js'; @@ -13,6 +15,8 @@ import { Logger } from '@patternfly/pfe-core/controllers/logger.js'; import styles from '@rhds/tokens/css/color-context-provider.css.js'; +import { StyleController } from '@patternfly/pfe-core/controllers/style-controller.js'; + /** * A `ColorPalette` is a collection of specific color values * Choosing a palette sets both color properties and, if the component is a context provider, @@ -42,9 +46,7 @@ export interface ColorContextProviderOptions * `ColorContextProvider` is responsible to derive a context value from CSS and provide it to its * descendents. */ -export class ColorContextProvider< - T extends ReactiveElement -> extends ColorContextController implements ReactiveController { +export class ColorContextProvider extends ContextProvider { static contexts = new Map(Object.entries({ darkest: 'dark' as const, darker: 'dark' as const, @@ -54,6 +56,9 @@ export class ColorContextProvider< lightest: 'light' as const, })); + /** The last-known color context on the host */ + protected last: ColorTheme | null = null; + #attribute: string; /** Cache of context callbacks. Call each to update consumers */ @@ -66,14 +71,12 @@ export class ColorContextProvider< * Cached (live) computed style declaration * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle */ - // eslint-disable-next-line no-unused-private-class-members - #style: CSSStyleDeclaration; #initialized = false; #logger: Logger; - #consumer: ColorContextConsumer; + #consumer: ColorContextConsumer; get local() { return this.#local; @@ -88,14 +91,15 @@ export class ColorContextProvider< return this.#local ?? this.#consumer.value; } - constructor(host: T, options?: ColorContextProviderOptions) { + constructor(protected host: T, options?: ColorContextProviderOptions) { + super(host, { context }); + this.#styles = new StyleController(host, styles); + this.#logger = new Logger(host); + host.addController(this); const { attribute = 'color-palette' } = options ?? {}; - super(host, styles); this.#consumer = new ColorContextConsumer(host, { callback: value => this.update(value), }); - this.#logger = new Logger(host); - this.#style = window.getComputedStyle(host); this.#attribute = attribute; if (this.#attribute !== 'color-palette') { this.#logger.warn('color context currently supports the `color-palette` attribute only.'); @@ -110,9 +114,8 @@ export class ColorContextProvider< async hostConnected() { this.host.addEventListener('context-request', e => this.#onChildContextRequestEvent(e)); this.#mo.observe(this.host, { attributes: true, attributeFilter: [this.#attribute] }); - await this.host.updateComplete; - for (const [host, fired] of contextEvents) { - host.dispatchEvent(fired); + if (!isServer) { + await this.host.updateComplete; } this.update(); return true; @@ -122,10 +125,11 @@ export class ColorContextProvider< if (!this.#initialized) { this.hostConnected(); } - this.#initialized ||= await this.hostConnected(); + this.#initialized ||= isServer || await this.hostConnected(); if (this.#local && this.value !== this.#consumer.value) { this.#consumer.update(this.#local); this.update(); + console.log(this.host.localName, this.value); } if (!isServer) { // This is definitely overkill, but it's the only @@ -147,8 +151,8 @@ export class ColorContextProvider< * @param event some event */ #isColorContextEvent( - event: ContextRequestEvent - ): event is ContextRequestEvent { + event: ContextEvent + ): event is ContextEvent { return event.composedPath().at(0) !== this.host && event.context === ColorContextController.context; } @@ -159,7 +163,7 @@ export class ColorContextProvider< * and add its callback to the Set of children if it requests multiple updates * @param event context-request event */ - async #onChildContextRequestEvent(event: ContextRequestEvent) { + async #onChildContextRequestEvent(event: ContextEvent) { // only handle ContextEvents relevant to colour context if (this.#isColorContextEvent(event)) { event.stopPropagation(); diff --git a/lib/context/event.ts b/lib/context/event.ts deleted file mode 100644 index a8c2e18df3..0000000000 --- a/lib/context/event.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Context Protocol - * @link https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md - */ - -/** - * 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 = KeyType & { - __context__: ValueType; -}; - -/** - * An unknown context type - */ -export type UnknownContext = Context; - -/** - * A helper type which can extract a Context value type from a Context type - */ -export type ContextType = T extends Context - ? V - : never; - -/** - * A function which creates a Context value object - * @param key context key - */ -export function createContext( - key: unknown, -): Readonly> { - return key as Context; -} - -/** - * A callback which is provided by a context requester and is called with the value satisfying the request. - * This callback can be called multiple times by context providers as the requested value is changed. - */ -export type ContextCallback = ( - value: ValueType, - unsubscribe?: () => void -) => void; - -/** - * An event fired by a context requester to signal it desires a named context. - * - * 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 `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 ContextRequestEvent extends Event { - public constructor( - public readonly context: T, - public readonly callback: ContextCallback>, - public readonly subscribe?: boolean - ) { - super('context-request', { bubbles: true, composed: true }); - } -} - -declare global { - interface HTMLElementEventMap { - /** - * A 'context-request' event can be emitted by any element which desires - * a context value to be injected by an external provider. - */ - 'context-request': ContextRequestEvent>; - } -} diff --git a/package-lock.json b/package-lock.json index 7d6bd27430..c392349b42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "lit": "^3.2.1", "prism-esm": "^1.29.0-fix.6", "tslib": "^2.7.0", - "web-dev-server-plugin-lit-css": "^3.0.1" + "web-dev-server-plugin-lit-css": "^3.0.2" }, "devDependencies": { "@11ty/eleventy": "^3.0.0", @@ -25,8 +25,8 @@ "@11ty/eleventy-plugin-rss": "^2.0.2", "@commitlint/cli": "^19.5.0", "@commitlint/config-conventional": "^19.5.0", - "@lit-labs/eleventy-plugin-lit": "^1.0.3", - "@lit-labs/ssr": "^3.2.2", + "@lit-labs/eleventy-plugin-lit": "^1.0.4", + "@lit-labs/ssr": "^3.3.0", "@lit-labs/ssr-client": "^1.1.7", "@lit/reactive-element": "^2.0.4", "@lit/ts-transformers": "^2.0.1", @@ -81,7 +81,7 @@ "ts-patch": "^3.2.1", "tsx": "^4.19.1", "typescript": "^5.6.3", - "typescript-transform-lit-css": "^2.0.0", + "typescript-transform-lit-css": "^2.0.1", "wireit": "^0.14.9" }, "optionalDependencies": { @@ -2730,11 +2730,13 @@ } }, "node_modules/@lit-labs/eleventy-plugin-lit": { - "version": "1.0.3", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@lit-labs/eleventy-plugin-lit/-/eleventy-plugin-lit-1.0.4.tgz", + "integrity": "sha512-daOZzKqJIGuuM2W+SMIoRsUNl5I6jdGF0189BO3ZkClU9vXfjQIcbWZWGTdP1rCQ1vJ7pGxzXEkPCk8Wg1Sf+w==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@lit-labs/ssr": "^3.1.8", + "@lit-labs/ssr": "^3.3.0", "lit": "^2.7.0 || ^3.0.0" }, "engines": { @@ -2742,14 +2744,14 @@ } }, "node_modules/@lit-labs/ssr": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr/-/ssr-3.2.2.tgz", - "integrity": "sha512-He5TzeNPM9ECmVpgXRYmVlz0UA5YnzHlT43kyLi2Lu6mUidskqJVonk9W5K699+2DKhoXp8Ra4EJmHR6KrcW1Q==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr/-/ssr-3.3.0.tgz", + "integrity": "sha512-OGlPfWfJIC2CXQLuXXRtbWlgidryVI8VOEFUPc++Vk7gQ4aapAJwHJFi7Mi614ekebNLzhkpA/10IZy5g+nGcQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@lit-labs/ssr-client": "^1.1.7", - "@lit-labs/ssr-dom-shim": "^1.2.0", + "@lit-labs/ssr-dom-shim": "^1.3.0", "@lit/reactive-element": "^2.0.4", "@parse5/tools": "^0.3.0", "@types/node": "^16.0.0", @@ -2777,7 +2779,9 @@ } }, "node_modules/@lit-labs/ssr-dom-shim": { - "version": "1.2.1", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz", + "integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==", "license": "BSD-3-Clause" }, "node_modules/@lit-labs/ssr/node_modules/@parse5/tools": { @@ -4605,447 +4609,12 @@ } }, "node_modules/@pwrs/lit-css": { - "version": "3.0.0", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@pwrs/lit-css/-/lit-css-3.0.1.tgz", + "integrity": "sha512-N3oac0XYqKEEoWMT7y02EDxoyM++805V+nXh3rEBGV2IW2Y0NjK4hepLea65HGFlu+ezSK48XAJYD4xAyvVsaQ==", "license": "ISC", "dependencies": { - "cssnano": "^6.1.2" - } - }, - "node_modules/@pwrs/lit-css/node_modules/cssnano": { - "version": "6.1.2", - "license": "MIT", - "dependencies": { - "cssnano-preset-default": "^6.1.2", - "lilconfig": "^3.1.1" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/cssnano-preset-default": { - "version": "6.1.2", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "css-declaration-sorter": "^7.2.0", - "cssnano-utils": "^4.0.2", - "postcss-calc": "^9.0.1", - "postcss-colormin": "^6.1.0", - "postcss-convert-values": "^6.1.0", - "postcss-discard-comments": "^6.0.2", - "postcss-discard-duplicates": "^6.0.3", - "postcss-discard-empty": "^6.0.3", - "postcss-discard-overridden": "^6.0.2", - "postcss-merge-longhand": "^6.0.5", - "postcss-merge-rules": "^6.1.1", - "postcss-minify-font-values": "^6.1.0", - "postcss-minify-gradients": "^6.0.3", - "postcss-minify-params": "^6.1.0", - "postcss-minify-selectors": "^6.0.4", - "postcss-normalize-charset": "^6.0.2", - "postcss-normalize-display-values": "^6.0.2", - "postcss-normalize-positions": "^6.0.2", - "postcss-normalize-repeat-style": "^6.0.2", - "postcss-normalize-string": "^6.0.2", - "postcss-normalize-timing-functions": "^6.0.2", - "postcss-normalize-unicode": "^6.1.0", - "postcss-normalize-url": "^6.0.2", - "postcss-normalize-whitespace": "^6.0.2", - "postcss-ordered-values": "^6.0.2", - "postcss-reduce-initial": "^6.1.0", - "postcss-reduce-transforms": "^6.0.2", - "postcss-svgo": "^6.0.3", - "postcss-unique-selectors": "^6.0.4" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/cssnano-utils": { - "version": "4.0.2", - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-calc": { - "version": "9.0.1", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.11", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.2" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-colormin": { - "version": "6.1.0", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-api": "^3.0.0", - "colord": "^2.9.3", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-convert-values": { - "version": "6.1.0", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-discard-comments": { - "version": "6.0.2", - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-discard-duplicates": { - "version": "6.0.3", - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-discard-empty": { - "version": "6.0.3", - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-discard-overridden": { - "version": "6.0.2", - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-merge-longhand": { - "version": "6.0.5", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^6.1.1" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-merge-rules": { - "version": "6.1.1", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^4.0.2", - "postcss-selector-parser": "^6.0.16" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-minify-font-values": { - "version": "6.1.0", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-minify-gradients": { - "version": "6.0.3", - "license": "MIT", - "dependencies": { - "colord": "^2.9.3", - "cssnano-utils": "^4.0.2", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-minify-params": { - "version": "6.1.0", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "cssnano-utils": "^4.0.2", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-minify-selectors": { - "version": "6.0.4", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.16" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-normalize-charset": { - "version": "6.0.2", - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-normalize-display-values": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-normalize-positions": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-normalize-repeat-style": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-normalize-string": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-normalize-timing-functions": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-normalize-unicode": { - "version": "6.1.0", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-normalize-url": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-normalize-whitespace": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-ordered-values": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "cssnano-utils": "^4.0.2", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-reduce-initial": { - "version": "6.1.0", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-api": "^3.0.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-reduce-transforms": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-svgo": { - "version": "6.0.3", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "svgo": "^3.2.0" - }, - "engines": { - "node": "^14 || ^16 || >= 18" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/postcss-unique-selectors": { - "version": "6.0.4", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.16" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/@pwrs/lit-css/node_modules/stylehacks": { - "version": "6.1.1", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "postcss-selector-parser": "^6.0.16" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "cssnano": "^7.0.6" } }, "node_modules/@rhds/icons": { @@ -11570,7 +11139,6 @@ }, "node_modules/cssnano": { "version": "7.0.6", - "dev": true, "license": "MIT", "dependencies": { "cssnano-preset-default": "^7.0.6", @@ -11589,7 +11157,6 @@ }, "node_modules/cssnano-preset-default": { "version": "7.0.6", - "dev": true, "license": "MIT", "dependencies": { "browserslist": "^4.23.3", @@ -11632,7 +11199,6 @@ }, "node_modules/cssnano-utils": { "version": "5.0.0", - "dev": true, "license": "MIT", "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" @@ -20851,7 +20417,6 @@ }, "node_modules/postcss-calc": { "version": "10.0.2", - "dev": true, "license": "MIT", "dependencies": { "postcss-selector-parser": "^6.1.2", @@ -20866,7 +20431,6 @@ }, "node_modules/postcss-colormin": { "version": "7.0.2", - "dev": true, "license": "MIT", "dependencies": { "browserslist": "^4.23.3", @@ -20883,7 +20447,6 @@ }, "node_modules/postcss-convert-values": { "version": "7.0.4", - "dev": true, "license": "MIT", "dependencies": { "browserslist": "^4.23.3", @@ -20898,7 +20461,6 @@ }, "node_modules/postcss-discard-comments": { "version": "7.0.3", - "dev": true, "license": "MIT", "dependencies": { "postcss-selector-parser": "^6.1.2" @@ -20912,7 +20474,6 @@ }, "node_modules/postcss-discard-duplicates": { "version": "7.0.1", - "dev": true, "license": "MIT", "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" @@ -20923,7 +20484,6 @@ }, "node_modules/postcss-discard-empty": { "version": "7.0.0", - "dev": true, "license": "MIT", "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" @@ -20934,7 +20494,6 @@ }, "node_modules/postcss-discard-overridden": { "version": "7.0.0", - "dev": true, "license": "MIT", "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" @@ -20945,7 +20504,6 @@ }, "node_modules/postcss-merge-longhand": { "version": "7.0.4", - "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0", @@ -20960,7 +20518,6 @@ }, "node_modules/postcss-merge-rules": { "version": "7.0.4", - "dev": true, "license": "MIT", "dependencies": { "browserslist": "^4.23.3", @@ -20977,7 +20534,6 @@ }, "node_modules/postcss-minify-font-values": { "version": "7.0.0", - "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -20991,7 +20547,6 @@ }, "node_modules/postcss-minify-gradients": { "version": "7.0.0", - "dev": true, "license": "MIT", "dependencies": { "colord": "^2.9.3", @@ -21007,7 +20562,6 @@ }, "node_modules/postcss-minify-params": { "version": "7.0.2", - "dev": true, "license": "MIT", "dependencies": { "browserslist": "^4.23.3", @@ -21023,7 +20577,6 @@ }, "node_modules/postcss-minify-selectors": { "version": "7.0.4", - "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -21091,7 +20644,6 @@ }, "node_modules/postcss-normalize-charset": { "version": "7.0.0", - "dev": true, "license": "MIT", "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" @@ -21102,7 +20654,6 @@ }, "node_modules/postcss-normalize-display-values": { "version": "7.0.0", - "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -21116,7 +20667,6 @@ }, "node_modules/postcss-normalize-positions": { "version": "7.0.0", - "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -21130,7 +20680,6 @@ }, "node_modules/postcss-normalize-repeat-style": { "version": "7.0.0", - "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -21144,7 +20693,6 @@ }, "node_modules/postcss-normalize-string": { "version": "7.0.0", - "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -21158,7 +20706,6 @@ }, "node_modules/postcss-normalize-timing-functions": { "version": "7.0.0", - "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -21172,7 +20719,6 @@ }, "node_modules/postcss-normalize-unicode": { "version": "7.0.2", - "dev": true, "license": "MIT", "dependencies": { "browserslist": "^4.23.3", @@ -21187,7 +20733,6 @@ }, "node_modules/postcss-normalize-url": { "version": "7.0.0", - "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -21201,7 +20746,6 @@ }, "node_modules/postcss-normalize-whitespace": { "version": "7.0.0", - "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -21215,7 +20759,6 @@ }, "node_modules/postcss-ordered-values": { "version": "7.0.1", - "dev": true, "license": "MIT", "dependencies": { "cssnano-utils": "^5.0.0", @@ -21238,7 +20781,6 @@ }, "node_modules/postcss-reduce-initial": { "version": "7.0.2", - "dev": true, "license": "MIT", "dependencies": { "browserslist": "^4.23.3", @@ -21253,7 +20795,6 @@ }, "node_modules/postcss-reduce-transforms": { "version": "7.0.0", - "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" @@ -21306,7 +20847,6 @@ }, "node_modules/postcss-svgo": { "version": "7.0.1", - "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0", @@ -21321,7 +20861,6 @@ }, "node_modules/postcss-unique-selectors": { "version": "7.0.3", - "dev": true, "license": "MIT", "dependencies": { "postcss-selector-parser": "^6.1.2" @@ -24208,7 +23747,6 @@ }, "node_modules/stylehacks": { "version": "7.0.4", - "dev": true, "license": "MIT", "dependencies": { "browserslist": "^4.23.3", @@ -25749,11 +25287,13 @@ } }, "node_modules/typescript-transform-lit-css": { - "version": "2.0.0", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/typescript-transform-lit-css/-/typescript-transform-lit-css-2.0.1.tgz", + "integrity": "sha512-653wRTbE8hYno91oZUWhXfvSTsXAd9IiBZYegGPYDHFipI8adNmuswa9/CQeCwEqX1buhmaLZAO/qVj0UuaX/g==", "dev": true, "license": "ISC", "dependencies": { - "@pwrs/lit-css": "^3.0.0" + "@pwrs/lit-css": "^3.0.1" }, "peerDependencies": { "clean-css": "^5", @@ -26274,10 +25814,12 @@ } }, "node_modules/web-dev-server-plugin-lit-css": { - "version": "3.0.1", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/web-dev-server-plugin-lit-css/-/web-dev-server-plugin-lit-css-3.0.2.tgz", + "integrity": "sha512-4vjTpJ3ECi73jySuIKs76ejEss5hHzSbAbdANqAkn/muFNH2SJEgI2lVn0bskP46E1vILsnTFG/q95bSwW8crw==", "license": "ISC", "dependencies": { - "@pwrs/lit-css": "^3.0.0", + "@pwrs/lit-css": "^3.0.1", "@rollup/pluginutils": "^5.1.0" } }, diff --git a/package.json b/package.json index 5cf92a5bbe..ebc9438c7f 100644 --- a/package.json +++ b/package.json @@ -292,7 +292,7 @@ "lit": "^3.2.1", "prism-esm": "^1.29.0-fix.6", "tslib": "^2.7.0", - "web-dev-server-plugin-lit-css": "^3.0.1" + "web-dev-server-plugin-lit-css": "^3.0.2" }, "devDependencies": { "@11ty/eleventy": "^3.0.0", @@ -300,8 +300,8 @@ "@11ty/eleventy-plugin-rss": "^2.0.2", "@commitlint/cli": "^19.5.0", "@commitlint/config-conventional": "^19.5.0", - "@lit-labs/eleventy-plugin-lit": "^1.0.3", - "@lit-labs/ssr": "^3.2.2", + "@lit-labs/eleventy-plugin-lit": "^1.0.4", + "@lit-labs/ssr": "^3.3.0", "@lit-labs/ssr-client": "^1.1.7", "@lit/reactive-element": "^2.0.4", "@lit/ts-transformers": "^2.0.1", @@ -356,7 +356,7 @@ "ts-patch": "^3.2.1", "tsx": "^4.19.1", "typescript": "^5.6.3", - "typescript-transform-lit-css": "^2.0.0", + "typescript-transform-lit-css": "^2.0.1", "wireit": "^0.14.9" }, "optionalDependencies": { diff --git a/patches/@patternfly+pfe-core+4.0.4.patch b/patches/@patternfly+pfe-core+4.0.4.patch index db57726aaa..8cc1be72be 100644 --- a/patches/@patternfly+pfe-core+4.0.4.patch +++ b/patches/@patternfly+pfe-core+4.0.4.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@patternfly/pfe-core/decorators/observes.js b/node_modules/@patternfly/pfe-core/decorators/observes.js -index d2a82be..67be76d 100644 +index d2a82be..5c38c6f 100644 --- a/node_modules/@patternfly/pfe-core/decorators/observes.js +++ b/node_modules/@patternfly/pfe-core/decorators/observes.js @@ -13,18 +13,30 @@ import { PropertyObserverController, } from '@patternfly/pfe-core/controllers/pr @@ -44,3 +44,4 @@ index d2a82be..67be76d 100644 }; } //# sourceMappingURL=observes.js.map +\ No newline at end of file diff --git a/patches/@patternfly+pfe-tools+4.0.1.patch b/patches/@patternfly+pfe-tools+4.0.1.patch index 4db41dd759..5e3c78476c 100644 --- a/patches/@patternfly+pfe-tools+4.0.1.patch +++ b/patches/@patternfly+pfe-tools+4.0.1.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@patternfly/pfe-tools/dev-server/plugins/import-map-generator.js b/node_modules/@patternfly/pfe-tools/dev-server/plugins/import-map-generator.js -index ad61e24..40ade17 100644 +index ad61e24..40579d3 100644 --- a/node_modules/@patternfly/pfe-tools/dev-server/plugins/import-map-generator.js +++ b/node_modules/@patternfly/pfe-tools/dev-server/plugins/import-map-generator.js @@ -92,6 +92,8 @@ export function importMapGeneratorPlugin(options) { From f65b971cb13c551bf363bb54c783256a2c669832 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Thu, 16 Jan 2025 19:16:49 +0200 Subject: [PATCH 2/4] fix(context): port decorators to @lit/context WIP --- lib/context/color/consumer.ts | 125 ++++--------------------- lib/context/color/context.ts | 6 ++ lib/context/color/controller.ts | 46 ---------- lib/context/color/provider.ts | 143 ++++++----------------------- lib/context/headings/consumer.ts | 51 +++------- lib/context/headings/context.ts | 61 ++++++++++++ lib/context/headings/controller.ts | 105 --------------------- lib/context/headings/provider.ts | 73 +++++++-------- 8 files changed, 166 insertions(+), 444 deletions(-) create mode 100644 lib/context/color/context.ts delete mode 100644 lib/context/color/controller.ts create mode 100644 lib/context/headings/context.ts delete mode 100644 lib/context/headings/controller.ts diff --git a/lib/context/color/consumer.ts b/lib/context/color/consumer.ts index 3a8d7c5d8b..e28de76c95 100644 --- a/lib/context/color/consumer.ts +++ b/lib/context/color/consumer.ts @@ -1,12 +1,9 @@ -import { isServer, type ReactiveController, type ReactiveElement } from 'lit'; +import type { ReactiveElement } from 'lit'; -import { - contextEvents, - ColorContextController, - type ColorContextOptions, -} from './controller.js'; +import { ContextConsumer, type ContextType } from '@lit/context'; +import { StyleController } from '@patternfly/pfe-core/controllers/style-controller.js'; -import { ContextRequestEvent } from '../event.js'; +import { context } from './context.js'; import styles from '@rhds/tokens/css/color-context-consumer.css.js'; @@ -21,102 +18,21 @@ export type ColorTheme = ( | 'saturated' ); -interface ColorContextConsumerOptions extends ColorContextOptions { - /** Private callback for instances where a consumer is also a provider. */ - callback?: (value: ColorTheme) => void; -} - /** * A color context consumer receives sets it's context property based on the context provided * by the closest color context provider. * The consumer has no direct access to the context, it must receive it from the provider. */ -export class ColorContextConsumer< - T extends ReactiveElement -> extends ColorContextController implements ReactiveController { - #propertyName: keyof T; - - get #propertyValue() { - return this.host[this.#propertyName] as ColorTheme; - } - - set #propertyValue(x) { - this.host[this.#propertyName] = x as T[keyof T]; - this.host.requestUpdate(); - } - - get value() { - return this.#propertyValue; - } - - #dispose?: () => void; - - #override: ColorTheme | null = null; - - constructor(host: T, private options?: ColorContextConsumerOptions) { - super(host, styles); - this.#propertyName = options?.propertyName ?? 'on' as keyof T; - } +export class ColorContextConsumer extends ContextConsumer { + /** The last-known color context on the host */ + protected last: ColorTheme | null = null; - /** When a consumer connects, it requests colour context from the closest provider. */ - async hostConnected() { - 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; - this.host.dispatchEvent(event); - this.#override = null; - } - - async hostUpdated() { - if (!isServer && !this.host.hasUpdated) { - // This is definitely overkill, but it's the only - // way we've found so far to work around lit-ssr hydration woes - const original = this.#propertyValue; - - if (original) { - await this.host.updateComplete; - this.#propertyValue = '__LIT_SSR_WORKAROUND__' as ColorTheme; - await this.host.updateComplete; - this.#propertyValue = original as ColorTheme; - } - } - } - - /** When a consumer disconnects, it's removed from the list of consumers. */ - hostDisconnected() { - this.#dispose?.(); - this.#dispose = undefined; - contextEvents.delete(this.host); - } - - /** - * Register the dispose callback for hosts that requested multiple updates, - * then update the colour-context - * @param value the color theme - * @param dispose cleanup callback - */ - #contextCallback(value: ColorTheme | null, dispose?: () => void) { - // protect against changing providers - if (dispose && dispose !== this.#dispose) { - this.#dispose?.(); - this.#dispose = dispose; - } - this.update(value); - } - - /** - * Sets the `on` attribute on the host and any children that requested multiple updates - * @param next the color theme - */ - public update(next: ColorTheme | null) { - const { last } = this; - if (!this.#override && next !== last) { - this.last = next; - this.#propertyValue = (next ?? undefined) as ColorTheme; - } - this.options?.callback?.(this.#propertyValue); + constructor( + host: ReactiveElement, + callback: (value: ContextType, dispose?: () => void) => void, + ) { + super(host, { callback, context, subscribe: true }); + new StyleController(host, styles); } } @@ -124,15 +40,12 @@ export class ColorContextConsumer< * Makes this element a color context consumer * @param options options */ -export function colorContextConsumer< - T extends ReactiveElement ->(options?: ColorContextOptions) { - return function(proto: T, _propertyName: string | keyof T) { - const propertyName = _propertyName as keyof T; - (proto.constructor as typeof ReactiveElement).addInitializer(instance => { - const controller = new ColorContextConsumer(instance as T, { propertyName, ...options }); - // @ts-expect-error: this assignment is strictly for debugging purposes - instance.__DEBUG_colorContextConsumer = controller; +export function colorContextConsumer() { + return function(proto: T, key: string | keyof T) { + (proto.constructor as typeof ReactiveElement).addInitializer((instance: ReactiveElement) => { + new ColorContextConsumer(instance, (value: ColorTheme | null) => { + (instance as T)[key as keyof T] = value as T[keyof T]; + }); }); }; } diff --git a/lib/context/color/context.ts b/lib/context/color/context.ts new file mode 100644 index 0000000000..2c6e8046b4 --- /dev/null +++ b/lib/context/color/context.ts @@ -0,0 +1,6 @@ +import type { ColorTheme } from './consumer.js'; + +import { createContextWithRoot } from '@patternfly/pfe-core/functions/context.js'; + +/** The context object which acts as the key for providers and consumers */ +export const context = createContextWithRoot('rh-color-context'); diff --git a/lib/context/color/controller.ts b/lib/context/color/controller.ts deleted file mode 100644 index f230b4fee0..0000000000 --- a/lib/context/color/controller.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { ColorTheme } from './consumer.js'; -import type { CSSResult, ReactiveController } from 'lit'; -import type { ContextEvent, Context } from '@lit/context'; - -import { ReactiveElement } from 'lit'; - -import { StyleController } from '@patternfly/pfe-core/controllers/style-controller.js'; - -import { createContextWithRoot } from '@patternfly/pfe-core/functions/context.js'; - -export type UnknownContext = Context; - -export interface ColorContextOptions { - prefix?: string; - propertyName?: keyof T; -} - -type ColorContext = typeof context; - -/** The context object which acts as the key for providers and consumers */ -export const context = createContextWithRoot('rh-color-context'); - -export const isColorContextEvent = - (event: ContextEvent): event is ContextEvent => - event.context === ColorContextController.context; - -/** - * Color context is derived from the `--context` css custom property, - * which *must* be set by the `color-palette` attribute - * This property is set (in most cases) in `color-context.scss`, - * which is added to components via `StyleController`. - * - * In this way, we avoid the need to execute javascript in order to convert from a given - * `ColorPalette` to a given `ColorTheme`, since those relationships are specified in CSS. - */ -export abstract class ColorContextController< - T extends ReactiveElement -> implements ReactiveController { - /** The last-known color context on the host */ - protected last: ColorTheme | null = null; - - constructor(protected host: T, styles: CSSStyleSheet | CSSResult) { - new StyleController(host, styles); - host.addController(this); - } -} diff --git a/lib/context/color/provider.ts b/lib/context/color/provider.ts index 37a47423d6..a411782ac9 100644 --- a/lib/context/color/provider.ts +++ b/lib/context/color/provider.ts @@ -1,22 +1,16 @@ import type { ReactiveElement } from 'lit'; -import type { ContextCallback, ContextEvent } from '@lit/context'; +import type { ColorTheme } from './consumer.js'; import { ContextProvider } from '@lit/context'; -import { context, type ColorContextOptions, type UnknownContext } from './controller.js'; - -import { isServer } from 'lit'; - -import { ColorContextController } from './controller.js'; +import { Logger } from '@patternfly/pfe-core/controllers/logger.js'; -import { ColorContextConsumer, type ColorTheme } from './consumer.js'; +import { StyleController } from '@patternfly/pfe-core/controllers/style-controller.js'; -import { Logger } from '@patternfly/pfe-core/controllers/logger.js'; +import { context } from './context.js'; import styles from '@rhds/tokens/css/color-context-provider.css.js'; -import { StyleController } from '@patternfly/pfe-core/controllers/style-controller.js'; - /** * A `ColorPalette` is a collection of specific color values * Choosing a palette sets both color properties and, if the component is a context provider, @@ -36,8 +30,7 @@ export type ColorPalette = ( | 'darkest' ); -export interface ColorContextProviderOptions - extends ColorContextOptions { +export interface ColorContextProviderOptions { /** Attribute to set context. Providers only */ attribute?: string; } @@ -47,7 +40,7 @@ export interface ColorContextProviderOptions * descendents. */ export class ColorContextProvider extends ContextProvider { - static contexts = new Map(Object.entries({ + static readonly contexts = new Map(Object.entries({ darkest: 'dark' as const, darker: 'dark' as const, dark: 'dark' as const, @@ -61,45 +54,20 @@ export class ColorContextProvider extends ContextProvider { #attribute: string; - /** Cache of context callbacks. Call each to update consumers */ - #callbacks = new Set>(); - /** Mutation observer which updates consumers when `color-palette` attribute change. */ #mo = new MutationObserver(() => this.update()); - /** - * Cached (live) computed style declaration - * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle - */ - - #initialized = false; - #logger: Logger; - #consumer: ColorContextConsumer; - - get local() { - return this.#local; - } - get #local() { - return ColorContextProvider - .contexts.get(this.host.getAttribute(this.#attribute) ?? ''); - } - - get value(): ColorTheme { - return this.#local ?? this.#consumer.value; + return ColorContextProvider.contexts.get(this.host.getAttribute(this.#attribute) ?? ''); } - constructor(protected host: T, options?: ColorContextProviderOptions) { + constructor(protected host: ReactiveElement, options?: ColorContextProviderOptions) { super(host, { context }); - this.#styles = new StyleController(host, styles); + new StyleController(host, styles); this.#logger = new Logger(host); - host.addController(this); const { attribute = 'color-palette' } = options ?? {}; - this.#consumer = new ColorContextConsumer(host, { - callback: value => this.update(value), - }); this.#attribute = attribute; if (this.#attribute !== 'color-palette') { this.#logger.warn('color context currently supports the `color-palette` attribute only.'); @@ -112,80 +80,23 @@ export class ColorContextProvider extends ContextProvider { * in case this context provider upgraded after and is closer to a given consumer. */ async hostConnected() { - this.host.addEventListener('context-request', e => this.#onChildContextRequestEvent(e)); + super.hostConnected(); this.#mo.observe(this.host, { attributes: true, attributeFilter: [this.#attribute] }); - if (!isServer) { - await this.host.updateComplete; - } - this.update(); - return true; - } - - async hostUpdated() { - if (!this.#initialized) { - this.hostConnected(); - } - this.#initialized ||= isServer || await this.hostConnected(); - if (this.#local && this.value !== this.#consumer.value) { - this.#consumer.update(this.#local); - this.update(); - console.log(this.host.localName, this.value); - } - if (!isServer) { - // This is definitely overkill, but it's the only - // way we've found so far to work around lit-ssr hydration woes - this.update(); - } } /** * When a context provider disconnects, it disconnects its mutation observer */ hostDisconnected() { - this.#callbacks.forEach(x => this.#callbacks.delete(x)); this.#mo.disconnect(); } - /** - * Was the context event fired requesting our colour-context context? - * @param event some event - */ - #isColorContextEvent( - event: ContextEvent - ): event is ContextEvent { - return event.composedPath().at(0) !== 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 - * @param event context-request event - */ - async #onChildContextRequestEvent(event: ContextEvent) { - // only handle ContextEvents relevant to colour context - if (this.#isColorContextEvent(event)) { - event.stopPropagation(); - - // Run the callback to initialize the child's colour-context - event.callback(this.value); - - // Cache the callback for future updates, if requested - if (event.subscribe) { - this.#callbacks.add(event.callback); - } - } - } - /** * Calls the context callback for all consumers * @param [force] override theme */ - public override async update(force?: ColorTheme) { - for (const cb of this.#callbacks) { - cb(force ?? this.value); - } + public update(force?: ColorTheme) { + this.setValue(force ?? this.#local ?? null); } } @@ -193,20 +104,26 @@ export class ColorContextProvider extends ContextProvider { * Makes this element a color context provider which updates its consumers when the decorated field changes * @param options options */ -export function colorContextProvider(options?: ColorContextOptions) { - return function(proto: T, _propertyName: string) { - const propertyName = _propertyName as keyof T; +export function colorContextProvider(options?: ColorContextProviderOptions) { + return (proto: ReactiveElement, propertyName: string) => { + const controllers = new WeakMap(); + const values = new Map(); const klass = (proto.constructor as typeof ReactiveElement); - const propOpts = klass.getPropertyOptions(_propertyName); + const propOpts = klass.getPropertyOptions(propertyName); const attribute = typeof propOpts.attribute === 'boolean' ? undefined : propOpts.attribute; - klass.addInitializer(instance => { - const controller = new ColorContextProvider(instance as T, { - propertyName, - attribute, - ...options, - }); - // @ts-expect-error: this assignment is strictly for debugging purposes - instance.__DEBUG_colorContextProvider = controller; + klass.addInitializer((instance: ReactiveElement) => { + controllers.set(instance, new ColorContextProvider(instance, { attribute, ...options })); + }); + Object.defineProperty(proto, propertyName, { + get(this: ReactiveElement) { + return values.get(this); + }, + set(this: ReactiveElement, value: ColorTheme) { + controllers.get(this)!.setValue(value); + values.set(this, value); + }, + enumerable: true, + configurable: true, }); }; } diff --git a/lib/context/headings/consumer.ts b/lib/context/headings/consumer.ts index 2d4fc84c9f..4822826c30 100644 --- a/lib/context/headings/consumer.ts +++ b/lib/context/headings/consumer.ts @@ -1,6 +1,8 @@ -import { ContextRequestEvent } from '../event.js'; +import type { ReactiveElement } from 'lit'; -import { contextEvents, HeadingLevelController } from './controller.js'; +import { ContextConsumer } from '@lit/context'; + +import { context } from './context.js'; export interface HeadingTemplateOptions { id?: string; @@ -8,45 +10,22 @@ export interface HeadingTemplateOptions { level?: number; } +export { wrap } from './context.js'; + /** * Determines which heading level immediately precedes the host element, * and provides templates for shadow headings. */ -export class HeadingLevelContextConsumer extends HeadingLevelController { - #dispose?: () => void; +export class HeadingLevelContextConsumer extends ContextConsumer { + public level = 1; - /** When a consumer connects, it requests context from the closest provider. */ - hostConnected() { - const { context } = HeadingLevelController; - const event = new ContextRequestEvent( + constructor(host: ReactiveElement) { + super(host, { context, - e => this.#contextCallback(e), - true, - ); - this.host.dispatchEvent(event); - contextEvents.set(this.host, event); - } - - /** When a consumer disconnects, it's removed from the list of consumers. */ - hostDisconnected() { - this.#dispose?.(); - this.#dispose = undefined; - contextEvents.delete(this.host); - } - - /** Register the dispose callback for hosts that requested multiple updates, then update the colour-context */ - #contextCallback(value: number, dispose?: () => void) { - // protect against changing providers - if (dispose && dispose !== this.#dispose) { - this.#dispose?.(); - this.#dispose = dispose; - } - this.update(value); - } - - /** Sets the heading level on the host and any children that requested multiple updates */ - public update(next: number) { - this.level = next; - this.host.requestUpdate(); + subscribe: true, + callback: value => { + this.level = value.level ?? 1; + }, + }); } } diff --git a/lib/context/headings/context.ts b/lib/context/headings/context.ts new file mode 100644 index 0000000000..d0d8aeda72 --- /dev/null +++ b/lib/context/headings/context.ts @@ -0,0 +1,61 @@ +import type { HeadingLevelContextConsumer } from './consumer.js'; +import type { HeadingLevelContextProvider } from './provider.js'; + +import { html } from 'lit'; + +import { createContextWithRoot } from '@patternfly/pfe-core/functions/context.js'; +import { ifDefined } from 'lit-html/directives/if-defined.js'; + +export interface HeadingLevelTemplateOptions { + id?: string; + hidden?: boolean; +} + +export interface HeadingLevelContextOptions { + /** Root Heading level. default 1 */ + level?: number; + /** Heading offset for children. default 1 */ + offset?: number; + /** + * Attribute to read on the host which will determine root heading level. + */ + attribute?: string; + /** + * Only for providers which are also consumers. + */ + parent?: HeadingLevelContextConsumer; +} + +interface HeadingLevelContext { + level: number; + offset?: number; +} + +/** + * Determines which heading level immediately precedes the host element, + * and provides templates for shadow headings. + */ +export const context = createContextWithRoot('rh-heading-level-context'); + +/** + * Wraps any renderable content in a heading, based on heading level + * @param content + * @param options + */ +export function wrap( + this: HeadingLevelContextConsumer | HeadingLevelContextProvider, + content: unknown, + options?: HeadingLevelTemplateOptions, +) { + const { level, offset } = this.value ?? { level: 1, offset: 1 }; + const id = options?.id; + const hidden = options?.hidden ?? false; + switch (Math.max(1, level + (offset ?? 1))) { + case 1: return html`

${content}

`; + case 2: return html`

${content}

`; + case 3: return html`

${content}

`; + case 4: return html`

${content}

`; + case 5: return html`
${content}
`; + default: return html`
${content}
`; + } +} diff --git a/lib/context/headings/controller.ts b/lib/context/headings/controller.ts deleted file mode 100644 index 9b42acac21..0000000000 --- a/lib/context/headings/controller.ts +++ /dev/null @@ -1,105 +0,0 @@ -import type { ReactiveController, ReactiveElement, TemplateResult } from 'lit'; -import { html } from 'lit'; -import { ifDefined } from 'lit/directives/if-defined.js'; - -import { - createContext, - type ContextRequestEvent, - type UnknownContext, -} from '../event.js'; - -import type { HeadingLevelContextConsumer } from './consumer.js'; - -export interface HeadingLevelTemplateOptions { - id?: string; - hidden?: boolean; -} - -export interface HeadingLevelContextOptions { - /** Root Heading level. default 1 */ - level?: number; - /** Heading offset for children. default 1 */ - offset?: number; - /** - * Attribute to read on the host which will determine root heading level. - */ - attribute?: string; - /** - * Only for providers which are also consumers. - */ - parent?: HeadingLevelContextConsumer; -} - -/** - * Maps from consumer host elements to already-fired request events - * We hold these in memory in order to re-fire the events every time a new provider connects. - * This is a hedge against cases where an early-upgrading provider claims an early-upgrading - * consumer before a late-upgrading provider has a chance to register as the rightful provider - * @example Monkey-in-the-middle error - * In this example, we must re-fire the event from eager-consumer when late-provider - * upgrades, so as to ensure that late-provider claims it for itself - * ```html - * - * - * - * - * - * ``` - */ -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 { - public static readonly context = createContext(Symbol('rh-heading-level-context')); - - public offset: number; - - #level = 1; - - get level(): number { - return this.#level; - } - - set level(level: string | number | undefined | null) { - const val = typeof level === 'string' ? parseInt(level) : level; - if (typeof val === 'number' && !Number.isNaN(val)) { - this.#level = val; - } - } - - constructor( - protected host: ReactiveElement, - /** Heading level preceding component document, as in 1 for

, 2 for

etc. */ - protected options?: HeadingLevelContextOptions, - ) { - host.addController(this); - this.offset = options?.offset ?? 1; - if (options?.parent) { - this.level = options.parent.level; - } else if (options?.level) { - this.level = options.level; - } - } - - hostConnected?(): void; - - /** - * Wraps any renderable content in a heading, based on heading level - */ - wrap(content: unknown, options?: HeadingLevelTemplateOptions): TemplateResult { - const level = Math.max(1, this.level + this.offset); - const id = options?.id; - const hidden = options?.hidden ?? false; - switch (level) { - case 1: return html`

${content}

`; - case 2: return html`

${content}

`; - case 3: return html`

${content}

`; - case 4: return html`

${content}

`; - case 5: return html`
${content}
`; - default: return html`
${content}
`; - } - } -} diff --git a/lib/context/headings/provider.ts b/lib/context/headings/provider.ts index c6b63a1216..d5a4b1c000 100644 --- a/lib/context/headings/provider.ts +++ b/lib/context/headings/provider.ts @@ -1,10 +1,13 @@ -import { contextEvents, HeadingLevelController } from './controller.js'; +import type { HeadingLevelContextOptions } from './context.js'; +import type { ReactiveElement } from 'lit'; -import { - ContextRequestEvent, - type UnknownContext, - type ContextCallback, -} from '../event.js'; +import { isServer } from 'lit'; + +import { ContextProvider } from '@lit/context'; + +import { context } from './context.js'; + +export { wrap } from './context.js'; const SELECTORS = `H1,H2,H3,H4,H5,H6`; @@ -30,18 +33,35 @@ function canQuery(node: Node): node is Document | ShadowRoot { * Determines which heading level immediately precedes the host element, * and provides templates for shadow headings. */ -export class HeadingLevelContextProvider extends HeadingLevelController { - /** Cache of context callbacks. Call each to update consumers */ - #callbacks = new Set>(); +export class HeadingLevelContextProvider extends ContextProvider { + get level(): number { + return this.value.level; + } + + set level(lvl: string | number | undefined | null) { + const level = typeof lvl === 'string' ? parseInt(lvl) : lvl; + if (typeof level === 'number' && !Number.isNaN(level)) { + const { offset } = this.value; + this.setValue({ level, offset }); + } + } + + constructor( + host: ReactiveElement, + /** Heading level preceding component document, as in 1 for

, 2 for

etc. */ + protected options?: HeadingLevelContextOptions, + ) { + super(host, { context, initialValue: { + offset: options?.offset ?? 1, + level: options?.level ?? 1, + } }); + } hostConnected() { - this.host.addEventListener('context-request', e => this.#onChildContextRequestEvent(e)); - for (const [host, fired] of contextEvents) { - host.dispatchEvent(fired); + super.hostConnected(); + if (!isServer) { + this.level = this.#computeLevelFromChildren(); } - this.level = - this.host.getAttribute(this.options?.attribute ?? '') - ?? this.#computeLevelFromChildren(); } #computeLevelFromChildren() { @@ -59,27 +79,4 @@ export class HeadingLevelContextProvider extends HeadingLevelController { } } } - - /** Was the context event fired requesting our colour-context context? */ - #isHeadingContextRequestEvent( - event: ContextRequestEvent - ): event is ContextRequestEvent { - return event.target !== this.host && event.context === HeadingLevelController.context; - } - - 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(); - - // Run the callback to initialize the child's value - event.callback(this.level); - - // Cache the callback for future updates, if requested - if (event.subscribe) { - this.#callbacks.add(event.callback); - } - } - } } From 773e084890f242d786a3467eb9d4bbd22486f762 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Thu, 16 Jan 2025 19:17:07 +0200 Subject: [PATCH 3/4] fix: port headings controller callsites --- elements/rh-accordion/rh-accordion-header.ts | 5 +++-- elements/rh-audio-player/rh-audio-player-about.ts | 4 ++-- elements/rh-audio-player/rh-audio-player-subscribe.ts | 4 ++-- elements/rh-audio-player/rh-cue.ts | 4 ++-- elements/rh-audio-player/rh-transcript.ts | 10 +++------- 5 files changed, 12 insertions(+), 15 deletions(-) diff --git a/elements/rh-accordion/rh-accordion-header.ts b/elements/rh-accordion/rh-accordion-header.ts index b41e325297..6c8c14d107 100644 --- a/elements/rh-accordion/rh-accordion-header.ts +++ b/elements/rh-accordion/rh-accordion-header.ts @@ -18,7 +18,8 @@ import { consume } from '@lit/context'; import { context } from './context.js'; import styles from './rh-accordion-header.css'; -import { HeadingLevelController } from '@rhds/elements/lib/context/headings/controller.js'; + +import { HeadingLevelContextProvider } from '@rhds/elements/lib/context/headings/provider.js'; export class AccordionHeaderChangeEvent extends Event { declare target: RhAccordionHeader; @@ -65,7 +66,7 @@ export class RhAccordionHeader extends LitElement { ariaLevel: '2', }); - #heading = new HeadingLevelController(this); + #heading = new HeadingLevelContextProvider(this); override connectedCallback() { super.connectedCallback(); diff --git a/elements/rh-audio-player/rh-audio-player-about.ts b/elements/rh-audio-player/rh-audio-player-about.ts index 42306bbccf..946714b49f 100644 --- a/elements/rh-audio-player/rh-audio-player-about.ts +++ b/elements/rh-audio-player/rh-audio-player-about.ts @@ -9,7 +9,7 @@ import '@rhds/elements/rh-avatar/rh-avatar.js'; import panelStyles from './rh-audio-player-panel.css'; import styles from './rh-audio-player-about.css'; -import { HeadingLevelContextConsumer } from '../../lib/context/headings/consumer.js'; +import { HeadingLevelContextConsumer, wrap } from '../../lib/context/headings/consumer.js'; /** * Audio Player About Panel @@ -42,7 +42,7 @@ export class RhAudioPlayerAbout extends LitElement { override render() { const { label, mediaseries, mediatitle } = this; const hasContent = (this.content?.length ?? 0) >= 1; - const heading = this.#headings.wrap(mediatitle ?? ''); + const heading = wrap.call(this.#headings, mediatitle ?? ''); return html` diff --git a/elements/rh-audio-player/rh-audio-player-subscribe.ts b/elements/rh-audio-player/rh-audio-player-subscribe.ts index 2f2cc0c984..9eebbe7920 100644 --- a/elements/rh-audio-player/rh-audio-player-subscribe.ts +++ b/elements/rh-audio-player/rh-audio-player-subscribe.ts @@ -3,7 +3,7 @@ import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; import { queryAssignedElements } from 'lit/decorators/query-assigned-elements.js'; -import { HeadingLevelContextConsumer } from '../../lib/context/headings/consumer.js'; +import { HeadingLevelContextConsumer, wrap } from '../../lib/context/headings/consumer.js'; import './rh-audio-player-scrolling-text-overflow.js'; @@ -37,7 +37,7 @@ export class RhAudioPlayerSubscribe extends LitElement { override render() { return html` - ${this.#headings.wrap(this.menuLabel)} + ${wrap.call(this.#headings, this.menuLabel)} `; diff --git a/elements/rh-audio-player/rh-cue.ts b/elements/rh-audio-player/rh-cue.ts index 30cd0f0718..0d72173b12 100644 --- a/elements/rh-audio-player/rh-cue.ts +++ b/elements/rh-audio-player/rh-cue.ts @@ -2,7 +2,7 @@ import { LitElement, html, nothing } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; -import { HeadingLevelContextConsumer } from '../../lib/context/headings/consumer.js'; +import { HeadingLevelContextConsumer, wrap } from '../../lib/context/headings/consumer.js'; import styles from './rh-cue.css'; @@ -105,7 +105,7 @@ export class RhCue extends LitElement { render() { const { start, voice } = this; - return html`${!this.#hasVoice ? nothing : this.#headings.wrap(this.#linkTemplate(html` + return html`${!this.#hasVoice ? nothing : wrap.call(this.#headings, this.#linkTemplate(html` ${start} - ${voice}`, true))}${this.#linkTemplate(html` `)}`; diff --git a/elements/rh-audio-player/rh-transcript.ts b/elements/rh-audio-player/rh-transcript.ts index a68812623d..98ea6b55af 100644 --- a/elements/rh-audio-player/rh-transcript.ts +++ b/elements/rh-audio-player/rh-transcript.ts @@ -6,8 +6,7 @@ import { queryAssignedElements } from 'lit/decorators/query-assigned-elements.js import { RhCue, getFormattedTime } from './rh-cue.js'; -import { HeadingLevelContextConsumer } from '@rhds/elements/lib/context/headings/consumer.js'; -import { HeadingLevelContextProvider } from '@rhds/elements/lib/context/headings/provider.js'; +import { HeadingLevelContextProvider, wrap } from '@rhds/elements/lib/context/headings/provider.js'; import buttonStyles from './rh-audio-player-button.css'; import panelStyles from './rh-audio-player-panel.css'; @@ -48,10 +47,7 @@ export class RhTranscript extends LitElement { #duration?: number; - #headings = new HeadingLevelContextProvider(this, { - offset: 0, - parent: new HeadingLevelContextConsumer(this), - }); + #headings = new HeadingLevelContextProvider(this, { offset: 0 }); set autoscrollLabel(label: string) { this._autoscroll = label; @@ -84,7 +80,7 @@ export class RhTranscript extends LitElement { render() { return html` - ${this.#headings.wrap(this.menuLabel)} + ${wrap.call(this.#headings, this.menuLabel)}
${this._cues.length < 0 ? '' : html`