Files
authentik/web/src/elements/buttons/SpinnerButton/BaseTaskButton.ts
T
Teffen Ellis 2e8a1d80a3 web: Fix numeric values in search select inputs, search input fixes (#16928)
* web: Fix numeric values in search select inputs.

* web: Fix ARIA attributes on menu items.

* web: Fix issues surrounding nested modal actions, selectors, labels.

* web: Prepare group forms for testing, ARIA, etc.

* web: Clarify when spinner buttons are busy.

* web: Fix dark theme toggle input visibility.

* web: Fix issue where tests complete before optional search inputs load.

* web: Add user creation tests, group creation. Flesh out fixtures.
2025-10-02 03:04:38 +00:00

161 lines
4.7 KiB
TypeScript

import { ERROR_CLASS, PROGRESS_CLASS, SUCCESS_CLASS } from "#common/constants";
import { PFSize } from "#common/enums";
import { AKElement } from "#elements/Base";
import { ifPresent } from "#elements/utils/attributes";
import { CustomEmitterElement } from "#elements/utils/eventEmitter";
import { Task, TaskStatus } from "@lit/task";
import { css, html } from "lit";
import { property } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFSpinner from "@patternfly/patternfly/components/Spinner/spinner.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
// `pointer-events: none` makes the button inaccessible during the processing phase.
const buttonStyles = [
PFBase,
PFButton,
PFSpinner,
css`
#spinner-button {
transition: all var(--pf-c-button--m-progress--TransitionDuration) ease 0s;
}
#spinner-button.working {
pointer-events: none;
}
.pf-c-button {
&.pf-m-primary.pf-m-success {
color: var(--pf-c-button--m-primary--Color) !important;
}
&.pf-m-secondary.pf-m-success {
color: var(--pf-c-button--m-secondary--Color) !important;
}
}
`,
];
const StatusMap = {
[TaskStatus.INITIAL]: "",
[TaskStatus.PENDING]: PROGRESS_CLASS,
[TaskStatus.COMPLETE]: SUCCESS_CLASS,
[TaskStatus.ERROR]: ERROR_CLASS,
} as const satisfies Record<TaskStatus, string>;
const SPINNER_TIMEOUT = 1000 * 1.5; // milliseconds
/**
* BaseTaskButton
*
* This is an abstract base class for our four-state buttons. It provides the four basic events of
* this class: click, success, failure, reset. Subclasses can override any of these event handlers,
* but overriding onSuccess() or onFailure() means that you must either call `onComplete` if you
* want to preserve the TaskButton's "reset after completion" semantics, or inside `onSuccess` and
* `onFailure` call their `super.` equivalents.
*
*/
export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) {
eventPrefix = "ak-button";
static styles = [...buttonStyles];
callAction!: () => Promise<unknown>;
actionTask: Task;
@property({ type: Boolean })
public disabled = false;
@property({ type: String })
public label: string | null = null;
constructor() {
super();
this.onSuccess = this.onSuccess.bind(this);
this.onError = this.onError.bind(this);
this.onClick = this.onClick.bind(this);
this.actionTask = this.buildTask();
}
buildTask() {
return new Task(this, {
task: () => this.callAction(),
args: () => [],
autoRun: false,
onComplete: (r: unknown) => this.onSuccess(r),
onError: (r: unknown) => this.onError(r),
});
}
onComplete() {
setTimeout(() => {
this.dispatchCustomEvent(`${this.eventPrefix}-reset`);
// set-up for the next task...
this.actionTask = this.buildTask();
this.requestUpdate();
}, SPINNER_TIMEOUT);
}
onSuccess(r: unknown) {
this.dispatchCustomEvent(`${this.eventPrefix}-success`, {
result: r,
});
this.onComplete();
}
onError(error: unknown) {
this.dispatchCustomEvent(`${this.eventPrefix}-failure`, {
error,
});
this.onComplete();
}
onClick() {
// Don't accept clicks when a task is in progress..
if (this.actionTask.status === TaskStatus.PENDING) {
return;
}
this.dispatchCustomEvent(`${this.eventPrefix}-click`);
this.actionTask.run();
}
private spinner = html`<span class="pf-c-button__progress">
<ak-spinner size=${PFSize.Medium}></ak-spinner>
</span>`;
get buttonClasses() {
return [
...this.classList,
StatusMap[this.actionTask.status],
this.actionTask.status === TaskStatus.INITIAL ? "" : "working",
]
.join(" ")
.trim();
}
render() {
return html`<button
id="spinner-button"
part="spinner-button"
class="pf-c-button pf-m-progress ${this.buttonClasses}"
@click=${this.onClick}
type="button"
aria-label=${ifPresent(this.label)}
aria-busy=${this.actionTask.status === TaskStatus.PENDING ? "true" : "false"}
?disabled=${this.disabled}
>
${this.actionTask.render({
pending: () => this.spinner,
})}
<slot></slot>
</button>`;
}
}
export default BaseTaskButton;