web: Document the labels. Fix the alerts

This commit documents and adds unit tests for our `Labels` component, which are usually called
"chips" in other design systems.

I've also reverted to allowing the components that take 'level' information to take it as a single
argument that is _either_ an attribute or a property.  If it's a property, it reverts to the older
behavior.
This commit is contained in:
Ken Sternberg
2024-05-17 09:52:26 -07:00
parent 3797cc25be
commit 79d73b0d57
9 changed files with 294 additions and 101 deletions

View File

@ -7,14 +7,6 @@ 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",
@ -22,6 +14,15 @@ export enum Level {
Danger = "pf-m-danger",
}
export const levelNames = ["warning", "info", "success", "danger"];
export type Levels = (typeof levelNames)[number];
export interface IAlert {
inline?: boolean;
icon?: string;
level?: string;
}
/**
* @class Alert
* @element ak-alert
@ -41,60 +42,29 @@ export class Alert extends AKElement implements IAlert {
inline = false;
/**
* Fallback method of determining severity
* Method of determining severity
*
* @attr
*/
@property()
level: Level = Level.Warning;
level: Level | Levels = Level.Warning;
/**
* Highest severity level.
* Icon to display
*
* @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;
@property()
icon = "fa-exclamation-circle";
static get styles() {
return [PFBase, PFAlert];
}
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;
const level = levelNames.includes(this.level)
? `pf-m-${this.level}`
: (this.level as string);
return {
"pf-c-alert": true,
"pf-m-inline": this.inline,
@ -105,7 +75,7 @@ export class Alert extends AKElement implements IAlert {
render() {
return html`<div class="${classMap(this.classmap)}">
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-circle"></i>
<i class="fas ${this.icon}"></i>
</div>
<h4 class="pf-c-alert__title">
<slot></slot>

View File

@ -2,58 +2,103 @@ import { AKElement } from "@goauthentik/elements/Base";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import PFLabel from "@patternfly/patternfly/components/Label/label.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
export enum PFColor {
Green = "pf-m-green",
Orange = "pf-m-orange",
Red = "pf-m-red",
Grey = "",
Green = "success",
Orange = "warning",
Red = "danger",
Grey = "info",
}
@customElement("ak-label")
export class Label extends AKElement {
@property()
color: PFColor = PFColor.Grey;
export const levelNames = ["warning", "info", "success", "danger"];
export type Level = (typeof levelNames)[number];
type Chrome = [Level, PFColor, string, string];
const chromeList: Chrome[] = [
["danger", PFColor.Red, "pf-m-red", "fa-times"],
["warning", PFColor.Orange, "pf-m-orange", "fa-exclamation-triangle"],
["success", PFColor.Green, "pf-m-green", "fa-check"],
["info", PFColor.Grey, "pf-m-grey", "fa-info-circle"],
];
export interface ILabel {
icon?: string;
compact?: boolean;
color?: string;
}
/**
* @class Label
* @element ak-label
*
* Labels are in-page elements for labeling visual elements.
*
* @slot - Content of the label
*/
@customElement("ak-label")
export class Label extends AKElement implements ILabel {
/**
* The icon to show next to the label
*
* @attr
*/
@property()
icon?: string;
/**
* When true, creates a smaller label with tighter layout
*
* @attr
*/
@property({ type: Boolean })
compact = false;
/**
* Severity level
*
* @attr
*/
@property()
color: PFColor | Level = PFColor.Grey;
static get styles(): CSSResult[] {
return [PFBase, PFLabel];
}
getDefaultIcon(): string {
switch (this.color) {
case PFColor.Green:
return "fa-check";
case PFColor.Orange:
return "fa-exclamation-triangle";
case PFColor.Red:
return "fa-times";
case PFColor.Grey:
return "fa-info-circle";
default:
return "";
}
get classesAndIcon() {
const chrome = chromeList.find(
([level, color]) => this.color === level || this.color === color,
);
const [illo, icon] = chrome ? chrome.slice(2) : ["pf-m-grey", "fa-info-circle"];
return {
classes: {
"pf-c-label": true,
"pf-m-compact": this.compact,
...(illo ? { [illo]: true } : {}),
},
icon: this.icon ? this.icon : icon,
};
}
render(): TemplateResult {
return html`<span class="pf-c-label ${this.color} ${this.compact ? "pf-m-compact" : ""}">
const { classes, icon } = this.classesAndIcon;
return html`<span class=${classMap(classes)}>
<span class="pf-c-label__content">
<span class="pf-c-label__icon">
<i
class="fas fa-fw ${this.icon || this.getDefaultIcon()}"
aria-hidden="true"
></i>
<i class="fas fa-fw ${icon}" aria-hidden="true"></i>
</span>
<slot></slot>
</span>
</span>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-label": Label;
}
}

View File

@ -32,6 +32,9 @@ The default state of an alert is _warning_.
### Info
The icon can be changed via the `icon` attribute. It takes the name of a valid `fas`-class Font
Awesome icon. Changing the icon can be helpful, as not everyone can see the color changes.
<Story of={AlertStories.InfoAlert} />
### Success

View File

@ -1,6 +1,7 @@
import type { Meta, StoryObj } from "@storybook/web-components";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { Alert, type IAlert } from "../Alert.js";
import "../Alert.js";
@ -17,10 +18,8 @@ const metadata: Meta<Alert> = {
},
argTypes: {
inline: { control: "boolean" },
warning: { control: "boolean" },
info: { control: "boolean" },
success: { control: "boolean" },
danger: { control: "boolean" },
level: { control: "text" },
icon: { control: "text" },
// @ts-ignore
message: { control: "text" },
},
@ -31,15 +30,11 @@ 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) => {
render: ({ inline, level, icon, message }: IAlertForTesting) => {
return html` <div style="background-color: #f0f0f0; padding: 1rem;">
<style>
ak-alert {
@ -48,13 +43,7 @@ export const DefaultStory: StoryObj = {
max-width: 32rem;
}
</style>
<ak-alert
?inline=${inline}
?warning=${warning}
?info=${info}
?success=${success}
?danger=${danger}
>
<ak-alert level=${ifDefined(level)} ?inline=${inline} icon=${ifDefined(icon)}>
<p>${message}</p>
</ak-alert>
</div>`;
@ -63,15 +52,18 @@ export const DefaultStory: StoryObj = {
export const SuccessAlert = {
...DefaultStory,
args: { ...DefaultStory, ...{ success: true, message: "He's a tribute to your genius!" } },
args: { ...DefaultStory, ...{ level: "success", message: "He's a tribute to your genius!" } },
};
export const InfoAlert = {
...DefaultStory,
args: { ...DefaultStory, ...{ info: true, message: "An octopus has tastebuds on its arms." } },
args: {
...DefaultStory,
...{ level: "info", icon: "fa-coffee", message: "It is time for coffee." },
},
};
export const DangerAlert = {
...DefaultStory,
args: { ...DefaultStory, ...{ danger: true, message: "Danger, Will Robinson! Danger!" } },
args: { ...DefaultStory, ...{ level: "danger", message: "Danger, Will Robinson! Danger!" } },
};

View File

@ -16,17 +16,11 @@ describe("ak-alert", () => {
});
it("should render an alert with the attribute", async () => {
render(html`<ak-alert info>This is an alert</ak-alert>`, document.body);
render(html`<ak-alert level="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");

View File

@ -6,7 +6,7 @@ import * as ExpandStories from "./Expand.stories";
# Expand
Expand is an in-page element used to hid cluttering details that a user may wish to reveal, such as raw
Expand is an in-page element used to hide 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.

View File

@ -0,0 +1,53 @@
import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks";
import * as LabelStories from "./Label.stories";
<Meta of={LabelStories} />
# Labels
Labels are in-page elements that provide pointers or guidance. Frequently called "chips" in other
design systems. Labels are used alongside other elements to warn users of conditions or status that
they might want to be aware of
## Usage
```Typescript
import "@goauthentik/elements/Label.js";
```
Note that the content of a label _must_ be a valid HTML component; plain text does not work here. The
default label is informational:
```html
<ak-label><p>This is the content of your alert!</p></ak-label>
```
## Demo
### Default: Info
The default state of an alert is _info_.
<Story of={LabelStories.DefaultStory} />
### Warning
The icon can be changed via the `icon` attribute. It takes the name of a valid `fas`-class Font
Awesome icon. Changing the icon can be helpful, as not everyone can see the color changes. It has
also been set "compact" to show the difference.
<Story of={LabelStories.CompactWarningLabel} />
### Success
This label is illustrated using the PFColor enum, rather than the attribute name. Note that the content
is _slotted_, and can be styled.
<Story of={LabelStories.SuccessLabel} />
### Danger
This label is illustrated using the PFColor enum, rather than the attribute name.
<Story of={LabelStories.DangerLabel} />

View File

@ -0,0 +1,83 @@
import type { Meta, StoryObj } from "@storybook/web-components";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { type ILabel, Label, PFColor } from "../Label.js";
import "../Label.js";
type ILabelForTesting = ILabel & { message: string };
const metadata: Meta<Label> = {
title: "Elements/<ak-label>",
component: "ak-label",
parameters: {
docs: {
description: "An alert",
},
},
argTypes: {
compact: { control: "boolean" },
color: { control: "text" },
icon: { control: "text" },
// @ts-ignore
message: { control: "text" },
},
};
export default metadata;
export const DefaultStory: StoryObj = {
args: {
compact: false,
message: "Eat at Joe's.",
},
// @ts-ignore
render: ({ compact, color, icon, message }: ILabelForTesting) => {
return html` <div style="background-color: #f0f0f0; padding: 1rem;">
<style>
ak-label {
display: inline-block;
width: 48rem;
max-width: 48rem;
}
</style>
<ak-label color=${ifDefined(color)} ?compact=${compact} icon=${ifDefined(icon)}>
<p>${message}</p>
</ak-label>
</div>`;
},
};
export const SuccessLabel = {
...DefaultStory,
args: {
...DefaultStory,
...{
color: PFColor.Green,
message: html`I'll show them! I'll show them <i>all</i>&nbsp;! Mwahahahahaha!`,
},
},
};
export const CompactWarningLabel = {
...DefaultStory,
args: {
...DefaultStory,
...{
compact: true,
color: "warning",
icon: "fa-coffee",
message: "It is time for coffee.",
},
},
};
export const DangerLabel = {
...DefaultStory,
args: {
...DefaultStory,
...{ color: "danger", message: "Grave danger? Is there another kind?" },
},
};

View File

@ -0,0 +1,53 @@
import { $, expect } from "@wdio/globals";
import { html, render } from "lit";
import "../Label.js";
import { PFColor } from "../Label.js";
describe("ak-label", () => {
it("should render a label with the enum", async () => {
render(html`<ak-label color=${PFColor.Red}>This is a label</ak-label>`, document.body);
await expect(await $("ak-label").$(">>>span.pf-c-label")).toHaveElementClass("pf-c-label");
await expect(await $("ak-label").$(">>>span.pf-c-label")).not.toHaveElementClass(
"pf-m-compact",
);
await expect(await $("ak-label").$(">>>span.pf-c-label")).toHaveElementClass("pf-m-red");
await expect(await $("ak-label").$(">>>i.fas")).toHaveElementClass("fa-times");
await expect(await $("ak-label").$(">>>.pf-c-label__content")).toHaveText(
"This is a label",
);
});
it("should render a label with the attribute", async () => {
render(html`<ak-label color="success">This is a label</ak-label>`, document.body);
await expect(await $("ak-label").$(">>>span.pf-c-label")).toHaveElementClass("pf-m-green");
await expect(await $("ak-label").$(">>>.pf-c-label__content")).toHaveText(
"This is a label",
);
});
it("should render a compart label with the default level", async () => {
render(html`<ak-label compact>This is a label</ak-label>`, document.body);
await expect(await $("ak-label").$(">>>span.pf-c-label")).toHaveElementClass("pf-m-grey");
await expect(await $("ak-label").$(">>>span.pf-c-label")).toHaveElementClass(
"pf-m-compact",
);
await expect(await $("ak-label").$(">>>i.fas")).toHaveElementClass("fa-info-circle");
await expect(await $("ak-label").$(">>>.pf-c-label__content")).toHaveText(
"This is a label",
);
});
it("should render a compact label with an icon and the default level", async () => {
render(html`<ak-label compact icon="fa-coffee">This is a label</ak-label>`, document.body);
await expect(await $("ak-label").$(">>>span.pf-c-label")).toHaveElementClass("pf-m-grey");
await expect(await $("ak-label").$(">>>span.pf-c-label")).toHaveElementClass(
"pf-m-compact",
);
await expect(await $("ak-label").$(">>>.pf-c-label__content")).toHaveText(
"This is a label",
);
await expect(await $("ak-label").$(">>>i.fas")).toHaveElementClass("fa-coffee");
});
});