@jirawatpyk/aura-react 5.9.0 → 5.10.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,15 @@ 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.10 — Chamber-OS addendum 10 (adopting 5.9)
190
+
191
+ - **Tabs `variant="segmented"`** (item 72): the tab list as a pill track with the selected tab raised — SegmentedControl's look — keeping every Tabs behaviour (tab / tabpanel roles, `keepMounted`, `activation`, `tabProps`). `fullWidth` stretches the track and shares it equally. Tabs are 44px on touch screens, the list doesn't scroll so the focus ring is never clipped, and in forced colours the selected tab gets a `Highlight` outline. Default (`underline`) output is unchanged.
192
+ ```tsx
193
+ <Tabs label="Payment method" variant="segmented" fullWidth keepMounted activation="manual" … />
194
+ ```
195
+ - **Disabled menu items stay reachable** (item 73): a disabled `MenuItem` is `aria-disabled` rather than a disabled button, so the arrow keys reach it and screen readers announce it as dimmed (WAI-ARIA APG); choosing it does nothing and keeps the menu open. `disabledReason` shows in place of the hint and is read with the item ("Again in 5 min"). A menu with nothing to focus takes focus itself, so Escape and Tab still close it and focus returns to the trigger. Enabled items are unchanged; tests that expected the arrows to skip a disabled item now land on it.
196
+ - **SegmentedControl boundary** (item 74): measured again — the selected pill's inset edge (`aura-border-control`, zinc-500) is 4.6:1 against the light track (zinc-50) and 5.8:1 in dark; CI now measures the rendered edge in both themes (≥3:1). A value near 2.5:1 is zinc-400, dark mode's `aura-border-control`: check that the light page isn't picking up the dark tokens (a `.dark` / `data-theme="dark"` ancestor, or a theme override).
197
+
189
198
  ## 5.9 — Chamber-OS addendum 9 (pay sheet)
190
199
 
191
200
  - **Drawer** (item 70): other attributes (`id`, `data-*`, `aria-*`) go on the `.aura-drawer` panel; `closeLabel` names the close button and its tooltip after what it closes ("Close payment drawer" — pass it in the page's language); `closeProps` puts attributes on that button (`{ 'data-testid': 'pay-sheet-close' }`; its `onClick` runs first and can `preventDefault()` to keep the drawer open). Without them the output is as before; focus trap, Escape, scrim and focus return are unchanged.
@@ -1233,13 +1233,14 @@ window.Aura = (() => {
1233
1233
  // src/Menu.tsx
1234
1234
  var React8 = __toESM(require_react(), 1);
1235
1235
  var import_react_dom = __toESM(require_react_dom(), 1);
1236
+ var ITEMS = '[role^="menuitem"]';
1236
1237
  var Menu = React8.forwardRef(function Menu2(props, ref) {
1237
1238
  const own = React8.useRef(null), merged = useMergedRef(ref, own);
1238
1239
  const posState = React8.useState(null);
1239
1240
  const pos = posState[0], setPos = posState[1];
1240
1241
  const items2 = props.items || [];
1241
1242
  const mounted = useMounted();
1242
- const headerId = uid();
1243
+ const headerId = uid(), itemId = uid();
1243
1244
  const hasHeader = props.header != null && props.header !== false && props.header !== "";
1244
1245
  const Link = useLinkComponent(props.linkComponent);
1245
1246
  useIsoLayoutEffect(
@@ -1267,8 +1268,8 @@ window.Aura = (() => {
1267
1268
  hadHeader.current = hasHeader;
1268
1269
  const a = document.activeElement;
1269
1270
  if (own.current && (!a || a === document.body)) {
1270
- const first = own.current.querySelector('[role^="menuitem"]:not([disabled])');
1271
- if (first) first.focus();
1271
+ const first = own.current.querySelector(ITEMS);
1272
+ (first || own.current.querySelector('[role="menu"]') || own.current).focus();
1272
1273
  }
1273
1274
  },
1274
1275
  [hasHeader]
@@ -1276,8 +1277,11 @@ window.Aura = (() => {
1276
1277
  React8.useEffect(
1277
1278
  function() {
1278
1279
  if (!mounted) return;
1279
- const first = own.current && own.current.querySelector('[role^="menuitem"]:not([disabled])');
1280
- if (first && props.autoFocus !== false) first.focus();
1280
+ const first = own.current && own.current.querySelector(ITEMS);
1281
+ if (props.autoFocus !== false) {
1282
+ if (first) first.focus();
1283
+ else if (own.current) (own.current.querySelector('[role="menu"]') || own.current).focus();
1284
+ }
1281
1285
  function outside(e) {
1282
1286
  if (own.current && !own.current.contains(e.target) && !(props.anchor && props.anchor.contains(e.target)))
1283
1287
  props.onClose(false);
@@ -1297,16 +1301,18 @@ window.Aura = (() => {
1297
1301
  [mounted]
1298
1302
  );
1299
1303
  function onKeyDown(e) {
1300
- const list2 = Array.prototype.slice.call(
1301
- own.current.querySelectorAll('[role^="menuitem"]:not([disabled])')
1302
- );
1304
+ const list2 = Array.prototype.slice.call(own.current.querySelectorAll(ITEMS));
1303
1305
  const i = list2.indexOf(document.activeElement);
1306
+ if (!list2.length && e.key !== "Escape" && e.key !== "Tab") {
1307
+ e.stopPropagation();
1308
+ return;
1309
+ }
1304
1310
  if (e.key === "ArrowDown") {
1305
1311
  e.preventDefault();
1306
1312
  list2[(i + 1) % list2.length].focus();
1307
1313
  } else if (e.key === "ArrowUp") {
1308
1314
  e.preventDefault();
1309
- list2[(i - 1 + list2.length) % list2.length].focus();
1315
+ list2[i < 0 ? list2.length - 1 : (i - 1 + list2.length) % list2.length].focus();
1310
1316
  } else if (e.key === "Home") {
1311
1317
  e.preventDefault();
1312
1318
  list2[0].focus();
@@ -1345,6 +1351,10 @@ window.Aura = (() => {
1345
1351
  it.hint ? /* @__PURE__ */ React8.createElement("span", { key: "h", className: "aura-menu__hint" }, it.hint) : null
1346
1352
  ];
1347
1353
  const cls = cx("aura-menu__item", it.tone === "danger" && "aura-menu__item--danger");
1354
+ const reasonId = it.disabled && it.disabledReason ? itemId + "-r" + i : void 0;
1355
+ if (reasonId)
1356
+ body[2] = /* Hidden from the name (it would be read twice); aria-describedby still reads it. */
1357
+ /* @__PURE__ */ React8.createElement("span", { key: "h", id: reasonId, className: "aura-menu__hint aura-menu__reason", "aria-hidden": true }, it.disabledReason);
1348
1358
  if (it.href && !it.disabled)
1349
1359
  return /* @__PURE__ */ React8.createElement(
1350
1360
  Link,
@@ -1369,11 +1379,13 @@ window.Aura = (() => {
1369
1379
  key: i,
1370
1380
  type: "button",
1371
1381
  tabIndex: -1,
1372
- disabled: it.disabled,
1382
+ "aria-disabled": it.disabled ? true : void 0,
1383
+ "aria-describedby": reasonId,
1373
1384
  role: isRadio ? "menuitemradio" : isCheck ? "menuitemcheckbox" : "menuitem",
1374
1385
  "aria-checked": isRadio || isCheck ? !!it.checked : void 0,
1375
1386
  className: cls,
1376
1387
  onClick: function() {
1388
+ if (it.disabled) return;
1377
1389
  if (it.onSelect) it.onSelect();
1378
1390
  if (!it.keepOpen) props.onClose(true);
1379
1391
  }
@@ -1402,7 +1414,19 @@ window.Aura = (() => {
1402
1414
  }
1403
1415
  },
1404
1416
  props.header
1405
- ), /* @__PURE__ */ React8.createElement("div", { role: "menu", "aria-label": props.label, "aria-describedby": headerId, className: "aura-menu__list" }, list)) : /* @__PURE__ */ React8.createElement("div", { ref: merged, role: "menu", "aria-label": props.label, className: "aura-menu", onKeyDown, style }, list);
1417
+ ), /* @__PURE__ */ React8.createElement("div", { role: "menu", tabIndex: -1, "aria-label": props.label, "aria-describedby": headerId, className: "aura-menu__list" }, list)) : /* @__PURE__ */ React8.createElement(
1418
+ "div",
1419
+ {
1420
+ ref: merged,
1421
+ role: "menu",
1422
+ tabIndex: -1,
1423
+ "aria-label": props.label,
1424
+ className: "aura-menu",
1425
+ onKeyDown,
1426
+ style
1427
+ },
1428
+ list
1429
+ );
1406
1430
  return mounted ? (0, import_react_dom.createPortal)(el, document.body) : null;
1407
1431
  });
1408
1432
 
@@ -1417,7 +1441,7 @@ window.Aura = (() => {
1417
1441
  function() {
1418
1442
  if (!anchor || !toLast.current || !menuRef.current) return;
1419
1443
  toLast.current = false;
1420
- const list = menuRef.current.querySelectorAll('[role^="menuitem"]:not([disabled])');
1444
+ const list = menuRef.current.querySelectorAll('[role^="menuitem"]');
1421
1445
  if (list.length) list[list.length - 1].focus();
1422
1446
  },
1423
1447
  [anchor]
@@ -5876,6 +5900,12 @@ window.Aura = (() => {
5876
5900
  })[0] || firstOn || items2[0];
5877
5901
  const Link = useLinkComponent(props.linkComponent);
5878
5902
  const manual = props.activation === "manual";
5903
+ const seg = props.variant === "segmented";
5904
+ const listCls = cx("aura-tabs__list", seg && "aura-segmented", seg && props.fullWidth && "is-full");
5905
+ const tabCls = function(on, extra) {
5906
+ return cx("aura-tab", on && "is-active", seg && "aura-segmented__option", seg && on && "is-selected", extra);
5907
+ };
5908
+ const rootCls = cx("aura-tabs", seg && "aura-tabs--segmented");
5879
5909
  const [focusId, setFocusId] = React32.useState(null);
5880
5910
  items2.forEach(function(t) {
5881
5911
  const al = t.tabProps && t.tabProps["aria-label"];
@@ -5891,9 +5921,9 @@ window.Aura = (() => {
5891
5921
  {
5892
5922
  ref,
5893
5923
  "aria-label": props.label,
5894
- className: cx("aura-tabs aura-tabs--links", props.className)
5924
+ className: cx(rootCls, "aura-tabs--links", props.className)
5895
5925
  },
5896
- /* @__PURE__ */ React32.createElement("div", { className: "aura-tabs__list" }, items2.map(function(t) {
5926
+ /* @__PURE__ */ React32.createElement("div", { className: listCls }, items2.map(function(t) {
5897
5927
  const on = t.id === cur;
5898
5928
  const inner = [
5899
5929
  t.icon ? /* @__PURE__ */ React32.createElement(Icon, { key: "i", name: t.icon }) : null,
@@ -5917,7 +5947,7 @@ window.Aura = (() => {
5917
5947
  {
5918
5948
  ...omit(own, ["onClick"]),
5919
5949
  key: t.id,
5920
- className: cx("aura-tab is-disabled", own.className),
5950
+ className: cx(tabCls(false), "is-disabled", own.className),
5921
5951
  "aria-disabled": true
5922
5952
  },
5923
5953
  inner
@@ -5927,7 +5957,7 @@ window.Aura = (() => {
5927
5957
  ...own,
5928
5958
  key: t.id,
5929
5959
  href: t.href,
5930
- className: cx("aura-tab", on && "is-active", own.className),
5960
+ className: tabCls(on, own.className),
5931
5961
  "aria-current": on ? "page" : void 0,
5932
5962
  onClick: function(e) {
5933
5963
  if (own.onClick) own.onClick(e);
@@ -5951,12 +5981,12 @@ window.Aura = (() => {
5951
5981
  const stop = manual && focusId && items2.filter(function(t) {
5952
5982
  return t.id === focusId && !t.disabled;
5953
5983
  })[0] || current2;
5954
- return /* @__PURE__ */ React32.createElement("div", { ref, className: cx("aura-tabs", props.className) }, /* @__PURE__ */ React32.createElement(
5984
+ return /* @__PURE__ */ React32.createElement("div", { ref, className: cx(rootCls, props.className) }, /* @__PURE__ */ React32.createElement(
5955
5985
  "div",
5956
5986
  {
5957
5987
  role: "tablist",
5958
5988
  "aria-label": props.label,
5959
- className: "aura-tabs__list",
5989
+ className: listCls,
5960
5990
  onBlur: manual ? function(e) {
5961
5991
  if (!e.currentTarget.contains(e.relatedTarget)) setFocusId(null);
5962
5992
  } : void 0,
@@ -5999,7 +6029,7 @@ window.Aura = (() => {
5999
6029
  ref: function(el) {
6000
6030
  refs.current[t.id] = el;
6001
6031
  },
6002
- className: cx("aura-tab", on && "is-active", own.className),
6032
+ className: tabCls(on, own.className),
6003
6033
  onFocus: manual ? function(e) {
6004
6034
  if (own.onFocus) own.onFocus(e);
6005
6035
  setFocusId(t.id);
@@ -1215,13 +1215,14 @@ var IconButton = React7.forwardRef(function IconButton2(props, ref) {
1215
1215
  // src/Menu.tsx
1216
1216
  var React8 = __toESM(require("react"), 1);
1217
1217
  var import_react_dom = require("react-dom");
1218
+ var ITEMS = '[role^="menuitem"]';
1218
1219
  var Menu = React8.forwardRef(function Menu2(props, ref) {
1219
1220
  const own = React8.useRef(null), merged = useMergedRef(ref, own);
1220
1221
  const posState = React8.useState(null);
1221
1222
  const pos = posState[0], setPos = posState[1];
1222
1223
  const items2 = props.items || [];
1223
1224
  const mounted = useMounted();
1224
- const headerId = uid();
1225
+ const headerId = uid(), itemId = uid();
1225
1226
  const hasHeader = props.header != null && props.header !== false && props.header !== "";
1226
1227
  const Link = useLinkComponent(props.linkComponent);
1227
1228
  useIsoLayoutEffect(
@@ -1249,8 +1250,8 @@ var Menu = React8.forwardRef(function Menu2(props, ref) {
1249
1250
  hadHeader.current = hasHeader;
1250
1251
  const a = document.activeElement;
1251
1252
  if (own.current && (!a || a === document.body)) {
1252
- const first = own.current.querySelector('[role^="menuitem"]:not([disabled])');
1253
- if (first) first.focus();
1253
+ const first = own.current.querySelector(ITEMS);
1254
+ (first || own.current.querySelector('[role="menu"]') || own.current).focus();
1254
1255
  }
1255
1256
  },
1256
1257
  [hasHeader]
@@ -1258,8 +1259,11 @@ var Menu = React8.forwardRef(function Menu2(props, ref) {
1258
1259
  React8.useEffect(
1259
1260
  function() {
1260
1261
  if (!mounted) return;
1261
- const first = own.current && own.current.querySelector('[role^="menuitem"]:not([disabled])');
1262
- if (first && props.autoFocus !== false) first.focus();
1262
+ const first = own.current && own.current.querySelector(ITEMS);
1263
+ if (props.autoFocus !== false) {
1264
+ if (first) first.focus();
1265
+ else if (own.current) (own.current.querySelector('[role="menu"]') || own.current).focus();
1266
+ }
1263
1267
  function outside(e) {
1264
1268
  if (own.current && !own.current.contains(e.target) && !(props.anchor && props.anchor.contains(e.target)))
1265
1269
  props.onClose(false);
@@ -1279,16 +1283,18 @@ var Menu = React8.forwardRef(function Menu2(props, ref) {
1279
1283
  [mounted]
1280
1284
  );
1281
1285
  function onKeyDown(e) {
1282
- const list2 = Array.prototype.slice.call(
1283
- own.current.querySelectorAll('[role^="menuitem"]:not([disabled])')
1284
- );
1286
+ const list2 = Array.prototype.slice.call(own.current.querySelectorAll(ITEMS));
1285
1287
  const i = list2.indexOf(document.activeElement);
1288
+ if (!list2.length && e.key !== "Escape" && e.key !== "Tab") {
1289
+ e.stopPropagation();
1290
+ return;
1291
+ }
1286
1292
  if (e.key === "ArrowDown") {
1287
1293
  e.preventDefault();
1288
1294
  list2[(i + 1) % list2.length].focus();
1289
1295
  } else if (e.key === "ArrowUp") {
1290
1296
  e.preventDefault();
1291
- list2[(i - 1 + list2.length) % list2.length].focus();
1297
+ list2[i < 0 ? list2.length - 1 : (i - 1 + list2.length) % list2.length].focus();
1292
1298
  } else if (e.key === "Home") {
1293
1299
  e.preventDefault();
1294
1300
  list2[0].focus();
@@ -1327,6 +1333,10 @@ var Menu = React8.forwardRef(function Menu2(props, ref) {
1327
1333
  it.hint ? /* @__PURE__ */ React8.createElement("span", { key: "h", className: "aura-menu__hint" }, it.hint) : null
1328
1334
  ];
1329
1335
  const cls = cx("aura-menu__item", it.tone === "danger" && "aura-menu__item--danger");
1336
+ const reasonId = it.disabled && it.disabledReason ? itemId + "-r" + i : void 0;
1337
+ if (reasonId)
1338
+ body[2] = /* Hidden from the name (it would be read twice); aria-describedby still reads it. */
1339
+ /* @__PURE__ */ React8.createElement("span", { key: "h", id: reasonId, className: "aura-menu__hint aura-menu__reason", "aria-hidden": true }, it.disabledReason);
1330
1340
  if (it.href && !it.disabled)
1331
1341
  return /* @__PURE__ */ React8.createElement(
1332
1342
  Link,
@@ -1351,11 +1361,13 @@ var Menu = React8.forwardRef(function Menu2(props, ref) {
1351
1361
  key: i,
1352
1362
  type: "button",
1353
1363
  tabIndex: -1,
1354
- disabled: it.disabled,
1364
+ "aria-disabled": it.disabled ? true : void 0,
1365
+ "aria-describedby": reasonId,
1355
1366
  role: isRadio ? "menuitemradio" : isCheck ? "menuitemcheckbox" : "menuitem",
1356
1367
  "aria-checked": isRadio || isCheck ? !!it.checked : void 0,
1357
1368
  className: cls,
1358
1369
  onClick: function() {
1370
+ if (it.disabled) return;
1359
1371
  if (it.onSelect) it.onSelect();
1360
1372
  if (!it.keepOpen) props.onClose(true);
1361
1373
  }
@@ -1384,7 +1396,19 @@ var Menu = React8.forwardRef(function Menu2(props, ref) {
1384
1396
  }
1385
1397
  },
1386
1398
  props.header
1387
- ), /* @__PURE__ */ React8.createElement("div", { role: "menu", "aria-label": props.label, "aria-describedby": headerId, className: "aura-menu__list" }, list)) : /* @__PURE__ */ React8.createElement("div", { ref: merged, role: "menu", "aria-label": props.label, className: "aura-menu", onKeyDown, style }, list);
1399
+ ), /* @__PURE__ */ React8.createElement("div", { role: "menu", tabIndex: -1, "aria-label": props.label, "aria-describedby": headerId, className: "aura-menu__list" }, list)) : /* @__PURE__ */ React8.createElement(
1400
+ "div",
1401
+ {
1402
+ ref: merged,
1403
+ role: "menu",
1404
+ tabIndex: -1,
1405
+ "aria-label": props.label,
1406
+ className: "aura-menu",
1407
+ onKeyDown,
1408
+ style
1409
+ },
1410
+ list
1411
+ );
1388
1412
  return mounted ? (0, import_react_dom.createPortal)(el, document.body) : null;
1389
1413
  });
1390
1414
 
@@ -1399,7 +1423,7 @@ var DropdownMenu = React9.forwardRef(function DropdownMenu2(props, ref) {
1399
1423
  function() {
1400
1424
  if (!anchor || !toLast.current || !menuRef.current) return;
1401
1425
  toLast.current = false;
1402
- const list = menuRef.current.querySelectorAll('[role^="menuitem"]:not([disabled])');
1426
+ const list = menuRef.current.querySelectorAll('[role^="menuitem"]');
1403
1427
  if (list.length) list[list.length - 1].focus();
1404
1428
  },
1405
1429
  [anchor]
@@ -5858,6 +5882,12 @@ var Tabs = React32.forwardRef(function Tabs2(props, ref) {
5858
5882
  })[0] || firstOn || items2[0];
5859
5883
  const Link = useLinkComponent(props.linkComponent);
5860
5884
  const manual = props.activation === "manual";
5885
+ const seg = props.variant === "segmented";
5886
+ const listCls = cx("aura-tabs__list", seg && "aura-segmented", seg && props.fullWidth && "is-full");
5887
+ const tabCls = function(on, extra) {
5888
+ return cx("aura-tab", on && "is-active", seg && "aura-segmented__option", seg && on && "is-selected", extra);
5889
+ };
5890
+ const rootCls = cx("aura-tabs", seg && "aura-tabs--segmented");
5861
5891
  const [focusId, setFocusId] = React32.useState(null);
5862
5892
  items2.forEach(function(t) {
5863
5893
  const al = t.tabProps && t.tabProps["aria-label"];
@@ -5873,9 +5903,9 @@ var Tabs = React32.forwardRef(function Tabs2(props, ref) {
5873
5903
  {
5874
5904
  ref,
5875
5905
  "aria-label": props.label,
5876
- className: cx("aura-tabs aura-tabs--links", props.className)
5906
+ className: cx(rootCls, "aura-tabs--links", props.className)
5877
5907
  },
5878
- /* @__PURE__ */ React32.createElement("div", { className: "aura-tabs__list" }, items2.map(function(t) {
5908
+ /* @__PURE__ */ React32.createElement("div", { className: listCls }, items2.map(function(t) {
5879
5909
  const on = t.id === cur;
5880
5910
  const inner = [
5881
5911
  t.icon ? /* @__PURE__ */ React32.createElement(Icon, { key: "i", name: t.icon }) : null,
@@ -5899,7 +5929,7 @@ var Tabs = React32.forwardRef(function Tabs2(props, ref) {
5899
5929
  {
5900
5930
  ...omit(own, ["onClick"]),
5901
5931
  key: t.id,
5902
- className: cx("aura-tab is-disabled", own.className),
5932
+ className: cx(tabCls(false), "is-disabled", own.className),
5903
5933
  "aria-disabled": true
5904
5934
  },
5905
5935
  inner
@@ -5909,7 +5939,7 @@ var Tabs = React32.forwardRef(function Tabs2(props, ref) {
5909
5939
  ...own,
5910
5940
  key: t.id,
5911
5941
  href: t.href,
5912
- className: cx("aura-tab", on && "is-active", own.className),
5942
+ className: tabCls(on, own.className),
5913
5943
  "aria-current": on ? "page" : void 0,
5914
5944
  onClick: function(e) {
5915
5945
  if (own.onClick) own.onClick(e);
@@ -5933,12 +5963,12 @@ var Tabs = React32.forwardRef(function Tabs2(props, ref) {
5933
5963
  const stop = manual && focusId && items2.filter(function(t) {
5934
5964
  return t.id === focusId && !t.disabled;
5935
5965
  })[0] || current2;
5936
- return /* @__PURE__ */ React32.createElement("div", { ref, className: cx("aura-tabs", props.className) }, /* @__PURE__ */ React32.createElement(
5966
+ return /* @__PURE__ */ React32.createElement("div", { ref, className: cx(rootCls, props.className) }, /* @__PURE__ */ React32.createElement(
5937
5967
  "div",
5938
5968
  {
5939
5969
  role: "tablist",
5940
5970
  "aria-label": props.label,
5941
- className: "aura-tabs__list",
5971
+ className: listCls,
5942
5972
  onBlur: manual ? function(e) {
5943
5973
  if (!e.currentTarget.contains(e.relatedTarget)) setFocusId(null);
5944
5974
  } : void 0,
@@ -5981,7 +6011,7 @@ var Tabs = React32.forwardRef(function Tabs2(props, ref) {
5981
6011
  ref: function(el) {
5982
6012
  refs.current[t.id] = el;
5983
6013
  },
5984
- className: cx("aura-tab", on && "is-active", own.className),
6014
+ className: tabCls(on, own.className),
5985
6015
  onFocus: manual ? function(e) {
5986
6016
  if (own.onFocus) own.onFocus(e);
5987
6017
  setFocusId(t.id);
@@ -11,7 +11,7 @@ const DropdownMenu = React.forwardRef(function DropdownMenu2(props, ref) {
11
11
  function() {
12
12
  if (!anchor || !toLast.current || !menuRef.current) return;
13
13
  toLast.current = false;
14
- const list = menuRef.current.querySelectorAll('[role^="menuitem"]:not([disabled])');
14
+ const list = menuRef.current.querySelectorAll('[role^="menuitem"]');
15
15
  if (list.length) list[list.length - 1].focus();
16
16
  },
17
17
  [anchor]
package/dist/esm/Menu.js CHANGED
@@ -5,13 +5,14 @@ import { cx, uid, useIsoLayoutEffect, useMounted, useMergedRef } from "./interna
5
5
  import { Icon } from "./Icon.js";
6
6
  import { useLinkComponent } from "./locale.js";
7
7
  import { IconCheck } from "./icons.js";
8
+ const ITEMS = '[role^="menuitem"]';
8
9
  const Menu = React.forwardRef(function Menu2(props, ref) {
9
10
  const own = React.useRef(null), merged = useMergedRef(ref, own);
10
11
  const posState = React.useState(null);
11
12
  const pos = posState[0], setPos = posState[1];
12
13
  const items = props.items || [];
13
14
  const mounted = useMounted();
14
- const headerId = uid();
15
+ const headerId = uid(), itemId = uid();
15
16
  const hasHeader = props.header != null && props.header !== false && props.header !== "";
16
17
  const Link = useLinkComponent(props.linkComponent);
17
18
  useIsoLayoutEffect(
@@ -39,8 +40,8 @@ const Menu = React.forwardRef(function Menu2(props, ref) {
39
40
  hadHeader.current = hasHeader;
40
41
  const a = document.activeElement;
41
42
  if (own.current && (!a || a === document.body)) {
42
- const first = own.current.querySelector('[role^="menuitem"]:not([disabled])');
43
- if (first) first.focus();
43
+ const first = own.current.querySelector(ITEMS);
44
+ (first || own.current.querySelector('[role="menu"]') || own.current).focus();
44
45
  }
45
46
  },
46
47
  [hasHeader]
@@ -48,8 +49,11 @@ const Menu = React.forwardRef(function Menu2(props, ref) {
48
49
  React.useEffect(
49
50
  function() {
50
51
  if (!mounted) return;
51
- const first = own.current && own.current.querySelector('[role^="menuitem"]:not([disabled])');
52
- if (first && props.autoFocus !== false) first.focus();
52
+ const first = own.current && own.current.querySelector(ITEMS);
53
+ if (props.autoFocus !== false) {
54
+ if (first) first.focus();
55
+ else if (own.current) (own.current.querySelector('[role="menu"]') || own.current).focus();
56
+ }
53
57
  function outside(e) {
54
58
  if (own.current && !own.current.contains(e.target) && !(props.anchor && props.anchor.contains(e.target)))
55
59
  props.onClose(false);
@@ -69,16 +73,18 @@ const Menu = React.forwardRef(function Menu2(props, ref) {
69
73
  [mounted]
70
74
  );
71
75
  function onKeyDown(e) {
72
- const list2 = Array.prototype.slice.call(
73
- own.current.querySelectorAll('[role^="menuitem"]:not([disabled])')
74
- );
76
+ const list2 = Array.prototype.slice.call(own.current.querySelectorAll(ITEMS));
75
77
  const i = list2.indexOf(document.activeElement);
78
+ if (!list2.length && e.key !== "Escape" && e.key !== "Tab") {
79
+ e.stopPropagation();
80
+ return;
81
+ }
76
82
  if (e.key === "ArrowDown") {
77
83
  e.preventDefault();
78
84
  list2[(i + 1) % list2.length].focus();
79
85
  } else if (e.key === "ArrowUp") {
80
86
  e.preventDefault();
81
- list2[(i - 1 + list2.length) % list2.length].focus();
87
+ list2[i < 0 ? list2.length - 1 : (i - 1 + list2.length) % list2.length].focus();
82
88
  } else if (e.key === "Home") {
83
89
  e.preventDefault();
84
90
  list2[0].focus();
@@ -117,6 +123,10 @@ const Menu = React.forwardRef(function Menu2(props, ref) {
117
123
  it.hint ? /* @__PURE__ */ React.createElement("span", { key: "h", className: "aura-menu__hint" }, it.hint) : null
118
124
  ];
119
125
  const cls = cx("aura-menu__item", it.tone === "danger" && "aura-menu__item--danger");
126
+ const reasonId = it.disabled && it.disabledReason ? itemId + "-r" + i : void 0;
127
+ if (reasonId)
128
+ body[2] = /* Hidden from the name (it would be read twice); aria-describedby still reads it. */
129
+ /* @__PURE__ */ React.createElement("span", { key: "h", id: reasonId, className: "aura-menu__hint aura-menu__reason", "aria-hidden": true }, it.disabledReason);
120
130
  if (it.href && !it.disabled)
121
131
  return /* @__PURE__ */ React.createElement(
122
132
  Link,
@@ -141,11 +151,13 @@ const Menu = React.forwardRef(function Menu2(props, ref) {
141
151
  key: i,
142
152
  type: "button",
143
153
  tabIndex: -1,
144
- disabled: it.disabled,
154
+ "aria-disabled": it.disabled ? true : void 0,
155
+ "aria-describedby": reasonId,
145
156
  role: isRadio ? "menuitemradio" : isCheck ? "menuitemcheckbox" : "menuitem",
146
157
  "aria-checked": isRadio || isCheck ? !!it.checked : void 0,
147
158
  className: cls,
148
159
  onClick: function() {
160
+ if (it.disabled) return;
149
161
  if (it.onSelect) it.onSelect();
150
162
  if (!it.keepOpen) props.onClose(true);
151
163
  }
@@ -174,7 +186,19 @@ const Menu = React.forwardRef(function Menu2(props, ref) {
174
186
  }
175
187
  },
176
188
  props.header
177
- ), /* @__PURE__ */ React.createElement("div", { role: "menu", "aria-label": props.label, "aria-describedby": headerId, className: "aura-menu__list" }, list)) : /* @__PURE__ */ React.createElement("div", { ref: merged, role: "menu", "aria-label": props.label, className: "aura-menu", onKeyDown, style }, list);
189
+ ), /* @__PURE__ */ React.createElement("div", { role: "menu", tabIndex: -1, "aria-label": props.label, "aria-describedby": headerId, className: "aura-menu__list" }, list)) : /* @__PURE__ */ React.createElement(
190
+ "div",
191
+ {
192
+ ref: merged,
193
+ role: "menu",
194
+ tabIndex: -1,
195
+ "aria-label": props.label,
196
+ className: "aura-menu",
197
+ onKeyDown,
198
+ style
199
+ },
200
+ list
201
+ );
178
202
  return mounted ? createPortal(el, document.body) : null;
179
203
  });
180
204
  export {
package/dist/esm/Tabs.js CHANGED
@@ -23,6 +23,12 @@ const Tabs = React.forwardRef(function Tabs2(props, ref) {
23
23
  })[0] || firstOn || items[0];
24
24
  const Link = useLinkComponent(props.linkComponent);
25
25
  const manual = props.activation === "manual";
26
+ const seg = props.variant === "segmented";
27
+ const listCls = cx("aura-tabs__list", seg && "aura-segmented", seg && props.fullWidth && "is-full");
28
+ const tabCls = function(on, extra) {
29
+ return cx("aura-tab", on && "is-active", seg && "aura-segmented__option", seg && on && "is-selected", extra);
30
+ };
31
+ const rootCls = cx("aura-tabs", seg && "aura-tabs--segmented");
26
32
  const [focusId, setFocusId] = React.useState(null);
27
33
  items.forEach(function(t) {
28
34
  const al = t.tabProps && t.tabProps["aria-label"];
@@ -38,9 +44,9 @@ const Tabs = React.forwardRef(function Tabs2(props, ref) {
38
44
  {
39
45
  ref,
40
46
  "aria-label": props.label,
41
- className: cx("aura-tabs aura-tabs--links", props.className)
47
+ className: cx(rootCls, "aura-tabs--links", props.className)
42
48
  },
43
- /* @__PURE__ */ React.createElement("div", { className: "aura-tabs__list" }, items.map(function(t) {
49
+ /* @__PURE__ */ React.createElement("div", { className: listCls }, items.map(function(t) {
44
50
  const on = t.id === cur;
45
51
  const inner = [
46
52
  t.icon ? /* @__PURE__ */ React.createElement(Icon, { key: "i", name: t.icon }) : null,
@@ -64,7 +70,7 @@ const Tabs = React.forwardRef(function Tabs2(props, ref) {
64
70
  {
65
71
  ...omit(own, ["onClick"]),
66
72
  key: t.id,
67
- className: cx("aura-tab is-disabled", own.className),
73
+ className: cx(tabCls(false), "is-disabled", own.className),
68
74
  "aria-disabled": true
69
75
  },
70
76
  inner
@@ -74,7 +80,7 @@ const Tabs = React.forwardRef(function Tabs2(props, ref) {
74
80
  ...own,
75
81
  key: t.id,
76
82
  href: t.href,
77
- className: cx("aura-tab", on && "is-active", own.className),
83
+ className: tabCls(on, own.className),
78
84
  "aria-current": on ? "page" : void 0,
79
85
  onClick: function(e) {
80
86
  if (own.onClick) own.onClick(e);
@@ -98,12 +104,12 @@ const Tabs = React.forwardRef(function Tabs2(props, ref) {
98
104
  const stop = manual && focusId && items.filter(function(t) {
99
105
  return t.id === focusId && !t.disabled;
100
106
  })[0] || current;
101
- return /* @__PURE__ */ React.createElement("div", { ref, className: cx("aura-tabs", props.className) }, /* @__PURE__ */ React.createElement(
107
+ return /* @__PURE__ */ React.createElement("div", { ref, className: cx(rootCls, props.className) }, /* @__PURE__ */ React.createElement(
102
108
  "div",
103
109
  {
104
110
  role: "tablist",
105
111
  "aria-label": props.label,
106
- className: "aura-tabs__list",
112
+ className: listCls,
107
113
  onBlur: manual ? function(e) {
108
114
  if (!e.currentTarget.contains(e.relatedTarget)) setFocusId(null);
109
115
  } : void 0,
@@ -146,7 +152,7 @@ const Tabs = React.forwardRef(function Tabs2(props, ref) {
146
152
  ref: function(el) {
147
153
  refs.current[t.id] = el;
148
154
  },
149
- className: cx("aura-tab", on && "is-active", own.className),
155
+ className: tabCls(on, own.className),
150
156
  onFocus: manual ? function(e) {
151
157
  if (own.onFocus) own.onFocus(e);
152
158
  setFocusId(t.id);
package/dist/index.d.cts CHANGED
@@ -278,7 +278,10 @@ export interface MenuItem {
278
278
  icon?: IconInput | undefined;
279
279
  /** Present = a checkable item (menuitemcheckbox). */
280
280
  checked?: boolean | undefined;
281
+ /** Dimmed and does nothing when chosen, but still reachable with the arrow keys and announced (5.10). */
281
282
  disabled?: boolean | undefined;
283
+ /** Why it's disabled ("You can resend in 5 minutes"): shown in place of the hint and read with the item. (5.10) */
284
+ disabledReason?: string | undefined;
282
285
  /** Keep the menu open after choosing (checkbox lists). */
283
286
  keepOpen?: boolean | undefined;
284
287
  /** Short right-aligned hint, e.g. a shortcut. */
@@ -617,6 +620,11 @@ export interface TabsProps {
617
620
  /** `auto` (default): arrow keys select as they move. `manual`: arrows, Home and End only move focus; Enter, Space
618
621
  * or a click selects — for tabs whose selection does work (loads data, starts a payment). (5.9) */
619
622
  activation?: "auto" | "manual" | undefined;
623
+ /** `underline` (default) or `segmented`: the tabs as a pill track with the selected one raised, like
624
+ * SegmentedControl, keeping every Tabs behaviour (panels, keepMounted, activation, tabProps). (5.10, Chamber-OS 72) */
625
+ variant?: "underline" | "segmented" | undefined;
626
+ /** With `variant="segmented"`: stretch the track to the full width, tabs sharing it equally. (5.10) */
627
+ fullWidth?: boolean | undefined;
620
628
  }
621
629
  export interface NavItem {
622
630
  id: string;
package/dist/index.d.ts CHANGED
@@ -278,7 +278,10 @@ export interface MenuItem {
278
278
  icon?: IconInput | undefined;
279
279
  /** Present = a checkable item (menuitemcheckbox). */
280
280
  checked?: boolean | undefined;
281
+ /** Dimmed and does nothing when chosen, but still reachable with the arrow keys and announced (5.10). */
281
282
  disabled?: boolean | undefined;
283
+ /** Why it's disabled ("You can resend in 5 minutes"): shown in place of the hint and read with the item. (5.10) */
284
+ disabledReason?: string | undefined;
282
285
  /** Keep the menu open after choosing (checkbox lists). */
283
286
  keepOpen?: boolean | undefined;
284
287
  /** Short right-aligned hint, e.g. a shortcut. */
@@ -617,6 +620,11 @@ export interface TabsProps {
617
620
  /** `auto` (default): arrow keys select as they move. `manual`: arrows, Home and End only move focus; Enter, Space
618
621
  * or a click selects — for tabs whose selection does work (loads data, starts a payment). (5.9) */
619
622
  activation?: "auto" | "manual" | undefined;
623
+ /** `underline` (default) or `segmented`: the tabs as a pill track with the selected one raised, like
624
+ * SegmentedControl, keeping every Tabs behaviour (panels, keepMounted, activation, tabProps). (5.10, Chamber-OS 72) */
625
+ variant?: "underline" | "segmented" | undefined;
626
+ /** With `variant="segmented"`: stretch the track to the full width, tabs sharing it equally. (5.10) */
627
+ fullWidth?: boolean | undefined;
620
628
  }
621
629
  export interface NavItem {
622
630
  id: string;
package/dist/styles.css CHANGED
@@ -226,7 +226,13 @@ a.aura-btn { text-decoration: none; }
226
226
  }
227
227
  .aura-menu__item:hover, .aura-menu__item:focus { background: var(--aura-bg-surface-hover); outline: none; }
228
228
  .aura-menu__item:focus-visible { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: calc(-1 * var(--aura-focus-ring-width)); }
229
- .aura-menu__item:disabled { opacity: var(--aura-disabled-opacity); cursor: default; background: transparent; }
229
+ .aura-menu__item:disabled { opacity: var(--aura-disabled-opacity); cursor: default; }
230
+ /* 5.10: a disabled item is dimmed but focusable; its reason stays at full strength (it is the information). */
231
+ .aura-menu__item[aria-disabled="true"] { cursor: default; }
232
+ .aura-menu__item[aria-disabled="true"] > :not(.aura-menu__reason) { opacity: var(--aura-disabled-opacity); }
233
+ .aura-menu__reason { padding-left: var(--aura-space-4); }
234
+ .aura-menu__item:disabled, .aura-menu__item[aria-disabled="true"]:hover:not(:focus) { background: transparent; }
235
+ .aura-menu:focus, .aura-menu__list:focus { outline: none; }
230
236
  .aura-menu__item .aura-icon { color: var(--aura-fg-secondary); }
231
237
  .aura-menu__label { flex: 1; }
232
238
  .aura-menu__blank { width: var(--aura-icon-sm); flex: none; }
@@ -1203,6 +1209,16 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
1203
1209
  .aura-segmented--sm .aura-segmented__option { height: 24px; padding: 0 var(--aura-space-3); font-size: 12px; }
1204
1210
  .aura-segmented--sm .aura-segmented__option.is-icon { width: 28px; padding: 0; }
1205
1211
  .aura-segmented.is-disabled { opacity: var(--aura-disabled-opacity); }
1212
+ /* 5.10 (Chamber-OS 72): Tabs variant="segmented" — the tablist wears SegmentedControl's track and pills. The focus
1213
+ * ring is drawn inside the pill, so neither the scrolling list (many tabs on a phone) nor the next pill can cover it;
1214
+ * full-width tabs share the track but never shrink below their label (the track scrolls instead). The phone rule's
1215
+ * wider tab gap doesn't apply. */
1216
+ .aura-tabs--segmented .aura-tabs__list.aura-segmented { gap: 2px; }
1217
+ .aura-tabs--segmented .aura-tabs__list.aura-segmented.is-full .aura-tab { min-width: max-content; }
1218
+ .aura-tabs--segmented .aura-tab.aura-segmented__option { margin-bottom: 0; }
1219
+ .aura-tabs--segmented .aura-tab.aura-segmented__option:focus-visible { border-radius: var(--aura-radius-full); outline-offset: calc(-1 * var(--aura-focus-ring-width)); }
1220
+ .aura-tabs--segmented .aura-tab.aura-segmented__option:disabled,
1221
+ .aura-tabs--segmented .aura-tab.aura-segmented__option.is-disabled { opacity: var(--aura-disabled-opacity); cursor: not-allowed; }
1206
1222
 
1207
1223
  /* A field inside a row Stack takes the remaining width (a search box beside its button). */
1208
1224
  .aura-stack > .aura-field { flex: 1 1 auto; min-width: 0; }
@@ -1590,4 +1606,8 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
1590
1606
  .aura-segmented__option.is-selected { outline: 2px solid Highlight; outline-offset: -2px; }
1591
1607
  .aura-tab { border-bottom-color: Canvas; }
1592
1608
  .aura-tab.is-active { border-bottom-color: Highlight; }
1609
+ /* 5.10: segmented tabs mark the selection with a border and focus with a dashed ring inside it, so a focused tab
1610
+ * and the selected one never look alike (manual activation moves focus without selecting). */
1611
+ .aura-tabs--segmented .aura-tab.aura-segmented__option.is-selected { outline: none; border: 2px solid Highlight; }
1612
+ .aura-tabs--segmented .aura-tab.aura-segmented__option:focus-visible { outline: 2px dashed CanvasText; outline-offset: -5px; }
1593
1613
  }
@@ -228,7 +228,13 @@ a.aura-btn { text-decoration: none; }
228
228
  }
229
229
  .aura-menu__item:hover, .aura-menu__item:focus { background: var(--aura-bg-surface-hover); outline: none; }
230
230
  .aura-menu__item:focus-visible { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: calc(-1 * var(--aura-focus-ring-width)); }
231
- .aura-menu__item:disabled { opacity: var(--aura-disabled-opacity); cursor: default; background: transparent; }
231
+ .aura-menu__item:disabled { opacity: var(--aura-disabled-opacity); cursor: default; }
232
+ /* 5.10: a disabled item is dimmed but focusable; its reason stays at full strength (it is the information). */
233
+ .aura-menu__item[aria-disabled="true"] { cursor: default; }
234
+ .aura-menu__item[aria-disabled="true"] > :not(.aura-menu__reason) { opacity: var(--aura-disabled-opacity); }
235
+ .aura-menu__reason { padding-left: var(--aura-space-4); }
236
+ .aura-menu__item:disabled, .aura-menu__item[aria-disabled="true"]:hover:not(:focus) { background: transparent; }
237
+ .aura-menu:focus, .aura-menu__list:focus { outline: none; }
232
238
  .aura-menu__item .aura-icon { color: var(--aura-fg-secondary); }
233
239
  .aura-menu__label { flex: 1; }
234
240
  .aura-menu__blank { width: var(--aura-icon-sm); flex: none; }
@@ -1205,6 +1211,16 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
1205
1211
  .aura-segmented--sm .aura-segmented__option { height: 24px; padding: 0 var(--aura-space-3); font-size: 12px; }
1206
1212
  .aura-segmented--sm .aura-segmented__option.is-icon { width: 28px; padding: 0; }
1207
1213
  .aura-segmented.is-disabled { opacity: var(--aura-disabled-opacity); }
1214
+ /* 5.10 (Chamber-OS 72): Tabs variant="segmented" — the tablist wears SegmentedControl's track and pills. The focus
1215
+ * ring is drawn inside the pill, so neither the scrolling list (many tabs on a phone) nor the next pill can cover it;
1216
+ * full-width tabs share the track but never shrink below their label (the track scrolls instead). The phone rule's
1217
+ * wider tab gap doesn't apply. */
1218
+ .aura-tabs--segmented .aura-tabs__list.aura-segmented { gap: 2px; }
1219
+ .aura-tabs--segmented .aura-tabs__list.aura-segmented.is-full .aura-tab { min-width: max-content; }
1220
+ .aura-tabs--segmented .aura-tab.aura-segmented__option { margin-bottom: 0; }
1221
+ .aura-tabs--segmented .aura-tab.aura-segmented__option:focus-visible { border-radius: var(--aura-radius-full); outline-offset: calc(-1 * var(--aura-focus-ring-width)); }
1222
+ .aura-tabs--segmented .aura-tab.aura-segmented__option:disabled,
1223
+ .aura-tabs--segmented .aura-tab.aura-segmented__option.is-disabled { opacity: var(--aura-disabled-opacity); cursor: not-allowed; }
1208
1224
 
1209
1225
  /* A field inside a row Stack takes the remaining width (a search box beside its button). */
1210
1226
  .aura-stack > .aura-field { flex: 1 1 auto; min-width: 0; }
@@ -1592,6 +1608,10 @@ button.aura-stepper__step:hover .aura-stepper__label { text-decoration: underlin
1592
1608
  .aura-segmented__option.is-selected { outline: 2px solid Highlight; outline-offset: -2px; }
1593
1609
  .aura-tab { border-bottom-color: Canvas; }
1594
1610
  .aura-tab.is-active { border-bottom-color: Highlight; }
1611
+ /* 5.10: segmented tabs mark the selection with a border and focus with a dashed ring inside it, so a focused tab
1612
+ * and the selected one never look alike (manual activation moves focus without selecting). */
1613
+ .aura-tabs--segmented .aura-tab.aura-segmented__option.is-selected { outline: none; border: 2px solid Highlight; }
1614
+ .aura-tabs--segmented .aura-tab.aura-segmented__option:focus-visible { outline: 2px dashed CanvasText; outline-offset: -5px; }
1595
1615
  }
1596
1616
 
1597
1617
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jirawatpyk/aura-react",
3
- "version": "5.9.0",
3
+ "version": "5.10.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",