web: package up horizontal elements into their own components (#7053)
* web: laying the groundwork for future expansion This commit is a hodge-podge of updates and changes to the web. Functional changes: - Makefile: Fixed a bug in the `help` section that prevented the WIDTH from being accurately calculated if `help` was included rather than in-lined. - ESLint: Modified the "unused vars" rule so that variables starting with an underline are not considered by the rule. This allows for elided variables in event handlers. It's not a perfect solution-- a better one would be to use Typescript's function-specialization typing, but there are too many places where we elide or ignore some variables in a function's usage that switching over to specialization would be a huge lift. - locale: It turns out, lit-locale does its own context management. We don't need to have a context at all in this space, and that's one less listener we need to attach t othe DOM. - ModalButton: A small thing, but using `nothing` instead of "html``" allows lit better control over rendering and reduces the number of actual renders of the page. - FormGroup: Provided a means to modify the aria-label, rather than stick with the just the word "Details." Specializing this field will both help users of screen readers in the future, and will allow test suites to find specific form groups now. - RadioButton: provide a more consistent interface to the RadioButton. First, we dispatch the events to the outside world, and we set the value locally so that the current `Form.ts` continues to behave as expected. We also prevent the "button lost value" event from propagating; this presents a unified select-like interface to users of the RadioButtonGroup. The current value semantics are preserved; other clients of the RadioButton do not see a change in behavior. - EventEmitter: If the custom event detail is *not* an object, do not use the object-like semantics for forwarding it; just send it as-is. - Comments: In the course of laying the groundwork for the application wizard, I throw a LOT of comments into the code, describing APIs, interfaces, class and function signatures, to better document the behavior inside and as signposts for future work. * web: permit arrays to be sent in custom events without interpolation. * actually use assignValue or rather serializeFieldRecursive Signed-off-by: Jens Langhammer <jens@goauthentik.io> * web: package up horizontal elements into their own components. This commit introduces a number of "components." Jens has this idiom: ``` <ak-form-element-horizontal label=${msg("Name")} name="name" ?required=${true}> <input type="text" value="${ifDefined(this.instance?.name)}" class="pf-c-form-control" required /> </ak-form-element-horizontal> ``` It's a very web-oriented idiom in that it's built out of two building blocks, the "element-horizontal" descriptor, and the input object itself. This idiom is repeated a lot throughout the code. As an alternative, let's wrap everything into an inheritable interface: ``` <ak-text-input name="name" label=${msg("Name")} value="${ifDefined(this.instance?.name)} required > </ak-text-input> ``` This preserves all the information of the above, makes it much clearer what kind of interaction we're having (sometimes the `type=` information in an input is lost or easily missed), and while it does require you know that there are provided components rather than the pair of layout-behavior as in the original it also gives the developer more precision over the look and feel of the components. *Right now* these components are placed into the LightDOM, as they are in the existing source code, because the Form handler has a need to be able to "peer into" the "element-horizontal" component to find the values of the input objects. In a future revision I hope to place the burden of type/value processing onto the input objects themselves such that the form handler will need only look for the `.value` of the associated input control. Other fixes: - update the FlowSearch() such that it actually emits an input event when its value changes. - Disable the storybook shortcuts; on Chrome, at least, they get confused with simple inputs - Fix an issue with precommit to not scan any Python with ESLint! :-) * web: provide storybook stories for the components This commit provides storybook stories for the ak-horizontal-element wrappers. A few bugs were found along the way, including one rather nasty one from Radio where we were still getting the "set/unset" pair in the wrong order, so I had to knuckle down and fix the event handler properly. * web: test oauth2 provider "guinea pig" for new components I used the Oauth2 provider page as my experiment in seeing if the horizontal-element wrappers could be used instead of the raw wrappers themselves, and I wanted to make sure a test existed that asserts that filling out THAT form in the ProvidersList and ProvidersForm didn't break anything. This commit updates the WDIO tests to do just that; the test is simple, but it does exercise the `name` field of the Provider, something not needed in the Wizard because it's set automatically based on the Application name, and it even asserts that the new Provider exists in the list of available Providers when it's done. * web: making sure ESlint and Prettier are happy * "fix" lint 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:
@ -13,7 +13,7 @@ import { Application } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-app-icon")
|
||||
export class AppIcon extends AKElement {
|
||||
@property({ attribute: false })
|
||||
@property({ type: Object, attribute: false })
|
||||
app?: Application;
|
||||
|
||||
@property()
|
||||
|
||||
66
web/src/components/ak-file-input.ts
Normal file
66
web/src/components/ak-file-input.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-file-input")
|
||||
export class AkFileInput 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
|
||||
// find the children of this component.
|
||||
//
|
||||
// TODO: This abstraction is wrong; it's putting *more* layers in as a way of managing the
|
||||
// visual clutter and legibility issues of ak-form-elemental-horizontal and patternfly in
|
||||
// general.
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@property({ type: String })
|
||||
name!: string;
|
||||
|
||||
@property({ type: String })
|
||||
label = "";
|
||||
|
||||
/*
|
||||
* The message to show next to the "current icon".
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: String })
|
||||
current = msg("Currently set to:");
|
||||
|
||||
@property({ type: String })
|
||||
value = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
required = false;
|
||||
|
||||
@property({ type: String })
|
||||
help = "";
|
||||
|
||||
@query('input[type="file"]')
|
||||
input!: HTMLInputElement;
|
||||
|
||||
get files() {
|
||||
return this.input.files;
|
||||
}
|
||||
|
||||
render() {
|
||||
const currentMsg =
|
||||
this.value && this.current
|
||||
? html` <p class="pf-c-form__helper-text">${this.current} ${this.value}</p> `
|
||||
: nothing;
|
||||
|
||||
return html`<ak-form-element-horizontal
|
||||
?required="${this.required}"
|
||||
label=${this.label}
|
||||
name=${this.name}
|
||||
>
|
||||
<input type="file" value="" class="pf-c-form-control" />
|
||||
${currentMsg}
|
||||
${this.help.trim() ? html`<p class="pf-c-form__helper-text">${this.help}</p>` : nothing}
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
}
|
||||
52
web/src/components/ak-number-input.ts
Normal file
52
web/src/components/ak-number-input.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
|
||||
import { html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
@customElement("ak-number-input")
|
||||
export class AkNumberInput 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
|
||||
// find the children of this component.
|
||||
//
|
||||
// TODO: This abstraction is wrong; it's putting *more* layers in as a way of managing the
|
||||
// visual clutter and legibility issues of ak-form-elemental-horizontal and patternfly in
|
||||
// general.
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@property({ type: String })
|
||||
name!: string;
|
||||
|
||||
@property({ type: String })
|
||||
label = "";
|
||||
|
||||
@property({ type: Number, reflect: true })
|
||||
value = 0;
|
||||
|
||||
@property({ type: Boolean })
|
||||
required = false;
|
||||
|
||||
@property({ type: String })
|
||||
help = "";
|
||||
|
||||
render() {
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${this.label}
|
||||
?required=${this.required}
|
||||
name=${this.name}
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
value=${ifDefined(this.value)}
|
||||
class="pf-c-form-control"
|
||||
?required=${this.required}
|
||||
/>
|
||||
${this.help ? html`<p class="pf-c-form__helper-text">${this.help}</p>` : nothing}
|
||||
</ak-form-element-horizontal> `;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkNumberInput;
|
||||
61
web/src/components/ak-radio-input.ts
Normal file
61
web/src/components/ak-radio-input.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { RadioOption } from "@goauthentik/elements/forms/Radio";
|
||||
import "@goauthentik/elements/forms/Radio";
|
||||
|
||||
import { html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-radio-input")
|
||||
export class AkRadioInput<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
|
||||
// find the children of this component.
|
||||
//
|
||||
// TODO: This abstraction is wrong; it's putting *more* layers in as a way of managing the
|
||||
// visual clutter and legibility issues of ak-form-elemental-horizontal and patternfly in
|
||||
// general.
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@property({ type: String })
|
||||
name!: string;
|
||||
|
||||
@property({ type: String })
|
||||
label = "";
|
||||
|
||||
@property({ type: String })
|
||||
help = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
required = false;
|
||||
|
||||
@property({ type: Object })
|
||||
value!: T;
|
||||
|
||||
@property({ type: Array })
|
||||
options: RadioOption<T>[] = [];
|
||||
|
||||
handleInput(ev: CustomEvent) {
|
||||
this.value = ev.detail.value;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${this.label}
|
||||
?required=${this.required}
|
||||
name=${this.name}
|
||||
>
|
||||
<ak-radio
|
||||
.options=${this.options}
|
||||
.value=${this.value}
|
||||
@input=${this.handleInput}
|
||||
></ak-radio>
|
||||
${this.help.trim()
|
||||
? html`<p class="pf-c-form__helper-radio">${this.help}</p>`
|
||||
: nothing}
|
||||
</ak-form-element-horizontal> `;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkRadioInput;
|
||||
171
web/src/components/ak-slug-input.ts
Normal file
171
web/src/components/ak-slug-input.ts
Normal file
@ -0,0 +1,171 @@
|
||||
import { convertToSlug } from "@goauthentik/common/utils";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
|
||||
import { TemplateResult, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
@customElement("ak-slug-input")
|
||||
export class AkSlugInput 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
|
||||
// find the children of this component.
|
||||
//
|
||||
// TODO: This abstraction is wrong; it's putting *more* layers in as a way of managing the
|
||||
// visual clutter and legibility issues of ak-form-elemental-horizontal and patternfly in
|
||||
// general.
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@property({ type: String })
|
||||
name!: string;
|
||||
|
||||
@property({ type: String })
|
||||
label = "";
|
||||
|
||||
@property({ type: String, reflect: true })
|
||||
value = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
required = false;
|
||||
|
||||
@property({ type: String })
|
||||
help = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
hidden = false;
|
||||
|
||||
@property({ type: Object })
|
||||
bighelp!: TemplateResult | TemplateResult[];
|
||||
|
||||
@property({ type: String })
|
||||
source = "";
|
||||
|
||||
origin?: HTMLInputElement | null;
|
||||
|
||||
@query("input")
|
||||
input!: HTMLInputElement;
|
||||
|
||||
touched: boolean = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.slugify = this.slugify.bind(this);
|
||||
this.handleTouch = this.handleTouch.bind(this);
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.input.addEventListener("input", this.handleTouch);
|
||||
}
|
||||
|
||||
renderHelp() {
|
||||
return [
|
||||
this.help ? html`<p class="pf-c-form__helper-text">${this.help}</p>` : nothing,
|
||||
this.bighelp ? this.bighelp : nothing,
|
||||
];
|
||||
}
|
||||
|
||||
// Do not stop propagation of this event; it must be sent up the tree so that a parent
|
||||
// component, such as a custom forms manager, may receive it.
|
||||
handleTouch(ev: Event) {
|
||||
this.input.value = convertToSlug(this.input.value);
|
||||
this.value = this.input.value;
|
||||
|
||||
if (this.origin && this.origin.value === "" && this.input.value === "") {
|
||||
this.touched = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev && ev.target && ev.target instanceof HTMLInputElement) {
|
||||
this.touched = true;
|
||||
}
|
||||
}
|
||||
|
||||
slugify(ev: Event) {
|
||||
if (!(ev && ev.target && ev.target instanceof HTMLInputElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset 'touched' status if the slug & target have been reset
|
||||
if (ev.target.value === "" && this.input.value === "") {
|
||||
this.touched = false;
|
||||
}
|
||||
|
||||
// Don't proceed if the user has hand-modified the slug
|
||||
if (this.touched) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A very primitive heuristic: if the previous iteration of the slug and the current
|
||||
// iteration are *similar enough*, set the input value. "Similar enough" here is defined as
|
||||
// "any event which adds or removes a character but leaves the rest of the slug looking like
|
||||
// the previous iteration, set it to the current iteration."
|
||||
|
||||
const newSlug = convertToSlug(ev.target.value);
|
||||
const oldSlug = this.input.value;
|
||||
const [shorter, longer] =
|
||||
newSlug.length < oldSlug.length ? [newSlug, oldSlug] : [oldSlug, newSlug];
|
||||
|
||||
if (longer.substring(0, shorter.length) !== shorter) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The browser, as a security measure, sets the originating HTML object to be the
|
||||
// target; developers cannot change it. In order to provide a meaningful value
|
||||
// to listeners, both the name and value of the host must match those of the target
|
||||
// input. The name is already handled since it's both required and automatically
|
||||
// forwarded to our templated input, but the value must also be set.
|
||||
|
||||
this.value = this.input.value = newSlug;
|
||||
this.dispatchEvent(
|
||||
new Event("input", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
// Set up listener on source element, so we can slugify the content.
|
||||
setTimeout(() => {
|
||||
if (this.source) {
|
||||
const rootNode = this.getRootNode();
|
||||
if (rootNode instanceof ShadowRoot || rootNode instanceof Document) {
|
||||
this.origin = rootNode.querySelector(this.source);
|
||||
}
|
||||
if (this.origin) {
|
||||
this.origin.addEventListener("input", this.slugify);
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.origin) {
|
||||
this.origin.removeEventListener("input", this.slugify);
|
||||
}
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${this.label}
|
||||
?required=${this.required}
|
||||
?hidden=${this.hidden}
|
||||
name=${this.name}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value=${ifDefined(this.value)}
|
||||
class="pf-c-form-control"
|
||||
?required=${this.required}
|
||||
/>
|
||||
${this.renderHelp()}
|
||||
</ak-form-element-horizontal> `;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkSlugInput;
|
||||
55
web/src/components/ak-switch-input.ts
Normal file
55
web/src/components/ak-switch-input.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
|
||||
import { html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-switch-input")
|
||||
export class AkSwitchInput 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
|
||||
// find the children of this component.
|
||||
//
|
||||
// TODO: This abstraction is wrong; it's putting *more* layers in as a way of managing the
|
||||
// visual clutter and legibility issues of ak-form-elemental-horizontal and patternfly in
|
||||
// general.
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@property({ type: String })
|
||||
name!: string;
|
||||
|
||||
@property({ type: String })
|
||||
label = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
checked: boolean = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
required = false;
|
||||
|
||||
@property({ type: String })
|
||||
help = "";
|
||||
|
||||
@query("input.pf-c-switch__input[type=checkbox]")
|
||||
checkbox!: HTMLInputElement;
|
||||
|
||||
render() {
|
||||
const doCheck = this.checked ? this.checked : undefined;
|
||||
|
||||
return html` <ak-form-element-horizontal name=${this.name} ?required=${this.required}>
|
||||
<label class="pf-c-switch">
|
||||
<input class="pf-c-switch__input" type="checkbox" ?checked=${doCheck} />
|
||||
<span class="pf-c-switch__toggle">
|
||||
<span class="pf-c-switch__toggle-icon">
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span class="pf-c-switch__label">${this.label}</span>
|
||||
</label>
|
||||
${this.help.trim() ? html`<p class="pf-c-form__helper-text">${this.help}</p>` : nothing}
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkSwitchInput;
|
||||
66
web/src/components/ak-text-input.ts
Normal file
66
web/src/components/ak-text-input.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
|
||||
import { TemplateResult, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
@customElement("ak-text-input")
|
||||
export class AkTextInput 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
|
||||
// find the children of this component.
|
||||
//
|
||||
// TODO: This abstraction is wrong; it's putting *more* layers in as a way of managing the
|
||||
// visual clutter and legibility issues of ak-form-elemental-horizontal and patternfly in
|
||||
// general.
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@property({ type: String })
|
||||
name!: string;
|
||||
|
||||
@property({ type: String })
|
||||
label = "";
|
||||
|
||||
@property({ type: String, reflect: true })
|
||||
value = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
required = false;
|
||||
|
||||
@property({ type: String })
|
||||
help = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
hidden = false;
|
||||
|
||||
@property({ type: Object })
|
||||
bighelp!: TemplateResult | TemplateResult[];
|
||||
|
||||
renderHelp() {
|
||||
return [
|
||||
this.help ? html`<p class="pf-c-form__helper-text">${this.help}</p>` : nothing,
|
||||
this.bighelp ? this.bighelp : nothing,
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${this.label}
|
||||
?required=${this.required}
|
||||
?hidden=${this.hidden}
|
||||
name=${this.name}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value=${ifDefined(this.value)}
|
||||
class="pf-c-form-control"
|
||||
?required=${this.required}
|
||||
/>
|
||||
${this.renderHelp()}
|
||||
</ak-form-element-horizontal> `;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkTextInput;
|
||||
58
web/src/components/ak-textarea-input.ts
Normal file
58
web/src/components/ak-textarea-input.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
|
||||
import { TemplateResult, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-textarea-input")
|
||||
export class AkTextareaInput 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
|
||||
// find the children of this component.
|
||||
//
|
||||
// TODO: This abstraction is wrong; it's putting *more* layers in as a way of managing the
|
||||
// visual clutter and legibility issues of ak-form-elemental-horizontal and patternfly in
|
||||
// general.
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@property({ type: String })
|
||||
name!: string;
|
||||
|
||||
@property({ type: String })
|
||||
label = "";
|
||||
|
||||
@property({ type: String })
|
||||
value = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
required = false;
|
||||
|
||||
@property({ type: String })
|
||||
help = "";
|
||||
|
||||
@property({ type: Object })
|
||||
bighelp!: TemplateResult | TemplateResult[];
|
||||
|
||||
renderHelp() {
|
||||
return [
|
||||
this.help ? html`<p class="pf-c-form__helper-textarea">${this.help}</p>` : nothing,
|
||||
this.bighelp ? this.bighelp : nothing,
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${this.label}
|
||||
?required=${this.required}
|
||||
name=${this.name}
|
||||
>
|
||||
<textarea class="pf-c-form-control" ?required=${this.required} name=${this.name}>
|
||||
${this.value !== undefined ? this.value : ""}</textarea
|
||||
>
|
||||
${this.renderHelp()}
|
||||
</ak-form-element-horizontal> `;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkTextareaInput;
|
||||
38
web/src/components/stories/ak-app-icon.stories.ts
Normal file
38
web/src/components/stories/ak-app-icon.stories.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import "@goauthentik/elements/messages/MessageContainer";
|
||||
import { Meta } from "@storybook/web-components";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
import "../ak-app-icon";
|
||||
import AkAppIcon from "../ak-app-icon";
|
||||
|
||||
const metadata: Meta<AkAppIcon> = {
|
||||
title: "Components / App Icon",
|
||||
component: "ak-app-icon",
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: "A small card displaying an application icon",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
const container = (testItem: TemplateResult) =>
|
||||
html` <div style="background: #000; padding: 2em">
|
||||
<style>
|
||||
li {
|
||||
display: block;
|
||||
}
|
||||
p {
|
||||
margin-top: 1em;
|
||||
}
|
||||
</style>
|
||||
${testItem}
|
||||
</div>`;
|
||||
|
||||
export const AppIcon = () => {
|
||||
return container(html`<ak-app-icon .app=${{ name: "Demo app" }} size="pf-m-md"></ak-app-icon>`);
|
||||
};
|
||||
55
web/src/components/stories/ak-number-input.stories.ts
Normal file
55
web/src/components/stories/ak-number-input.stories.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import "@goauthentik/elements/messages/MessageContainer";
|
||||
import { Meta } from "@storybook/web-components";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
import "../ak-number-input";
|
||||
import AkNumberInput from "../ak-number-input";
|
||||
|
||||
const metadata: Meta<AkNumberInput> = {
|
||||
title: "Components / Number Input",
|
||||
component: "ak-number-input",
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: "A stylized value control for number input",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
const container = (testItem: TemplateResult) =>
|
||||
html` <div style="background: #000; padding: 2em">
|
||||
<style>
|
||||
li {
|
||||
display: block;
|
||||
}
|
||||
p {
|
||||
margin-top: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
${testItem}
|
||||
|
||||
<ul id="number-message-pad" style="color: #fff; margin-top: 1em"></ul>
|
||||
</div>`;
|
||||
|
||||
export const NumberInput = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const displayChange = (ev: any) => {
|
||||
document.getElementById(
|
||||
"number-message-pad",
|
||||
)!.innerText = `Value selected: ${JSON.stringify(ev.target.value, null, 2)}`;
|
||||
};
|
||||
|
||||
return container(
|
||||
html`<ak-number-input
|
||||
@input=${displayChange}
|
||||
label="Test Number Input"
|
||||
name="ak-test-number-input"
|
||||
help="This is where you would read the help messages"
|
||||
></ak-number-input>`,
|
||||
);
|
||||
};
|
||||
67
web/src/components/stories/ak-radio-input.stories.ts
Normal file
67
web/src/components/stories/ak-radio-input.stories.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import "@goauthentik/elements/messages/MessageContainer";
|
||||
import { Meta } from "@storybook/web-components";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
import "../ak-radio-input";
|
||||
import AkRadioInput from "../ak-radio-input";
|
||||
|
||||
const metadata: Meta<AkRadioInput<Record<string, number>>> = {
|
||||
title: "Components / Radio Input",
|
||||
component: "ak-radio-input",
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: "A stylized value control for radio buttons",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
const container = (testItem: TemplateResult) =>
|
||||
html` <div style="background: #fff; padding: 2em">
|
||||
<style>
|
||||
li {
|
||||
display: block;
|
||||
}
|
||||
p {
|
||||
margin-top: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
${testItem}
|
||||
|
||||
<ul id="radio-message-pad" style="margin-top: 1em"></ul>
|
||||
</div>`;
|
||||
|
||||
const testOptions = [
|
||||
{ label: "Option One", description: html`This is option one.`, value: { funky: 1 } },
|
||||
{ label: "Option Two", description: html`This is option two.`, value: { invalid: 2 } },
|
||||
{ label: "Option Three", description: html`This is option three.`, value: { weird: 3 } },
|
||||
];
|
||||
|
||||
export const RadioInput = () => {
|
||||
const result = "";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const displayChange = (ev: any) => {
|
||||
document.getElementById("radio-message-pad")!.innerText = `Value selected: ${JSON.stringify(
|
||||
ev.target.value,
|
||||
null,
|
||||
2,
|
||||
)}`;
|
||||
};
|
||||
|
||||
return container(
|
||||
html`<ak-radio-input
|
||||
@input=${displayChange}
|
||||
label="Test Radio Button"
|
||||
name="ak-test-radio-input"
|
||||
help="This is where you would read the help messages"
|
||||
.options=${testOptions}
|
||||
></ak-radio-input>
|
||||
<div>${result}</div>`,
|
||||
);
|
||||
};
|
||||
64
web/src/components/stories/ak-slug-input.stories.ts
Normal file
64
web/src/components/stories/ak-slug-input.stories.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import "@goauthentik/elements/messages/MessageContainer";
|
||||
import { Meta } from "@storybook/web-components";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
import "../ak-slug-input";
|
||||
import AkSlugInput from "../ak-slug-input";
|
||||
import "../ak-text-input";
|
||||
|
||||
const metadata: Meta<AkSlugInput> = {
|
||||
title: "Components / Slug Input",
|
||||
component: "ak-slug-input",
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: "A stylized value control for slug input",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
const container = (testItem: TemplateResult) =>
|
||||
html` <div style="background: #000; padding: 2em">
|
||||
<style>
|
||||
li {
|
||||
display: block;
|
||||
}
|
||||
p {
|
||||
margin-top: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
${testItem}
|
||||
|
||||
<ul id="text-message-pad" style="color: #fff; margin-top: 1em"></ul>
|
||||
</div>`;
|
||||
|
||||
export const SlugInput = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const displayChange = (ev: any) => {
|
||||
document.getElementById("text-message-pad")!.innerText = `Value selected: ${JSON.stringify(
|
||||
ev.target.value,
|
||||
null,
|
||||
2,
|
||||
)}`;
|
||||
};
|
||||
|
||||
return container(
|
||||
html`<ak-text-input
|
||||
label="Test Text Input"
|
||||
name="ak-test-text-input"
|
||||
help="Type your input here"
|
||||
></ak-text-input>
|
||||
<ak-slug-input
|
||||
@input=${displayChange}
|
||||
source="ak-text-input[name=ak-test-text-input]"
|
||||
label="Test Text Input"
|
||||
name="ak-test-text-input"
|
||||
help="Here should be the slugified version"
|
||||
></ak-slug-input> `,
|
||||
);
|
||||
};
|
||||
63
web/src/components/stories/ak-switch-input.stories.ts
Normal file
63
web/src/components/stories/ak-switch-input.stories.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import "@goauthentik/elements/messages/MessageContainer";
|
||||
import { Meta } from "@storybook/web-components";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
// Necessary because we're NOT supplying the CSS for the interiors
|
||||
// in our "light" dom.
|
||||
import PFSwitch from "@patternfly/patternfly/components/Switch/switch.css";
|
||||
|
||||
import "../ak-switch-input";
|
||||
import AkSwitchInput from "../ak-switch-input";
|
||||
|
||||
const metadata: Meta<AkSwitchInput> = {
|
||||
title: "Components / Switch Input",
|
||||
component: "ak-switch-input",
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: "A stylized value control for a switch-like toggle",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
const container = (testItem: TemplateResult) =>
|
||||
html` <div style="background: #fff; padding: 2em">
|
||||
<style>
|
||||
li {
|
||||
display: block;
|
||||
}
|
||||
p {
|
||||
margin-top: 1em;
|
||||
}
|
||||
${PFSwitch};
|
||||
</style>
|
||||
|
||||
${testItem}
|
||||
|
||||
<ul id="switch-message-pad" style="margin-top: 1em"></ul>
|
||||
</div>`;
|
||||
|
||||
export const SwitchInput = () => {
|
||||
const result = "";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const displayChange = (ev: any) => {
|
||||
document.getElementById(
|
||||
"switch-message-pad",
|
||||
)!.innerText = `Value selected: ${JSON.stringify(ev.target.checked, null, 2)}`;
|
||||
};
|
||||
|
||||
return container(
|
||||
html`<ak-switch-input
|
||||
@input=${displayChange}
|
||||
name="ak-test-switch-input"
|
||||
label="Test Switch Toggle"
|
||||
help="This is where you would read the help messages"
|
||||
></ak-switch-input>
|
||||
<div>${result}</div>`,
|
||||
);
|
||||
};
|
||||
57
web/src/components/stories/ak-text-input.stories.ts
Normal file
57
web/src/components/stories/ak-text-input.stories.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import "@goauthentik/elements/messages/MessageContainer";
|
||||
import { Meta } from "@storybook/web-components";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
import "../ak-text-input";
|
||||
import AkTextInput from "../ak-text-input";
|
||||
|
||||
const metadata: Meta<AkTextInput> = {
|
||||
title: "Components / Text Input",
|
||||
component: "ak-text-input",
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: "A stylized value control for text input",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
const container = (testItem: TemplateResult) =>
|
||||
html` <div style="background: #000; padding: 2em">
|
||||
<style>
|
||||
li {
|
||||
display: block;
|
||||
}
|
||||
p {
|
||||
margin-top: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
${testItem}
|
||||
|
||||
<ul id="text-message-pad" style="color: #fff; margin-top: 1em"></ul>
|
||||
</div>`;
|
||||
|
||||
export const TextInput = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const displayChange = (ev: any) => {
|
||||
document.getElementById("text-message-pad")!.innerText = `Value selected: ${JSON.stringify(
|
||||
ev.target.value,
|
||||
null,
|
||||
2,
|
||||
)}`;
|
||||
};
|
||||
|
||||
return container(
|
||||
html`<ak-text-input
|
||||
@input=${displayChange}
|
||||
label="Test Text Input"
|
||||
name="ak-test-text-input"
|
||||
help="This is where you would read the help messages"
|
||||
></ak-text-input>`,
|
||||
);
|
||||
};
|
||||
55
web/src/components/stories/ak-textarea-input.stories.ts
Normal file
55
web/src/components/stories/ak-textarea-input.stories.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import "@goauthentik/elements/messages/MessageContainer";
|
||||
import { Meta } from "@storybook/web-components";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
import "../ak-textarea-input";
|
||||
import AkTextareaInput from "../ak-textarea-input";
|
||||
|
||||
const metadata: Meta<AkTextareaInput> = {
|
||||
title: "Components / Textarea Input",
|
||||
component: "ak-textarea-input",
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: "A stylized value control for textarea input",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
const container = (testItem: TemplateResult) =>
|
||||
html` <div style="background: #000; padding: 2em">
|
||||
<style>
|
||||
li {
|
||||
display: block;
|
||||
}
|
||||
p {
|
||||
margin-top: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
${testItem}
|
||||
|
||||
<ul id="textarea-message-pad" style="color: #fff; margin-top: 1em"></ul>
|
||||
</div>`;
|
||||
|
||||
export const TextareaInput = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const displayChange = (ev: any) => {
|
||||
document.getElementById(
|
||||
"textarea-message-pad",
|
||||
)!.innerText = `Value selected: ${JSON.stringify(ev.target.value, null, 2)}`;
|
||||
};
|
||||
|
||||
return container(
|
||||
html`<ak-textarea-input
|
||||
@input=${displayChange}
|
||||
label="Test Textarea Input"
|
||||
name="ak-test-textarea-input"
|
||||
help="This is where you would read the help messages"
|
||||
></ak-textarea-input>`,
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user