web: Safari fixes merge branch (#14181)
* web/admin: Fix layout centering. Adjust theming. * web: Fix issue where references to Lit SSR break page styles. * web: Fix issues surrounding color scheme/theme mixup in UI.
This commit is contained in:
		| @ -1,165 +1,125 @@ | ||||
| import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants"; | ||||
| import { globalAK } from "@goauthentik/common/global"; | ||||
| import { UIConfig } from "@goauthentik/common/ui/config"; | ||||
| import { adaptCSS } from "@goauthentik/common/utils"; | ||||
| import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet"; | ||||
| import { | ||||
|     StyleSheetInit, | ||||
|     StyleSheetParent, | ||||
|     appendStyleSheet, | ||||
|     createStyleSheetUnsafe, | ||||
|     removeStyleSheet, | ||||
|     resolveStyleSheetParent, | ||||
| } from "@goauthentik/common/stylesheets"; | ||||
| import { ResolvedUITheme, createUIThemeEffect, resolveUITheme } from "@goauthentik/common/theme"; | ||||
| import { type ThemedElement } from "@goauthentik/common/theme"; | ||||
|  | ||||
| import { localized } from "@lit/localize"; | ||||
| import { LitElement, ReactiveElement } from "lit"; | ||||
| import { CSSResultGroup, CSSResultOrNative, LitElement } from "lit"; | ||||
| import { property } from "lit/decorators.js"; | ||||
|  | ||||
| import AKGlobal from "@goauthentik/common/styles/authentik.css"; | ||||
| import OneDark from "@goauthentik/common/styles/one-dark.css"; | ||||
| import ThemeDark from "@goauthentik/common/styles/theme-dark.css"; | ||||
|  | ||||
| import { Config, CurrentBrand, UiThemeEnum } from "@goauthentik/api"; | ||||
| import { CurrentBrand, UiThemeEnum } from "@goauthentik/api"; | ||||
|  | ||||
| type AkInterface = HTMLElement & { | ||||
|     getTheme: () => Promise<UiThemeEnum>; | ||||
|     brand?: CurrentBrand; | ||||
|     uiConfig?: UIConfig; | ||||
|     config?: Config; | ||||
|     get activeTheme(): UiThemeEnum | undefined; | ||||
| }; | ||||
| // Re-export the theme helpers | ||||
| export { rootInterface } from "@goauthentik/common/theme"; | ||||
|  | ||||
| export const rootInterface = <T extends AkInterface>(): T | undefined => | ||||
|     (document.body.querySelector("[data-ak-interface-root]") as T) ?? undefined; | ||||
|  | ||||
| export const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)"; | ||||
|  | ||||
| // Ensure themes are converted to a static instance of CSS Stylesheet, otherwise the | ||||
| // when changing themes we might not remove the correct css stylesheet instance. | ||||
| const _darkTheme = ensureCSSStyleSheet(ThemeDark); | ||||
| export interface AKElementInit { | ||||
|     brand?: Partial<CurrentBrand>; | ||||
|     styleParents?: StyleSheetParent[]; | ||||
| } | ||||
|  | ||||
| @localized() | ||||
| export class AKElement extends LitElement { | ||||
|     _mediaMatcher?: MediaQueryList; | ||||
|     _mediaMatcherHandler?: (ev?: MediaQueryListEvent) => void; | ||||
|     _activeTheme?: UiThemeEnum; | ||||
|  | ||||
|     get activeTheme(): UiThemeEnum | undefined { | ||||
|         return this._activeTheme; | ||||
|     } | ||||
|  | ||||
|     constructor() { | ||||
|         super(); | ||||
|     } | ||||
|  | ||||
|     setInitialStyles(root: DocumentOrShadowRoot) { | ||||
|         const styleRoot: DocumentOrShadowRoot = ( | ||||
|             "ShadyDOM" in window ? document : root | ||||
|         ) as DocumentOrShadowRoot; | ||||
|         styleRoot.adoptedStyleSheets = adaptCSS([ | ||||
|             ...styleRoot.adoptedStyleSheets, | ||||
|             ensureCSSStyleSheet(AKGlobal), | ||||
|             ensureCSSStyleSheet(OneDark), | ||||
|         ]); | ||||
|         this._initTheme(styleRoot); | ||||
|         this._initCustomCSS(styleRoot); | ||||
|     } | ||||
|  | ||||
|     protected createRenderRoot() { | ||||
|         this.fixElementStyles(); | ||||
|         const root = super.createRenderRoot(); | ||||
|         this.setInitialStyles(root as unknown as DocumentOrShadowRoot); | ||||
|         return root; | ||||
|     } | ||||
|  | ||||
|     async getTheme(): Promise<UiThemeEnum> { | ||||
|         return rootInterface()?.getTheme() || UiThemeEnum.Automatic; | ||||
|     } | ||||
|  | ||||
|     fixElementStyles() { | ||||
|         // Ensure all style sheets being passed are really style sheets. | ||||
|         (this.constructor as typeof ReactiveElement).elementStyles = ( | ||||
|             this.constructor as typeof ReactiveElement | ||||
|         ).elementStyles.map(ensureCSSStyleSheet); | ||||
|     } | ||||
|  | ||||
|     async _initTheme(root: DocumentOrShadowRoot): Promise<void> { | ||||
|         // Early activate theme based on media query to prevent light flash | ||||
|         // when dark is preferred | ||||
|         this._applyTheme(root, globalAK().brand.uiTheme); | ||||
|         this._applyTheme(root, await this.getTheme()); | ||||
|     } | ||||
|  | ||||
|     async _initCustomCSS(root: DocumentOrShadowRoot): Promise<void> { | ||||
|         const brand = globalAK().brand; | ||||
|         if (!brand) { | ||||
|             return; | ||||
|         } | ||||
|         const sheet = await new CSSStyleSheet().replace(brand.brandingCustomCss); | ||||
|         root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet]; | ||||
|     } | ||||
|  | ||||
|     _applyTheme(root: DocumentOrShadowRoot, theme?: UiThemeEnum): void { | ||||
|         if (!theme) { | ||||
|             theme = UiThemeEnum.Automatic; | ||||
|         } | ||||
|         if (theme === UiThemeEnum.Automatic) { | ||||
|             // Create a media matcher to automatically switch the theme depending on | ||||
|             // prefers-color-scheme | ||||
|             if (!this._mediaMatcher) { | ||||
|                 this._mediaMatcher = window.matchMedia(QUERY_MEDIA_COLOR_LIGHT); | ||||
|                 this._mediaMatcherHandler = (ev?: MediaQueryListEvent) => { | ||||
|                     const theme = | ||||
|                         ev?.matches || this._mediaMatcher?.matches | ||||
|                             ? UiThemeEnum.Light | ||||
|                             : UiThemeEnum.Dark; | ||||
|                     this._activateTheme(theme, root); | ||||
|                 }; | ||||
|                 this._mediaMatcherHandler(undefined); | ||||
|                 this._mediaMatcher.addEventListener("change", this._mediaMatcherHandler); | ||||
|             } | ||||
|             return; | ||||
|         } else if (this._mediaMatcher && this._mediaMatcherHandler) { | ||||
|             // Theme isn't automatic and we have a matcher configured, remove the matcher | ||||
|             // to prevent changes | ||||
|             this._mediaMatcher.removeEventListener("change", this._mediaMatcherHandler); | ||||
|             this._mediaMatcher = undefined; | ||||
|         } | ||||
|         this._activateTheme(theme, root); | ||||
|     } | ||||
|  | ||||
|     static themeToStylesheet(theme?: UiThemeEnum): CSSStyleSheet | undefined { | ||||
|         if (theme === UiThemeEnum.Dark) { | ||||
|             return _darkTheme; | ||||
|         } | ||||
|         return undefined; | ||||
|     } | ||||
|  | ||||
| export class AKElement extends LitElement implements ThemedElement { | ||||
|     /** | ||||
|      * Directly activate a given theme, accepts multiple document/ShadowDOMs to apply the stylesheet | ||||
|      * to. The stylesheets are applied to each DOM in order. Does nothing if the given theme is already active. | ||||
|      * The resolved theme of the current element. | ||||
|      * | ||||
|      * @remarks | ||||
|      * | ||||
|      * Unlike the browser's current color scheme, this is a value that can be | ||||
|      * resolved to a specific theme, i.e. dark or light. | ||||
|      */ | ||||
|     _activateTheme(theme: UiThemeEnum, ...roots: DocumentOrShadowRoot[]) { | ||||
|         if (theme === this._activeTheme) { | ||||
|             return; | ||||
|     @property({ | ||||
|         attribute: "theme", | ||||
|         type: String, | ||||
|         reflect: true, | ||||
|     }) | ||||
|     public activeTheme: ResolvedUITheme; | ||||
|  | ||||
|     protected static readonly DarkColorSchemeStyleSheet = createStyleSheetUnsafe(ThemeDark); | ||||
|  | ||||
|     protected static finalizeStyles(styles?: CSSResultGroup): CSSResultOrNative[] { | ||||
|         // Ensure all style sheets being passed are really style sheets. | ||||
|         const baseStyles: StyleSheetInit[] = [AKGlobal, OneDark]; | ||||
|  | ||||
|         if (!styles) return baseStyles.map(createStyleSheetUnsafe); | ||||
|  | ||||
|         if (Array.isArray(styles)) { | ||||
|             return [ | ||||
|                 //--- | ||||
|                 ...(styles as unknown as CSSResultOrNative[]), | ||||
|                 ...baseStyles, | ||||
|             ].flatMap(createStyleSheetUnsafe); | ||||
|         } | ||||
|         // Make sure we only get to this callback once we've picked a concise theme choice | ||||
|         this.dispatchEvent( | ||||
|             new CustomEvent(EVENT_THEME_CHANGE, { | ||||
|                 bubbles: true, | ||||
|                 composed: true, | ||||
|                 detail: theme, | ||||
|             }), | ||||
|         return [styles, ...baseStyles].map(createStyleSheetUnsafe); | ||||
|     } | ||||
|  | ||||
|     constructor(init?: AKElementInit) { | ||||
|         super(); | ||||
|  | ||||
|         const config = globalAK(); | ||||
|         const { brand = config.brand, styleParents = [] } = init || {}; | ||||
|  | ||||
|         this.activeTheme = resolveUITheme(brand?.uiTheme); | ||||
|         this.#styleParents = styleParents; | ||||
|  | ||||
|         this.#customCSSStyleSheet = brand?.brandingCustomCss | ||||
|             ? createStyleSheetUnsafe(brand.brandingCustomCss) | ||||
|             : null; | ||||
|     } | ||||
|  | ||||
|     #styleParents: StyleSheetParent[] = []; | ||||
|     #customCSSStyleSheet: CSSStyleSheet | null; | ||||
|  | ||||
|     #themeAbortController: AbortController | null = null; | ||||
|  | ||||
|     public disconnectedCallback(): void { | ||||
|         super.disconnectedCallback(); | ||||
|         this.#themeAbortController?.abort(); | ||||
|     } | ||||
|  | ||||
|     protected createRenderRoot(): HTMLElement | DocumentFragment { | ||||
|         const renderRoot = super.createRenderRoot(); | ||||
|  | ||||
|         const styleRoot = resolveStyleSheetParent(renderRoot); | ||||
|         const styleParents = Array.from( | ||||
|             new Set<StyleSheetParent>([styleRoot, ...this.#styleParents]), | ||||
|         ); | ||||
|         this.setAttribute("theme", theme); | ||||
|         const stylesheet = AKElement.themeToStylesheet(theme); | ||||
|         const oldStylesheet = AKElement.themeToStylesheet(this._activeTheme); | ||||
|         roots.forEach((root) => { | ||||
|             if (stylesheet) { | ||||
|                 root.adoptedStyleSheets = [ | ||||
|                     ...root.adoptedStyleSheets, | ||||
|                     ensureCSSStyleSheet(stylesheet), | ||||
|                 ]; | ||||
|             } | ||||
|             if (oldStylesheet) { | ||||
|                 root.adoptedStyleSheets = root.adoptedStyleSheets.filter( | ||||
|                     (v) => v !== oldStylesheet, | ||||
|                 ); | ||||
|             } | ||||
|         }); | ||||
|         this._activeTheme = theme; | ||||
|         this.requestUpdate(); | ||||
|  | ||||
|         if (this.#customCSSStyleSheet) { | ||||
|             console.debug(`authentik/element[${this.tagName.toLowerCase()}]: Adding custom CSS`); | ||||
|  | ||||
|             styleRoot.adoptedStyleSheets = [ | ||||
|                 ...styleRoot.adoptedStyleSheets, | ||||
|                 this.#customCSSStyleSheet, | ||||
|             ]; | ||||
|         } | ||||
|  | ||||
|         this.#themeAbortController = new AbortController(); | ||||
|  | ||||
|         createUIThemeEffect( | ||||
|             (currentUITheme) => { | ||||
|                 if (currentUITheme === UiThemeEnum.Dark) { | ||||
|                     appendStyleSheet(AKElement.DarkColorSchemeStyleSheet, ...styleParents); | ||||
|                 } else { | ||||
|                     removeStyleSheet(AKElement.DarkColorSchemeStyleSheet, ...styleParents); | ||||
|                 } | ||||
|                 this.activeTheme = currentUITheme; | ||||
|             }, | ||||
|             { | ||||
|                 signal: this.#themeAbortController.signal, | ||||
|             }, | ||||
|         ); | ||||
|  | ||||
|         return renderRoot; | ||||
|     } | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Teffen Ellis
					Teffen Ellis