@jinntec/fore 3.3.2 → 4.0.0

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 (73) hide show
  1. package/README.md +98 -70
  2. package/dist/fore-dev.js +5593 -6832
  3. package/dist/fore.js +5602 -4887
  4. package/index.js +5 -10
  5. package/package.json +7 -7
  6. package/resources/fore.css +33 -0
  7. package/src/DependencyNotifyingDomFacade.js +90 -21
  8. package/src/DependentXPathQueries.js +15 -2
  9. package/src/ForeElementMixin.js +110 -16
  10. package/src/UndoManager.js +267 -0
  11. package/src/actions/abstract-action.js +71 -30
  12. package/src/actions/fx-action.js +5 -0
  13. package/src/actions/fx-append.js +3 -3
  14. package/src/actions/fx-commit-history.js +26 -0
  15. package/src/actions/fx-hide.js +1 -1
  16. package/src/actions/fx-insert.js +25 -22
  17. package/src/actions/fx-load.js +5 -5
  18. package/src/actions/fx-redo.js +58 -0
  19. package/src/actions/fx-refresh.js +2 -2
  20. package/src/actions/fx-reload.js +1 -1
  21. package/src/actions/fx-replace.js +1 -1
  22. package/src/actions/fx-send.js +27 -5
  23. package/src/actions/fx-setattribute.js +11 -7
  24. package/src/actions/fx-undo.js +58 -0
  25. package/src/createNodes.js +314 -0
  26. package/src/fore.js +53 -18
  27. package/src/functions/fx-functionlib.js +10 -10
  28. package/src/fx-bind.js +30 -18
  29. package/src/fx-fore.js +222 -200
  30. package/src/fx-instance.js +18 -1
  31. package/src/fx-model.js +236 -69
  32. package/src/fx-submission.js +37 -29
  33. package/src/fx-var.js +49 -13
  34. package/src/getInScopeContext.js +1 -1
  35. package/src/json/JSONDomFacade.js +1 -1
  36. package/src/json/JSONLens.js +2 -2
  37. package/src/ui/UIElement.js +18 -8
  38. package/src/ui/abstract-control.js +45 -3
  39. package/src/ui/fx-alert.js +4 -0
  40. package/src/ui/fx-case.js +1 -1
  41. package/src/ui/fx-container.js +3 -0
  42. package/src/ui/fx-control-menu.js +79 -11
  43. package/src/ui/fx-control.js +130 -41
  44. package/src/ui/fx-dialog.js +5 -0
  45. package/src/ui/fx-items.js +6 -6
  46. package/src/ui/fx-output.js +37 -1
  47. package/src/ui/fx-repeat.js +1065 -103
  48. package/src/ui/fx-repeatitem.js +4 -1
  49. package/src/ui/fx-switch.js +116 -3
  50. package/src/ui/fx-trigger.js +9 -4
  51. package/src/ui/fx-upload.js +10 -4
  52. package/src/ui/repeat-base.js +20 -12
  53. package/src/withDraggability.js +10 -1
  54. package/src/xpath-evaluation.js +30 -18
  55. package/src/xpath-path.js +122 -0
  56. package/src/xpath-util.js +11 -126
  57. package/src/actions/StringTpl.js +0 -17
  58. package/src/extract-predicate-deps.js +0 -57
  59. package/src/extractPredicateDependencies.js +0 -36
  60. package/src/json/lensFromNode.js +0 -5
  61. package/src/json-util.js +0 -27
  62. package/src/tools/adi.js +0 -1111
  63. package/src/tools/deprecation.md +0 -1
  64. package/src/tools/fx-action-log.js +0 -745
  65. package/src/tools/fx-devtools.js +0 -444
  66. package/src/tools/fx-dom-inspector.js +0 -610
  67. package/src/tools/fx-json-instance.js +0 -444
  68. package/src/tools/fx-log-item.js +0 -128
  69. package/src/tools/fx-log-settings.js +0 -533
  70. package/src/tools/fx-minimap.js +0 -203
  71. package/src/tools/helpers.js +0 -132
  72. package/src/ui/TemplateExpression.js +0 -12
  73. package/src/ui/fx-dom-inspector.js +0 -1255
@@ -152,7 +152,9 @@ export class FxSubmission extends ForeElementMixin {
152
152
 
153
153
  const instance = this.getInstance();
154
154
  if (!instance) {
155
- Fore.dispatch(this, 'warn', { message: `instance not found ${instance?.getAttribute?.('id')}` });
155
+ Fore.dispatch(this, 'warn', {
156
+ message: `instance not found ${instance?.getAttribute?.('id')}`,
157
+ });
156
158
  }
157
159
  const instType = instance.getAttribute('type');
158
160
 
@@ -244,8 +246,8 @@ export class FxSubmission extends ForeElementMixin {
244
246
 
245
247
  if (!response.ok || response.status > 400) {
246
248
  console.info(
247
- `%csubmit-error #${this.id}`,
248
- 'background:red; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
249
+ `%csubmit-error #${this.id}`,
250
+ 'background:red; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
249
251
  );
250
252
 
251
253
  Fore.dispatch(this, 'submit-error', {
@@ -322,13 +324,14 @@ export class FxSubmission extends ForeElementMixin {
322
324
  // If no payload was passed, derive it from instance default context/nodeset
323
325
  if (payload == null && instanceEl) {
324
326
  payload =
325
- (typeof instanceEl.getDefaultContext === 'function' && instanceEl.getDefaultContext()) ||
326
- instanceEl.nodeset ||
327
- null;
327
+ (typeof instanceEl.getDefaultContext === 'function' && instanceEl.getDefaultContext()) ||
328
+ instanceEl.nodeset ||
329
+ null;
328
330
  }
329
331
 
330
332
  // Decide JSON vs XML by instance type (NOT by ref expression)
331
- const isJsonInstance = instanceEl?.getAttribute?.('type') === 'json' || instanceEl?.type === 'json';
333
+ const isJsonInstance =
334
+ instanceEl?.getAttribute?.('type') === 'json' || instanceEl?.type === 'json';
332
335
 
333
336
  if (isJsonInstance) {
334
337
  // Convert JSON lens nodes to plain JS before stringify
@@ -346,8 +349,7 @@ export class FxSubmission extends ForeElementMixin {
346
349
  try {
347
350
  if (payload && payload.nodeType) {
348
351
  // Document => serialize documentElement, Node => serialize node
349
- const node =
350
- payload.nodeType === Node.DOCUMENT_NODE ? payload.documentElement : payload;
352
+ const node = payload.nodeType === Node.DOCUMENT_NODE ? payload.documentElement : payload;
351
353
  return new XMLSerializer().serializeToString(node);
352
354
  }
353
355
  } catch (_e) {
@@ -410,11 +412,11 @@ export class FxSubmission extends ForeElementMixin {
410
412
  const val = node.value;
411
413
 
412
414
  if (
413
- val === null ||
414
- val === undefined ||
415
- typeof val === 'string' ||
416
- typeof val === 'number' ||
417
- typeof val === 'boolean'
415
+ val === null ||
416
+ val === undefined ||
417
+ typeof val === 'string' ||
418
+ typeof val === 'number' ||
419
+ typeof val === 'boolean'
418
420
  ) {
419
421
  return val ?? null;
420
422
  }
@@ -445,7 +447,6 @@ export class FxSubmission extends ForeElementMixin {
445
447
 
446
448
  return result;
447
449
  }
448
- 1
449
450
  // Fallback: last-resort scalar conversion
450
451
  try {
451
452
  if (typeof node.get === 'function') {
@@ -529,14 +530,14 @@ export class FxSubmission extends ForeElementMixin {
529
530
 
530
531
  if (this.targetref) {
531
532
  const [theTarget] = evaluateXPath(
532
- this.targetref,
533
- targetInstance.instanceData.firstElementChild,
534
- this,
533
+ this.targetref,
534
+ targetInstance.instanceData.firstElementChild,
535
+ this,
535
536
  );
536
537
 
537
538
  if (
538
- this.responseMediatype === 'application/xml' ||
539
- this.responseMediatype === 'text/html'
539
+ this.responseMediatype === 'application/xml' ||
540
+ this.responseMediatype === 'text/html'
540
541
  ) {
541
542
  const clone = data.firstElementChild;
542
543
  const parent = theTarget.parentNode;
@@ -550,9 +551,9 @@ export class FxSubmission extends ForeElementMixin {
550
551
  }
551
552
  } else if (this.into) {
552
553
  const [theTarget] = evaluateXPath(
553
- this.into,
554
- targetInstance.instanceData.firstElementChild,
555
- this,
554
+ this.into,
555
+ targetInstance.instanceData.firstElementChild,
556
+ this,
556
557
  );
557
558
  if (data?.nodeType === Node.DOCUMENT_NODE) {
558
559
  theTarget.appendChild(data.firstElementChild);
@@ -571,9 +572,9 @@ export class FxSubmission extends ForeElementMixin {
571
572
 
572
573
  // ✅ treat instance replacement as a structural change
573
574
  const fore =
574
- (typeof this.getOwnerForm === 'function' && this.getOwnerForm()) ||
575
- this.closest('fx-fore') ||
576
- this.getModel()?.parentNode;
575
+ (typeof this.getOwnerForm === 'function' && this.getOwnerForm()) ||
576
+ this.closest('fx-fore') ||
577
+ this.getModel()?.parentNode;
577
578
 
578
579
  if (fore) {
579
580
  fore.someInstanceDataStructureChanged = true;
@@ -589,10 +590,11 @@ export class FxSubmission extends ForeElementMixin {
589
590
  }
590
591
 
591
592
  if (this.replace === 'download') {
592
- const target = this._getProperty('target');
593
+ let target = this._getProperty('target');
593
594
  if (!target) {
594
595
  throw new Error(`${this.id} needs to specify "target" attribute`);
595
596
  }
597
+ target = this.evaluateAttributeTemplateExpression(target, this);
596
598
  const downloadLink = document.createElement('a');
597
599
  downloadLink.setAttribute('download', target);
598
600
  downloadLink.setAttribute('href', `data:${contentType},${encodeURIComponent(data)}`);
@@ -602,7 +604,10 @@ export class FxSubmission extends ForeElementMixin {
602
604
  }
603
605
 
604
606
  if (this.replace === 'all') {
605
- const target = this._getProperty('target');
607
+ let target = this._getProperty('target');
608
+ if (target) {
609
+ target = this.evaluateAttributeTemplateExpression(target, this);
610
+ }
606
611
  if (target && target === '_blank') {
607
612
  const win = window.open('', '_blank');
608
613
  win.document.write(`<pre>${data}</pre>`);
@@ -617,7 +622,10 @@ export class FxSubmission extends ForeElementMixin {
617
622
  }
618
623
 
619
624
  if (this.replace === 'target') {
620
- const target = this._getProperty('target');
625
+ let target = this._getProperty('target');
626
+ if (target) {
627
+ target = this.evaluateAttributeTemplateExpression(target, this);
628
+ }
621
629
  const targetNode = document.querySelector(target);
622
630
  if (targetNode) {
623
631
  if (contentType && contentType.startsWith('text/html')) {
package/src/fx-var.js CHANGED
@@ -17,7 +17,6 @@ export class FxVariable extends ForeElementMixin {
17
17
  this.name = '';
18
18
  this.valueQuery = '';
19
19
  this.value = null;
20
- this.precedingVariables = [];
21
20
  // Re-entrancy guard for variable evaluation
22
21
  this._isRefreshing = false;
23
22
  // Cached typed value (Fonto sequence wrapper)
@@ -41,10 +40,11 @@ export class FxVariable extends ForeElementMixin {
41
40
 
42
41
  const inscope = getInScopeContext(this, this.valueQuery);
43
42
 
44
- // Evaluate using the preceding variables snapshot (do NOT pull live variables here)
45
- const values = evaluateXPath(this.valueQuery, inscope, this, this.precedingVariables);
43
+ // Evaluate with the lexically scoped variables (preceding vars only), values read live
44
+ const values = evaluateXPath(this.valueQuery, inscope, this, this._precedingVariables());
46
45
 
47
46
  // Cache typed value for other computations to consume without triggering evaluation
47
+ this._rawValues = values;
48
48
  this._value = typedValueFactory(values, domFacade);
49
49
  this.value = this._value;
50
50
  } finally {
@@ -52,28 +52,64 @@ export class FxVariable extends ForeElementMixin {
52
52
  }
53
53
  }
54
54
 
55
+ /**
56
+ * Runs one evaluation point for this variable and reports whether its value changed.
57
+ *
58
+ * Comparison is shallow over the raw result sequence: node items by identity,
59
+ * atomic items by value. A node-valued variable whose nodes mutated content but kept
60
+ * identity is NOT flagged — variables hold node references, and consumers read the
61
+ * current content through them.
62
+ *
63
+ * @returns {boolean} true when the raw result sequence differs from the previous one
64
+ */
65
+ refreshAndReportChange() {
66
+ const before = this._rawValues;
67
+ this.refresh();
68
+ const after = this._rawValues;
69
+ if (before === after) return false;
70
+ if (!Array.isArray(before) || !Array.isArray(after)) return true;
71
+ if (before.length !== after.length) return true;
72
+ return before.some((item, i) => item !== after[i]);
73
+ }
74
+
75
+ /**
76
+ * Variables visible to this var per lexical scoping: the entries of the Map cloned at
77
+ * registration time (preceding vars only), with values read live at the evaluation point.
78
+ * The var itself is excluded — a variable is not visible to its own value expression.
79
+ *
80
+ * @returns {Object<string, *>}
81
+ * @private
82
+ */
83
+ _precedingVariables() {
84
+ const variables = {};
85
+ if (!this.inScopeVariables) return variables;
86
+ for (const [name, variableOrValue] of this.inScopeVariables) {
87
+ // eslint-disable-next-line no-continue
88
+ if (!variableOrValue || variableOrValue === this) continue;
89
+ if (variableOrValue.nodeType) {
90
+ // eslint-disable-next-line no-continue
91
+ if (variableOrValue._isRefreshing) continue;
92
+ variables[name] = variableOrValue._value ?? variableOrValue.value ?? null;
93
+ } else {
94
+ variables[name] = variableOrValue;
95
+ }
96
+ }
97
+ return variables;
98
+ }
99
+
55
100
  /**
56
101
  * @param {Map<string, FxVariable>} inScopeVariables
57
102
  */
58
103
  setInScopeVariables(inScopeVariables) {
59
104
  if (inScopeVariables.has(this.name)) {
60
105
  console.error(`The variable ${this.name} is declared more than once`);
61
- Fore.dispatch(this, 'xforms-binding-error', {});
106
+ Fore.dispatch(this, 'binding-error', {});
62
107
  return;
63
108
  }
64
109
  inScopeVariables.set(this.name, this);
65
110
  // Clone the preceding variables to make sure we are not going to get access to variables we
66
111
  // should not get access to
67
112
  this.inScopeVariables = new Map(inScopeVariables);
68
-
69
- // Set precedingVariables based on inScopeVariables
70
- this.precedingVariables = Array.from(inScopeVariables.entries()).map(([name, variable]) => {
71
- // IMPORTANT: do not trigger evaluation while taking the snapshot
72
- if (variable && variable._isRefreshing) {
73
- return { name, value: null };
74
- }
75
- return { name, value: variable?._value ?? variable?.value ?? null };
76
- });
77
113
  }
78
114
  }
79
115
  if (!customElements.get('fx-var')) {
@@ -69,7 +69,7 @@ function _getInitialContext(node, ref) {
69
69
  export default function getInScopeContext(node, ref) {
70
70
  // console.log('getInScopeContext', ref, node);
71
71
 
72
- //todo: check for multi-step pathes
72
+ // todo: check for multi-step pathes
73
73
  const parentElement = _getElement(node);
74
74
  // console.log('getInScopeContext parent', parentElement);
75
75
 
@@ -81,4 +81,4 @@ export class JSONDomFacade {
81
81
  getAttribute() {
82
82
  return null;
83
83
  }
84
- }
84
+ }
@@ -39,7 +39,7 @@ export class JSONLens {
39
39
  }
40
40
 
41
41
  insert(value, keyOrIndex = null) {
42
- const target = this.get();
42
+ const target = this.get();
43
43
 
44
44
  if (Array.isArray(target)) {
45
45
  if (keyOrIndex === null || keyOrIndex >= target.length) {
@@ -62,6 +62,6 @@ export class JSONLens {
62
62
  }
63
63
 
64
64
  pathString() {
65
- return '/' + this.path.map(k => (typeof k === 'number' ? `[${k}]` : k)).join('/');
65
+ return `/${this.path.map(k => (typeof k === 'number' ? `[${k}]` : k)).join('/')}`;
66
66
  }
67
67
  }
@@ -43,10 +43,14 @@ export class UIElement extends ForeElementMixin {
43
43
 
44
44
  disconnectedCallback() {
45
45
  if (this.modelItem && typeof this.modelItem.removeObserver === 'function') {
46
- console.log(`[UIElement] Removing observer for ref="${this.ref}"`);
47
46
  this.modelItem.removeObserver(this);
48
47
  }
49
48
 
49
+ this._refTrackedModelItems.forEach(modelItems => {
50
+ modelItems.forEach(mi => mi.removeObserver(this));
51
+ });
52
+ this._refTrackedModelItems.clear();
53
+
50
54
  for (const removeEventListener of this._removeEventListeners) {
51
55
  removeEventListener();
52
56
  }
@@ -96,6 +100,15 @@ export class UIElement extends ForeElementMixin {
96
100
  }
97
101
  */
98
102
 
103
+ /**
104
+ * Mirrors relevant/nonrelevant into the `inert` primitive so a nonrelevant control is
105
+ * unreachable by keyboard/AT even if CSS visibility rules ever change. CSS already hides
106
+ * nonrelevant elements (display:none), so this is a semantic backstop, not the primary mechanism.
107
+ */
108
+ _reflectRelevantInert(enabled) {
109
+ this.inert = !enabled;
110
+ }
111
+
99
112
  attachObserver() {
100
113
  const modelItem = this.getModelItem();
101
114
  if (!modelItem || typeof modelItem.addObserver !== 'function') return;
@@ -120,7 +133,7 @@ export class UIElement extends ForeElementMixin {
120
133
  * @param {import('../modelitem.js').ModelItem} modelItem - The ModelItem that changed
121
134
  */
122
135
 
123
- /*
136
+ /*
124
137
  update(modelItem) {
125
138
  if (this.isBound()) {
126
139
  // console.log('[UIElement] update()', modelItem);
@@ -146,16 +159,13 @@ export class UIElement extends ForeElementMixin {
146
159
  // throw new Error('You have to implement the method init!');
147
160
  // }
148
161
 
149
- async refresh(force) {
150
- console.log(`🔄 [UIElement] refresh() called for ref="${this.ref}"`);
151
- }
162
+ async refresh(force) {}
152
163
 
153
164
  async refreshChildren(force) {
154
165
  await Fore.refreshChildren(this, force);
155
166
  }
156
167
 
157
168
  activate() {
158
- console.log('UIElement.activate() called');
159
169
  this.removeAttribute('on-demand');
160
170
  this.style.display = '';
161
171
  if (this.isBound()) {
@@ -198,12 +208,12 @@ export class UIElement extends ForeElementMixin {
198
208
  icon.style.cursor = 'pointer';
199
209
  icon.style.marginLeft = '0.5em';
200
210
 
201
- icon.addEventListener('click', e => {
211
+ icon.addEventListener('click', async e => {
202
212
  e.stopPropagation();
203
213
  this.setAttribute('on-demand', 'true');
204
214
  this.style.display = 'none';
205
215
  document.dispatchEvent(new CustomEvent('update-control-menu'));
206
- Fore.dispatch(this, 'hide-control', {});
216
+ await Fore.dispatch(this, 'hide-control', {});
207
217
  });
208
218
 
209
219
  this.appendChild(icon);
@@ -31,6 +31,8 @@ function isDifferent(oldNodeValue, oldControlValue, newControlValue) {
31
31
  * is a general base class for control elements.
32
32
  *
33
33
  */
34
+ const NATIVE_FORM_TAGS = new Set(['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON']);
35
+
34
36
  export default class AbstractControl extends UIElement {
35
37
  constructor() {
36
38
  super();
@@ -50,6 +52,31 @@ export default class AbstractControl extends UIElement {
50
52
  throw new Error('You have to implement the method getWidget!');
51
53
  }
52
54
 
55
+ /**
56
+ * Native form elements (input/select/textarea/button) already expose required/readonly/disabled
57
+ * to assistive tech via their native attributes - mirroring those into aria-* would be redundant
58
+ * ARIA. Non-native widgets (custom elements used as `.widget`) have no such native mapping, so
59
+ * they need the explicit aria-* attributes.
60
+ */
61
+ // eslint-disable-next-line class-methods-use-this
62
+ _isNativeFormWidget(widget) {
63
+ return !!widget && NATIVE_FORM_TAGS.has(widget.tagName);
64
+ }
65
+
66
+ /**
67
+ * Appends `id` to the widget's aria-describedby without clobbering any existing references
68
+ * (e.g. a hint that already contributed its id).
69
+ */
70
+ _addDescribedBy(id) {
71
+ if (!id) return;
72
+ const widget = this.getWidget?.() || this.widget;
73
+ if (!widget) return;
74
+ const existing = (widget.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean);
75
+ if (!existing.includes(id)) {
76
+ widget.setAttribute('aria-describedby', [...existing, id].join(' '));
77
+ }
78
+ }
79
+
53
80
  /**
54
81
  * (re)apply all modelItem state properties to this control. model -> UI
55
82
  */
@@ -232,6 +259,9 @@ export default class AbstractControl extends UIElement {
232
259
  if (!this.modelItem.required) {
233
260
  this.widget.removeAttribute('required');
234
261
  this.removeAttribute('required');
262
+ if (!this._isNativeFormWidget(this.widget)) {
263
+ this.widget.removeAttribute('aria-required');
264
+ }
235
265
  if (wasRequired !== this.modelItem.required) {
236
266
  this._dispatchEvent('optional');
237
267
  }
@@ -250,6 +280,9 @@ export default class AbstractControl extends UIElement {
250
280
  }
251
281
  this.widget.setAttribute('required', '');
252
282
  this.setAttribute('required', '');
283
+ if (!this._isNativeFormWidget(this.widget)) {
284
+ this.widget.setAttribute('aria-required', 'true');
285
+ }
253
286
  if (wasRequired !== this.modelItem.required) {
254
287
  this._dispatchEvent('required');
255
288
  }
@@ -325,10 +358,16 @@ export default class AbstractControl extends UIElement {
325
358
  if (effectiveReadonly) {
326
359
  this.widget.setAttribute('readonly', '');
327
360
  this.setAttribute('readonly', '');
361
+ if (!this._isNativeFormWidget(this.widget)) {
362
+ this.widget.setAttribute('aria-readonly', 'true');
363
+ }
328
364
  this._dispatchEvent('readonly');
329
365
  } else {
330
366
  this.widget.removeAttribute('readonly');
331
367
  this.removeAttribute('readonly');
368
+ if (!this._isNativeFormWidget(this.widget)) {
369
+ this.widget.removeAttribute('aria-readonly');
370
+ }
332
371
  this._dispatchEvent('readwrite');
333
372
  }
334
373
  }
@@ -346,7 +385,6 @@ export default class AbstractControl extends UIElement {
346
385
  }
347
386
 
348
387
  // todo - review alert handling altogether. There could be potentially multiple ones in model
349
- // TODO: both required and handleValid set valid attrs and aria attrs. Duplicate code
350
388
  handleValid() {
351
389
  // console.log('mip valid', this.modelItem.required);
352
390
 
@@ -363,12 +401,10 @@ export default class AbstractControl extends UIElement {
363
401
  this._dispatchEvent('valid');
364
402
  this.setAttribute('valid', '');
365
403
  this.removeAttribute('invalid');
366
- this.getWidget().setAttribute('aria-invalid', 'false');
367
404
  // also reset other dependent CSS classes
368
405
  this.classList.remove('isEmpty');
369
406
  } else {
370
407
  this.setAttribute('invalid', '');
371
- this.getWidget().setAttribute('aria-invalid', 'true');
372
408
  this.removeAttribute('valid');
373
409
  // ### constraint is invalid - handle alerts
374
410
  /*
@@ -384,10 +420,15 @@ export default class AbstractControl extends UIElement {
384
420
  alerts.forEach(modelAlert => {
385
421
  const newAlert = document.createElement('fx-alert');
386
422
  // const newAlert = document.createElement('span');
423
+ newAlert.id = newAlert.id || `fx-alert-${Fore.createUUID()}`;
387
424
  newAlert.innerHTML = modelAlert;
388
425
  this.appendChild(newAlert);
426
+ this._addDescribedBy(newAlert.id);
389
427
  // newAlert.style.display = 'block';
390
428
  });
429
+ } else {
430
+ controlAlert.id = controlAlert.id || `fx-alert-${Fore.createUUID()}`;
431
+ this._addDescribedBy(controlAlert.id);
391
432
  }
392
433
  }
393
434
 
@@ -425,6 +466,7 @@ export default class AbstractControl extends UIElement {
425
466
  this.setAttribute('nonrelevant', '');
426
467
  this.removeAttribute('relevant');
427
468
  }
469
+ this._reflectRelevantInert(newEnabled);
428
470
 
429
471
  // Dispatch only on actual change
430
472
  if (wasEnabled !== newEnabled) {
@@ -7,6 +7,10 @@ export class FxAlert extends AbstractControl {
7
7
  }
8
8
 
9
9
  connectedCallback() {
10
+ this.setAttribute('role', 'alert');
11
+ this.setAttribute('aria-live', 'assertive');
12
+ this.setAttribute('aria-atomic', 'true');
13
+
10
14
  const style = `
11
15
  :host {
12
16
  height: auto;
package/src/ui/fx-case.js CHANGED
@@ -93,7 +93,7 @@ export class FxCase extends FxContainer {
93
93
  ownerForm.addToBatchedNotifications(target);
94
94
  ownerForm.refresh(false);
95
95
  });
96
- this.addEventListener('deselect', (event) => {
96
+ this.addEventListener('deselect', event => {
97
97
  const ownerForm = this.getOwnerForm();
98
98
  ownerForm.addToBatchedNotifications(event.target);
99
99
  });
@@ -113,6 +113,7 @@ export class FxContainer extends UIElement {
113
113
  // console.log('container is not relevant');
114
114
  this.removeAttribute('relevant', '');
115
115
  this.setAttribute('nonrelevant', '');
116
+ this._reflectRelevantInert(false);
116
117
  this.dispatchEvent(new CustomEvent('disabled', {}));
117
118
  return;
118
119
  }
@@ -122,10 +123,12 @@ export class FxContainer extends UIElement {
122
123
  // this.style.display = 'block';
123
124
  this.removeAttribute('nonrelevant', '');
124
125
  this.setAttribute('relevant', '');
126
+ this._reflectRelevantInert(true);
125
127
  this.dispatchEvent(new CustomEvent('enabled', {}));
126
128
  } else {
127
129
  this.removeAttribute('relevant', '');
128
130
  this.setAttribute('nonrelevant', '');
131
+ this._reflectRelevantInert(false);
129
132
  this.dispatchEvent(new CustomEvent('disabled', {}));
130
133
  }
131
134
  }