web/admin: Text and Textarea Fields that "hide" their contents until prompted (#15024)
* web: Add InvalidationFlow to Radius Provider dialogues
## What
- Bugfix: adds the InvalidationFlow to the Radius Provider dialogues
- Repairs: `{"invalidation_flow":["This field is required."]}` message, which was *not* propagated
to the Notification.
- Nitpick: Pretties `?foo=${true}` expressions: `s/\?([^=]+)=\$\{true\}/\1/`
## Note
Yes, I know I'm going to have to do more magic when we harmonize the forms, and no, I didn't add the
Property Mappings to the wizard, and yes, I know I'm going to have pain with the *new* version of
the wizard. But this is a serious bug; you can't make Radius servers with *either* of the current
dialogues at the moment.
* This (temporary) change is needed to prevent the unit tests from failing.
\# What
\# Why
\# How
\# Designs
\# Test Steps
\# Other Notes
* Revert "This (temporary) change is needed to prevent the unit tests from failing."
This reverts commit dddde09be5
.
* web/admin: Provide `hidden` text and textarea components
## Details
This commit provides two new elements (technically, since they're API-unaware), one for `<input
type="text">`, and one for `<textarea>`, that provide for the ability to create fields that are (or
can be) hidden. A new boolean attribute, `revealed`, shows the state of the component (the content
is therefore *not* revealed by default).
It also includes a third new element, `ak-visibility-toggle`, that creates a hide/show toggle with
all the right icons, styling, and eventing. It's straightforward, and isolating it improved the
DX of everything that uses that feature by quite a bit.
Storybook stories (with autodoc documentation) have been provided for `ak-hidden-text-input`,
`ak-hidden-textarea-input`, and `ak-visibility-toggle`.
## Maintenance Notice
As a maintenance detail, the field `ak-private-text` has been renamed `ak-secret-text` to reflect
its usage, and the places where it was used have all been changed to reflect that update.
* web/component: embed styling (for now) to handle the lightDom/shadowDom/slot conflicts in HorizontalLightComponent and HorizontalFormElement
* Comments and Types. I really shouldn't have to catch this stuff with my eyeballs.
* fix typo
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
---------
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@ -1,5 +1,5 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-private-textarea-input.js";
|
||||
import "@goauthentik/components/ak-secret-textarea-input.js";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||
@ -46,7 +46,7 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-private-textarea-input
|
||||
<ak-secret-textarea-input
|
||||
label=${msg("Certificate")}
|
||||
name="certificateData"
|
||||
input-hint="code"
|
||||
@ -54,8 +54,8 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string
|
||||
required
|
||||
?revealed=${this.instance === undefined}
|
||||
help=${msg("PEM-encoded Certificate data.")}
|
||||
></ak-private-textarea-input>
|
||||
<ak-private-textarea-input
|
||||
></ak-secret-textarea-input>
|
||||
<ak-secret-textarea-input
|
||||
label=${msg("Private Key")}
|
||||
name="keyData"
|
||||
input-hint="code"
|
||||
@ -63,7 +63,7 @@ export class CertificateKeyPairForm extends ModelForm<CertificateKeyPair, string
|
||||
help=${msg(
|
||||
"Optional Private Key. If this is set, you can use this keypair for encryption.",
|
||||
)}
|
||||
></ak-private-textarea-input>`;
|
||||
></ak-secret-textarea-input>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/common/constants";
|
||||
import "@goauthentik/components/ak-private-textarea-input.js";
|
||||
import "@goauthentik/components/ak-secret-textarea-input.js";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||
@ -62,13 +62,13 @@ export class EnterpriseLicenseForm extends ModelForm<License, string> {
|
||||
value="${ifDefined(this.installID)}"
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-private-textarea-input
|
||||
<ak-secret-textarea-input
|
||||
name="key"
|
||||
?revealed=${this.instance === undefined}
|
||||
label=${msg("License key")}
|
||||
input-hint="code"
|
||||
>
|
||||
</ak-private-textarea-input>`;
|
||||
</ak-secret-textarea-input>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,8 +7,8 @@ import {
|
||||
UserMatchingModeToLabel,
|
||||
} from "@goauthentik/admin/sources/oauth/utils";
|
||||
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-private-text-input.js";
|
||||
import "@goauthentik/components/ak-private-textarea-input.js";
|
||||
import "@goauthentik/components/ak-secret-text-input.js";
|
||||
import "@goauthentik/components/ak-secret-textarea-input.js";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import "@goauthentik/components/ak-textarea-input";
|
||||
@ -248,22 +248,22 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
|
||||
value=${ifDefined(this.instance?.syncPrincipal)}
|
||||
help=${msg("Principal used to authenticate to the KDC for syncing.")}
|
||||
></ak-text-input>
|
||||
<ak-private-text-input
|
||||
<ak-secret-text-input
|
||||
name="syncPassword"
|
||||
label=${msg("Sync password")}
|
||||
?revealed=${this.instance === undefined}
|
||||
help=${msg(
|
||||
"Password used to authenticate to the KDC for syncing. Optional if Sync keytab or Sync credentials cache is provided.",
|
||||
)}
|
||||
></ak-private-text-input>
|
||||
<ak-private-textarea-input
|
||||
></ak-secret-text-input>
|
||||
<ak-secret-textarea-input
|
||||
name="syncKeytab"
|
||||
label=${msg("Sync keytab")}
|
||||
?revealed=${this.instance === undefined}
|
||||
help=${msg(
|
||||
"Keytab used to authenticate to the KDC for syncing. Optional if Sync password or Sync credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
|
||||
)}
|
||||
></ak-private-textarea-input>
|
||||
></ak-secret-textarea-input>
|
||||
<ak-text-input
|
||||
name="syncCcache"
|
||||
label=${msg("Sync credentials cache")}
|
||||
@ -285,14 +285,14 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
|
||||
"Force the use of a specific server name for SPNEGO. Must be in the form HTTP@domain",
|
||||
)}
|
||||
></ak-text-input>
|
||||
<ak-private-textarea-input
|
||||
<ak-secret-textarea-input
|
||||
name="spnegoKeytab"
|
||||
label=${msg("SPNEGO keytab")}
|
||||
?revealed=${this.instance === undefined}
|
||||
help=${msg(
|
||||
"Keytab used for SPNEGO. Optional if SPNEGO credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
|
||||
)}
|
||||
></ak-private-textarea-input>
|
||||
></ak-secret-textarea-input>
|
||||
<ak-text-input
|
||||
name="spnegoCcache"
|
||||
label=${msg("SPNEGO credentials cache")}
|
||||
|
@ -2,7 +2,7 @@ import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import { placeholderHelperText } from "@goauthentik/admin/helperText";
|
||||
import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-private-text-input.js";
|
||||
import "@goauthentik/components/ak-secret-text-input.js";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
@ -260,11 +260,11 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-private-text-input
|
||||
<ak-secret-text-input
|
||||
label=${msg("Bind Password")}
|
||||
name="bindPassword"
|
||||
?revealed=${this.instance === undefined}
|
||||
></ak-private-text-input>
|
||||
></ak-secret-text-input>
|
||||
<ak-form-element-horizontal label=${msg("Base DN")} required name="baseDn">
|
||||
<input
|
||||
type="text"
|
||||
|
@ -8,8 +8,8 @@ import {
|
||||
UserMatchingModeToLabel,
|
||||
} from "@goauthentik/admin/sources/oauth/utils";
|
||||
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-private-textarea-input.js";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/components/ak-secret-textarea-input.js";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||
@ -441,14 +441,14 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">${msg("Also known as Client ID.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-private-textarea-input
|
||||
<ak-secret-textarea-input
|
||||
label=${msg("Consumer secret")}
|
||||
name="consumerSecret"
|
||||
input-hint="code"
|
||||
help=${msg("Also known as Client Secret.")}
|
||||
required
|
||||
?revealed=${this.instance === undefined}
|
||||
></ak-private-textarea-input>
|
||||
></ak-secret-textarea-input>
|
||||
<ak-form-element-horizontal label=${msg("Scopes")} name="additionalScopes">
|
||||
<input
|
||||
type="text"
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-private-text-input.js";
|
||||
import "@goauthentik/components/ak-secret-text-input.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
@ -95,13 +95,13 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-private-text-input
|
||||
<ak-secret-text-input
|
||||
name="clientSecret"
|
||||
label=${msg("Secret key")}
|
||||
input-hint="code"
|
||||
required
|
||||
?revealed=${this.instance === undefined}
|
||||
></ak-private-text-input>
|
||||
></ak-secret-text-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
@ -125,12 +125,12 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
|
||||
spellcheck="false"
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-private-text-input
|
||||
<ak-secret-text-input
|
||||
name="adminSecretKey"
|
||||
label=${msg("Secret key")}
|
||||
input-hint="code"
|
||||
?revealed=${this.instance === undefined}
|
||||
></ak-private-text-input>
|
||||
></ak-secret-text-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group expanded>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
|
||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-private-text-input.js";
|
||||
import "@goauthentik/components/ak-secret-text-input.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/Radio";
|
||||
@ -77,11 +77,11 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-private-text-input
|
||||
<ak-secret-text-input
|
||||
name="password"
|
||||
label=${msg("SMTP Password")}
|
||||
?revealed=${this.instance === undefined}
|
||||
></ak-private-text-input>
|
||||
></ak-secret-text-input>
|
||||
|
||||
<ak-form-element-horizontal name="useTls">
|
||||
<label class="pf-c-switch">
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-number-input";
|
||||
import "@goauthentik/components/ak-private-text-input.js";
|
||||
import "@goauthentik/components/ak-secret-text-input.js";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
@ -70,7 +70,7 @@ export class CaptchaStageForm extends BaseStageForm<CaptchaStage> {
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-private-text-input
|
||||
<ak-secret-text-input
|
||||
name="privateKey"
|
||||
label=${msg("Private Key")}
|
||||
input-hint="code"
|
||||
@ -79,7 +79,7 @@ export class CaptchaStageForm extends BaseStageForm<CaptchaStage> {
|
||||
help=${msg(
|
||||
"Private key, acquired from https://www.google.com/recaptcha/intro/v3.html.",
|
||||
)}
|
||||
></ak-private-text-input>
|
||||
></ak-secret-text-input>
|
||||
|
||||
<ak-switch-input
|
||||
name="interactive"
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-private-text-input.js";
|
||||
import "@goauthentik/components/ak-secret-text-input.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
||||
@ -73,11 +73,11 @@ export class EmailStageForm extends BaseStageForm<EmailStage> {
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-private-text-input
|
||||
<ak-secret-text-input
|
||||
label=${msg("SMTP Password")}
|
||||
name="password"
|
||||
?revealed=${this.instance === undefined}
|
||||
></ak-private-text-input>
|
||||
></ak-secret-text-input>
|
||||
<ak-form-element-horizontal name="useTls">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { AKElement, type AKElementProps } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement.js";
|
||||
|
||||
import { TemplateResult, html, nothing } from "lit";
|
||||
@ -6,6 +6,19 @@ import { property } from "lit/decorators.js";
|
||||
|
||||
type HelpType = TemplateResult | typeof nothing;
|
||||
|
||||
export interface HorizontalLightComponentProps<T> extends AKElementProps {
|
||||
name: string;
|
||||
label?: string;
|
||||
required?: boolean;
|
||||
help?: string;
|
||||
bighelp?: TemplateResult | TemplateResult[];
|
||||
hidden?: boolean;
|
||||
invalid?: boolean;
|
||||
errorMessages?: string[];
|
||||
value?: T;
|
||||
inputHint?: string;
|
||||
}
|
||||
|
||||
export class HorizontalLightComponent<T> extends AKElement {
|
||||
// Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but
|
||||
// we're not actually using that and, for the meantime, we need the form handlers to be able to
|
||||
@ -18,37 +31,81 @@ export class HorizontalLightComponent<T> extends AKElement {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name attribute for the form element
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String, reflect: true })
|
||||
name!: string;
|
||||
|
||||
/**
|
||||
* The label for the input control
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String, reflect: true })
|
||||
label = "";
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true })
|
||||
required = false;
|
||||
|
||||
/**
|
||||
* Help text to display below the form element. Optional
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String, reflect: true })
|
||||
help = "";
|
||||
|
||||
/**
|
||||
* Extended help content. Optional. Expects to be a TemplateResult
|
||||
* @property
|
||||
*/
|
||||
@property({ type: Object })
|
||||
bighelp?: TemplateResult | TemplateResult[];
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true })
|
||||
hidden = false;
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true })
|
||||
invalid = false;
|
||||
|
||||
/**
|
||||
* @property
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
errorMessages: string[] = [];
|
||||
|
||||
/**
|
||||
* @attribute
|
||||
* @property
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
value?: T;
|
||||
|
||||
/**
|
||||
* Input hint.
|
||||
* - `code`: uses a monospace font and disables spellcheck & autocomplete
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String, attribute: "input-hint" })
|
||||
inputHint = "";
|
||||
|
||||
renderControl() {
|
||||
protected renderControl() {
|
||||
throw new Error("Must be implemented in a subclass");
|
||||
}
|
||||
|
||||
|
159
web/src/components/ak-hidden-text-input.ts
Normal file
159
web/src/components/ak-hidden-text-input.ts
Normal file
@ -0,0 +1,159 @@
|
||||
import { bound } from "#elements/decorators/bound";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, html } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators.js";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
HorizontalLightComponent,
|
||||
HorizontalLightComponentProps,
|
||||
} from "./HorizontalLightComponent";
|
||||
import "./ak-visibility-toggle.js";
|
||||
import type { VisibilityToggleProps } from "./ak-visibility-toggle.js";
|
||||
|
||||
type BaseProps = HorizontalLightComponentProps<string> &
|
||||
Pick<VisibilityToggleProps, "showMessage" | "hideMessage">;
|
||||
|
||||
export interface AkHiddenTextInputProps extends BaseProps {
|
||||
revealed: boolean;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export type InputLike = HTMLTextAreaElement | HTMLInputElement;
|
||||
|
||||
/**
|
||||
* @element ak-hidden-text-input
|
||||
* @class AkHiddenTextInput
|
||||
*
|
||||
* A text-input field with a visibility control, so you can show/hide sensitive fields.
|
||||
*
|
||||
* ## CSS Parts
|
||||
* @csspart container - The main container div
|
||||
* @csspart input - The input element
|
||||
* @csspart toggle - The visibility toggle button
|
||||
*
|
||||
*/
|
||||
@customElement("ak-hidden-text-input")
|
||||
export class AkHiddenTextInput<T extends InputLike = HTMLInputElement>
|
||||
extends HorizontalLightComponent<string>
|
||||
implements AkHiddenTextInputProps
|
||||
{
|
||||
public static get styles() {
|
||||
return [
|
||||
css`
|
||||
main {
|
||||
display: flex;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String, reflect: true })
|
||||
public value = "";
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public revealed = false;
|
||||
|
||||
/**
|
||||
* Text for when the input has no set value
|
||||
*
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String })
|
||||
public placeholder?: string;
|
||||
|
||||
/**
|
||||
* Specify kind of help the browser should try to provide
|
||||
*
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String })
|
||||
public autocomplete?: "none" | AutoFill;
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String, attribute: "show-message" })
|
||||
public showMessage = msg("Show field content");
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String, attribute: "hide-message" })
|
||||
public hideMessage = msg("Hide field content");
|
||||
|
||||
@query("#main > input")
|
||||
protected inputField!: T;
|
||||
|
||||
@bound
|
||||
private handleToggleVisibility() {
|
||||
this.revealed = !this.revealed;
|
||||
|
||||
// Maintain focus on input after toggle
|
||||
this.updateComplete.then(() => {
|
||||
if (this.inputField && document.activeElement === this) {
|
||||
this.inputField.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Because of the peculiarities of how HorizontalLightComponent works, keeping its content
|
||||
// in the LightDom so the inner components actually inherit styling, the normal `css` options
|
||||
// aren't available. Embedding styles is bad styling, and we'll fix it in the next style
|
||||
// refresh.
|
||||
protected renderInputField(setValue: (ev: InputEvent) => void, code: boolean) {
|
||||
return html` <input
|
||||
style="flex: 1 1 auto; min-width: 0;"
|
||||
part="input"
|
||||
type=${this.revealed ? "text" : "password"}
|
||||
@input=${setValue}
|
||||
value=${ifDefined(this.value)}
|
||||
placeholder=${ifDefined(this.placeholder)}
|
||||
class="${classMap({
|
||||
"pf-c-form-control": true,
|
||||
"pf-m-monospace": code,
|
||||
})}"
|
||||
spellcheck=${code ? "false" : "true"}
|
||||
?required=${this.required}
|
||||
/>`;
|
||||
}
|
||||
|
||||
protected override renderControl() {
|
||||
const code = this.inputHint === "code";
|
||||
const setValue = (ev: InputEvent) => {
|
||||
this.value = (ev.target as T).value;
|
||||
};
|
||||
return html` <div style="display: flex; gap: 0.25rem">
|
||||
${this.renderInputField(setValue, code)}
|
||||
<!-- -->
|
||||
<ak-visibility-toggle
|
||||
part="toggle"
|
||||
style="flex: 0 0 auto; align-self: flex-start"
|
||||
?open=${this.revealed}
|
||||
show-message=${this.showMessage}
|
||||
hide-message=${this.hideMessage}
|
||||
@click=${() => (this.revealed = !this.revealed)}
|
||||
></ak-visibility-toggle>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-hidden-text-input": AkHiddenTextInput;
|
||||
}
|
||||
}
|
128
web/src/components/ak-hidden-textarea-input.ts
Normal file
128
web/src/components/ak-hidden-textarea-input.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import { css, html } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators.js";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { AkHiddenTextInput, type AkHiddenTextInputProps } from "./ak-hidden-text-input.js";
|
||||
|
||||
export interface AkHiddenTextAreaInputProps extends AkHiddenTextInputProps {
|
||||
/**
|
||||
* Number of visible text lines (rows)
|
||||
*/
|
||||
rows?: number;
|
||||
|
||||
/**
|
||||
* Number of visible character width (cols)
|
||||
*/
|
||||
cols?: number;
|
||||
|
||||
/**
|
||||
* How the textarea can be resized
|
||||
*/
|
||||
resize?: "none" | "both" | "horizontal" | "vertical";
|
||||
|
||||
/**
|
||||
* Whether text should wrap
|
||||
*/
|
||||
wrap?: "soft" | "hard" | "off";
|
||||
}
|
||||
|
||||
/**
|
||||
* @element ak-hidden-text-input
|
||||
* @class AkHiddenTextInput
|
||||
*
|
||||
* A text-input field with a visibility control, so you can show/hide sensitive fields.
|
||||
*
|
||||
* ## CSS Parts
|
||||
* @csspart container - The main container div
|
||||
* @csspart input - The input element
|
||||
* @csspart toggle - The visibility toggle button
|
||||
*
|
||||
*/
|
||||
@customElement("ak-hidden-textarea-input")
|
||||
export class AkHiddenTextAreaInput
|
||||
extends AkHiddenTextInput<HTMLTextAreaElement>
|
||||
implements AkHiddenTextAreaInputProps
|
||||
{
|
||||
/* These are mostly just forwarded to the textarea component. */
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: Number })
|
||||
rows?: number = 4;
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: Number })
|
||||
cols?: number;
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*
|
||||
* You want `resize=true` so that the resize value is visible in the component tag, activating
|
||||
* the CSS associated with these values.
|
||||
*/
|
||||
@property({ type: String, reflect: true })
|
||||
resize?: "none" | "both" | "horizontal" | "vertical" = "vertical";
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String })
|
||||
wrap?: "soft" | "hard" | "off" = "soft";
|
||||
|
||||
@query("#main > textarea")
|
||||
protected inputField!: HTMLTextAreaElement;
|
||||
|
||||
get displayValue() {
|
||||
const value = this.value ?? "";
|
||||
if (this.revealed) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return value
|
||||
.split("\n")
|
||||
.reduce((acc: string[], line: string) => [...acc, "*".repeat(line.length)], [])
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
// TODO: Because of the peculiarities of how HorizontalLightComponent works, keeping its content
|
||||
// in the LightDom so the inner components actually inherit styling, the normal `css` options
|
||||
// aren't available. Embedding styles is bad styling, and we'll fix it in the next style
|
||||
// refresh.
|
||||
protected override renderInputField(setValue: (ev: InputEvent) => void, code: boolean) {
|
||||
const wrap = this.revealed ? this.wrap : "soft";
|
||||
|
||||
return html`
|
||||
<textarea
|
||||
style="flex: 1 1 auto; min-width: 0;"
|
||||
part="textarea"
|
||||
@input=${setValue}
|
||||
placeholder=${ifDefined(this.placeholder)}
|
||||
rows=${ifDefined(this.rows)}
|
||||
cols=${ifDefined(this.cols)}
|
||||
wrap=${ifDefined(wrap)}
|
||||
class=${classMap({
|
||||
"pf-c-form-control": true,
|
||||
"pf-m-monospace": code,
|
||||
})}
|
||||
spellcheck=${code ? "false" : "true"}
|
||||
?required=${this.required}
|
||||
>
|
||||
${this.displayValue}</textarea
|
||||
>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-hidden-textarea-input": AkHiddenTextAreaInput;
|
||||
}
|
||||
}
|
@ -8,8 +8,8 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { HorizontalLightComponent } from "./HorizontalLightComponent";
|
||||
|
||||
@customElement("ak-private-text-input")
|
||||
export class AkPrivateTextInput extends HorizontalLightComponent<string> {
|
||||
@customElement("ak-secret-text-input")
|
||||
export class AkSecretTextInput extends HorizontalLightComponent<string> {
|
||||
@property({ type: String, reflect: true })
|
||||
public value = "";
|
||||
|
||||
@ -23,7 +23,7 @@ export class AkPrivateTextInput extends HorizontalLightComponent<string> {
|
||||
this.revealed = true;
|
||||
}
|
||||
|
||||
#renderPrivateInput() {
|
||||
#renderSecretInput() {
|
||||
return html`<div class="pf-c-form__horizontal-group" @click=${() => this.#onReveal()}>
|
||||
<input
|
||||
class="pf-c-form-control"
|
||||
@ -60,14 +60,14 @@ export class AkPrivateTextInput extends HorizontalLightComponent<string> {
|
||||
}
|
||||
|
||||
public override renderControl() {
|
||||
return this.revealed ? this.renderVisibleInput() : this.#renderPrivateInput();
|
||||
return this.revealed ? this.renderVisibleInput() : this.#renderSecretInput();
|
||||
}
|
||||
}
|
||||
|
||||
export default AkPrivateTextInput;
|
||||
export default AkSecretTextInput;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-private-text-input": AkPrivateTextInput;
|
||||
"ak-secret-text-input": AkSecretTextInput;
|
||||
}
|
||||
}
|
@ -5,10 +5,10 @@ import { customElement, property } from "lit/decorators.js";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { AkPrivateTextInput } from "./ak-private-text-input.js";
|
||||
import { AkSecretTextInput } from "./ak-secret-text-input.js";
|
||||
|
||||
@customElement("ak-private-textarea-input")
|
||||
export class AkPrivateTextAreaInput extends AkPrivateTextInput {
|
||||
@customElement("ak-secret-textarea-input")
|
||||
export class AkSecretTextAreaInput extends AkSecretTextInput {
|
||||
protected override renderVisibleInput() {
|
||||
const code = this.inputHint === "code";
|
||||
const setValue = (ev: InputEvent) => {
|
||||
@ -34,10 +34,10 @@ export class AkPrivateTextAreaInput extends AkPrivateTextInput {
|
||||
}
|
||||
}
|
||||
|
||||
export default AkPrivateTextAreaInput;
|
||||
export default AkSecretTextAreaInput;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-private-textarea-input": AkPrivateTextAreaInput;
|
||||
"ak-secret-textarea-input": AkSecretTextAreaInput;
|
||||
}
|
||||
}
|
89
web/src/components/ak-visibility-toggle.ts
Normal file
89
web/src/components/ak-visibility-toggle.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base.js";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
export interface VisibilityToggleProps {
|
||||
open: boolean;
|
||||
disabled: boolean;
|
||||
showMessage: string;
|
||||
hideMessage: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @component ak-visibility-toggle
|
||||
* @class VisibilityToggle
|
||||
*
|
||||
* A straightforward two-state iconic button we use in a few places as way of telling users to hide
|
||||
* or show something secret, such as a password or private key. Expects the client to manage its
|
||||
* state.
|
||||
*
|
||||
* @events
|
||||
* - click: when the toggle is clicked.
|
||||
*/
|
||||
@customElement("ak-visibility-toggle")
|
||||
export class VisibilityToggle extends AKElement implements VisibilityToggleProps {
|
||||
static get styles() {
|
||||
return [PFBase, PFButton];
|
||||
}
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true })
|
||||
open = false;
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true })
|
||||
disabled = false;
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String, attribute: "show-message" })
|
||||
showMessage = msg("Show field content");
|
||||
|
||||
/**
|
||||
* @property
|
||||
* @attribute
|
||||
*/
|
||||
@property({ type: String, attribute: "hide-message" })
|
||||
hideMessage = msg("Hide field content");
|
||||
|
||||
render() {
|
||||
const [label, icon] = this.open
|
||||
? [this.hideMessage, "fa-eye"]
|
||||
: [this.showMessage, "fa-eye-slash"];
|
||||
|
||||
const onClick = (ev: PointerEvent) => {
|
||||
ev.stopPropagation();
|
||||
this.dispatchEvent(new PointerEvent(ev.type, ev));
|
||||
};
|
||||
|
||||
return html`<button
|
||||
aria-label=${label}
|
||||
title=${label}
|
||||
@click=${onClick}
|
||||
?disabled=${this.disabled}
|
||||
class="pf-c-button pf-m-control"
|
||||
type="button"
|
||||
>
|
||||
<i class="fas ${icon}" aria-hidden="true"></i>
|
||||
</button>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-visibility-toggle": VisibilityToggle;
|
||||
}
|
||||
}
|
93
web/src/components/stories/ak-hidden-text-input.stories.ts
Normal file
93
web/src/components/stories/ak-hidden-text-input.stories.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import type { Meta, StoryObj } from "@storybook/web-components";
|
||||
|
||||
import { html, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import "../ak-hidden-text-input";
|
||||
import { type AkHiddenTextInput, type AkHiddenTextInputProps } from "../ak-hidden-text-input.js";
|
||||
|
||||
const metadata: Meta<AkHiddenTextInputProps> = {
|
||||
title: "Components / <ak-hidden-text-input>",
|
||||
component: "ak-hidden-text-input",
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: `
|
||||
# Hidden Text Input Component
|
||||
|
||||
A text-input field with a visibility control, so you can show/hide sensitive fields.
|
||||
`,
|
||||
},
|
||||
},
|
||||
layout: "padded",
|
||||
},
|
||||
argTypes: {
|
||||
label: {
|
||||
control: "text",
|
||||
description: "Label text for the input field",
|
||||
},
|
||||
value: {
|
||||
control: "text",
|
||||
description: "Current value of the input",
|
||||
},
|
||||
revealed: {
|
||||
control: "boolean",
|
||||
description: "Whether the text is currently visible",
|
||||
},
|
||||
placeholder: {
|
||||
control: "text",
|
||||
description: "Placeholder text for the input",
|
||||
},
|
||||
required: {
|
||||
control: "boolean",
|
||||
description: "Whether the input is required",
|
||||
},
|
||||
inputHint: {
|
||||
control: "select",
|
||||
options: ["text", "code"],
|
||||
description: "Input type hint for styling and behavior",
|
||||
},
|
||||
showMessage: {
|
||||
control: "text",
|
||||
description: "Custom message for show action",
|
||||
},
|
||||
hideMessage: {
|
||||
control: "text",
|
||||
description: "Custom message for hide action",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
type Story = StoryObj<AkHiddenTextInput>;
|
||||
|
||||
const Template: Story = {
|
||||
args: {
|
||||
label: "Hidden Text Input",
|
||||
value: "",
|
||||
revealed: false,
|
||||
},
|
||||
render: (args) => html`
|
||||
<ak-hidden-text-input
|
||||
label=${ifDefined(args.label)}
|
||||
value=${ifDefined(args.value)}
|
||||
?revealed=${args.revealed}
|
||||
placeholder=${ifDefined(args.placeholder)}
|
||||
?required=${args.required}
|
||||
input-hint=${ifDefined(args.inputHint)}
|
||||
show-message=${ifDefined(args.showMessage)}
|
||||
hide-message=${ifDefined(args.hideMessage)}
|
||||
></ak-hidden-text-input>
|
||||
`,
|
||||
};
|
||||
|
||||
export const Password: Story = {
|
||||
...Template,
|
||||
args: {
|
||||
label: "Password",
|
||||
placeholder: "Enter your password",
|
||||
required: true,
|
||||
},
|
||||
};
|
140
web/src/components/stories/ak-hidden-textarea-input.stories.ts
Normal file
140
web/src/components/stories/ak-hidden-textarea-input.stories.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import type { Meta, StoryObj } from "@storybook/web-components";
|
||||
|
||||
import { html, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import "../ak-hidden-textarea-input";
|
||||
import {
|
||||
type AkHiddenTextAreaInput,
|
||||
type AkHiddenTextAreaInputProps,
|
||||
} from "../ak-hidden-textarea-input.js";
|
||||
|
||||
const metadata: Meta<AkHiddenTextAreaInputProps> = {
|
||||
title: "Components / <ak-hidden-textarea-input>",
|
||||
component: "ak-hidden-textarea-input",
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: `
|
||||
# Hidden Textarea Input Component
|
||||
|
||||
A textarea input field with a visibility control, so you can show/hide sensitive fields.
|
||||
`,
|
||||
},
|
||||
},
|
||||
layout: "padded",
|
||||
},
|
||||
argTypes: {
|
||||
label: {
|
||||
control: "text",
|
||||
description: "Label text for the input field",
|
||||
},
|
||||
value: {
|
||||
control: "text",
|
||||
description: "Current value of the input",
|
||||
},
|
||||
revealed: {
|
||||
control: "boolean",
|
||||
description: "Whether the text is currently visible",
|
||||
},
|
||||
placeholder: {
|
||||
control: "text",
|
||||
description: "Placeholder text for the input",
|
||||
},
|
||||
required: {
|
||||
control: "boolean",
|
||||
description: "Whether the input is required",
|
||||
},
|
||||
inputHint: {
|
||||
control: "select",
|
||||
options: ["text", "code"],
|
||||
description: "Input type hint for styling and behavior",
|
||||
},
|
||||
showMessage: {
|
||||
control: "text",
|
||||
description: "Custom message for show action",
|
||||
},
|
||||
hideMessage: {
|
||||
control: "text",
|
||||
description: "Custom message for hide action",
|
||||
},
|
||||
rows: {
|
||||
control: { type: "number", min: 1, max: 50 },
|
||||
description: "Number of visible text lines",
|
||||
},
|
||||
cols: {
|
||||
control: { type: "number", min: 10, max: 200 },
|
||||
description: "Number of visible character width",
|
||||
},
|
||||
resize: {
|
||||
control: "select",
|
||||
options: ["none", "both", "horizontal", "vertical"],
|
||||
description: "How the textarea can be resized",
|
||||
},
|
||||
wrap: {
|
||||
control: "select",
|
||||
options: ["soft", "hard", "off"],
|
||||
description: "Text wrapping behavior",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
type Story = StoryObj<AkHiddenTextAreaInput>;
|
||||
|
||||
const Template: Story = {
|
||||
args: {
|
||||
label: "Hidden Textarea Input",
|
||||
value: "",
|
||||
revealed: false,
|
||||
rows: 4,
|
||||
},
|
||||
render: (args) => html`
|
||||
<ak-hidden-textarea-input
|
||||
label=${ifDefined(args.label)}
|
||||
value=${ifDefined(args.value)}
|
||||
?revealed=${args.revealed}
|
||||
placeholder=${ifDefined(args.placeholder)}
|
||||
rows=${ifDefined(args.rows)}
|
||||
cols=${ifDefined(args.cols)}
|
||||
resize=${ifDefined(args.resize)}
|
||||
wrap=${ifDefined(args.wrap)}
|
||||
?required=${args.required}
|
||||
input-hint=${ifDefined(args.inputHint)}
|
||||
show-message=${ifDefined(args.showMessage)}
|
||||
hide-message=${ifDefined(args.hideMessage)}
|
||||
></ak-hidden-textarea-input>
|
||||
`,
|
||||
};
|
||||
|
||||
export const SslCertificate: Story = {
|
||||
...Template,
|
||||
args: {
|
||||
label: "SSL Certificate",
|
||||
value: `-----BEGIN CERTIFICATE-----
|
||||
MIIDXTCCAkWgAwIBAgIJAKoK/heBjcOuMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
|
||||
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
|
||||
aWRnaXRzIFB0eSBMdGQwHhcNMTcwNTEwMTk0MDA2WhcNMTgwNTEwMTk0MDA2WjBF
|
||||
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
|
||||
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTE3MDUxMDE5NDAwNloXDTE4MDUxMDE5
|
||||
NDAwNlowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNV
|
||||
BAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQAD
|
||||
ggEPADCCAQoCggEBALdUlNS31SzxwoFShahGfjHj6GgpcVbzL1Siq0Pqnf82T6M2
|
||||
EDuneMLzAgMBAAECggEBAJkPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgMBAAECggE
|
||||
BAJkPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgMBAAECggEBAJkPFn6jeMHyiq0Pqn
|
||||
f82T6M2EDuneMLzAgMBAAECggEBAJkPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgM
|
||||
BAAECggEBAJkPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgMBAAECggEBAJkPFn6jeM
|
||||
Hyiq0Pqnf82T6M2EDuneMLzAgMBAAECggEBAJkPFn6jeMHyiq0Pqnf82T6M2EDu
|
||||
neMLzAgMBAAECggEBAJkPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgMBAAECggEBAJ
|
||||
kPFn6jeMHyiq0Pqnf82T6M2EDuneMLzAgMBAAE=
|
||||
-----END CERTIFICATE-----`,
|
||||
inputHint: "code",
|
||||
rows: 15,
|
||||
resize: "vertical",
|
||||
showMessage: "Show certificate content",
|
||||
hideMessage: "Hide certificate content",
|
||||
autocomplete: "off",
|
||||
},
|
||||
};
|
121
web/src/components/stories/ak-visibility-toggle.stories.ts
Normal file
121
web/src/components/stories/ak-visibility-toggle.stories.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import type { Meta, StoryObj } from "@storybook/web-components";
|
||||
|
||||
import { html, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import "../ak-visibility-toggle";
|
||||
import { type VisibilityToggle, type VisibilityToggleProps } from "../ak-visibility-toggle.js";
|
||||
|
||||
const metadata: Meta<VisibilityToggleProps> = {
|
||||
title: "Elements/<ak-visibility-toggle>",
|
||||
component: "ak-visibility-toggle",
|
||||
tags: ["autodocs"],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: `
|
||||
# Visibility Toggle Component
|
||||
|
||||
A straightforward two-state iconic button for toggling the visibility of sensitive content such as passwords, private keys, or other secret information.
|
||||
|
||||
- Use for sensitive content that users might want to temporarily reveal
|
||||
- There are default hide/show messages for screen readers, but they can be overridden
|
||||
- Clients always handle the state
|
||||
- The \`open\` state is false by default; we assume you want sensitive content hidden at start
|
||||
`,
|
||||
},
|
||||
},
|
||||
layout: "padded",
|
||||
},
|
||||
argTypes: {
|
||||
open: {
|
||||
control: "boolean",
|
||||
description: "Whether the toggle is in the 'show' state (true) or 'hide' state (false)",
|
||||
},
|
||||
showMessage: {
|
||||
control: "text",
|
||||
description:
|
||||
'Message for screen readers when in hide state (default: "Show field content")',
|
||||
},
|
||||
hideMessage: {
|
||||
control: "text",
|
||||
description:
|
||||
'Message for screen readers when in show state (default: "Hide field content")',
|
||||
},
|
||||
disabled: {
|
||||
control: "boolean",
|
||||
description: "Whether the button should be disabled (for demo purposes)",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
type Story = StoryObj<VisibilityToggle>;
|
||||
|
||||
const Template: Story = {
|
||||
args: {
|
||||
open: false,
|
||||
showMessage: "Show field content",
|
||||
hideMessage: "Hide field content",
|
||||
},
|
||||
render: (args) => html`
|
||||
<ak-visibility-toggle
|
||||
?open=${args.open}
|
||||
show-message=${ifDefined(args.showMessage)}
|
||||
hide-message=${ifDefined(args.hideMessage)}
|
||||
@click=${(e: Event) => {
|
||||
const target = e.target as VisibilityToggle;
|
||||
target.open = !target.open;
|
||||
// In a real application, you would also toggle the visibility
|
||||
// of the associated content here
|
||||
}}
|
||||
></ak-visibility-toggle>
|
||||
`,
|
||||
};
|
||||
|
||||
// Password field integration example
|
||||
export const PasswordFieldExample: Story = {
|
||||
args: {
|
||||
showMessage: "Reveal password",
|
||||
hideMessage: "Conceal password",
|
||||
},
|
||||
render: () => {
|
||||
let isVisible = false;
|
||||
|
||||
const toggleVisibility = (e: Event) => {
|
||||
isVisible = !isVisible;
|
||||
const toggle = e.target as VisibilityToggle;
|
||||
const passwordField = document.querySelector("#demo-password") as HTMLInputElement;
|
||||
|
||||
toggle.open = isVisible;
|
||||
if (passwordField) {
|
||||
passwordField.type = isVisible ? "text" : "password";
|
||||
}
|
||||
};
|
||||
|
||||
return html`
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem; max-width: 300px;">
|
||||
<label for="demo-password" style="font-weight: bold;">Password:</label>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<input
|
||||
id="demo-password"
|
||||
type="password"
|
||||
value="supersecretpassword123"
|
||||
style="flex: 1; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px;"
|
||||
readonly
|
||||
/>
|
||||
<ak-visibility-toggle
|
||||
?open=${isVisible}
|
||||
show-message="Show password"
|
||||
hide-message="Hide password"
|
||||
@click=${toggleVisibility}
|
||||
></ak-visibility-toggle>
|
||||
</div>
|
||||
<p style="font-size: 0.875rem; color: #666;">
|
||||
Click the eye icon to toggle password visibility
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
};
|
@ -16,8 +16,12 @@ import { property } from "lit/decorators.js";
|
||||
|
||||
import { UiThemeEnum } from "@goauthentik/api";
|
||||
|
||||
export interface AKElementProps {
|
||||
activeTheme: ResolvedUITheme;
|
||||
}
|
||||
|
||||
@localized()
|
||||
export class AKElement extends LitElement {
|
||||
export class AKElement extends LitElement implements AKElementProps {
|
||||
//#region Static Properties
|
||||
|
||||
public static styles?: Array<CSSResult | CSSModule>;
|
||||
|
Reference in New Issue
Block a user