@jirawatpyk/aura-react 5.6.0 → 5.7.1

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,19 @@ 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.7.1 — SideNav labels wrap
190
+
191
+ - **SideNav** (item 63): a long label wraps to a second line instead of ending in an ellipsis, so Thai and Swedish names read in full in the 240px nav and the phone drawer. One-line rows stay 36px (44px on touch); a two-line row grows to 44px with its icon centred. Past two lines the label clamps; the accessible name is always complete. The collapsed rail is unchanged.
192
+
193
+ ## 5.7 — Chamber-OS addendum 5 (shell details)
194
+
195
+ - **DropdownMenu `header`**: content above the items, e.g. name, email and role in an account menu. It sits outside `role="menu"` (arrow keys, Home/End skip it; clicking it keeps focus on the item) and is the menu's accessible description (`aria-describedby`). Screen readers differ in whether they speak a menu's description on open; keep the essentials (who is signed in) in the trigger's name too if they must be heard. With a header, the menu's `ref` and `.aura-menu` are the outer box and `role="menu"` is `.aura-menu__list` inside it.
196
+ - **Breadcrumb**: an item with neither `href` nor `onClick` is plain text (`.aura-crumbs__text`), not a button that does nothing.
197
+ - **AppShell** `<main>` has `tabIndex={-1}`, so a skip link or `document.getElementById(mainId)?.focus({ preventScroll: true })` moves focus there when the row you acted on leaves the list. No ring is drawn on the landmark.
198
+ - **Dialog / Drawer `dismissOnScrim`**: `false` keeps it open on a scrim click; Escape and the close button still close it, and focus stays inside. **`role="alertdialog"` now defaults to `false`**, so a stray click outside a confirmation doesn't throw away a typed reason; pass `dismissOnScrim` to get the old behaviour.
199
+ - **BottomNav item `ariaLabel`**: the full name when the visible label is shortened (`label="Konto"`, `ariaLabel="Mitt konto"`); the count or badge words still follow. Include the visible text in it (WCAG 2.5.3).
200
+ - **SideNav** rows are 44px on touch screens (AppShell's phone drawer); 36px with a mouse.
201
+
189
202
  ## 5.6 — Chamber-OS addendum 4 (selectable rows, richer toasts)
190
203
 
191
204
  - **DataTable** `isRowSelectable={(r) => r.status === 'Awaiting review'}`: a row it rejects shows no checkbox (the cell stays, so columns line up), and select-all, Space, the "N selected" count and `onSelectionChange` skip it. A rejected key passed in through `selected` is never shown as selected and is dropped from your state with one `onSelectionChange` call. `rowSelectDisabledLabel={(r) => …}` names the empty cell for screen readers. Server HTML has no checkbox for those rows.
@@ -828,6 +828,8 @@ window.Aura = (() => {
828
828
  const pos = posState[0], setPos = posState[1];
829
829
  const items2 = props.items || [];
830
830
  const mounted = useMounted();
831
+ const headerId = uid();
832
+ const hasHeader = props.header != null && props.header !== false && props.header !== "";
831
833
  const Link = useLinkComponent(props.linkComponent);
832
834
  useIsoLayoutEffect(
833
835
  function() {
@@ -845,7 +847,20 @@ window.Aura = (() => {
845
847
  const left = Math.max(8, Math.min(r.right - mw, window.innerWidth - mw - 8));
846
848
  setPos({ top, left, maxHeight });
847
849
  },
848
- [props.anchor, mounted]
850
+ [props.anchor, mounted, hasHeader]
851
+ );
852
+ const hadHeader = React6.useRef(hasHeader);
853
+ React6.useEffect(
854
+ function() {
855
+ if (hadHeader.current === hasHeader) return;
856
+ hadHeader.current = hasHeader;
857
+ const a = document.activeElement;
858
+ if (own.current && (!a || a === document.body)) {
859
+ const first = own.current.querySelector('[role^="menuitem"]:not([disabled])');
860
+ if (first) first.focus();
861
+ }
862
+ },
863
+ [hasHeader]
849
864
  );
850
865
  React6.useEffect(
851
866
  function() {
@@ -871,22 +886,22 @@ window.Aura = (() => {
871
886
  [mounted]
872
887
  );
873
888
  function onKeyDown(e) {
874
- const list = Array.prototype.slice.call(
889
+ const list2 = Array.prototype.slice.call(
875
890
  own.current.querySelectorAll('[role^="menuitem"]:not([disabled])')
876
891
  );
877
- const i = list.indexOf(document.activeElement);
892
+ const i = list2.indexOf(document.activeElement);
878
893
  if (e.key === "ArrowDown") {
879
894
  e.preventDefault();
880
- list[(i + 1) % list.length].focus();
895
+ list2[(i + 1) % list2.length].focus();
881
896
  } else if (e.key === "ArrowUp") {
882
897
  e.preventDefault();
883
- list[(i - 1 + list.length) % list.length].focus();
898
+ list2[(i - 1 + list2.length) % list2.length].focus();
884
899
  } else if (e.key === "Home") {
885
900
  e.preventDefault();
886
- list[0].focus();
901
+ list2[0].focus();
887
902
  } else if (e.key === "End") {
888
903
  e.preventDefault();
889
- list[list.length - 1].focus();
904
+ list2[list2.length - 1].focus();
890
905
  } else if (e.key === " " && document.activeElement && document.activeElement.tagName === "A") {
891
906
  e.preventDefault();
892
907
  document.activeElement.click();
@@ -898,9 +913,9 @@ window.Aura = (() => {
898
913
  }
899
914
  e.stopPropagation();
900
915
  }
901
- function groupItems(list) {
916
+ function groupItems(list2) {
902
917
  const out = [];
903
- list.forEach(function(it, i) {
918
+ list2.forEach(function(it, i) {
904
919
  const g = it.type === "radio" && it.group ? it.group : null;
905
920
  const last = out[out.length - 1];
906
921
  if (last && last.group === g && g !== null) last.items.push({ it, i });
@@ -955,27 +970,28 @@ window.Aura = (() => {
955
970
  body
956
971
  );
957
972
  }
958
- const el = /* @__PURE__ */ React6.createElement(
973
+ const style = {
974
+ top: pos ? pos.top : -9999,
975
+ left: pos ? pos.left : -9999,
976
+ maxHeight: pos && pos.maxHeight ? pos.maxHeight : void 0
977
+ };
978
+ const list = groupItems(items2).map(function(block, bi) {
979
+ const rendered = block.items.map(function(x) {
980
+ return renderItem(x.it, x.i);
981
+ });
982
+ return block.group ? /* @__PURE__ */ React6.createElement("div", { key: "g" + bi, role: "group", "aria-label": block.group, className: "aura-menu__group" }, rendered) : /* @__PURE__ */ React6.createElement(React6.Fragment, { key: "f" + bi }, rendered);
983
+ });
984
+ const el = hasHeader ? /* @__PURE__ */ React6.createElement("div", { ref: merged, className: "aura-menu has-header", onKeyDown, style }, /* @__PURE__ */ React6.createElement(
959
985
  "div",
960
986
  {
961
- ref: merged,
962
- role: "menu",
963
- "aria-label": props.label,
964
- className: "aura-menu",
965
- onKeyDown,
966
- style: {
967
- top: pos ? pos.top : -9999,
968
- left: pos ? pos.left : -9999,
969
- maxHeight: pos && pos.maxHeight ? pos.maxHeight : void 0
987
+ className: "aura-menu__header",
988
+ id: headerId,
989
+ onMouseDown: function(e) {
990
+ e.preventDefault();
970
991
  }
971
992
  },
972
- groupItems(items2).map(function(block, bi) {
973
- const rendered = block.items.map(function(x) {
974
- return renderItem(x.it, x.i);
975
- });
976
- return block.group ? /* @__PURE__ */ React6.createElement("div", { key: "g" + bi, role: "group", "aria-label": block.group, className: "aura-menu__group" }, rendered) : /* @__PURE__ */ React6.createElement(React6.Fragment, { key: "f" + bi }, rendered);
977
- })
978
- );
993
+ props.header
994
+ ), /* @__PURE__ */ React6.createElement("div", { role: "menu", "aria-label": props.label, "aria-describedby": headerId, className: "aura-menu__list" }, list)) : /* @__PURE__ */ React6.createElement("div", { ref: merged, role: "menu", "aria-label": props.label, className: "aura-menu", onKeyDown, style }, list);
979
995
  return mounted ? (0, import_react_dom.createPortal)(el, document.body) : null;
980
996
  });
981
997
 
@@ -1015,6 +1031,7 @@ window.Aura = (() => {
1015
1031
  label: props.label,
1016
1032
  items: props.items,
1017
1033
  linkComponent: props.linkComponent,
1034
+ header: props.header,
1018
1035
  onClose: function(restore) {
1019
1036
  setAnchor(null);
1020
1037
  if (restore && typeof anchor.focus === "function") anchor.focus();
@@ -4014,6 +4031,7 @@ window.Aura = (() => {
4014
4031
  function close() {
4015
4032
  if (props.dismissible !== false && props.onClose) props.onClose();
4016
4033
  }
4034
+ const scrimCloses = props.dismissible !== false && (props.dismissOnScrim !== void 0 ? props.dismissOnScrim : props.role !== "alertdialog");
4017
4035
  const modal = useModal(props.open, own, {
4018
4036
  autoFocus: props.autoFocus,
4019
4037
  onEscape: close,
@@ -4022,7 +4040,17 @@ window.Aura = (() => {
4022
4040
  });
4023
4041
  if (!modal.ready) return null;
4024
4042
  return (0, import_react_dom8.createPortal)(
4025
- /* @__PURE__ */ React27.createElement("div", { className: "aura-dialog-layer", "data-density": density, onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React27.createElement("div", { className: "aura-scrim", onClick: close, "aria-hidden": true }), /* @__PURE__ */ React27.createElement(
4043
+ /* @__PURE__ */ React27.createElement("div", { className: "aura-dialog-layer", "data-density": density, onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React27.createElement(
4044
+ "div",
4045
+ {
4046
+ className: "aura-scrim",
4047
+ onClick: scrimCloses ? close : void 0,
4048
+ onMouseDown: scrimCloses ? void 0 : function(e) {
4049
+ e.preventDefault();
4050
+ },
4051
+ "aria-hidden": true
4052
+ }
4053
+ ), /* @__PURE__ */ React27.createElement(
4026
4054
  "div",
4027
4055
  {
4028
4056
  ref: merged,
@@ -4056,8 +4084,19 @@ window.Aura = (() => {
4056
4084
  });
4057
4085
  if (!modal.ready) return null;
4058
4086
  const side = props.side === "left" ? "left" : "right";
4087
+ const drawerScrimCloses = props.dismissible !== false && props.dismissOnScrim !== false;
4059
4088
  return (0, import_react_dom8.createPortal)(
4060
- /* @__PURE__ */ React27.createElement("div", { className: "aura-dialog-layer aura-drawer-layer", "data-density": density, onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React27.createElement("div", { className: "aura-scrim", onClick: close, "aria-hidden": true }), /* @__PURE__ */ React27.createElement(
4089
+ /* @__PURE__ */ React27.createElement("div", { className: "aura-dialog-layer aura-drawer-layer", "data-density": density, onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React27.createElement(
4090
+ "div",
4091
+ {
4092
+ className: "aura-scrim",
4093
+ onClick: drawerScrimCloses ? close : void 0,
4094
+ onMouseDown: !drawerScrimCloses ? function(e) {
4095
+ e.preventDefault();
4096
+ } : void 0,
4097
+ "aria-hidden": true
4098
+ }
4099
+ ), /* @__PURE__ */ React27.createElement(
4061
4100
  "div",
4062
4101
  {
4063
4102
  ref: merged,
@@ -5726,7 +5765,10 @@ window.Aura = (() => {
5726
5765
  const items2 = props.items || [];
5727
5766
  return /* @__PURE__ */ React32.createElement("nav", { ref, "aria-label": props.label || t.breadcrumb, className: cx("aura-crumbs", props.className) }, /* @__PURE__ */ React32.createElement("ol", null, items2.map(function(it, i) {
5728
5767
  const last = i === items2.length - 1;
5729
- return /* @__PURE__ */ React32.createElement("li", { key: i }, last ? /* @__PURE__ */ React32.createElement("span", { "aria-current": "page", className: "aura-crumbs__current" }, it.label) : it.href ? /* @__PURE__ */ React32.createElement(Link, { href: it.href, onClick: it.onClick }, it.label) : /* @__PURE__ */ React32.createElement("button", { type: "button", onClick: it.onClick }, it.label), last ? null : /* @__PURE__ */ React32.createElement(Icon, { name: "chevron-right", size: 12, className: "aura-crumbs__sep" }));
5768
+ return /* @__PURE__ */ React32.createElement("li", { key: i }, last ? /* @__PURE__ */ React32.createElement("span", { "aria-current": "page", className: "aura-crumbs__current" }, it.label) : it.href ? /* @__PURE__ */ React32.createElement(Link, { href: it.href, onClick: it.onClick }, it.label) : it.onClick ? /* @__PURE__ */ React32.createElement("button", { type: "button", onClick: it.onClick }, it.label) : (
5769
+ /* 5.7: a segment with no page and no action is text, not a button that does nothing. */
5770
+ /* @__PURE__ */ React32.createElement("span", { className: "aura-crumbs__text" }, it.label)
5771
+ ), last ? null : /* @__PURE__ */ React32.createElement(Icon, { name: "chevron-right", size: 12, className: "aura-crumbs__sep" }));
5730
5772
  })));
5731
5773
  });
5732
5774
 
@@ -5921,7 +5963,7 @@ window.Aura = (() => {
5921
5963
  },
5922
5964
  "aria-expanded": open
5923
5965
  }
5924
- ) : null, /* @__PURE__ */ React38.createElement("div", { className: "aura-shell__bar-content" }, props.header)) : null, /* @__PURE__ */ React38.createElement("main", { className: "aura-shell__content", id: props.mainId || "main" }, props.children), props.bottomNav || null));
5966
+ ) : null, /* @__PURE__ */ React38.createElement("div", { className: "aura-shell__bar-content" }, props.header)) : null, /* @__PURE__ */ React38.createElement("main", { className: "aura-shell__content", id: props.mainId || "main", tabIndex: -1 }, props.children), props.bottomNav || null));
5925
5967
  });
5926
5968
 
5927
5969
  // src/ActionBar.tsx
@@ -6076,8 +6118,11 @@ window.Aura = (() => {
6076
6118
  /* @__PURE__ */ React42.createElement("ul", { className: "aura-bottomnav__list" }, props.items.map(function(it) {
6077
6119
  const on = active === it.id;
6078
6120
  const count = it.count != null && it.count > 0 ? it.count > 99 ? "99+" : String(it.count) : null;
6121
+ const suffix = count || it.badge && it.badgeLabel ? " (" + (count || it.badgeLabel) + ")" : "";
6079
6122
  const common = {
6080
6123
  className: cx("aura-bottomnav__item", on && "is-active"),
6124
+ /* 5.7: a full name when the label is shortened; the count / badge words still follow. */
6125
+ "aria-label": it.ariaLabel ? it.ariaLabel + suffix : void 0,
6081
6126
  "aria-current": on ? "page" : void 0,
6082
6127
  onClick: function(e) {
6083
6128
  if (!it.href) e.preventDefault();
@@ -6088,7 +6133,7 @@ window.Aura = (() => {
6088
6133
  const inner = [
6089
6134
  /* @__PURE__ */ React42.createElement("span", { key: "i", className: "aura-bottomnav__icon" }, /* @__PURE__ */ React42.createElement(Icon, { name: it.icon, size: "md" }), count ? /* @__PURE__ */ React42.createElement("span", { className: "aura-bottomnav__count", "aria-hidden": "true" }, count) : it.badge ? /* @__PURE__ */ React42.createElement("span", { className: "aura-bottomnav__dot", "aria-hidden": "true" }) : null),
6090
6135
  /* @__PURE__ */ React42.createElement("span", { key: "l", className: "aura-bottomnav__label" }, it.label),
6091
- count || it.badge && it.badgeLabel ? /* @__PURE__ */ React42.createElement("span", { key: "s", className: "aura-sr-only" }, " (" + (count || it.badgeLabel) + ")") : null
6136
+ suffix && !it.ariaLabel ? /* @__PURE__ */ React42.createElement("span", { key: "s", className: "aura-sr-only" }, suffix) : null
6092
6137
  ];
6093
6138
  return /* @__PURE__ */ React42.createElement("li", { key: it.id, className: "aura-bottomnav__cell" }, it.href ? /* @__PURE__ */ React42.createElement(Link, { href: it.href, ...common }, inner) : /* @__PURE__ */ React42.createElement("button", { type: "button", ...common }, inner));
6094
6139
  }))
@@ -810,6 +810,8 @@ var Menu = React6.forwardRef(function Menu2(props, ref) {
810
810
  const pos = posState[0], setPos = posState[1];
811
811
  const items2 = props.items || [];
812
812
  const mounted = useMounted();
813
+ const headerId = uid();
814
+ const hasHeader = props.header != null && props.header !== false && props.header !== "";
813
815
  const Link = useLinkComponent(props.linkComponent);
814
816
  useIsoLayoutEffect(
815
817
  function() {
@@ -827,7 +829,20 @@ var Menu = React6.forwardRef(function Menu2(props, ref) {
827
829
  const left = Math.max(8, Math.min(r.right - mw, window.innerWidth - mw - 8));
828
830
  setPos({ top, left, maxHeight });
829
831
  },
830
- [props.anchor, mounted]
832
+ [props.anchor, mounted, hasHeader]
833
+ );
834
+ const hadHeader = React6.useRef(hasHeader);
835
+ React6.useEffect(
836
+ function() {
837
+ if (hadHeader.current === hasHeader) return;
838
+ hadHeader.current = hasHeader;
839
+ const a = document.activeElement;
840
+ if (own.current && (!a || a === document.body)) {
841
+ const first = own.current.querySelector('[role^="menuitem"]:not([disabled])');
842
+ if (first) first.focus();
843
+ }
844
+ },
845
+ [hasHeader]
831
846
  );
832
847
  React6.useEffect(
833
848
  function() {
@@ -853,22 +868,22 @@ var Menu = React6.forwardRef(function Menu2(props, ref) {
853
868
  [mounted]
854
869
  );
855
870
  function onKeyDown(e) {
856
- const list = Array.prototype.slice.call(
871
+ const list2 = Array.prototype.slice.call(
857
872
  own.current.querySelectorAll('[role^="menuitem"]:not([disabled])')
858
873
  );
859
- const i = list.indexOf(document.activeElement);
874
+ const i = list2.indexOf(document.activeElement);
860
875
  if (e.key === "ArrowDown") {
861
876
  e.preventDefault();
862
- list[(i + 1) % list.length].focus();
877
+ list2[(i + 1) % list2.length].focus();
863
878
  } else if (e.key === "ArrowUp") {
864
879
  e.preventDefault();
865
- list[(i - 1 + list.length) % list.length].focus();
880
+ list2[(i - 1 + list2.length) % list2.length].focus();
866
881
  } else if (e.key === "Home") {
867
882
  e.preventDefault();
868
- list[0].focus();
883
+ list2[0].focus();
869
884
  } else if (e.key === "End") {
870
885
  e.preventDefault();
871
- list[list.length - 1].focus();
886
+ list2[list2.length - 1].focus();
872
887
  } else if (e.key === " " && document.activeElement && document.activeElement.tagName === "A") {
873
888
  e.preventDefault();
874
889
  document.activeElement.click();
@@ -880,9 +895,9 @@ var Menu = React6.forwardRef(function Menu2(props, ref) {
880
895
  }
881
896
  e.stopPropagation();
882
897
  }
883
- function groupItems(list) {
898
+ function groupItems(list2) {
884
899
  const out = [];
885
- list.forEach(function(it, i) {
900
+ list2.forEach(function(it, i) {
886
901
  const g = it.type === "radio" && it.group ? it.group : null;
887
902
  const last = out[out.length - 1];
888
903
  if (last && last.group === g && g !== null) last.items.push({ it, i });
@@ -937,27 +952,28 @@ var Menu = React6.forwardRef(function Menu2(props, ref) {
937
952
  body
938
953
  );
939
954
  }
940
- const el = /* @__PURE__ */ React6.createElement(
955
+ const style = {
956
+ top: pos ? pos.top : -9999,
957
+ left: pos ? pos.left : -9999,
958
+ maxHeight: pos && pos.maxHeight ? pos.maxHeight : void 0
959
+ };
960
+ const list = groupItems(items2).map(function(block, bi) {
961
+ const rendered = block.items.map(function(x) {
962
+ return renderItem(x.it, x.i);
963
+ });
964
+ return block.group ? /* @__PURE__ */ React6.createElement("div", { key: "g" + bi, role: "group", "aria-label": block.group, className: "aura-menu__group" }, rendered) : /* @__PURE__ */ React6.createElement(React6.Fragment, { key: "f" + bi }, rendered);
965
+ });
966
+ const el = hasHeader ? /* @__PURE__ */ React6.createElement("div", { ref: merged, className: "aura-menu has-header", onKeyDown, style }, /* @__PURE__ */ React6.createElement(
941
967
  "div",
942
968
  {
943
- ref: merged,
944
- role: "menu",
945
- "aria-label": props.label,
946
- className: "aura-menu",
947
- onKeyDown,
948
- style: {
949
- top: pos ? pos.top : -9999,
950
- left: pos ? pos.left : -9999,
951
- maxHeight: pos && pos.maxHeight ? pos.maxHeight : void 0
969
+ className: "aura-menu__header",
970
+ id: headerId,
971
+ onMouseDown: function(e) {
972
+ e.preventDefault();
952
973
  }
953
974
  },
954
- groupItems(items2).map(function(block, bi) {
955
- const rendered = block.items.map(function(x) {
956
- return renderItem(x.it, x.i);
957
- });
958
- return block.group ? /* @__PURE__ */ React6.createElement("div", { key: "g" + bi, role: "group", "aria-label": block.group, className: "aura-menu__group" }, rendered) : /* @__PURE__ */ React6.createElement(React6.Fragment, { key: "f" + bi }, rendered);
959
- })
960
- );
975
+ props.header
976
+ ), /* @__PURE__ */ React6.createElement("div", { role: "menu", "aria-label": props.label, "aria-describedby": headerId, className: "aura-menu__list" }, list)) : /* @__PURE__ */ React6.createElement("div", { ref: merged, role: "menu", "aria-label": props.label, className: "aura-menu", onKeyDown, style }, list);
961
977
  return mounted ? (0, import_react_dom.createPortal)(el, document.body) : null;
962
978
  });
963
979
 
@@ -997,6 +1013,7 @@ var DropdownMenu = React7.forwardRef(function DropdownMenu2(props, ref) {
997
1013
  label: props.label,
998
1014
  items: props.items,
999
1015
  linkComponent: props.linkComponent,
1016
+ header: props.header,
1000
1017
  onClose: function(restore) {
1001
1018
  setAnchor(null);
1002
1019
  if (restore && typeof anchor.focus === "function") anchor.focus();
@@ -3996,6 +4013,7 @@ var Dialog = React27.forwardRef(function Dialog2(props, ref) {
3996
4013
  function close() {
3997
4014
  if (props.dismissible !== false && props.onClose) props.onClose();
3998
4015
  }
4016
+ const scrimCloses = props.dismissible !== false && (props.dismissOnScrim !== void 0 ? props.dismissOnScrim : props.role !== "alertdialog");
3999
4017
  const modal = useModal(props.open, own, {
4000
4018
  autoFocus: props.autoFocus,
4001
4019
  onEscape: close,
@@ -4004,7 +4022,17 @@ var Dialog = React27.forwardRef(function Dialog2(props, ref) {
4004
4022
  });
4005
4023
  if (!modal.ready) return null;
4006
4024
  return (0, import_react_dom8.createPortal)(
4007
- /* @__PURE__ */ React27.createElement("div", { className: "aura-dialog-layer", "data-density": density, onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React27.createElement("div", { className: "aura-scrim", onClick: close, "aria-hidden": true }), /* @__PURE__ */ React27.createElement(
4025
+ /* @__PURE__ */ React27.createElement("div", { className: "aura-dialog-layer", "data-density": density, onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React27.createElement(
4026
+ "div",
4027
+ {
4028
+ className: "aura-scrim",
4029
+ onClick: scrimCloses ? close : void 0,
4030
+ onMouseDown: scrimCloses ? void 0 : function(e) {
4031
+ e.preventDefault();
4032
+ },
4033
+ "aria-hidden": true
4034
+ }
4035
+ ), /* @__PURE__ */ React27.createElement(
4008
4036
  "div",
4009
4037
  {
4010
4038
  ref: merged,
@@ -4038,8 +4066,19 @@ var Drawer = React27.forwardRef(function Drawer2(props, ref) {
4038
4066
  });
4039
4067
  if (!modal.ready) return null;
4040
4068
  const side = props.side === "left" ? "left" : "right";
4069
+ const drawerScrimCloses = props.dismissible !== false && props.dismissOnScrim !== false;
4041
4070
  return (0, import_react_dom8.createPortal)(
4042
- /* @__PURE__ */ React27.createElement("div", { className: "aura-dialog-layer aura-drawer-layer", "data-density": density, onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React27.createElement("div", { className: "aura-scrim", onClick: close, "aria-hidden": true }), /* @__PURE__ */ React27.createElement(
4071
+ /* @__PURE__ */ React27.createElement("div", { className: "aura-dialog-layer aura-drawer-layer", "data-density": density, onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React27.createElement(
4072
+ "div",
4073
+ {
4074
+ className: "aura-scrim",
4075
+ onClick: drawerScrimCloses ? close : void 0,
4076
+ onMouseDown: !drawerScrimCloses ? function(e) {
4077
+ e.preventDefault();
4078
+ } : void 0,
4079
+ "aria-hidden": true
4080
+ }
4081
+ ), /* @__PURE__ */ React27.createElement(
4043
4082
  "div",
4044
4083
  {
4045
4084
  ref: merged,
@@ -5708,7 +5747,10 @@ var Breadcrumb = React32.forwardRef(function Breadcrumb2(props, ref) {
5708
5747
  const items2 = props.items || [];
5709
5748
  return /* @__PURE__ */ React32.createElement("nav", { ref, "aria-label": props.label || t.breadcrumb, className: cx("aura-crumbs", props.className) }, /* @__PURE__ */ React32.createElement("ol", null, items2.map(function(it, i) {
5710
5749
  const last = i === items2.length - 1;
5711
- return /* @__PURE__ */ React32.createElement("li", { key: i }, last ? /* @__PURE__ */ React32.createElement("span", { "aria-current": "page", className: "aura-crumbs__current" }, it.label) : it.href ? /* @__PURE__ */ React32.createElement(Link, { href: it.href, onClick: it.onClick }, it.label) : /* @__PURE__ */ React32.createElement("button", { type: "button", onClick: it.onClick }, it.label), last ? null : /* @__PURE__ */ React32.createElement(Icon, { name: "chevron-right", size: 12, className: "aura-crumbs__sep" }));
5750
+ return /* @__PURE__ */ React32.createElement("li", { key: i }, last ? /* @__PURE__ */ React32.createElement("span", { "aria-current": "page", className: "aura-crumbs__current" }, it.label) : it.href ? /* @__PURE__ */ React32.createElement(Link, { href: it.href, onClick: it.onClick }, it.label) : it.onClick ? /* @__PURE__ */ React32.createElement("button", { type: "button", onClick: it.onClick }, it.label) : (
5751
+ /* 5.7: a segment with no page and no action is text, not a button that does nothing. */
5752
+ /* @__PURE__ */ React32.createElement("span", { className: "aura-crumbs__text" }, it.label)
5753
+ ), last ? null : /* @__PURE__ */ React32.createElement(Icon, { name: "chevron-right", size: 12, className: "aura-crumbs__sep" }));
5712
5754
  })));
5713
5755
  });
5714
5756
 
@@ -5903,7 +5945,7 @@ var AppShell = React38.forwardRef(function AppShell2(props, ref) {
5903
5945
  },
5904
5946
  "aria-expanded": open
5905
5947
  }
5906
- ) : null, /* @__PURE__ */ React38.createElement("div", { className: "aura-shell__bar-content" }, props.header)) : null, /* @__PURE__ */ React38.createElement("main", { className: "aura-shell__content", id: props.mainId || "main" }, props.children), props.bottomNav || null));
5948
+ ) : null, /* @__PURE__ */ React38.createElement("div", { className: "aura-shell__bar-content" }, props.header)) : null, /* @__PURE__ */ React38.createElement("main", { className: "aura-shell__content", id: props.mainId || "main", tabIndex: -1 }, props.children), props.bottomNav || null));
5907
5949
  });
5908
5950
 
5909
5951
  // src/ActionBar.tsx
@@ -6058,8 +6100,11 @@ var BottomNav = React42.forwardRef(function BottomNav2(props, ref) {
6058
6100
  /* @__PURE__ */ React42.createElement("ul", { className: "aura-bottomnav__list" }, props.items.map(function(it) {
6059
6101
  const on = active === it.id;
6060
6102
  const count = it.count != null && it.count > 0 ? it.count > 99 ? "99+" : String(it.count) : null;
6103
+ const suffix = count || it.badge && it.badgeLabel ? " (" + (count || it.badgeLabel) + ")" : "";
6061
6104
  const common = {
6062
6105
  className: cx("aura-bottomnav__item", on && "is-active"),
6106
+ /* 5.7: a full name when the label is shortened; the count / badge words still follow. */
6107
+ "aria-label": it.ariaLabel ? it.ariaLabel + suffix : void 0,
6063
6108
  "aria-current": on ? "page" : void 0,
6064
6109
  onClick: function(e) {
6065
6110
  if (!it.href) e.preventDefault();
@@ -6070,7 +6115,7 @@ var BottomNav = React42.forwardRef(function BottomNav2(props, ref) {
6070
6115
  const inner = [
6071
6116
  /* @__PURE__ */ React42.createElement("span", { key: "i", className: "aura-bottomnav__icon" }, /* @__PURE__ */ React42.createElement(Icon, { name: it.icon, size: "md" }), count ? /* @__PURE__ */ React42.createElement("span", { className: "aura-bottomnav__count", "aria-hidden": "true" }, count) : it.badge ? /* @__PURE__ */ React42.createElement("span", { className: "aura-bottomnav__dot", "aria-hidden": "true" }) : null),
6072
6117
  /* @__PURE__ */ React42.createElement("span", { key: "l", className: "aura-bottomnav__label" }, it.label),
6073
- count || it.badge && it.badgeLabel ? /* @__PURE__ */ React42.createElement("span", { key: "s", className: "aura-sr-only" }, " (" + (count || it.badgeLabel) + ")") : null
6118
+ suffix && !it.ariaLabel ? /* @__PURE__ */ React42.createElement("span", { key: "s", className: "aura-sr-only" }, suffix) : null
6074
6119
  ];
6075
6120
  return /* @__PURE__ */ React42.createElement("li", { key: it.id, className: "aura-bottomnav__cell" }, it.href ? /* @__PURE__ */ React42.createElement(Link, { href: it.href, ...common }, inner) : /* @__PURE__ */ React42.createElement("button", { type: "button", ...common }, inner));
6076
6121
  }))
@@ -59,7 +59,7 @@ const AppShell = React.forwardRef(function AppShell2(props, ref) {
59
59
  },
60
60
  "aria-expanded": open
61
61
  }
62
- ) : null, /* @__PURE__ */ React.createElement("div", { className: "aura-shell__bar-content" }, props.header)) : null, /* @__PURE__ */ React.createElement("main", { className: "aura-shell__content", id: props.mainId || "main" }, props.children), props.bottomNav || null));
62
+ ) : null, /* @__PURE__ */ React.createElement("div", { className: "aura-shell__bar-content" }, props.header)) : null, /* @__PURE__ */ React.createElement("main", { className: "aura-shell__content", id: props.mainId || "main", tabIndex: -1 }, props.children), props.bottomNav || null));
63
63
  });
64
64
  export {
65
65
  AppShell
@@ -23,8 +23,11 @@ const BottomNav = React.forwardRef(function BottomNav2(props, ref) {
23
23
  /* @__PURE__ */ React.createElement("ul", { className: "aura-bottomnav__list" }, props.items.map(function(it) {
24
24
  const on = active === it.id;
25
25
  const count = it.count != null && it.count > 0 ? it.count > 99 ? "99+" : String(it.count) : null;
26
+ const suffix = count || it.badge && it.badgeLabel ? " (" + (count || it.badgeLabel) + ")" : "";
26
27
  const common = {
27
28
  className: cx("aura-bottomnav__item", on && "is-active"),
29
+ /* 5.7: a full name when the label is shortened; the count / badge words still follow. */
30
+ "aria-label": it.ariaLabel ? it.ariaLabel + suffix : void 0,
28
31
  "aria-current": on ? "page" : void 0,
29
32
  onClick: function(e) {
30
33
  if (!it.href) e.preventDefault();
@@ -35,7 +38,7 @@ const BottomNav = React.forwardRef(function BottomNav2(props, ref) {
35
38
  const inner = [
36
39
  /* @__PURE__ */ React.createElement("span", { key: "i", className: "aura-bottomnav__icon" }, /* @__PURE__ */ React.createElement(Icon, { name: it.icon, size: "md" }), count ? /* @__PURE__ */ React.createElement("span", { className: "aura-bottomnav__count", "aria-hidden": "true" }, count) : it.badge ? /* @__PURE__ */ React.createElement("span", { className: "aura-bottomnav__dot", "aria-hidden": "true" }) : null),
37
40
  /* @__PURE__ */ React.createElement("span", { key: "l", className: "aura-bottomnav__label" }, it.label),
38
- count || it.badge && it.badgeLabel ? /* @__PURE__ */ React.createElement("span", { key: "s", className: "aura-sr-only" }, " (" + (count || it.badgeLabel) + ")") : null
41
+ suffix && !it.ariaLabel ? /* @__PURE__ */ React.createElement("span", { key: "s", className: "aura-sr-only" }, suffix) : null
39
42
  ];
40
43
  return /* @__PURE__ */ React.createElement("li", { key: it.id, className: "aura-bottomnav__cell" }, it.href ? /* @__PURE__ */ React.createElement(Link, { href: it.href, ...common }, inner) : /* @__PURE__ */ React.createElement("button", { type: "button", ...common }, inner));
41
44
  }))
@@ -9,7 +9,10 @@ const Breadcrumb = React.forwardRef(function Breadcrumb2(props, ref) {
9
9
  const items = props.items || [];
10
10
  return /* @__PURE__ */ React.createElement("nav", { ref, "aria-label": props.label || t.breadcrumb, className: cx("aura-crumbs", props.className) }, /* @__PURE__ */ React.createElement("ol", null, items.map(function(it, i) {
11
11
  const last = i === items.length - 1;
12
- return /* @__PURE__ */ React.createElement("li", { key: i }, last ? /* @__PURE__ */ React.createElement("span", { "aria-current": "page", className: "aura-crumbs__current" }, it.label) : it.href ? /* @__PURE__ */ React.createElement(Link, { href: it.href, onClick: it.onClick }, it.label) : /* @__PURE__ */ React.createElement("button", { type: "button", onClick: it.onClick }, it.label), last ? null : /* @__PURE__ */ React.createElement(Icon, { name: "chevron-right", size: 12, className: "aura-crumbs__sep" }));
12
+ return /* @__PURE__ */ React.createElement("li", { key: i }, last ? /* @__PURE__ */ React.createElement("span", { "aria-current": "page", className: "aura-crumbs__current" }, it.label) : it.href ? /* @__PURE__ */ React.createElement(Link, { href: it.href, onClick: it.onClick }, it.label) : it.onClick ? /* @__PURE__ */ React.createElement("button", { type: "button", onClick: it.onClick }, it.label) : (
13
+ /* 5.7: a segment with no page and no action is text, not a button that does nothing. */
14
+ /* @__PURE__ */ React.createElement("span", { className: "aura-crumbs__text" }, it.label)
15
+ ), last ? null : /* @__PURE__ */ React.createElement(Icon, { name: "chevron-right", size: 12, className: "aura-crumbs__sep" }));
13
16
  })));
14
17
  });
15
18
  export {
@@ -12,6 +12,7 @@ const Dialog = React.forwardRef(function Dialog2(props, ref) {
12
12
  function close() {
13
13
  if (props.dismissible !== false && props.onClose) props.onClose();
14
14
  }
15
+ const scrimCloses = props.dismissible !== false && (props.dismissOnScrim !== void 0 ? props.dismissOnScrim : props.role !== "alertdialog");
15
16
  const modal = useModal(props.open, own, {
16
17
  autoFocus: props.autoFocus,
17
18
  onEscape: close,
@@ -20,7 +21,17 @@ const Dialog = React.forwardRef(function Dialog2(props, ref) {
20
21
  });
21
22
  if (!modal.ready) return null;
22
23
  return createPortal(
23
- /* @__PURE__ */ React.createElement("div", { className: "aura-dialog-layer", "data-density": density, onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React.createElement("div", { className: "aura-scrim", onClick: close, "aria-hidden": true }), /* @__PURE__ */ React.createElement(
24
+ /* @__PURE__ */ React.createElement("div", { className: "aura-dialog-layer", "data-density": density, onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React.createElement(
25
+ "div",
26
+ {
27
+ className: "aura-scrim",
28
+ onClick: scrimCloses ? close : void 0,
29
+ onMouseDown: scrimCloses ? void 0 : function(e) {
30
+ e.preventDefault();
31
+ },
32
+ "aria-hidden": true
33
+ }
34
+ ), /* @__PURE__ */ React.createElement(
24
35
  "div",
25
36
  {
26
37
  ref: merged,
@@ -54,8 +65,19 @@ const Drawer = React.forwardRef(function Drawer2(props, ref) {
54
65
  });
55
66
  if (!modal.ready) return null;
56
67
  const side = props.side === "left" ? "left" : "right";
68
+ const drawerScrimCloses = props.dismissible !== false && props.dismissOnScrim !== false;
57
69
  return createPortal(
58
- /* @__PURE__ */ React.createElement("div", { className: "aura-dialog-layer aura-drawer-layer", "data-density": density, onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React.createElement("div", { className: "aura-scrim", onClick: close, "aria-hidden": true }), /* @__PURE__ */ React.createElement(
70
+ /* @__PURE__ */ React.createElement("div", { className: "aura-dialog-layer aura-drawer-layer", "data-density": density, onKeyDown: modal.onKeyDown }, /* @__PURE__ */ React.createElement(
71
+ "div",
72
+ {
73
+ className: "aura-scrim",
74
+ onClick: drawerScrimCloses ? close : void 0,
75
+ onMouseDown: !drawerScrimCloses ? function(e) {
76
+ e.preventDefault();
77
+ } : void 0,
78
+ "aria-hidden": true
79
+ }
80
+ ), /* @__PURE__ */ React.createElement(
59
81
  "div",
60
82
  {
61
83
  ref: merged,
@@ -36,6 +36,7 @@ const DropdownMenu = React.forwardRef(function DropdownMenu2(props, ref) {
36
36
  label: props.label,
37
37
  items: props.items,
38
38
  linkComponent: props.linkComponent,
39
+ header: props.header,
39
40
  onClose: function(restore) {
40
41
  setAnchor(null);
41
42
  if (restore && typeof anchor.focus === "function") anchor.focus();
package/dist/esm/Menu.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import * as React from "react";
3
3
  import { createPortal } from "react-dom";
4
- import { cx, useIsoLayoutEffect, useMounted, useMergedRef } from "./internal.js";
4
+ import { cx, uid, useIsoLayoutEffect, useMounted, useMergedRef } from "./internal.js";
5
5
  import { Icon } from "./Icon.js";
6
6
  import { useLinkComponent } from "./locale.js";
7
7
  const Menu = React.forwardRef(function Menu2(props, ref) {
@@ -10,6 +10,8 @@ const Menu = React.forwardRef(function Menu2(props, ref) {
10
10
  const pos = posState[0], setPos = posState[1];
11
11
  const items = props.items || [];
12
12
  const mounted = useMounted();
13
+ const headerId = uid();
14
+ const hasHeader = props.header != null && props.header !== false && props.header !== "";
13
15
  const Link = useLinkComponent(props.linkComponent);
14
16
  useIsoLayoutEffect(
15
17
  function() {
@@ -27,7 +29,20 @@ const Menu = React.forwardRef(function Menu2(props, ref) {
27
29
  const left = Math.max(8, Math.min(r.right - mw, window.innerWidth - mw - 8));
28
30
  setPos({ top, left, maxHeight });
29
31
  },
30
- [props.anchor, mounted]
32
+ [props.anchor, mounted, hasHeader]
33
+ );
34
+ const hadHeader = React.useRef(hasHeader);
35
+ React.useEffect(
36
+ function() {
37
+ if (hadHeader.current === hasHeader) return;
38
+ hadHeader.current = hasHeader;
39
+ const a = document.activeElement;
40
+ if (own.current && (!a || a === document.body)) {
41
+ const first = own.current.querySelector('[role^="menuitem"]:not([disabled])');
42
+ if (first) first.focus();
43
+ }
44
+ },
45
+ [hasHeader]
31
46
  );
32
47
  React.useEffect(
33
48
  function() {
@@ -53,22 +68,22 @@ const Menu = React.forwardRef(function Menu2(props, ref) {
53
68
  [mounted]
54
69
  );
55
70
  function onKeyDown(e) {
56
- const list = Array.prototype.slice.call(
71
+ const list2 = Array.prototype.slice.call(
57
72
  own.current.querySelectorAll('[role^="menuitem"]:not([disabled])')
58
73
  );
59
- const i = list.indexOf(document.activeElement);
74
+ const i = list2.indexOf(document.activeElement);
60
75
  if (e.key === "ArrowDown") {
61
76
  e.preventDefault();
62
- list[(i + 1) % list.length].focus();
77
+ list2[(i + 1) % list2.length].focus();
63
78
  } else if (e.key === "ArrowUp") {
64
79
  e.preventDefault();
65
- list[(i - 1 + list.length) % list.length].focus();
80
+ list2[(i - 1 + list2.length) % list2.length].focus();
66
81
  } else if (e.key === "Home") {
67
82
  e.preventDefault();
68
- list[0].focus();
83
+ list2[0].focus();
69
84
  } else if (e.key === "End") {
70
85
  e.preventDefault();
71
- list[list.length - 1].focus();
86
+ list2[list2.length - 1].focus();
72
87
  } else if (e.key === " " && document.activeElement && document.activeElement.tagName === "A") {
73
88
  e.preventDefault();
74
89
  document.activeElement.click();
@@ -80,9 +95,9 @@ const Menu = React.forwardRef(function Menu2(props, ref) {
80
95
  }
81
96
  e.stopPropagation();
82
97
  }
83
- function groupItems(list) {
98
+ function groupItems(list2) {
84
99
  const out = [];
85
- list.forEach(function(it, i) {
100
+ list2.forEach(function(it, i) {
86
101
  const g = it.type === "radio" && it.group ? it.group : null;
87
102
  const last = out[out.length - 1];
88
103
  if (last && last.group === g && g !== null) last.items.push({ it, i });
@@ -137,27 +152,28 @@ const Menu = React.forwardRef(function Menu2(props, ref) {
137
152
  body
138
153
  );
139
154
  }
140
- const el = /* @__PURE__ */ React.createElement(
155
+ const style = {
156
+ top: pos ? pos.top : -9999,
157
+ left: pos ? pos.left : -9999,
158
+ maxHeight: pos && pos.maxHeight ? pos.maxHeight : void 0
159
+ };
160
+ const list = groupItems(items).map(function(block, bi) {
161
+ const rendered = block.items.map(function(x) {
162
+ return renderItem(x.it, x.i);
163
+ });
164
+ return block.group ? /* @__PURE__ */ React.createElement("div", { key: "g" + bi, role: "group", "aria-label": block.group, className: "aura-menu__group" }, rendered) : /* @__PURE__ */ React.createElement(React.Fragment, { key: "f" + bi }, rendered);
165
+ });
166
+ const el = hasHeader ? /* @__PURE__ */ React.createElement("div", { ref: merged, className: "aura-menu has-header", onKeyDown, style }, /* @__PURE__ */ React.createElement(
141
167
  "div",
142
168
  {
143
- ref: merged,
144
- role: "menu",
145
- "aria-label": props.label,
146
- className: "aura-menu",
147
- onKeyDown,
148
- style: {
149
- top: pos ? pos.top : -9999,
150
- left: pos ? pos.left : -9999,
151
- maxHeight: pos && pos.maxHeight ? pos.maxHeight : void 0
169
+ className: "aura-menu__header",
170
+ id: headerId,
171
+ onMouseDown: function(e) {
172
+ e.preventDefault();
152
173
  }
153
174
  },
154
- groupItems(items).map(function(block, bi) {
155
- const rendered = block.items.map(function(x) {
156
- return renderItem(x.it, x.i);
157
- });
158
- return block.group ? /* @__PURE__ */ React.createElement("div", { key: "g" + bi, role: "group", "aria-label": block.group, className: "aura-menu__group" }, rendered) : /* @__PURE__ */ React.createElement(React.Fragment, { key: "f" + bi }, rendered);
159
- })
160
- );
175
+ props.header
176
+ ), /* @__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);
161
177
  return mounted ? createPortal(el, document.body) : null;
162
178
  });
163
179
  export {
package/dist/index.d.cts CHANGED
@@ -310,6 +310,9 @@ export interface MenuProps {
310
310
  autoFocus?: boolean | undefined;
311
311
  /** Router link for items with `href`; defaults to AuraProvider's `linkComponent`, then `<a>`. (4.19) */
312
312
  linkComponent?: React$1.ElementType | undefined;
313
+ /** Content above the items (5.7), e.g. who is signed in. Not an item: arrow keys skip it; it is the menu's
314
+ * accessible description, read once when the menu opens. */
315
+ header?: React$1.ReactNode | undefined;
313
316
  }
314
317
  export interface FieldProps {
315
318
  /** Visible label; also the accessible name. */
@@ -547,6 +550,9 @@ export interface DialogProps {
547
550
  size?: "sm" | "md" | "lg" | undefined;
548
551
  /** Default true. False hides the close button and ignores Escape and scrim clicks. */
549
552
  dismissible?: boolean | undefined;
553
+ /** Whether a click on the scrim closes it (5.7). Default true, false for `role="alertdialog"`: a stray click
554
+ * outside shouldn't throw away a typed reason. Escape and the close button still close it. */
555
+ dismissOnScrim?: boolean | undefined;
550
556
  /** Use `alertdialog` for destructive confirmations. */
551
557
  role?: "dialog" | "alertdialog" | undefined;
552
558
  /** Default true. Turn off only for static demos. */
@@ -638,6 +644,7 @@ export interface SideNavProps {
638
644
  }
639
645
  export interface BreadcrumbProps {
640
646
  /** Root first; the last item is the current page. */
647
+ /** An item with neither `href` nor `onClick` is plain text (5.7), for a segment with no page of its own. */
641
648
  items: Array<{
642
649
  label: string;
643
650
  href?: string | undefined;
@@ -665,6 +672,9 @@ export interface DropdownMenuProps {
665
672
  label?: string | undefined;
666
673
  /** Router link for items with `href`. (4.19) */
667
674
  linkComponent?: React$1.ElementType | undefined;
675
+ /** Content above the items (5.7), e.g. name, email and role in an account menu. Not an item: arrow keys skip
676
+ * it; it is the menu's accessible description (`aria-describedby`). */
677
+ header?: React$1.ReactNode | undefined;
668
678
  }
669
679
  export interface ComboboxOption {
670
680
  value: string;
@@ -865,6 +875,8 @@ export interface DrawerProps {
865
875
  footer?: React$1.ReactNode | undefined;
866
876
  /** Scrim click and Escape close it. Default true. */
867
877
  dismissible?: boolean | undefined;
878
+ /** Whether a click on the scrim closes it (5.7). Default true; Escape and the close button still close it. */
879
+ dismissOnScrim?: boolean | undefined;
868
880
  /** Default true: focus moves to the element with `data-autofocus`, else the first control in the body. False only for static demos. */
869
881
  autoFocus?: boolean | undefined;
870
882
  /** Needed when there is no title. */
@@ -905,7 +917,8 @@ export interface AppShellProps {
905
917
  children?: React$1.ReactNode | undefined;
906
918
  navLabel?: string | undefined;
907
919
  menuLabel?: string | undefined;
908
- /** id of `<main>`, for a skip link. Default `main`. */
920
+ /** id of `<main>`, for a skip link. Default `main`. `<main>` has `tabIndex={-1}` (5.7), so a skip link or
921
+ * `document.getElementById(mainId).focus()` moves focus there (no ring is drawn on it). */
909
922
  mainId?: string | undefined;
910
923
  /** A BottomNav for phones (4.19). Shown below its `hideFrom` breakpoint; the content keeps room for it, and a
911
924
  * viewport ActionBar sits on top of it. With a `bottomNav` and no `nav`, there is no menu button. */
@@ -979,6 +992,9 @@ export interface BottomNavItem {
979
992
  badge?: boolean | undefined;
980
993
  /** Read after the label when `badge` is set, e.g. "new". */
981
994
  badgeLabel?: string | undefined;
995
+ /** The full name for screen readers when `label` is shortened to fit (5.7), e.g. label "Konto", ariaLabel
996
+ * "Mitt konto". Include the visible label in it (WCAG 2.5.3). The count or badge is still appended. */
997
+ ariaLabel?: string | undefined;
982
998
  }
983
999
  export interface BottomNavProps {
984
1000
  /** 2–5 items. */
package/dist/index.d.ts CHANGED
@@ -310,6 +310,9 @@ export interface MenuProps {
310
310
  autoFocus?: boolean | undefined;
311
311
  /** Router link for items with `href`; defaults to AuraProvider's `linkComponent`, then `<a>`. (4.19) */
312
312
  linkComponent?: React$1.ElementType | undefined;
313
+ /** Content above the items (5.7), e.g. who is signed in. Not an item: arrow keys skip it; it is the menu's
314
+ * accessible description, read once when the menu opens. */
315
+ header?: React$1.ReactNode | undefined;
313
316
  }
314
317
  export interface FieldProps {
315
318
  /** Visible label; also the accessible name. */
@@ -547,6 +550,9 @@ export interface DialogProps {
547
550
  size?: "sm" | "md" | "lg" | undefined;
548
551
  /** Default true. False hides the close button and ignores Escape and scrim clicks. */
549
552
  dismissible?: boolean | undefined;
553
+ /** Whether a click on the scrim closes it (5.7). Default true, false for `role="alertdialog"`: a stray click
554
+ * outside shouldn't throw away a typed reason. Escape and the close button still close it. */
555
+ dismissOnScrim?: boolean | undefined;
550
556
  /** Use `alertdialog` for destructive confirmations. */
551
557
  role?: "dialog" | "alertdialog" | undefined;
552
558
  /** Default true. Turn off only for static demos. */
@@ -638,6 +644,7 @@ export interface SideNavProps {
638
644
  }
639
645
  export interface BreadcrumbProps {
640
646
  /** Root first; the last item is the current page. */
647
+ /** An item with neither `href` nor `onClick` is plain text (5.7), for a segment with no page of its own. */
641
648
  items: Array<{
642
649
  label: string;
643
650
  href?: string | undefined;
@@ -665,6 +672,9 @@ export interface DropdownMenuProps {
665
672
  label?: string | undefined;
666
673
  /** Router link for items with `href`. (4.19) */
667
674
  linkComponent?: React$1.ElementType | undefined;
675
+ /** Content above the items (5.7), e.g. name, email and role in an account menu. Not an item: arrow keys skip
676
+ * it; it is the menu's accessible description (`aria-describedby`). */
677
+ header?: React$1.ReactNode | undefined;
668
678
  }
669
679
  export interface ComboboxOption {
670
680
  value: string;
@@ -865,6 +875,8 @@ export interface DrawerProps {
865
875
  footer?: React$1.ReactNode | undefined;
866
876
  /** Scrim click and Escape close it. Default true. */
867
877
  dismissible?: boolean | undefined;
878
+ /** Whether a click on the scrim closes it (5.7). Default true; Escape and the close button still close it. */
879
+ dismissOnScrim?: boolean | undefined;
868
880
  /** Default true: focus moves to the element with `data-autofocus`, else the first control in the body. False only for static demos. */
869
881
  autoFocus?: boolean | undefined;
870
882
  /** Needed when there is no title. */
@@ -905,7 +917,8 @@ export interface AppShellProps {
905
917
  children?: React$1.ReactNode | undefined;
906
918
  navLabel?: string | undefined;
907
919
  menuLabel?: string | undefined;
908
- /** id of `<main>`, for a skip link. Default `main`. */
920
+ /** id of `<main>`, for a skip link. Default `main`. `<main>` has `tabIndex={-1}` (5.7), so a skip link or
921
+ * `document.getElementById(mainId).focus()` moves focus there (no ring is drawn on it). */
909
922
  mainId?: string | undefined;
910
923
  /** A BottomNav for phones (4.19). Shown below its `hideFrom` breakpoint; the content keeps room for it, and a
911
924
  * viewport ActionBar sits on top of it. With a `bottomNav` and no `nav`, there is no menu button. */
@@ -979,6 +992,9 @@ export interface BottomNavItem {
979
992
  badge?: boolean | undefined;
980
993
  /** Read after the label when `badge` is set, e.g. "new". */
981
994
  badgeLabel?: string | undefined;
995
+ /** The full name for screen readers when `label` is shortened to fit (5.7), e.g. label "Konto", ariaLabel
996
+ * "Mitt konto". Include the visible label in it (WCAG 2.5.3). The count or badge is still appended. */
997
+ ariaLabel?: string | undefined;
982
998
  }
983
999
  export interface BottomNavProps {
984
1000
  /** 2–5 items. */
package/dist/styles.css CHANGED
@@ -247,6 +247,12 @@ a.aura-menu__item { text-decoration: none; box-sizing: border-box; }
247
247
  }
248
248
  .aura-menu__radio.is-on { border-color: var(--aura-button-primary-bg); }
249
249
  .aura-menu__radio.is-on::after { content: ''; width: 8px; height: 8px; border-radius: var(--aura-radius-full); background: var(--aura-button-primary-bg); }
250
+ /* 5.7: a header above the items (who is signed in), set off by a hairline like a separator. */
251
+ .aura-menu__header { margin: calc(-1 * var(--aura-space-1)) calc(-1 * var(--aura-space-1)) var(--aura-space-1); padding: var(--aura-space-3); border-bottom: 1px solid var(--aura-border-default); color: var(--aura-fg-secondary); font-size: 12px; line-height: 18px; overflow-wrap: anywhere; }
252
+ .aura-menu__header > * { margin: 0; }
253
+ /* Stays in view when a long menu scrolls. */
254
+ .aura-menu.has-header .aura-menu__header { position: sticky; top: calc(-1 * var(--aura-space-1)); z-index: 1; background: var(--aura-bg-surface); }
255
+ .aura-menu__header strong, .aura-menu__header b { color: var(--aura-fg-primary); font-weight: 500; font-size: 13px; }
250
256
  .aura-menu__sep { height: 1px; margin: var(--aura-space-1) calc(-1 * var(--aura-space-1)); background: var(--aura-border-default); }
251
257
 
252
258
  /* Footer + pager */
@@ -635,7 +641,7 @@ a.aura-tab { text-decoration: none; }
635
641
  .aura-nav__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
636
642
  .aura-nav__item {
637
643
  box-sizing: border-box; display: flex; align-items: center; gap: var(--aura-space-3); width: 100%;
638
- height: 36px; padding: 0 var(--aura-space-3); border: 0; border-radius: var(--aura-radius-sm);
644
+ min-height: 36px; padding: var(--aura-space-1) var(--aura-space-3); border: 0; border-radius: var(--aura-radius-sm);
639
645
  background: transparent; color: var(--aura-fg-secondary); font: inherit; font-size: 13px; font-weight: 500;
640
646
  text-decoration: none; text-align: left; cursor: pointer;
641
647
  transition: background var(--aura-duration-fast) var(--aura-ease), color var(--aura-duration-fast) var(--aura-ease);
@@ -644,7 +650,14 @@ a.aura-tab { text-decoration: none; }
644
650
  .aura-nav__item.is-active { background: var(--aura-bg-selected); color: var(--aura-fg-primary); }
645
651
  .aura-nav__item.is-active .aura-icon { color: var(--aura-fg-accent); }
646
652
  .aura-nav__item:focus-visible { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: -2px; }
647
- .aura-nav__label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
653
+ /* Touch (5.7): rows are 44px on coarse pointers (AppShell's phone drawer); desktop keeps 36px. */
654
+ @media (pointer: coarse) { .aura-nav__item { min-height: var(--aura-touch-target); } }
655
+ /* 5.7.1: a long label wraps to a second line (the row grows; one-line rows stay 36px) instead of an ellipsis with no
656
+ * way to read the rest — Thai and Swedish names in a 240px nav or the phone drawer. Beyond two lines it clamps. */
657
+ .aura-nav__label {
658
+ flex: 1; min-width: 0; line-height: 18px; overflow: hidden; overflow-wrap: anywhere;
659
+ display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2;
660
+ }
648
661
  .aura-nav__count { font-size: 11px; font-variant-numeric: tabular-nums; color: var(--aura-fg-secondary); }
649
662
  .aura-nav__badge { display: inline-flex; align-items: center; flex: none; }
650
663
  /* Groups: a disclosure button, its items indented one step per level. */
@@ -689,6 +702,8 @@ a.aura-tab { text-decoration: none; }
689
702
  .aura-crumbs a:hover, .aura-crumbs button:hover { color: var(--aura-fg-primary); text-decoration: underline; text-underline-offset: 3px; }
690
703
  .aura-crumbs a:focus-visible, .aura-crumbs button:focus-visible { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: 2px; }
691
704
  .aura-crumbs__current { color: var(--aura-fg-primary); font-weight: 500; }
705
+ /* 5.7: a segment with no page of its own — the links' colour, not interactive. */
706
+ .aura-crumbs__text { color: var(--aura-fg-secondary); }
692
707
  .aura-crumbs__sep { color: var(--aura-fg-tertiary); }
693
708
 
694
709
  /* ---------- Avatar ---------- */
@@ -884,6 +899,8 @@ a.aura-tab { text-decoration: none; }
884
899
  }
885
900
  .aura-shell__bar-content { flex: 1; min-width: 0; display: flex; align-items: center; gap: var(--aura-space-3); }
886
901
  .aura-shell__content { flex: 1; min-width: 0; padding: var(--aura-space-6) var(--aura-space-4); }
902
+ /* 5.7: <main> takes focus (tabIndex -1) for skip links and "the row you acted on is gone"; a landmark gets no ring. */
903
+ .aura-shell__content:focus { outline: none; }
887
904
  @media (min-width: 640px) { .aura-shell__content { padding: var(--aura-space-6); } }
888
905
  @media (min-width: 1024px) { .aura-shell__content { padding: var(--aura-space-8); } }
889
906
  /* The shell already pads its content: a Container directly inside only caps the width. */
@@ -249,6 +249,12 @@ a.aura-menu__item { text-decoration: none; box-sizing: border-box; }
249
249
  }
250
250
  .aura-menu__radio.is-on { border-color: var(--aura-button-primary-bg); }
251
251
  .aura-menu__radio.is-on::after { content: ''; width: 8px; height: 8px; border-radius: var(--aura-radius-full); background: var(--aura-button-primary-bg); }
252
+ /* 5.7: a header above the items (who is signed in), set off by a hairline like a separator. */
253
+ .aura-menu__header { margin: calc(-1 * var(--aura-space-1)) calc(-1 * var(--aura-space-1)) var(--aura-space-1); padding: var(--aura-space-3); border-bottom: 1px solid var(--aura-border-default); color: var(--aura-fg-secondary); font-size: 12px; line-height: 18px; overflow-wrap: anywhere; }
254
+ .aura-menu__header > * { margin: 0; }
255
+ /* Stays in view when a long menu scrolls. */
256
+ .aura-menu.has-header .aura-menu__header { position: sticky; top: calc(-1 * var(--aura-space-1)); z-index: 1; background: var(--aura-bg-surface); }
257
+ .aura-menu__header strong, .aura-menu__header b { color: var(--aura-fg-primary); font-weight: 500; font-size: 13px; }
252
258
  .aura-menu__sep { height: 1px; margin: var(--aura-space-1) calc(-1 * var(--aura-space-1)); background: var(--aura-border-default); }
253
259
 
254
260
  /* Footer + pager */
@@ -637,7 +643,7 @@ a.aura-tab { text-decoration: none; }
637
643
  .aura-nav__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
638
644
  .aura-nav__item {
639
645
  box-sizing: border-box; display: flex; align-items: center; gap: var(--aura-space-3); width: 100%;
640
- height: 36px; padding: 0 var(--aura-space-3); border: 0; border-radius: var(--aura-radius-sm);
646
+ min-height: 36px; padding: var(--aura-space-1) var(--aura-space-3); border: 0; border-radius: var(--aura-radius-sm);
641
647
  background: transparent; color: var(--aura-fg-secondary); font: inherit; font-size: 13px; font-weight: 500;
642
648
  text-decoration: none; text-align: left; cursor: pointer;
643
649
  transition: background var(--aura-duration-fast) var(--aura-ease), color var(--aura-duration-fast) var(--aura-ease);
@@ -646,7 +652,14 @@ a.aura-tab { text-decoration: none; }
646
652
  .aura-nav__item.is-active { background: var(--aura-bg-selected); color: var(--aura-fg-primary); }
647
653
  .aura-nav__item.is-active .aura-icon { color: var(--aura-fg-accent); }
648
654
  .aura-nav__item:focus-visible { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: -2px; }
649
- .aura-nav__label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
655
+ /* Touch (5.7): rows are 44px on coarse pointers (AppShell's phone drawer); desktop keeps 36px. */
656
+ @media (pointer: coarse) { .aura-nav__item { min-height: var(--aura-touch-target); } }
657
+ /* 5.7.1: a long label wraps to a second line (the row grows; one-line rows stay 36px) instead of an ellipsis with no
658
+ * way to read the rest — Thai and Swedish names in a 240px nav or the phone drawer. Beyond two lines it clamps. */
659
+ .aura-nav__label {
660
+ flex: 1; min-width: 0; line-height: 18px; overflow: hidden; overflow-wrap: anywhere;
661
+ display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2;
662
+ }
650
663
  .aura-nav__count { font-size: 11px; font-variant-numeric: tabular-nums; color: var(--aura-fg-secondary); }
651
664
  .aura-nav__badge { display: inline-flex; align-items: center; flex: none; }
652
665
  /* Groups: a disclosure button, its items indented one step per level. */
@@ -691,6 +704,8 @@ a.aura-tab { text-decoration: none; }
691
704
  .aura-crumbs a:hover, .aura-crumbs button:hover { color: var(--aura-fg-primary); text-decoration: underline; text-underline-offset: 3px; }
692
705
  .aura-crumbs a:focus-visible, .aura-crumbs button:focus-visible { outline: var(--aura-focus-ring-width) solid var(--aura-focus-ring); outline-offset: 2px; }
693
706
  .aura-crumbs__current { color: var(--aura-fg-primary); font-weight: 500; }
707
+ /* 5.7: a segment with no page of its own — the links' colour, not interactive. */
708
+ .aura-crumbs__text { color: var(--aura-fg-secondary); }
694
709
  .aura-crumbs__sep { color: var(--aura-fg-tertiary); }
695
710
 
696
711
  /* ---------- Avatar ---------- */
@@ -886,6 +901,8 @@ a.aura-tab { text-decoration: none; }
886
901
  }
887
902
  .aura-shell__bar-content { flex: 1; min-width: 0; display: flex; align-items: center; gap: var(--aura-space-3); }
888
903
  .aura-shell__content { flex: 1; min-width: 0; padding: var(--aura-space-6) var(--aura-space-4); }
904
+ /* 5.7: <main> takes focus (tabIndex -1) for skip links and "the row you acted on is gone"; a landmark gets no ring. */
905
+ .aura-shell__content:focus { outline: none; }
889
906
  @media (min-width: 640px) { .aura-shell__content { padding: var(--aura-space-6); } }
890
907
  @media (min-width: 1024px) { .aura-shell__content { padding: var(--aura-space-8); } }
891
908
  /* The shell already pads its content: a Container directly inside only caps the width. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jirawatpyk/aura-react",
3
- "version": "5.6.0",
3
+ "version": "5.7.1",
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",