mirror of
https://github.com/goauthentik/authentik
synced 2026-05-05 22:52:42 +02:00
Compare commits
6 Commits
modal-revi
...
a11y-modal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4a8703329 | ||
|
|
3db9450651 | ||
|
|
f55e53ea6a | ||
|
|
c2261d2528 | ||
|
|
f5a772abbc | ||
|
|
7bfd40f555 |
@@ -3,137 +3,172 @@ import "#elements/EmptyState";
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { globalAK } from "#common/global";
|
||||
|
||||
import { ModalButton } from "#elements/buttons/ModalButton";
|
||||
import { WithBrandConfig } from "#elements/mixins/branding";
|
||||
import { WithLicenseSummary } from "#elements/mixins/license";
|
||||
import { AKModal } from "#elements/modals/ak-modal";
|
||||
import { asInvoker } from "#elements/modals/utils";
|
||||
import { ThemedImage } from "#elements/utils/images";
|
||||
|
||||
import { AdminApi, CapabilitiesEnum, LicenseSummaryStatusEnum } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, html, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { createRef, ref } from "lit/directives/ref.js";
|
||||
import { until } from "lit/directives/until.js";
|
||||
import { ref } from "lit-html/directives/ref.js";
|
||||
import { styleMap } from "lit-html/directives/style-map.js";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
import PFAbout from "@patternfly/patternfly/components/AboutModalBox/about-modal-box.css";
|
||||
|
||||
@customElement("ak-about-modal")
|
||||
export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton)) {
|
||||
static styles = [
|
||||
...ModalButton.styles,
|
||||
PFAbout,
|
||||
css`
|
||||
.pf-c-about-modal-box {
|
||||
--pf-c-about-modal-box--BackgroundColor: var(--pf-global--palette--black-900);
|
||||
}
|
||||
const DEFAULT_BRAND_IMAGE = "/static/dist/assets/images/flow_background.jpg";
|
||||
|
||||
.pf-c-about-modal-box__hero {
|
||||
background-image: url("/static/dist/assets/images/flow_background.jpg");
|
||||
}
|
||||
.pf-c-about-modal-box__brand {
|
||||
--pf-c-about-modal-box__brand-image--Height: 6.25rem;
|
||||
}
|
||||
.pf-c-about-modal-box__brand i {
|
||||
font-size: var(--pf-c-about-modal-box__brand-image--Height);
|
||||
type AboutEntry = [label: string, content: string | TemplateResult];
|
||||
|
||||
async function fetchAboutDetails(): Promise<AboutEntry[]> {
|
||||
const api = new AdminApi(DEFAULT_CONFIG);
|
||||
|
||||
const [status, version] = await Promise.all([
|
||||
api.adminSystemRetrieve(),
|
||||
api.adminVersionRetrieve(),
|
||||
]);
|
||||
|
||||
let build: string | TemplateResult = msg("Release");
|
||||
|
||||
if (globalAK().config.capabilities.includes(CapabilitiesEnum.CanDebug)) {
|
||||
build = msg("Development");
|
||||
} else if (version.buildHash) {
|
||||
build = html`<a
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/goauthentik/authentik/commit/${version.buildHash}"
|
||||
target="_blank"
|
||||
>${version.buildHash}</a
|
||||
>`;
|
||||
}
|
||||
|
||||
return [
|
||||
[msg("Version"), version.versionCurrent],
|
||||
[msg("UI Version"), import.meta.env.AK_VERSION],
|
||||
[msg("Build"), build],
|
||||
[msg("Python version"), status.runtime.pythonVersion],
|
||||
[msg("Platform"), status.runtime.platform],
|
||||
[msg("Kernel"), status.runtime.uname],
|
||||
[
|
||||
msg("OpenSSL"),
|
||||
`${status.runtime.opensslVersion} ${status.runtime.opensslFipsEnabled ? "FIPS" : ""}`,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@customElement("ak-about-modal")
|
||||
export class AboutModal extends WithLicenseSummary(WithBrandConfig(AKModal)) {
|
||||
static hostStyles = [
|
||||
css`
|
||||
.ak-c-modal:has(ak-about-modal) {
|
||||
--ak-c-modal--BackgroundColor: var(--pf-global--palette--black-900);
|
||||
--ak-c-modal--BorderColor: var(--pf-global--palette--black-600);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
async getAboutEntries(): Promise<[string, string | TemplateResult][]> {
|
||||
const status = await new AdminApi(DEFAULT_CONFIG).adminSystemRetrieve();
|
||||
const version = await new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve();
|
||||
let build: string | TemplateResult = msg("Release");
|
||||
if (globalAK().config.capabilities.includes(CapabilitiesEnum.CanDebug)) {
|
||||
build = msg("Development");
|
||||
} else if (version.buildHash !== "") {
|
||||
build = html`<a
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/goauthentik/authentik/commit/${version.buildHash}"
|
||||
target="_blank"
|
||||
>${version.buildHash}</a
|
||||
>`;
|
||||
}
|
||||
return [
|
||||
[msg("Version"), version.versionCurrent],
|
||||
[msg("UI Version"), import.meta.env.AK_VERSION],
|
||||
[msg("Build"), build],
|
||||
[msg("Python version"), status.runtime.pythonVersion],
|
||||
[msg("Platform"), status.runtime.platform],
|
||||
[msg("Kernel"), status.runtime.uname],
|
||||
[
|
||||
msg("OpenSSL"),
|
||||
`${status.runtime.opensslVersion} ${status.runtime.opensslFipsEnabled ? "FIPS" : ""}`,
|
||||
],
|
||||
];
|
||||
static styles = [
|
||||
...AKModal.styles,
|
||||
PFAbout,
|
||||
css`
|
||||
:host {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pf-c-about-modal-box {
|
||||
--pf-c-about-modal-box--BackgroundColor: var(--ak-c-modal--BackgroundColor);
|
||||
width: unset;
|
||||
height: 100%;
|
||||
max-height: unset;
|
||||
max-width: unset;
|
||||
z-index: unset;
|
||||
position: unset;
|
||||
box-shadow: unset;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public static open = asInvoker(AboutModal);
|
||||
|
||||
@state()
|
||||
protected entries: AboutEntry[] | null = null;
|
||||
|
||||
public refresh() {
|
||||
return fetchAboutDetails().then((entries) => {
|
||||
this.entries = entries;
|
||||
});
|
||||
}
|
||||
|
||||
#contentRef = createRef<HTMLDivElement>();
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
#backdropListener = (event: PointerEvent) => {
|
||||
// We only want to close the modal when the backdrop is clicked, not when it's children are clicked.
|
||||
//#region Renderers
|
||||
|
||||
if (this.#contentRef.value?.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
this.close();
|
||||
};
|
||||
protected override renderCloseButton() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override renderModal() {
|
||||
protected override render() {
|
||||
let product = this.brandingTitle;
|
||||
|
||||
if (this.licenseSummary?.status !== LicenseSummaryStatusEnum.Unlicensed) {
|
||||
product += ` ${msg("Enterprise")}`;
|
||||
}
|
||||
return html`<div class="pf-c-backdrop" @click=${this.#backdropListener}>
|
||||
<div class="pf-l-bullseye">
|
||||
<div
|
||||
${ref(this.#contentRef)}
|
||||
class="pf-c-about-modal-box"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-title"
|
||||
|
||||
return html`<div
|
||||
${ref(this.scrollContainerRef)}
|
||||
class="pf-c-about-modal-box"
|
||||
style=${styleMap({
|
||||
"--pf-c-about-modal-box__hero--sm--BackgroundImage": `url(${DEFAULT_BRAND_IMAGE})`,
|
||||
})}
|
||||
aria-labelledby="modal-title"
|
||||
>
|
||||
<div class="pf-c-about-modal-box__close">
|
||||
<button
|
||||
class="pf-c-button pf-m-plain"
|
||||
type="button"
|
||||
@click=${this.closeListener}
|
||||
aria-label=${msg("Close dialog")}
|
||||
>
|
||||
<div class="pf-c-about-modal-box__brand">
|
||||
${ThemedImage({
|
||||
src: this.brandingFavicon,
|
||||
alt: msg("authentik Logo"),
|
||||
className: "pf-c-about-modal-box__brand-image",
|
||||
theme: this.activeTheme,
|
||||
themedUrls: this.brandingFaviconThemedUrls,
|
||||
})}
|
||||
</div>
|
||||
<div class="pf-c-about-modal-box__close">
|
||||
<button class="pf-c-button pf-m-plain" type="button" @click=${this.close}>
|
||||
<i class="fas fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="pf-c-about-modal-box__header">
|
||||
<h1 class="pf-c-title pf-m-4xl" id="modal-title">${product}</h1>
|
||||
</div>
|
||||
<div class="pf-c-about-modal-box__hero"></div>
|
||||
<div class="pf-c-about-modal-box__content">
|
||||
<div class="pf-c-about-modal-box__body">
|
||||
<div class="pf-c-content">
|
||||
${until(
|
||||
this.getAboutEntries().then((entries) => {
|
||||
return html`<dl>
|
||||
${entries.map(([label, value]) => {
|
||||
return html`<dt>${label}</dt>
|
||||
<dd>${value}</dd>`;
|
||||
})}
|
||||
</dl>`;
|
||||
}),
|
||||
html`<ak-empty-state loading></ak-empty-state>`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p class="pf-c-about-modal-box__strapline"></p>
|
||||
<i class="fas fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="pf-c-about-modal-box__brand">
|
||||
${ThemedImage({
|
||||
src: this.brandingFavicon,
|
||||
alt: msg("authentik Logo"),
|
||||
className: "pf-c-about-modal-box__brand-image",
|
||||
theme: this.activeTheme,
|
||||
themedUrls: this.brandingFaviconThemedUrls,
|
||||
})}
|
||||
</div>
|
||||
<div class="pf-c-about-modal-box__header">
|
||||
<h1 class="pf-c-title pf-m-4xl" id="modal-title">${product}</h1>
|
||||
</div>
|
||||
<div class="pf-c-about-modal-box__hero"></div>
|
||||
<div class="pf-c-about-modal-box__content">
|
||||
<div class="pf-c-about-modal-box__body">
|
||||
<div class="pf-c-content">
|
||||
${this.entries
|
||||
? html`<dl>
|
||||
${this.entries.map(([label, value]) => {
|
||||
return html`<dt>${label}</dt>
|
||||
<dd>${value}</dd>`;
|
||||
})}
|
||||
</dl>`
|
||||
: html`<ak-empty-state loading></ak-empty-state>`}
|
||||
</div>
|
||||
</div>
|
||||
<p class="pf-c-about-modal-box__strapline"></p>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import "#admin/AdminInterface/AboutModal";
|
||||
import "#elements/banner/EnterpriseStatusBanner";
|
||||
import "#elements/banner/VersionBanner";
|
||||
import "#elements/messages/MessageContainer";
|
||||
@@ -30,7 +29,6 @@ import {
|
||||
renderNotificationDrawerPanel,
|
||||
} from "#elements/notifications/utils";
|
||||
|
||||
import type { AboutModal } from "#admin/AdminInterface/AboutModal";
|
||||
import Styles from "#admin/AdminInterface/index.entrypoint.css";
|
||||
import { ROUTES } from "#admin/Routes";
|
||||
|
||||
@@ -38,7 +36,7 @@ import { CapabilitiesEnum } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, html, nothing, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, eventOptions, property, query, state } from "lit/decorators.js";
|
||||
import { customElement, eventOptions, property, state } from "lit/decorators.js";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
@@ -62,9 +60,6 @@ export class AdminInterface extends WithCapabilitiesConfig(
|
||||
|
||||
//#region Properties
|
||||
|
||||
@query("ak-about-modal")
|
||||
public aboutModal?: AboutModal;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "sidebar" })
|
||||
public sidebarOpen = false;
|
||||
|
||||
@@ -201,7 +196,6 @@ export class AdminInterface extends WithCapabilitiesConfig(
|
||||
</div>
|
||||
</div>
|
||||
${renderNotificationDrawerPanel(this.drawer)}
|
||||
<ak-about-modal></ak-about-modal>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -40,6 +40,9 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
||||
export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Application, string>) {
|
||||
#api = new CoreApi(DEFAULT_CONFIG);
|
||||
|
||||
protected entitySingular = msg("Application");
|
||||
protected entityPlural = msg("Applications");
|
||||
|
||||
protected override async loadInstance(pk: string): Promise<Application> {
|
||||
const app = await this.#api.coreApplicationsRetrieve({
|
||||
slug: pk,
|
||||
|
||||
@@ -5,24 +5,29 @@ import "#elements/ak-mdx/ak-mdx";
|
||||
import "#elements/buttons/SpinnerButton/ak-spinner-button";
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/modals/ak-modal";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
import "./ApplicationWizardHint.js";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { WithBrandConfig } from "#elements/mixins/branding";
|
||||
import { asInvoker, renderModal } from "#elements/modals/utils";
|
||||
import { getURLParam } from "#elements/router/RouteMatch";
|
||||
import { PaginatedResponse, TableColumn } from "#elements/table/Table";
|
||||
import { TablePage } from "#elements/table/TablePage";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
import { ifPresent } from "#elements/utils/attributes";
|
||||
|
||||
import { ApplicationForm } from "#admin/applications/ApplicationForm";
|
||||
import { AkApplicationWizard } from "#admin/applications/wizard/ak-application-wizard";
|
||||
|
||||
import { Application, CoreApi, PoliciesApi } from "@goauthentik/api";
|
||||
|
||||
import MDApplication from "~docs/add-secure-apps/applications/index.md";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { css, CSSResult, html, nothing, TemplateResult } from "lit";
|
||||
import { css, CSSResult, html, nothing, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
@@ -69,6 +74,26 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
||||
|
||||
static styles: CSSResult[] = [...TablePage.styles, PFCard, applicationListStyle];
|
||||
|
||||
public override firstUpdated(changed: PropertyValues<this>): void {
|
||||
super.firstUpdated(changed);
|
||||
|
||||
if (getURLParam("createWizard", false)) {
|
||||
this.#openCreateWizard();
|
||||
} else if (getURLParam("createForm", false)) {
|
||||
this.#openCreateModal();
|
||||
}
|
||||
}
|
||||
|
||||
#openEditModal(event: Event) {
|
||||
const pk = (event.currentTarget as HTMLElement).dataset.pk;
|
||||
|
||||
renderModal(html`<ak-application-form .instancePk=${pk}></ak-application-form>`);
|
||||
}
|
||||
|
||||
#openCreateWizard = AkApplicationWizard.open;
|
||||
|
||||
#openCreateModal = asInvoker(ApplicationForm);
|
||||
|
||||
protected columns: TableColumn[] = [
|
||||
["", undefined, msg("Application Icon")],
|
||||
[msg("Name"), "name"],
|
||||
@@ -133,21 +158,16 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
||||
: html`-`,
|
||||
html`${item.providerObj?.verboseName || msg("-")}`,
|
||||
html`<div>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Update")}</span>
|
||||
<span slot="header">${msg("Update Application")}</span>
|
||||
<ak-application-form slot="form" .instancePk=${item.slug}>
|
||||
</ak-application-form>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-plain"
|
||||
aria-label=${msg(str`Edit "${item.name}"`)}
|
||||
>
|
||||
<pf-tooltip position="top" content=${msg("Edit")}>
|
||||
<i class="fas fa-edit" aria-hidden="true"></i>
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
<button
|
||||
class="pf-c-button pf-m-plain"
|
||||
aria-label=${msg(str`Edit "${item.name}"`)}
|
||||
data-pk=${item.slug}
|
||||
@click=${this.#openEditModal}
|
||||
>
|
||||
<pf-tooltip position="top" content=${msg("Edit")}>
|
||||
<i class="fas fa-edit" aria-hidden="true"></i>
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
${item.launchUrl
|
||||
? html`<a
|
||||
href=${item.launchUrl}
|
||||
@@ -165,21 +185,12 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
||||
}
|
||||
|
||||
renderObjectCreate(): TemplateResult {
|
||||
return html` <ak-application-wizard .open=${getURLParam("createWizard", false)}>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-primary"
|
||||
data-ouia-component-id="start-application-wizard"
|
||||
>
|
||||
${msg("Create with Provider")}
|
||||
</button>
|
||||
</ak-application-wizard>
|
||||
<ak-forms-modal .open=${getURLParam("createForm", false)}>
|
||||
<span slot="submit">${msg("Create")}</span>
|
||||
<span slot="header">${msg("Create Application")}</span>
|
||||
<ak-application-form slot="form"> </ak-application-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
|
||||
</ak-forms-modal>`;
|
||||
return html`<button class="pf-c-button pf-m-primary" @click=${this.#openCreateWizard}>
|
||||
${msg("Create with Provider")}
|
||||
</button>
|
||||
<button class="pf-c-button pf-m-primary" @click=${this.#openCreateModal}>
|
||||
${msg("Create")}
|
||||
</button>`;
|
||||
}
|
||||
|
||||
renderToolbar(): TemplateResult {
|
||||
|
||||
@@ -50,6 +50,10 @@ export const providerTypePriority: ProviderModelNameEnum[] = [
|
||||
|
||||
@customElement("ak-application-wizard-main")
|
||||
export class AkApplicationWizardMain extends AKElement {
|
||||
protected createRenderRoot(): HTMLElement | DocumentFragment {
|
||||
return this;
|
||||
}
|
||||
|
||||
@state()
|
||||
wizard: ApplicationWizardState = freshWizardState();
|
||||
|
||||
|
||||
@@ -1,28 +1,39 @@
|
||||
import "./ak-application-wizard-main.js";
|
||||
|
||||
import { ModalButton } from "#elements/buttons/ModalButton";
|
||||
import { bound } from "#elements/decorators/bound";
|
||||
import { AKModal } from "#elements/modals/ak-modal";
|
||||
import { asInvoker } from "#elements/modals/utils";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { WizardCloseEvent } from "#components/ak-wizard/events";
|
||||
|
||||
import { html } from "lit";
|
||||
import { css, CSSResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-application-wizard")
|
||||
export class AkApplicationWizard extends ModalButton {
|
||||
export class AkApplicationWizard extends AKModal {
|
||||
public static override styles: CSSResult[] = [
|
||||
...super.styles,
|
||||
css`
|
||||
[part="main"] {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public static open = asInvoker(AkApplicationWizard);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener(WizardCloseEvent.eventName, this.onCloseEvent);
|
||||
|
||||
this.addEventListener(WizardCloseEvent.eventName, this.closeListener);
|
||||
}
|
||||
|
||||
@bound
|
||||
onCloseEvent(ev: WizardCloseEvent) {
|
||||
ev.stopPropagation();
|
||||
this.open = false;
|
||||
protected renderCloseButton(): SlottedTemplateResult {
|
||||
return null;
|
||||
}
|
||||
|
||||
renderModalInner() {
|
||||
return html` <ak-application-wizard-main> </ak-application-wizard-main>`;
|
||||
render() {
|
||||
return html`<ak-application-wizard-main part="main"></ak-application-wizard-main>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -371,21 +371,4 @@ export function applyBackgroundImageProperty(
|
||||
target.style.setProperty(AKBackgroundImageProperty, `url("${nextURL.href}")`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root interface element of the page.
|
||||
*
|
||||
* @deprecated Use context controllers to access the interface root instead.
|
||||
*/
|
||||
export function rootInterface<T extends HTMLElement = HTMLElement>(): T {
|
||||
const element = document.body.querySelector<T>("[data-test-id=interface-root]");
|
||||
|
||||
if (!element) {
|
||||
throw new Error(
|
||||
`Could not find root interface element. Was this element added before the parent interface element?`,
|
||||
);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import "#elements/buttons/Dropdown";
|
||||
|
||||
import { StripHTMLTrustPolicy } from "#common/purify";
|
||||
import { rootInterface } from "#common/theme";
|
||||
|
||||
import { FormAssociated, FormAssociatedElement } from "#elements/forms/form-associated-element";
|
||||
import { PaginatedResponse } from "#elements/table/Table";
|
||||
import { ifPresent } from "#elements/utils/attributes";
|
||||
import { resolveInterface } from "#elements/utils/render-roots";
|
||||
|
||||
import Styles from "#components/ak-search-ql/styles.css";
|
||||
|
||||
@@ -143,8 +143,7 @@ export class QLSearch extends FormAssociatedElement<string> implements FormAssoc
|
||||
public override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.#scrollContainer =
|
||||
rootInterface<LitElement>().renderRoot.querySelector("#main-content");
|
||||
this.#scrollContainer = resolveInterface().renderRoot.querySelector("#main-content");
|
||||
|
||||
this.#scrollContainer?.addEventListener("scroll", this.#updateDropdownPosition, {
|
||||
passive: true,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { AKElement } from "#elements/Base";
|
||||
import { bound } from "#elements/decorators/bound";
|
||||
|
||||
import { Context, ContextProvider } from "@lit/context";
|
||||
import { html, nothing } from "lit";
|
||||
import { css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
/**
|
||||
@@ -25,6 +25,14 @@ import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-wizard-steps")
|
||||
export class WizardStepsManager extends AKElement {
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@property({ type: String, attribute: true })
|
||||
public currentStep?: string;
|
||||
|
||||
@@ -97,6 +105,10 @@ export class WizardStepsManager extends AKElement {
|
||||
|
||||
@bound
|
||||
onSlotchange(ev: Event) {
|
||||
console.debug(
|
||||
"Slot change detected in WizardStepsManager; recalculating slots and step labels.",
|
||||
ev,
|
||||
);
|
||||
ev.stopPropagation();
|
||||
this.findSlots();
|
||||
this.findSlot(this.currentStep);
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { globalAK } from "#common/global";
|
||||
import { createCSSResult, createStyleSheetUnsafe, StyleRoot } from "#common/stylesheets";
|
||||
import {
|
||||
createCSSResult,
|
||||
createStyleSheetUnsafe,
|
||||
setAdoptedStyleSheets,
|
||||
StyleRoot,
|
||||
} from "#common/stylesheets";
|
||||
import { applyUITheme, ResolvedUITheme, resolveUITheme, ThemeChangeEvent } from "#common/theme";
|
||||
|
||||
import AKBase from "#styles/authentik/base.css" with { type: "bundled-text" };
|
||||
@@ -34,7 +39,22 @@ export class AKElement extends LitElement implements AKElementProps {
|
||||
|
||||
public static styles?: Array<CSSResult | CSSModule>;
|
||||
|
||||
/**
|
||||
* Host styles are styles that are applied to the element's render root,
|
||||
* but are not scoped to the element itself.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This is useful if the element is a wrapper around a third-party component
|
||||
* that requires styles to be applied to the host, such as Patternfly's modals.
|
||||
*/
|
||||
public static hostStyles?: Array<CSSResult | CSSModule>;
|
||||
|
||||
private static hostStyleSheets: CSSStyleSheet[] | null = null;
|
||||
|
||||
protected static override finalizeStyles(styles: CSSResultGroup = []): CSSResultOrNative[] {
|
||||
this.hostStyleSheets = this.hostStyles ? this.hostStyles.map(createStyleSheetUnsafe) : null;
|
||||
|
||||
const elementStyles = [
|
||||
$PFBase,
|
||||
// Route around TSC`s known-to-fail typechecking of `.flat(Infinity)`. Removes types.
|
||||
@@ -102,10 +122,35 @@ export class AKElement extends LitElement implements AKElementProps {
|
||||
|
||||
this.activeTheme = preferredColorScheme;
|
||||
}
|
||||
|
||||
const rootNode = this.getRootNode();
|
||||
|
||||
if (rootNode instanceof ShadowRoot) {
|
||||
const { hostStyleSheets } = this.constructor as typeof AKElement;
|
||||
|
||||
if (hostStyleSheets) {
|
||||
setAdoptedStyleSheets(rootNode, (currentStyleSheets) => {
|
||||
return [...currentStyleSheets, ...hostStyleSheets];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override disconnectedCallback(): void {
|
||||
this.#themeAbortController?.abort();
|
||||
|
||||
const rootNode = this.getRootNode();
|
||||
|
||||
if (rootNode instanceof ShadowRoot) {
|
||||
const { hostStyleSheets } = this.constructor as typeof AKElement;
|
||||
|
||||
if (hostStyleSheets) {
|
||||
setAdoptedStyleSheets(rootNode, (currentStyleSheets) => {
|
||||
return currentStyleSheets.filter((sheet) => !hostStyleSheets.includes(sheet));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export abstract class Interface extends AKElement {
|
||||
this.addController(new BrandingContextController(this, brand), BrandingContext);
|
||||
this.addController(new ModalOrchestrationController());
|
||||
|
||||
this.dataset.testId = "interface-root";
|
||||
this.id = "interface-root";
|
||||
}
|
||||
|
||||
public override addController(
|
||||
|
||||
@@ -239,11 +239,11 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
@property({ type: String })
|
||||
public autocomplete?: Exclude<AutoFillBase, "">;
|
||||
|
||||
@property({ type: String })
|
||||
public headline?: string;
|
||||
@property({ type: String, useDefault: true })
|
||||
public headline?: string | null = null;
|
||||
|
||||
@property({ type: String, attribute: "action-label" })
|
||||
public actionLabel?: string;
|
||||
@property({ type: String, attribute: "action-label", useDefault: true })
|
||||
public actionLabel: string | null = null;
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -254,6 +254,9 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
@state()
|
||||
protected nonFieldErrors: readonly string[] | null = null;
|
||||
|
||||
protected entitySingular?: string;
|
||||
protected entityPlural?: string;
|
||||
|
||||
static styles: CSSResult[] = [
|
||||
PFCard,
|
||||
PFButton,
|
||||
@@ -317,6 +320,20 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* An overridable method for formatting the form headline.
|
||||
*/
|
||||
protected formatHeadline(headline = this.headline): string {
|
||||
return headline || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* An overridable method for formatting the submit button label.
|
||||
*/
|
||||
protected formatSubmitLabel(actionLabel = this.actionLabel): string {
|
||||
return actionLabel || msg("Submit");
|
||||
}
|
||||
|
||||
//#region Public methods
|
||||
|
||||
public reset(): void {
|
||||
@@ -512,20 +529,22 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
}
|
||||
|
||||
/**
|
||||
* An overridable method for rendering the form heading.
|
||||
* An overridable method for rendering the form header.
|
||||
*
|
||||
* @remarks
|
||||
* If this form is slotted, such as in a modal, this method will not render anything,
|
||||
* allowing the slot parent to provide the heading in a more visually appropriate manner.
|
||||
* allowing the slot parent to provide the header in a more visually appropriate manner.
|
||||
*/
|
||||
protected renderHeading(): SlottedTemplateResult {
|
||||
return guard([this.assignedSlot, this.headline], () => {
|
||||
if (this.assignedSlot) {
|
||||
public renderHeader(force?: boolean): SlottedTemplateResult {
|
||||
const { assignedSlot, headline } = this;
|
||||
|
||||
return guard([force, assignedSlot, headline], () => {
|
||||
if (!force && assignedSlot && !assignedSlot.name) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<header>
|
||||
<h1 class="pf-c-title pf-m-2xl">${this.headline}</h1>
|
||||
<h1 class="pf-c-title pf-m-2xl">${this.formatHeadline(headline)}</h1>
|
||||
</header>`;
|
||||
});
|
||||
}
|
||||
@@ -537,23 +556,33 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
* If this form is slotted, such as in a modal, this method will not render anything,
|
||||
* allowing the slot parent to provide the actions in a more visually appropriate manner.
|
||||
*/
|
||||
protected renderActions(): SlottedTemplateResult {
|
||||
return guard([this.assignedSlot], () => {
|
||||
if (this.assignedSlot) {
|
||||
public renderActions(force?: boolean): SlottedTemplateResult {
|
||||
const { assignedSlot, actionLabel } = this;
|
||||
|
||||
return guard([force, assignedSlot, actionLabel], () => {
|
||||
if (!force && assignedSlot && !assignedSlot.name) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<fieldset part="form-actions" class="pf-c-card__footer">
|
||||
<legend class="sr-only">${msg("Form actions")}</legend>
|
||||
<button
|
||||
type="submit"
|
||||
form="form"
|
||||
type="button"
|
||||
class="pf-c-button pf-m-primary"
|
||||
@click=${(event: Event) => {
|
||||
this.submit(
|
||||
new SubmitEvent("submit", {
|
||||
submitter: event.currentTarget as HTMLButtonElement,
|
||||
cancelable: true,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
part="submit-button"
|
||||
formmethod="dialog"
|
||||
aria-description=${msg("Submit action")}
|
||||
>
|
||||
${this.actionLabel || msg("Submit")}
|
||||
${this.formatSubmitLabel(actionLabel)}
|
||||
</button>
|
||||
</fieldset>`;
|
||||
});
|
||||
@@ -563,8 +592,12 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
* An overridable method for rendering the form when it is visible.
|
||||
*/
|
||||
protected renderVisible(): SlottedTemplateResult {
|
||||
return html`${this.renderHeading()}${this.renderNonFieldErrors()}
|
||||
${this.renderFormWrapper()}${this.renderActions()}`;
|
||||
return [
|
||||
this.renderHeader(),
|
||||
this.renderNonFieldErrors(),
|
||||
this.renderFormWrapper(),
|
||||
this.renderActions(),
|
||||
];
|
||||
}
|
||||
|
||||
protected override render(): SlottedTemplateResult {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { ConsoleLogger } from "#logger/browser";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
|
||||
@@ -109,6 +110,22 @@ export abstract class ModelForm<
|
||||
});
|
||||
};
|
||||
|
||||
protected override formatSubmitLabel(): string {
|
||||
return this.#instancePk ? msg("Update") : msg("Create");
|
||||
}
|
||||
|
||||
protected override formatHeadline(): string {
|
||||
const verb = this.#instancePk ? msg("Edit") : msg("New");
|
||||
const noun = this.entitySingular;
|
||||
|
||||
if (!noun) return verb;
|
||||
|
||||
return msg(str`${verb} ${noun}`, {
|
||||
id: "model-form.headline",
|
||||
desc: "The headline for a form that creates or updates a model instance.",
|
||||
});
|
||||
}
|
||||
|
||||
public override reset(): void {
|
||||
super.reset();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { randomId } from "#elements/utils/randomId";
|
||||
import { resolveInterface } from "#elements/utils/render-roots";
|
||||
|
||||
import { autoUpdate, computePosition, flip, hide } from "@floating-ui/dom";
|
||||
|
||||
@@ -68,13 +69,17 @@ export class Portal extends LitElement implements IPortal {
|
||||
this.setAttribute("data-ouia-component-type", "ak-portal");
|
||||
this.setAttribute("data-ouia-component-id", this.getAttribute("id") || randomId());
|
||||
|
||||
this.dropdownContainer = document.createElement("div");
|
||||
this.dropdownContainer = this.ownerDocument.createElement("div");
|
||||
this.dropdownContainer.dataset.managedBy = "ak-portal";
|
||||
if (this.name) {
|
||||
this.dropdownContainer.dataset.managedFor = this.name;
|
||||
}
|
||||
|
||||
document.body.append(this.dropdownContainer);
|
||||
const interfaceElement = resolveInterface();
|
||||
const container =
|
||||
interfaceElement.renderRoot.querySelector("dialog") || this.ownerDocument.body;
|
||||
|
||||
container.append(this.dropdownContainer);
|
||||
|
||||
if (!this.anchor) {
|
||||
throw new Error("Tether entrance initialized incorrectly: missing anchor");
|
||||
|
||||
@@ -229,11 +229,13 @@ export class SearchSelectView extends AKElement implements ISearchSelectView {
|
||||
this.setAttribute("data-ouia-component-safe", "true");
|
||||
}
|
||||
|
||||
public override firstUpdated() {
|
||||
public override firstUpdated(changed: PropertyValues<this>) {
|
||||
super.firstUpdated(changed);
|
||||
|
||||
// Route around Lit's scheduling algorithm complaining about re-renders
|
||||
window.setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
this.inputRefIsAvailable = Boolean(this.#inputRef?.value);
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
|
||||
496
web/src/elements/modals/ak-modal.ts
Normal file
496
web/src/elements/modals/ak-modal.ts
Normal file
@@ -0,0 +1,496 @@
|
||||
import { PFSize } from "#common/enums";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { AKFormSubmittedEvent } from "#elements/forms/events";
|
||||
import { Form } from "#elements/forms/Form";
|
||||
import Styles from "#elements/modals/styles.css";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { ConsoleLogger, Logger } from "#logger/browser";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, html, PropertyValues } from "lit";
|
||||
import { createRef } from "lit-html/directives/ref.js";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { guard } from "lit/directives/guard.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
|
||||
@customElement("ak-modal")
|
||||
export class AKModal extends AKElement {
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
...AKElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether the modal should open the parent dialog element when it is connected to the DOM.
|
||||
*/
|
||||
static openOnConnect = true;
|
||||
|
||||
public static styles: CSSResult[] = [
|
||||
PFButton,
|
||||
PFForm,
|
||||
PFTitle,
|
||||
PFFormControl,
|
||||
PFPage,
|
||||
PFCard,
|
||||
PFContent,
|
||||
Styles,
|
||||
];
|
||||
|
||||
#hostResizeObserver: ResizeObserver;
|
||||
|
||||
//#region Protected Properties
|
||||
|
||||
protected logger: Logger;
|
||||
|
||||
/**
|
||||
* An optional Lit ref which can automatically synchronize the modal's height with the element's height.
|
||||
*/
|
||||
protected scrollContainerRef = createRef<HTMLElement>();
|
||||
|
||||
declare parentElement: HTMLDialogElement | null;
|
||||
|
||||
//#region Public Properties
|
||||
|
||||
@property({ type: String, useDefault: true })
|
||||
public headline: string | null = null;
|
||||
|
||||
/**
|
||||
* Whether the parent dialog element is currently open.
|
||||
*/
|
||||
public get open(): boolean {
|
||||
return this.parentElement?.open ?? false;
|
||||
}
|
||||
|
||||
@property({ type: Boolean, attribute: false, reflect: false })
|
||||
public set open(value: boolean) {
|
||||
if (value) {
|
||||
this.show();
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
@property({ type: String })
|
||||
public size: PFSize = PFSize.Large;
|
||||
|
||||
protected defaultSlot: HTMLSlotElement;
|
||||
|
||||
@state()
|
||||
protected form: Form | null = null;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Public methods
|
||||
|
||||
/**
|
||||
* Show the modal, rendering its contents.
|
||||
*/
|
||||
public show() {
|
||||
const dialogElement = this.parentElement;
|
||||
|
||||
if (!dialogElement) {
|
||||
this.logger.debug("No parentElement, cannot show modal", this);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
dialogElement.addEventListener("transitionend", this.fadeInListener, {
|
||||
once: true,
|
||||
passive: true,
|
||||
});
|
||||
|
||||
dialogElement.showModal();
|
||||
|
||||
dialogElement.classList.add("fade-in");
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the modal, fading it out and then removing it from the DOM,
|
||||
* optionally with a return value.
|
||||
*
|
||||
* @param returnValue The return value for the dialog, if any.
|
||||
*/
|
||||
public close(returnValue?: string) {
|
||||
const dialogElement = this.parentElement;
|
||||
|
||||
if (!dialogElement) {
|
||||
this.logger.debug("No parentElement, cannot close modal", this);
|
||||
return;
|
||||
}
|
||||
|
||||
dialogElement.addEventListener(
|
||||
"transitionend",
|
||||
(event) => this.delegateClose(event, returnValue),
|
||||
{ once: true, passive: true },
|
||||
);
|
||||
|
||||
dialogElement.classList.remove("fade-in", "fade-in-complete");
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Event listeners
|
||||
|
||||
/**
|
||||
* A stable reference to the dialog's open event listener.
|
||||
*
|
||||
* This is useful for simplifying adding and removing event listeners.
|
||||
*/
|
||||
protected showListener = () => {
|
||||
this.show();
|
||||
};
|
||||
|
||||
#heightSyncFrameID = -1;
|
||||
/**
|
||||
* A map of observed elements to their expected sizes,
|
||||
* used to prevent unnecessary height synchronizations.
|
||||
*/
|
||||
#expectedSizes = new WeakMap<Element, number>();
|
||||
|
||||
protected lastScrollHeight = 0;
|
||||
|
||||
protected synchronizeHeight: ResizeObserverCallback = ([entry]) => {
|
||||
this.#heightSyncFrameID = requestAnimationFrame(() => {
|
||||
const dialogElement = this.parentElement;
|
||||
|
||||
if (!dialogElement || !dialogElement.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expectedSize = this.#expectedSizes.get(entry.target);
|
||||
const blockSize = Math.ceil(entry.borderBoxSize[0].blockSize);
|
||||
const desiredSize = Math.max(entry.target.scrollHeight, blockSize);
|
||||
|
||||
if (desiredSize === expectedSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
dialogElement.style.height = desiredSize + "px";
|
||||
|
||||
this.#expectedSizes.set(entry.target, desiredSize);
|
||||
});
|
||||
};
|
||||
|
||||
#heightResetAnimationFrameID = -1;
|
||||
|
||||
protected resetHeight = () => {
|
||||
cancelAnimationFrame(this.#heightResetAnimationFrameID);
|
||||
cancelAnimationFrame(this.#heightSyncFrameID);
|
||||
|
||||
this.#heightResetAnimationFrameID = requestAnimationFrame(() => {
|
||||
if (this.parentElement) {
|
||||
this.parentElement.style.height = "";
|
||||
}
|
||||
|
||||
this.#heightResetAnimationFrameID = requestAnimationFrame(() => {
|
||||
const scrollContainer = this.scrollContainerRef.value || this;
|
||||
const scrollHeight = scrollContainer.scrollHeight;
|
||||
|
||||
this.lastScrollHeight = scrollHeight;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
protected fadeInListener = async (event: TransitionEvent) => {
|
||||
this.logger.debug("fade-in complete", event);
|
||||
|
||||
this.removeEventListener("transitionend", this.fadeInListener);
|
||||
|
||||
const dialogElement = this.parentElement;
|
||||
|
||||
if (!dialogElement) {
|
||||
this.logger.debug("Skipping height synchronization, no dialog element", this);
|
||||
return;
|
||||
}
|
||||
|
||||
dialogElement.classList.add("fade-in-complete");
|
||||
|
||||
dialogElement.style.height = dialogElement.clientHeight + "px";
|
||||
|
||||
const scrollContainer = this.scrollContainerRef.value || this;
|
||||
|
||||
this.#hostResizeObserver.observe(scrollContainer);
|
||||
|
||||
window.addEventListener("resize", this.resetHeight, {
|
||||
passive: true,
|
||||
});
|
||||
};
|
||||
|
||||
#closing = false;
|
||||
|
||||
/**
|
||||
* Delegate the close action to the parent dialog element,
|
||||
* ensuring that the correct event listeners are triggered and the modal is properly closed.
|
||||
*/
|
||||
protected delegateClose(event: TransitionEvent, returnValue?: string) {
|
||||
if (this.#closing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.debug("Closing", event);
|
||||
|
||||
this.#closing = true;
|
||||
|
||||
this.parentElement?.close(returnValue);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.#closing = false;
|
||||
this.#hostResizeObserver.unobserve(this);
|
||||
window.removeEventListener("resize", this.resetHeight);
|
||||
this.resetHeight();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A stable reference to the dialog's close event listener.
|
||||
*
|
||||
* This is useful for simplifying adding and removing event listeners.
|
||||
*/
|
||||
protected closeListener = () => {
|
||||
this.close();
|
||||
};
|
||||
|
||||
/**
|
||||
* A stable reference to the dialog's backdrop click event listener.
|
||||
*
|
||||
* @remarks
|
||||
* Note that if the browser supports {@linkcode HTMLDialogElement}'s `closeBy` property,
|
||||
* the backdrop click may trigger a "cancel" event instead of a "click" event.
|
||||
*/
|
||||
protected backdropClickListener = (event: Event) => {
|
||||
if (event.target === this.parentElement) {
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A stable reference to the dialog's cancel event listener.
|
||||
*
|
||||
* This is useful for simplifying adding and removing event listeners.
|
||||
*/
|
||||
protected cancelListener = (event: Event) => {
|
||||
if (!this.parentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
this.close();
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
/**
|
||||
* A bound render method that can be safely passed as a callback without losing the correct `this` context.
|
||||
*
|
||||
* @remarks
|
||||
* This allows the implementing class to use `render`, reducing the mental
|
||||
* overhead of which method should be used for rendering content.
|
||||
*/
|
||||
protected renderContent: () => unknown;
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
|
||||
this.logger = ConsoleLogger.prefix(this.tagName.toLowerCase());
|
||||
|
||||
this.renderContent = this.render.bind(this);
|
||||
this.render = this.renderInternal;
|
||||
|
||||
this.#hostResizeObserver = new ResizeObserver(this.synchronizeHeight);
|
||||
|
||||
this.defaultSlot = this.ownerDocument.createElement("slot");
|
||||
}
|
||||
|
||||
public override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
if (!this.parentElement) {
|
||||
this.logger.debug("Skipping connectedCallback, no parentElement", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(this.parentElement instanceof HTMLDialogElement)) {
|
||||
throw new TypeError(
|
||||
`authentik/modal: ${this.tagName.toLowerCase()} must be placed inside a <dialog> element.`,
|
||||
);
|
||||
}
|
||||
|
||||
const tagName = this.tagName.toLowerCase();
|
||||
|
||||
this.parentElement.dataset.akModal = tagName;
|
||||
this.parentElement.classList.add("ak-c-modal", this.size);
|
||||
// eslint-disable-next-line wc/no-self-class
|
||||
this.classList.add("ak-c-modal__content");
|
||||
|
||||
this.parentElement.addEventListener("cancel", this.cancelListener);
|
||||
this.parentElement.addEventListener("click", this.backdropClickListener, { passive: true });
|
||||
|
||||
this.addEventListener(AKFormSubmittedEvent.eventName, this.closeListener);
|
||||
|
||||
const { openOnConnect } = this.constructor as typeof AKModal;
|
||||
|
||||
if (openOnConnect) {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
|
||||
public override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
|
||||
this.#hostResizeObserver.disconnect();
|
||||
window.removeEventListener("resize", this.resetHeight);
|
||||
}
|
||||
|
||||
public override updated(changedProperties: PropertyValues<this>): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
const assignedElements = this.defaultSlot.assignedElements({ flatten: true });
|
||||
|
||||
const form =
|
||||
assignedElements.find((element): element is Form => element instanceof Form) ?? null;
|
||||
|
||||
if (form && form !== this.form) {
|
||||
this.form = form;
|
||||
this.form.viewportCheck = false;
|
||||
}
|
||||
|
||||
const dialogElement = this.parentElement;
|
||||
|
||||
if (dialogElement && dialogElement.open) {
|
||||
requestAnimationFrame(() => {
|
||||
const scrollContainer = this.scrollContainerRef.value || this;
|
||||
const scrollHeight = scrollContainer.scrollHeight;
|
||||
|
||||
if (scrollHeight !== this.lastScrollHeight) {
|
||||
this.resetHeight();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Render
|
||||
|
||||
/**
|
||||
* An overridable method that determines whether the modal content should be rendered.
|
||||
*
|
||||
* By default, the modal content is only rendered when the modal is open,
|
||||
* to avoid unnecessary rendering and potential issues with elements that
|
||||
* require being in the DOM to function properly (e.g., autofocus).
|
||||
*/
|
||||
protected shouldRenderModalContent(): boolean {
|
||||
return this.open;
|
||||
}
|
||||
|
||||
/**
|
||||
* The internal render method, including the close button and header, which are common to all modals.
|
||||
*/
|
||||
protected renderInternal() {
|
||||
if (!this.shouldRenderModalContent()) {
|
||||
return super.render();
|
||||
}
|
||||
|
||||
return [
|
||||
this.renderCloseButton(),
|
||||
this.renderHeader(),
|
||||
this.renderContent(),
|
||||
this.renderActions(),
|
||||
];
|
||||
}
|
||||
|
||||
protected renderCloseButton(): SlottedTemplateResult {
|
||||
return html`<button
|
||||
@click=${this.closeListener}
|
||||
class="pf-c-button pf-m-plain"
|
||||
type="button"
|
||||
aria-label=${msg("Close dialog")}
|
||||
>
|
||||
<i class="fas fa-times" aria-hidden="true"></i>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the modal header.
|
||||
*
|
||||
* This method may be overridden to customize the modal header.
|
||||
*
|
||||
* @protected
|
||||
* @abstract
|
||||
*/
|
||||
protected renderHeader(): SlottedTemplateResult {
|
||||
const { headline, form } = this;
|
||||
const hasHeaderSlot = this.hasSlotted("header");
|
||||
|
||||
return guard([headline, hasHeaderSlot, form], () => {
|
||||
if (!headline && !hasHeaderSlot && !form) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return html`<header class="ak-c-modal__header">
|
||||
<div class="ak-c-modal__title">
|
||||
<h1 class="ak-c-modal__title-text" id="modal-title">
|
||||
${this.headline}
|
||||
<slot name="header"></slot>
|
||||
${form ? form.renderHeader(true) : null}
|
||||
</h1>
|
||||
</div>
|
||||
</header>`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the modal actions.
|
||||
*
|
||||
* This method may be overridden to customize the modal actions.
|
||||
*
|
||||
* @protected
|
||||
* @abstract
|
||||
*/
|
||||
protected renderActions(): SlottedTemplateResult {
|
||||
const { form } = this;
|
||||
const hasActionsSlot = this.hasSlotted("actions");
|
||||
return guard([hasActionsSlot, form], () => {
|
||||
if (!hasActionsSlot && !form) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return html`<footer class="ak-c-modal__footer">
|
||||
<slot name="actions"></slot>
|
||||
${form ? form.renderActions(true) : null}
|
||||
</footer>`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the modal content.
|
||||
*
|
||||
* Note that this method is only called when the modal is open.
|
||||
*
|
||||
* @protected
|
||||
* @abstract
|
||||
*/
|
||||
protected render(): unknown {
|
||||
return html`<div class="ak-c-modal__body">${this.defaultSlot}</div>`;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-modal": AKModal;
|
||||
}
|
||||
}
|
||||
144
web/src/elements/modals/styles.css
Normal file
144
web/src/elements/modals/styles.css
Normal file
@@ -0,0 +1,144 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* #region Buttons */
|
||||
|
||||
:host > .pf-c-button {
|
||||
position: absolute;
|
||||
top: var(--ak-c-modal--c-button--Top);
|
||||
right: var(--ak-c-modal--c-button--Right);
|
||||
}
|
||||
|
||||
:host > .pf-c-button + .ak-c-modal__header {
|
||||
margin-right: var(--ak-c-modal--c-button--sibling--MarginRight);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Header */
|
||||
|
||||
.ak-c-modal__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
padding-top: var(--ak-c-modal__header--PaddingTop);
|
||||
padding-bottom: var(--ak-c-modal__header--PaddingBottom);
|
||||
padding-right: var(--ak-c-modal__header--PaddingRight);
|
||||
padding-left: var(--ak-c-modal__header--PaddingLeft);
|
||||
}
|
||||
|
||||
.ak-c-modal__header.pf-m-help {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.ak-c-modal__header:last-child {
|
||||
padding-bottom: var(--ak-c-modal__header--last-child--PaddingBottom);
|
||||
}
|
||||
|
||||
.ak-c-modal__header + .ak-c-modal__body,
|
||||
.ak-c-modal__header + slot + .ak-c-modal__body {
|
||||
/* --ak-c-modal__body--PaddingTop: var(--ak-c-modal__header--body--PaddingTop); */
|
||||
--ak-c-modal__body--PaddingTop: 0;
|
||||
}
|
||||
|
||||
.ak-c-modal__header-main {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Title */
|
||||
|
||||
.ak-c-modal__title,
|
||||
.ak-c-modal__title-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ak-c-modal__title {
|
||||
flex: 0 0 auto;
|
||||
font-family: var(--ak-c-modal__title--FontFamily);
|
||||
font-size: var(--ak-c-modal__title--FontSize);
|
||||
line-height: var(--ak-c-modal__title--LineHeight);
|
||||
}
|
||||
|
||||
.ak-c-modal__title.pf-m-icon {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ak-c-modal__title-icon {
|
||||
margin-right: var(--ak-c-modal__title-icon--MarginRight);
|
||||
color: var(--ak-c-modal__title-icon--Color);
|
||||
}
|
||||
|
||||
.ak-c-modal__description {
|
||||
padding-top: var(--ak-c-modal__description--PaddingTop);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Body */
|
||||
|
||||
.ak-c-modal__body {
|
||||
flex: 1 1 auto;
|
||||
|
||||
max-height: var(--ak-c-modal__body--MaxHeight);
|
||||
min-height: var(--ak-c-modal__body--MinHeight);
|
||||
padding-top: var(--ak-c-modal__body--PaddingTop);
|
||||
padding-right: var(--ak-c-modal__body--PaddingRight);
|
||||
padding-left: var(--ak-c-modal__body--PaddingLeft);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
word-break: break-word;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
max-height: calc(
|
||||
75dvh - 2rem - var(--ak-c-modal__header--PaddingTop) -
|
||||
var(--ak-c-modal__footer--PaddingBottom)
|
||||
);
|
||||
}
|
||||
|
||||
.ak-c-modal__body:last-child {
|
||||
padding-bottom: var(--ak-c-modal__body--last-child--PaddingBottom);
|
||||
}
|
||||
|
||||
.ak-c-modal__footer {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
padding-top: var(--ak-c-modal__footer--PaddingTop);
|
||||
padding-right: var(--ak-c-modal__footer--PaddingRight);
|
||||
padding-bottom: var(--ak-c-modal__footer--PaddingBottom);
|
||||
padding-left: var(--ak-c-modal__footer--PaddingLeft);
|
||||
|
||||
gap: var(--pf-global--spacer--xs);
|
||||
|
||||
@media screen and (min-width: 576px) {
|
||||
gap: var(--pf-global--spacer--sm);
|
||||
}
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Footer */
|
||||
|
||||
fieldset.ak-c-modal__footer {
|
||||
--ak-legend-padding-inline-base: var(--pf-global--spacer--md);
|
||||
padding-block: calc(var(--ak-legend-padding-inline-base) / 2);
|
||||
border-inline: none;
|
||||
border-block-end: none;
|
||||
|
||||
--ak-c-modal__footer--c-button--sm--MarginRight: var(
|
||||
--ak-c-modal__footer--c-button--MarginRight
|
||||
);
|
||||
|
||||
& > ak-spinner-button:not(:last-child) {
|
||||
margin-right: var(--ak-c-modal__footer--c-button--MarginRight);
|
||||
}
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
127
web/src/elements/modals/utils.ts
Normal file
127
web/src/elements/modals/utils.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* @file Modal rendering utilities.
|
||||
*/
|
||||
|
||||
import "#elements/modals/ak-modal";
|
||||
|
||||
import { AKModal } from "#elements/modals/ak-modal";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
import { isAKElementConstructor } from "#elements/utils/unsafe";
|
||||
|
||||
import { html, render } from "lit";
|
||||
|
||||
/**
|
||||
* Resolves the container element for a dialog, given an optional parent element or selector.
|
||||
*
|
||||
* If the parent element is not provided or cannot be resolved, the document body is returned.
|
||||
*
|
||||
* @param ownerDocument The document to query for the parent element. Defaults to the global document.
|
||||
* @param parentElement An optional HTMLElement or selector string to use as the dialog container.
|
||||
*
|
||||
* @returns The resolved container HTMLElement for the dialog.
|
||||
*/
|
||||
export function resolveDialogContainer(
|
||||
ownerDocument: Document = document,
|
||||
parentElement?: HTMLElement | string | null,
|
||||
): HTMLElement {
|
||||
if (parentElement instanceof HTMLElement) {
|
||||
return parentElement;
|
||||
}
|
||||
|
||||
if (typeof parentElement === "string") {
|
||||
const resolvedElement = ownerDocument.querySelector(parentElement);
|
||||
|
||||
if (resolvedElement instanceof HTMLElement) {
|
||||
return resolvedElement;
|
||||
}
|
||||
}
|
||||
|
||||
return ownerDocument.body;
|
||||
}
|
||||
|
||||
export interface RenderDialogInit {
|
||||
ownerDocument?: Document;
|
||||
parentElement?: HTMLElement | string | null;
|
||||
closedBy?: ClosedBy;
|
||||
classList?: string[];
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a dialog with the given template.
|
||||
*
|
||||
* @param renderable The template to render inside the dialog.
|
||||
* @param init Initialization options for the dialog.
|
||||
*
|
||||
* @returns A promise that resolves when the dialog is closed.
|
||||
*/
|
||||
export function renderDialog(
|
||||
renderable: unknown,
|
||||
{
|
||||
ownerDocument = document,
|
||||
signal,
|
||||
parentElement = ownerDocument.getElementById("interface-root"),
|
||||
closedBy = "any",
|
||||
classList = [],
|
||||
}: RenderDialogInit = {},
|
||||
): Promise<void> {
|
||||
const dialog = ownerDocument.createElement("dialog");
|
||||
dialog.classList.add("ak-c-modal", ...classList);
|
||||
dialog.closedBy = closedBy;
|
||||
|
||||
const resolvers = Promise.withResolvers<void>();
|
||||
|
||||
const container = resolveDialogContainer(ownerDocument, parentElement);
|
||||
const shadowRoot = container.shadowRoot ?? container;
|
||||
|
||||
shadowRoot.appendChild(dialog);
|
||||
|
||||
const dispose = () => {
|
||||
dialog.close();
|
||||
dialog.remove();
|
||||
resolvers.resolve();
|
||||
};
|
||||
|
||||
dialog.addEventListener("close", dispose);
|
||||
|
||||
signal?.addEventListener("abort", dispose);
|
||||
|
||||
render(renderable, dialog);
|
||||
|
||||
return resolvers.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a modal dialog with the given template.
|
||||
*
|
||||
* @param renderable The template to render inside the modal.
|
||||
* @param init Initialization options for the modal.
|
||||
* @returns A promise that resolves when the modal is closed.
|
||||
*/
|
||||
export function renderModal(renderable: unknown, init?: RenderDialogInit): Promise<void> {
|
||||
return renderDialog(html`<ak-modal>${renderable}</ak-modal>`, init);
|
||||
}
|
||||
|
||||
export type ModalChildRenderer = () => SlottedTemplateResult;
|
||||
|
||||
/**
|
||||
* A utility function that takes either a {@linkcode CustomElementConstructor}
|
||||
* or a {@linkcode ModalChildRenderer} and returns a function that renders the corresponding modal dialog.
|
||||
*
|
||||
* @param input The input to render as a modal dialog, either a custom element constructor or a function that returns a template result.
|
||||
* @param init Initialization options for the modal dialog.
|
||||
*/
|
||||
export function asInvoker(
|
||||
input: CustomElementConstructor | (() => SlottedTemplateResult),
|
||||
init?: RenderDialogInit,
|
||||
) {
|
||||
return () => {
|
||||
const child = isAKElementConstructor(input) ? new input() : (input as ModalChildRenderer)();
|
||||
|
||||
if (child instanceof AKModal) {
|
||||
return renderDialog(child, init);
|
||||
}
|
||||
|
||||
renderModal(child, init);
|
||||
};
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import { globalAK } from "#common/global";
|
||||
import { rootInterface } from "#common/theme";
|
||||
import { DefaultBrand } from "#common/ui/config";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { WithLicenseSummary } from "#elements/mixins/license";
|
||||
import { WithVersion } from "#elements/mixins/version";
|
||||
|
||||
import type { AdminInterface } from "#admin/AdminInterface/index.entrypoint";
|
||||
import { AboutModal } from "#admin/AdminInterface/AboutModal";
|
||||
|
||||
import { LicenseSummaryStatusEnum } from "@goauthentik/api";
|
||||
|
||||
@@ -48,15 +47,13 @@ export class SidebarVersion extends WithLicenseSummary(WithVersion(AKElement)) {
|
||||
if (this.licenseSummary.status !== LicenseSummaryStatusEnum.Unlicensed) {
|
||||
product += ` ${msg("Enterprise")}`;
|
||||
}
|
||||
|
||||
return html`<button
|
||||
part="trigger"
|
||||
role="contentinfo"
|
||||
aria-label=${msg("Open about dialog")}
|
||||
class="pf-c-button pf-m-plain"
|
||||
@click=${() => {
|
||||
const int = rootInterface<AdminInterface>();
|
||||
int?.aboutModal?.show();
|
||||
}}
|
||||
@click=${AboutModal.open}
|
||||
>
|
||||
<p
|
||||
role="heading"
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export function getRootStyle(selector: string, element: HTMLElement = document.documentElement) {
|
||||
return getComputedStyle(element, null).getPropertyValue(selector);
|
||||
}
|
||||
|
||||
export default getRootStyle;
|
||||
18
web/src/elements/utils/render-roots.ts
Normal file
18
web/src/elements/utils/render-roots.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { LitElement } from "lit";
|
||||
|
||||
/**
|
||||
* Returns the root interface element of the page.
|
||||
*
|
||||
* @param ownerDocument The document to query for the interface element. Defaults to the global document.
|
||||
*/
|
||||
export function resolveInterface<T extends HTMLElement = LitElement>(ownerDocument = document): T {
|
||||
const element = ownerDocument.getElementById("interface-root") as T | null;
|
||||
|
||||
if (!element) {
|
||||
throw new Error(
|
||||
`Could not find root interface element. Was this element added before the parent interface element?`,
|
||||
);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
@@ -31,7 +31,9 @@ export function assertAKRegisteredElement(
|
||||
* Type predicate to determine if a given {@linkcode CustomElementConstructor}
|
||||
* extends {@linkcode AKElement}.
|
||||
*/
|
||||
export function isAKElementConstructor(input: CustomElementConstructor): input is typeof AKElement {
|
||||
export function isAKElementConstructor(
|
||||
input: CustomElementConstructor | (() => SlottedTemplateResult),
|
||||
): input is typeof AKElement {
|
||||
return Object.prototype.isPrototypeOf.call(AKElement, input);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@import "./base/common.css";
|
||||
@import "./base/scrollbars.css";
|
||||
@import "./base/modal.css";
|
||||
@import "./components/Alert/alert.css";
|
||||
@import "./components/Banner/banner.css";
|
||||
@import "./components/Label/label.css";
|
||||
@@ -10,7 +11,6 @@
|
||||
@import "./components/Drawer/drawer.css";
|
||||
@import "./components/Description/description.css";
|
||||
@import "./components/Card/card.css";
|
||||
@import "./components/Modal/modal.css";
|
||||
@import "./components/Content/content.css";
|
||||
@import "./components/Table/table.css";
|
||||
@import "./components/Form/form.css";
|
||||
|
||||
232
web/src/styles/authentik/base/modal.css
Normal file
232
web/src/styles/authentik/base/modal.css
Normal file
@@ -0,0 +1,232 @@
|
||||
/* #region Variables */
|
||||
|
||||
.ak-c-modal {
|
||||
--ak-c-modal--BackgroundColor: var(--pf-global--BackgroundColor--100);
|
||||
--ak-c-modal--BoxShadow: var(--pf-global--BoxShadow--xl);
|
||||
--ak-c-modal--ZIndex: var(--pf-global--ZIndex--xl);
|
||||
--ak-c-modal--Width: 100%;
|
||||
--ak-c-modal--MaxWidth: calc(100% - var(--pf-global--spacer--xl));
|
||||
--ak-c-modal--MarginBlockStart: 10dvh;
|
||||
--ak-c-modal--BorderColor: var(--pf-global--BackgroundColor--150);
|
||||
--ak-c-modal__backdrop--BackgroundColor: transparent;
|
||||
--ak-c-modal__backdrop--BackdropFilter: blur(0);
|
||||
--ak-c-modal__backdrop--active--BackgroundColor: hsla(0, 0%, 0%, 0.5);
|
||||
--ak-c-modal__backdrop--active--BackdropFilter: blur(2px);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Dialog */
|
||||
|
||||
.ak-c-modal {
|
||||
box-sizing: content-box;
|
||||
scrollbar-gutter: stable;
|
||||
background-color: var(--ak-c-modal--BackgroundColor);
|
||||
box-shadow: var(--ak-c-modal--BoxShadow);
|
||||
padding: var(--ak-c-modal--Padding);
|
||||
|
||||
z-index: var(--ak-c-modal--ZIndex);
|
||||
width: var(--ak-c-modal--Width);
|
||||
max-width: var(--ak-c-modal--MaxWidth);
|
||||
background-color: var(--ak-c-modal--BackgroundColor);
|
||||
box-shadow: var(--ak-c-modal--BoxShadow);
|
||||
|
||||
border-color: var(--ak-c-modal--BorderColor);
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Transitions */
|
||||
|
||||
.ak-c-modal {
|
||||
--ak-c-modal--TransitionDuration--fast: 100ms;
|
||||
--ak-c-modal--TransitionDuration--slow: 150ms;
|
||||
margin-block-start: var(--ak-c-modal--MarginBlockStart);
|
||||
|
||||
interpolate-size: allow-keywords;
|
||||
|
||||
transition:
|
||||
height var(--ak-c-modal--TransitionDuration--fast),
|
||||
opacity var(--ak-c-modal--TransitionDuration--slow),
|
||||
transform var(--ak-c-modal--TransitionDuration--slow),
|
||||
overlay var(--ak-c-modal--TransitionDuration--slow) allow-discrete,
|
||||
display var(--ak-c-modal--TransitionDuration--slow) allow-discrete;
|
||||
|
||||
transition-timing-function: ease-in-out;
|
||||
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
transform: translateY(1em);
|
||||
|
||||
&::backdrop {
|
||||
backdrop-filter: var(--ak-c-modal__backdrop--BackdropFilter);
|
||||
background-color: var(--ak-c-modal__backdrop--BackgroundColor);
|
||||
|
||||
transition:
|
||||
backdrop-filter,
|
||||
overlay allow-discrete,
|
||||
background-color;
|
||||
|
||||
transition-timing-function: ease-in-out;
|
||||
transition-duration: var(--ak-c-modal--TransitionDuration--slow);
|
||||
}
|
||||
|
||||
&.fade-in {
|
||||
--ak-c-modal__backdrop--BackdropFilter: var(--ak-c-modal__backdrop--active--BackdropFilter);
|
||||
--ak-c-modal__backdrop--BackgroundColor: var(
|
||||
--ak-c-modal__backdrop--active--BackgroundColor
|
||||
);
|
||||
|
||||
opacity: 1;
|
||||
height: calc-size(max-content, size);
|
||||
}
|
||||
|
||||
@media screen and (prefers-reduced-motion: reduce) {
|
||||
/* A small duration is still needed to smoothly handle display changes. */
|
||||
--ak-c-modal--TransitionDuration--fast: 25ms;
|
||||
--ak-c-modal--TransitionDuration--slow: 50ms;
|
||||
}
|
||||
}
|
||||
|
||||
.ak-c-modal[open] {
|
||||
transform: translateY(0);
|
||||
|
||||
@starting-style {
|
||||
transform: translateY(-1em);
|
||||
}
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Buttons */
|
||||
|
||||
.ak-c-modal {
|
||||
--ak-c-modal--c-button--Top: var(--pf-global--spacer--lg);
|
||||
--ak-c-modal--c-button--Right: var(--pf-global--spacer--md);
|
||||
--ak-c-modal--c-button--sibling--MarginRight: calc(
|
||||
var(--pf-global--spacer--xl) + var(--pf-global--spacer--sm)
|
||||
);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Header */
|
||||
|
||||
.ak-c-modal {
|
||||
--ak-c-modal__header--PaddingTop: var(--pf-global--spacer--lg);
|
||||
--ak-c-modal__header--PaddingBottom: var(--pf-global--spacer--lg);
|
||||
--ak-c-modal__header--PaddingRight: var(--pf-global--spacer--lg);
|
||||
--ak-c-modal__header--PaddingLeft: var(--pf-global--spacer--lg);
|
||||
--ak-c-modal__header--last-child--PaddingBottom: var(--pf-global--spacer--lg);
|
||||
--ak-c-modal__header--body--PaddingTop: var(--pf-global--spacer--md);
|
||||
|
||||
--ak-c-modal__title--LineHeight: var(--pf-global--LineHeight--sm);
|
||||
--ak-c-modal__title--FontFamily: var(--pf-global--FontFamily--heading--sans-serif);
|
||||
--ak-c-modal__title--FontSize: var(--pf-global--FontSize--2xl);
|
||||
--ak-c-modal__title-icon--MarginRight: var(--pf-global--spacer--sm);
|
||||
--ak-c-modal__title-icon--Color: var(--pf-global--Color--100);
|
||||
|
||||
--ak-c-modal__description--PaddingTop: var(--pf-global--spacer--xs);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Body */
|
||||
|
||||
.ak-c-modal {
|
||||
--ak-c-modal__body--MinHeight: calc(
|
||||
var(--pf-global--FontSize--md) * var(--pf-global--LineHeight--md)
|
||||
);
|
||||
--ak-c-modal__body--MaxHeight: calc(75dvh - var(--pf-global--spacer--2xl));
|
||||
|
||||
--ak-c-modal__body--PaddingTop: var(--pf-global--spacer--lg);
|
||||
--ak-c-modal__body--PaddingRight: var(--pf-global--spacer--lg);
|
||||
--ak-c-modal__body--PaddingLeft: var(--pf-global--spacer--lg);
|
||||
--ak-c-modal__body--last-child--PaddingBottom: var(--pf-global--spacer--lg);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Footer */
|
||||
|
||||
.ak-c-modal {
|
||||
--ak-c-modal__footer--PaddingTop: var(--pf-global--spacer--lg);
|
||||
--ak-c-modal__footer--PaddingRight: var(--pf-global--spacer--lg);
|
||||
--ak-c-modal__footer--PaddingBottom: var(--pf-global--spacer--lg);
|
||||
--ak-c-modal__footer--PaddingLeft: var(--pf-global--spacer--lg);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Modifiers */
|
||||
|
||||
.ak-c-modal {
|
||||
--ak-c-modal--m-sm--sm--MaxWidth: 35rem;
|
||||
--ak-c-modal--m-md--Width: 52.5rem;
|
||||
--ak-c-modal--m-lg--lg--MaxWidth: 70rem;
|
||||
|
||||
--ak-c-modal--m-align-top--spacer: var(--pf-global--spacer--sm);
|
||||
--ak-c-modal--m-align-top--xl--spacer: var(--pf-global--spacer--xl);
|
||||
--ak-c-modal--m-align-top--MarginTop: var(--ak-c-modal--m-align-top--spacer);
|
||||
--ak-c-modal--m-align-top--MaxHeight: calc(
|
||||
100% - min(var(--ak-c-modal--m-align-top--spacer), var(--pf-global--spacer--2xl)) -
|
||||
var(--ak-c-modal--m-align-top--spacer)
|
||||
);
|
||||
--ak-c-modal--m-align-top--MaxWidth: calc(
|
||||
100% - min(var(--ak-c-modal--m-align-top--spacer) * 2, var(--pf-global--spacer--xl))
|
||||
);
|
||||
--ak-c-modal--m-danger__title-icon--Color: var(--pf-global--danger-color--100);
|
||||
--ak-c-modal--m-warning__title-icon--Color: var(--pf-global--warning-color--100);
|
||||
--ak-c-modal--m-success__title-icon--Color: var(--pf-global--success-color--100);
|
||||
--ak-c-modal--m-info__title-icon--Color: var(--pf-global--info-color--100);
|
||||
--ak-c-modal--m-default__title-icon--Color: var(--pf-global--default-color--200);
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
--ak-c-modal--m-align-top--spacer: var(--ak-c-modal--m-align-top--xl--spacer);
|
||||
}
|
||||
|
||||
&.pf-m-sm {
|
||||
--ak-c-modal--Width: var(--ak-c-modal--m-sm--sm--MaxWidth);
|
||||
}
|
||||
|
||||
&.pf-m-md {
|
||||
--ak-c-modal--Width: var(--ak-c-modal--m-md--Width);
|
||||
}
|
||||
|
||||
&.pf-m-lg {
|
||||
--ak-c-modal--Width: var(--ak-c-modal--m-lg--lg--MaxWidth);
|
||||
}
|
||||
|
||||
&.pf-m-align-top {
|
||||
top: var(--ak-c-modal--m-align-top--MarginTop);
|
||||
align-self: flex-start;
|
||||
max-width: var(--ak-c-modal--m-align-top--MaxWidth);
|
||||
max-height: var(--ak-c-modal--m-align-top--MaxHeight);
|
||||
}
|
||||
|
||||
&.pf-m-danger {
|
||||
--ak-c-modal__title-icon--Color: var(--ak-c-modal--m-danger__title-icon--Color);
|
||||
}
|
||||
|
||||
&.pf-m-warning {
|
||||
--ak-c-modal__title-icon--Color: var(--ak-c-modal--m-warning__title-icon--Color);
|
||||
}
|
||||
|
||||
&.pf-m-success {
|
||||
--ak-c-modal__title-icon--Color: var(--ak-c-modal--m-success__title-icon--Color);
|
||||
}
|
||||
|
||||
&.pf-m-default {
|
||||
--ak-c-modal__title-icon--Color: var(--ak-c-modal--m-default__title-icon--Color);
|
||||
}
|
||||
|
||||
&.pf-m-info {
|
||||
--ak-c-modal__title-icon--Color: var(--ak-c-modal--m-info__title-icon--Color);
|
||||
}
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
@@ -1,14 +0,0 @@
|
||||
/* #region Dark Theme */
|
||||
|
||||
[data-theme="dark"] .pf-c-modal-box,
|
||||
:host([theme="dark"]) .pf-c-modal-box {
|
||||
background-color: var(--ak-dark-background);
|
||||
|
||||
.pf-c-modal-box__header,
|
||||
.pf-c-modal-box__footer,
|
||||
.pf-c-modal-box__body {
|
||||
background-color: var(--ak-dark-background);
|
||||
}
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
@@ -11,6 +11,7 @@
|
||||
@import "./base/globals.css";
|
||||
@import "./base/common.css";
|
||||
@import "./base/placeholder.css";
|
||||
@import "./base/modal.css";
|
||||
@import "#styles/locales/ja/globals.css";
|
||||
@import "#styles/locales/ko/globals.css";
|
||||
@import "#styles/locales/zh/globals.css";
|
||||
|
||||
22
web/types/dom.d.ts
vendored
Normal file
22
web/types/dom.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @file Global DOM-related types.
|
||||
*/
|
||||
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
/**
|
||||
* The possible values for the `closedBy` property of {@linkcode HTMLDialogElement}.
|
||||
*/
|
||||
type ClosedBy = "any" | "closerequest" | "none";
|
||||
|
||||
interface HTMLDialogElement {
|
||||
/**
|
||||
* Indicates the types of user actions that can be used to close the associated <dialog> element.
|
||||
*
|
||||
* @attr closedby
|
||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/closedBy MDN}
|
||||
*/
|
||||
closedBy: ClosedBy;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user