Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions renderers/lit/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- (v0_9) Re-style the v0_9 catalog components using the default theme from
`web_core`. [#1079](https://github.com/google/A2UI/pull/1079)
- (v0_9) Add missing features to ChoicePicker and CheckBox. [#1145](https://github.com/google/A2UI/pull/1145)

## 0.9.0

Expand Down
2 changes: 1 addition & 1 deletion renderers/lit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"service": true
},
"test": {
"command": "node --test --enable-source-maps --test-reporter spec dist/src/0.8/*.test.js dist/src/v0_9/tests/*.test.js",
"command": "node --test --enable-source-maps --test-reporter spec dist/src/0.8/*.test.js dist/src/v0_9/tests/*.test.js dist/src/v0_9/tests/**/*.test.js",
"dependencies": [
"build"
]
Expand Down
36 changes: 25 additions & 11 deletions renderers/lit/src/v0_9/catalogs/basic/components/CheckBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ export class A2uiCheckBoxElement extends BasicCatalogA2uiLitElement<typeof Check
*/
static styles = css`
:host {
display: inline-block;
display: block;
}
.container {
display: flex;
flex-direction: column;
margin: var(--a2ui-checkbox-margin, var(--a2ui-spacing-m));
}
label.a2ui-checkbox {
Expand All @@ -61,6 +65,11 @@ export class A2uiCheckBoxElement extends BasicCatalogA2uiLitElement<typeof Check
input.invalid {
outline: 1px solid var(--a2ui-checkbox-color-error, red);
}
.error {
color: var(--a2ui-checkbox-color-error, red);
font-size: var(--a2ui-font-size-xs, 0.75rem);
margin-top: 4px;
}
`;

protected createController() {
Expand All @@ -76,16 +85,21 @@ export class A2uiCheckBoxElement extends BasicCatalogA2uiLitElement<typeof Check
const inputClasses = { invalid: isInvalid };

return html`
<label class=${classMap(labelClasses)}>
<input
type="checkbox"
class=${classMap(inputClasses)}
.checked=${props.value || false}
@change=${(e: Event) =>
props.setValue?.((e.target as HTMLInputElement).checked)}
/>
${props.label}
</label>
<div class="container">
<label class=${classMap(labelClasses)}>
<input
type="checkbox"
class=${classMap(inputClasses)}
.checked=${props.value || false}
@change=${(e: Event) =>
props.setValue?.((e.target as HTMLInputElement).checked)}
/>
${props.label}
</label>
${isInvalid && props.validationErrors?.length
? html`<div class="error">${props.validationErrors[0]}</div>`
: nothing}
</div>
`;
}
}
Expand Down
98 changes: 85 additions & 13 deletions renderers/lit/src/v0_9/catalogs/basic/components/ChoicePicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
*/

import { html, nothing, css } from "lit";
import { customElement } from "lit/decorators.js";
import { customElement, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { ChoicePickerApi } from "@a2ui/web_core/v0_9/basic_catalog";
import { BasicCatalogA2uiLitElement } from "../basic-catalog-a2ui-lit-element.js";
import { A2uiController } from "@a2ui/lit/v0_9";
Expand All @@ -32,6 +33,9 @@ export class A2uiChoicePickerElement extends BasicCatalogA2uiLitElement<
* - `--a2ui-choicepicker-label-font-size`: Font size of all labels. Defaults to `--a2ui-label-font-size` then `--a2ui-font-size-s` for the main label.
* - `--a2ui-choicepicker-label-font-weight`: Font weight of the main label. Defaults to `--a2ui-label-font-weight` then `bold`.
* - `--a2ui-choicepicker-gap`: Spacing between options.
* - `--a2ui-choicepicker-filter-padding`: Padding for the filter input. Defaults to `--a2ui-spacing-xs` and `--a2ui-spacing-s` (4px 8px).
* - `--a2ui-choicepicker-chip-padding`: Padding for chips. Defaults to `--a2ui-spacing-s` and `--a2ui-spacing-m` (4px 8px).
* - `--a2ui-choicepicker-chip-border-radius`: Border radius for chips. Defaults to `999px`.
*/
static styles = css`
:host {
Expand All @@ -53,8 +57,43 @@ export class A2uiChoicePickerElement extends BasicCatalogA2uiLitElement<
font-size: var(--a2ui-choicepicker-label-font-size, var(--a2ui-label-font-size, var(--a2ui-font-size-s)));
font-weight: var(--a2ui-choicepicker-label-font-weight, var(--a2ui-label-font-weight, bold));
}
.filter-input {
background-color: var(--a2ui-color-input, #fff);
color: var(--a2ui-color-on-input, #333);
border: var(--a2ui-textfield-border, var(--a2ui-border));
border-radius: var(--a2ui-textfield-border-radius, var(--a2ui-spacing-m));
padding: var(--a2ui-choicepicker-filter-padding, var(--a2ui-spacing-xs, 4px) var(--a2ui-spacing-s, 8px));
font-family: inherit;
}
.filter-input:focus {
outline: none;
border-color: var(--a2ui-textfield-color-border-focus, var(--a2ui-color-primary, #17e));
}
.chips {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: var(--a2ui-choicepicker-gap, var(--a2ui-spacing-xs, 0.25rem));
}
.chip {
padding: var(--a2ui-choicepicker-chip-padding, var(--a2ui-spacing-s, 4px) var(--a2ui-spacing-m, 8px));
border-radius: var(--a2ui-choicepicker-chip-border-radius, 999px);
border: 1px solid var(--a2ui-color-border, #ccc);
background-color: var(--a2ui-color-surface, #fff);
color: var(--a2ui-color-on-surface, inherit);
cursor: pointer;
font-size: var(--a2ui-font-size-xs, 0.75rem);
font-family: inherit;
}
.chip.selected {
background-color: var(--a2ui-color-primary, #007bff);
color: var(--a2ui-color-on-primary, #fff);
border-color: var(--a2ui-color-primary, #007bff);
}
`;

@state() accessor filter = '';

protected createController() {
return new A2uiController(this, ChoicePickerApi);
}
Expand All @@ -65,6 +104,7 @@ export class A2uiChoicePickerElement extends BasicCatalogA2uiLitElement<

const selected = Array.isArray(props.value) ? props.value : [];
const isMulti = props.variant === "multipleSelection";
const isChips = props.displayStyle === "chips";

const toggle = (val: string) => {
if (!props.setValue) return;
Expand All @@ -79,20 +119,52 @@ export class A2uiChoicePickerElement extends BasicCatalogA2uiLitElement<
}
};

const options = (props.options || []).filter(
(opt: any) =>
!props.filterable ||
this.filter === "" ||
String(opt.label).toLowerCase().includes(this.filter.toLowerCase())
);

return html`
${props.label ? html`<label>${props.label}</label>` : nothing}
<div class="options">
${props.options?.map(
(opt: any) => html`
<label>
<input
type=${isMulti ? "checkbox" : "radio"}
.checked=${selected.includes(opt.value)}
@change=${() => toggle(opt.value)}
/>
${opt.label}
</label>
`,
${props.filterable
? html`
<input
type="text"
class="filter-input"
placeholder="Filter options..."
aria-label="Filter options"
.value=${this.filter}
@input=${(e: Event) => (this.filter = (e.target as HTMLInputElement).value)}
/>
`
: nothing}
<div class=${classMap({ options: true, chips: isChips })}>
${options.map((opt: any) =>
isChips
? html`
<button
class=${classMap({
chip: true,
selected: selected.includes(opt.value),
})}
aria-pressed=${selected.includes(opt.value)}
@click=${() => toggle(opt.value)}
>
${opt.label}
</button>
`
: html`
<label>
<input
type=${isMulti ? "checkbox" : "radio"}
.checked=${selected.includes(opt.value)}
@change=${() => toggle(opt.value)}
/>
${opt.label}
</label>
`
)}
</div>
`;
Expand Down
82 changes: 82 additions & 0 deletions renderers/lit/src/v0_9/tests/components/CheckBox.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { setupTestDom, teardownTestDom, asyncUpdate } from "../dom-setup.js";
import assert from "node:assert";
import { describe, it, beforeEach, after, before } from "node:test";
import { ComponentContext, MessageProcessor } from "@a2ui/web_core/v0_9";

describe("CheckBox Component", () => {
let basicCatalog: any;

before(async () => {
setupTestDom();
basicCatalog = (await import("../../catalogs/basic/index.js")).basicCatalog;
// Ensure component is registered
await import("../../catalogs/basic/components/CheckBox.js");
});

after(teardownTestDom);

let processor: MessageProcessor<any>;
let surface: any;

beforeEach(() => {
processor = new MessageProcessor([basicCatalog]);
processor.processMessages([
{
version: "v0.9",
createSurface: {
surfaceId: "test-surface",
catalogId: basicCatalog.id,
},
},
{
version: "v0.9",
updateComponents: {
surfaceId: "test-surface",
components: [
{
id: "checkbox_invalid",
component: "CheckBox",
label: "Check me",
value: false,
isValid: false,
validationErrors: ["This is required"],
},
],
},
},
]);
surface = processor.model.getSurface("test-surface")!;
});

it("should render validation error in CheckBox", async () => {
const el = document.createElement("a2ui-checkbox") as any;
document.body.appendChild(el);

const context = new ComponentContext(surface, "checkbox_invalid");
await asyncUpdate(el, (e) => {
e.context = context;
});

const errorDiv = el.shadowRoot.querySelector(".error");
assert.ok(errorDiv);
assert.strictEqual(errorDiv.textContent.trim(), "This is required");

document.body.removeChild(el);
});
});
Loading
Loading