@intent-framework/dom 0.1.0-alpha.6 → 0.1.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/dist/index.js +247 -143
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -22,6 +22,66 @@ npm install @intent-framework/core @intent-framework/dom
|
|
|
22
22
|
- Opt-in screen-name heading via `showScreenName`
|
|
23
23
|
- Opt-in semantic data attributes via `showSemanticIds`
|
|
24
24
|
|
|
25
|
+
## renderDom behavior
|
|
26
|
+
|
|
27
|
+
### Input types
|
|
28
|
+
|
|
29
|
+
| Ask kind | Renders as |
|
|
30
|
+
|----------|------------|
|
|
31
|
+
| `$.state.text()` / default | `<input type="text">` |
|
|
32
|
+
| `asContact("email")` | `<input type="email">` with `autocomplete="email"` |
|
|
33
|
+
| `asSecret()` | `<input type="password">` |
|
|
34
|
+
| `$.state.boolean()` | `<input type="checkbox">` |
|
|
35
|
+
| `$.state.choice()` with `asChoice()` | `<select>` with `<option>` elements |
|
|
36
|
+
|
|
37
|
+
### Boolean and choice asks
|
|
38
|
+
|
|
39
|
+
Boolean asks render as checkboxes. The checked state is synced with the underlying `BooleanState`.
|
|
40
|
+
|
|
41
|
+
Choice asks render as `<select>` elements. Options are populated from the `ChoiceState` options array. The selected value is synced with the underlying state.
|
|
42
|
+
|
|
43
|
+
### Blocked reasons
|
|
44
|
+
|
|
45
|
+
When an action is disabled due to blocked reasons, the renderer adds:
|
|
46
|
+
|
|
47
|
+
- `disabled` attribute on the button
|
|
48
|
+
- `<p class="intent-blocked-reason" role="alert">` with the first blocked reason text
|
|
49
|
+
- `aria-describedby` on the button pointing to the reason element
|
|
50
|
+
|
|
51
|
+
When the action becomes enabled, the reason element is removed from the DOM and `aria-describedby` is cleared.
|
|
52
|
+
|
|
53
|
+
### Single-surface output
|
|
54
|
+
|
|
55
|
+
When a screen has exactly one surface, the output is backward compatible: a single `<main>` element containing one `<form>` with all asks and actions. No surface name suffix is added to DOM ids.
|
|
56
|
+
|
|
57
|
+
### Multi-surface output
|
|
58
|
+
|
|
59
|
+
When a screen has multiple surfaces, each surface renders as a separate `<section>` with `aria-label` set to the surface name. Each section contains its own `<form>` with only the items belonging to that surface.
|
|
60
|
+
|
|
61
|
+
**DOM id suffixes:** All ask inputs, buttons, hint paragraphs, reason paragraphs, and feedback outputs get a `--<surfaceName>` suffix to avoid id collisions.
|
|
62
|
+
|
|
63
|
+
**Duplicate controls share state:** The same ask appearing in multiple surfaces shares the underlying state. Typing in one surface updates the input in all surfaces.
|
|
64
|
+
|
|
65
|
+
**Duplicate action buttons:** The same action appearing in multiple surfaces renders separate buttons, all executing the same `executeAct()` call.
|
|
66
|
+
|
|
67
|
+
**Per-surface feedback:** Each surface gets its own `<output aria-live="polite">` element. Action feedback is updated independently per surface.
|
|
68
|
+
|
|
69
|
+
**`data-intent-*` semantic ids:** The `data-intent-ask` and `data-intent-action` attributes remain the same across all copies — they identify the semantic node, not the DOM position.
|
|
70
|
+
|
|
71
|
+
### Enter key default action
|
|
72
|
+
|
|
73
|
+
When exactly one primary action exists (or exactly one action total), pressing Enter in any text input triggers that action. The Enter hint ("Press Enter to ...") appears reactively when the default action becomes enabled, and is hidden when disabled. Shift/ Meta/ Ctrl/ Alt + Enter are ignored. Enter is ignored on `<textarea>`, `<select>`, and checkbox inputs.
|
|
74
|
+
|
|
75
|
+
### Options
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
renderDom(Screen, {
|
|
79
|
+
target: document.getElementById("root")!,
|
|
80
|
+
showScreenName: true, // render screen name as <h1>
|
|
81
|
+
showSemanticIds: true, // add data-intent-screen, data-intent-ask, data-intent-action attributes
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
25
85
|
## Minimal example
|
|
26
86
|
|
|
27
87
|
```ts
|
package/dist/index.js
CHANGED
|
@@ -63,15 +63,18 @@ function renderRouter(router, options) {
|
|
|
63
63
|
|
|
64
64
|
//#endregion
|
|
65
65
|
//#region src/index.ts
|
|
66
|
-
function getReasonId(actId) {
|
|
67
|
-
return `${actId}-reason`;
|
|
66
|
+
function getReasonId(actId, suffix = "") {
|
|
67
|
+
return `${actId}-reason${suffix}`;
|
|
68
68
|
}
|
|
69
|
-
function getEnterHintId(askId) {
|
|
70
|
-
return `${askId}-enter-hint`;
|
|
69
|
+
function getEnterHintId(askId, suffix = "") {
|
|
70
|
+
return `${askId}-enter-hint${suffix}`;
|
|
71
71
|
}
|
|
72
72
|
function sanitizeLabel(label) {
|
|
73
73
|
return label.replace(/\.+$/, "");
|
|
74
74
|
}
|
|
75
|
+
function surfaceSuffix(surfaceName, isMulti) {
|
|
76
|
+
return isMulti ? `--${surfaceName}` : "";
|
|
77
|
+
}
|
|
75
78
|
function findDefaultAction(acts) {
|
|
76
79
|
const primaryActs = acts.filter((a) => a.primary);
|
|
77
80
|
if (primaryActs.length === 1) return primaryActs[0];
|
|
@@ -81,19 +84,22 @@ function findDefaultAction(acts) {
|
|
|
81
84
|
function renderDom(screenDef, options) {
|
|
82
85
|
const { target, services, showScreenName, showSemanticIds } = options;
|
|
83
86
|
target.innerHTML = "";
|
|
87
|
+
const isMulti = screenDef.surfaces.length > 1;
|
|
84
88
|
const root = buildDom(screenDef, showScreenName, showSemanticIds);
|
|
85
89
|
target.appendChild(root);
|
|
86
90
|
const runtime = createScreenRuntime(screenDef, { services });
|
|
87
91
|
runtime.start();
|
|
88
|
-
const form = target.querySelector("form");
|
|
89
|
-
const output = target.querySelector("output#feedback-output");
|
|
90
92
|
const unsubscribers = [];
|
|
91
93
|
for (const act of screenDef.acts) {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
const unsub = act.enabled.subscribe(() => {
|
|
95
|
+
for (const surface of screenDef.surfaces) {
|
|
96
|
+
const suffix = surfaceSuffix(surface.name, isMulti);
|
|
97
|
+
const form = getSurfaceForm(root, surface, isMulti);
|
|
98
|
+
if (!form) continue;
|
|
99
|
+
const button = form.querySelector(`#${act.id}${suffix}`);
|
|
100
|
+
if (!button) continue;
|
|
95
101
|
button.disabled = !act.enabled.current;
|
|
96
|
-
const reasonId = getReasonId(act.id);
|
|
102
|
+
const reasonId = getReasonId(act.id, suffix);
|
|
97
103
|
let reasonEl = form.querySelector(`#${reasonId}`);
|
|
98
104
|
if (!act.enabled.current && act.blockedReasons.length > 0) {
|
|
99
105
|
button.setAttribute("aria-describedby", reasonId);
|
|
@@ -109,18 +115,27 @@ function renderDom(screenDef, options) {
|
|
|
109
115
|
button.removeAttribute("aria-describedby");
|
|
110
116
|
if (reasonEl) reasonEl.remove();
|
|
111
117
|
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
unsubscribers.push(unsub);
|
|
115
121
|
}
|
|
116
122
|
for (const act of screenDef.acts) {
|
|
117
123
|
const unsub = act.onStatusChange(() => {
|
|
118
|
-
|
|
124
|
+
for (const surface of screenDef.surfaces) {
|
|
125
|
+
const suffix = surfaceSuffix(surface.name, isMulti);
|
|
126
|
+
const form = getSurfaceForm(root, surface, isMulti);
|
|
127
|
+
if (!form) continue;
|
|
128
|
+
const output = form.querySelector(`#feedback-output${suffix}`);
|
|
129
|
+
if (output) updateFeedback(act, output);
|
|
130
|
+
}
|
|
119
131
|
});
|
|
120
132
|
unsubscribers.push(unsub);
|
|
121
133
|
}
|
|
122
|
-
for (const act of screenDef.acts) {
|
|
123
|
-
const
|
|
134
|
+
for (const act of screenDef.acts) for (const surface of screenDef.surfaces) {
|
|
135
|
+
const suffix = surfaceSuffix(surface.name, isMulti);
|
|
136
|
+
const form = getSurfaceForm(root, surface, isMulti);
|
|
137
|
+
if (!form) continue;
|
|
138
|
+
const button = form.querySelector(`#${act.id}${suffix}`);
|
|
124
139
|
if (button) button.addEventListener("click", () => {
|
|
125
140
|
if (act.enabled.current) runtime.executeAct(act);
|
|
126
141
|
});
|
|
@@ -129,52 +144,88 @@ function renderDom(screenDef, options) {
|
|
|
129
144
|
if (defaultActionForHint) {
|
|
130
145
|
const unsub = defaultActionForHint.enabled.subscribe(() => {
|
|
131
146
|
const isEnabled = defaultActionForHint.enabled.current;
|
|
132
|
-
for (const
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
for (const surface of screenDef.surfaces) {
|
|
148
|
+
const suffix = surfaceSuffix(surface.name, isMulti);
|
|
149
|
+
const form = getSurfaceForm(root, surface, isMulti);
|
|
150
|
+
if (!form) continue;
|
|
151
|
+
for (const item of surface.items) {
|
|
152
|
+
if (!("state" in item)) continue;
|
|
153
|
+
const ask = item;
|
|
154
|
+
const input = form.querySelector(`#${ask.id}${suffix}`);
|
|
155
|
+
const hint = form.querySelector(`#${getEnterHintId(ask.id, suffix)}`);
|
|
156
|
+
if (input && hint) {
|
|
157
|
+
const hintId = getEnterHintId(ask.id, suffix);
|
|
158
|
+
if (isEnabled) {
|
|
159
|
+
hint.style.display = "";
|
|
160
|
+
const existing = input.getAttribute("aria-describedby") || "";
|
|
161
|
+
const ids = existing.split(/\s+/).filter(Boolean);
|
|
162
|
+
if (!ids.includes(hintId)) ids.push(hintId);
|
|
163
|
+
input.setAttribute("aria-describedby", ids.join(" "));
|
|
164
|
+
} else {
|
|
165
|
+
hint.style.display = "none";
|
|
166
|
+
const existing = input.getAttribute("aria-describedby") || "";
|
|
167
|
+
const ids = existing.split(/\s+/).filter(Boolean).filter((id) => id !== hintId);
|
|
168
|
+
if (ids.length > 0) input.setAttribute("aria-describedby", ids.join(" "));
|
|
169
|
+
else input.removeAttribute("aria-describedby");
|
|
170
|
+
}
|
|
149
171
|
}
|
|
150
172
|
}
|
|
151
173
|
}
|
|
152
174
|
});
|
|
153
175
|
unsubscribers.push(unsub);
|
|
154
176
|
}
|
|
155
|
-
for (const
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
177
|
+
for (const surface of screenDef.surfaces) {
|
|
178
|
+
const suffix = surfaceSuffix(surface.name, isMulti);
|
|
179
|
+
const form = getSurfaceForm(root, surface, isMulti);
|
|
180
|
+
if (!form) continue;
|
|
181
|
+
for (const item of surface.items) {
|
|
182
|
+
if (!("state" in item)) continue;
|
|
183
|
+
const ask = item;
|
|
184
|
+
const input = form.querySelector(`#${ask.id}${suffix}`);
|
|
185
|
+
if (input) {
|
|
186
|
+
const onKeyDown = (event) => {
|
|
187
|
+
if (event.key !== "Enter") return;
|
|
188
|
+
if (event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
189
|
+
if (input.tagName === "TEXTAREA") return;
|
|
190
|
+
if (input.tagName === "SELECT") return;
|
|
191
|
+
if (input.type === "checkbox") return;
|
|
192
|
+
const defaultAction = findDefaultAction(screenDef.acts);
|
|
193
|
+
if (!defaultAction || !defaultAction.enabled.current) return;
|
|
194
|
+
event.preventDefault();
|
|
195
|
+
runtime.executeAct(defaultAction);
|
|
196
|
+
};
|
|
197
|
+
input.addEventListener("keydown", onKeyDown);
|
|
198
|
+
unsubscribers.push(() => input.removeEventListener("keydown", onKeyDown));
|
|
199
|
+
}
|
|
171
200
|
}
|
|
172
201
|
}
|
|
202
|
+
for (const ask of screenDef.asks) {
|
|
203
|
+
const unsub = ask.subscribe(() => {
|
|
204
|
+
for (const surface of screenDef.surfaces) {
|
|
205
|
+
const suffix = surfaceSuffix(surface.name, isMulti);
|
|
206
|
+
const form = getSurfaceForm(root, surface, isMulti);
|
|
207
|
+
if (!form) continue;
|
|
208
|
+
const control = form.querySelector(`#${ask.id}${suffix}`);
|
|
209
|
+
if (!control) continue;
|
|
210
|
+
if (typeof ask.state.value === "boolean") control.checked = ask.state.value;
|
|
211
|
+
else if (ask.kind === "choice") control.value = ask.state.value;
|
|
212
|
+
else control.value = ask.state.value;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
unsubscribers.push(unsub);
|
|
216
|
+
}
|
|
173
217
|
return () => {
|
|
174
218
|
for (const unsub of unsubscribers) unsub();
|
|
175
219
|
runtime.dispose();
|
|
176
220
|
};
|
|
177
221
|
}
|
|
222
|
+
function getSurfaceForm(root, surface, isMulti) {
|
|
223
|
+
if (isMulti) {
|
|
224
|
+
const section = root.querySelector(`#${surface.id}`);
|
|
225
|
+
return section ? section.querySelector("form") : null;
|
|
226
|
+
}
|
|
227
|
+
return root.querySelector("form");
|
|
228
|
+
}
|
|
178
229
|
function buildDom(screenDef, showScreenName, showSemanticIds) {
|
|
179
230
|
let inspected;
|
|
180
231
|
let askSemanticIds;
|
|
@@ -184,118 +235,171 @@ function buildDom(screenDef, showScreenName, showSemanticIds) {
|
|
|
184
235
|
askSemanticIds = new Map(inspected.asks.map((a) => [a.id, a.semanticId]));
|
|
185
236
|
actSemanticIds = new Map(inspected.acts.map((a) => [a.id, a.semanticId]));
|
|
186
237
|
}
|
|
187
|
-
const
|
|
238
|
+
const isMulti = screenDef.surfaces.length > 1;
|
|
188
239
|
const main = document.createElement("main");
|
|
189
|
-
if (surface) main.id = surface.id;
|
|
190
240
|
if (showSemanticIds && inspected) main.setAttribute("data-intent-screen", inspected.semanticId);
|
|
191
241
|
if (showScreenName) {
|
|
192
242
|
const heading = document.createElement("h1");
|
|
193
243
|
heading.textContent = screenDef.name;
|
|
194
244
|
main.appendChild(heading);
|
|
195
245
|
}
|
|
246
|
+
if (isMulti) for (const surface of screenDef.surfaces) {
|
|
247
|
+
const section = buildSurface(screenDef, surface, showSemanticIds, askSemanticIds, actSemanticIds, isMulti);
|
|
248
|
+
main.appendChild(section);
|
|
249
|
+
}
|
|
250
|
+
else if (screenDef.surfaces.length === 1) {
|
|
251
|
+
const surface = screenDef.surfaces[0];
|
|
252
|
+
main.id = surface.id;
|
|
253
|
+
const form = buildForm(screenDef, surface, showSemanticIds, askSemanticIds, actSemanticIds, isMulti);
|
|
254
|
+
main.appendChild(form);
|
|
255
|
+
}
|
|
256
|
+
return main;
|
|
257
|
+
}
|
|
258
|
+
function buildSurface(screenDef, surface, showSemanticIds, askSemanticIds, actSemanticIds, isMulti) {
|
|
259
|
+
const section = document.createElement("section");
|
|
260
|
+
section.id = surface.id;
|
|
261
|
+
section.setAttribute("aria-label", surface.name);
|
|
262
|
+
const form = buildForm(screenDef, surface, showSemanticIds, askSemanticIds, actSemanticIds, isMulti);
|
|
263
|
+
section.appendChild(form);
|
|
264
|
+
return section;
|
|
265
|
+
}
|
|
266
|
+
function buildForm(screenDef, surface, showSemanticIds, askSemanticIds, actSemanticIds, isMulti) {
|
|
267
|
+
const isMultiSurface = isMulti ?? screenDef.surfaces.length > 1;
|
|
268
|
+
const suffix = surfaceSuffix(surface.name, isMultiSurface);
|
|
196
269
|
const form = document.createElement("form");
|
|
197
270
|
form.setAttribute("method", "POST");
|
|
198
271
|
form.setAttribute("novalidate", "");
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
control.name = ask.id;
|
|
213
|
-
if (showSemanticIds && askSemanticIds) {
|
|
214
|
-
const sid = askSemanticIds.get(ask.id);
|
|
215
|
-
if (sid) control.setAttribute("data-intent-ask", sid);
|
|
216
|
-
}
|
|
217
|
-
if (ask.required) control.required = true;
|
|
218
|
-
if (ask.kind === "contact" && ask.contactKind) control.setAttribute("autocomplete", ask.contactKind);
|
|
219
|
-
if (typeof ask.state.value === "boolean") {
|
|
220
|
-
const stateObj = ask.state;
|
|
221
|
-
control.checked = stateObj.value;
|
|
222
|
-
control.addEventListener("change", () => {
|
|
223
|
-
stateObj.set(control.checked);
|
|
224
|
-
});
|
|
225
|
-
} else if (ask.kind === "choice") {
|
|
226
|
-
const stateObj = ask.state;
|
|
227
|
-
const select = control;
|
|
228
|
-
for (const opt of stateObj.options) {
|
|
229
|
-
const option = document.createElement("option");
|
|
230
|
-
option.value = opt;
|
|
231
|
-
option.textContent = opt;
|
|
232
|
-
select.appendChild(option);
|
|
233
|
-
}
|
|
234
|
-
select.value = stateObj.value;
|
|
235
|
-
select.addEventListener("change", () => {
|
|
236
|
-
stateObj.set(select.value);
|
|
272
|
+
if (isMultiSurface) {
|
|
273
|
+
for (const item of surface.items) if ("state" in item) buildAskControl(form, screenDef, item, suffix, showSemanticIds, askSemanticIds);
|
|
274
|
+
else if ("handler" in item) buildActionButton(form, item, suffix, showSemanticIds, actSemanticIds);
|
|
275
|
+
} else {
|
|
276
|
+
const surfaceAskIds = /* @__PURE__ */ new Set();
|
|
277
|
+
const surfaceActIds = /* @__PURE__ */ new Set();
|
|
278
|
+
const orderedItems = [];
|
|
279
|
+
for (const item of surface.items) if ("state" in item) {
|
|
280
|
+
const ask = item;
|
|
281
|
+
surfaceAskIds.add(ask.id);
|
|
282
|
+
orderedItems.push({
|
|
283
|
+
kind: "ask",
|
|
284
|
+
node: ask
|
|
237
285
|
});
|
|
238
|
-
} else {
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
286
|
+
} else if ("handler" in item) {
|
|
287
|
+
const act = item;
|
|
288
|
+
surfaceActIds.add(act.id);
|
|
289
|
+
orderedItems.push({
|
|
290
|
+
kind: "act",
|
|
291
|
+
node: act
|
|
243
292
|
});
|
|
244
293
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
294
|
+
for (const ask of screenDef.asks) if (!surfaceAskIds.has(ask.id)) orderedItems.push({
|
|
295
|
+
kind: "ask",
|
|
296
|
+
node: ask
|
|
297
|
+
});
|
|
298
|
+
for (const act of screenDef.acts) if (!surfaceActIds.has(act.id)) orderedItems.push({
|
|
299
|
+
kind: "act",
|
|
300
|
+
node: act
|
|
301
|
+
});
|
|
302
|
+
for (const item of orderedItems) if (item.kind === "ask") buildAskControl(form, screenDef, item.node, suffix, showSemanticIds, askSemanticIds);
|
|
303
|
+
else buildActionButton(form, item.node, suffix, showSemanticIds, actSemanticIds);
|
|
304
|
+
}
|
|
305
|
+
const output = document.createElement("output");
|
|
306
|
+
output.id = `feedback-output${suffix}`;
|
|
307
|
+
output.setAttribute("aria-live", "polite");
|
|
308
|
+
form.appendChild(output);
|
|
309
|
+
return form;
|
|
310
|
+
}
|
|
311
|
+
function buildAskControl(form, screenDef, ask, suffix, showSemanticIds, askSemanticIds) {
|
|
312
|
+
const container = document.createElement("div");
|
|
313
|
+
container.className = "ask-group";
|
|
314
|
+
const label = document.createElement("label");
|
|
315
|
+
label.textContent = ask.label;
|
|
316
|
+
label.htmlFor = `${ask.id}${suffix}`;
|
|
317
|
+
if (showSemanticIds && askSemanticIds) {
|
|
318
|
+
const sid = askSemanticIds.get(ask.id);
|
|
319
|
+
if (sid) label.setAttribute("data-intent-ask", sid);
|
|
320
|
+
}
|
|
321
|
+
container.appendChild(label);
|
|
322
|
+
const control = createInputForAsk(ask);
|
|
323
|
+
control.id = `${ask.id}${suffix}`;
|
|
324
|
+
control.name = `${ask.id}${suffix}`;
|
|
325
|
+
if (showSemanticIds && askSemanticIds) {
|
|
326
|
+
const sid = askSemanticIds.get(ask.id);
|
|
327
|
+
if (sid) control.setAttribute("data-intent-ask", sid);
|
|
328
|
+
}
|
|
329
|
+
if (ask.required) control.required = true;
|
|
330
|
+
if (ask.kind === "contact" && ask.contactKind) control.setAttribute("autocomplete", ask.contactKind);
|
|
331
|
+
const stateObj = ask.state;
|
|
332
|
+
if (typeof ask.state.value === "boolean") {
|
|
333
|
+
control.checked = stateObj.value;
|
|
334
|
+
control.addEventListener("change", () => {
|
|
335
|
+
stateObj.set(control.checked);
|
|
336
|
+
});
|
|
337
|
+
} else if (ask.kind === "choice") {
|
|
338
|
+
const select = control;
|
|
339
|
+
const options = stateObj.options;
|
|
340
|
+
for (const opt of options) {
|
|
341
|
+
const option = document.createElement("option");
|
|
342
|
+
option.value = opt;
|
|
343
|
+
option.textContent = opt;
|
|
344
|
+
select.appendChild(option);
|
|
265
345
|
}
|
|
266
|
-
|
|
346
|
+
select.value = stateObj.value;
|
|
347
|
+
select.addEventListener("change", () => {
|
|
348
|
+
stateObj.set(select.value);
|
|
349
|
+
});
|
|
350
|
+
} else {
|
|
351
|
+
const textInput = control;
|
|
352
|
+
textInput.addEventListener("input", () => {
|
|
353
|
+
if (typeof stateObj.set === "function") stateObj.set(textInput.value);
|
|
354
|
+
});
|
|
267
355
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
356
|
+
container.appendChild(control);
|
|
357
|
+
if (ask.hintText) {
|
|
358
|
+
const hint = document.createElement("p");
|
|
359
|
+
hint.id = `${ask.id}-hint${suffix}`;
|
|
360
|
+
hint.textContent = ask.hintText;
|
|
361
|
+
container.appendChild(hint);
|
|
362
|
+
}
|
|
363
|
+
const defaultAction = findDefaultAction(screenDef.acts);
|
|
364
|
+
if (defaultAction) {
|
|
365
|
+
const hintId = getEnterHintId(ask.id, suffix);
|
|
366
|
+
const hint = document.createElement("p");
|
|
367
|
+
hint.id = hintId;
|
|
368
|
+
hint.textContent = `Press Enter to ${sanitizeLabel(defaultAction.label)}.`;
|
|
369
|
+
if (!defaultAction.enabled.current) hint.style.display = "none";
|
|
370
|
+
container.appendChild(hint);
|
|
371
|
+
if (defaultAction.enabled.current) {
|
|
372
|
+
const existing = control.getAttribute("aria-describedby");
|
|
373
|
+
if (existing) control.setAttribute("aria-describedby", `${existing} ${hintId}`);
|
|
374
|
+
else control.setAttribute("aria-describedby", hintId);
|
|
276
375
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
376
|
+
}
|
|
377
|
+
form.appendChild(container);
|
|
378
|
+
}
|
|
379
|
+
function buildActionButton(form, act, suffix, showSemanticIds, actSemanticIds) {
|
|
380
|
+
const button = document.createElement("button");
|
|
381
|
+
button.id = `${act.id}${suffix}`;
|
|
382
|
+
button.type = "button";
|
|
383
|
+
button.textContent = act.label;
|
|
384
|
+
if (showSemanticIds && actSemanticIds) {
|
|
385
|
+
const sid = actSemanticIds.get(act.id);
|
|
386
|
+
if (sid) button.setAttribute("data-intent-action", sid);
|
|
387
|
+
}
|
|
388
|
+
if (act.primary) button.className = "primary";
|
|
389
|
+
if (!act.enabled.current) {
|
|
390
|
+
button.disabled = true;
|
|
391
|
+
if (act.blockedReasons.length > 0) {
|
|
392
|
+
const reasonId = getReasonId(act.id, suffix);
|
|
393
|
+
button.setAttribute("aria-describedby", reasonId);
|
|
394
|
+
const reasonEl = document.createElement("p");
|
|
395
|
+
reasonEl.id = reasonId;
|
|
396
|
+
reasonEl.className = "intent-blocked-reason";
|
|
397
|
+
reasonEl.setAttribute("role", "alert");
|
|
398
|
+
reasonEl.textContent = act.blockedReasons[0];
|
|
399
|
+
form.appendChild(reasonEl);
|
|
290
400
|
}
|
|
291
|
-
form.appendChild(button);
|
|
292
401
|
}
|
|
293
|
-
|
|
294
|
-
output.id = "feedback-output";
|
|
295
|
-
output.setAttribute("aria-live", "polite");
|
|
296
|
-
form.appendChild(output);
|
|
297
|
-
main.appendChild(form);
|
|
298
|
-
return main;
|
|
402
|
+
form.appendChild(button);
|
|
299
403
|
}
|
|
300
404
|
function updateFeedback(act, output) {
|
|
301
405
|
const msg = act.feedback && act.statusMessage ? act.statusMessage : "";
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.1.0-alpha.
|
|
6
|
+
"version": "0.1.0-alpha.8",
|
|
7
7
|
"description": "DOM materializer for Intent screens and router",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"repository": {
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"dist"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@intent-framework/core": "^0.1.0-alpha.
|
|
30
|
-
"@intent-framework/router": "^0.1.0-alpha.
|
|
29
|
+
"@intent-framework/core": "^0.1.0-alpha.8",
|
|
30
|
+
"@intent-framework/router": "^0.1.0-alpha.8"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"jsdom": "^29.1.1",
|