Files
authentik/web/src/user/LibraryPage/ApplicationList.ts
T
authentik-automation[bot] b72709ebbc web/a11y: User library -- fix issues surrounding element focus, ARIA labeling. (cherry-pick #17522 to version-2025.10) (#17828)
web/a11y: User library -- fix issues surrounding element focus, ARIA labeling. (#17522)

* web/a11y: Fix issues surrounding element focus, aria labeling.

* web: Fix focus

* web: Fix nested focus

* web: Fix menu visibility when anchor positioning is not supported.

* web: Fix icon fallback behavior, labels.

* web: Fix flickering, descriptions.

* web: Fix excess width on mobile.

* web: Fix rendering artifacts on mobile.

* web: Remove aria-controls behavior.

- This is buggy, similar to aria-owns, and may cause crashes.

* web: Fix tabpanel focus attempting to scroll page.

* web: Fix issues surrounding consistent tab panel parameter testing.

* web: add shared helpers.

* web: Tidy comments.

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
2025-11-01 17:05:19 +01:00

110 lines
3.9 KiB
TypeScript

import type { AppGroupEntry } from "./types.js";
import { rootInterface } from "#common/theme";
import { LayoutType } from "#common/ui/config";
import { LitFC } from "#elements/types";
import { ifPresent } from "#elements/utils/attributes";
import { UserInterface } from "#user/index.entrypoint";
import { AnchorPositionSupported } from "#user/LibraryApplication/CardMenu";
import { AKLibraryApp } from "#user/LibraryApplication/index";
import { ApplicationRoute } from "#admin/Routes";
import { Application } from "@goauthentik/api";
import { spread } from "@open-wc/lit-helpers";
import { kebabCase } from "change-case";
import { HTMLAttributes } from "react";
import { msg } from "@lit/localize";
import { html, nothing } from "lit";
import { RefOrCallback } from "lit/directives/ref.js";
import { repeat } from "lit/directives/repeat.js";
const LayoutColumnCount = {
[LayoutType.row]: 1,
[LayoutType.column_2]: 2,
[LayoutType.column_3]: 3,
} as const satisfies Record<LayoutType, number>;
export interface AKLibraryApplicationListProps extends HTMLAttributes<HTMLDivElement> {
groupedApps: AppGroupEntry[];
layout: LayoutType;
background?: string | null;
selectedApp?: Application | null;
targetRef?: RefOrCallback | null;
}
/**
* Renders the current library list of a User's Applications.
*/
export const AKLibraryApplicationList: LitFC<AKLibraryApplicationListProps> = ({
groupedApps,
layout = LayoutType.row,
background,
selectedApp,
targetRef,
...props
}) => {
const columnCount = LayoutColumnCount[layout] ?? 1;
const { me, uiConfig } = rootInterface<UserInterface>();
const canEdit = !!(uiConfig?.enabledFeatures.applicationEdit && me?.user.isSuperuser);
return html`<div
role="presentation"
part="app-list"
data-anchor-strategy=${AnchorPositionSupported ? "anchor-position" : "fallback"}
style="--app-list-column-count: ${columnCount}"
${spread(props)}
>
${repeat(
groupedApps,
([groupLabel]) => groupLabel,
([groupLabel, apps], groupIndex) => {
const groupID = kebabCase(groupLabel);
const activeDescendantID =
selectedApp && apps.includes(selectedApp) ? `app-${selectedApp.pk}` : nothing;
return html`<fieldset
data-group-id=${ifPresent(groupID)}
part="app-group"
data-group-index=${groupIndex}
data-app-count=${apps.length}
aria-activedescendant=${activeDescendantID}
>
<legend
class="pf-c-content ${!groupLabel ? "less-contrast-sr-only" : ""}"
part="app-group-header"
>
<h2 id=${`app-group-${groupID}`}>${groupLabel || msg("Ungrouped")}</h2>
</legend>
${repeat(
apps,
(application) => application.pk,
(application, appIndex) => {
const selected = selectedApp === application;
const editURL = canEdit
? ApplicationRoute.EditURL(application.slug)
: null;
return AKLibraryApp({
application,
appIndex,
groupIndex,
background,
editURL,
"targetRef": selected ? targetRef : null,
"aria-selected": selected,
});
},
)}
<hr part="app-group-separator" aria-hidden="true" />
</fieldset>`;
},
)}
</div>`;
};