web: patternfly hints as ak-web-component (#7120)

* web: patternfly hints as ak-web-component

Patternfly 5's "Hints" React Component, but ported to web components.
The discovery that CSS Custom Properties are still available in
child components, even if they're within independent ShadowDOMs,
made this fairly easy to port from Handlebars to Lit-HTML.  Moving
the definitions into `:host` and the applications into the root DIV
of the component made duplicating the Patternfly 5 structure
straightforward.

Despite the [Patternfly
Elements]documentation](https://patternflyelements.org/docs/develop/create/),
there's a lot to Patternfly Elements that isn't well documented,
such as their slot controller, which near as I can tell just makes
it easy to determine if a slot with the given name is actually being
used by the client code, but it's hard to tell why, other than that it
provides an easy way to determine if some CSS should be included.

* Pre-commit fixes.

* web: fix some issues with styling found while testing.

* web: separated the "with Title" and "without Title" stories.

* Added footer story, fixed some CSS.

* web: hint controller

Add the `ShowHintController`.  This ReactiveController takes a token
in its constructor, and looks in LocalStorage for that token and
an associated value.  If that value is not `undefined`, it sets the
field `this.host.showHint` to the value found.

It also provides a `render()` method that provides an `ak-hint-footer`
with a checkbox and the "Don't show this message again," and responds
to clicks on the checkbox by setting the `this.hint.showHint` and
LocalStorage values to "false".

An example web component using it has been supplied.

* web: support dark mode for hints.

This was nifty.  Still not entirely sure about the `theme="dark"`
rippling through the product, but in this case it works quite well.
All it took was defining the alternative dark mode values in a CSS
entry, `:host([theme="dark"]) { ... }` and exploiting Patternfly's
already intensely atomized CSS Custom Properties properly.

* web: revise colors to use more of the Authentik dark-mode style.

* Update web/src/components/ak-hint/ak-hint.ts

Signed-off-by: Jens L. <jens@beryju.org>

* remove any

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

---------

Signed-off-by: Jens L. <jens@beryju.org>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
This commit is contained in:
Ken Sternberg 2023-10-12 10:44:15 -07:00 committed by GitHub
parent b503379319
commit 21e5441f92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 448 additions and 0 deletions

View File

@ -0,0 +1,69 @@
import { MessageLevel } from "@goauthentik/common/messages";
import {
ShowHintController,
ShowHintControllerHost,
} from "@goauthentik/components/ak-hint/ShowHintController";
import "@goauthentik/components/ak-hint/ak-hint";
import "@goauthentik/components/ak-hint/ak-hint-body";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/buttons/ActionButton/ak-action-button";
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
import { html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
@customElement("ak-application-wizard-hint")
export class AkApplicationWizardHint extends AKElement implements ShowHintControllerHost {
static get styles() {
return [PFPage];
}
@property({ type: Boolean, attribute: "show-hint" })
forceHint: boolean = false;
@state()
showHint: boolean = true;
showHintController: ShowHintController;
constructor() {
super();
this.showHintController = new ShowHintController(
this,
"202310-application-wizard-announcement",
);
}
renderHint() {
return html` <section class="pf-c-page__main-section pf-m-no-padding-mobile">
<ak-hint>
<ak-hint-body>
<p>
Authentik has a new Application Wizard that can configure both an
application and its authentication provider at the same time.
<a href="(link to docs)">Learn more about the wizard here.</a>
</p>
<ak-action-button
class="pf-m-secondary"
.apiRequest=${() => {
showMessage({
message: "This would have shown the wizard",
level: MessageLevel.success,
});
}}
>Create with Wizard</ak-action-button
></ak-hint-body
>
${this.showHintController.render()}
</ak-hint>
</section>`;
}
render() {
return this.showHint || this.forceHint ? this.renderHint() : nothing;
}
}
export default AkApplicationWizardHint;

View File

@ -22,3 +22,5 @@ export const EVENT_THEME_CHANGE = "ak-theme-change";
export const WS_MSG_TYPE_MESSAGE = "message";
export const WS_MSG_TYPE_REFRESH = "refresh";
export const LOCALSTORAGE_AUTHENTIK_KEY = "authentik-local-settings";

View File

@ -0,0 +1,63 @@
import { LOCALSTORAGE_AUTHENTIK_KEY } from "@goauthentik/common/constants";
import { msg } from "@lit/localize";
import { LitElement, ReactiveController, ReactiveControllerHost, html } from "lit";
type ReactiveLitElement = LitElement & ReactiveControllerHost;
export interface ShowHintControllerHost extends ReactiveLitElement {
showHint: boolean;
showHintController: ShowHintController;
}
const getCurrentStorageValue = (): Record<string, unknown> => {
try {
return JSON.parse(window?.localStorage.getItem(LOCALSTORAGE_AUTHENTIK_KEY) ?? "{}");
} catch (_err: unknown) {
return {};
}
};
export class ShowHintController implements ReactiveController {
host: ShowHintControllerHost;
hintToken: string;
constructor(host: ShowHintControllerHost, hintToken: string) {
(this.host = host).addController(this);
this.hintToken = hintToken;
this.hideTheHint = this.hideTheHint.bind(this);
}
hideTheHint() {
window?.localStorage.setItem(
LOCALSTORAGE_AUTHENTIK_KEY,
JSON.stringify({
...getCurrentStorageValue(),
[this.hintToken]: false,
}),
);
this.host.showHint = false;
}
hostConnected() {
const localStores = getCurrentStorageValue();
if (!(this.hintToken in localStores)) {
return;
}
// Note that we only do this IF the field exists and is defined. `undefined` means "do the
// default thing of showing the hint."
this.host.showHint = localStores[this.hintToken] as boolean;
}
render() {
return html`<ak-hint-footer
><div style="text-align: right">
<input type="checkbox" @input=${this.hideTheHint} />${msg(
"Don't show this message again.",
)}
</div></ak-hint-footer
>`;
}
}

View File

@ -0,0 +1,32 @@
import { AKElement } from "@goauthentik/app/elements/Base";
import { css, html } from "lit";
import { customElement } from "lit/decorators.js";
const style = css`
div {
display: inline-grid;
grid-row: 1;
grid-column: 2;
grid-auto-flow: column;
margin-left: var(--ak-hint__actions--MarginLeft);
text-align: right;
}
::slotted(ak-hint-body) {
grid-column: 1;
}
`;
@customElement("ak-hint-actions")
export class AkHintActions extends AKElement {
static get styles() {
return [style];
}
render() {
return html`<div part="ak-hint-actions"><slot></slot></div>`;
}
}
export default AkHintActions;

View File

@ -0,0 +1,24 @@
import { AKElement } from "@goauthentik/app/elements/Base";
import { css, html } from "lit";
import { customElement } from "lit/decorators.js";
const style = css`
div {
grid-column: 1 / -1;
font-size: var(--ak-hint__body--FontSize);
}
`;
@customElement("ak-hint-body")
export class AkHintBody extends AKElement {
static get styles() {
return [style];
}
render() {
return html`<div part="ak-hint-body"><slot></slot></div>`;
}
}
export default AkHintBody;

View File

@ -0,0 +1,26 @@
import { AKElement } from "@goauthentik/app/elements/Base";
import { css, html } from "lit";
import { customElement } from "lit/decorators.js";
const style = css`
#host {
grid-column: 1 / -1;
}
::slotted(div#host > *:not(:last-child)) {
margin-right: var(--ak-hint__footer--child--MarginRight);
}
`;
@customElement("ak-hint-footer")
export class AkHintFooter extends AKElement {
static get styles() {
return [style];
}
render() {
return html`<div id="host" part="ak-hint-footer"><slot></slot></div>`;
}
}
export default AkHintFooter;

View File

@ -0,0 +1,23 @@
import { AKElement } from "@goauthentik/app/elements/Base";
import { css, html } from "lit";
import { customElement } from "lit/decorators.js";
const style = css`
#host {
font-size: var(--ak-hint__title--FontSize);
}
`;
@customElement("ak-hint-title")
export class AkHintTitle extends AKElement {
static get styles() {
return [style];
}
render() {
return html`<div id="host" part="ak-hint-title"><slot></slot></div>`;
}
}
export default AkHintTitle;

View File

@ -0,0 +1,137 @@
import { MessageLevel } from "@goauthentik/common/messages";
import "@goauthentik/elements/buttons/ActionButton/ak-action-button";
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
import { Meta } from "@storybook/web-components";
import { TemplateResult, html } from "lit";
import "../ak-radio-input";
import "./ak-hint";
import AkHint from "./ak-hint";
import "./ak-hint-body";
import "./ak-hint-title";
const metadata: Meta<AkHint> = {
title: "Components / Patternfly Hint",
component: "ak-hint",
parameters: {
docs: {
description: {
component: "A stylized hint box",
},
},
},
};
export default metadata;
const container = (testItem: TemplateResult) =>
html` <div style="background: #fff; padding: 2em">
<style>
li {
display: block;
}
p {
color: black;
margin-top: 1em;
}
* {
--ak-hint--Color: black !important;
}
ak-hint-title::part(ak-hint-title),
ak-hint-footer::part(ak-hint-footer),
slotted::(*) {
color: black;
}
</style>
${testItem}
<ul id="radio-message-pad" style="margin-top: 1em"></ul>
</div>`;
export const Default = () => {
return container(
html` <section class="pf-c-page__main-section pf-m-no-padding-mobile">
<ak-hint>
<ak-hint-body>
<p style="padding-bottom: 1rem;">
Authentik has a new Application Wizard that can configure both an
application and its authentication provider at the same time.
<a href="(link to docs)">Learn more about the wizard here.</a>
</p>
<ak-action-button
class="pf-m-secondary"
.apiRequest=${() => {
showMessage({
message: "This would have shown the wizard",
level: MessageLevel.success,
});
}}
>Create with Wizard</ak-action-button
></ak-hint-body
>
</ak-hint>
</section>`,
);
};
export const WithTitle = () => {
return container(
html` <section class="pf-c-page__main-section pf-m-no-padding-mobile">
<ak-hint>
<ak-hint-title>New Application Wizard</ak-hint-title>
<ak-hint-body>
<p style="padding-bottom: 1rem;">
Authentik has a new Application Wizard that can configure both an
application and its authentication provider at the same time.
<a href="(link to docs)">Learn more about the wizard here.</a>
</p>
<ak-action-button
class="pf-m-secondary"
.apiRequest=${() => {
showMessage({
message: "This would have shown the wizard",
level: MessageLevel.success,
});
}}
>Create with Wizard</ak-action-button
></ak-hint-body
>
</ak-hint>
</section>`,
);
};
export const WithTitleAndFooter = () => {
return container(
html` <section class="pf-c-page__main-section pf-m-no-padding-mobile">
<ak-hint>
<ak-hint-title>New Application Wizard</ak-hint-title>
<ak-hint-body>
<p style="padding-bottom: 1rem;">
Authentik has a new Application Wizard that can configure both an
application and its authentication provider at the same time.
<a href="(link to docs)">Learn more about the wizard here.</a>
</p>
<ak-action-button
class="pf-m-secondary"
.apiRequest=${() => {
showMessage({
message: "This would have shown the wizard",
level: MessageLevel.success,
});
}}
>Create with Wizard</ak-action-button
></ak-hint-body
>
<ak-hint-footer
><div style="text-align: right">
<input type="checkbox" /> Don't show this message again.
</div></ak-hint-footer
>
</ak-hint>
</section>`,
);
};

View File

@ -0,0 +1,72 @@
import { AKElement } from "@goauthentik/app/elements/Base";
import { css, html } from "lit";
import { customElement } from "lit/decorators.js";
const styles = css`
:host {
--ak-hint--GridRowGap: var(--pf-global--spacer--md);
--ak-hint--PaddingTop: var(--pf-global--spacer--md);
--ak-hint--PaddingRight: var(--pf-global--spacer--lg);
--ak-hint--PaddingBottom: var(--pf-global--spacer--md);
--ak-hint--PaddingLeft: var(--pf-global--spacer--lg);
--ak-hint--BackgroundColor: var(--pf-global--palette--blue-50);
--ak-hint--BorderColor: var(--pf-global--palette--blue-100);
--ak-hint--BorderWidth: var(--pf-global--BorderWidth--sm);
--ak-hint--BoxShadow: var(--pf-global--BoxShadow--sm);
--ak-hint--Color: var(--pf-global--Color--100);
/* Hint Title */
--ak-hint__title--FontSize: var(--pf-global--FontSize--lg);
/* Hint Body */
--ak-hint__body--FontSize: var(--pf-global--FontSize--md);
/* Hint Footer */
--ak-hint__footer--child--MarginRight: var(--pf-global--spacer--md);
/* Hint Actions */
--ak-hint__actions--MarginLeft: var(--pf-global--spacer--2xl);
--ak-hint__actions--c-dropdown--MarginTop: calc(
var(--pf-global--spacer--form-element) * -1
);
}
:host([theme="dark"]) {
--ak-hint--BackgroundColor: var(--ak-dark-background-darker);
--ak-hint--BorderColor: var(--ak-dark-background-lighter);
--ak-hint--Color: var(--ak-dark-foreground);
}
div#host {
display: flex;
flex-direction: column;
gap: var(--ak-hint--GridRowGap);
background-color: var(--ak-hint--BackgroundColor);
color: var(--ak-hint--Color);
border: var(--ak-hint--BorderWidth) solid var(--ak-hint--BorderColor);
box-shadow: var(--ak-hint--BoxShadow);
padding: var(--ak-hint--PaddingTop) var(--ak-hint--PaddingRight)
var(--ak-hint--PaddingBottom) var(--ak-hint--PaddingLeft);
}
::slotted(ak-hint-title),
::slotted(ak-hint-body) {
display: grid;
grid-template-columns: 1fr auto;
grid-row-gap: var(--ak-hint--GridRowGap);
}
`;
@customElement("ak-hint")
export class AkHint extends AKElement {
static get styles() {
return [styles];
}
render() {
return html`<div part="ak-hint" id="host"><slot></slot></div>`;
}
}
export default AkHint;