web/elements: add scrollbar helpers and apply to Interface (#21511)

Introduce `elements/utils/scrollbars.ts` with `measureScrollbarWidth`
and `applyScrollbarClass`, and call it from `Interface` so the root
document picks up `ak-m-visible-scrollbars` / `ak-m-overlay-scrollbars`
depending on the platform. Add an `ak-m-thin-scrollbar` selector to
`base/scrollbars.css` so ad-hoc containers can opt in.

Drop the unused `elements/utils/isVisible.ts`.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Teffen Ellis
2026-04-10 17:52:20 +02:00
committed by GitHub
parent b590bffa57
commit 2f3b38623a
4 changed files with 51 additions and 23 deletions

View File

@@ -14,6 +14,7 @@ import { ModalOrchestrationController } from "#elements/controllers/ModalOrchest
import { ReactiveContextController } from "#elements/controllers/ReactiveContextController";
import { BrandingContext } from "#elements/mixins/branding";
import { AuthentikConfigContext } from "#elements/mixins/config";
import { applyScrollbarClass } from "#elements/utils/scrollbars";
import { ConsoleLogger, Logger } from "#logger/browser";
@@ -43,11 +44,12 @@ export abstract class Interface extends AKElement {
constructor() {
super();
this.logger = ConsoleLogger.prefix(this.tagName.toLowerCase());
this.logger = ConsoleLogger.prefix(this.localName);
const { config, brand, locale } = globalAK();
createUIThemeEffect(applyDocumentTheme);
applyScrollbarClass(this.ownerDocument);
this.addController(new LocaleContextController(this, locale));
this.addController(new ConfigContextController(this, config), AuthentikConfigContext);

View File

@@ -1,22 +0,0 @@
const isStyledVisible = ({ visibility, display }: CSSStyleDeclaration) =>
visibility !== "hidden" && display !== "none";
const isDisplayContents = ({ display }: CSSStyleDeclaration) => display === "contents";
function computedStyleIsVisible(element: HTMLElement) {
const computedStyle = window.getComputedStyle(element);
return (
isStyledVisible(computedStyle) &&
(isDisplayContents(computedStyle) ||
!!(element.offsetWidth || element.offsetHeight || element.getClientRects().length))
);
}
export function isVisible(element: HTMLElement) {
return (
element &&
element.isConnected &&
isStyledVisible(element.style) &&
computedStyleIsVisible(element)
);
}

View File

@@ -0,0 +1,47 @@
/**
* @file Scrollbar utilities.
*/
/**
* @returns The width of the scrollbar in pixels, or 0 if the browser uses overlay scrollbars.
*/
export function measureScrollbarWidth(container: HTMLElement = document.body): number {
const outer = container.ownerDocument.createElement("div");
outer.style.overflow = "scroll";
outer.style.width = "100px";
outer.style.visibility = "hidden";
container.appendChild(outer);
const width = outer.offsetWidth - outer.clientWidth;
container.removeChild(outer);
return width;
}
export const ScrollbarClassName = {
Visible: "ak-m-visible-scrollbars",
Overlay: "ak-m-overlay-scrollbars",
} as const;
export type ScrollbarClassName = (typeof ScrollbarClassName)[keyof typeof ScrollbarClassName];
/**
* Applies the appropriate scrollbar class to the given container element
* based on whether the browser uses visible or overlay scrollbars.
*
* @param ownerDocument The document to apply the scrollbar class to.
*/
export function applyScrollbarClass(ownerDocument: Document = document): void {
const scrollbarWidth = measureScrollbarWidth(ownerDocument.body);
if (scrollbarWidth) {
ownerDocument.documentElement.classList.add(ScrollbarClassName.Visible);
ownerDocument.documentElement.classList.remove(ScrollbarClassName.Overlay);
} else {
ownerDocument.documentElement.classList.add(ScrollbarClassName.Overlay);
ownerDocument.documentElement.classList.remove(ScrollbarClassName.Visible);
}
}

View File

@@ -89,6 +89,7 @@ html[data-theme="dark"],
}
@supports (scrollbar-width: thin) {
.ak-m-thin-scrollbar,
.pf-c-page__main,
.pf-c-nav__list,
.pf-c-card__body {