From 8798d8133f69e37fddfda339b53aa549b1cf061e Mon Sep 17 00:00:00 2001 From: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com> Date: Thu, 9 Apr 2026 08:18:12 +0200 Subject: [PATCH 01/21] web/elements: rename hasSlotted to findSlotted and refactor host styles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the slot-inspection helper on `AKElement` from `hasSlotted` to `findSlotted` and return the first matching element rather than a boolean, so callers can both check for presence and reach the node. Update every call site in the tree (default callers pass no argument instead of `null`). Along the way, tidy `AKElement`'s host-style plumbing: expose `hostStyles` as a getter/setter backed by a `CSSStyleSheet` cache and move the adoption logic into `attachHostStyles` / `detachHostStyles` class methods, so subclasses can share the lifecycle. Drop the now unused `@localized` decorator import. Also add a `findAssignedSlot` helper in `elements/utils/slots.ts` for light-DOM → slot lookups, and give `EmptyState` an explicit `display: block` so empty-state placement doesn't collapse when wrapped. --- web/src/elements/Base.ts | 82 +++++++++++++++++-------- web/src/elements/EmptyState.ts | 10 ++- web/src/elements/LoadingOverlay.ts | 4 +- web/src/elements/ak-progress-bar.ts | 6 +- web/src/elements/utils/slots.ts | 29 +++++++++ web/src/flow/components/ak-flow-card.ts | 6 +- 6 files changed, 100 insertions(+), 37 deletions(-) diff --git a/web/src/elements/Base.ts b/web/src/elements/Base.ts index d6690e699672..105048329cc2 100644 --- a/web/src/elements/Base.ts +++ b/web/src/elements/Base.ts @@ -10,7 +10,6 @@ import { applyUITheme, ResolvedUITheme, resolveUITheme, ThemeChangeEvent } from import AKBase from "#styles/authentik/base.css" with { type: "bundled-text" }; import PFBase from "#styles/patternfly/base.css" with { type: "bundled-text" }; -import { localized } from "@lit/localize"; import { CSSResult, CSSResultGroup, CSSResultOrNative, LitElement, PropertyValues } from "lit"; import { property } from "lit/decorators.js"; @@ -33,7 +32,6 @@ export interface AKElementProps { activeTheme: ResolvedUITheme; } -@localized() export class AKElement extends LitElement implements AKElementProps { //#region Static Properties @@ -48,13 +46,27 @@ export class AKElement extends LitElement implements AKElementProps { * This is useful if the element is a wrapper around a third-party component * that requires styles to be applied to the host, such as Patternfly's modals. */ - public static hostStyles?: Array; + public static get hostStyles(): CSSResultOrNative[] { + return this.hostStyleSheets ?? []; + } - private static hostStyleSheets: CSSStyleSheet[] | null = null; + public static set hostStyles(styles: CSSResultOrNative[]) { + this.hostStyleSheets = styles.map(createStyleSheetUnsafe); + } - protected static override finalizeStyles(styles: CSSResultGroup = []): CSSResultOrNative[] { - this.hostStyleSheets = this.hostStyles ? this.hostStyles.map(createStyleSheetUnsafe) : null; + /** + * A cache of the element's host styles, converted to {@linkcode CSSStyleSheet} + * instances to avoid duplicated references. + * + * **You should not need to interact with this property directly.** + * + * @see {@linkcode hostStyles} for the public API for this property. + * + * @protected + */ + protected static hostStyleSheets: CSSStyleSheet[] | null = null; + protected static override finalizeStyles(styles: CSSResultGroup = []): CSSResultOrNative[] { const elementStyles = [ $PFBase, // Route around TSC`s known-to-fail typechecking of `.flat(Infinity)`. Removes types. @@ -71,6 +83,26 @@ export class AKElement extends LitElement implements AKElementProps { return Array.from(elementSet).reverse().map(createCSSResult); } + protected static attachHostStyles(rootNode: ShadowRoot): void { + const { hostStyleSheets } = this; + + if (!hostStyleSheets) return; + + setAdoptedStyleSheets(rootNode, (currentStyleSheets) => { + return [...currentStyleSheets, ...hostStyleSheets]; + }); + } + + protected static detachHostStyles(rootNode: ShadowRoot): void { + const { hostStyleSheets } = this; + + if (!hostStyleSheets) return; + + setAdoptedStyleSheets(rootNode, (currentStyleSheets) => { + return currentStyleSheets.filter((sheet) => !hostStyleSheets.includes(sheet)); + }); + } + //#endregion //#region Lifecycle @@ -126,13 +158,7 @@ export class AKElement extends LitElement implements AKElementProps { const rootNode = this.getRootNode(); if (rootNode instanceof ShadowRoot) { - const { hostStyleSheets } = this.constructor as typeof AKElement; - - if (hostStyleSheets) { - setAdoptedStyleSheets(rootNode, (currentStyleSheets) => { - return [...currentStyleSheets, ...hostStyleSheets]; - }); - } + (this.constructor as typeof AKElement).attachHostStyles(rootNode); } } @@ -142,13 +168,7 @@ export class AKElement extends LitElement implements AKElementProps { const rootNode = this.getRootNode(); if (rootNode instanceof ShadowRoot) { - const { hostStyleSheets } = this.constructor as typeof AKElement; - - if (hostStyleSheets) { - setAdoptedStyleSheets(rootNode, (currentStyleSheets) => { - return currentStyleSheets.filter((sheet) => !hostStyleSheets.includes(sheet)); - }); - } + (this.constructor as typeof AKElement).detachHostStyles(rootNode); } super.disconnectedCallback(); @@ -242,22 +262,30 @@ export class AKElement extends LitElement implements AKElementProps { } } - protected hasSlotted(name: string | null) { + /** + * Finds a slotted element by name, ensuring that it is not nested within another slotted element. + * + * @param slotName The name of the slot to find. Omit to find elements in the default slot. + * @return The slotted element, or `null` if no matching element is found. + */ + protected findSlotted(slotName?: string): T | null { const isNotNestedSlot = (start: Element) => { let node = start.parentNode; + while (node && node !== this) { if (node instanceof Element && node.hasAttribute("slot")) { - return false; + return null; } node = node.parentNode; } - return true; + + return node; }; // All child slots accessible from the component's LightDOM that match the request const allChildSlotRequests = - typeof name === "string" - ? [...this.querySelectorAll(`[slot="${name}"]`)] + typeof slotName === "string" + ? [...this.querySelectorAll(`[slot="${slotName}"]`)] : [...this.children].filter((child) => { const slotAttr = child.getAttribute("slot"); return !slotAttr || slotAttr === ""; @@ -265,7 +293,9 @@ export class AKElement extends LitElement implements AKElementProps { // All child slots accessible from the LightDom that match the request *and* are not nested // within another slotted element. - return allChildSlotRequests.filter((node) => isNotNestedSlot(node)).length > 0; + const match = allChildSlotRequests.find((node) => isNotNestedSlot(node)); + + return (match ?? null) as T | null; } //#endregion diff --git a/web/src/elements/EmptyState.ts b/web/src/elements/EmptyState.ts index 1850c4da538a..7be7daa23027 100644 --- a/web/src/elements/EmptyState.ts +++ b/web/src/elements/EmptyState.ts @@ -69,6 +69,10 @@ export class EmptyState extends AKElement implements IEmptyState { PFEmptyState, PFTitle, css` + :host { + display: block; + } + i.pf-c-empty-state__icon { height: var(--pf-global--icon--FontSize--2xl); line-height: var(--pf-global--icon--FontSize--2xl); @@ -88,7 +92,7 @@ export class EmptyState extends AKElement implements IEmptyState { } render() { - const hasHeading = this.hasSlotted(null); + const hasHeading = this.findSlotted(); const loading = this.loading || this.defaultLabel; const classes = { "pf-c-empty-state": true, @@ -112,12 +116,12 @@ export class EmptyState extends AKElement implements IEmptyState { ` : nothing} - ${this.hasSlotted("body") + ${this.findSlotted("body") ? html`
` : nothing} - ${this.hasSlotted("primary") + ${this.findSlotted("primary") ? html`
` diff --git a/web/src/elements/LoadingOverlay.ts b/web/src/elements/LoadingOverlay.ts index f4ee256703af..25bf2def6971 100644 --- a/web/src/elements/LoadingOverlay.ts +++ b/web/src/elements/LoadingOverlay.ts @@ -57,8 +57,8 @@ export class LoadingOverlay extends AKElement implements ILoadingOverlay { render() { // Nested slots. Can get a little cognitively heavy, so be careful if you're editing here... return html` - ${this.hasSlotted(null) ? html`` : nothing} - ${this.hasSlotted("body") + ${this.findSlotted() ? html`` : nothing} + ${this.findSlotted("body") ? html`` : nothing} `; diff --git a/web/src/elements/ak-progress-bar.ts b/web/src/elements/ak-progress-bar.ts index 50cd88009365..003000a55aa5 100644 --- a/web/src/elements/ak-progress-bar.ts +++ b/web/src/elements/ak-progress-bar.ts @@ -88,14 +88,14 @@ export class ProgressBar extends AKElement { ? "pf-m-indeterminate" : ""} ${this.size}" > - ${this.hasSlotted("description") + ${this.findSlotted("description") ? html`
` : nothing} - ${this.hasSlotted("status") + ${this.findSlotted("status") ? html`