@jirawatpyk/aura-react 5.18.0 → 5.19.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.
package/README.md CHANGED
@@ -186,6 +186,12 @@ DataTable with sort and page in the URL (4.17): `onStateChange={({ sort, page })
186
186
  - **Time zone**: `timeZone` on AuraProvider, DatePicker, DateRangePicker and Calendar decides "today" (the marker, `min`/`max="today"`, the first month shown); or pass `today` as an ISO date. `todayIn('Asia/Bangkok')` is exported from the root and `/server`.
187
187
  - **Toasts** queue past three instead of dropping: six in a row all show, in order, three at a time.
188
188
 
189
+ ## 5.19 — Chamber-OS addendum 20
190
+
191
+ - **Switch `readOnly`, `icon` and `aria-describedby`** (item 113): `readOnly` keeps a locked switch in the Tab order with `aria-readonly="true"`, so its name, state and notes are heard; click, Space, its label and its row change nothing. It keeps its colours (a locked "on" reads as on) and loses the pointer. `icon` puts an icon at the end of the row, the field icon's size, in fg-secondary (`icon={<IconLock />}`), hidden from screen readers. `aria-describedby` is merged with the description's id, as on TextField and Select. `disabled` wins over `readOnly`.
192
+ - **Select `readOnly`** (item 114): the field stays in the Tab order with `aria-readonly="true"` and shows the chosen option on the read-only ground of a read-only TextField, with no chevron; no click or key opens the list. Unlike `disabled`, the value is still posted with the form, and your code can still set it (`reset()`, `setValue()`). Before JavaScript runs, the options from `options` other than the value are disabled so the native control keeps it (`<option>` children aren't; with no `value` or `defaultValue` nothing is); once hydrated they are enabled again, since a disabled selected option is left out of the form's data. A `multiple` / `size` list box ignores the mouse and the keys that pick (arrows, Home/End, Page keys, letters, Space, Ctrl+A); a change that gets through anyway (a touch tap, a phone's picker, `selectOption` in a test) is put back, and your `onChange` isn't called — though a native `change` or `input` listener on the form still sees the event, with the value already restored.
193
+ - Read-only is `aria-readonly` alone; pair it with a note that says why (`aria-describedby`), which screen readers read after the state.
194
+
189
195
  ## 5.18 — Chamber-OS addendum 19
190
196
 
191
197
  - **Stepper step errors** (item 112): a step can take `status: 'error'` — say, Save found errors on it. It keeps its place in the order and its state (a completed step is still a button with `onStepClick`, the current one keeps `aria-current="step"`), shows the danger tone with an alert icon instead of the check or number (outlined when it is the current step), and its accessible name ends ", has errors" instead of ", completed". On a phone the line reads "Step 2 of 4 — has errors" when the current step has errors; the markers, error ones included, stay visible. Steps without `status` look the same as before.
@@ -1869,10 +1869,22 @@ window.Aura = (() => {
1869
1869
  var Select = React15.forwardRef(function Select2(props, ref) {
1870
1870
  const auto = uid(), id = props.id || auto;
1871
1871
  const filter = props[FILTER_KEY];
1872
- const rest = omit(props, FIELD_KEYS.concat(["children", FILTER_KEY]));
1872
+ const rest = omit(props, FIELD_KEYS.concat(["children", FILTER_KEY, "readOnly"]));
1873
+ const ro = !!props.readOnly && !props.disabled;
1874
+ const mounted = useMounted();
1875
+ const kept = React15.useRef(null);
1876
+ const current2 = [].concat(props.value != null ? props.value : props.defaultValue != null ? props.defaultValue : []).map(String);
1873
1877
  const opts = (props.options || []).map(function(o) {
1874
1878
  const v = typeof o === "object" ? o : { value: o, label: o };
1875
- return /* @__PURE__ */ React15.createElement("option", { key: v.value, value: v.value, disabled: v.disabled }, v.label);
1879
+ return /* @__PURE__ */ React15.createElement(
1880
+ "option",
1881
+ {
1882
+ key: v.value,
1883
+ value: v.value,
1884
+ disabled: v.disabled || ro && !mounted && current2.length > 0 && current2.indexOf(v.value) < 0 || void 0
1885
+ },
1886
+ v.label
1887
+ );
1876
1888
  });
1877
1889
  if (props.placeholder)
1878
1890
  opts.unshift(
@@ -1880,7 +1892,6 @@ window.Aura = (() => {
1880
1892
  );
1881
1893
  const extra = props.value === void 0 && props.defaultValue === void 0 && props.placeholder ? { defaultValue: "" } : {};
1882
1894
  const native = !!props.multiple || props.size != null && props.size > 1;
1883
- const mounted = useMounted();
1884
1895
  const live = !native && mounted;
1885
1896
  const density = useDensity();
1886
1897
  React15.useEffect(function() {
@@ -2002,7 +2013,7 @@ window.Aura = (() => {
2002
2013
  }
2003
2014
  function choose(it) {
2004
2015
  const el = selRef.current;
2005
- if (!el || it.disabled) return;
2016
+ if (!el || it.disabled || ro) return;
2006
2017
  if (el.value !== it.value) {
2007
2018
  el.value = it.value;
2008
2019
  el.dispatchEvent(new Event("input", { bubbles: true }));
@@ -2012,7 +2023,7 @@ window.Aura = (() => {
2012
2023
  close(true);
2013
2024
  }
2014
2025
  function openList(at) {
2015
- if (props.disabled || trigRef.current && trigRef.current.disabled) return;
2026
+ if (props.disabled || ro || trigRef.current && trigRef.current.disabled) return;
2016
2027
  const list = readItems(selRef.current);
2017
2028
  const ok = choices(list);
2018
2029
  if (!ok.length) return;
@@ -2143,6 +2154,45 @@ window.Aura = (() => {
2143
2154
  [open, active, pos[0] != null]
2144
2155
  );
2145
2156
  const described = [describedBy(id, props), rest["aria-describedby"]].filter(Boolean).join(" ") || void 0;
2157
+ function keep(e) {
2158
+ kept.current = Array.prototype.filter.call(e.currentTarget.options, function(o) {
2159
+ return o.selected;
2160
+ }).map(function(o) {
2161
+ return o.value;
2162
+ });
2163
+ }
2164
+ const listBoxLock = ro && native ? {
2165
+ onFocus: function(e) {
2166
+ const user = rest.onFocus;
2167
+ if (user) user(e);
2168
+ keep(e);
2169
+ },
2170
+ onPointerDown: function(e) {
2171
+ const user = rest.onPointerDown;
2172
+ if (user) user(e);
2173
+ keep(e);
2174
+ },
2175
+ onMouseDown: function(e) {
2176
+ const user = rest.onMouseDown;
2177
+ if (user) user(e);
2178
+ e.preventDefault();
2179
+ e.currentTarget.focus();
2180
+ },
2181
+ onKeyDown: function(e) {
2182
+ const user = rest.onKeyDown;
2183
+ if (user) user(e);
2184
+ const k = e.key;
2185
+ const picks = /^(Arrow(Up|Down|Left|Right)|Home|End|Page(Up|Down))$/.test(k) || k.length === 1 && !e.altKey && (!(e.ctrlKey || e.metaKey) || k === " " || k.toLowerCase() === "a");
2186
+ if (picks) e.preventDefault();
2187
+ },
2188
+ onChange: function(e) {
2189
+ const was = kept.current;
2190
+ if (!was) return;
2191
+ Array.prototype.forEach.call(e.currentTarget.options, function(o) {
2192
+ o.selected = was.indexOf(o.value) >= 0;
2193
+ });
2194
+ }
2195
+ } : {};
2146
2196
  const field = (child) => filter ? child : /* @__PURE__ */ React15.createElement(
2147
2197
  Field,
2148
2198
  {
@@ -2175,19 +2225,25 @@ window.Aura = (() => {
2175
2225
  ));
2176
2226
  if (!live)
2177
2227
  return field(
2178
- /* @__PURE__ */ React15.createElement("div", { className: cx("aura-input aura-select", props.icon && "has-icon") }, props.icon ? /* @__PURE__ */ React15.createElement(Icon, { name: props.icon, className: "aura-input__icon" }) : null, h4(
2228
+ /* @__PURE__ */ React15.createElement("div", { className: cx("aura-input aura-select", props.icon && "has-icon", ro && "is-readonly") }, props.icon ? /* @__PURE__ */ React15.createElement(Icon, { name: props.icon, className: "aura-input__icon" }) : null, h4(
2179
2229
  "select",
2180
- Object.assign(extra, rest, {
2181
- ref: native ? ref : selMerged,
2182
- id,
2183
- className: "aura-input__control",
2184
- required: props.required,
2185
- "aria-invalid": props.error ? true : void 0,
2186
- "aria-describedby": described
2187
- }),
2230
+ Object.assign(
2231
+ extra,
2232
+ rest,
2233
+ {
2234
+ ref: native ? ref : selMerged,
2235
+ id,
2236
+ className: "aura-input__control",
2237
+ required: props.required,
2238
+ "aria-invalid": props.error ? true : void 0,
2239
+ "aria-readonly": ro || void 0,
2240
+ "aria-describedby": described
2241
+ },
2242
+ listBoxLock
2243
+ ),
2188
2244
  opts,
2189
2245
  props.children
2190
- ), /* @__PURE__ */ React15.createElement(Icon, { name: /* @__PURE__ */ React15.createElement(IconChevronDown, null), className: "aura-select__chevron" }))
2246
+ ), ro ? null : /* @__PURE__ */ React15.createElement(Icon, { name: /* @__PURE__ */ React15.createElement(IconChevronDown, null), className: "aura-select__chevron" }))
2191
2247
  );
2192
2248
  const listId = id + "-list", labelId = id + "-label";
2193
2249
  const optId = function(i) {
@@ -2276,6 +2332,7 @@ window.Aura = (() => {
2276
2332
  filter ? "aura-filterselect" : "aura-input",
2277
2333
  "aura-select aura-select--custom",
2278
2334
  props.icon && "has-icon",
2335
+ ro && "is-readonly",
2279
2336
  open && "is-open",
2280
2337
  filter && props.disabled && "is-disabled",
2281
2338
  filter && props.className
@@ -2328,6 +2385,7 @@ window.Aura = (() => {
2328
2385
  "aria-labelledby": rest["aria-labelledby"] || (filter && !rest["aria-label"] ? nameId : void 0),
2329
2386
  "aria-required": props.required || void 0,
2330
2387
  "aria-invalid": props.error ? true : void 0,
2388
+ "aria-readonly": ro || void 0,
2331
2389
  "aria-describedby": described,
2332
2390
  onFocus: function(e) {
2333
2391
  if (userFocus) userFocus(e);
@@ -2350,7 +2408,7 @@ window.Aura = (() => {
2350
2408
  },
2351
2409
  face || /* @__PURE__ */ React15.createElement("span", { id: id + "-value", className: cx("aura-select__value", shown[0].empty && "is-placeholder") }, shown[0].label || " ")
2352
2410
  ),
2353
- filter ? null : /* @__PURE__ */ React15.createElement(Icon, { name: /* @__PURE__ */ React15.createElement(IconChevronDown, null), className: "aura-select__chevron" }),
2411
+ filter || ro ? null : /* @__PURE__ */ React15.createElement(Icon, { name: /* @__PURE__ */ React15.createElement(IconChevronDown, null), className: "aura-select__chevron" }),
2354
2412
  popup
2355
2413
  )
2356
2414
  );
@@ -2429,13 +2487,15 @@ window.Aura = (() => {
2429
2487
  const auto = uid(), id = props.id || auto;
2430
2488
  const st = useMaybeControlled(props.checked, !!props.defaultChecked, props.onChange);
2431
2489
  const on = !!st[0];
2490
+ const ro = !!props.readOnly && !props.disabled;
2491
+ const described = [props.description ? id + "-desc" : "", props["aria-describedby"] || ""].filter(Boolean).join(" ");
2432
2492
  return /* @__PURE__ */ React18.createElement(
2433
2493
  "div",
2434
2494
  {
2435
- className: cx("aura-switch-row", props.disabled && "is-disabled", props.className),
2495
+ className: cx("aura-switch-row", props.disabled && "is-disabled", ro && "is-readonly", props.className),
2436
2496
  onClick: function(e) {
2437
2497
  const t = e.target;
2438
- if (props.disabled || t.closest("button, label, a, input")) return;
2498
+ if (props.disabled || ro || t.closest("button, label, a, input")) return;
2439
2499
  st[1](!on);
2440
2500
  }
2441
2501
  },
@@ -2450,15 +2510,17 @@ window.Aura = (() => {
2450
2510
  disabled: props.disabled,
2451
2511
  "aria-labelledby": props.label ? id + "-label" : void 0,
2452
2512
  "aria-label": props.label ? void 0 : props["aria-label"],
2453
- "aria-describedby": props.description ? id + "-desc" : void 0,
2513
+ "aria-readonly": ro || void 0,
2514
+ "aria-describedby": described || void 0,
2454
2515
  className: cx("aura-switch", on && "is-on"),
2455
2516
  onClick: function() {
2456
- st[1](!on);
2517
+ if (!ro) st[1](!on);
2457
2518
  }
2458
2519
  },
2459
2520
  /* @__PURE__ */ React18.createElement("span", { className: "aura-switch__thumb" })
2460
2521
  ),
2461
- props.label ? /* @__PURE__ */ React18.createElement("span", { className: "aura-choice__text" }, /* @__PURE__ */ React18.createElement("label", { className: "aura-choice__label", id: id + "-label", htmlFor: id }, props.label), props.description ? /* @__PURE__ */ React18.createElement("span", { className: "aura-choice__desc", id: id + "-desc" }, props.description) : null) : props.description ? /* @__PURE__ */ React18.createElement("span", { className: "aura-sr-only", id: id + "-desc" }, props.description) : null
2522
+ props.label ? /* @__PURE__ */ React18.createElement("span", { className: "aura-choice__text" }, /* @__PURE__ */ React18.createElement("label", { className: "aura-choice__label", id: id + "-label", htmlFor: id }, props.label), props.description ? /* @__PURE__ */ React18.createElement("span", { className: "aura-choice__desc", id: id + "-desc" }, props.description) : null) : props.description ? /* @__PURE__ */ React18.createElement("span", { className: "aura-sr-only", id: id + "-desc" }, props.description) : null,
2523
+ props.icon ? /* @__PURE__ */ React18.createElement(Icon, { name: props.icon, className: "aura-switch-row__icon" }) : null
2462
2524
  );
2463
2525
  });
2464
2526
 
@@ -1851,10 +1851,22 @@ function initialLabel(props, filter) {
1851
1851
  var Select = React15.forwardRef(function Select2(props, ref) {
1852
1852
  const auto = uid(), id = props.id || auto;
1853
1853
  const filter = props[FILTER_KEY];
1854
- const rest = omit(props, FIELD_KEYS.concat(["children", FILTER_KEY]));
1854
+ const rest = omit(props, FIELD_KEYS.concat(["children", FILTER_KEY, "readOnly"]));
1855
+ const ro = !!props.readOnly && !props.disabled;
1856
+ const mounted = useMounted();
1857
+ const kept = React15.useRef(null);
1858
+ const current2 = [].concat(props.value != null ? props.value : props.defaultValue != null ? props.defaultValue : []).map(String);
1855
1859
  const opts = (props.options || []).map(function(o) {
1856
1860
  const v = typeof o === "object" ? o : { value: o, label: o };
1857
- return /* @__PURE__ */ React15.createElement("option", { key: v.value, value: v.value, disabled: v.disabled }, v.label);
1861
+ return /* @__PURE__ */ React15.createElement(
1862
+ "option",
1863
+ {
1864
+ key: v.value,
1865
+ value: v.value,
1866
+ disabled: v.disabled || ro && !mounted && current2.length > 0 && current2.indexOf(v.value) < 0 || void 0
1867
+ },
1868
+ v.label
1869
+ );
1858
1870
  });
1859
1871
  if (props.placeholder)
1860
1872
  opts.unshift(
@@ -1862,7 +1874,6 @@ var Select = React15.forwardRef(function Select2(props, ref) {
1862
1874
  );
1863
1875
  const extra = props.value === void 0 && props.defaultValue === void 0 && props.placeholder ? { defaultValue: "" } : {};
1864
1876
  const native = !!props.multiple || props.size != null && props.size > 1;
1865
- const mounted = useMounted();
1866
1877
  const live = !native && mounted;
1867
1878
  const density = useDensity();
1868
1879
  React15.useEffect(function() {
@@ -1984,7 +1995,7 @@ var Select = React15.forwardRef(function Select2(props, ref) {
1984
1995
  }
1985
1996
  function choose(it) {
1986
1997
  const el = selRef.current;
1987
- if (!el || it.disabled) return;
1998
+ if (!el || it.disabled || ro) return;
1988
1999
  if (el.value !== it.value) {
1989
2000
  el.value = it.value;
1990
2001
  el.dispatchEvent(new Event("input", { bubbles: true }));
@@ -1994,7 +2005,7 @@ var Select = React15.forwardRef(function Select2(props, ref) {
1994
2005
  close(true);
1995
2006
  }
1996
2007
  function openList(at) {
1997
- if (props.disabled || trigRef.current && trigRef.current.disabled) return;
2008
+ if (props.disabled || ro || trigRef.current && trigRef.current.disabled) return;
1998
2009
  const list = readItems(selRef.current);
1999
2010
  const ok = choices(list);
2000
2011
  if (!ok.length) return;
@@ -2125,6 +2136,45 @@ var Select = React15.forwardRef(function Select2(props, ref) {
2125
2136
  [open, active, pos[0] != null]
2126
2137
  );
2127
2138
  const described = [describedBy(id, props), rest["aria-describedby"]].filter(Boolean).join(" ") || void 0;
2139
+ function keep(e) {
2140
+ kept.current = Array.prototype.filter.call(e.currentTarget.options, function(o) {
2141
+ return o.selected;
2142
+ }).map(function(o) {
2143
+ return o.value;
2144
+ });
2145
+ }
2146
+ const listBoxLock = ro && native ? {
2147
+ onFocus: function(e) {
2148
+ const user = rest.onFocus;
2149
+ if (user) user(e);
2150
+ keep(e);
2151
+ },
2152
+ onPointerDown: function(e) {
2153
+ const user = rest.onPointerDown;
2154
+ if (user) user(e);
2155
+ keep(e);
2156
+ },
2157
+ onMouseDown: function(e) {
2158
+ const user = rest.onMouseDown;
2159
+ if (user) user(e);
2160
+ e.preventDefault();
2161
+ e.currentTarget.focus();
2162
+ },
2163
+ onKeyDown: function(e) {
2164
+ const user = rest.onKeyDown;
2165
+ if (user) user(e);
2166
+ const k = e.key;
2167
+ const picks = /^(Arrow(Up|Down|Left|Right)|Home|End|Page(Up|Down))$/.test(k) || k.length === 1 && !e.altKey && (!(e.ctrlKey || e.metaKey) || k === " " || k.toLowerCase() === "a");
2168
+ if (picks) e.preventDefault();
2169
+ },
2170
+ onChange: function(e) {
2171
+ const was = kept.current;
2172
+ if (!was) return;
2173
+ Array.prototype.forEach.call(e.currentTarget.options, function(o) {
2174
+ o.selected = was.indexOf(o.value) >= 0;
2175
+ });
2176
+ }
2177
+ } : {};
2128
2178
  const field = (child) => filter ? child : /* @__PURE__ */ React15.createElement(
2129
2179
  Field,
2130
2180
  {
@@ -2157,19 +2207,25 @@ var Select = React15.forwardRef(function Select2(props, ref) {
2157
2207
  ));
2158
2208
  if (!live)
2159
2209
  return field(
2160
- /* @__PURE__ */ React15.createElement("div", { className: cx("aura-input aura-select", props.icon && "has-icon") }, props.icon ? /* @__PURE__ */ React15.createElement(Icon, { name: props.icon, className: "aura-input__icon" }) : null, h4(
2210
+ /* @__PURE__ */ React15.createElement("div", { className: cx("aura-input aura-select", props.icon && "has-icon", ro && "is-readonly") }, props.icon ? /* @__PURE__ */ React15.createElement(Icon, { name: props.icon, className: "aura-input__icon" }) : null, h4(
2161
2211
  "select",
2162
- Object.assign(extra, rest, {
2163
- ref: native ? ref : selMerged,
2164
- id,
2165
- className: "aura-input__control",
2166
- required: props.required,
2167
- "aria-invalid": props.error ? true : void 0,
2168
- "aria-describedby": described
2169
- }),
2212
+ Object.assign(
2213
+ extra,
2214
+ rest,
2215
+ {
2216
+ ref: native ? ref : selMerged,
2217
+ id,
2218
+ className: "aura-input__control",
2219
+ required: props.required,
2220
+ "aria-invalid": props.error ? true : void 0,
2221
+ "aria-readonly": ro || void 0,
2222
+ "aria-describedby": described
2223
+ },
2224
+ listBoxLock
2225
+ ),
2170
2226
  opts,
2171
2227
  props.children
2172
- ), /* @__PURE__ */ React15.createElement(Icon, { name: /* @__PURE__ */ React15.createElement(IconChevronDown, null), className: "aura-select__chevron" }))
2228
+ ), ro ? null : /* @__PURE__ */ React15.createElement(Icon, { name: /* @__PURE__ */ React15.createElement(IconChevronDown, null), className: "aura-select__chevron" }))
2173
2229
  );
2174
2230
  const listId = id + "-list", labelId = id + "-label";
2175
2231
  const optId = function(i) {
@@ -2258,6 +2314,7 @@ var Select = React15.forwardRef(function Select2(props, ref) {
2258
2314
  filter ? "aura-filterselect" : "aura-input",
2259
2315
  "aura-select aura-select--custom",
2260
2316
  props.icon && "has-icon",
2317
+ ro && "is-readonly",
2261
2318
  open && "is-open",
2262
2319
  filter && props.disabled && "is-disabled",
2263
2320
  filter && props.className
@@ -2310,6 +2367,7 @@ var Select = React15.forwardRef(function Select2(props, ref) {
2310
2367
  "aria-labelledby": rest["aria-labelledby"] || (filter && !rest["aria-label"] ? nameId : void 0),
2311
2368
  "aria-required": props.required || void 0,
2312
2369
  "aria-invalid": props.error ? true : void 0,
2370
+ "aria-readonly": ro || void 0,
2313
2371
  "aria-describedby": described,
2314
2372
  onFocus: function(e) {
2315
2373
  if (userFocus) userFocus(e);
@@ -2332,7 +2390,7 @@ var Select = React15.forwardRef(function Select2(props, ref) {
2332
2390
  },
2333
2391
  face || /* @__PURE__ */ React15.createElement("span", { id: id + "-value", className: cx("aura-select__value", shown[0].empty && "is-placeholder") }, shown[0].label || " ")
2334
2392
  ),
2335
- filter ? null : /* @__PURE__ */ React15.createElement(Icon, { name: /* @__PURE__ */ React15.createElement(IconChevronDown, null), className: "aura-select__chevron" }),
2393
+ filter || ro ? null : /* @__PURE__ */ React15.createElement(Icon, { name: /* @__PURE__ */ React15.createElement(IconChevronDown, null), className: "aura-select__chevron" }),
2336
2394
  popup
2337
2395
  )
2338
2396
  );
@@ -2411,13 +2469,15 @@ var Switch = React18.forwardRef(function Switch2(props, ref) {
2411
2469
  const auto = uid(), id = props.id || auto;
2412
2470
  const st = useMaybeControlled(props.checked, !!props.defaultChecked, props.onChange);
2413
2471
  const on = !!st[0];
2472
+ const ro = !!props.readOnly && !props.disabled;
2473
+ const described = [props.description ? id + "-desc" : "", props["aria-describedby"] || ""].filter(Boolean).join(" ");
2414
2474
  return /* @__PURE__ */ React18.createElement(
2415
2475
  "div",
2416
2476
  {
2417
- className: cx("aura-switch-row", props.disabled && "is-disabled", props.className),
2477
+ className: cx("aura-switch-row", props.disabled && "is-disabled", ro && "is-readonly", props.className),
2418
2478
  onClick: function(e) {
2419
2479
  const t = e.target;
2420
- if (props.disabled || t.closest("button, label, a, input")) return;
2480
+ if (props.disabled || ro || t.closest("button, label, a, input")) return;
2421
2481
  st[1](!on);
2422
2482
  }
2423
2483
  },
@@ -2432,15 +2492,17 @@ var Switch = React18.forwardRef(function Switch2(props, ref) {
2432
2492
  disabled: props.disabled,
2433
2493
  "aria-labelledby": props.label ? id + "-label" : void 0,
2434
2494
  "aria-label": props.label ? void 0 : props["aria-label"],
2435
- "aria-describedby": props.description ? id + "-desc" : void 0,
2495
+ "aria-readonly": ro || void 0,
2496
+ "aria-describedby": described || void 0,
2436
2497
  className: cx("aura-switch", on && "is-on"),
2437
2498
  onClick: function() {
2438
- st[1](!on);
2499
+ if (!ro) st[1](!on);
2439
2500
  }
2440
2501
  },
2441
2502
  /* @__PURE__ */ React18.createElement("span", { className: "aura-switch__thumb" })
2442
2503
  ),
2443
- props.label ? /* @__PURE__ */ React18.createElement("span", { className: "aura-choice__text" }, /* @__PURE__ */ React18.createElement("label", { className: "aura-choice__label", id: id + "-label", htmlFor: id }, props.label), props.description ? /* @__PURE__ */ React18.createElement("span", { className: "aura-choice__desc", id: id + "-desc" }, props.description) : null) : props.description ? /* @__PURE__ */ React18.createElement("span", { className: "aura-sr-only", id: id + "-desc" }, props.description) : null
2504
+ props.label ? /* @__PURE__ */ React18.createElement("span", { className: "aura-choice__text" }, /* @__PURE__ */ React18.createElement("label", { className: "aura-choice__label", id: id + "-label", htmlFor: id }, props.label), props.description ? /* @__PURE__ */ React18.createElement("span", { className: "aura-choice__desc", id: id + "-desc" }, props.description) : null) : props.description ? /* @__PURE__ */ React18.createElement("span", { className: "aura-sr-only", id: id + "-desc" }, props.description) : null,
2505
+ props.icon ? /* @__PURE__ */ React18.createElement(Icon, { name: props.icon, className: "aura-switch-row__icon" }) : null
2444
2506
  );
2445
2507
  });
2446
2508
 
@@ -83,10 +83,22 @@ function initialLabel(props, filter) {
83
83
  const Select = React.forwardRef(function Select2(props, ref) {
84
84
  const auto = uid(), id = props.id || auto;
85
85
  const filter = props[FILTER_KEY];
86
- const rest = omit(props, FIELD_KEYS.concat(["children", FILTER_KEY]));
86
+ const rest = omit(props, FIELD_KEYS.concat(["children", FILTER_KEY, "readOnly"]));
87
+ const ro = !!props.readOnly && !props.disabled;
88
+ const mounted = useMounted();
89
+ const kept = React.useRef(null);
90
+ const current = [].concat(props.value != null ? props.value : props.defaultValue != null ? props.defaultValue : []).map(String);
87
91
  const opts = (props.options || []).map(function(o) {
88
92
  const v = typeof o === "object" ? o : { value: o, label: o };
89
- return /* @__PURE__ */ React.createElement("option", { key: v.value, value: v.value, disabled: v.disabled }, v.label);
93
+ return /* @__PURE__ */ React.createElement(
94
+ "option",
95
+ {
96
+ key: v.value,
97
+ value: v.value,
98
+ disabled: v.disabled || ro && !mounted && current.length > 0 && current.indexOf(v.value) < 0 || void 0
99
+ },
100
+ v.label
101
+ );
90
102
  });
91
103
  if (props.placeholder)
92
104
  opts.unshift(
@@ -94,7 +106,6 @@ const Select = React.forwardRef(function Select2(props, ref) {
94
106
  );
95
107
  const extra = props.value === void 0 && props.defaultValue === void 0 && props.placeholder ? { defaultValue: "" } : {};
96
108
  const native = !!props.multiple || props.size != null && props.size > 1;
97
- const mounted = useMounted();
98
109
  const live = !native && mounted;
99
110
  const density = useDensity();
100
111
  React.useEffect(function() {
@@ -216,7 +227,7 @@ const Select = React.forwardRef(function Select2(props, ref) {
216
227
  }
217
228
  function choose(it) {
218
229
  const el = selRef.current;
219
- if (!el || it.disabled) return;
230
+ if (!el || it.disabled || ro) return;
220
231
  if (el.value !== it.value) {
221
232
  el.value = it.value;
222
233
  el.dispatchEvent(new Event("input", { bubbles: true }));
@@ -226,7 +237,7 @@ const Select = React.forwardRef(function Select2(props, ref) {
226
237
  close(true);
227
238
  }
228
239
  function openList(at) {
229
- if (props.disabled || trigRef.current && trigRef.current.disabled) return;
240
+ if (props.disabled || ro || trigRef.current && trigRef.current.disabled) return;
230
241
  const list = readItems(selRef.current);
231
242
  const ok = choices(list);
232
243
  if (!ok.length) return;
@@ -357,6 +368,45 @@ const Select = React.forwardRef(function Select2(props, ref) {
357
368
  [open, active, pos[0] != null]
358
369
  );
359
370
  const described = [describedBy(id, props), rest["aria-describedby"]].filter(Boolean).join(" ") || void 0;
371
+ function keep(e) {
372
+ kept.current = Array.prototype.filter.call(e.currentTarget.options, function(o) {
373
+ return o.selected;
374
+ }).map(function(o) {
375
+ return o.value;
376
+ });
377
+ }
378
+ const listBoxLock = ro && native ? {
379
+ onFocus: function(e) {
380
+ const user = rest.onFocus;
381
+ if (user) user(e);
382
+ keep(e);
383
+ },
384
+ onPointerDown: function(e) {
385
+ const user = rest.onPointerDown;
386
+ if (user) user(e);
387
+ keep(e);
388
+ },
389
+ onMouseDown: function(e) {
390
+ const user = rest.onMouseDown;
391
+ if (user) user(e);
392
+ e.preventDefault();
393
+ e.currentTarget.focus();
394
+ },
395
+ onKeyDown: function(e) {
396
+ const user = rest.onKeyDown;
397
+ if (user) user(e);
398
+ const k = e.key;
399
+ const picks = /^(Arrow(Up|Down|Left|Right)|Home|End|Page(Up|Down))$/.test(k) || k.length === 1 && !e.altKey && (!(e.ctrlKey || e.metaKey) || k === " " || k.toLowerCase() === "a");
400
+ if (picks) e.preventDefault();
401
+ },
402
+ onChange: function(e) {
403
+ const was = kept.current;
404
+ if (!was) return;
405
+ Array.prototype.forEach.call(e.currentTarget.options, function(o) {
406
+ o.selected = was.indexOf(o.value) >= 0;
407
+ });
408
+ }
409
+ } : {};
360
410
  const field = (child) => filter ? child : /* @__PURE__ */ React.createElement(
361
411
  Field,
362
412
  {
@@ -389,19 +439,25 @@ const Select = React.forwardRef(function Select2(props, ref) {
389
439
  ));
390
440
  if (!live)
391
441
  return field(
392
- /* @__PURE__ */ React.createElement("div", { className: cx("aura-input aura-select", props.icon && "has-icon") }, props.icon ? /* @__PURE__ */ React.createElement(Icon, { name: props.icon, className: "aura-input__icon" }) : null, h(
442
+ /* @__PURE__ */ React.createElement("div", { className: cx("aura-input aura-select", props.icon && "has-icon", ro && "is-readonly") }, props.icon ? /* @__PURE__ */ React.createElement(Icon, { name: props.icon, className: "aura-input__icon" }) : null, h(
393
443
  "select",
394
- Object.assign(extra, rest, {
395
- ref: native ? ref : selMerged,
396
- id,
397
- className: "aura-input__control",
398
- required: props.required,
399
- "aria-invalid": props.error ? true : void 0,
400
- "aria-describedby": described
401
- }),
444
+ Object.assign(
445
+ extra,
446
+ rest,
447
+ {
448
+ ref: native ? ref : selMerged,
449
+ id,
450
+ className: "aura-input__control",
451
+ required: props.required,
452
+ "aria-invalid": props.error ? true : void 0,
453
+ "aria-readonly": ro || void 0,
454
+ "aria-describedby": described
455
+ },
456
+ listBoxLock
457
+ ),
402
458
  opts,
403
459
  props.children
404
- ), /* @__PURE__ */ React.createElement(Icon, { name: /* @__PURE__ */ React.createElement(IconChevronDown, null), className: "aura-select__chevron" }))
460
+ ), ro ? null : /* @__PURE__ */ React.createElement(Icon, { name: /* @__PURE__ */ React.createElement(IconChevronDown, null), className: "aura-select__chevron" }))
405
461
  );
406
462
  const listId = id + "-list", labelId = id + "-label";
407
463
  const optId = function(i) {
@@ -490,6 +546,7 @@ const Select = React.forwardRef(function Select2(props, ref) {
490
546
  filter ? "aura-filterselect" : "aura-input",
491
547
  "aura-select aura-select--custom",
492
548
  props.icon && "has-icon",
549
+ ro && "is-readonly",
493
550
  open && "is-open",
494
551
  filter && props.disabled && "is-disabled",
495
552
  filter && props.className
@@ -542,6 +599,7 @@ const Select = React.forwardRef(function Select2(props, ref) {
542
599
  "aria-labelledby": rest["aria-labelledby"] || (filter && !rest["aria-label"] ? nameId : void 0),
543
600
  "aria-required": props.required || void 0,
544
601
  "aria-invalid": props.error ? true : void 0,
602
+ "aria-readonly": ro || void 0,
545
603
  "aria-describedby": described,
546
604
  onFocus: function(e) {
547
605
  if (userFocus) userFocus(e);
@@ -564,7 +622,7 @@ const Select = React.forwardRef(function Select2(props, ref) {
564
622
  },
565
623
  face || /* @__PURE__ */ React.createElement("span", { id: id + "-value", className: cx("aura-select__value", shown[0].empty && "is-placeholder") }, shown[0].label || " ")
566
624
  ),
567
- filter ? null : /* @__PURE__ */ React.createElement(Icon, { name: /* @__PURE__ */ React.createElement(IconChevronDown, null), className: "aura-select__chevron" }),
625
+ filter || ro ? null : /* @__PURE__ */ React.createElement(Icon, { name: /* @__PURE__ */ React.createElement(IconChevronDown, null), className: "aura-select__chevron" }),
568
626
  popup
569
627
  )
570
628
  );
@@ -1,17 +1,20 @@
1
1
  'use client';
2
2
  import * as React from "react";
3
+ import { Icon } from "./Icon.js";
3
4
  import { cx, uid, useMaybeControlled } from "./internal.js";
4
5
  const Switch = React.forwardRef(function Switch2(props, ref) {
5
6
  const auto = uid(), id = props.id || auto;
6
7
  const st = useMaybeControlled(props.checked, !!props.defaultChecked, props.onChange);
7
8
  const on = !!st[0];
9
+ const ro = !!props.readOnly && !props.disabled;
10
+ const described = [props.description ? id + "-desc" : "", props["aria-describedby"] || ""].filter(Boolean).join(" ");
8
11
  return /* @__PURE__ */ React.createElement(
9
12
  "div",
10
13
  {
11
- className: cx("aura-switch-row", props.disabled && "is-disabled", props.className),
14
+ className: cx("aura-switch-row", props.disabled && "is-disabled", ro && "is-readonly", props.className),
12
15
  onClick: function(e) {
13
16
  const t = e.target;
14
- if (props.disabled || t.closest("button, label, a, input")) return;
17
+ if (props.disabled || ro || t.closest("button, label, a, input")) return;
15
18
  st[1](!on);
16
19
  }
17
20
  },
@@ -26,15 +29,17 @@ const Switch = React.forwardRef(function Switch2(props, ref) {
26
29
  disabled: props.disabled,
27
30
  "aria-labelledby": props.label ? id + "-label" : void 0,
28
31
  "aria-label": props.label ? void 0 : props["aria-label"],
29
- "aria-describedby": props.description ? id + "-desc" : void 0,
32
+ "aria-readonly": ro || void 0,
33
+ "aria-describedby": described || void 0,
30
34
  className: cx("aura-switch", on && "is-on"),
31
35
  onClick: function() {
32
- st[1](!on);
36
+ if (!ro) st[1](!on);
33
37
  }
34
38
  },
35
39
  /* @__PURE__ */ React.createElement("span", { className: "aura-switch__thumb" })
36
40
  ),
37
- props.label ? /* @__PURE__ */ React.createElement("span", { className: "aura-choice__text" }, /* @__PURE__ */ React.createElement("label", { className: "aura-choice__label", id: id + "-label", htmlFor: id }, props.label), props.description ? /* @__PURE__ */ React.createElement("span", { className: "aura-choice__desc", id: id + "-desc" }, props.description) : null) : props.description ? /* @__PURE__ */ React.createElement("span", { className: "aura-sr-only", id: id + "-desc" }, props.description) : null
41
+ props.label ? /* @__PURE__ */ React.createElement("span", { className: "aura-choice__text" }, /* @__PURE__ */ React.createElement("label", { className: "aura-choice__label", id: id + "-label", htmlFor: id }, props.label), props.description ? /* @__PURE__ */ React.createElement("span", { className: "aura-choice__desc", id: id + "-desc" }, props.description) : null) : props.description ? /* @__PURE__ */ React.createElement("span", { className: "aura-sr-only", id: id + "-desc" }, props.description) : null,
42
+ props.icon ? /* @__PURE__ */ React.createElement(Icon, { name: props.icon, className: "aura-switch-row__icon" }) : null
38
43
  );
39
44
  });
40
45
  export {
package/dist/index.d.cts CHANGED
@@ -517,6 +517,10 @@ export interface SelectProps extends Omit<FieldProps, "label">, Omit<React$1.Sel
517
517
  /** Shown in fg-tertiary on the closed field until something is chosen; not listed as a choice. */
518
518
  placeholder?: string | undefined;
519
519
  icon?: IconInput | undefined;
520
+ /** 5.19 (Chamber-OS 114): a locked value. The field stays in the Tab order with `aria-readonly` and shows the chosen
521
+ * option on the read-only ground, with no chevron; no click or key opens the list. The value is still submitted
522
+ * with the form (unlike `disabled`). Before JavaScript runs, the other options from `options` are disabled. */
523
+ readOnly?: boolean | undefined;
520
524
  }
521
525
  /** A compact filter for FilterBar (5.12): "Status All ▾", as wide as its words. */
522
526
  export interface FilterSelectProps extends Omit<React$1.SelectHTMLAttributes<HTMLSelectElement>, "onChange" | "value" | "defaultValue" | "multiple" | "size" | "placeholder" | "required" | "children"> {
@@ -557,8 +561,16 @@ export interface SwitchProps {
557
561
  defaultChecked?: boolean | undefined;
558
562
  onChange?: ((checked: boolean) => void) | undefined;
559
563
  disabled?: boolean | undefined;
564
+ /** 5.19 (Chamber-OS 113): a locked value. The switch stays in the Tab order with `aria-readonly`, so its name,
565
+ * state and description are heard; click, Space and the row ignore it. Its colours stay (on reads as on). */
566
+ readOnly?: boolean | undefined;
567
+ /** 5.19: an icon at the end of the row, in fg-secondary and the field icon's size — `icon={<IconLock />}` or
568
+ * `icon="lock"` on a locked row. Hidden from screen readers; say what it means in the description. */
569
+ icon?: IconInput | undefined;
560
570
  /** Required when there is no visible label. */
561
571
  "aria-label"?: string | undefined;
572
+ /** 5.19: ids of more text that describes the switch (a lock note); merged with `description`'s. */
573
+ "aria-describedby"?: string | undefined;
562
574
  id?: string | undefined;
563
575
  className?: string | undefined;
564
576
  }
package/dist/index.d.ts CHANGED
@@ -517,6 +517,10 @@ export interface SelectProps extends Omit<FieldProps, "label">, Omit<React$1.Sel
517
517
  /** Shown in fg-tertiary on the closed field until something is chosen; not listed as a choice. */
518
518
  placeholder?: string | undefined;
519
519
  icon?: IconInput | undefined;
520
+ /** 5.19 (Chamber-OS 114): a locked value. The field stays in the Tab order with `aria-readonly` and shows the chosen
521
+ * option on the read-only ground, with no chevron; no click or key opens the list. The value is still submitted
522
+ * with the form (unlike `disabled`). Before JavaScript runs, the other options from `options` are disabled. */
523
+ readOnly?: boolean | undefined;
520
524
  }
521
525
  /** A compact filter for FilterBar (5.12): "Status All ▾", as wide as its words. */
522
526
  export interface FilterSelectProps extends Omit<React$1.SelectHTMLAttributes<HTMLSelectElement>, "onChange" | "value" | "defaultValue" | "multiple" | "size" | "placeholder" | "required" | "children"> {
@@ -557,8 +561,16 @@ export interface SwitchProps {
557
561
  defaultChecked?: boolean | undefined;
558
562
  onChange?: ((checked: boolean) => void) | undefined;
559
563
  disabled?: boolean | undefined;
564
+ /** 5.19 (Chamber-OS 113): a locked value. The switch stays in the Tab order with `aria-readonly`, so its name,
565
+ * state and description are heard; click, Space and the row ignore it. Its colours stay (on reads as on). */
566
+ readOnly?: boolean | undefined;
567
+ /** 5.19: an icon at the end of the row, in fg-secondary and the field icon's size — `icon={<IconLock />}` or
568
+ * `icon="lock"` on a locked row. Hidden from screen readers; say what it means in the description. */
569
+ icon?: IconInput | undefined;
560
570
  /** Required when there is no visible label. */
561
571
  "aria-label"?: string | undefined;
572
+ /** 5.19: ids of more text that describes the switch (a lock note); merged with `description`'s. */
573
+ "aria-describedby"?: string | undefined;
562
574
  id?: string | undefined;
563
575
  className?: string | undefined;
564
576
  }
package/dist/styles.css CHANGED
@@ -487,6 +487,10 @@ a.aura-icon-btn { text-decoration: none; }
487
487
  .aura-select__value.is-placeholder { color: var(--aura-fg-tertiary); }
488
488
  .aura-select__group { padding: var(--aura-space-2) var(--aura-space-2) var(--aura-space-1); font-family: var(--font-mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--aura-fg-tertiary); }
489
489
  .aura-select.is-open .aura-select__chevron { transform: rotate(180deg); }
490
+ /* 5.19 (Chamber-OS 114): a read-only Select looks like a read-only TextField — the same ground, no chevron (it doesn't
491
+ * open), no pointer; the value keeps full contrast. */
492
+ .aura-select.is-readonly { background: var(--aura-bg-input-disabled); }
493
+ .aura-select.is-readonly .aura-input__control { cursor: default; padding-right: var(--aura-space-3); }
490
494
 
491
495
  .aura-radio-group > legend { margin-bottom: 6px; }
492
496
  .aura-radio-group__list { display: flex; flex-direction: column; gap: var(--aura-space-3); }
@@ -528,6 +532,10 @@ a.aura-icon-btn { text-decoration: none; }
528
532
  .aura-switch.is-on .aura-switch__thumb { transform: translateX(16px); background: var(--aura-button-primary-fg); }
529
533
  .aura-switch:focus-visible { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: var(--aura-focus-ring-offset); }
530
534
  .aura-switch:disabled { cursor: not-allowed; }
535
+ /* 5.19 (Chamber-OS 113): a read-only switch keeps its colours (a locked "on" reads as on at full contrast) and stays
536
+ * focusable; only the pointer says it won't change. The row's icon (a lock) sits at its end, like a field's icon. */
537
+ .aura-switch-row.is-readonly, .aura-switch-row.is-readonly .aura-switch, .aura-switch-row.is-readonly .aura-choice__label { cursor: default; }
538
+ .aura-switch-row__icon { flex: none; margin-inline-start: auto; margin-top: 2px; width: var(--aura-icon-sm); height: var(--aura-icon-sm); color: var(--aura-fg-secondary); }
531
539
 
532
540
  /* ---------- Alerts & toasts ---------- */
533
541
  .aura-alert {
@@ -1414,7 +1422,7 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
1414
1422
  padding-block: calc((var(--aura-touch-target) - 20px) / 2);
1415
1423
  }
1416
1424
  .aura-check--labelled .aura-check__input { top: calc((var(--aura-touch-target) - 20px) / 2 + 3px); }
1417
- .aura-switch-row:not(.is-disabled) { cursor: pointer; }
1425
+ .aura-switch-row:not(.is-disabled):not(.is-readonly) { cursor: pointer; }
1418
1426
  .aura-radio-group__list { gap: 0; }
1419
1427
  .aura-radio-group__list.is-horizontal { gap: 0 var(--aura-space-6); }
1420
1428
  }
@@ -489,6 +489,10 @@ a.aura-icon-btn { text-decoration: none; }
489
489
  .aura-select__value.is-placeholder { color: var(--aura-fg-tertiary); }
490
490
  .aura-select__group { padding: var(--aura-space-2) var(--aura-space-2) var(--aura-space-1); font-family: var(--font-mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--aura-fg-tertiary); }
491
491
  .aura-select.is-open .aura-select__chevron { transform: rotate(180deg); }
492
+ /* 5.19 (Chamber-OS 114): a read-only Select looks like a read-only TextField — the same ground, no chevron (it doesn't
493
+ * open), no pointer; the value keeps full contrast. */
494
+ .aura-select.is-readonly { background: var(--aura-bg-input-disabled); }
495
+ .aura-select.is-readonly .aura-input__control { cursor: default; padding-right: var(--aura-space-3); }
492
496
 
493
497
  .aura-radio-group > legend { margin-bottom: 6px; }
494
498
  .aura-radio-group__list { display: flex; flex-direction: column; gap: var(--aura-space-3); }
@@ -530,6 +534,10 @@ a.aura-icon-btn { text-decoration: none; }
530
534
  .aura-switch.is-on .aura-switch__thumb { transform: translateX(16px); background: var(--aura-button-primary-fg); }
531
535
  .aura-switch:focus-visible { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: var(--aura-focus-ring-offset); }
532
536
  .aura-switch:disabled { cursor: not-allowed; }
537
+ /* 5.19 (Chamber-OS 113): a read-only switch keeps its colours (a locked "on" reads as on at full contrast) and stays
538
+ * focusable; only the pointer says it won't change. The row's icon (a lock) sits at its end, like a field's icon. */
539
+ .aura-switch-row.is-readonly, .aura-switch-row.is-readonly .aura-switch, .aura-switch-row.is-readonly .aura-choice__label { cursor: default; }
540
+ .aura-switch-row__icon { flex: none; margin-inline-start: auto; margin-top: 2px; width: var(--aura-icon-sm); height: var(--aura-icon-sm); color: var(--aura-fg-secondary); }
533
541
 
534
542
  /* ---------- Alerts & toasts ---------- */
535
543
  .aura-alert {
@@ -1416,7 +1424,7 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
1416
1424
  padding-block: calc((var(--aura-touch-target) - 20px) / 2);
1417
1425
  }
1418
1426
  .aura-check--labelled .aura-check__input { top: calc((var(--aura-touch-target) - 20px) / 2 + 3px); }
1419
- .aura-switch-row:not(.is-disabled) { cursor: pointer; }
1427
+ .aura-switch-row:not(.is-disabled):not(.is-readonly) { cursor: pointer; }
1420
1428
  .aura-radio-group__list { gap: 0; }
1421
1429
  .aura-radio-group__list.is-horizontal { gap: 0 var(--aura-space-6); }
1422
1430
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jirawatpyk/aura-react",
3
- "version": "5.18.0",
3
+ "version": "5.19.0",
4
4
  "description": "AURA Design System — React components (Enterprise Standard, Human Creative). Thai-first: Buddhist-era dates, Thai collation.",
5
5
  "license": "MIT",
6
6
  "type": "module",