Revert-revert: Safari fixes (#14331)
* Reapply "web: Safari fixes merge branch (#14181)"
This reverts commit a41d45834c.
* web: Fix brand preference order. Adjust header height.
			
			
This commit is contained in:
		| @ -1,165 +1,140 @@ | ||||
| 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 { | ||||
|     CSSColorSchemeValue, | ||||
|     ResolvedUITheme, | ||||
|     UIThemeListener, | ||||
|     createUIThemeEffect, | ||||
|     formatColorScheme, | ||||
|     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 { UiThemeEnum } from "@goauthentik/api"; | ||||
|  | ||||
| type AkInterface = HTMLElement & { | ||||
|     getTheme: () => Promise<UiThemeEnum>; | ||||
|     brand?: CurrentBrand; | ||||
|     uiConfig?: UIConfig; | ||||
|     config?: Config; | ||||
|     get activeTheme(): UiThemeEnum | undefined; | ||||
| }; | ||||
|  | ||||
| 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); | ||||
| // Re-export the theme helpers | ||||
| export { rootInterface } from "@goauthentik/common/theme"; | ||||
|  | ||||
| @localized() | ||||
| export class AKElement extends LitElement { | ||||
|     _mediaMatcher?: MediaQueryList; | ||||
|     _mediaMatcherHandler?: (ev?: MediaQueryListEvent) => void; | ||||
|     _activeTheme?: UiThemeEnum; | ||||
| export class AKElement extends LitElement implements ThemedElement { | ||||
|     //#region Properties | ||||
|  | ||||
|     get activeTheme(): UiThemeEnum | undefined { | ||||
|         return this._activeTheme; | ||||
|     /** | ||||
|      * 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. | ||||
|      */ | ||||
|     @property({ | ||||
|         attribute: "theme", | ||||
|         type: String, | ||||
|         reflect: true, | ||||
|     }) | ||||
|     public activeTheme: ResolvedUITheme; | ||||
|  | ||||
|     //#endregion | ||||
|  | ||||
|     //#region Private Properties | ||||
|  | ||||
|     readonly #preferredColorScheme: CSSColorSchemeValue; | ||||
|  | ||||
|     #customCSSStyleSheet: CSSStyleSheet | null; | ||||
|     #darkThemeStyleSheet: CSSStyleSheet | null = null; | ||||
|     #themeAbortController: AbortController | null = null; | ||||
|  | ||||
|     //#endregion | ||||
|  | ||||
|     //#region Lifecycle | ||||
|  | ||||
|     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); | ||||
|         } | ||||
|         return [styles, ...baseStyles].map(createStyleSheetUnsafe); | ||||
|     } | ||||
|  | ||||
|     constructor() { | ||||
|         super(); | ||||
|  | ||||
|         const { brand } = globalAK(); | ||||
|  | ||||
|         this.#preferredColorScheme = formatColorScheme(brand.uiTheme); | ||||
|         this.activeTheme = resolveUITheme(brand?.uiTheme); | ||||
|  | ||||
|         this.#customCSSStyleSheet = brand?.brandingCustomCss | ||||
|             ? createStyleSheetUnsafe(brand.brandingCustomCss) | ||||
|             : null; | ||||
|     } | ||||
|  | ||||
|     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); | ||||
|     public disconnectedCallback(): void { | ||||
|         super.disconnectedCallback(); | ||||
|         this.#themeAbortController?.abort(); | ||||
|     } | ||||
|  | ||||
|     protected createRenderRoot() { | ||||
|         this.fixElementStyles(); | ||||
|         const root = super.createRenderRoot(); | ||||
|         this.setInitialStyles(root as unknown as DocumentOrShadowRoot); | ||||
|         return root; | ||||
|     } | ||||
|     #styleRoot?: StyleSheetParent; | ||||
|  | ||||
|     async getTheme(): Promise<UiThemeEnum> { | ||||
|         return rootInterface()?.getTheme() || UiThemeEnum.Automatic; | ||||
|     } | ||||
|     #dispatchTheme: UIThemeListener = (nextUITheme) => { | ||||
|         if (!this.#styleRoot) return; | ||||
|  | ||||
|     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; | ||||
|         if (nextUITheme === UiThemeEnum.Dark) { | ||||
|             this.#darkThemeStyleSheet ||= createStyleSheetUnsafe(ThemeDark); | ||||
|             appendStyleSheet(this.#styleRoot, this.#darkThemeStyleSheet); | ||||
|             this.activeTheme = UiThemeEnum.Dark; | ||||
|         } else if (this.#darkThemeStyleSheet) { | ||||
|             removeStyleSheet(this.#styleRoot, this.#darkThemeStyleSheet); | ||||
|             this.#darkThemeStyleSheet = null; | ||||
|             this.activeTheme = UiThemeEnum.Light; | ||||
|         } | ||||
|         const sheet = await new CSSStyleSheet().replace(brand.brandingCustomCss); | ||||
|         root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet]; | ||||
|     }; | ||||
|  | ||||
|     protected createRenderRoot(): HTMLElement | DocumentFragment { | ||||
|         const renderRoot = super.createRenderRoot(); | ||||
|         this.#styleRoot = resolveStyleSheetParent(renderRoot); | ||||
|  | ||||
|         if (this.#customCSSStyleSheet) { | ||||
|             console.debug(`authentik/element[${this.tagName.toLowerCase()}]: Adding custom CSS`); | ||||
|  | ||||
|             appendStyleSheet(this.#styleRoot, this.#customCSSStyleSheet); | ||||
|         } | ||||
|  | ||||
|         this.#themeAbortController = new AbortController(); | ||||
|  | ||||
|         if (this.#preferredColorScheme === "dark") { | ||||
|             this.#dispatchTheme(UiThemeEnum.Dark); | ||||
|         } else if (this.#preferredColorScheme === "auto") { | ||||
|             createUIThemeEffect(this.#dispatchTheme, { | ||||
|                 signal: this.#themeAbortController.signal, | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         return renderRoot; | ||||
|     } | ||||
|  | ||||
|     _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; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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. | ||||
|      */ | ||||
|     _activateTheme(theme: UiThemeEnum, ...roots: DocumentOrShadowRoot[]) { | ||||
|         if (theme === this._activeTheme) { | ||||
|             return; | ||||
|         } | ||||
|         // 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, | ||||
|             }), | ||||
|         ); | ||||
|         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(); | ||||
|     } | ||||
|     //#endregion | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Teffen Ellis
					Teffen Ellis