mirror of
https://github.com/goauthentik/authentik
synced 2026-05-12 01:47:06 +02:00
Compare commits
1 Commits
version/20
...
a11y-effec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
155b13f35d |
142
web/src/common/a11y.ts
Normal file
142
web/src/common/a11y.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* @file Accessibility utilities.
|
||||
*/
|
||||
|
||||
import { createMediaQueryEffect } from "#common/matchers";
|
||||
|
||||
//#region Constants
|
||||
|
||||
/**
|
||||
* Class names applied to the document element based on user accessibility preferences.
|
||||
*
|
||||
* @category Accessibility
|
||||
*/
|
||||
export const A11YClassName = {
|
||||
ReduceMotion: "ak-m-reduce-motion",
|
||||
ReduceTransparency: "ak-m-reduce-transparency",
|
||||
MoreContrast: "ak-m-more-contrast",
|
||||
LessContrast: "ak-m-less-contrast",
|
||||
} as const;
|
||||
|
||||
export type A11YClassName = (typeof A11YClassName)[keyof typeof A11YClassName];
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Effects
|
||||
|
||||
/**
|
||||
* Create an effect that applies the user's motion preference to the document.
|
||||
*
|
||||
* ```ts
|
||||
* const dispose = createMotionPreferenceEffect();
|
||||
* ```
|
||||
*
|
||||
* @category Accessibility
|
||||
*/
|
||||
export function createMotionPreferenceEffect() {
|
||||
const dispose = createMediaQueryEffect(
|
||||
"(prefers-reduced-motion: reduce)",
|
||||
(event) => {
|
||||
document.documentElement.classList.toggle(A11YClassName.ReduceMotion, event.matches);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
return dispose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an effect that applies the user's motion preference to the document.
|
||||
*
|
||||
* ```ts
|
||||
* const dispose = createMotionPreferenceEffect();
|
||||
* ```
|
||||
*
|
||||
* @category Accessibility
|
||||
*/
|
||||
export function createContrastPreferenceEffect() {
|
||||
const dispose = createMediaQueryEffect(
|
||||
"(prefers-contrast: more)",
|
||||
(event) => {
|
||||
document.documentElement.classList.toggle(A11YClassName.MoreContrast, event.matches);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
return dispose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an effect that applies the user's motion preference to the document.
|
||||
*
|
||||
* ```ts
|
||||
* const dispose = createMotionPreferenceEffect();
|
||||
* ```
|
||||
*
|
||||
* @category Accessibility
|
||||
*/
|
||||
export function createTransparencyPreferenceEffect() {
|
||||
const dispose = createMediaQueryEffect(
|
||||
"(prefers-reduced-transparency: reduce)",
|
||||
(event) => {
|
||||
document.documentElement.classList.toggle(
|
||||
A11YClassName.ReduceTransparency,
|
||||
event.matches,
|
||||
);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
return dispose;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Predicates
|
||||
|
||||
/**
|
||||
* Predicate to determine if the user agent indicates a preference for reduced motion.
|
||||
*
|
||||
* ```ts
|
||||
* const prefersReducedMotion = isReducedMotionPreferred();
|
||||
* ```
|
||||
*
|
||||
* @category Accessibility
|
||||
*/
|
||||
export function isReducedMotionPreferred(): boolean {
|
||||
return document.documentElement.classList.contains(A11YClassName.ReduceMotion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Predicate to determine if the user agent indicates a preference for reduced transparency.
|
||||
*
|
||||
* ```ts
|
||||
* const prefersReducedTransparency = isReducedTransparencyPreferred();
|
||||
* ```
|
||||
*
|
||||
* @category Accessibility
|
||||
*/
|
||||
export function isReducedTransparencyPreferred(): boolean {
|
||||
return document.documentElement.classList.contains(A11YClassName.ReduceTransparency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Predicate to determine if the user agent indicates a preference for more contrast.
|
||||
*
|
||||
* ```ts
|
||||
* const prefersMoreContrast = isMoreContrastPreferred();
|
||||
* ```
|
||||
*
|
||||
* @category Accessibility
|
||||
*/
|
||||
export function isMoreContrastPreferred(): boolean {
|
||||
return document.documentElement.classList.contains(A11YClassName.MoreContrast);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
59
web/src/common/matchers.ts
Normal file
59
web/src/common/matchers.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Invoked when cleanup is required.
|
||||
*/
|
||||
export type MediaMatcherDispose = () => void;
|
||||
|
||||
/**
|
||||
* Callback invoked when a media query match changes.
|
||||
*/
|
||||
export type MediaChangeEffect = (event: MediaQueryListEvent) => void;
|
||||
|
||||
export interface MediaQueryEffectListenerOptions extends AddEventListenerOptions {
|
||||
/**
|
||||
* Whether to invoke the effect immediately upon creation.
|
||||
*/
|
||||
immediate?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an effect that runs on a media query match.
|
||||
*
|
||||
* @param query The media query to match.
|
||||
* @param effect The callback to run when the media query matches.
|
||||
* @param listenerOptions Options for the event listener.
|
||||
*
|
||||
* @returns A cleanup function that removes the effect.
|
||||
*/
|
||||
export function createMediaQueryEffect(
|
||||
query: string | MediaQueryList,
|
||||
effect: MediaChangeEffect,
|
||||
listenerOptions?: MediaQueryEffectListenerOptions,
|
||||
): MediaMatcherDispose {
|
||||
const mediaQueryList = typeof query === "string" ? window.matchMedia(query) : query;
|
||||
|
||||
// First, wrap the effect to ensure we can abort it.
|
||||
const mediaChangeListener = (event: MediaQueryListEvent) => {
|
||||
if (listenerOptions?.signal?.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
effect(event);
|
||||
};
|
||||
|
||||
mediaQueryList.addEventListener("change", mediaChangeListener, listenerOptions);
|
||||
|
||||
const dispose: MediaMatcherDispose = () => {
|
||||
mediaQueryList.removeEventListener("change", mediaChangeListener);
|
||||
};
|
||||
|
||||
if (listenerOptions?.immediate) {
|
||||
effect(
|
||||
new MediaQueryListEvent("change", {
|
||||
matches: mediaQueryList.matches,
|
||||
media: mediaQueryList.media,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return dispose;
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
* @file Theme utilities.
|
||||
*/
|
||||
|
||||
import { createMediaQueryEffect, MediaChangeEffect } from "#common/matchers";
|
||||
import { setAdoptedStyleSheets, type StyleRoot } from "#common/stylesheets";
|
||||
|
||||
import { UiThemeEnum } from "@goauthentik/api";
|
||||
@@ -156,9 +157,7 @@ export function createUIThemeEffect(
|
||||
const mediaQueryList = createColorSchemeTarget(colorSchemeTarget);
|
||||
|
||||
// First, wrap the effect to ensure we can abort it.
|
||||
const mediaChangeListener = (event: MediaQueryListEvent) => {
|
||||
if (listenerOptions?.signal?.aborted) return;
|
||||
|
||||
const mediaChangeListener: MediaChangeEffect = (event) => {
|
||||
const { themeChoice, theme: previousTheme } = document.documentElement.dataset;
|
||||
|
||||
if (themeChoice && themeChoice !== "auto") {
|
||||
@@ -197,12 +196,16 @@ export function createUIThemeEffect(
|
||||
});
|
||||
|
||||
// Listen for changes to the color scheme...
|
||||
mediaQueryList.addEventListener("change", mediaChangeListener);
|
||||
const disposeChangeListener = createMediaQueryEffect(
|
||||
mediaQueryList,
|
||||
mediaChangeListener,
|
||||
listenerOptions,
|
||||
);
|
||||
|
||||
// Finally, allow the caller to remove the effect.
|
||||
const cleanup = () => {
|
||||
documentObserver.disconnect();
|
||||
mediaQueryList.removeEventListener("change", mediaChangeListener);
|
||||
disposeChangeListener();
|
||||
};
|
||||
|
||||
listenerOptions?.signal?.addEventListener("abort", cleanup);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
createContrastPreferenceEffect,
|
||||
createMotionPreferenceEffect,
|
||||
createTransparencyPreferenceEffect,
|
||||
} from "#common/a11y";
|
||||
import { globalAK } from "#common/global";
|
||||
import { applyDocumentTheme, createUIThemeEffect } from "#common/theme";
|
||||
|
||||
@@ -20,6 +25,10 @@ export abstract class Interface extends AKElement {
|
||||
this.addController(new ConfigContextController(this, config));
|
||||
this.addController(new BrandingContextController(this, brand));
|
||||
this.addController(new ModalOrchestrationController());
|
||||
|
||||
createMotionPreferenceEffect();
|
||||
createContrastPreferenceEffect();
|
||||
createTransparencyPreferenceEffect();
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
|
||||
Reference in New Issue
Block a user