From d5c27c2a9a9aef631893da568b27ce9976119f11 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Mon, 13 Jan 2025 12:53:40 +0200 Subject: [PATCH 01/17] chore: nvm use lts --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index 790e1105f2..2c022021b8 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.10.0 +v22.13.0 From ba4408c40a27ab3319f65dcbdd8043a21f2ccabb Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Mon, 13 Jan 2025 12:53:53 +0200 Subject: [PATCH 02/17] chore: bump lit-labs/ssr --- package-lock.json | 24 ++++++++++++------------ tools/pfe-tools/package.json | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 196e03c708..cdd25761a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,7 +53,7 @@ }, "core/pfe-core": { "name": "@patternfly/pfe-core", - "version": "4.0.1", + "version": "4.0.4", "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.6.10", @@ -63,7 +63,7 @@ }, "elements": { "name": "@patternfly/elements", - "version": "4.0.1", + "version": "4.0.2", "license": "MIT", "dependencies": { "@lit/context": "^1.1.2", @@ -2795,13 +2795,13 @@ } }, "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==", "peer": true, "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", @@ -2828,9 +2828,9 @@ } }, "node_modules/@lit-labs/ssr-dom-shim": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz", - "integrity": "sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ==" + "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==" }, "node_modules/@lit-labs/ssr/node_modules/@types/node": { "version": "16.18.105", @@ -17593,7 +17593,7 @@ }, "tools/pfe-tools": { "name": "@patternfly/pfe-tools", - "version": "3.0.1", + "version": "4.0.1", "license": "MIT", "devDependencies": { "@types/dedent": "^0.7.2", @@ -17611,14 +17611,14 @@ "node": ">=18" }, "peerDependencies": { - "@11ty/eleventy": "^2.0.1", + "@11ty/eleventy": "^2.0.1 || ^3.0.0", "@11ty/eleventy-plugin-directory-output": "^1.0.1", "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0", "@changesets/cli": "^2.27.1", "@custom-elements-manifest/analyzer": "^0.5.7", "@jspm/generator": "^2.0.1", "@koa/router": "^12.0.1", - "@lit-labs/ssr": "^3.2.2", + "@lit-labs/ssr": "^3.3.0", "@open-wc/testing": "^4.0.0", "@playwright/test": "^1.44.0", "@rollup/plugin-replace": "^5.0.5", diff --git a/tools/pfe-tools/package.json b/tools/pfe-tools/package.json index c8df6670bb..67283ae623 100644 --- a/tools/pfe-tools/package.json +++ b/tools/pfe-tools/package.json @@ -70,7 +70,7 @@ "@custom-elements-manifest/analyzer": "^0.5.7", "@jspm/generator": "^2.0.1", "@koa/router": "^12.0.1", - "@lit-labs/ssr": "^3.2.2", + "@lit-labs/ssr": "^3.3.0", "@open-wc/testing": "^4.0.0", "@playwright/test": "^1.44.0", "@rollup/plugin-replace": "^5.0.5", From cd54b8bfd073cb560b78dab26892f905b1fb8473 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Mon, 13 Jan 2025 13:36:42 +0200 Subject: [PATCH 03/17] chore: bump lit/context --- core/pfe-core/package.json | 2 +- package-lock.json | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/core/pfe-core/package.json b/core/pfe-core/package.json index 55fdb012d6..2950d595f1 100644 --- a/core/pfe-core/package.json +++ b/core/pfe-core/package.json @@ -62,7 +62,7 @@ }, "dependencies": { "@floating-ui/dom": "^1.6.10", - "@lit/context": "^1.1.2", + "@lit/context": "^1.1.3", "lit": "^3.2.0" }, "repository": { diff --git a/package-lock.json b/package-lock.json index cdd25761a9..dee8b97917 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,7 +57,7 @@ "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.6.10", - "@lit/context": "^1.1.2", + "@lit/context": "^1.1.3", "lit": "^3.2.0" } }, @@ -2839,9 +2839,10 @@ "peer": true }, "node_modules/@lit/context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.2.tgz", - "integrity": "sha512-S0nw2C6Tkm7fVX5TGYqeROGD+Z9Coa2iFpW+ysYBDH3YvCqOY3wVQvSgwbaliLJkjTnSEYCBe9qFqKV8WUFpVw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.3.tgz", + "integrity": "sha512-Auh37F4S0PZM93HTDfZWs97mmzaQ7M3vnTc9YvxAGyP3UItSK/8Fs0vTOGT+njuvOwbKio/l8Cx/zWL4vkutpQ==", + "license": "BSD-3-Clause", "dependencies": { "@lit/reactive-element": "^1.6.2 || ^2.0.0" } From 1bddaa9f43055ceac7e913df9ef220bf20c6c2eb Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Mon, 13 Jan 2025 13:37:02 +0200 Subject: [PATCH 04/17] fix: update context usage for ssr --- .changeset/clear-pugs-make.md | 5 +++++ core/pfe-core/functions/context.ts | 5 +++++ core/pfe-core/ssr-shims.ts | 3 ++- 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .changeset/clear-pugs-make.md diff --git a/.changeset/clear-pugs-make.md b/.changeset/clear-pugs-make.md new file mode 100644 index 0000000000..c4b55e1a98 --- /dev/null +++ b/.changeset/clear-pugs-make.md @@ -0,0 +1,5 @@ +--- +"@patternfly/pfe-core": patch +"@patternfly/elements": patch +--- +Enable context protocol in SSR scenarios. diff --git a/core/pfe-core/functions/context.ts b/core/pfe-core/functions/context.ts index 2e109b6b2c..d3ebc1f0e4 100644 --- a/core/pfe-core/functions/context.ts +++ b/core/pfe-core/functions/context.ts @@ -7,6 +7,11 @@ function makeContextRoot() { const root = new ContextRoot(); if (!isServer) { root.attach(document.body); + } else { + root.attach( + // @ts-expect-error: enable context root in ssr + globalThis.litServerRoot, + ); } return root; } diff --git a/core/pfe-core/ssr-shims.ts b/core/pfe-core/ssr-shims.ts index 2fe31e6d6a..dd6248e381 100644 --- a/core/pfe-core/ssr-shims.ts +++ b/core/pfe-core/ssr-shims.ts @@ -34,6 +34,8 @@ globalThis.window ??= globalThis; globalThis.document ??= new MiniDocument(); // @ts-expect-error: this runs in node globalThis.navigator ??= { userAgent: '' }; +// @ts-expect-error: opt in to event support in ssr +globalThis.litSsrCallConnectedCallback = true; // @ts-expect-error: this runs in node globalThis.ErrorEvent ??= Event; // @ts-expect-error: this runs in node @@ -55,4 +57,3 @@ globalThis.getComputedStyle ??= function() { } ; - From ed2a4c94947c9e8f2580f03236de1cb6570c5296 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Mon, 13 Jan 2025 13:46:47 +0200 Subject: [PATCH 05/17] fix: use lit-ssr html function --- tools/pfe-tools/ssr/ssr.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/pfe-tools/ssr/ssr.ts b/tools/pfe-tools/ssr/ssr.ts index a7a956fbac..4239b6c0e8 100644 --- a/tools/pfe-tools/ssr/ssr.ts +++ b/tools/pfe-tools/ssr/ssr.ts @@ -1,9 +1,9 @@ -import { render } from '@lit-labs/ssr'; +import { render, html } from '@lit-labs/ssr'; import { collectResult } from '@lit-labs/ssr/lib/render-result.js'; -import { html } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; /** + * composes the `unsafeHTML`, `html`, `render`, and `collectResult` functions from lit ssr * @param input html partial */ export async function ssr(input: string): Promise { From 0088148e6ef1f4f7c71476edeacb59d03ad519e7 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Mon, 13 Jan 2025 14:52:11 +0200 Subject: [PATCH 06/17] fix(core): rely on lit's dom-shim --- core/pfe-core/ssr-shims.ts | 57 +++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/core/pfe-core/ssr-shims.ts b/core/pfe-core/ssr-shims.ts index dd6248e381..32d0168960 100644 --- a/core/pfe-core/ssr-shims.ts +++ b/core/pfe-core/ssr-shims.ts @@ -1,3 +1,5 @@ +import { installWindowOnGlobal } from '@lit-labs/ssr/lib/dom-shim.js'; + class ObserverShim { observe(): void { void 0; @@ -17,35 +19,7 @@ class MiniHTMLTemplateElement extends MiniHTMLElement { content = { cloneNode: (): string => this.innerHTML }; } -class MiniDocument { - createElement(tagName: string): MiniHTMLElement { - switch (tagName) { - case 'template': - return new MiniHTMLTemplateElement(tagName); - default: - return new MiniHTMLElement(tagName); - } - } -} - -// @ts-expect-error: this runs in node -globalThis.window ??= globalThis; -// @ts-expect-error: this runs in node -globalThis.document ??= new MiniDocument(); -// @ts-expect-error: this runs in node -globalThis.navigator ??= { userAgent: '' }; -// @ts-expect-error: opt in to event support in ssr -globalThis.litSsrCallConnectedCallback = true; -// @ts-expect-error: this runs in node -globalThis.ErrorEvent ??= Event; -// @ts-expect-error: this runs in node -globalThis.IntersectionObserver ??= ObserverShim; -// @ts-expect-error: this runs in node -globalThis.MutationObserver ??= ObserverShim; -// @ts-expect-error: this runs in node -globalThis.ResizeObserver ??= ObserverShim; -// @ts-expect-error: this runs in node -globalThis.getComputedStyle ??= function() { +function getComputedStyle() { return { getPropertyPriority() { return ''; @@ -54,6 +28,27 @@ globalThis.getComputedStyle ??= function() { return ''; }, }; -} +}; -; +// @ts-expect-error: opt in to event support in ssr +globalThis.litSsrCallConnectedCallback = true; + +installWindowOnGlobal({ + ErrorEvent: Event, + IntersectionObserver: ObserverShim, + MutationObserver: ObserverShim, + ResizeObserver: ObserverShim, + getComputedStyle, +}); + +// @ts-expect-error: this runs in node +globalThis.navigator.userAgent ??= '@lit-labs/ssr'; + +globalThis.document.createElement = function createElement(tagName: string): HTMLElement { + switch (tagName) { + case 'template': + return new MiniHTMLTemplateElement(tagName) as unknown as HTMLElement; + default: + return new MiniHTMLElement(tagName) as HTMLElement; + } +}; From 837f66a4e6a2f0d40187b5f05a57587484805df7 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Mon, 13 Jan 2025 14:52:32 +0200 Subject: [PATCH 07/17] chore: update and align lit version --- core/pfe-core/package.json | 2 +- elements/package.json | 2 +- package-lock.json | 15 ++++++++------- package.json | 2 +- tools/pfe-tools/package.json | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/core/pfe-core/package.json b/core/pfe-core/package.json index 2950d595f1..1b84a32f27 100644 --- a/core/pfe-core/package.json +++ b/core/pfe-core/package.json @@ -63,7 +63,7 @@ "dependencies": { "@floating-ui/dom": "^1.6.10", "@lit/context": "^1.1.3", - "lit": "^3.2.0" + "lit": "^3.2.1" }, "repository": { "type": "git", diff --git a/elements/package.json b/elements/package.json index 4b159d1d9c..f82c073a11 100644 --- a/elements/package.json +++ b/elements/package.json @@ -131,7 +131,7 @@ "@lit/context": "^1.1.2", "@patternfly/icons": "^1.0.3", "@patternfly/pfe-core": "^4.0.1", - "lit": "^3.2.0", + "lit": "^3.2.1", "tslib": "^2.6.3" } } diff --git a/package-lock.json b/package-lock.json index dee8b97917..033bc9e7ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "fuse.js": "^7.0.0", "husky": "^9.1.4", "leasot": "^14.4.0", - "lit": "^3.2.0", + "lit": "^3.2.1", "nunjucks": "^3.2.4", "postcss-nesting": "^13.0.0", "prompts": "^2.4.2", @@ -58,7 +58,7 @@ "dependencies": { "@floating-ui/dom": "^1.6.10", "@lit/context": "^1.1.3", - "lit": "^3.2.0" + "lit": "^3.2.1" } }, "elements": { @@ -69,7 +69,7 @@ "@lit/context": "^1.1.2", "@patternfly/icons": "^1.0.3", "@patternfly/pfe-core": "^4.0.1", - "lit": "^3.2.0", + "lit": "^3.2.1", "tslib": "^2.6.3" } }, @@ -11442,9 +11442,10 @@ "peer": true }, "node_modules/lit": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.2.0.tgz", - "integrity": "sha512-s6tI33Lf6VpDu7u4YqsSX78D28bYQulM+VAzsGch4fx2H0eLZnJsUBsPWmGYSGoKDNbjtRv02rio1o+UdPVwvw==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.2.1.tgz", + "integrity": "sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==", + "license": "BSD-3-Clause", "dependencies": { "@lit/reactive-element": "^2.0.4", "lit-element": "^4.1.0", @@ -17649,7 +17650,7 @@ "eslint": "^9.0.0", "execa": "^9.3.0", "glob": "^10.3.12", - "lit": "^3.1.2", + "lit": "^3.2.1", "markdown-it-anchor": "^8.6.7", "nunjucks": "^3.2.4", "patch-package": "^8.0.0", diff --git a/package.json b/package.json index 6bf63797e6..4f6d8fee37 100644 --- a/package.json +++ b/package.json @@ -314,7 +314,7 @@ "fuse.js": "^7.0.0", "husky": "^9.1.4", "leasot": "^14.4.0", - "lit": "^3.2.0", + "lit": "^3.2.1", "nunjucks": "^3.2.4", "postcss-nesting": "^13.0.0", "prompts": "^2.4.2", diff --git a/tools/pfe-tools/package.json b/tools/pfe-tools/package.json index 67283ae623..185ef125dd 100644 --- a/tools/pfe-tools/package.json +++ b/tools/pfe-tools/package.json @@ -100,7 +100,7 @@ "eslint": "^9.0.0", "execa": "^9.3.0", "glob": "^10.3.12", - "lit": "^3.1.2", + "lit": "^3.2.1", "markdown-it-anchor": "^8.6.7", "nunjucks": "^3.2.4", "patch-package": "^8.0.0", From a1271145770f05973343d0671d689b17564d31de Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Mon, 13 Jan 2025 14:52:45 +0200 Subject: [PATCH 08/17] fix(core): ssr connected callback --- core/pfe-core/controllers/light-dom-controller.ts | 14 +++++++++----- core/pfe-core/controllers/scroll-spy-controller.ts | 4 ++-- core/pfe-core/controllers/slot-controller.ts | 8 ++++++-- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/core/pfe-core/controllers/light-dom-controller.ts b/core/pfe-core/controllers/light-dom-controller.ts index e53d11cd5c..321010a467 100644 --- a/core/pfe-core/controllers/light-dom-controller.ts +++ b/core/pfe-core/controllers/light-dom-controller.ts @@ -1,4 +1,4 @@ -import type { ReactiveController, ReactiveElement } from 'lit'; +import { isServer, type ReactiveController, type ReactiveElement } from 'lit'; import { Logger } from './logger.js'; @@ -52,9 +52,13 @@ export class LightDOMController implements ReactiveController { * Returns a boolean statement of whether or not this component contains any light DOM. */ hasLightDOM(): boolean { - return !!( - this.host.children.length > 0 - || (this.host.textContent ?? '').trim().length > 0 - ); + if (isServer) { + return false; + } else { + return !!( + this.host.children.length > 0 + || (this.host.textContent ?? '').trim().length > 0 + ); + } } } diff --git a/core/pfe-core/controllers/scroll-spy-controller.ts b/core/pfe-core/controllers/scroll-spy-controller.ts index 89af9d2481..90cb254b1d 100644 --- a/core/pfe-core/controllers/scroll-spy-controller.ts +++ b/core/pfe-core/controllers/scroll-spy-controller.ts @@ -44,7 +44,7 @@ export class ScrollSpyController implements ReactiveController { #rootMargin?: string; #threshold: number | number[]; - #getRootNode: () => Node; + #getRootNode: () => Node | null; #getHash: (el: Element) => string | null; get #linkChildren(): Element[] { @@ -92,7 +92,7 @@ export class ScrollSpyController implements ReactiveController { this.#rootMargin = options.rootMargin; this.#activeAttribute = options.activeAttribute ?? 'active'; this.#threshold = options.threshold ?? 0.85; - this.#getRootNode = () => options.rootNode ?? host.getRootNode(); + this.#getRootNode = () => options.rootNode ?? host.getRootNode?.() ?? null; this.#getHash = options?.getHash ?? ((el: Element) => el.getAttribute('href')); } diff --git a/core/pfe-core/controllers/slot-controller.ts b/core/pfe-core/controllers/slot-controller.ts index a18344b989..2a7671bc33 100644 --- a/core/pfe-core/controllers/slot-controller.ts +++ b/core/pfe-core/controllers/slot-controller.ts @@ -191,8 +191,12 @@ export class SlotController implements ReactiveController { #getChildrenForSlot( name: string | typeof SlotController.default, ): T[] { - const children = Array.from(this.host.children) as T[]; - return children.filter(isSlot(name)); + if (isServer) { + return []; + } else { + const children = Array.from(this.host.children) as T[]; + return children.filter(isSlot(name)); + } } #initSlot = (slotName: string | null) => { From 1fffb2c3477024065925f9a784528a109fb2b2ad Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Mon, 13 Jan 2025 14:53:15 +0200 Subject: [PATCH 09/17] fix(elements): ssr connected callbacks --- elements/pf-back-to-top/pf-back-to-top.ts | 4 +++- elements/pf-clipboard-copy/pf-clipboard-copy.ts | 8 +++++--- elements/pf-table/pf-table.ts | 2 +- elements/pf-tabs/pf-tab.ts | 7 ++++--- elements/pf-tooltip/pf-tooltip.ts | 4 ++-- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/elements/pf-back-to-top/pf-back-to-top.ts b/elements/pf-back-to-top/pf-back-to-top.ts index 7912dcd2eb..3d3da33ec9 100644 --- a/elements/pf-back-to-top/pf-back-to-top.ts +++ b/elements/pf-back-to-top/pf-back-to-top.ts @@ -158,7 +158,9 @@ export class PfBackToTop extends LitElement { } this.#scrollSpy = !!this.scrollableSelector; - if (this.#scrollSpy && this.scrollableSelector) { + if (isServer) { + return; + } else if (this.#scrollSpy && this.scrollableSelector) { const scrollableElement = this.#rootNode?.querySelector?.(this.scrollableSelector); if (!scrollableElement) { this.#logger.error(`unable to find element with selector ${this.scrollableSelector}`); diff --git a/elements/pf-clipboard-copy/pf-clipboard-copy.ts b/elements/pf-clipboard-copy/pf-clipboard-copy.ts index 6b69717f93..1dfbf823a7 100644 --- a/elements/pf-clipboard-copy/pf-clipboard-copy.ts +++ b/elements/pf-clipboard-copy/pf-clipboard-copy.ts @@ -1,4 +1,4 @@ -import { LitElement, html, type TemplateResult } from 'lit'; +import { LitElement, html, isServer, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; import { classMap } from 'lit/directives/class-map.js'; @@ -108,7 +108,9 @@ export class PfClipboardCopy extends LitElement { connectedCallback(): void { super.connectedCallback(); this.#mo.observe(this, { characterData: true }); - this.#onMutation(); + if (!isServer) { + this.#onMutation(); + } } /** @@ -167,7 +169,7 @@ export class PfClipboardCopy extends LitElement { } #onMutation() { - if (this.childNodes.length > 0) { + if (this.childNodes?.length > 0) { this.value = this.getAttribute('value') ?? this.#dedent(Array.from(this.childNodes, child => (child instanceof Element || child instanceof Text) ? (child.textContent ?? '') : '') .join('')); diff --git a/elements/pf-table/pf-table.ts b/elements/pf-table/pf-table.ts index 4611daf11c..bf44f8f7dc 100644 --- a/elements/pf-table/pf-table.ts +++ b/elements/pf-table/pf-table.ts @@ -708,7 +708,7 @@ export class PfTable extends LitElement { } #onSlotchange() { - this.columns = this.querySelector('pf-tr')?.querySelectorAll('pf-th')?.length ?? 0; + this.columns = this.querySelector?.('pf-tr')?.querySelectorAll('pf-th')?.length ?? 0; this.requestUpdate(); } diff --git a/elements/pf-tabs/pf-tab.ts b/elements/pf-tabs/pf-tab.ts index 69c9b1ca71..601daa5e9d 100644 --- a/elements/pf-tabs/pf-tab.ts +++ b/elements/pf-tabs/pf-tab.ts @@ -1,4 +1,4 @@ -import { LitElement, html, type TemplateResult } from 'lit'; +import { LitElement, html, isServer, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; import { queryAssignedElements } from 'lit/decorators/query-assigned-elements.js'; @@ -63,7 +63,7 @@ export class PfTab extends LitElement { static readonly styles: CSSStyleSheet[] = [styles]; @queryAssignedElements({ slot: 'icon', flatten: true }) - private icons!: HTMLElement[]; + private icons?: HTMLElement[]; @property({ reflect: true, type: Boolean }) active = false; @@ -105,13 +105,14 @@ export class PfTab extends LitElement { const { box, fill = false, vertical = false } = this.ctx ?? {}; const light = box === 'light'; const dark = box === 'dark'; + const icons = isServer ? [] : this.icons; return html`
diff --git a/elements/pf-tooltip/pf-tooltip.ts b/elements/pf-tooltip/pf-tooltip.ts index e97344d9a1..6bd4954c3a 100644 --- a/elements/pf-tooltip/pf-tooltip.ts +++ b/elements/pf-tooltip/pf-tooltip.ts @@ -130,11 +130,11 @@ export class PfTooltip extends LitElement { }) flipBehavior?: Placement[]; get #invoker(): HTMLSlotElement | null { - return this.shadowRoot?.querySelector('#invoker') ?? null; + return this.shadowRoot?.querySelector?.('#invoker') ?? null; } get #content(): HTMLElement | null { - return this.shadowRoot?.querySelector('#tooltip') ?? null; + return this.shadowRoot?.querySelector?.('#tooltip') ?? null; } #referenceTrigger?: HTMLElement | null; From 9bd0874b22b1c4c14cfa67fe01a9898c13b3c3cb Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Mon, 13 Jan 2025 15:04:22 +0200 Subject: [PATCH 10/17] fix(elements): table th role from context instead of dom --- elements/pf-table/context.ts | 5 +++++ elements/pf-table/pf-table.ts | 7 ++++++- elements/pf-table/pf-th.ts | 13 ++++++++----- elements/pf-table/pf-thead.ts | 5 +++++ 4 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 elements/pf-table/context.ts diff --git a/elements/pf-table/context.ts b/elements/pf-table/context.ts new file mode 100644 index 0000000000..d128980e17 --- /dev/null +++ b/elements/pf-table/context.ts @@ -0,0 +1,5 @@ +import { createContextWithRoot } from '@patternfly/pfe-core/functions/context.js'; + +export const thRoleContext: { + __context__: unknown; +} = createContextWithRoot<'rowheader' | 'colheader'>('pf-th-role'); diff --git a/elements/pf-table/pf-table.ts b/elements/pf-table/pf-table.ts index bf44f8f7dc..94034a1ee1 100644 --- a/elements/pf-table/pf-table.ts +++ b/elements/pf-table/pf-table.ts @@ -3,7 +3,11 @@ import { customElement } from 'lit/decorators/custom-element.js'; import { styleMap } from 'lit/directives/style-map.js'; import { state } from 'lit/decorators/state.js'; +import { provide } from '@lit/context'; +import { thRoleContext } from './context.js'; + import { PfTh, RequestSortEvent } from './pf-th.js'; +import { PfTd } from './pf-td.js'; import { PfTr, RequestExpandEvent } from './pf-tr.js'; export * from './pf-caption.js'; @@ -14,7 +18,6 @@ export * from './pf-th.js'; export * from './pf-td.js'; import styles from './pf-table.css'; -import { PfTd } from './pf-td.js'; const rowQuery = [ ':scope > pf-tbody:not([expandable]) > pf-tr', @@ -671,6 +674,8 @@ export class PfTable extends LitElement { @state() private columns = 0; + @provide({ context: thRoleContext }) private thRowContext = 'rowheader'; + override connectedCallback(): void { super.connectedCallback(); this.setAttribute('role', 'table'); diff --git a/elements/pf-table/pf-th.ts b/elements/pf-table/pf-th.ts index ed95cee6ed..9effd562bf 100644 --- a/elements/pf-table/pf-th.ts +++ b/elements/pf-table/pf-th.ts @@ -3,6 +3,10 @@ import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; import { classMap } from 'lit/directives/class-map.js'; +import { consume } from '@lit/context'; + +import { thRoleContext } from './context.js'; + import '@patternfly/elements/pf-button/pf-button.js'; import styles from './pf-th.css'; @@ -46,13 +50,12 @@ export class PfTh extends LitElement { @property() key!: string; + @consume({ context: thRoleContext }) + private contextualRole: 'colheader' | 'rowheader' = 'rowheader'; + override connectedCallback(): void { super.connectedCallback(); - const closestThead = this.closest('pf-thead'); - const closestTable = this.closest('pf-table'); - const isChildOfThead = !!closestThead && !!closestTable?.contains(closestThead); - const role = isChildOfThead ? 'colheader' : 'rowheader'; - this.setAttribute('role', role); + this.setAttribute('role', this.contextualRole); } render(): TemplateResult<1> { diff --git a/elements/pf-table/pf-thead.ts b/elements/pf-table/pf-thead.ts index 35bc35c897..c46a51ccb4 100644 --- a/elements/pf-table/pf-thead.ts +++ b/elements/pf-table/pf-thead.ts @@ -1,7 +1,10 @@ import { LitElement, html, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; +import { thRoleContext } from './context.js'; + import styles from './pf-thead.css'; +import { provide } from '@lit/context'; /** * Table head @@ -11,6 +14,8 @@ import styles from './pf-thead.css'; export class PfThead extends LitElement { static readonly styles: CSSStyleSheet[] = [styles]; + @provide({ context: thRoleContext }) private thRowContext = 'colheader'; + connectedCallback(): void { super.connectedCallback(); this.setAttribute('role', 'rowgroup'); From d605a41c0cd5434ed4a071d3f262c41c42afca17 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Tue, 21 Jan 2025 16:45:05 +0200 Subject: [PATCH 11/17] feat(core): wip slots decorator --- core/pfe-core/controllers/slot-controller.ts | 160 +++++++++---------- core/pfe-core/decorators/slots.ts | 27 ++++ 2 files changed, 106 insertions(+), 81 deletions(-) create mode 100644 core/pfe-core/decorators/slots.ts diff --git a/core/pfe-core/controllers/slot-controller.ts b/core/pfe-core/controllers/slot-controller.ts index 2a7671bc33..7a2896028f 100644 --- a/core/pfe-core/controllers/slot-controller.ts +++ b/core/pfe-core/controllers/slot-controller.ts @@ -1,7 +1,5 @@ import { isServer, type ReactiveController, type ReactiveElement } from 'lit'; -import { Logger } from './logger.js'; - interface AnonymousSlot { hasContent: boolean; elements: Element[]; @@ -32,7 +30,9 @@ export interface SlotsConfig { deprecations?: Record; } -function isObjectConfigSpread( +export type SlotControllerArgs = [SlotsConfig] | (string | null)[]; + +function isObjectSpread( config: ([SlotsConfig] | (string | null)[]), ): config is [SlotsConfig] { return config.length === 1 && typeof config[0] === 'object' && config[0] !== null; @@ -55,60 +55,108 @@ export class SlotController implements ReactiveController { /** @deprecated use `default` */ public static anonymous: symbol = this.default; - #nodes = new Map(); + private static singletons = new WeakMap(); - #logger: Logger; + #nodes = new Map(); - #firstUpdated = false; + #slotMapInitialized = false; - #mo = new MutationObserver(records => this.#onMutation(records)); + #slotNames: (string | null)[] = []; - #slotNames: (string | null)[]; + #ssrHintHasSlotted: (string | null)[] = []; #deprecations: Record = {}; - constructor(public host: ReactiveElement, ...config: ([SlotsConfig] | (string | null)[])) { - this.#logger = new Logger(this.host); + #mo = new MutationObserver(this.#initSlotMap.bind(this)); + + constructor(public host: ReactiveElement, ...args: SlotControllerArgs) { + const singleton = SlotController.singletons.get(host); + if (singleton) { + singleton.#initialize(...args); + return singleton; + } + this.#initialize(...args); + host.addController(this); + SlotController.singletons.set(host, this); + if (!this.#slotNames.length) { + this.#slotNames = [null]; + } + } - if (isObjectConfigSpread(config)) { + #initialize(...config: SlotControllerArgs) { + if (isObjectSpread(config)) { const [{ slots, deprecations }] = config; this.#slotNames = slots; this.#deprecations = deprecations ?? {}; } else if (config.length >= 1) { this.#slotNames = config; this.#deprecations = {}; - } else { - this.#slotNames = [null]; } - - - host.addController(this); } async hostConnected(): Promise { - this.host.addEventListener('slotchange', this.#onSlotChange as EventListener); - this.#firstUpdated = false; this.#mo.observe(this.host, { childList: true }); + this.#ssrHintHasSlotted = + this.host + // @ts-expect-error: this is a ponyfill for ::has-slotted, is not intended as a public API + .ssrHintHasSlotted + ?? []; // Map the defined slots into an object that is easier to query this.#nodes.clear(); - // Loop over the properties provided by the schema - this.#slotNames.forEach(this.#initSlot); - Object.values(this.#deprecations).forEach(this.#initSlot); - this.host.requestUpdate(); + this.#initSlotMap(); // insurance for framework integrations await this.host.updateComplete; this.host.requestUpdate(); } + hostDisconnected(): void { + this.#mo.disconnect(); + } + hostUpdated(): void { - if (!this.#firstUpdated) { - this.#slotNames.forEach(this.#initSlot); - this.#firstUpdated = true; + if (!this.#slotMapInitialized) { + this.#initSlotMap(); } } - hostDisconnected(): void { - this.#mo.disconnect(); + #initSlotMap() { + // Loop over the properties provided by the schema + for (const slotName of this.#slotNames + .concat(Object.values(this.#deprecations))) { + const slotId = slotName || SlotController.default; + const name = slotName ?? ''; + const elements = this.#getChildrenForSlot(slotId); + const slot = this.#getSlotElement(slotId); + const hasContent = + isServer ? this.#ssrHintHasSlotted.includes(slotName) + : !!elements.length || !!slot?.assignedNodes?.()?.filter(x => x.textContent?.trim()).length; + this.#nodes.set(slotId, { elements, name, hasContent, slot }); + } + this.host.requestUpdate(); + this.#slotMapInitialized = true; + } + + #getSlotElement(slotId: string | symbol) { + if (isServer) { + return null; + } else { + const selector = + slotId === SlotController.default ? 'slot:not([name])' : `slot[name="${slotId as string}"]`; + return this.host.shadowRoot?.querySelector?.(selector) ?? null; + } + } + + #getChildrenForSlot( + name: string | typeof SlotController.default, + ): T[] { + if (isServer) { + return []; + } else if (this.#nodes.has(name)) { + return this.#nodes.get(name)!.slot?.assignedElements?.() as T[]; + } else { + const children = Array.from(this.host.children) as T[]; + return children.filter(isSlot(name)); + } } /** @@ -143,19 +191,11 @@ export class SlotController implements ReactiveController { * @example this.hasSlotted('header'); */ hasSlotted(...names: (string | null | undefined)[]): boolean { - if (isServer) { - return this.host - .getAttribute('ssr-hint-has-slotted') - ?.split(',') - .map(name => name.trim()) - .some(name => names.includes(name === 'default' ? null : name)) ?? false; - } else { - const slotNames = Array.from(names, x => x == null ? SlotController.default : x); - if (!slotNames.length) { - slotNames.push(SlotController.default); - } - return slotNames.some(x => this.#nodes.get(x)?.hasContent ?? false); + const slotNames = Array.from(names, x => x == null ? SlotController.default : x); + if (!slotNames.length) { + slotNames.push(SlotController.default); } + return slotNames.some(x => this.#nodes.get(x)?.hasContent ?? false); } /** @@ -168,46 +208,4 @@ export class SlotController implements ReactiveController { isEmpty(...names: (string | null | undefined)[]): boolean { return !this.hasSlotted(...names); } - - #onSlotChange = (event: Event & { target: HTMLSlotElement }) => { - const slotName = event.target.name; - this.#initSlot(slotName); - this.host.requestUpdate(); - }; - - #onMutation = async (records: MutationRecord[]) => { - const changed = []; - for (const { addedNodes, removedNodes } of records) { - for (const node of [...addedNodes, ...removedNodes]) { - if (node instanceof HTMLElement && node.slot) { - this.#initSlot(node.slot); - changed.push(node.slot); - } - } - } - this.host.requestUpdate(); - }; - - #getChildrenForSlot( - name: string | typeof SlotController.default, - ): T[] { - if (isServer) { - return []; - } else { - const children = Array.from(this.host.children) as T[]; - return children.filter(isSlot(name)); - } - } - - #initSlot = (slotName: string | null) => { - const name = slotName || SlotController.default; - const elements = this.#nodes.get(name)?.slot?.assignedElements?.() - ?? this.#getChildrenForSlot(name); - const selector = slotName ? `slot[name="${slotName}"]` : 'slot:not([name])'; - const slot = this.host.shadowRoot?.querySelector?.(selector) ?? null; - const nodes = slot?.assignedNodes?.(); - const hasContent = !!elements.length || !!nodes?.filter(x => x.textContent?.trim()).length; - this.#nodes.set(name, { elements, name: slotName ?? '', hasContent, slot }); - this.#logger.debug(slotName, hasContent); - }; } diff --git a/core/pfe-core/decorators/slots.ts b/core/pfe-core/decorators/slots.ts new file mode 100644 index 0000000000..c27f16c675 --- /dev/null +++ b/core/pfe-core/decorators/slots.ts @@ -0,0 +1,27 @@ +import type { ReactiveElement } from 'lit'; + +import { SlotController, type SlotControllerArgs } from '../controllers/slot-controller.js'; + +/** + * Enable ssr hints for element + * @param args a spread of slot names, or a config object. + * @see SlotController constructor args + */ +export function slots(...args: SlotControllerArgs) { + return function(klass: T): void { + klass.createProperty('ssrHintHasSlotted', { + attribute: 'ssr-hint-has-slotted', + converter: { + fromAttribute(slots) { + return (slots ?? '') + .split(/[, ]/) + .map(x => x.trim()) + .map(x => x === 'default' ? null : x); + }, + }, + }); + klass.addInitializer(instance => { + new SlotController(instance, ...args); + }); + }; +} From 49c320279adbea85bf57a09a0514ba866e6b5c08 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Mon, 27 Jan 2025 14:20:10 +0200 Subject: [PATCH 12/17] fix(core): empty array check --- core/pfe-core/controllers/slot-controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/pfe-core/controllers/slot-controller.ts b/core/pfe-core/controllers/slot-controller.ts index 7a2896028f..1f3d2368e7 100644 --- a/core/pfe-core/controllers/slot-controller.ts +++ b/core/pfe-core/controllers/slot-controller.ts @@ -152,7 +152,7 @@ export class SlotController implements ReactiveController { if (isServer) { return []; } else if (this.#nodes.has(name)) { - return this.#nodes.get(name)!.slot?.assignedElements?.() as T[]; + return (this.#nodes.get(name)!.slot?.assignedElements?.() ?? []) as T[]; } else { const children = Array.from(this.host.children) as T[]; return children.filter(isSlot(name)); From 0b3dffa5083bfe22948a3f848b4192953be22c11 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Thu, 30 Jan 2025 13:18:42 +0200 Subject: [PATCH 13/17] fix(core): remove need for static decorator for ssr slot hints --- .../controllers/slot-controller-server.ts | 45 ++++++++++++ core/pfe-core/controllers/slot-controller.ts | 69 +++++++++++++++---- core/pfe-core/decorators/slots.ts | 27 -------- core/pfe-core/package.json | 6 +- 4 files changed, 105 insertions(+), 42 deletions(-) create mode 100644 core/pfe-core/controllers/slot-controller-server.ts delete mode 100644 core/pfe-core/decorators/slots.ts diff --git a/core/pfe-core/controllers/slot-controller-server.ts b/core/pfe-core/controllers/slot-controller-server.ts new file mode 100644 index 0000000000..b0ed7330d6 --- /dev/null +++ b/core/pfe-core/controllers/slot-controller-server.ts @@ -0,0 +1,45 @@ +import type { ReactiveElement } from 'lit'; +import { + type SlotControllerArgs, + type SlotControllerPublicAPI, +} from './slot-controller.js'; + +export class SlotController implements SlotControllerPublicAPI { + public static default = Symbol('default slot') satisfies symbol as symbol; + + /** @deprecated use `default` */ + public static anonymous: symbol = this.default; + + static property = 'ssrHintHasSlotted' as const; + + static attribute = 'ssr-hint-has-slotted' as const; + + static anonymousAttribute = 'ssr-hint-has-slotted-anonymous' as const; + + constructor(public host: ReactiveElement, ..._: SlotControllerArgs) { + host.addController(this); + } + + hostConnected?(): Promise; + + private fromAttribute(slots: string | null) { + return (slots ?? '') + .split(/[, ]/) + .map(x => x.trim()); + } + + getSlotted(..._: string[]): T[] { + return []; + } + + hasSlotted(...names: (string | null)[]): boolean { + const attr = this.host.getAttribute(SlotController.attribute); + const anon = this.host.hasAttribute(SlotController.anonymousAttribute); + const hints = new Set(this.fromAttribute(attr)); + return names.every(x => x === null ? anon : hints.has(x)); + } + + isEmpty(...names: (string | null)[]): boolean { + return !this.hasSlotted(...names); + } +} diff --git a/core/pfe-core/controllers/slot-controller.ts b/core/pfe-core/controllers/slot-controller.ts index 1f3d2368e7..3e0a7c7bca 100644 --- a/core/pfe-core/controllers/slot-controller.ts +++ b/core/pfe-core/controllers/slot-controller.ts @@ -13,8 +13,10 @@ interface NamedSlot extends AnonymousSlot { export type Slot = NamedSlot | AnonymousSlot; +export type SlotName = string | null; + export interface SlotsConfig { - slots: (string | null)[]; + slots: SlotName[]; /** * Object mapping new slot name keys to deprecated slot name values * @example `pf-modal--header` is deprecated in favour of `header` @@ -30,11 +32,9 @@ export interface SlotsConfig { deprecations?: Record; } -export type SlotControllerArgs = [SlotsConfig] | (string | null)[]; +export type SlotControllerArgs = [SlotsConfig] | SlotName[]; -function isObjectSpread( - config: ([SlotsConfig] | (string | null)[]), -): config is [SlotsConfig] { +export function isObjectSpread(config: SlotControllerArgs): config is [SlotsConfig] { return config.length === 1 && typeof config[0] === 'object' && config[0] !== null; } @@ -49,14 +49,61 @@ const isSlot = n === SlotController.default ? !child.hasAttribute('slot') : child.getAttribute('slot') === n; -export class SlotController implements ReactiveController { +export declare class SlotControllerPublicAPI implements ReactiveController { + static default: symbol; + + public host: ReactiveElement; + + constructor(host: ReactiveElement, ...args: SlotControllerArgs); + + hostConnected?(): Promise; + + hostDisconnected?(): void; + + hostUpdated?(): void; + + /** + * Given a slot name or slot names, returns elements assigned to the requested slots as an array. + * If no value is provided, it returns all children not assigned to a slot (without a slot attribute). + * @param slotNames slots to query + * @example Get header-slotted elements + * ```js + * this.getSlotted('header') + * ``` + * @example Get header- and footer-slotted elements + * ```js + * this.getSlotted('header', 'footer') + * ``` + * @example Get default-slotted elements + * ```js + * this.getSlotted(); + * ``` + */ + getSlotted(...slotNames: string[]): T[]; + + /** + * Returns a boolean statement of whether or not any of those slots exists in the light DOM. + * @param names The slot names to check. + * @example this.hasSlotted('header'); + */ + hasSlotted(...names: (string | null | undefined)[]): boolean; + + /** + * Whether or not all the requested slots are empty. + * @param names The slot names to query. If no value is provided, it returns the default slot. + * @example this.isEmpty('header', 'footer'); + * @example this.isEmpty(); + * @returns + */ + isEmpty(...names: (string | null | undefined)[]): boolean; +} + +export class SlotController implements SlotControllerPublicAPI { public static default = Symbol('default slot') satisfies symbol as symbol; /** @deprecated use `default` */ public static anonymous: symbol = this.default; - private static singletons = new WeakMap(); - #nodes = new Map(); #slotMapInitialized = false; @@ -70,14 +117,8 @@ export class SlotController implements ReactiveController { #mo = new MutationObserver(this.#initSlotMap.bind(this)); constructor(public host: ReactiveElement, ...args: SlotControllerArgs) { - const singleton = SlotController.singletons.get(host); - if (singleton) { - singleton.#initialize(...args); - return singleton; - } this.#initialize(...args); host.addController(this); - SlotController.singletons.set(host, this); if (!this.#slotNames.length) { this.#slotNames = [null]; } diff --git a/core/pfe-core/decorators/slots.ts b/core/pfe-core/decorators/slots.ts deleted file mode 100644 index c27f16c675..0000000000 --- a/core/pfe-core/decorators/slots.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { ReactiveElement } from 'lit'; - -import { SlotController, type SlotControllerArgs } from '../controllers/slot-controller.js'; - -/** - * Enable ssr hints for element - * @param args a spread of slot names, or a config object. - * @see SlotController constructor args - */ -export function slots(...args: SlotControllerArgs) { - return function(klass: T): void { - klass.createProperty('ssrHintHasSlotted', { - attribute: 'ssr-hint-has-slotted', - converter: { - fromAttribute(slots) { - return (slots ?? '') - .split(/[, ]/) - .map(x => x.trim()) - .map(x => x === 'default' ? null : x); - }, - }, - }); - klass.addInitializer(instance => { - new SlotController(instance, ...args); - }); - }; -} diff --git a/core/pfe-core/package.json b/core/pfe-core/package.json index 1b84a32f27..3b56186a3c 100644 --- a/core/pfe-core/package.json +++ b/core/pfe-core/package.json @@ -32,7 +32,11 @@ "./controllers/property-observer-controller.js": "./controllers/property-observer-controller.js", "./controllers/roving-tabindex-controller.js": "./controllers/roving-tabindex-controller.js", "./controllers/scroll-spy-controller.js": "./controllers/scroll-spy-controller.js", - "./controllers/slot-controller.js": "./controllers/slot-controller.js", + "./controllers/slot-controller.js": { + "node": "./controllers/slot-controller-server.js", + "import": "./controllers/slot-controller.js", + "default": "./controllers/slot-controller.js" + }, "./controllers/style-controller.js": "./controllers/style-controller.js", "./controllers/timestamp-controller.js": "./controllers/timestamp-controller.js", "./controllers/tabs-controller.js": "./controllers/tabs-controller.js", From dd752d563169eefc3c06ce63bf653f4aa05096f0 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Thu, 30 Jan 2025 18:19:10 +0200 Subject: [PATCH 14/17] fix: revert slot controller changes This will be addressed in a later PR --- .../controllers/slot-controller-server.ts | 45 ----------- core/pfe-core/controllers/slot-controller.ts | 77 ++----------------- 2 files changed, 7 insertions(+), 115 deletions(-) delete mode 100644 core/pfe-core/controllers/slot-controller-server.ts diff --git a/core/pfe-core/controllers/slot-controller-server.ts b/core/pfe-core/controllers/slot-controller-server.ts deleted file mode 100644 index b0ed7330d6..0000000000 --- a/core/pfe-core/controllers/slot-controller-server.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { ReactiveElement } from 'lit'; -import { - type SlotControllerArgs, - type SlotControllerPublicAPI, -} from './slot-controller.js'; - -export class SlotController implements SlotControllerPublicAPI { - public static default = Symbol('default slot') satisfies symbol as symbol; - - /** @deprecated use `default` */ - public static anonymous: symbol = this.default; - - static property = 'ssrHintHasSlotted' as const; - - static attribute = 'ssr-hint-has-slotted' as const; - - static anonymousAttribute = 'ssr-hint-has-slotted-anonymous' as const; - - constructor(public host: ReactiveElement, ..._: SlotControllerArgs) { - host.addController(this); - } - - hostConnected?(): Promise; - - private fromAttribute(slots: string | null) { - return (slots ?? '') - .split(/[, ]/) - .map(x => x.trim()); - } - - getSlotted(..._: string[]): T[] { - return []; - } - - hasSlotted(...names: (string | null)[]): boolean { - const attr = this.host.getAttribute(SlotController.attribute); - const anon = this.host.hasAttribute(SlotController.anonymousAttribute); - const hints = new Set(this.fromAttribute(attr)); - return names.every(x => x === null ? anon : hints.has(x)); - } - - isEmpty(...names: (string | null)[]): boolean { - return !this.hasSlotted(...names); - } -} diff --git a/core/pfe-core/controllers/slot-controller.ts b/core/pfe-core/controllers/slot-controller.ts index 3e0a7c7bca..ef02c97625 100644 --- a/core/pfe-core/controllers/slot-controller.ts +++ b/core/pfe-core/controllers/slot-controller.ts @@ -1,4 +1,4 @@ -import { isServer, type ReactiveController, type ReactiveElement } from 'lit'; +import { type ReactiveController, type ReactiveElement } from 'lit'; interface AnonymousSlot { hasContent: boolean; @@ -49,56 +49,7 @@ const isSlot = n === SlotController.default ? !child.hasAttribute('slot') : child.getAttribute('slot') === n; -export declare class SlotControllerPublicAPI implements ReactiveController { - static default: symbol; - - public host: ReactiveElement; - - constructor(host: ReactiveElement, ...args: SlotControllerArgs); - - hostConnected?(): Promise; - - hostDisconnected?(): void; - - hostUpdated?(): void; - - /** - * Given a slot name or slot names, returns elements assigned to the requested slots as an array. - * If no value is provided, it returns all children not assigned to a slot (without a slot attribute). - * @param slotNames slots to query - * @example Get header-slotted elements - * ```js - * this.getSlotted('header') - * ``` - * @example Get header- and footer-slotted elements - * ```js - * this.getSlotted('header', 'footer') - * ``` - * @example Get default-slotted elements - * ```js - * this.getSlotted(); - * ``` - */ - getSlotted(...slotNames: string[]): T[]; - - /** - * Returns a boolean statement of whether or not any of those slots exists in the light DOM. - * @param names The slot names to check. - * @example this.hasSlotted('header'); - */ - hasSlotted(...names: (string | null | undefined)[]): boolean; - - /** - * Whether or not all the requested slots are empty. - * @param names The slot names to query. If no value is provided, it returns the default slot. - * @example this.isEmpty('header', 'footer'); - * @example this.isEmpty(); - * @returns - */ - isEmpty(...names: (string | null | undefined)[]): boolean; -} - -export class SlotController implements SlotControllerPublicAPI { +export class SlotController implements ReactiveController { public static default = Symbol('default slot') satisfies symbol as symbol; /** @deprecated use `default` */ @@ -110,8 +61,6 @@ export class SlotController implements SlotControllerPublicAPI { #slotNames: (string | null)[] = []; - #ssrHintHasSlotted: (string | null)[] = []; - #deprecations: Record = {}; #mo = new MutationObserver(this.#initSlotMap.bind(this)); @@ -137,11 +86,6 @@ export class SlotController implements SlotControllerPublicAPI { async hostConnected(): Promise { this.#mo.observe(this.host, { childList: true }); - this.#ssrHintHasSlotted = - this.host - // @ts-expect-error: this is a ponyfill for ::has-slotted, is not intended as a public API - .ssrHintHasSlotted - ?? []; // Map the defined slots into an object that is easier to query this.#nodes.clear(); this.#initSlotMap(); @@ -169,8 +113,7 @@ export class SlotController implements SlotControllerPublicAPI { const elements = this.#getChildrenForSlot(slotId); const slot = this.#getSlotElement(slotId); const hasContent = - isServer ? this.#ssrHintHasSlotted.includes(slotName) - : !!elements.length || !!slot?.assignedNodes?.()?.filter(x => x.textContent?.trim()).length; + !!elements.length || !!slot?.assignedNodes?.()?.filter(x => x.textContent?.trim()).length; this.#nodes.set(slotId, { elements, name, hasContent, slot }); } this.host.requestUpdate(); @@ -178,22 +121,16 @@ export class SlotController implements SlotControllerPublicAPI { } #getSlotElement(slotId: string | symbol) { - if (isServer) { - return null; - } else { - const selector = + const selector = slotId === SlotController.default ? 'slot:not([name])' : `slot[name="${slotId as string}"]`; - return this.host.shadowRoot?.querySelector?.(selector) ?? null; - } + return this.host.shadowRoot?.querySelector(selector) ?? null; } #getChildrenForSlot( name: string | typeof SlotController.default, ): T[] { - if (isServer) { - return []; - } else if (this.#nodes.has(name)) { - return (this.#nodes.get(name)!.slot?.assignedElements?.() ?? []) as T[]; + if (this.#nodes.has(name)) { + return (this.#nodes.get(name)!.slot?.assignedElements() ?? []) as T[]; } else { const children = Array.from(this.host.children) as T[]; return children.filter(isSlot(name)); From 453280bb5dcbc86db95e8ddd85c0e4b3cb05afda Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Thu, 30 Jan 2025 18:34:40 +0200 Subject: [PATCH 15/17] fix: keep isServer in client-side slot controller pending later PR --- core/pfe-core/controllers/slot-controller.ts | 20 ++++++++++++++------ core/pfe-core/package.json | 1 - 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/core/pfe-core/controllers/slot-controller.ts b/core/pfe-core/controllers/slot-controller.ts index ef02c97625..63dc3bf444 100644 --- a/core/pfe-core/controllers/slot-controller.ts +++ b/core/pfe-core/controllers/slot-controller.ts @@ -1,4 +1,4 @@ -import { type ReactiveController, type ReactiveElement } from 'lit'; +import { isServer, type ReactiveController, type ReactiveElement } from 'lit'; interface AnonymousSlot { hasContent: boolean; @@ -113,7 +113,9 @@ export class SlotController implements ReactiveController { const elements = this.#getChildrenForSlot(slotId); const slot = this.#getSlotElement(slotId); const hasContent = - !!elements.length || !!slot?.assignedNodes?.()?.filter(x => x.textContent?.trim()).length; + !isServer + && !!elements.length + || !!slot?.assignedNodes?.()?.filter(x => x.textContent?.trim()).length; this.#nodes.set(slotId, { elements, name, hasContent, slot }); } this.host.requestUpdate(); @@ -121,16 +123,22 @@ export class SlotController implements ReactiveController { } #getSlotElement(slotId: string | symbol) { - const selector = + if (isServer) { + return null; + } else { + const selector = slotId === SlotController.default ? 'slot:not([name])' : `slot[name="${slotId as string}"]`; - return this.host.shadowRoot?.querySelector(selector) ?? null; + return this.host.shadowRoot?.querySelector?.(selector) ?? null; + } } #getChildrenForSlot( name: string | typeof SlotController.default, ): T[] { - if (this.#nodes.has(name)) { - return (this.#nodes.get(name)!.slot?.assignedElements() ?? []) as T[]; + if (isServer) { + return []; + } else if (this.#nodes.has(name)) { + return (this.#nodes.get(name)!.slot?.assignedElements?.() ?? []) as T[]; } else { const children = Array.from(this.host.children) as T[]; return children.filter(isSlot(name)); diff --git a/core/pfe-core/package.json b/core/pfe-core/package.json index 3b56186a3c..fb4aa7744d 100644 --- a/core/pfe-core/package.json +++ b/core/pfe-core/package.json @@ -33,7 +33,6 @@ "./controllers/roving-tabindex-controller.js": "./controllers/roving-tabindex-controller.js", "./controllers/scroll-spy-controller.js": "./controllers/scroll-spy-controller.js", "./controllers/slot-controller.js": { - "node": "./controllers/slot-controller-server.js", "import": "./controllers/slot-controller.js", "default": "./controllers/slot-controller.js" }, From 81ef4f2784b88b7cbf8f4d75393374338d353b5f Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Wed, 12 Feb 2025 16:42:09 +0200 Subject: [PATCH 16/17] refactor(tools): readability --- tools/pfe-tools/ssr/global.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/pfe-tools/ssr/global.ts b/tools/pfe-tools/ssr/global.ts index 5209242b4f..eecb84a620 100644 --- a/tools/pfe-tools/ssr/global.ts +++ b/tools/pfe-tools/ssr/global.ts @@ -8,10 +8,10 @@ export async function renderGlobal( html: string, importSpecifiers: string[], ): Promise { - // avoid tsconfig problems - await import(['@patternfly', 'pfe-core', 'ssr-shims.js'].join('/')); + // hack to avoid circular typescript project reference + const spec = '@patternfly/pfe-core/ssr-shims.js'; + await import(spec); const { ssr } = await import('./ssr.js'); await Promise.all(importSpecifiers.map(x => import(x))); return ssr(html); } - From 9217a54ec34c2a71ae1cc44e9805378ae868e456 Mon Sep 17 00:00:00 2001 From: Benny Powers Date: Wed, 12 Feb 2025 16:48:15 +0200 Subject: [PATCH 17/17] docs: major changeset --- .changeset/clear-pugs-make.md | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/.changeset/clear-pugs-make.md b/.changeset/clear-pugs-make.md index c4b55e1a98..2bc7175b94 100644 --- a/.changeset/clear-pugs-make.md +++ b/.changeset/clear-pugs-make.md @@ -1,5 +1,31 @@ --- -"@patternfly/pfe-core": patch +"@patternfly/pfe-core": major "@patternfly/elements": patch --- -Enable context protocol in SSR scenarios. +Enable `connectedCallback()` and context protocol in SSR scenarios. + +BREAKING CHANGE +This change affects any element which is expected to execute in node JS when +lit-ssr shims are present. By enabling the `connectedCallback()` to execute +server side. Elements must ensure that their connectedCallbacks do not try to +access the DOM. + +Before: + +```js +connectedCallback() { + super.connectedCallback(); + this.items = this.querySelectorAll('my-item'); +} +``` + +After: +```js +connectedCallback() { + super.connectedCallback(); + if (!isServer) { + this.items = this.querySelectorAll('my-item'); + } +} +``` +