Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
4088f53
Merge branch 'element/ak-icon' into element/ak-switch
kensternberg-authentik Sep 8, 2025
36157c9
.
kensternberg-authentik Sep 9, 2025
26aed9a
Improved lint. Switch underway.
kensternberg-authentik Sep 11, 2025
a259bbc
Holding: Have a lint bug elsewhere.
kensternberg-authentik Sep 11, 2025
01a961a
Merge branch 'element/ak-icon' into element/ak-switch
kensternberg-authentik Sep 12, 2025
955eee3
Finished the switch. All tests passing. Looks far better than the v…
kensternberg-authentik Sep 12, 2025
fb934ee
Fix icon rendering and icon position.
kensternberg-authentik Sep 12, 2025
d27f7ff
Checkbox working, tests sorta sorted.
kensternberg-authentik Sep 16, 2025
132dcef
Changed the 'indeterminate' icon to be the more traditional dash, and…
kensternberg-authentik Sep 16, 2025
c2c3f70
Updated and prettiered.
kensternberg-authentik Sep 16, 2025
a202462
Merge branch 'element/ak-icon' into element/ak-switch
kensternberg-authentik Sep 16, 2025
7d01922
Fixed bad test passes.
kensternberg-authentik Sep 16, 2025
ebe9c7d
Fixed a bug where the wrong value was being sent to aria-checked.
kensternberg-authentik Sep 16, 2025
9a0fed2
Bringing switch up-to-date with the mixin.
kensternberg-authentik Sep 16, 2025
8f29296
Merge branch 'main' into element/ak-switch
kensternberg-authentik Nov 21, 2025
0377f9d
Including package-lock, which was lost in the merge.
kensternberg-authentik Nov 21, 2025
2514916
Fixed a lot of bugs-- there was some transitional code in here that w…
kensternberg-authentik Nov 21, 2025
671586a
Merge branch 'element/ak-switch' into element/ak-checkbox
kensternberg-authentik Nov 21, 2025
e4f6f97
Interim ... need to check something out.
kensternberg-authentik Nov 21, 2025
16810ec
Turns out, I didn't need that extra. The grid handled it for me; no …
kensternberg-authentik Nov 21, 2025
63090a6
Merge branch 'element/ak-switch' into element/ak-checkbox
kensternberg-authentik Nov 21, 2025
ba6c346
Checkbox has been fully devolved into themable and operant CSS. The …
kensternberg-authentik Nov 21, 2025
f0482cd
Merge branch 'main' into element/ak-checkbox
kensternberg-authentik Jan 12, 2026
ffec255
Updated checkbox tests to be vitest-ready.
kensternberg-authentik Jan 12, 2026
b28b652
Merge branch 'main' into element/ak-switch
kensternberg-authentik Jan 12, 2026
1121fb5
Brings switch into the vitest world.
kensternberg-authentik Jan 12, 2026
f424ed5
Updated builder with spread pattern; deleted unused interface declara…
kensternberg-authentik Jan 12, 2026
9833397
Fixed dark mode for switch.
kensternberg-authentik Jan 12, 2026
2215987
Merge branch 'element/ak-switch' into element/ak-checkbox
kensternberg-authentik Jan 12, 2026
789a074
Merge branch 'main' into element/ak-switch
kensternberg-authentik Jan 15, 2026
025b075
Some fixes found with a deeper code analysis pass.
kensternberg-authentik Jan 15, 2026
826d1b8
Fixed logic for rendering icon and labels. It's confusing, but not a…
kensternberg-authentik Jan 15, 2026
f917712
Merge branch 'element/ak-switch' into element/ak-checkbox
kensternberg-authentik Jan 15, 2026
28f550e
Refinements and adjustments.
kensternberg-authentik Jan 15, 2026
002f528
Removed silence flag that was blocking error reporting.
kensternberg-authentik Jan 15, 2026
1c67675
Merge branch 'element/ak-switch' into element/ak-checkbox
kensternberg-authentik Jan 15, 2026
64a6546
Restoring this; I didn't know we were using it for netlify, and I don…
kensternberg-authentik Jan 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"clean:artifacts": "rm -f tsconfig*.tsbuildinfo",
"clean:dist": "rimraf ./dist",
"fix:styles": "stylelint --fix 'src/**/*.css'",
"lint": "run-s -sn lint:types lint:eslint lint:package",
"lint": "run-s -n lint:types lint:eslint",
"lint-staged": "lint-staged",
"lint:eslint": "eslint --max-warnings 0 --fix",
"lint:styles": "stylelint 'src/**/*.css'",
Expand Down
91 changes: 91 additions & 0 deletions src/ak-checkbox/ak-checkbox.builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import type { ElementRest } from "../types.js";
import { CheckboxInput } from "./ak-checkbox.js";

import { spread } from "@open-wc/lit-helpers";

import { html, TemplateResult } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";

/**
* Configuration options for the akCheckbox helper function
*/
export type CheckboxProps = ElementRest &
Partial<
Pick<
CheckboxInput,
| "name"
| "checked"
| "indeterminate"
| "required"
| "disabled"
| "value"
| "showLabel"
| "ariaLabel"
>
> & {
label?: TemplateResult | string;
labelOn?: TemplateResult | string;
icon?: TemplateResult | string;
indeterminateIcon?: TemplateResult | string;
reverse?: boolean;
};

/**
* @summary Helper function to create a Checkbox component programmatically
*
* @returns {TemplateResult} A Lit template result containing the configured ak-checkbox element
*
* @see {@link CheckboxInput} - The underlying web component
*/
export function akCheckbox(options: CheckboxProps = {}): TemplateResult {
const {
name,
checked,
indeterminate,
required,
disabled,
value,
reverse,
showLabel,
ariaLabel,
label,
labelOn,
icon,
indeterminateIcon,
...rest
} = options;

const intoSlot = (slot: string, s?: TemplateResult | string) =>
typeof s === "string" ? html`<span slot=${slot}>${s}</span>` : (s ?? "");

// The icon handling looks odd, but bear with it:
// - If icon is a string, we pass it as an attribute to checkbox,
// so checkbox can look up the icon itself.
// - If icon is nullish, we put nothing into the template.
// - Otherwise, we assume icon is some renderable thing of
// TemplateResultwith the proper `slot="icon"` attribute.
//
return html`
<ak-checkbox
${spread(rest)}
name=${ifDefined(name)}
?checked=${Boolean(checked)}
?indeterminate=${Boolean(indeterminate)}
?required=${Boolean(required)}
?reverse=${Boolean(reverse)}
?disabled=${Boolean(disabled)}
icon=${ifDefined(typeof icon === "string" ? icon : undefined)}
value=${ifDefined(value)}
?label=${Boolean(showLabel)}
aria-label=${ifDefined(ariaLabel ?? undefined)}
>
${intoSlot("label", label)}
<!-- -->
${intoSlot("label-on", labelOn)}
<!-- -->
${intoSlot("icon", icon)}
<!-- -->
${intoSlot("indeterminate", indeterminateIcon)}
</ak-checkbox>
`;
}
127 changes: 127 additions & 0 deletions src/ak-checkbox/ak-checkbox.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import "../ak-icon/ak-icon.js";

import { AkLitElement } from "../component-base.js";
import { FormAssociatedBooleanMixin } from "../mixins/form-associated-boolean-mixin.js";
import styles from "./ak-checkbox.scss";

import { match, P } from "ts-pattern";

import { html, nothing } from "lit";
import { property } from "lit/decorators.js";

const CHECK_ICON = () =>
html`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M21 7L9 19l-5.5-5.5l1.41-1.41L9 16.17L19.59 5.59z" />
</svg>`;

const DOT_ICON = () => html`
<svg viewBox="0 0 16 16" fill="currentColor">
<path d="M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8z" />
</svg>
`;

/**
* @element ak-checkbox
*
* @summary A checkbox component for boolean form inputs with customizable label positioning and accessibility features
*
* @attr {string} name - Name attribute for form submission
* @attr {boolean} checked - Whether the checkbox is checked
* @attr {boolean} required - Whether the checkbox is required in a form
* @attr {boolean} disabled - Whether the checkbox is disabled
* @attr {boolean} indeterminate - Whether the checkbox should show the "indeterminate" state
* @attr {string} value - Value submitted when checkbox is checked
* @attr {boolean} label - Whether to show label content alongside the checkbox
* @attr {boolean} reverse - Whether to reverse the checkbox and label positions
* @attr {string} aria-label - Aria label for the checkbox
*
* @fires change - Fired when the checkbox is toggled, contains detail with checked state and value
*
* @slot icon (Optional) - An alternative icon for the "checked" state
* @slot indeterminate (Optional) - An alternative icon for the "indeterminate" state
* @slot label (Optional) - Label content displayed next to the checkbox
* @slot label-on (Optional) - Label content displayed next to the checkbox when it is checked
*
* @csspart checkbox - The main container element
* @csspart toggle - The checkbox visual element
* @csspart label - The label container
*
* @cssprop --pf-v5-c-checkbox--FontSize - Font size of the checkbox component
* @cssprop --pf-v5-c-checkbox--LineHeight - Line height of the checkbox component
* @cssprop --pf-v5-c-checkbox--ColumnGap - Gap between checkbox and label
* @cssprop --pf-v5-c-checkbox__toggle--Height - Height of the checkbox
* @cssprop --pf-v5-c-checkbox__toggle--Width - Width of the checkbox
* @cssprop --pf-v5-c-checkbox__toggle--BorderColor - Border color of the checkbox
* @cssprop --pf-v5-c-checkbox__toggle--BorderRadius - Border radius of the checkbox
* @cssprop --pf-v5-c-checkbox__toggle--BorderWidth - Border width of the checkbox
* @cssprop --pf-v5-c-checkbox__toggle--Color - Color of the checkmark icon
* @cssprop --pf-v5-c-checkbox--focus__toggle--OutlineColor - Outline color when focused
* @cssprop --pf-v5-c-checkbox--focus__toggle--OutlineWidth - Outline width when focused
* @cssprop --pf-v5-c-checkbox--disabled__toggle--Color - Color when disabled
* @cssprop --pf-v5-c-checkbox--disabled__label--Color - Label color when disabled
* @cssprop --pf-v5-c-checkbox__label--Color - Default label color
*/
export class CheckboxInput extends FormAssociatedBooleanMixin(AkLitElement) {
static readonly styles = [styles];

@property({ type: Boolean, attribute: "show-label" })
public showLabel = false;

@property({ type: Boolean })
public indeterminate = false;

connectedCallback() {
super.connectedCallback();
if (!this.hasAttribute("role")) {
this.setAttribute("role", "checkbox");
}
}

protected renderIcon() {
const [hasIndeterminate, hasIcon] = [
this.hasSlotted("indeterminate"),
this.hasSlotted("icon"),
];
return match([this.indeterminate, this.checked, hasIndeterminate, hasIcon])
.with([false, false, P._, P._], () => nothing)
.with([true, P._, false, P._], () => DOT_ICON())
.with([true, P._, true, P._], () => html`<slot name="indeterminate"></slot>`)
.with([false, true, P._, false], () => CHECK_ICON())
.with([false, true, P._, true], () => html`<slot name="icon"></slot>`)
.exhaustive();
}

protected renderLabel() {
return this.hasSlotted("label-on") && this.checked
? html`<slot name="label-on"></slot>`
: html`<slot name="label"></slot>`;
}

private renderCheckbox() {
return html`<div part="toggle">${this.renderIcon()}</div>`;
}

private renderWithLabels() {
return html`<div part="toggle">${this.renderIcon()}</div>
<span part="label"> ${this.renderLabel()} </span>`;
}

public override render() {
return html`
<div part="checkbox">
${this.hasSlotted("label") ? this.renderWithLabels() : this.renderCheckbox()}
</div>
`;
}

public override setAriaChecked() {
this.setAriaAttribute(
"aria-checked",
match([this.indeterminate, this.checked])
.with([true, P._], () => "mixed")
.with([false, true], () => "true")
.with([false, false], () => "false")
.exhaustive(),
);
}
}
135 changes: 135 additions & 0 deletions src/ak-checkbox/ak-checkbox.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/* Checkbox is a wholly custom component, so there's no WCCSS file. */

:host {
--checkbox--FontSize: var(--pf-v5-c-checkbox--FontSize, 1rem);
--checkbox__label--PaddingLeft: var(--pf-v5-c-checkbox__label--PaddingLeft, 1rem);
--checkbox--ColumnGap: var(--checkbox__label--PaddingLeft);
--checkbox--LineHeight: var(--pf-v5-c-checkbox--LineHeight, 1.5);
--checkbox--Height: calc(var(--checkbox--FontSize) * var(--checkbox--LineHeight));
--checkbox--Width: auto;
--checkbox__toggle--Color: var(--pf-v5-c-checkbox__toggle--Color, #151515);
--checkbox__toggle--BorderRadius: var(--pf-v5-c-checkbox__toggle--BorderRadius, 3px);
--checkbox__toggle--BorderColor: var(--checkbox__toggle--Color);
--checkbox__toggle--BorderWidth: var(--pf-v5-c-checkbox__toggle--BorderWidth, 2px);
--checkbox__toggle--Height: var(--checkbox--FontSize);
--checkbox__toggle--Width: var(--checkbox__toggle--Height);
--checkbox__toggle--BackgroundColor: inherit;
--checkbox__toggle--Padding: 1px;
--checkbox--disabled__toggle--Color: var(--pf-v5-c-checkbox--disabled__toggle--Color, #6a6e73);
--checkbox--disabled__toggle--BorderColor: var(
--pf-v5-c-checkbox--disabled__toggle--BorderColor,
#d2d2d2
);
--checkbox--disabled__toggle--BackgroundColor: var(
--pf-v5-c-checkbox--disabled__toggle--BackgroundColor,
#f5f5f5
);
--checkbox--disabled__label--Color: var(--pf-v5-c-checkbox--disabled__label--Color, #6a6e73);
--checkbox--focus__toggle--OutlineWidth: var(
--pf-v5-c-checkbox--focus__toggle--OutlineWidth,
2px
);
--checkbox--focus__toggle--OutlineOffset: var(
--pf-v5-c-checkbox--focus__toggle--OutlineOffset,
0.5rem
);
--checkbox--focus__toggle--OutlineColor: var(
--pf-v5-c-checkbox--focus__toggle--OutlineColor,
#06c
);
--checkbox__input--checked__label--Color: var(--pf-v5-c-checkbox__toggle--Color);
--checkbox__input--not-checked__label--Color: var(
--pf-v5-c-checkbox__input--not-checked__label--Color,
#6a6e73
);
--checkbox__label--Color: var(--pf-v5-c-checkbox__label--Color, #151515);

position: relative;
display: inline;
}

[part="checkbox"] {
position: relative;
grid-template-columns: auto;
grid-auto-columns: 1fr;
column-gap: var(--checkbox--ColumnGap);
height: var(--checkbox--Height);
font-size: var(--checkbox--FontSize);
line-height: var(--checkbox--LineHeight);
vertical-align: middle;
cursor: pointer;
display: grid;
}

:host([reverse]) [part="label"] {
grid-row: 1;
grid-column: 1;
}

:host([reverse]) [part="toggle"] {
grid-column: 2;
}

:host([checked]) [part="label"] {
color: var(--checkbox__input--checked__label--Color);
}

[part="checkbox"]:focus-visible [part="toggle"] {
outline: var(--checkbox--focus__toggle--OutlineWidth) solid
var(--checkbox--focus__toggle--OutlineColor);
outline-offset: var(--checkbox--focus__toggle--OutlineOffset);
}

[part="toggle"] {
display: flex;
display: inline-flex;
justify-content: center; /* Centers horizontally */
align-items: center; /* Centers vertically */
width: var(--checkbox__toggle--Width);
height: var(--checkbox__toggle--Height);
border-style: solid;
border-radius: var(--checkbox__toggle--BorderRadius);
border-color: var(--checkbox__toggle--BorderColor);
border-width: var(--checkbox__toggle--BorderWidth);
}

[part="toggle"] > svg {
width: calc(var(--checkbox__toggle--Width) - 2 * var(--checkbox__toggle--Padding));
height: calc(var(--checkbox__toggle--Height) - 2 * var(--checkbox__toggle--Padding));
fill: var(--checkbox__toggle--Color);
}

[part="toggle"] > svg > path {
fill: var(--checkbox__toggle--Color);
}

:host(:disabled),
:host([disabled]) {
cursor: not-allowed;
}

:host(:disabled) [part="label"],
:host([disabled]) [part="label"] {
color: var(--checkbox--disabled__label--Color);
cursor: not-allowed;
}

:host(:disabled) [part="toggle"],
:host([disabled]) [part="toggle"] {
--checkbox__toggle--BorderColor: var(--checkbox--disabled__toggle--BorderColor);

cursor: not-allowed;
background-color: var(--checkbox--disabled__toggle--BackgroundColor);
}

:host(:disabled) [part="toggle"],
:host([disabled]) [part="toggle"] {
--checkbox__toggle--Color: var(--checkbox--disabled__toggle--Color);
}

[part="label"] {
display: inline-block;
grid-column: 2;
color: var(--checkbox__label--Color);
vertical-align: top;
}
Loading