web: Testing and documenting the simple things
This commit adds unit tests for the Alerts and EmptyState elements. It includes a new test/documentation feature; elements can now be fully documented with text description and active controls. It *removes* the `babel` imports entirely. Either we don't need them, or the components that do need them are importing them automatically. [An outstanding bug in WebDriverIO](https://github.com/webdriverio/webdriverio/issues/12056) unfortunately means that the tests cannot be run in parallel for the time being.While one test is running, the compiler for other tests becomes unreliable. They're currently working on this issue. I have set the `maxInstances` to **1**. I have updated the `<ak-alert>` component just a bit, providing an attribute alternative to the `Level` property; now instead of passing it a `<ak-alert level=${Levels.Warning}>` properties, you can just say `<ak-alert warning>` and it'll work just fine. The old way is still the default behavior. The default behavior for `EmptyState` was a little confusing; I've re-arranged it for clarity. Since I touched it, I also added the `interface` and `HTMLElementTagNameMap` declarations. Added documentation to all the elements I've touched (so far).
This commit is contained in:
@ -1,11 +1,20 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
|
||||
import { CSSResult, TemplateResult, html } from "lit";
|
||||
import { html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
|
||||
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
export interface IAlert {
|
||||
inline?: boolean;
|
||||
warning?: boolean;
|
||||
info?: boolean;
|
||||
success?: boolean;
|
||||
danger?: boolean;
|
||||
}
|
||||
|
||||
export enum Level {
|
||||
Warning = "pf-m-warning",
|
||||
Info = "pf-m-info",
|
||||
@ -13,20 +22,88 @@ export enum Level {
|
||||
Danger = "pf-m-danger",
|
||||
}
|
||||
|
||||
/**
|
||||
* @class Alert
|
||||
* @element ak-alert
|
||||
*
|
||||
* Alerts are in-page elements intended to draw the user's attention and alert them to important
|
||||
* details. Alerts are used alongside form elements to warn users of potential mistakes they can
|
||||
* make, as well as in in-line documentation.
|
||||
*/
|
||||
@customElement("ak-alert")
|
||||
export class Alert extends AKElement {
|
||||
export class Alert extends AKElement implements IAlert {
|
||||
/**
|
||||
* Whether or not to display the entire component's contents in-line or not.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean })
|
||||
inline = false;
|
||||
|
||||
/**
|
||||
* Fallback method of determining severity
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property()
|
||||
level: Level = Level.Warning;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
/**
|
||||
* Highest severity level.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean })
|
||||
danger = false;
|
||||
|
||||
/**
|
||||
* Next severity level.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean })
|
||||
warning = false;
|
||||
|
||||
/**
|
||||
* Next severity level. The default severity level.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean })
|
||||
success = false;
|
||||
|
||||
/**
|
||||
* Lowest severity level.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean })
|
||||
info = false;
|
||||
|
||||
static get styles() {
|
||||
return [PFBase, PFAlert];
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<div class="pf-c-alert ${this.inline ? "pf-m-inline" : ""} ${this.level}">
|
||||
get classmap() {
|
||||
const leveltags = ["danger", "warning", "success", "info"].filter(
|
||||
// @ts-ignore
|
||||
(level) => this[level] && this[level] === true,
|
||||
);
|
||||
|
||||
if (leveltags.length > 1) {
|
||||
console.warn("ak-alert has multiple levels defined");
|
||||
}
|
||||
const level = leveltags.length > 0 ? `pf-m-${leveltags[0]}` : this.level;
|
||||
|
||||
return {
|
||||
"pf-c-alert": true,
|
||||
"pf-m-inline": this.inline,
|
||||
[level]: true,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<div class="${classMap(this.classmap)}">
|
||||
<div class="pf-c-alert__icon">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
</div>
|
||||
@ -36,3 +113,9 @@ export class Alert extends AKElement {
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-alert": Alert;
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,14 @@ import { customElement } from "lit/decorators.js";
|
||||
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
/**
|
||||
* @class Divider
|
||||
* @element ak-divider
|
||||
*
|
||||
* Divider is a horizontal rule, an in-page element to separate displayed items.
|
||||
*
|
||||
* @slot - HTML to display in-line in the middle of the horizontal rule.
|
||||
*/
|
||||
@customElement("ak-divider")
|
||||
export class Divider extends AKElement {
|
||||
static get styles(): CSSResult[] {
|
||||
@ -39,3 +47,9 @@ export class Divider extends AKElement {
|
||||
return html`<div class="separator"><slot></slot></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-divider": Divider;
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,17 +9,64 @@ import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-sta
|
||||
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
@customElement("ak-empty-state")
|
||||
export class EmptyState extends AKElement {
|
||||
@property({ type: String })
|
||||
icon = "";
|
||||
export interface IEmptyState {
|
||||
icon?: string;
|
||||
loading?: boolean;
|
||||
fullHeight?: boolean;
|
||||
header?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @class EmptyState
|
||||
* @element ak-empty-state
|
||||
*
|
||||
* The EmptyState is an in-page element to indicate that something is either loading or unavailable.
|
||||
* When "loading" is true it displays a spinner, otherwise it displays a static icon. The default
|
||||
* icon is a question mark in a circle.
|
||||
*
|
||||
* @slot body - Optional low-priority text that appears beneath the state indicator.
|
||||
* @slot primary - Optional high-priority text that appears some distance between the state indicator.
|
||||
*
|
||||
* The layout of the component is always centered, and from top to bottom:
|
||||
*
|
||||
* ```
|
||||
* icon or spinner
|
||||
* header
|
||||
* body
|
||||
* primary
|
||||
* ```
|
||||
*/
|
||||
@customElement("ak-empty-state")
|
||||
export class EmptyState extends AKElement implements IEmptyState {
|
||||
/**
|
||||
* The Font Awesome icon to display. Defaults to the <20> symbol.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: String })
|
||||
icon = "fa-question-circle";
|
||||
|
||||
/**
|
||||
* Whether or not to show the spinner, or the end icon
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean })
|
||||
loading = false;
|
||||
|
||||
/**
|
||||
* If set, will attempt to occupy the full viewport.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean })
|
||||
fullHeight = false;
|
||||
|
||||
/**
|
||||
* [Optional] If set, will display a message in large text beneath the icon
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property()
|
||||
header = "";
|
||||
|
||||
@ -45,8 +92,7 @@ export class EmptyState extends AKElement {
|
||||
<ak-spinner size=${PFSize.XLarge}></ak-spinner>
|
||||
</div>`
|
||||
: html`<i
|
||||
class="pf-icon fa ${this.icon ||
|
||||
"fa-question-circle"} pf-c-empty-state__icon"
|
||||
class="pf-icon fa ${this.icon} pf-c-empty-state__icon"
|
||||
aria-hidden="true"
|
||||
></i>`}
|
||||
<h1 class="pf-c-title pf-m-lg">${this.header}</h1>
|
||||
@ -60,3 +106,9 @@ export class EmptyState extends AKElement {
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-empty-state": EmptyState;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,24 +1,53 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
import { css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFExpandableSection from "@patternfly/patternfly/components/ExpandableSection/expandable-section.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
export interface IExpand {
|
||||
expanded?: boolean;
|
||||
textOpen?: string;
|
||||
textClosed?: string;
|
||||
}
|
||||
/**
|
||||
* @class Expand
|
||||
* @element ak-expand
|
||||
*
|
||||
* An `ak-expand` is used to hide cluttering details that a user may wish to reveal, such as the raw
|
||||
* details of an alert or event.
|
||||
*
|
||||
* slot - The contents to be hidden or displayed.
|
||||
*/
|
||||
@customElement("ak-expand")
|
||||
export class Expand extends AKElement {
|
||||
@property({ type: Boolean })
|
||||
/**
|
||||
* The state of the expanded content
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true })
|
||||
expanded = false;
|
||||
|
||||
/**
|
||||
* The text to display next to the open/close control when the accordion is closed.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property()
|
||||
textOpen = msg("Show less");
|
||||
|
||||
/**
|
||||
* The text to display next to the open/close control when the accordion is .
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property()
|
||||
textClosed = msg("Show more");
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
static get styles() {
|
||||
return [
|
||||
PFBase,
|
||||
PFExpandableSection,
|
||||
@ -30,7 +59,7 @@ export class Expand extends AKElement {
|
||||
];
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
render() {
|
||||
return html`<div
|
||||
class="pf-c-expandable-section pf-m-display-lg pf-m-indented ${this.expanded
|
||||
? "pf-m-expanded"
|
||||
@ -57,3 +86,9 @@ export class Expand extends AKElement {
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-expand": Expand;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,9 +4,9 @@ import { Meta } from "@storybook/web-components";
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
import "./ak-action-button";
|
||||
import AKActionButton from "./ak-action-button";
|
||||
import { ActionButton } from "./ak-action-button";
|
||||
|
||||
const metadata: Meta<AKActionButton> = {
|
||||
const metadata: Meta<ActionButton> = {
|
||||
title: "Elements / Action Button",
|
||||
component: "ak-action-button",
|
||||
parameters: {
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
import { MessageLevel } from "@goauthentik/common/messages";
|
||||
import { BaseTaskButton } from "@goauthentik/elements/buttons/SpinnerButton/BaseTaskButton";
|
||||
import {
|
||||
BaseTaskButton,
|
||||
type IBaseTaskButton,
|
||||
} from "@goauthentik/elements/buttons/SpinnerButton/BaseTaskButton";
|
||||
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
|
||||
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
type IActionButton = IBaseTaskButton & { apiRequest: () => Promise<unknown> };
|
||||
|
||||
/**
|
||||
* A button associated with an event handler for loading data. Takes an asynchronous function as its
|
||||
* only property.
|
||||
@ -19,7 +24,7 @@ import { customElement, property } from "lit/decorators.js";
|
||||
*/
|
||||
|
||||
@customElement("ak-action-button")
|
||||
export class ActionButton extends BaseTaskButton {
|
||||
export class ActionButton extends BaseTaskButton implements IActionButton {
|
||||
/**
|
||||
* The command to run when the button is pressed. Must return a promise. If the promise is a
|
||||
* reject or throw, we process the content of the promise and deliver it to the Notification
|
||||
@ -27,7 +32,6 @@ export class ActionButton extends BaseTaskButton {
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
|
||||
@property({ attribute: false })
|
||||
apiRequest: () => Promise<unknown> = () => {
|
||||
throw new Error();
|
||||
@ -52,4 +56,10 @@ export class ActionButton extends BaseTaskButton {
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-action-button": ActionButton;
|
||||
}
|
||||
}
|
||||
|
||||
export default ActionButton;
|
||||
|
||||
@ -36,6 +36,10 @@ const StatusMap = new Map<TaskStatus, string>([
|
||||
|
||||
const SPINNER_TIMEOUT = 1000 * 1.5; // milliseconds
|
||||
|
||||
export interface IBaseTaskButton {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* BaseTaskButton
|
||||
*
|
||||
@ -46,7 +50,6 @@ const SPINNER_TIMEOUT = 1000 * 1.5; // milliseconds
|
||||
* `onFailure` call their `super.` equivalents.
|
||||
*
|
||||
*/
|
||||
|
||||
export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) {
|
||||
eventPrefix = "ak-button";
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { property } from "lit/decorators.js";
|
||||
|
||||
import { BaseTaskButton } from "./BaseTaskButton";
|
||||
import { BaseTaskButton, type IBaseTaskButton } from "./BaseTaskButton";
|
||||
|
||||
/**
|
||||
* A button associated with an event handler for loading data. Takes an asynchronous function as its
|
||||
@ -17,8 +17,10 @@ import { BaseTaskButton } from "./BaseTaskButton";
|
||||
* @fires ak-button-reset - When the button is reset after the async process completes
|
||||
*/
|
||||
|
||||
type ISpinnerButton = IBaseTaskButton & { callAction: () => Promise<unknown> };
|
||||
|
||||
@customElement("ak-spinner-button")
|
||||
export class SpinnerButton extends BaseTaskButton {
|
||||
export class SpinnerButton extends BaseTaskButton implements ISpinnerButton {
|
||||
/**
|
||||
* The command to run when the button is pressed. Must return a promise. We don't do anything
|
||||
* with that promise other than check if it's a resolve or reject, and rethrow the event after.
|
||||
@ -29,4 +31,10 @@ export class SpinnerButton extends BaseTaskButton {
|
||||
callAction!: () => Promise<unknown>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-spinner-button": SpinnerButton;
|
||||
}
|
||||
}
|
||||
|
||||
export default SpinnerButton;
|
||||
|
||||
43
web/src/elements/stories/Alert.docs.mdx
Normal file
43
web/src/elements/stories/Alert.docs.mdx
Normal file
@ -0,0 +1,43 @@
|
||||
import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks";
|
||||
|
||||
import * as AlertStories from "./Alert.stories";
|
||||
|
||||
<Meta of={AlertStories} />
|
||||
|
||||
# Alerts
|
||||
|
||||
Alerts are in-page elements intended to draw the user's attention and alert them to important
|
||||
details. Alerts are used alongside form elements to warn users of potential mistakes they can
|
||||
make, as well as in in-line documentation.
|
||||
|
||||
## Usage
|
||||
|
||||
```Typescript
|
||||
import "@goauthentik/elements/Alert.js";
|
||||
```
|
||||
|
||||
Note that the content of an alert _must_ be a valid HTML component; plain text does not work here.
|
||||
|
||||
```html
|
||||
<ak-alert><p>This is the content of your alert!</p></ak-alert>
|
||||
```
|
||||
|
||||
## Demo
|
||||
|
||||
### Default
|
||||
|
||||
The default state of an alert is _warning_.
|
||||
|
||||
<Story of={AlertStories.DefaultStory} />
|
||||
|
||||
### Info
|
||||
|
||||
<Story of={AlertStories.InfoAlert} />
|
||||
|
||||
### Success
|
||||
|
||||
<Story of={AlertStories.SuccessAlert} />
|
||||
|
||||
### Danger
|
||||
|
||||
<Story of={AlertStories.DangerAlert} />
|
||||
77
web/src/elements/stories/Alert.stories.ts
Normal file
77
web/src/elements/stories/Alert.stories.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import type { Meta, StoryObj } from "@storybook/web-components";
|
||||
|
||||
import { html } from "lit";
|
||||
|
||||
import { Alert, type IAlert } from "../Alert.js";
|
||||
import "../Alert.js";
|
||||
|
||||
type IAlertForTesting = IAlert & { message: string };
|
||||
|
||||
const metadata: Meta<Alert> = {
|
||||
title: "Elements/<ak-alert>",
|
||||
component: "ak-alert",
|
||||
parameters: {
|
||||
docs: {
|
||||
description: "An alert",
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
inline: { control: "boolean" },
|
||||
warning: { control: "boolean" },
|
||||
info: { control: "boolean" },
|
||||
success: { control: "boolean" },
|
||||
danger: { control: "boolean" },
|
||||
// @ts-ignore
|
||||
message: { control: "text" },
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
export const DefaultStory: StoryObj = {
|
||||
args: {
|
||||
inline: false,
|
||||
warning: false,
|
||||
info: false,
|
||||
success: false,
|
||||
danger: false,
|
||||
message: "You should be alarmed.",
|
||||
},
|
||||
|
||||
// @ts-ignore
|
||||
render: ({ inline, warning, info, success, danger, message }: IAlertForTesting) => {
|
||||
return html` <div style="background-color: #f0f0f0; padding: 1rem;">
|
||||
<style>
|
||||
ak-alert {
|
||||
display: inline-block;
|
||||
width: 32rem;
|
||||
max-width: 32rem;
|
||||
}
|
||||
</style>
|
||||
<ak-alert
|
||||
?inline=${inline}
|
||||
?warning=${warning}
|
||||
?info=${info}
|
||||
?success=${success}
|
||||
?danger=${danger}
|
||||
>
|
||||
<p>${message}</p>
|
||||
</ak-alert>
|
||||
</div>`;
|
||||
},
|
||||
};
|
||||
|
||||
export const SuccessAlert = {
|
||||
...DefaultStory,
|
||||
args: { ...DefaultStory, ...{ success: true, message: "He's a tribute to your genius!" } },
|
||||
};
|
||||
|
||||
export const InfoAlert = {
|
||||
...DefaultStory,
|
||||
args: { ...DefaultStory, ...{ info: true, message: "An octopus has tastebuds on its arms." } },
|
||||
};
|
||||
|
||||
export const DangerAlert = {
|
||||
...DefaultStory,
|
||||
args: { ...DefaultStory, ...{ danger: true, message: "Danger, Will Robinson! Danger!" } },
|
||||
};
|
||||
36
web/src/elements/stories/Alert.test.ts
Normal file
36
web/src/elements/stories/Alert.test.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { $, expect } from "@wdio/globals";
|
||||
|
||||
import { html, render } from "lit";
|
||||
|
||||
import "../Alert.js";
|
||||
import { Level } from "../Alert.js";
|
||||
|
||||
describe("ak-alert", () => {
|
||||
it("should render an alert with the enum", async () => {
|
||||
render(html`<ak-alert level=${Level.Info}>This is an alert</ak-alert>`, document.body);
|
||||
|
||||
await expect(await $("ak-alert").$(">>>div")).not.toHaveElementClass("pf-m-inline");
|
||||
await expect(await $("ak-alert").$(">>>div")).toHaveElementClass("pf-c-alert");
|
||||
await expect(await $("ak-alert").$(">>>div")).toHaveElementClass("pf-m-info");
|
||||
await expect(await $("ak-alert").$(">>>.pf-c-alert__title")).toHaveText("This is an alert");
|
||||
});
|
||||
|
||||
it("should render an alert with the attribute", async () => {
|
||||
render(html`<ak-alert info>This is an alert</ak-alert>`, document.body);
|
||||
await expect(await $("ak-alert").$(">>>div")).toHaveElementClass("pf-m-info");
|
||||
await expect(await $("ak-alert").$(">>>.pf-c-alert__title")).toHaveText("This is an alert");
|
||||
});
|
||||
|
||||
it("should render an alert with conflicting attributes in priority order", async () => {
|
||||
render(html`<ak-alert danger warning>This is an alert</ak-alert>`, document.body);
|
||||
await expect(await $("ak-alert").$(">>>div")).toHaveElementClass("pf-m-danger");
|
||||
await expect(await $("ak-alert").$(">>>.pf-c-alert__title")).toHaveText("This is an alert");
|
||||
});
|
||||
|
||||
it("should render an alert with an inline class and the default level", async () => {
|
||||
render(html`<ak-alert inline>This is an alert</ak-alert>`, document.body);
|
||||
await expect(await $("ak-alert").$(">>>div")).toHaveElementClass("pf-m-warning");
|
||||
await expect(await $("ak-alert").$(">>>div")).toHaveElementClass("pf-m-inline");
|
||||
await expect(await $("ak-alert").$(">>>.pf-c-alert__title")).toHaveText("This is an alert");
|
||||
});
|
||||
});
|
||||
51
web/src/elements/stories/Divider.docs.mdx
Normal file
51
web/src/elements/stories/Divider.docs.mdx
Normal file
@ -0,0 +1,51 @@
|
||||
import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks";
|
||||
|
||||
import * as DividerStories from "./Divider.stories";
|
||||
|
||||
<Meta of={DividerStories} />
|
||||
|
||||
# Divider
|
||||
|
||||
Divider is a horizontal rule, an in-page element to separate displayed items.
|
||||
|
||||
It has no configurable attributes. It does have a single unnamed slot, which is displayed in-line in
|
||||
the center of the rule. If the CSS Base in loaded into the parent context, icons defined in the base
|
||||
can be used here.
|
||||
|
||||
## Usage
|
||||
|
||||
```Typescript
|
||||
import "@goauthentik/elements/Divider.js";
|
||||
```
|
||||
|
||||
```html
|
||||
<ak-divider></ak-divider>
|
||||
```
|
||||
|
||||
With content:
|
||||
|
||||
```html
|
||||
<ak-divider><p>Your content here</p></ak-divider>
|
||||
```
|
||||
|
||||
With an icon:
|
||||
|
||||
```html
|
||||
<ak-divider><i class="fa fa-life-ring"></i></ak-divider>
|
||||
```
|
||||
|
||||
## Demo
|
||||
|
||||
Note that the Divider inherits its background from its parent component.
|
||||
|
||||
### Default Horizontal Rule
|
||||
|
||||
<Story of={DividerStories.DefaultStory} />
|
||||
|
||||
### With A Message
|
||||
|
||||
<Story of={DividerStories.DividerWithSlottedContent} />
|
||||
|
||||
### With an Icon
|
||||
|
||||
<Story of={DividerStories.DividerWithSlottedIcon} />
|
||||
41
web/src/elements/stories/Divider.stories.ts
Normal file
41
web/src/elements/stories/Divider.stories.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import type { Meta, StoryObj } from "@storybook/web-components";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
import { Divider } from "../Divider.js";
|
||||
import "../Divider.js";
|
||||
|
||||
const metadata: Meta<Divider> = {
|
||||
title: "Elements/<ak-divider>",
|
||||
component: "ak-divider",
|
||||
parameters: {
|
||||
docs: {
|
||||
description: "our most simple divider",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
const container = (content: TemplateResult) =>
|
||||
html` <div style="background-color: #f0f0f0; padding: 1rem;">
|
||||
<style>
|
||||
ak-divider {
|
||||
display: inline-block;
|
||||
width: 32rem;
|
||||
max-width: 32rem;
|
||||
}</style
|
||||
>${content}
|
||||
</div>`;
|
||||
|
||||
export const DefaultStory: StoryObj = {
|
||||
render: () => container(html` <ak-divider> </ak-divider> `),
|
||||
};
|
||||
|
||||
export const DividerWithSlottedContent: StoryObj = {
|
||||
render: () => container(html` <ak-divider><p>Time for bed!</p></ak-divider> `),
|
||||
};
|
||||
|
||||
export const DividerWithSlottedIcon: StoryObj = {
|
||||
render: () => container(html` <ak-divider><i class="fa fa-bed"></i></ak-divider> `),
|
||||
};
|
||||
33
web/src/elements/stories/Divider.test.ts
Normal file
33
web/src/elements/stories/Divider.test.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet.js";
|
||||
import { $, expect } from "@wdio/globals";
|
||||
|
||||
import { TemplateResult, html, render as litRender } from "lit";
|
||||
|
||||
import AKGlobal from "@goauthentik/common/styles/authentik.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import "../Divider.js";
|
||||
|
||||
const render = (body: TemplateResult) => {
|
||||
document.adoptedStyleSheets = [
|
||||
...document.adoptedStyleSheets,
|
||||
ensureCSSStyleSheet(PFBase),
|
||||
ensureCSSStyleSheet(AKGlobal),
|
||||
];
|
||||
return litRender(body, document.body);
|
||||
};
|
||||
|
||||
describe("ak-divider", () => {
|
||||
it("should render the divider", async () => {
|
||||
render(html`<ak-divider></ak-divider>`);
|
||||
const empty = await $("ak-divider");
|
||||
await expect(empty).toExist();
|
||||
});
|
||||
|
||||
it("should render the divider with the specified text", async () => {
|
||||
render(html`<ak-divider><span>Your Message Here</span></ak-divider>`);
|
||||
const span = await $("ak-divider").$("span");
|
||||
await expect(span).toExist();
|
||||
await expect(span).toHaveText("Your Message Here");
|
||||
});
|
||||
});
|
||||
59
web/src/elements/stories/EmptyState.docs.mdx
Normal file
59
web/src/elements/stories/EmptyState.docs.mdx
Normal file
@ -0,0 +1,59 @@
|
||||
import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks";
|
||||
|
||||
import * as EmptyStateStories from "./EmptyState.stories";
|
||||
|
||||
<Meta of={EmptyStateStories} />
|
||||
|
||||
# EmptyState
|
||||
|
||||
The EmptyState is an in-page element to indicate that something is either loading or unavailable.
|
||||
When "loading" is true it displays a spinner, otherwise it displays a static icon. The default
|
||||
icon is a question mark in a circle.
|
||||
|
||||
It has two named slots, `body` and `primary`, to communicate further details about the current state
|
||||
this element is meant to display.
|
||||
|
||||
## Usage
|
||||
|
||||
```Typescript
|
||||
import "@goauthentik/elements/EmptyState.js";
|
||||
```
|
||||
|
||||
Note that the content of an alert _must_ be a valid HTML component; plain text does not work here.
|
||||
|
||||
```html
|
||||
<ak-empty-state icon="fa-eject"
|
||||
><span slot="primary">This would display in the "primary" slot</span></ak-empty-state
|
||||
>
|
||||
```
|
||||
|
||||
## Demo
|
||||
|
||||
### Default: Loading
|
||||
|
||||
The default state is _loading_
|
||||
|
||||
<Story of={EmptyStateStories.DefaultStory} />
|
||||
|
||||
### Done
|
||||
|
||||
<Story of={EmptyStateStories.DefaultAndLoadingDone} />
|
||||
|
||||
### Alternative "Done" Icon
|
||||
|
||||
This also shows the "header" attribute filled, which is rendered in a large, dark typeface.
|
||||
|
||||
<Story of={EmptyStateStories.DoneWithAlternativeIcon} />
|
||||
|
||||
### The Body Slot Filled
|
||||
|
||||
The body content slot is rendered in a lighter typeface at default size.
|
||||
|
||||
<Story of={EmptyStateStories.WithBodySlotFilled} />
|
||||
|
||||
### The Body and Primary Slot Filled
|
||||
|
||||
The primary content is rendered in the normal dark typeface at default size. It is also spaced
|
||||
significantly below the spinner itself.
|
||||
|
||||
<Story of={EmptyStateStories.WithBodyAndPrimarySlotsFilled} />
|
||||
108
web/src/elements/stories/EmptyState.stories.ts
Normal file
108
web/src/elements/stories/EmptyState.stories.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import type { Meta, StoryObj } from "@storybook/web-components";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { EmptyState, type IEmptyState } from "../EmptyState.js";
|
||||
import "../EmptyState.js";
|
||||
|
||||
const metadata: Meta<EmptyState> = {
|
||||
title: "Elements/<ak-empty-state>",
|
||||
component: "ak-empty-state",
|
||||
parameters: {
|
||||
docs: {
|
||||
description: "Our empty state spinner",
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
icon: { control: "text" },
|
||||
loading: { control: "boolean" },
|
||||
fullHeight: { control: "boolean" },
|
||||
header: { control: "text" },
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
const container = (content: TemplateResult) =>
|
||||
html` <div style="background-color: #f0f0f0; padding: 1rem;">
|
||||
<style>
|
||||
ak-divider {
|
||||
display: inline-block;
|
||||
width: 32rem;
|
||||
max-width: 32rem;
|
||||
}</style
|
||||
>${content}
|
||||
</div>`;
|
||||
|
||||
export const DefaultStory: StoryObj = {
|
||||
args: {
|
||||
icon: undefined,
|
||||
loading: true,
|
||||
fullHeight: false,
|
||||
header: undefined,
|
||||
},
|
||||
|
||||
render: ({ icon, loading, fullHeight, header }: IEmptyState) =>
|
||||
container(
|
||||
html` <ak-empty-state
|
||||
?loading=${loading}
|
||||
?fullHeight=${fullHeight}
|
||||
icon=${ifDefined(icon)}
|
||||
header=${ifDefined(header)}
|
||||
>
|
||||
</ak-empty-state>`,
|
||||
),
|
||||
};
|
||||
|
||||
export const DefaultAndLoadingDone = {
|
||||
...DefaultStory,
|
||||
args: { ...DefaultStory, ...{ loading: false } },
|
||||
};
|
||||
|
||||
export const DoneWithAlternativeIcon = {
|
||||
...DefaultStory,
|
||||
args: {
|
||||
...DefaultStory,
|
||||
...{ loading: false, icon: "fa-space-shuttle", header: "The final frontier" },
|
||||
},
|
||||
};
|
||||
|
||||
export const WithBodySlotFilled = {
|
||||
...DefaultStory,
|
||||
args: {
|
||||
...DefaultStory,
|
||||
...{ loading: false, icon: "fa-space-shuttle", header: "The final frontier" },
|
||||
},
|
||||
render: ({ icon, loading, fullHeight, header }: IEmptyState) =>
|
||||
container(html`
|
||||
<ak-empty-state
|
||||
?loading=${loading}
|
||||
?fullHeight=${fullHeight}
|
||||
icon=${ifDefined(icon)}
|
||||
header=${ifDefined(header)}
|
||||
>
|
||||
<span slot="body">This is the body content</span>
|
||||
</ak-empty-state>
|
||||
`),
|
||||
};
|
||||
|
||||
export const WithBodyAndPrimarySlotsFilled = {
|
||||
...DefaultStory,
|
||||
args: {
|
||||
...DefaultStory,
|
||||
...{ loading: false, icon: "fa-space-shuttle", header: "The final frontier" },
|
||||
},
|
||||
render: ({ icon, loading, fullHeight, header }: IEmptyState) =>
|
||||
container(
|
||||
html` <ak-empty-state
|
||||
?loading=${loading}
|
||||
?fullHeight=${fullHeight}
|
||||
icon=${ifDefined(icon)}
|
||||
header=${ifDefined(header)}
|
||||
>
|
||||
<span slot="body">This is the body content slot</span>
|
||||
<span slot="primary">This is the primary content slot</span>
|
||||
</ak-empty-state>`,
|
||||
),
|
||||
};
|
||||
@ -7,7 +7,7 @@ import { TemplateResult, html, render as litRender } from "lit";
|
||||
import AKGlobal from "@goauthentik/common/styles/authentik.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import "./EmptyState.js";
|
||||
import "../EmptyState.js";
|
||||
|
||||
const render = (body: TemplateResult) => {
|
||||
document.adoptedStyleSheets = [
|
||||
@ -58,6 +58,6 @@ describe("ak-empty-state", () => {
|
||||
);
|
||||
|
||||
const message = await $("ak-empty-state").$(">>>.pf-c-empty-state__body").$(">>>p");
|
||||
await expect(message).toHaveText("Try again with a different filter");
|
||||
await expect(message).toHaveText("Try again with a fucked filter");
|
||||
});
|
||||
});
|
||||
38
web/src/elements/stories/Expand.docs.mdx
Normal file
38
web/src/elements/stories/Expand.docs.mdx
Normal file
@ -0,0 +1,38 @@
|
||||
import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks";
|
||||
|
||||
import * as ExpandStories from "./Expand.stories";
|
||||
|
||||
<Meta of={ExpandStories} />
|
||||
|
||||
# Expand
|
||||
|
||||
Expand is an in-page element used to hid cluttering details that a user may wish to reveal, such as raw
|
||||
details of an alert or event.
|
||||
|
||||
It has one unnamed slot for the content to be displayed.
|
||||
|
||||
## Usage
|
||||
|
||||
```Typescript
|
||||
import "@goauthentik/elements/Expand.js";
|
||||
```
|
||||
|
||||
```html
|
||||
<ak-expand><p>Your primary content goes here</p></ak-expand>
|
||||
```
|
||||
|
||||
To show the expanded content on initial render:
|
||||
|
||||
```html
|
||||
<ak-expand expanded><p>Your primary content goes here</p></ak-expand>
|
||||
```
|
||||
|
||||
## Demo
|
||||
|
||||
### Default: The content is hidden
|
||||
|
||||
<Story of={ExpandStories.DefaultStory} />
|
||||
|
||||
### Expanded
|
||||
|
||||
<Story of={ExpandStories.Expanded} />
|
||||
60
web/src/elements/stories/Expand.stories.ts
Normal file
60
web/src/elements/stories/Expand.stories.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import type { Meta, StoryObj } from "@storybook/web-components";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { Expand, type IExpand } from "../Expand.js";
|
||||
import "../Expand.js";
|
||||
|
||||
const metadata: Meta<Expand> = {
|
||||
title: "Elements/<ak-expand>",
|
||||
component: "ak-expand",
|
||||
parameters: {
|
||||
docs: {
|
||||
description: "Our accordion component",
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
expanded: { control: "boolean" },
|
||||
textOpen: { control: "text" },
|
||||
textClosed: { control: "text" },
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
const container = (content: TemplateResult) =>
|
||||
html` <div style="background-color: #f0f0f0; padding: 1rem;">
|
||||
<style>
|
||||
ak-divider {
|
||||
display: inline-block;
|
||||
width: 32rem;
|
||||
max-width: 32rem;
|
||||
}</style
|
||||
>${content}
|
||||
</div>`;
|
||||
|
||||
export const DefaultStory: StoryObj = {
|
||||
args: {
|
||||
expanded: false,
|
||||
textOpen: undefined,
|
||||
textClosed: undefined,
|
||||
},
|
||||
|
||||
render: ({ expanded, textOpen, textClosed }: IExpand) =>
|
||||
container(
|
||||
html` <ak-expand
|
||||
?expanded=${expanded}
|
||||
textOpen=${ifDefined(textOpen)}
|
||||
textClosed=${ifDefined(textClosed)}
|
||||
><div>
|
||||
<p>Μήτ᾽ ἔμοι μέλι μήτε μέλισσα</p>
|
||||
<p>"Neither the bee nor the honey for me." - Sappho, 600 BC</p>
|
||||
</div>
|
||||
</ak-expand>`,
|
||||
),
|
||||
};
|
||||
export const Expanded = {
|
||||
...DefaultStory,
|
||||
args: { ...DefaultStory, ...{ expanded: true } },
|
||||
};
|
||||
52
web/src/elements/stories/Expand.test.ts
Normal file
52
web/src/elements/stories/Expand.test.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet.js";
|
||||
import { $, expect } from "@wdio/globals";
|
||||
|
||||
import { TemplateResult, html, render as litRender } from "lit";
|
||||
|
||||
import AKGlobal from "@goauthentik/common/styles/authentik.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import "../Expand.js";
|
||||
|
||||
const render = (body: TemplateResult) => {
|
||||
document.adoptedStyleSheets = [
|
||||
...document.adoptedStyleSheets,
|
||||
ensureCSSStyleSheet(PFBase),
|
||||
ensureCSSStyleSheet(AKGlobal),
|
||||
];
|
||||
return litRender(body, document.body);
|
||||
};
|
||||
|
||||
describe("ak-expand", () => {
|
||||
it("should render the expansion content hidden by default", async () => {
|
||||
render(html`<ak-expand><p>This is the expanded text</p></ak-expand>`);
|
||||
const text = await $("ak-expand").$(">>>.pf-c-expandable-section__content");
|
||||
await expect(text).not.toBeDisplayed();
|
||||
});
|
||||
|
||||
it("should render the expansion content visible on demand", async () => {
|
||||
render(html`<ak-expand expanded><p>This is the expanded text</p></ak-expand>`);
|
||||
const paragraph = await $("ak-expand").$(">>>p");
|
||||
await expect(paragraph).toExist();
|
||||
await expect(paragraph).toBeDisplayed();
|
||||
await expect(paragraph).toHaveText("This is the expanded text");
|
||||
});
|
||||
|
||||
it("should respond to the click event", async () => {
|
||||
render(html`<ak-expand><p>This is the expanded text</p></ak-expand>`);
|
||||
let content = await $("ak-expand").$(">>>.pf-c-expandable-section__content");
|
||||
await expect(content).toExist();
|
||||
await expect(content).not.toBeDisplayed();
|
||||
const control = await $("ak-expand").$(">>>button");
|
||||
|
||||
await control.click();
|
||||
content = await $("ak-expand").$(">>>.pf-c-expandable-section__content");
|
||||
await expect(content).toExist();
|
||||
await expect(content).toBeDisplayed();
|
||||
|
||||
await control.click();
|
||||
content = await $("ak-expand").$(">>>.pf-c-expandable-section__content");
|
||||
await expect(content).toExist();
|
||||
await expect(content).not.toBeDisplayed();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user