@aurodesignsystem-dev/auro-formkit 0.0.0-pr1395.0 → 0.0.0-pr1395.2

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.
Files changed (44) hide show
  1. package/components/checkbox/demo/api.min.js +1 -1
  2. package/components/checkbox/demo/index.min.js +1 -1
  3. package/components/checkbox/dist/index.js +1 -1
  4. package/components/checkbox/dist/registered.js +1 -1
  5. package/components/combobox/demo/api.min.js +151 -40
  6. package/components/combobox/demo/index.min.js +151 -40
  7. package/components/combobox/dist/index.js +3 -3
  8. package/components/combobox/dist/registered.js +3 -3
  9. package/components/counter/demo/api.min.js +2 -2
  10. package/components/counter/demo/index.min.js +2 -2
  11. package/components/counter/dist/index.js +2 -2
  12. package/components/counter/dist/registered.js +2 -2
  13. package/components/datepicker/demo/api.min.js +74 -13
  14. package/components/datepicker/demo/index.min.js +74 -13
  15. package/components/datepicker/dist/datepickerKeyboardStrategy.d.ts +4 -0
  16. package/components/datepicker/dist/index.js +74 -13
  17. package/components/datepicker/dist/registered.js +74 -13
  18. package/components/dropdown/demo/api.min.js +1 -1
  19. package/components/dropdown/demo/index.min.js +1 -1
  20. package/components/dropdown/dist/index.js +1 -1
  21. package/components/dropdown/dist/registered.js +1 -1
  22. package/components/form/demo/api.min.js +240 -63
  23. package/components/form/demo/index.min.js +240 -63
  24. package/components/input/demo/api.min.js +1 -1
  25. package/components/input/demo/index.min.js +1 -1
  26. package/components/input/dist/index.js +1 -1
  27. package/components/input/dist/registered.js +1 -1
  28. package/components/menu/demo/api.md +1 -1
  29. package/components/menu/demo/api.min.js +148 -37
  30. package/components/menu/demo/index.min.js +148 -37
  31. package/components/menu/dist/auro-menu.context.d.ts +15 -3
  32. package/components/menu/dist/auro-menu.d.ts +1 -1
  33. package/components/menu/dist/index.js +148 -37
  34. package/components/menu/dist/registered.js +148 -37
  35. package/components/radio/demo/api.min.js +1 -1
  36. package/components/radio/demo/index.min.js +1 -1
  37. package/components/radio/dist/index.js +1 -1
  38. package/components/radio/dist/registered.js +1 -1
  39. package/components/select/demo/api.min.js +158 -42
  40. package/components/select/demo/index.min.js +158 -42
  41. package/components/select/dist/index.js +10 -5
  42. package/components/select/dist/registered.js +10 -5
  43. package/custom-elements.json +1514 -1429
  44. package/package.json +11 -4
@@ -21,7 +21,7 @@ The `auro-menu` element provides users a way to select from a list of options.
21
21
  | [noCheckmark](#noCheckmark) | `nocheckmark` | | `boolean` | false | When true, selected option will not show the checkmark. |
22
22
  | [optionActive](#optionActive) | `optionactive` | | `object` | "undefined" | Specifies the current active menuOption. |
23
23
  | [optionSelected](#optionSelected) | `optionSelected` | | `object` | "undefined" | An array of currently selected menu options, type `HTMLElement` by default. In multi-select mode, `optionSelected` is an array of HTML elements. |
24
- | [options](#options) | | readonly | `array` | | Available menu options |
24
+ | [options](#options) | | readonly | `array` | | Available menu options. |
25
25
  | [selectAllMatchingOptions](#selectAllMatchingOptions) | `selectAllMatchingOptions` | | `boolean` | false | When true, selects all options that match the provided value/key when setting value and multiselect is enabled. |
26
26
  | [selectedOption](#selectedOption) | | readonly | `HTMLElement \| null` | | Gets the first selected option, or null if none. |
27
27
  | [selectedOptions](#selectedOptions) | | readonly | `HTMLElement[]` | | Gets the currently selected options. |
@@ -650,10 +650,20 @@ class AuroMenuOption extends AuroElement {
650
650
  subscribe: true
651
651
  });
652
652
 
653
- // Establish the key property as early as possible
653
+ // Establish the key property as early as possible.
654
+ // When a framework (e.g. Svelte) inserts the element into the DOM before
655
+ // setting its `value` property, both `getAttribute('value')` and
656
+ // `getAttribute('key')` return null here. Setting `this.key = null`
657
+ // would block the fallback in `updated()` that assigns key from the
658
+ // value property (the guard checked `=== undefined`). Only assign key
659
+ // if at least one source attribute is actually present so that the
660
+ // `updated()` fallback can run when the value property arrives later.
654
661
  const valueAttr = this.getAttribute('value');
655
662
  const keyAttr = this.getAttribute('key');
656
- this.key = keyAttr !== null ? keyAttr : valueAttr;
663
+ const resolvedKey = keyAttr !== null ? keyAttr : valueAttr;
664
+ if (resolvedKey !== null) {
665
+ this.key = resolvedKey;
666
+ }
657
667
  }
658
668
 
659
669
  firstUpdated() {
@@ -703,8 +713,14 @@ class AuroMenuOption extends AuroElement {
703
713
  this.updateTextHighlight();
704
714
  }
705
715
 
706
- // Set the key to be the passed value if no key is provided
707
- if (changedProperties.has('value') && this.key === undefined) {
716
+ // Set the key to be the passed value if no key is provided.
717
+ // Loose equality (== null) is intentional: it catches both null AND
718
+ // undefined. When a framework (e.g. Svelte, React) inserts the element
719
+ // before setting its value property, connectedCallback skips key
720
+ // assignment because both attributes are null at that point. The Lit
721
+ // property default for `key` is undefined (not null), so strict
722
+ // === null would miss the case and the fallback would never run.
723
+ if (changedProperties.has('value') && this.key == null) { // eslint-disable-line eqeqeq, no-eq-null
708
724
  this.key = this.value;
709
725
  }
710
726
  }
@@ -1073,6 +1089,9 @@ class MenuService {
1073
1089
  this._subscribers = [];
1074
1090
  this.internalUpdateInProgress = false;
1075
1091
  this.selectedOptions = [];
1092
+ this._pendingValue = null;
1093
+ this._pendingRetryScheduled = false;
1094
+ this._pendingRetryCount = 0;
1076
1095
  }
1077
1096
 
1078
1097
  /**
@@ -1112,6 +1131,9 @@ class MenuService {
1112
1131
  hostDisconnected() {
1113
1132
  this._subscribers = [];
1114
1133
  this._menuOptions = [];
1134
+ this._pendingValue = null;
1135
+ this._pendingRetryScheduled = false;
1136
+ this._pendingRetryCount = 0;
1115
1137
  }
1116
1138
 
1117
1139
  /**
@@ -1314,17 +1336,22 @@ class MenuService {
1314
1336
  * @param {string|number|Array<string|number>} value - The value(s) to select.
1315
1337
  */
1316
1338
  selectByValue(value) {
1317
- // Early exit for invalid/empty values or internal updates
1318
- if (this.internalUpdateInProgress ||
1319
- this.host.internalUpdateInProgress ||
1320
- value === undefined ||
1339
+ const isEmptyValue = value === undefined ||
1321
1340
  value === null ||
1322
1341
  (Array.isArray(value) && value.length === 0) ||
1323
- (typeof value === 'string' && value.trim() === '')) {
1342
+ (typeof value === 'string' && value.trim() === '');
1343
+
1344
+ // Early exit for invalid/empty values
1345
+ if (isEmptyValue) {
1324
1346
  return;
1325
1347
  }
1326
1348
 
1327
- this.reset();
1349
+ // If an internal update cycle is still in progress, defer value application
1350
+ // rather than dropping it.
1351
+ if (this.internalUpdateInProgress || this.host.internalUpdateInProgress) {
1352
+ this.queuePendingValue(value);
1353
+ return;
1354
+ }
1328
1355
 
1329
1356
  // Normalize values to array of strings
1330
1357
  const normalizedValues = this._getNormalizedValues(value);
@@ -1336,33 +1363,100 @@ class MenuService {
1336
1363
  validatedValues = [normalizedValues[0]];
1337
1364
  }
1338
1365
 
1366
+ if (this._menuOptions.length === 0) {
1367
+ this.queuePendingValue(value);
1368
+ return;
1369
+ }
1370
+
1339
1371
  // Find matching options by comparing available options to validated values
1340
1372
  const trackedKeys = new Set();
1341
1373
  const optionsToSelect = this._menuOptions.filter(option => {
1342
1374
  const passesFilter = validatedValues.includes(option.key);
1343
1375
  const alreadyTracked = trackedKeys.has(option.key);
1376
+ const isActive = option.isActive;
1344
1377
 
1345
1378
  trackedKeys.add(option.key);
1346
1379
 
1347
1380
  // Include the option in the options to be selected if it passes the filter check and
1348
1381
  // either hasn't been tracked yet or selectAllMatchingOptions is true
1349
- return passesFilter && (!alreadyTracked || (alreadyTracked && this.selectAllMatchingOptions));
1382
+ return isActive && passesFilter && (!alreadyTracked || (alreadyTracked && this.selectAllMatchingOptions));
1350
1383
  });
1351
1384
 
1352
- // Handle selection result
1353
- if (optionsToSelect.length && !this.optionsArraysMatch(optionsToSelect, this.selectedOptions)) {
1354
- this.selectOptions(optionsToSelect);
1355
- } else {
1356
- this.stageUpdate();
1385
+ // Handle no matches: clear existing selection, but do not dispatch an intermediate
1386
+ // undefined value that can overwrite the host value in parent components.
1387
+ if (!optionsToSelect.length) {
1388
+ const hasUnresolvedKeys = this._menuOptions.some((option) => option.isActive && option.key == null);
1389
+
1390
+ if (hasUnresolvedKeys) {
1391
+ this.queuePendingValue(value);
1392
+ return;
1393
+ }
1394
+
1395
+ this.clearPendingValue();
1396
+
1397
+ if (this.selectedOptions.length > 0) {
1398
+ this.selectedOptions = [];
1399
+ }
1400
+
1401
+ // Always notify so the host resets any stale invalid value, even when
1402
+ // selectedOptions was already empty (e.g. double-clicking set-invalid).
1403
+ this.stageUpdate({ reason: 'no-match' });
1404
+
1405
+ // Dispatch failure event if no matches found
1406
+ if (validatedValues.length) {
1407
+ this.dispatchChangeEvent('auroMenu-selectValueFailure', {
1408
+ message: 'No matching options found for the provided value(s).',
1409
+ values: validatedValues
1410
+ });
1411
+ }
1412
+
1413
+ return;
1357
1414
  }
1358
1415
 
1359
- // Dispatch failure event if no matches found
1360
- if (!optionsToSelect.length && validatedValues.length) {
1361
- this.dispatchChangeEvent('auroMenu-selectValueFailure', {
1362
- message: 'No matching options found for the provided value(s).',
1363
- values: validatedValues
1364
- });
1416
+ this.clearPendingValue();
1417
+
1418
+ if (this.optionsArraysMatch(optionsToSelect, this.selectedOptions)) {
1419
+ return;
1420
+ }
1421
+
1422
+ // Apply programmatic selection as a single transaction and emit one final state.
1423
+ this.selectedOptions = optionsToSelect;
1424
+ this.stageUpdate();
1425
+ }
1426
+
1427
+ /**
1428
+ * Queues a pending value and schedules a bounded retry.
1429
+ * @param {string|number|Array<string|number>} value - The value to retry.
1430
+ */
1431
+ queuePendingValue(value) {
1432
+ this._pendingValue = value;
1433
+
1434
+ if (this._pendingRetryScheduled || this._pendingRetryCount >= 5) {
1435
+ return;
1365
1436
  }
1437
+
1438
+ this._pendingRetryScheduled = true;
1439
+ this._pendingRetryCount += 1;
1440
+
1441
+ setTimeout(() => {
1442
+ this._pendingRetryScheduled = false;
1443
+
1444
+ if (this._pendingValue == null) {
1445
+ return;
1446
+ }
1447
+
1448
+ const pendingValue = this._pendingValue;
1449
+ this.selectByValue(pendingValue);
1450
+ }, 0);
1451
+ }
1452
+
1453
+ /**
1454
+ * Clears pending retry state.
1455
+ */
1456
+ clearPendingValue() {
1457
+ this._pendingValue = null;
1458
+ this._pendingRetryScheduled = false;
1459
+ this._pendingRetryCount = 0;
1366
1460
  }
1367
1461
 
1368
1462
  /**
@@ -1401,9 +1495,9 @@ class MenuService {
1401
1495
  /**
1402
1496
  * Stages an update to notify subscribers of state and value changes.
1403
1497
  */
1404
- stageUpdate() {
1405
- this.notifyStateChange();
1406
- this.notifyValueChange();
1498
+ stageUpdate(meta = {}) {
1499
+ this.notifyStateChange(meta);
1500
+ this.notifyValueChange(meta);
1407
1501
  }
1408
1502
 
1409
1503
  /**
@@ -1418,14 +1512,18 @@ class MenuService {
1418
1512
  /**
1419
1513
  * Notifies subscribers of a state change (selected options has changed).
1420
1514
  */
1421
- notifyStateChange() {
1422
- this.notify({ type: 'stateChange', selectedOptions: this.selectedOptions });
1515
+ notifyStateChange(meta = {}) {
1516
+ this.notify({
1517
+ type: 'stateChange',
1518
+ selectedOptions: this.selectedOptions,
1519
+ ...meta
1520
+ });
1423
1521
  }
1424
1522
 
1425
1523
  /**
1426
1524
  * Notifies subscribers of a value change (current value has changed).
1427
1525
  */
1428
- notifyValueChange() {
1526
+ notifyValueChange(meta = {}) {
1429
1527
 
1430
1528
  // Prepare details for the event
1431
1529
  const details = {
@@ -1441,10 +1539,9 @@ class MenuService {
1441
1539
 
1442
1540
  this.notify({
1443
1541
  type: 'valueChange',
1542
+ ...meta,
1444
1543
  ...details
1445
1544
  });
1446
-
1447
- this.dispatchChangeEvent('auroMenu-selectedOption', details);
1448
1545
  }
1449
1546
 
1450
1547
  /**
@@ -1472,6 +1569,10 @@ class MenuService {
1472
1569
  addMenuOption(option) {
1473
1570
  this._menuOptions.push(option);
1474
1571
  this.notify({ type: 'optionsChange', options: this._menuOptions });
1572
+
1573
+ if (this._pendingValue != null) {
1574
+ this.queuePendingValue(this._pendingValue);
1575
+ }
1475
1576
  }
1476
1577
 
1477
1578
  /**
@@ -1481,6 +1582,10 @@ class MenuService {
1481
1582
  removeMenuOption(option) {
1482
1583
  this._menuOptions = this._menuOptions.filter(opt => opt !== option);
1483
1584
  this.notify({ type: 'optionsChange', options: this._menuOptions });
1585
+
1586
+ if (this._menuOptions.length === 0) {
1587
+ this.clearPendingValue();
1588
+ }
1484
1589
  }
1485
1590
 
1486
1591
  /**
@@ -1754,7 +1859,7 @@ class AuroMenu extends AuroElement {
1754
1859
  },
1755
1860
 
1756
1861
  /**
1757
- * Available menu options
1862
+ * Available menu options.
1758
1863
  * @readonly
1759
1864
  */
1760
1865
  options: {
@@ -1821,7 +1926,7 @@ class AuroMenu extends AuroElement {
1821
1926
  /**
1822
1927
  * @readonly
1823
1928
  * @returns {Array<HTMLElement>} - Returns the array of available menu options.
1824
- * @deprecated use `options` property instead.
1929
+ * @deprecated Use `options` property instead.
1825
1930
  */
1826
1931
  get items() {
1827
1932
  return this.options;
@@ -1929,7 +2034,7 @@ class AuroMenu extends AuroElement {
1929
2034
  const newValue = event.stringValue;
1930
2035
 
1931
2036
  // Check if the option or value has actually changed
1932
- if (newValue === undefined || (this.optionSelected !== newOption || this.stringValue !== newValue)) {
2037
+ if (this.optionSelected !== newOption || this.stringValue !== newValue) {
1933
2038
  this.optionSelected = newOption;
1934
2039
  this.setInternalValue(newValue);
1935
2040
  }
@@ -2003,8 +2108,13 @@ class AuroMenu extends AuroElement {
2003
2108
  updated(changedProperties) {
2004
2109
  super.updated(changedProperties);
2005
2110
 
2006
- // Update menu service properties on host update
2007
- if (changedProperties.has('value')) {
2111
+ // Apply value selection synchronously so that static-HTML fixtures
2112
+ // resolve within a single update cycle. The refactored selectByValue
2113
+ // no longer calls reset() first, so the destructive intermediate-event
2114
+ // cascade that originally required deferral is eliminated. If option
2115
+ // keys are not yet resolved (framework mount-order race), selectByValue
2116
+ // queues a bounded retry automatically via queuePendingValue.
2117
+ if (changedProperties.has('value') && !this.internalUpdateInProgress) {
2008
2118
  this.menuService.selectByValue(this.value);
2009
2119
  }
2010
2120
 
@@ -2184,12 +2294,13 @@ class AuroMenu extends AuroElement {
2184
2294
  * @param {any} source - The source that triggers this event.
2185
2295
  * @private
2186
2296
  */
2187
- notifySelectionChange({value, stringValue, keys, options} = {}) {
2297
+ notifySelectionChange({value, stringValue, keys, options, reason} = {}) {
2188
2298
  dispatchMenuEvent(this, 'auroMenu-selectedOption', {
2189
2299
  value,
2190
2300
  stringValue,
2191
2301
  keys,
2192
- options
2302
+ options,
2303
+ reason
2193
2304
  });
2194
2305
  }
2195
2306
 
@@ -558,10 +558,20 @@ class AuroMenuOption extends AuroElement {
558
558
  subscribe: true
559
559
  });
560
560
 
561
- // Establish the key property as early as possible
561
+ // Establish the key property as early as possible.
562
+ // When a framework (e.g. Svelte) inserts the element into the DOM before
563
+ // setting its `value` property, both `getAttribute('value')` and
564
+ // `getAttribute('key')` return null here. Setting `this.key = null`
565
+ // would block the fallback in `updated()` that assigns key from the
566
+ // value property (the guard checked `=== undefined`). Only assign key
567
+ // if at least one source attribute is actually present so that the
568
+ // `updated()` fallback can run when the value property arrives later.
562
569
  const valueAttr = this.getAttribute('value');
563
570
  const keyAttr = this.getAttribute('key');
564
- this.key = keyAttr !== null ? keyAttr : valueAttr;
571
+ const resolvedKey = keyAttr !== null ? keyAttr : valueAttr;
572
+ if (resolvedKey !== null) {
573
+ this.key = resolvedKey;
574
+ }
565
575
  }
566
576
 
567
577
  firstUpdated() {
@@ -611,8 +621,14 @@ class AuroMenuOption extends AuroElement {
611
621
  this.updateTextHighlight();
612
622
  }
613
623
 
614
- // Set the key to be the passed value if no key is provided
615
- if (changedProperties.has('value') && this.key === undefined) {
624
+ // Set the key to be the passed value if no key is provided.
625
+ // Loose equality (== null) is intentional: it catches both null AND
626
+ // undefined. When a framework (e.g. Svelte, React) inserts the element
627
+ // before setting its value property, connectedCallback skips key
628
+ // assignment because both attributes are null at that point. The Lit
629
+ // property default for `key` is undefined (not null), so strict
630
+ // === null would miss the case and the fallback would never run.
631
+ if (changedProperties.has('value') && this.key == null) { // eslint-disable-line eqeqeq, no-eq-null
616
632
  this.key = this.value;
617
633
  }
618
634
  }
@@ -981,6 +997,9 @@ class MenuService {
981
997
  this._subscribers = [];
982
998
  this.internalUpdateInProgress = false;
983
999
  this.selectedOptions = [];
1000
+ this._pendingValue = null;
1001
+ this._pendingRetryScheduled = false;
1002
+ this._pendingRetryCount = 0;
984
1003
  }
985
1004
 
986
1005
  /**
@@ -1020,6 +1039,9 @@ class MenuService {
1020
1039
  hostDisconnected() {
1021
1040
  this._subscribers = [];
1022
1041
  this._menuOptions = [];
1042
+ this._pendingValue = null;
1043
+ this._pendingRetryScheduled = false;
1044
+ this._pendingRetryCount = 0;
1023
1045
  }
1024
1046
 
1025
1047
  /**
@@ -1222,17 +1244,22 @@ class MenuService {
1222
1244
  * @param {string|number|Array<string|number>} value - The value(s) to select.
1223
1245
  */
1224
1246
  selectByValue(value) {
1225
- // Early exit for invalid/empty values or internal updates
1226
- if (this.internalUpdateInProgress ||
1227
- this.host.internalUpdateInProgress ||
1228
- value === undefined ||
1247
+ const isEmptyValue = value === undefined ||
1229
1248
  value === null ||
1230
1249
  (Array.isArray(value) && value.length === 0) ||
1231
- (typeof value === 'string' && value.trim() === '')) {
1250
+ (typeof value === 'string' && value.trim() === '');
1251
+
1252
+ // Early exit for invalid/empty values
1253
+ if (isEmptyValue) {
1232
1254
  return;
1233
1255
  }
1234
1256
 
1235
- this.reset();
1257
+ // If an internal update cycle is still in progress, defer value application
1258
+ // rather than dropping it.
1259
+ if (this.internalUpdateInProgress || this.host.internalUpdateInProgress) {
1260
+ this.queuePendingValue(value);
1261
+ return;
1262
+ }
1236
1263
 
1237
1264
  // Normalize values to array of strings
1238
1265
  const normalizedValues = this._getNormalizedValues(value);
@@ -1244,33 +1271,100 @@ class MenuService {
1244
1271
  validatedValues = [normalizedValues[0]];
1245
1272
  }
1246
1273
 
1274
+ if (this._menuOptions.length === 0) {
1275
+ this.queuePendingValue(value);
1276
+ return;
1277
+ }
1278
+
1247
1279
  // Find matching options by comparing available options to validated values
1248
1280
  const trackedKeys = new Set();
1249
1281
  const optionsToSelect = this._menuOptions.filter(option => {
1250
1282
  const passesFilter = validatedValues.includes(option.key);
1251
1283
  const alreadyTracked = trackedKeys.has(option.key);
1284
+ const isActive = option.isActive;
1252
1285
 
1253
1286
  trackedKeys.add(option.key);
1254
1287
 
1255
1288
  // Include the option in the options to be selected if it passes the filter check and
1256
1289
  // either hasn't been tracked yet or selectAllMatchingOptions is true
1257
- return passesFilter && (!alreadyTracked || (alreadyTracked && this.selectAllMatchingOptions));
1290
+ return isActive && passesFilter && (!alreadyTracked || (alreadyTracked && this.selectAllMatchingOptions));
1258
1291
  });
1259
1292
 
1260
- // Handle selection result
1261
- if (optionsToSelect.length && !this.optionsArraysMatch(optionsToSelect, this.selectedOptions)) {
1262
- this.selectOptions(optionsToSelect);
1263
- } else {
1264
- this.stageUpdate();
1293
+ // Handle no matches: clear existing selection, but do not dispatch an intermediate
1294
+ // undefined value that can overwrite the host value in parent components.
1295
+ if (!optionsToSelect.length) {
1296
+ const hasUnresolvedKeys = this._menuOptions.some((option) => option.isActive && option.key == null);
1297
+
1298
+ if (hasUnresolvedKeys) {
1299
+ this.queuePendingValue(value);
1300
+ return;
1301
+ }
1302
+
1303
+ this.clearPendingValue();
1304
+
1305
+ if (this.selectedOptions.length > 0) {
1306
+ this.selectedOptions = [];
1307
+ }
1308
+
1309
+ // Always notify so the host resets any stale invalid value, even when
1310
+ // selectedOptions was already empty (e.g. double-clicking set-invalid).
1311
+ this.stageUpdate({ reason: 'no-match' });
1312
+
1313
+ // Dispatch failure event if no matches found
1314
+ if (validatedValues.length) {
1315
+ this.dispatchChangeEvent('auroMenu-selectValueFailure', {
1316
+ message: 'No matching options found for the provided value(s).',
1317
+ values: validatedValues
1318
+ });
1319
+ }
1320
+
1321
+ return;
1265
1322
  }
1266
1323
 
1267
- // Dispatch failure event if no matches found
1268
- if (!optionsToSelect.length && validatedValues.length) {
1269
- this.dispatchChangeEvent('auroMenu-selectValueFailure', {
1270
- message: 'No matching options found for the provided value(s).',
1271
- values: validatedValues
1272
- });
1324
+ this.clearPendingValue();
1325
+
1326
+ if (this.optionsArraysMatch(optionsToSelect, this.selectedOptions)) {
1327
+ return;
1328
+ }
1329
+
1330
+ // Apply programmatic selection as a single transaction and emit one final state.
1331
+ this.selectedOptions = optionsToSelect;
1332
+ this.stageUpdate();
1333
+ }
1334
+
1335
+ /**
1336
+ * Queues a pending value and schedules a bounded retry.
1337
+ * @param {string|number|Array<string|number>} value - The value to retry.
1338
+ */
1339
+ queuePendingValue(value) {
1340
+ this._pendingValue = value;
1341
+
1342
+ if (this._pendingRetryScheduled || this._pendingRetryCount >= 5) {
1343
+ return;
1273
1344
  }
1345
+
1346
+ this._pendingRetryScheduled = true;
1347
+ this._pendingRetryCount += 1;
1348
+
1349
+ setTimeout(() => {
1350
+ this._pendingRetryScheduled = false;
1351
+
1352
+ if (this._pendingValue == null) {
1353
+ return;
1354
+ }
1355
+
1356
+ const pendingValue = this._pendingValue;
1357
+ this.selectByValue(pendingValue);
1358
+ }, 0);
1359
+ }
1360
+
1361
+ /**
1362
+ * Clears pending retry state.
1363
+ */
1364
+ clearPendingValue() {
1365
+ this._pendingValue = null;
1366
+ this._pendingRetryScheduled = false;
1367
+ this._pendingRetryCount = 0;
1274
1368
  }
1275
1369
 
1276
1370
  /**
@@ -1309,9 +1403,9 @@ class MenuService {
1309
1403
  /**
1310
1404
  * Stages an update to notify subscribers of state and value changes.
1311
1405
  */
1312
- stageUpdate() {
1313
- this.notifyStateChange();
1314
- this.notifyValueChange();
1406
+ stageUpdate(meta = {}) {
1407
+ this.notifyStateChange(meta);
1408
+ this.notifyValueChange(meta);
1315
1409
  }
1316
1410
 
1317
1411
  /**
@@ -1326,14 +1420,18 @@ class MenuService {
1326
1420
  /**
1327
1421
  * Notifies subscribers of a state change (selected options has changed).
1328
1422
  */
1329
- notifyStateChange() {
1330
- this.notify({ type: 'stateChange', selectedOptions: this.selectedOptions });
1423
+ notifyStateChange(meta = {}) {
1424
+ this.notify({
1425
+ type: 'stateChange',
1426
+ selectedOptions: this.selectedOptions,
1427
+ ...meta
1428
+ });
1331
1429
  }
1332
1430
 
1333
1431
  /**
1334
1432
  * Notifies subscribers of a value change (current value has changed).
1335
1433
  */
1336
- notifyValueChange() {
1434
+ notifyValueChange(meta = {}) {
1337
1435
 
1338
1436
  // Prepare details for the event
1339
1437
  const details = {
@@ -1349,10 +1447,9 @@ class MenuService {
1349
1447
 
1350
1448
  this.notify({
1351
1449
  type: 'valueChange',
1450
+ ...meta,
1352
1451
  ...details
1353
1452
  });
1354
-
1355
- this.dispatchChangeEvent('auroMenu-selectedOption', details);
1356
1453
  }
1357
1454
 
1358
1455
  /**
@@ -1380,6 +1477,10 @@ class MenuService {
1380
1477
  addMenuOption(option) {
1381
1478
  this._menuOptions.push(option);
1382
1479
  this.notify({ type: 'optionsChange', options: this._menuOptions });
1480
+
1481
+ if (this._pendingValue != null) {
1482
+ this.queuePendingValue(this._pendingValue);
1483
+ }
1383
1484
  }
1384
1485
 
1385
1486
  /**
@@ -1389,6 +1490,10 @@ class MenuService {
1389
1490
  removeMenuOption(option) {
1390
1491
  this._menuOptions = this._menuOptions.filter(opt => opt !== option);
1391
1492
  this.notify({ type: 'optionsChange', options: this._menuOptions });
1493
+
1494
+ if (this._menuOptions.length === 0) {
1495
+ this.clearPendingValue();
1496
+ }
1392
1497
  }
1393
1498
 
1394
1499
  /**
@@ -1662,7 +1767,7 @@ class AuroMenu extends AuroElement {
1662
1767
  },
1663
1768
 
1664
1769
  /**
1665
- * Available menu options
1770
+ * Available menu options.
1666
1771
  * @readonly
1667
1772
  */
1668
1773
  options: {
@@ -1729,7 +1834,7 @@ class AuroMenu extends AuroElement {
1729
1834
  /**
1730
1835
  * @readonly
1731
1836
  * @returns {Array<HTMLElement>} - Returns the array of available menu options.
1732
- * @deprecated use `options` property instead.
1837
+ * @deprecated Use `options` property instead.
1733
1838
  */
1734
1839
  get items() {
1735
1840
  return this.options;
@@ -1837,7 +1942,7 @@ class AuroMenu extends AuroElement {
1837
1942
  const newValue = event.stringValue;
1838
1943
 
1839
1944
  // Check if the option or value has actually changed
1840
- if (newValue === undefined || (this.optionSelected !== newOption || this.stringValue !== newValue)) {
1945
+ if (this.optionSelected !== newOption || this.stringValue !== newValue) {
1841
1946
  this.optionSelected = newOption;
1842
1947
  this.setInternalValue(newValue);
1843
1948
  }
@@ -1911,8 +2016,13 @@ class AuroMenu extends AuroElement {
1911
2016
  updated(changedProperties) {
1912
2017
  super.updated(changedProperties);
1913
2018
 
1914
- // Update menu service properties on host update
1915
- if (changedProperties.has('value')) {
2019
+ // Apply value selection synchronously so that static-HTML fixtures
2020
+ // resolve within a single update cycle. The refactored selectByValue
2021
+ // no longer calls reset() first, so the destructive intermediate-event
2022
+ // cascade that originally required deferral is eliminated. If option
2023
+ // keys are not yet resolved (framework mount-order race), selectByValue
2024
+ // queues a bounded retry automatically via queuePendingValue.
2025
+ if (changedProperties.has('value') && !this.internalUpdateInProgress) {
1916
2026
  this.menuService.selectByValue(this.value);
1917
2027
  }
1918
2028
 
@@ -2092,12 +2202,13 @@ class AuroMenu extends AuroElement {
2092
2202
  * @param {any} source - The source that triggers this event.
2093
2203
  * @private
2094
2204
  */
2095
- notifySelectionChange({value, stringValue, keys, options} = {}) {
2205
+ notifySelectionChange({value, stringValue, keys, options, reason} = {}) {
2096
2206
  dispatchMenuEvent(this, 'auroMenu-selectedOption', {
2097
2207
  value,
2098
2208
  stringValue,
2099
2209
  keys,
2100
- options
2210
+ options,
2211
+ reason
2101
2212
  });
2102
2213
  }
2103
2214