Skip to content

Commit 1a31236

Browse files
authored
1. fix contact sample 2. Make dropdowns multi-selection in lit (google#594)
* fix contact sample * Fix dropdown and make it multi-choice
1 parent aa03e16 commit 1a31236

2 files changed

Lines changed: 195 additions & 54 deletions

File tree

renderers/lit/src/0.8/ui/multiple-choice.ts

Lines changed: 176 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import { html, css, PropertyValues, nothing } from "lit";
18-
import { customElement, property } from "lit/decorators.js";
18+
import { customElement, property, state } from "lit/decorators.js";
1919
import { Root } from "./root.js";
2020
import { A2uiMessageProcessor } from "@a2ui/web_core/data/model-processor";
2121
import * as Primitives from "@a2ui/web_core/types/primitives";
@@ -35,6 +35,9 @@ export class MultipleChoice extends Root {
3535
@property()
3636
accessor selections: Primitives.StringValue | string[] = [];
3737

38+
@state()
39+
accessor isOpen = false;
40+
3841
static styles = [
3942
structuralStyles,
4043
css`
@@ -46,20 +49,129 @@ export class MultipleChoice extends Root {
4649
display: block;
4750
flex: var(--weight);
4851
min-height: 0;
49-
overflow: auto;
52+
position: relative;
53+
font-family: 'Google Sans', 'Roboto', sans-serif;
54+
}
55+
56+
.container {
57+
display: flex;
58+
flex-direction: column;
59+
gap: 4px;
60+
position: relative;
61+
}
62+
63+
/* Header / Trigger */
64+
.dropdown-header {
65+
display: flex;
66+
align-items: center;
67+
justify-content: space-between;
68+
padding: 12px 16px;
69+
background: var(--md-sys-color-surface);
70+
border: 1px solid var(--md-sys-color-outline-variant);
71+
border-radius: 8px;
72+
cursor: pointer;
73+
user-select: none;
74+
transition: background-color 0.2s;
75+
box-shadow: var(--md-sys-elevation-level1);
76+
}
77+
78+
.dropdown-header:hover {
79+
background: var(--md-sys-color-surface-container-low);
80+
}
81+
82+
.header-text {
83+
font-size: 1rem;
84+
color: var(--md-sys-color-on-surface);
85+
font-weight: 400;
86+
}
87+
88+
.chevron {
89+
color: var(--md-sys-color-primary);
90+
font-size: 1.2rem;
91+
transition: transform 0.2s ease;
92+
}
93+
94+
.chevron.open {
95+
transform: rotate(180deg);
96+
}
97+
98+
/* Dropdown List */
99+
.options-list {
100+
background: var(--md-sys-color-surface);
101+
border: 1px solid var(--md-sys-color-outline-variant);
102+
border-radius: 8px; /* Consistent rounding */
103+
box-shadow: none; /* Remove shadow for inline feel, or keep subtle */
104+
overflow-y: auto;
105+
padding: 0;
106+
display: none;
107+
flex-direction: column;
108+
margin-top: 4px; /* Small gap */
109+
max-height: 0; /* Animate height? */
110+
transition: max-height 0.2s ease-out;
50111
}
51112
52-
select {
53-
width: 100%;
113+
.options-list.open {
114+
display: flex;
115+
max-height: 300px; /* Limit height but allow scrolling */
116+
border: 1px solid var(--md-sys-color-outline-variant); /* efficient border */
54117
}
55118
56-
.description {
119+
/* Option Item (Checkbox style) */
120+
.option-item {
121+
display: flex;
122+
align-items: center;
123+
gap: 12px;
124+
padding: 12px 16px;
125+
cursor: pointer;
126+
color: var(--md-sys-color-on-surface);
127+
font-size: 0.95rem;
128+
transition: background-color 0.1s;
129+
}
130+
131+
.option-item:hover {
132+
background: var(--md-sys-color-surface-container-highest);
133+
}
134+
135+
/* Custom Checkbox */
136+
.checkbox {
137+
width: 18px;
138+
height: 18px;
139+
border: 2px solid var(--md-sys-color-outline);
140+
border-radius: 2px;
141+
display: flex;
142+
align-items: center;
143+
justify-content: center;
144+
transition: all 0.2s;
145+
flex-shrink: 0;
146+
}
147+
148+
.option-item.selected .checkbox {
149+
background: var(--md-sys-color-primary);
150+
border-color: var(--md-sys-color-primary);
151+
}
152+
153+
.checkbox-icon {
154+
color: var(--md-sys-color-on-primary);
155+
font-size: 14px;
156+
font-weight: bold;
157+
opacity: 0;
158+
transform: scale(0.5);
159+
transition: all 0.2s;
160+
}
161+
162+
.option-item.selected .checkbox-icon {
163+
opacity: 1;
164+
transform: scale(1);
165+
}
166+
167+
@keyframes fadeIn {
168+
from { opacity: 0; transform: translateY(-8px); }
169+
to { opacity: 1; transform: translateY(0); }
57170
}
58171
`,
59172
];
60173

61174
#setBoundValue(value: string[]) {
62-
console.log(value);
63175
if (!this.selections || !this.processor) {
64176
return;
65177
}
@@ -78,65 +190,76 @@ export class MultipleChoice extends Root {
78190
);
79191
}
80192

81-
protected willUpdate(changedProperties: PropertyValues<this>): void {
82-
const shouldUpdate = changedProperties.has("options");
83-
if (!shouldUpdate) {
84-
return;
85-
}
86-
193+
getCurrentSelections(): string[] {
87194
if (!this.processor || !this.component || Array.isArray(this.selections)) {
88-
return;
195+
return [];
89196
}
90197

91-
this.selections;
92-
93198
const selectionValue = this.processor.getData(
94199
this.component,
95200
this.selections.path!,
96201
this.surfaceId ?? A2uiMessageProcessor.DEFAULT_SURFACE_ID
97202
);
98203

99-
if (!Array.isArray(selectionValue)) {
100-
return;
101-
}
204+
return Array.isArray(selectionValue) ? (selectionValue as string[]) : [];
205+
}
102206

103-
this.#setBoundValue(selectionValue as string[]);
207+
toggleSelection(value: string) {
208+
const current = this.getCurrentSelections();
209+
if (current.includes(value)) {
210+
this.#setBoundValue(current.filter((v) => v !== value));
211+
} else {
212+
this.#setBoundValue([...current, value]);
213+
}
214+
this.requestUpdate();
104215
}
105216

106217
render() {
107-
return html`<section class=${classMap(
108-
this.theme.components.MultipleChoice.container
109-
)}>
110-
<label class=${classMap(
111-
this.theme.components.MultipleChoice.label
112-
)} for="data">${this.description ?? "Select an item"}</label>
113-
<select
114-
name="data"
115-
id="data"
116-
class=${classMap(this.theme.components.MultipleChoice.element)}
117-
style=${
118-
this.theme.additionalStyles?.MultipleChoice
119-
? styleMap(this.theme.additionalStyles?.MultipleChoice)
120-
: nothing
121-
}
122-
@change=${(evt: Event) => {
123-
if (!(evt.target instanceof HTMLSelectElement)) {
124-
return;
125-
}
126-
127-
this.#setBoundValue([evt.target.value]);
128-
}}
129-
>
130-
${this.options.map((option) => {
131-
const label = extractStringValue(
132-
option.label,
133-
this.component,
134-
this.processor,
135-
this.surfaceId
136-
);
137-
return html`<option ${option.value}>${label}</option>`;
138-
})}
139-
</select>
140-
</section>`;
218+
const currentSelections = this.getCurrentSelections();
219+
const count = currentSelections.length;
220+
const headerText = count > 0 ? `${count} Selected` : (this.description ?? "Select items");
221+
222+
return html`
223+
<div class="container">
224+
<div
225+
class="dropdown-header"
226+
@click=${() => this.isOpen = !this.isOpen}
227+
>
228+
<span class="header-text">${headerText}</span>
229+
<span class="chevron ${this.isOpen ? "open" : ""}">
230+
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" fill="currentColor">
231+
<path d="M480-345 240-585l56-56 184 184 184-184 56 56-240 240Z"/>
232+
</svg>
233+
</span>
234+
</div>
235+
236+
<div class="options-list ${this.isOpen ? "open" : ""}">
237+
${this.options.map((option) => {
238+
const label = extractStringValue(
239+
option.label,
240+
this.component,
241+
this.processor,
242+
this.surfaceId
243+
);
244+
const isSelected = currentSelections.includes(option.value);
245+
246+
return html`
247+
<div
248+
class="option-item ${isSelected ? "selected" : ""}"
249+
@click=${(e: Event) => {
250+
e.stopPropagation();
251+
this.toggleSelection(option.value);
252+
}}
253+
>
254+
<div class="checkbox">
255+
<span class="checkbox-icon"></span>
256+
</div>
257+
<span>${label}</span>
258+
</div>
259+
`;
260+
})}
261+
</div>
262+
</div>
263+
`;
141264
}
142265
}

samples/client/lit/contact/tsconfig.json

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,25 @@
2929
"strict": false,
3030
"noUnusedLocals": false,
3131
"noUnusedParameters": true,
32-
"noFallthroughCasesInSwitch": true
32+
"noFallthroughCasesInSwitch": true,
33+
"baseUrl": ".",
34+
"paths": {
35+
"lit": [
36+
"../../../../renderers/lit/node_modules/lit"
37+
],
38+
"lit-html": [
39+
"../../../../renderers/lit/node_modules/lit-html"
40+
],
41+
"lit-element": [
42+
"../../../../renderers/lit/node_modules/lit-element"
43+
],
44+
"@lit/reactive-element": [
45+
"../../../../renderers/lit/node_modules/@lit/reactive-element"
46+
],
47+
"@lit/context": [
48+
"../../../../renderers/lit/node_modules/@lit/context"
49+
]
50+
}
3351
},
3452
"references": [{ "path": "../../../../renderers/lit" }]
3553
}

0 commit comments

Comments
 (0)