Skip to content

Commit e6d2142

Browse files
Element/ak switch (#51)
* . * Improved lint. Switch underway. * Finished the switch. All tests passing. Looks far better than the v4 version. * Fix icon rendering and icon position. * Updated and prettiered. * Fixed bad test passes. * Bringing switch up-to-date with the mixin. * Including package-lock, which was lost in the merge. * Fixed a lot of bugs-- there was some transitional code in here that was pretty messed up. Switch is now completely and externally themable. * Turns out, I didn't need that extra. The grid handled it for me; no code required to swap control & label. Excellent. * Brings switch into the vitest world. * Updated builder with spread pattern; deleted unused interface declaration from component. * Fixed dark mode for switch. * Some fixes found with a deeper code analysis pass. * Fixed logic for rendering icon and labels. It's confusing, but not as bad as before. * Removed silence flag that was blocking error reporting. --------- Co-authored-by: Jens L. <jens@goauthentik.io>
1 parent 624d6a4 commit e6d2142

13 files changed

+1154
-21538
lines changed

package-lock.json

Lines changed: 0 additions & 21536 deletions
This file was deleted.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"clean:artifacts": "rm -f tsconfig*.tsbuildinfo",
2323
"clean:dist": "rimraf ./dist",
2424
"fix:styles": "stylelint --fix 'src/**/*.css'",
25-
"lint": "run-s -sn lint:types lint:eslint lint:package",
25+
"lint": "run-s -n lint:types lint:eslint",
2626
"lint-staged": "lint-staged",
2727
"lint:eslint": "eslint --max-warnings 0 --fix",
2828
"lint:styles": "stylelint 'src/**/*.css'",

src/ak-switch/ak-switch.builder.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { ElementRest } from "../types.js";
2+
import { SwitchInput } from "./ak-switch.js";
3+
4+
import { spread } from "@open-wc/lit-helpers";
5+
6+
import { html, TemplateResult } from "lit";
7+
import { ifDefined } from "lit/directives/if-defined.js";
8+
9+
/**
10+
* Configuration options for the akSwitch helper function
11+
*/
12+
export type SwitchProps = ElementRest &
13+
Partial<
14+
Pick<
15+
SwitchInput,
16+
| "name"
17+
| "checked"
18+
| "required"
19+
| "disabled"
20+
| "value"
21+
| "useCheck"
22+
| "showLabel"
23+
| "ariaLabel"
24+
>
25+
> & {
26+
label?: TemplateResult | string;
27+
labelOn?: TemplateResult | string;
28+
icon?: TemplateResult | string;
29+
reverse?: boolean;
30+
};
31+
32+
/**
33+
* @summary Helper function to create a Switch component programmatically
34+
*
35+
* @returns {TemplateResult} A Lit template result containing the configured ak-switch element
36+
*
37+
* @see {@link SwitchInput} - The underlying web component
38+
*/
39+
export function akSwitch(options: SwitchProps = {}): TemplateResult {
40+
const {
41+
name,
42+
checked,
43+
required,
44+
disabled,
45+
value,
46+
useCheck,
47+
showLabel,
48+
ariaLabel,
49+
reverse,
50+
label,
51+
labelOn,
52+
icon,
53+
...rest
54+
} = options;
55+
56+
const intoSlot = (slot: string, s?: TemplateResult | string) =>
57+
typeof s === "string" ? html`<span slot=${slot}>${s}</span>` : (s ?? "");
58+
59+
// The icon handling looks odd, but bear with it:
60+
// - If icon is a string, we pass it as an attribute to switch,
61+
// so switch can look up the icon itself.a
62+
// - If icon is nullish, we put nothing into the template.
63+
// - Otherwise, we assume icon is some renderable thing of
64+
// TemplateResultwith the proper `slot="icon"` attribute.
65+
//
66+
return html`
67+
<ak-switch
68+
${spread(rest)}
69+
name=${ifDefined(name)}
70+
?checked=${Boolean(checked)}
71+
?required=${Boolean(required)}
72+
?disabled=${Boolean(disabled)}
73+
?reverse=${Boolean(reverse)}
74+
check-icon=${ifDefined(typeof icon === "string" ? icon : undefined)}
75+
value=${ifDefined(value ?? undefined)}
76+
?use-check=${Boolean(useCheck)}
77+
?show-label=${Boolean(showLabel)}
78+
aria-label=${ifDefined(ariaLabel ?? undefined)}
79+
>
80+
${intoSlot("label", label)} ${intoSlot("label-on", labelOn)}
81+
${typeof icon === "string" ? "" : (icon ?? "")}
82+
</ak-switch>
83+
`;
84+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import "../ak-icon/ak-icon.js";
2+
3+
import { AkLitElement } from "../component-base.js";
4+
import { FormAssociatedBooleanMixin } from "../mixins/form-associated-boolean-mixin.js";
5+
import styles from "./ak-switch.scss";
6+
7+
import { match, P } from "ts-pattern";
8+
9+
import { msg } from "@lit/localize";
10+
import { html, nothing, TemplateResult } from "lit";
11+
import { property } from "lit/decorators.js";
12+
13+
const CHECK_ICON = "fas fa-check";
14+
15+
/**
16+
* @element ak-switch
17+
*
18+
* @summary A toggle switch component for boolean settings with customizable text and accessibility features
19+
*
20+
* @attr {string} name - Name attribute for the input element
21+
* @attr {boolean} checked - Whether the switch is checked/enabled
22+
* @attr {boolean} required - Whether the input is required in a form
23+
* @attr {boolean} disabled - Whether the switch is disabled
24+
* @attr {string} value - Value attribute for the input element
25+
* @attr {boolean} show-label - Whether to show on/off text
26+
* @attr {string} aria-label - Aria label for the switch
27+
* @attr {boolean} reverse - Put the label to the *left* of the switch (To the right by default, if shown at all)
28+
* @attr {boolean} use-check - when true, adds an icon to the switch body when checked
29+
*
30+
* @fires change - Fired when the switch is toggled, contains detail with checked state and value
31+
*
32+
* @csspart toggle - The toggle track element
33+
* @csspart knob - The toggle knob element
34+
*
35+
* @cssprop --pf-v5-c-switch--FontSize - Font size of the switch
36+
* @cssprop --pf-v5-c-switch--LineHeight - Line height of the switch
37+
* @cssprop --pf-v5-c-switch--ColumnGap - Gap between switch and label
38+
* @cssprop --pf-v5-c-switch__toggle--Height - Height of the toggle track
39+
* @cssprop --pf-v5-c-switch__toggle--Width - Width of the toggle track
40+
* @cssprop --pf-v5-c-switch__toggle--BackgroundColor - Background color of toggle track
41+
* @cssprop --pf-v5-c-switch__toggle--BorderRadius - Border radius of toggle track
42+
* @cssprop --pf-v5-c-switch__knob--Size - Size of the toggle knob
43+
* @cssprop --pf-v5-c-switch__knob--BackgroundColor - Background color of the knob
44+
* @cssprop --pf-v5-c-switch__knob--BorderRadius - Border radius of the knob
45+
* @cssprop --pf-v5-c-switch__knob--BoxShadow - Box shadow of the knob
46+
* @cssprop --pf-v5-c-switch--checked--BackgroundColor - Background color when checked
47+
* @cssprop --pf-v5-c-switch--focus--OutlineColor - Outline color when focused
48+
* @cssprop --pf-v5-c-switch--focus--OutlineWidth - Outline width when focused
49+
* @cssprop --pf-v5-c-switch--disabled--BackgroundColor - Background color when disabled
50+
* @cssprop --pf-v5-c-switch__text--FontSize - Font size of on/off text
51+
* @cssprop --pf-v5-c-switch__text--Color - Color of on/off text
52+
* @cssprop --pf-v5-c-switch__icon--FontSize - Font size of the check icon
53+
* @cssprop --pf-v5-c-switch__icon--Color - Color of the check icon
54+
*
55+
* ## Slots
56+
* @slot label - The label to show next to switch (optional)
57+
* @slot label-on - An alternative label to show next to the switch when it is checked (optional)
58+
* @slot icon - Use an alternative icon for the checkmark
59+
*
60+
* NOTE: If you do not supply a `slot=label`, `slot=label-on` will also be ignored.
61+
*/
62+
export class SwitchInput extends FormAssociatedBooleanMixin(AkLitElement) {
63+
static readonly styles = [styles];
64+
65+
@property({ type: Boolean, attribute: "use-check" })
66+
public useCheck = false;
67+
68+
@property({ type: Object })
69+
public checkIcon?: TemplateResult | string = CHECK_ICON;
70+
71+
@property({ type: Boolean, attribute: "show-label" })
72+
public showLabel = false;
73+
74+
connectedCallback() {
75+
super.connectedCallback();
76+
if (!this.hasAttribute("role")) {
77+
this.setAttribute("role", "switch");
78+
}
79+
}
80+
81+
protected renderIcon() {
82+
const useSlot = this.hasSlotted("icon");
83+
const noIcon = !this.useCheck && !useSlot;
84+
85+
if (noIcon) {
86+
return nothing;
87+
}
88+
89+
const fallback =
90+
typeof this.checkIcon === "string"
91+
? html`<ak-icon size="sm" icon=${this.checkIcon}></ak-icon>`
92+
: this.checkIcon;
93+
94+
return html`<div part="toggle-icon" aria-hidden="true">
95+
<slot name="icon">${fallback}</slot>
96+
</div>`;
97+
}
98+
99+
protected renderLabel() {
100+
const dontShow = !this.showLabel && !this.hasSlotted("label");
101+
const showOnLabel = (this.showLabel || this.hasSlotted("label-on")) && this.checked;
102+
103+
return match([dontShow, showOnLabel])
104+
.with([true, P._], () => nothing)
105+
.with(
106+
[false, true],
107+
() => html`<span part="label"><slot name="label-on">${msg("On")}</slot></span>`,
108+
)
109+
.with(
110+
[false, false],
111+
() => html`<span part="label"><slot name="label">${msg("Off")}</slot></span>`,
112+
)
113+
.exhaustive();
114+
}
115+
116+
public override render() {
117+
return html`
118+
<div part="switch">
119+
<div part="toggle">${this.renderIcon()}</div>
120+
${this.renderLabel()}
121+
</div>
122+
`;
123+
}
124+
}

0 commit comments

Comments
 (0)