jsswift 1.0.49 → 1.0.50

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/AI.md CHANGED
@@ -93,3 +93,9 @@ Run the app's build after implementation and check values, validation, dialog ke
93
93
  - Other components: [index](components/README.md). MCP: read `guide:AI`, then search only for additional features.
94
94
 
95
95
  For a complete application flow, read [the compact CRUD example](examples/crud.md) only when the task needs editing, validation, persistence and focus restoration together.
96
+
97
+ ## Verification evidence and environment
98
+
99
+ Check whether browser tooling and server permissions are available before launching checks. Run Git commands only in a Git worktree; for example test `git rev-parse --is-inside-work-tree` before requesting a diff. Keep build, pure-logic tests and server startup results separate. An HTTP response confirms a reachable server, not that your startup command succeeded or that browser interactions work. Report environment failures separately from application failures. Test the implemented functions rather than rebuilding equivalent data inside an assertion.
100
+
101
+ Dialog autofocus preserves focus assigned inside `onOpen`; verify initial focus separately from validation focus and return focus. Field messages are associated with Input/Select through `aria-describedby` automatically.
@@ -93,3 +93,9 @@ Run the app's build after implementation and check values, validation, dialog ke
93
93
  - Other components: [index](components/README.md). MCP: read `guide:AI`, then search only for additional features.
94
94
 
95
95
  For a complete application flow, read [the compact CRUD example](examples/crud.md) only when the task needs editing, validation, persistence and focus restoration together.
96
+
97
+ ## Verification evidence and environment
98
+
99
+ Check whether browser tooling and server permissions are available before launching checks. Run Git commands only in a Git worktree; for example test `git rev-parse --is-inside-work-tree` before requesting a diff. Keep build, pure-logic tests and server startup results separate. An HTTP response confirms a reachable server, not that your startup command succeeded or that browser interactions work. Report environment failures separately from application failures. Test the implemented functions rather than rebuilding equivalent data inside an assertion.
100
+
101
+ Dialog autofocus preserves focus assigned inside `onOpen`; verify initial focus separately from validation focus and return focus. Field messages are associated with Input/Select through `aria-describedby` automatically.
@@ -82,3 +82,7 @@ dialog.open();
82
82
  ```
83
83
 
84
84
  Check Escape, Tab, focus restoration, and the accessible dialog name in the rendered application. Do not infer these checks from a successful build.
85
+
86
+ ## Initial focus ordering
87
+
88
+ `onOpen(entry)` runs synchronously after mounting. Deferred autofocus selects the first control only when focus is outside the panel; focus assigned inside `onOpen` is preserved. Use `autoFocus: false` to disable automatic placement entirely. Closed overlays do not run pending autofocus.
@@ -471,3 +471,5 @@ Use the documented `model` binding for reactive state. Verify the rendered contr
471
471
  `ariaLabel` sets the control's accessible name; a string `label` supplies the default. Use `ariaLabelledBy` for an external label ID and `ariaDescribedBy` for hint/error IDs. `ariaInvalid` overrides the invalid state; otherwise a truthy `error` marks the control invalid. `required` sets `aria-required` (and native required for Input). These props accept reactive values.
472
472
 
473
473
  Input forwards `min`, `max`, `step`, `pattern`, `minLength` and `maxLength` to the native input, including zero values. Native constraints do not replace application validation.
474
+
475
+ Rendered field messages (error, warning, success, note or hint) receive a stable ID and are automatically associated with the interactive control through `aria-describedby`. External `ariaDescribedBy` IDs are preserved; the generated ID is removed when its message disappears.
@@ -480,3 +480,5 @@ Mount both controls; choose Casa through the menu, then save. The logged model v
480
480
  `focus(options?)` on the returned field focuses the interactive input/combobox (it does not open Select). Prefer this method over private DOM references.
481
481
 
482
482
  `ariaLabel` sets the control's accessible name; a string `label` supplies the default. Use `ariaLabelledBy` for an external label ID and `ariaDescribedBy` for hint/error IDs. `ariaInvalid` overrides the invalid state; otherwise a truthy `error` marks the control invalid. `required` sets `aria-required` (and native required for Input). These props accept reactive values.
483
+
484
+ Rendered field messages (error, warning, success, note or hint) receive a stable ID and are automatically associated with the interactive control through `aria-describedby`. External `ariaDescribedBy` IDs are preserved; the generated ID is removed when its message disappears.
@@ -6,6 +6,7 @@ Load JSswift and its UI CSS, then run the following code with an `#app` element.
6
6
  const _ = window._;
7
7
  const key = 'jsswift-crud-example';
8
8
  const categories = ['Casa', 'Ufficio'];
9
+ const priceFormat = new Intl.NumberFormat('it-IT', {style:'currency', currency:'EUR'});
9
10
  const initial = () => Array.from({length: 6}, (_, i) => ({id:i+1, name:`Prodotto ${i+1}`, category:categories[i%2], price:10, quantity:2}));
10
11
  const valid = rows => Array.isArray(rows) && new Set(rows.map(r => r?.id)).size === rows.length && rows.every(r => r && Number.isSafeInteger(r.id) && r.id > 0 && typeof r.name === 'string' && r.name.trim() && categories.includes(r.category) && Number.isFinite(r.price) && r.price >= 0 && Number.isSafeInteger(r.quantity) && r.quantity >= 0);
11
12
  let rows = initial(), editing = null, origin = 'add-product', query = '', category = '', page = 1;
@@ -50,7 +51,7 @@ function render() {
50
51
  const found = rows.filter(r => r.name.toLowerCase().includes(query.trim().toLowerCase()) && (!category || r.category === category));
51
52
  const pages = Math.max(1,Math.ceil(found.length/5)); page = Math.min(page,pages);
52
53
  host.replaceChildren(_.Table({rows:found.slice((page-1)*5,page*5),rowKey:'id',pagination:false,hideFooter:true,minTableWidth:600,
53
- columns:[{key:'name',label:'Nome'},{key:'category',label:'Categoria'},{key:'price',label:'Prezzo'},{key:'quantity',label:'Quantità'},
54
+ columns:[{key:'name',label:'Nome'},{key:'category',label:'Categoria'},{key:'price',label:'Prezzo',format:value => priceFormat.format(value)},{key:'quantity',label:'Quantità'},
54
55
  {key:'actions',label:'Azioni',render:r => _.Button({id:`edit-${r.id}`,onClick:() => edit(r)},'Modifica')}] }));
55
56
  pager.replaceChildren(_.span(`${found.length} prodotti — pagina ${page}/${pages}`),
56
57
  _.Button({id:'previous',disabled:page===1,onClick:() => {page--;render();document.getElementById('previous').focus();}},'Precedente'),
@@ -59,4 +60,6 @@ function render() {
59
60
  _.mount('#app', _.div({style:{minWidth:0}}, add, search, filter, host, pager, notice)); render();
60
61
  ```
61
62
 
62
- Application validation remains explicit. `error` marks the control invalid; use `ariaDescribedBy` when associating an application-owned error message. This example does not provide delete, server synchronization or a complete visual design.
63
+ Application validation remains explicit. `error` marks the control invalid; rendered field messages are associated automatically. Use `ariaDescribedBy` for additional application-owned descriptions. This example does not provide delete, server synchronization or a complete visual design.
64
+
65
+ Performance: the currency formatter is reused. This small example rebuilds its table for clarity; profile larger datasets before changing rendering strategy. `onOpen` focuses the name field and automatic focus respects that choice.
@@ -983,5 +983,5 @@
983
983
  "documentAliases": {
984
984
  "components/button.md": "components/btn.md"
985
985
  },
986
- "packageVersion": "1.0.49"
986
+ "packageVersion": "1.0.50"
987
987
  }
package/dist/jsswift.js CHANGED
@@ -648,7 +648,10 @@
648
648
 
649
649
  // scroll lock + focus
650
650
  if (opts.lockScroll) lockScroll();
651
- if (opts.autoFocus !== false) setTimeout(() => focusFirst(panel), 0);
651
+ if (opts.autoFocus !== false) setTimeout(() => {
652
+ // Respect focus deliberately assigned by an onOpen callback.
653
+ if (panel.isConnected && stack.get(id) === entry && !panel.contains(document.activeElement)) focusFirst(panel);
654
+ }, 0);
652
655
 
653
656
  // positioning (for menus/tooltips)
654
657
  const position = (nextOpts = null) => {
@@ -7510,6 +7513,7 @@ const unitCover = (v, name = 'size') => {
7510
7513
  // props: { model: rod OR [get,set] signal OR plain, placeholder, type }
7511
7514
 
7512
7515
 
7516
+ let uiFieldMessageId = 0;
7513
7517
  UI.FormField = (props = {}) => {
7514
7518
 
7515
7519
  applyCommonProps(props);
@@ -7642,6 +7646,14 @@ const unitCover = (v, name = 'size') => {
7642
7646
  return true;
7643
7647
  };
7644
7648
  let messageEl = null;
7649
+ const messageId = `jsswift-field-message-${++uiFieldMessageId}`;
7650
+ const syncDescription = () => {
7651
+ if (!controlEl?.setAttribute) return;
7652
+ const ids = [uiUnwrap(props.ariaDescribedBy), messageEl?.id].filter(Boolean).join(" ").trim();
7653
+ if (ids) controlEl.setAttribute("aria-describedby", [...new Set(ids.split(/\s+/))].join(" "));
7654
+ else controlEl.removeAttribute("aria-describedby");
7655
+ };
7656
+ wrap._syncDescription = syncDescription;
7645
7657
  const renderMessage = () => {
7646
7658
  const statusOrder = [
7647
7659
  { key: "error", value: resolveValue(props.error), slot: "errorMessage", className: "cms-error" },
@@ -7669,7 +7681,7 @@ const unitCover = (v, name = 'size') => {
7669
7681
  ? renderSlotToArray(slots, active.slot, ctx, active.value)
7670
7682
  : [];
7671
7683
  const nextEl = nodes.length
7672
- ? _.div({ class: active?.className }, ...nodes)
7684
+ ? _.div({ id: messageId, class: active?.className }, ...nodes)
7673
7685
  : null;
7674
7686
 
7675
7687
  const stateKeys = ["error", "warning", "success", "note"];
@@ -7682,6 +7694,7 @@ const unitCover = (v, name = 'size') => {
7682
7694
  wrap.appendChild(nextEl);
7683
7695
  }
7684
7696
  messageEl = nextEl;
7697
+ syncDescription();
7685
7698
  };
7686
7699
 
7687
7700
  // state updater
@@ -7834,7 +7847,7 @@ const unitCover = (v, name = 'size') => {
7834
7847
  const label = uiUnwrap(props.ariaLabel) ?? (typeof props.label === "string" ? props.label : null);
7835
7848
  set("aria-label", label);
7836
7849
  set("aria-labelledby", uiUnwrap(props.ariaLabelledBy));
7837
- set("aria-describedby", uiUnwrap(props.ariaDescribedBy));
7850
+ field._syncDescription?.();
7838
7851
  const invalid = props.ariaInvalid == null ? !!uiUnwrap(props.error) : !!uiUnwrap(props.ariaInvalid);
7839
7852
  control.setAttribute("aria-invalid", String(invalid));
7840
7853
  const required = !!uiUnwrap(props.required);