@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
@@ -5,12 +5,15 @@ import XfAbstractControl from './abstract-control.js';
5
5
  * in a popup list for activation. 'on-demand' is not a state like 'relevant' but just
6
6
  * shows/hides controls on demand. The controls still behave as usual otherwise.
7
7
  *
8
- *
8
+ * Implements the ARIA "menu button" pattern: the slotted trigger gets `aria-haspopup`/
9
+ * `aria-expanded`/`aria-controls`, the popup gets `role="menu"` with `role="menuitem"`
10
+ * entries, and Up/Down/Home/End move a roving tabindex across the open menu.
9
11
  */
10
12
  export class FxControlMenu extends XfAbstractControl {
11
13
  connectedCallback() {
12
14
  this.attachShadow({ mode: 'open' });
13
15
  this.selectExpr = this.getAttribute('select');
16
+ this.triggerButton = null;
14
17
 
15
18
  const style = `
16
19
  :host {
@@ -52,10 +55,11 @@ export class FxControlMenu extends XfAbstractControl {
52
55
  this.shadowRoot.innerHTML = `
53
56
  <style>${style}</style>
54
57
  <slot></slot>
55
- <div class="menu" part="menu"></div>
58
+ <div class="menu" part="menu" role="menu"></div>
56
59
  `;
57
60
 
58
61
  this.menuEl = this.shadowRoot.querySelector('.menu');
62
+ this.menuEl.addEventListener('keydown', e => this._handleMenuKeydown(e));
59
63
 
60
64
  // Slotted button click
61
65
  const slot = this.shadowRoot.querySelector('slot');
@@ -64,12 +68,19 @@ export class FxControlMenu extends XfAbstractControl {
64
68
  const button = nodes.find(
65
69
  node => node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BUTTON',
66
70
  );
67
- if (button) {
71
+ if (button && button !== this.triggerButton) {
72
+ this.triggerButton = button;
73
+ button.setAttribute('aria-haspopup', 'true');
74
+ button.setAttribute('aria-expanded', 'false');
68
75
  button.addEventListener('click', e => {
69
76
  e.preventDefault();
70
77
  e.stopPropagation();
71
78
  this.updateMenu();
72
- this.menuEl.classList.toggle('visible');
79
+ if (this.menuEl.classList.contains('visible')) {
80
+ this._closeMenu();
81
+ } else {
82
+ this._openMenu();
83
+ }
73
84
  });
74
85
  }
75
86
  });
@@ -83,14 +94,14 @@ export class FxControlMenu extends XfAbstractControl {
83
94
  document.addEventListener('click', e => {
84
95
  const inside = this.contains(e.target) || this.shadowRoot.contains(e.target);
85
96
  if (!inside) {
86
- this.menuEl.classList.remove('visible');
97
+ this._closeMenu();
87
98
  }
88
99
  });
89
100
 
90
- // Close on Escape
101
+ // Close on Escape, returning focus to the trigger
91
102
  document.addEventListener('keydown', e => {
92
- if (e.key === 'Escape') {
93
- this.menuEl.classList.remove('visible');
103
+ if (e.key === 'Escape' && this.menuEl.classList.contains('visible')) {
104
+ this._closeMenu({ restoreFocus: true });
94
105
  }
95
106
  });
96
107
 
@@ -132,11 +143,66 @@ export class FxControlMenu extends XfAbstractControl {
132
143
  return null;
133
144
  }
134
145
 
146
+ /**
147
+ * Opens the popup, reflects `aria-expanded` on the trigger, and moves focus to the
148
+ * first menu item (ARIA "menu button" pattern).
149
+ */
150
+ _openMenu() {
151
+ this.menuEl.classList.add('visible');
152
+ this.triggerButton?.setAttribute('aria-expanded', 'true');
153
+ this.menuEl.querySelector('a')?.focus();
154
+ }
155
+
156
+ /**
157
+ * Closes the popup and reflects `aria-expanded` on the trigger. Focus is only restored
158
+ * to the trigger for dismissal paths (Escape, outside click) - not on item selection,
159
+ * where `el.activate()` already moves focus to the newly revealed control.
160
+ */
161
+ _closeMenu({ restoreFocus = false } = {}) {
162
+ this.menuEl.classList.remove('visible');
163
+ this.triggerButton?.setAttribute('aria-expanded', 'false');
164
+ if (restoreFocus) {
165
+ this.triggerButton?.focus();
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Roving-tabindex arrow-key navigation between menu items (Up/Down/Home/End), following
171
+ * the WAI-ARIA menu pattern.
172
+ */
173
+ _handleMenuKeydown(e) {
174
+ const items = Array.from(this.menuEl.querySelectorAll('a'));
175
+ if (items.length === 0) return;
176
+ const idx = items.indexOf(e.target);
177
+ if (idx === -1) return;
178
+
179
+ let newIdx;
180
+ switch (e.key) {
181
+ case 'ArrowDown':
182
+ newIdx = (idx + 1) % items.length;
183
+ break;
184
+ case 'ArrowUp':
185
+ newIdx = (idx - 1 + items.length) % items.length;
186
+ break;
187
+ case 'Home':
188
+ newIdx = 0;
189
+ break;
190
+ case 'End':
191
+ newIdx = items.length - 1;
192
+ break;
193
+ default:
194
+ return;
195
+ }
196
+ e.preventDefault();
197
+ items.forEach((item, i) => item.setAttribute('tabindex', i === newIdx ? '0' : '-1'));
198
+ items[newIdx].focus();
199
+ }
200
+
135
201
  updateMenu() {
136
202
  const container = this._getScopedContainer();
137
203
  if (!container) return;
138
204
 
139
- let targets = [];
205
+ const targets = [];
140
206
  // ✅ Include container itself if it has on-demand
141
207
  if (container.hasAttribute('on-demand')) {
142
208
  targets.push(container);
@@ -163,7 +229,7 @@ export class FxControlMenu extends XfAbstractControl {
163
229
  }
164
230
 
165
231
  if (targets.length === 0) {
166
- this.menuEl.classList.remove('visible');
232
+ this._closeMenu();
167
233
  return;
168
234
  }
169
235
 
@@ -182,6 +248,8 @@ export class FxControlMenu extends XfAbstractControl {
182
248
  const item = document.createElement('a');
183
249
  item.href = '#';
184
250
  item.textContent = label;
251
+ item.setAttribute('role', 'menuitem');
252
+ item.setAttribute('tabindex', index === 0 ? '0' : '-1');
185
253
 
186
254
  item.addEventListener('click', e => {
187
255
  e.preventDefault();
@@ -189,7 +257,7 @@ export class FxControlMenu extends XfAbstractControl {
189
257
  el.activate();
190
258
  }
191
259
 
192
- this.menuEl.classList.remove('visible');
260
+ this._closeMenu();
193
261
 
194
262
  // Wait one frame to let DOM updates (like on-demand removal) take effect
195
263
  requestAnimationFrame(() => {
@@ -9,8 +9,8 @@ import getInScopeContext from '../getInScopeContext.js';
9
9
  import { Fore } from '../fore.js';
10
10
  import { debounce } from '../events.js';
11
11
  import { FxModel } from '../fx-model.js';
12
+ import { FxFore } from '../fx-fore.js';
12
13
  import { DependencyNotifyingDomFacade } from '../DependencyNotifyingDomFacade';
13
- import { extractPredicateDependencies } from '../extract-predicate-deps.js';
14
14
  import '../fx-instance.js';
15
15
 
16
16
  const WIDGETCLASS = 'widget';
@@ -122,14 +122,23 @@ export default class FxControl extends XfAbstractControl {
122
122
  );
123
123
  }
124
124
 
125
- this.shadowRoot.innerHTML = `
125
+ const controlHtml = this.renderHTML(this.ref);
126
+ const sheet = Fore.getSharedStyleSheet(style);
127
+ if (sheet) {
128
+ this.shadowRoot.innerHTML = controlHtml;
129
+ this.shadowRoot.adoptedStyleSheets = [sheet];
130
+ } else {
131
+ this.shadowRoot.innerHTML = `
126
132
  <style>
127
133
  ${style}
128
134
  </style>
129
- ${this.renderHTML(this.ref)}
135
+ ${controlHtml}
130
136
  `;
137
+ }
131
138
 
132
139
  this.widget = this.getWidget();
140
+ this._associateLabel();
141
+ this._associateDescriptions();
133
142
 
134
143
  this.addEventListener('mousedown', e => {
135
144
  // ### prevent mousedown events on all control content that is not the widget or within the widget
@@ -155,6 +164,8 @@ export default class FxControl extends XfAbstractControl {
155
164
  listenOn = target;
156
165
  }
157
166
  }
167
+ // retained for setValue()'s undo-coalescing focus check (see there)
168
+ this._listenOn = listenOn;
158
169
 
159
170
  this.addEventListener('keyup', () => {
160
171
  FxModel.dataChanged = true;
@@ -172,6 +183,14 @@ export default class FxControl extends XfAbstractControl {
172
183
  });
173
184
  this.updateEvent = 'blur'; // needs to be registered too
174
185
  }
186
+ // blur both commits the final value and closes any open undo-coalescing session, so
187
+ // typing into this field again after leaving it starts a new undo step. Must be a
188
+ // normal (non-`once`) listener: a field can be focused/blurred more than once.
189
+ const onBlur = () => {
190
+ this.setValue(this._getValueOfWidget());
191
+ this.getModel()?.getEffectiveUndoManager()?.endCoalescingSession();
192
+ };
193
+
175
194
  if (this.debounceDelay) {
176
195
  listenOn.addEventListener(
177
196
  this.updateEvent,
@@ -185,6 +204,7 @@ export default class FxControl extends XfAbstractControl {
185
204
  this.debounceDelay,
186
205
  ),
187
206
  );
207
+ listenOn.addEventListener('blur', onBlur);
188
208
  } else {
189
209
  listenOn.addEventListener(this.updateEvent, event => {
190
210
  if (this._isRefreshing) {
@@ -193,13 +213,7 @@ export default class FxControl extends XfAbstractControl {
193
213
  }
194
214
  this.setValue(this._getValueOfWidget());
195
215
  });
196
- listenOn.addEventListener(
197
- 'blur',
198
- event => {
199
- this.setValue(this._getValueOfWidget());
200
- },
201
- { once: true },
202
- );
216
+ listenOn.addEventListener('blur', onBlur);
203
217
  }
204
218
 
205
219
  this.addEventListener('return', e => {
@@ -272,18 +286,61 @@ export default class FxControl extends XfAbstractControl {
272
286
  return; // do nothing when modelItem is readonly
273
287
  }
274
288
 
289
+ // direct widget edits bypass the action pipeline (no execute()), so undo capture
290
+ // happens here - unless an action chain is already running and capturing, or the
291
+ // form is mid-refresh (teardown blurs replay stale widget values; recording those
292
+ // would clear the redo stack right after an undo)
293
+ const undoManager = this.getModel()?.getEffectiveUndoManager();
294
+ const captureUndo =
295
+ !!undoManager?.enabled &&
296
+ FxFore.outermostHandler === null &&
297
+ !this.getOwnerForm()?.isRefreshing;
298
+ if (captureUndo) undoManager.beginCapture();
299
+ const undoKey = captureUndo
300
+ ? Array.isArray(modelitem?.node)
301
+ ? modelitem.node[0]
302
+ : modelitem?.node
303
+ : null;
304
+ const valueBefore = captureUndo ? modelitem?.value : null;
305
+ // coalesce only while the widget is genuinely focused - a session opened by a fully
306
+ // programmatic call (no real focus) would never see a blur to close it, and could
307
+ // silently absorb an unrelated later edit to the same node. Also coalesce when a
308
+ // session for this exact node is already open: the blur handler's own setValue() call
309
+ // fires *after* document.activeElement has already moved away, so without this it
310
+ // would wrongly split the final keystroke into its own entry instead of merging it
311
+ // into the session that blur is about to close.
312
+ const isFocused =
313
+ captureUndo &&
314
+ (document.activeElement === this._listenOn || undoManager?.openSession?.key === undoKey);
315
+ const commitUndo = () => {
316
+ if (!captureUndo) return;
317
+ if (modelitem && modelitem.value !== valueBefore) {
318
+ if (isFocused) {
319
+ undoManager.commitCoalesced(undoKey);
320
+ } else {
321
+ undoManager.commit(undoKey);
322
+ }
323
+ } else {
324
+ undoManager.discard();
325
+ }
326
+ };
327
+
275
328
  if (this.getAttribute('as') === 'node') {
276
329
  const replace = this.shadowRoot.getElementById('replace');
277
330
  replace.replace(this.nodeset, val);
278
331
  if (modelitem && val && val !== modelitem.value) {
279
332
  modelitem.value = val;
280
333
  FxModel.dataChanged = true;
334
+ commitUndo();
281
335
  replace.actionPerformed();
336
+ } else {
337
+ commitUndo();
282
338
  }
283
339
  return;
284
340
  }
285
341
  const setval = this.shadowRoot.getElementById('setvalue');
286
342
  setval.setValue(modelitem, val);
343
+ commitUndo();
287
344
 
288
345
  /*
289
346
  if (this.modelItem instanceof ModelItem && !this.modelItem?.boundControls.includes(this)) {
@@ -329,6 +386,41 @@ export default class FxControl extends XfAbstractControl {
329
386
  `;
330
387
  }
331
388
 
389
+ /**
390
+ * Associates the control's label with its widget.
391
+ *
392
+ * Two label mechanisms exist: a light-DOM `<label>` child (slotted alongside the widget, so
393
+ * both share the same tree and `for`/`id` works), and the `label="..."` attribute (rendered as
394
+ * bare text into the *shadow root* by `renderHTML()` - a different tree than the light-DOM
395
+ * widget, so an id reference cannot cross that boundary and `aria-label` (a string, not an id
396
+ * reference) is used instead).
397
+ */
398
+ _associateLabel() {
399
+ const label = this.querySelector(':scope > label');
400
+ if (label) {
401
+ const id = label.getAttribute('for') || this.widget.id || `fx-${Fore.createUUID()}`;
402
+ if (!label.hasAttribute('for')) label.setAttribute('for', id);
403
+ if (!this.widget.id) this.widget.id = id;
404
+ return;
405
+ }
406
+ if (this.label) {
407
+ this.widget.setAttribute('aria-label', this.label);
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Wires any statically-authored `<fx-hint>`/`<fx-alert>` light-DOM children into the widget's
413
+ * aria-describedby, so validation/help text is announced as part of the field's description.
414
+ * Dynamically-created alerts (from a `<fx-bind>`'s `alert`) are wired separately in
415
+ * `AbstractControl.handleValid()`.
416
+ */
417
+ _associateDescriptions() {
418
+ this.querySelectorAll(':scope > fx-hint, :scope > fx-alert').forEach(el => {
419
+ el.id = el.id || `fx-${el.localName}-${Fore.createUUID()}`;
420
+ this._addDescribedBy(el.id);
421
+ });
422
+ }
423
+
332
424
  /**
333
425
  * The widget is the actual control being used in the UI e.g. a native input control or any
334
426
  * other component that presents a control that can be interacted with.
@@ -551,13 +643,11 @@ export default class FxControl extends XfAbstractControl {
551
643
  this.widget = imported;
552
644
 
553
645
  if (!theFore) {
554
- Fore.dispatch('error', {
555
- detail: {
556
- message: `Fore element not found in '${this.src}'. Maybe wrapped within 'template' element?`,
557
- },
646
+ await Fore.dispatch(this, 'error', {
647
+ message: `Fore element not found in '${this.src}'. Maybe wrapped within 'template' element?`,
558
648
  });
559
649
  }
560
- Fore.dispatch('loaded', { detail: { fore: theFore } });
650
+ await Fore.dispatch(this, 'loaded', { fore: theFore });
561
651
  } catch (error) {
562
652
  // console.log('error', error);
563
653
  Fore.dispatch(this, 'error', {
@@ -689,7 +779,7 @@ export default class FxControl extends XfAbstractControl {
689
779
  if (dataRefd && dataRefd.closest('fx-control') === this) {
690
780
  this.boundList = dataRefd;
691
781
  const ref = dataRefd.getAttribute('data-ref');
692
- this._handleBoundWidget(dataRefd, true); //todo: revisit !!! observer
782
+ this._handleBoundWidget(dataRefd, true); // todo: revisit !!! observer
693
783
  }
694
784
  }
695
785
 
@@ -714,23 +804,18 @@ export default class FxControl extends XfAbstractControl {
714
804
  const inscope = getInScopeContext(this, ref);
715
805
  // const nodeset = evaluateXPathToNodes(ref, inscope, this);
716
806
 
717
- const touchedNodes = new Set();
718
- const domFacade = new DependencyNotifyingDomFacade(node => touchedNodes.add(node));
719
-
720
- const nodeset = evaluateXPath(ref, inscope, this, domFacade);
721
-
722
- const contextNode = Array.isArray(inscope) ? inscope[0] : inscope;
723
- // console.log('Extracting model', this.getModel());
724
- // console.log('Extracting model inited', this.getModel().inited);
725
- const model = this.getModel();
726
- if (!contextNode) return;
727
- // console.log('Extracting predicate deps from ref:', ref);
728
- extractPredicateDependencies(
729
- ref,
730
- model,
731
- mi => mi.addObserver(this),
732
- node => model.getModelItem(node),
733
- );
807
+ let touchedNodes = null;
808
+ let domFacade = null;
809
+ if (this._refNeedsDependencyTracking(ref)) {
810
+ touchedNodes = new Set();
811
+ domFacade = new DependencyNotifyingDomFacade(node => touchedNodes.add(node));
812
+ }
813
+
814
+ const nodeset = evaluateXPath(ref, inscope, this, {}, {}, domFacade);
815
+
816
+ if (touchedNodes) {
817
+ this._trackRefDependencies(touchedNodes, 'widget');
818
+ }
734
819
 
735
820
  // ### bail out when nodeset is array and empty
736
821
  if (Array.isArray(nodeset) && nodeset.length === 0) return;
@@ -783,6 +868,12 @@ export default class FxControl extends XfAbstractControl {
783
868
  this.updateEntry(newEntry, nodeset);
784
869
  }
785
870
  this.boundInitialized = true;
871
+
872
+ // Newly created entries may carry template expressions in attributes that
873
+ // updateEntry() does not know about (e.g. a `name="{$qno}"` on a generated
874
+ // radio input). Flag them for scanning on the next fx-fore refresh, the same
875
+ // way fx-repeat does for newly rendered repeat items.
876
+ this.getOwnerForm()?.scanForNewTemplateExpressionsNextRefresh();
786
877
  }
787
878
 
788
879
  if (this.valueProp === 'selectedOptions') {
@@ -809,8 +900,7 @@ export default class FxControl extends XfAbstractControl {
809
900
  }
810
901
 
811
902
  const valueExpr = valueAttribute;
812
- const cutted = valueExpr.substring(1, valueExpr.length - 1);
813
- const evaluated = evaluateXPathToString(cutted, node, this);
903
+ const evaluated = Fore.evaluateTemplateString(valueExpr, node, this);
814
904
  newEntry.setAttribute('value', evaluated);
815
905
 
816
906
  if (this.value === evaluated) {
@@ -818,10 +908,9 @@ export default class FxControl extends XfAbstractControl {
818
908
  }
819
909
 
820
910
  if (newEntry.hasAttribute('title')) {
821
- let titleExpr = newEntry.getAttribute('title');
822
- titleExpr = titleExpr.substring(1, titleExpr.length - 1);
823
- const evaluated = evaluateXPathToString(titleExpr, node, newEntry);
824
- newEntry.setAttribute('title', evaluated);
911
+ const titleExpr = newEntry.getAttribute('title');
912
+ const evaluatedTitle = Fore.evaluateTemplateString(titleExpr, node, newEntry);
913
+ newEntry.setAttribute('title', evaluatedTitle);
825
914
  }
826
915
  // ### set label
827
916
  const optionLabel = newEntry.textContent.trim();
@@ -836,10 +925,10 @@ export default class FxControl extends XfAbstractControl {
836
925
  * @param {HTMLElement} newEntry
837
926
  */
838
927
  evalLabel(optionLabel, node, newEntry) {
839
- const labelExpr = optionLabel.trim().substring(1, optionLabel.length - 1);
928
+ const labelExpr = optionLabel.trim();
840
929
  if (!labelExpr) return;
841
930
 
842
- const label = evaluateXPathToString(labelExpr, node, this);
931
+ const label = Fore.evaluateTemplateString(labelExpr, node, this);
843
932
  newEntry.textContent = label;
844
933
  }
845
934
 
@@ -1,5 +1,10 @@
1
1
  import { Fore } from '../fore.js';
2
2
 
3
+ /**
4
+ * @deprecated - use the native HTML `<dialog>` element instead, which already provides a
5
+ * focus trap, `aria-modal`, and Escape-to-close for free. `fx-show`/`fx-hide` work unchanged
6
+ * against `<dialog>` (see `demo/dialog.html`), so this is a markup-only migration.
7
+ */
3
8
  export class FxDialog extends HTMLElement {
4
9
  static get properties() {
5
10
  return {
@@ -1,4 +1,4 @@
1
- import { evaluateXPath, evaluateXPathToString, resolveId } from '../xpath-evaluation.js';
1
+ import { resolveId } from '../xpath-evaluation.js';
2
2
  import FxControl from './fx-control.js';
3
3
  import { Fore } from '../fore.js';
4
4
  import { XPathUtil } from '../xpath-util.js';
@@ -119,11 +119,12 @@ export class FxItems extends FxControl {
119
119
 
120
120
  // ### handle 'label'
121
121
  const label = newEntry.querySelector('label');
122
- const lblExpr = Fore.getExpression(label.textContent);
123
122
 
124
123
  // ROOT FIX: JSON lens nodes are objects; do NOT use direct JS property access.
125
124
  // Always go through evaluateXPathToString() for JSON too.
126
- const lblEvaluated = evaluateXPathToString(lblExpr, node, this);
125
+ // Use evaluateTemplateString() rather than getExpression() as the label may mix
126
+ // static text with one or more `{expr}` placeholders (e.g. "foo {name}").
127
+ const lblEvaluated = Fore.evaluateTemplateString(label.textContent, node, this);
127
128
  // console.log('lblEvaluated ', lblEvaluated);
128
129
  label.textContent = lblEvaluated;
129
130
 
@@ -134,10 +135,9 @@ export class FxItems extends FxControl {
134
135
  const input = newEntry.querySelector('[value]');
135
136
  // getting expr
136
137
  const expr = input.value;
137
- const cutted = Fore.getExpression(expr);
138
138
 
139
139
  // ROOT FIX: same here
140
- const evaluated = evaluateXPathToString(cutted, node, this);
140
+ const evaluated = Fore.evaluateTemplateString(expr, node, this);
141
141
  // console.log('evaluated ', lblEvaluated);
142
142
 
143
143
  // Set both property and attribute so *any* downstream code path works
@@ -158,4 +158,4 @@ export class FxItems extends FxControl {
158
158
 
159
159
  if (!customElements.get('fx-items')) {
160
160
  customElements.define('fx-items', FxItems);
161
- }
161
+ }
@@ -51,17 +51,39 @@ export class FxOutput extends XfAbstractControl {
51
51
  </span>
52
52
  `;
53
53
 
54
- this.shadowRoot.innerHTML = `
54
+ const sheet = Fore.getSharedStyleSheet(style);
55
+ if (sheet) {
56
+ this.shadowRoot.innerHTML = outputHtml;
57
+ this.shadowRoot.adoptedStyleSheets = [sheet];
58
+ } else {
59
+ this.shadowRoot.innerHTML = `
55
60
  <style>
56
61
  ${style}
57
62
  </style>
58
63
  ${outputHtml}
59
64
  `;
65
+ }
60
66
  // this.widget = this.shadowRoot.querySelector('#widget');
61
67
  // this.widget = this.getWidget();
62
68
  // console.log('widget ', this.widget);
63
69
  this.mediatype = this.hasAttribute('mediatype') ? this.getAttribute('mediatype') : null;
64
70
 
71
+ // The label is a slotted light-DOM element while the widget is a shadow-DOM <span> - an id
72
+ // reference (for/aria-labelledby) can't cross that boundary, so derive an aria-label string
73
+ // (which can) from the slotted label's text instead.
74
+ const labelSlot = this.shadowRoot.querySelector('slot[name="label"]');
75
+ const applyLabel = () => {
76
+ const text = this._getLabelText();
77
+ const valueEl = this.getWidget();
78
+ if (text) {
79
+ valueEl.setAttribute('aria-label', text);
80
+ } else {
81
+ valueEl.removeAttribute('aria-label');
82
+ }
83
+ };
84
+ labelSlot.addEventListener('slotchange', applyLabel);
85
+ applyLabel();
86
+
65
87
  /*
66
88
  this.addEventListener('slotchange', e => {
67
89
  console.log('slotchange ', e);
@@ -103,6 +125,16 @@ export class FxOutput extends XfAbstractControl {
103
125
  return valueWrapper;
104
126
  }
105
127
 
128
+ _getLabelText() {
129
+ const labelSlot = this.shadowRoot.querySelector('slot[name="label"]');
130
+ if (!labelSlot) return '';
131
+ return labelSlot
132
+ .assignedNodes({ flatten: true })
133
+ .map(n => n.textContent)
134
+ .join('')
135
+ .trim();
136
+ }
137
+
106
138
  handleReadonly() {
107
139
  // An output is always read-only
108
140
  this.setAttribute('readonly', 'readonly');
@@ -138,6 +170,10 @@ export class FxOutput extends XfAbstractControl {
138
170
  if (this.mediatype === 'image') {
139
171
  const img = document.createElement('img');
140
172
  img.setAttribute('src', this.value);
173
+ // axe/WCAG require the accessible name directly on the <img> - aria-label on the
174
+ // wrapping #value span (see applyLabel() above) doesn't satisfy that for a descendant
175
+ // img. Empty alt is still valid (marks it decorative) when no label is given.
176
+ img.setAttribute('alt', this._getLabelText());
141
177
  // Reset the output before adding the image
142
178
  this.innerHTML = '';
143
179
  valueWrapper.appendChild(img);