@ukiahinsure/a2ui-react-adapter 0.1.4 → 0.1.6

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/dist/index.js CHANGED
@@ -73,6 +73,10 @@ var A2UIClientAdapter = class {
73
73
  const values = {};
74
74
  for (const field of fields) {
75
75
  const value = surface.dataModel.get(fieldPointer(field));
76
+ if (Array.isArray(value) && value.length === 1 && isFieldValue(value[0])) {
77
+ values[field] = value[0];
78
+ continue;
79
+ }
76
80
  if (!isFieldValue(value)) {
77
81
  return null;
78
82
  }
@@ -89,8 +93,9 @@ var A2UIClientAdapter = class {
89
93
  let changed = false;
90
94
  for (const [field, value] of Object.entries(values)) {
91
95
  const path = fieldPointer(field);
92
- if (!Object.is(surface.dataModel.get(path), value)) {
93
- surface.dataModel.set(path, value);
96
+ const next = this.#dataModelValueForField(surfaceId, field, value);
97
+ if (!fieldValueEquivalent(surface.dataModel.get(path), next)) {
98
+ surface.dataModel.set(path, next);
94
99
  changed = true;
95
100
  }
96
101
  }
@@ -99,6 +104,22 @@ var A2UIClientAdapter = class {
99
104
  }
100
105
  return true;
101
106
  }
107
+ #dataModelValueForField(surfaceId, field, value) {
108
+ const descriptor = this.#fieldDescriptor(surfaceId, field);
109
+ if (descriptor?.control === "choice" && typeof value === "string" && value.length > 0) {
110
+ return [value];
111
+ }
112
+ return value;
113
+ }
114
+ #fieldDescriptor(surfaceId, field) {
115
+ const structure = this.#structureBySurface.get(surfaceId);
116
+ if (structure === void 0) {
117
+ return void 0;
118
+ }
119
+ return [...structure.fieldsByComponent.values()].find(
120
+ (descriptor) => descriptor.field === field
121
+ );
122
+ }
102
123
  /**
103
124
  * Apply one validated server envelope. Returns false for duplicates and
104
125
  * out-of-order deliveries (the per-session `seq` guard) and for envelopes
@@ -189,11 +210,20 @@ var A2UIClientAdapter = class {
189
210
  );
190
211
  return false;
191
212
  }
192
- const messages = envelope.kind === "snapshot" ? validated.data : mergePersistedListRowsIntoMessages(
213
+ const rawMessages = envelope.kind === "snapshot" ? validated.data : mergePersistedListRowsIntoMessages(
193
214
  validated.data,
194
215
  envelope.surfaceId,
195
216
  this.#persistedListRowsByPath
196
217
  );
218
+ const nextStructure = updateSurfaceStructures(
219
+ rawMessages,
220
+ envelope.kind === "snapshot" ? /* @__PURE__ */ new Map() : cloneSurfaceStructures(this.#structureBySurface)
221
+ );
222
+ const messages = normalizeChoiceValuesInMessages(
223
+ rawMessages,
224
+ envelope.surfaceId,
225
+ nextStructure.get(envelope.surfaceId)
226
+ );
197
227
  const candidate = this.#createProcessor();
198
228
  try {
199
229
  if (envelope.kind !== "snapshot") {
@@ -212,10 +242,6 @@ var A2UIClientAdapter = class {
212
242
  return false;
213
243
  }
214
244
  const previous = this.#processor;
215
- const nextStructure = updateSurfaceStructures(
216
- messages,
217
- envelope.kind === "snapshot" ? /* @__PURE__ */ new Map() : cloneSurfaceStructures(this.#structureBySurface)
218
- );
219
245
  pruneSurfaceStructures(nextStructure, candidate);
220
246
  if (!fieldInteractionBindingsTrusted(
221
247
  envelope.fieldInteractions ?? [],
@@ -435,6 +461,59 @@ function fieldDescriptor(component) {
435
461
  ...typeof component.max === "string" ? { max: component.max } : {}
436
462
  };
437
463
  }
464
+ function normalizeChoiceValuesInMessages(messages, surfaceId, structure) {
465
+ if (structure === void 0) {
466
+ return [...messages];
467
+ }
468
+ const choiceFields = new Set(
469
+ [...structure.fieldsByComponent.values()].filter((field) => field.control === "choice").map((field) => field.field)
470
+ );
471
+ if (choiceFields.size === 0) {
472
+ return [...messages];
473
+ }
474
+ return messages.map((message) => {
475
+ if (!isRecord(message) || !isRecord(message.updateDataModel)) {
476
+ return message;
477
+ }
478
+ const update = message.updateDataModel;
479
+ if (update.surfaceId !== surfaceId) {
480
+ return message;
481
+ }
482
+ const path = typeof update.path === "string" ? update.path : "/";
483
+ if (path === "/" && isRecord(update.value)) {
484
+ const value = cloneJsonRecord(update.value);
485
+ for (const field2 of choiceFields) {
486
+ if (Object.hasOwn(value, field2)) {
487
+ value[field2] = choiceDataModelValue(value[field2]);
488
+ }
489
+ }
490
+ return {
491
+ ...message,
492
+ updateDataModel: { ...update, value }
493
+ };
494
+ }
495
+ const field = fieldFromPointer(path);
496
+ if (field !== void 0 && choiceFields.has(field)) {
497
+ return {
498
+ ...message,
499
+ updateDataModel: {
500
+ ...update,
501
+ value: choiceDataModelValue(update.value)
502
+ }
503
+ };
504
+ }
505
+ return message;
506
+ });
507
+ }
508
+ function choiceDataModelValue(value) {
509
+ if (Array.isArray(value)) {
510
+ return value;
511
+ }
512
+ if (typeof value === "string") {
513
+ return value.length === 0 ? [] : [value];
514
+ }
515
+ return value;
516
+ }
438
517
  function hasRequiredCheck(value) {
439
518
  return Array.isArray(value) && value.some(
440
519
  (check) => isRecord(check) && isRecord(check.condition) && check.condition.call === "required"
@@ -602,6 +681,12 @@ function cloneJsonArray(value) {
602
681
  function isFieldValue(value) {
603
682
  return value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
604
683
  }
684
+ function fieldValueEquivalent(left, right) {
685
+ if (Array.isArray(left) && Array.isArray(right)) {
686
+ return left.length === right.length && left.every((value, index) => Object.is(value, right[index]));
687
+ }
688
+ return Object.is(left, right);
689
+ }
605
690
  function tabDescriptors(component) {
606
691
  if (component.component !== "Tabs" || !isNonEmptyString(component.id) || !Array.isArray(component.tabs)) {
607
692
  return [];
@@ -711,6 +796,7 @@ function A2UISurfaceHost({
711
796
  resolvedSurfaceMetadata?.fieldInteractions ?? []
712
797
  ),
713
798
  annotateActionControls(root),
799
+ blockInvalidActionControls(root),
714
800
  setPendingGroupsDisabled(
715
801
  root,
716
802
  new Set(editing.pendingGroupIds)
@@ -1104,6 +1190,10 @@ function annotateFields(root, fields, invalidFields, validationSummaryId) {
1104
1190
  }
1105
1191
  }
1106
1192
  applyControlValidationAttributes(control, field);
1193
+ const cleanup = attachControlValidationBehavior(control, field);
1194
+ if (cleanup !== void 0) {
1195
+ matchCleanups.push(cleanup);
1196
+ }
1107
1197
  }
1108
1198
  }
1109
1199
  return () => {
@@ -1133,13 +1223,90 @@ function applyControlValidationAttributes(control, field) {
1133
1223
  if (field.max !== void 0) {
1134
1224
  control.setAttribute("max", field.max);
1135
1225
  }
1136
- if (field.validationRegexp === "^[2-9]\\d{2}[2-9]\\d{6}$") {
1226
+ if (isPhoneValidationRegexp(field.validationRegexp)) {
1137
1227
  control.setAttribute("inputmode", "numeric");
1138
1228
  control.setAttribute("maxlength", "10");
1139
- } else if (field.validationRegexp === "^(?!00000$)\\d{5}$") {
1229
+ } else if (isZipValidationRegexp(field.validationRegexp)) {
1140
1230
  control.setAttribute("inputmode", "numeric");
1141
1231
  control.setAttribute("maxlength", "5");
1232
+ } else if (isIntegerValidationRegexp(field.validationRegexp)) {
1233
+ control.setAttribute("inputmode", "numeric");
1234
+ control.setAttribute("maxlength", "2");
1235
+ }
1236
+ }
1237
+ function attachControlValidationBehavior(control, field) {
1238
+ const maxLength = numericMaxLengthForField(field);
1239
+ if (maxLength === void 0 || !isTextInput(control)) {
1240
+ return void 0;
1241
+ }
1242
+ const sanitize = () => {
1243
+ const next = control.value.replace(/\D/g, "").slice(0, maxLength);
1244
+ if (control.value !== next) {
1245
+ control.value = next;
1246
+ control.dispatchEvent(new Event("input", { bubbles: true }));
1247
+ }
1248
+ };
1249
+ control.addEventListener("input", sanitize);
1250
+ control.addEventListener("change", sanitize);
1251
+ sanitize();
1252
+ return () => {
1253
+ control.removeEventListener("input", sanitize);
1254
+ control.removeEventListener("change", sanitize);
1255
+ };
1256
+ }
1257
+ function numericMaxLengthForField(field) {
1258
+ if (isPhoneValidationRegexp(field.validationRegexp)) {
1259
+ return 10;
1260
+ }
1261
+ if (isZipValidationRegexp(field.validationRegexp)) {
1262
+ return 5;
1263
+ }
1264
+ if (isIntegerValidationRegexp(field.validationRegexp)) {
1265
+ return 2;
1142
1266
  }
1267
+ return void 0;
1268
+ }
1269
+ function isPhoneValidationRegexp(pattern) {
1270
+ return pattern === "^[2-9]\\d{2}[2-9]\\d{6}$" || pattern?.startsWith("^(?:") === true && pattern.endsWith(")[2-9]\\d{6}$");
1271
+ }
1272
+ function isZipValidationRegexp(pattern) {
1273
+ return pattern === "^(?!00000$)\\d{5}$" || pattern === "^(?:00(?:50[1-9]|5[1-9][0-9]|[6-9][0-9]{2})|0[1-9][0-9]{3}|[1-8][0-9]{4}|9[0-8][0-9]{3}|99[0-8][0-9]{2}|999[0-4][0-9]|99950)$";
1274
+ }
1275
+ function isIntegerValidationRegexp(pattern) {
1276
+ return pattern === "^\\d{1,2}$";
1277
+ }
1278
+ function isTextInput(element) {
1279
+ return element instanceof HTMLInputElement && element.type !== "radio";
1280
+ }
1281
+ function blockInvalidActionControls(root) {
1282
+ const blockIfInvalid = (event) => {
1283
+ const target = event.target;
1284
+ if (!(target instanceof HTMLElement)) {
1285
+ return;
1286
+ }
1287
+ const action = target.closest("[data-a2ui-action='true']");
1288
+ if (action === null || !root.contains(action)) {
1289
+ return;
1290
+ }
1291
+ const invalid2 = firstInvalidInput(root);
1292
+ if (invalid2 === void 0) {
1293
+ return;
1294
+ }
1295
+ event.preventDefault();
1296
+ event.stopImmediatePropagation();
1297
+ invalid2.setAttribute("aria-invalid", "true");
1298
+ invalid2.reportValidity();
1299
+ invalid2.focus();
1300
+ };
1301
+ root.addEventListener("click", blockIfInvalid, true);
1302
+ root.addEventListener("keydown", blockIfInvalid, true);
1303
+ return () => {
1304
+ root.removeEventListener("click", blockIfInvalid, true);
1305
+ root.removeEventListener("keydown", blockIfInvalid, true);
1306
+ };
1307
+ }
1308
+ function firstInvalidInput(root) {
1309
+ return [...root.querySelectorAll("input[data-a2ui-field]")].filter((input) => input.type !== "radio" && !isControlDisabled(input)).find((input) => !input.checkValidity());
1143
1310
  }
1144
1311
  function findFieldControls(root, field) {
1145
1312
  const labels = [...root.querySelectorAll("label")];