Files
authentik/web/src/elements/forms/HorizontalFormElement.ts
Teffen Ellis 1bc2daae98 web: Form validation regressions, consistency fixes (#15894)
* web: Clean up naming of inputs.

* web: Flesh out label components, slots.

* web: Expand clickable area of labels.

* web: Fix issue where launch URL is required.

* web: Surface string errors verbatim.

* web: Fix issues surrounding client-side error reporting, form validation,
server-side error reporting.

* web: Clarify property visibility.

* web: Fix issue where provider errors are not surfaced.

* web: Fix issue where wizard steps do not consistently use form validation.

* web: Fix issue where render root is not preferred.

* web: Fix import path.

* web: Fix selectors.

* keep labels aligned

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* web: Fix field nesting.

* web: Fix issue where required secret text inputs fail validation when editing existing entities.

- We need to make the component have a better understanding of this concept.

* web: Fix slot alignment on legacy elements.

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-08-12 14:34:08 +00:00

145 lines
4.6 KiB
TypeScript

import { AkControlElement, isControlElement } from "#elements/AkControlElement";
import { AKElement } from "#elements/Base";
import { isNameableElement, NamedElement } from "#elements/utils/inputs";
import { AKFormErrors, ErrorProp } from "#components/ak-field-errors";
import { AKLabel } from "#components/ak-label";
import { css, CSSResult, html, PropertyValues, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
/**
* Horizontal Form Element Container.
*
* This element provides the interface between elements of our forms and the
* form itself.
* @custom-element ak-form-element-horizontal
*
* @TODO
* 1. Updated() has a lot of that slug code again. Really, all you want is for the slug input object
* to update itself if its content seems to have been tracking some other key element.
* 2. Updated() pushes the `name` field down to the children, as if that were necessary; why isn't
* it being written on-demand when the child is written? Because it's slotted... despite there
* being very few unique uses.
*/
@customElement("ak-form-element-horizontal")
export class HorizontalFormElement extends AKElement {
static styles: CSSResult[] = [
PFBase,
PFForm,
PFFormControl,
css`
.pf-c-form__group {
display: grid;
grid-template-columns:
var(--pf-c-form--m-horizontal__group-label--md--GridColumnWidth)
var(--pf-c-form--m-horizontal__group-control--md--GridColumnWidth);
}
`,
];
//#region Properties
/**
* A unique ID to associate with the input and label.
*/
@property({ type: String, reflect: false })
public fieldID?: string;
/**
* The label for the input control
* @property
* @attribute
* @deprecated Labels cannot associate with inputs across DOM roots. Use the slotted `label` element instead.
*/
@property({ type: String })
public label?: string;
@property({ type: Boolean, reflect: false })
public required?: boolean;
@property({ attribute: false })
public errorMessages?: ErrorProp[];
@property({ type: String })
public name?: string;
//#endregion
public controlledElement: NamedElement | AkControlElement | null = null;
//#region Lifecycle
public override firstUpdated(): void {
this.#synchronizeAttributes();
}
public override updated(changedProperties: PropertyValues<this>): void {
if (changedProperties.has("errorMessages") && this.controlledElement) {
this.controlledElement.setAttribute(
"aria-invalid",
this.errorMessages?.length ? "true" : "false",
);
}
}
/**
* Ensure that all inputs have a name attribute.
*
* TODO: Swap with `HTMLElement.prototype.attachInternals`.
*/
#synchronizeAttributes(): void {
// If we don't have a name, we can't do anything.
if (!this.name) return;
for (const element of this.querySelectorAll("*")) {
// Is this element capable of being named?
if (!isControlElement(element) && !isNameableElement(element)) continue;
// And does the element already match the name?
if (element.getAttribute("name") === this.name) continue;
element.setAttribute("name", this.name);
this.controlledElement = element;
break;
}
}
//#endregion
//#region Rendering
render(): TemplateResult {
this.#synchronizeAttributes();
return html`<div class="pf-c-form__group" role="group">
<div class="pf-c-form__group-label">
${this.label
? html`
${AKLabel({ htmlFor: this.fieldID, required: this.required }, this.label)}
</div>`
: html`<slot name="label"></slot>`}
</div>
<div class="pf-c-form__group-control">
<slot class="pf-c-form__horizontal-group"></slot>
<div class="pf-c-form__horizontal-group">
${AKFormErrors({ errors: this.errorMessages })}
</div>
</div>
</div>`;
}
//#endregion
}
declare global {
interface HTMLElementTagNameMap {
"ak-form-element-horizontal": HorizontalFormElement;
}
}