@jirawatpyk/aura-react 5.55.0 → 5.57.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,16 @@ 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.57 — Chamber-OS addenda 70–71 (#169, #170)
190
+
191
+ - **A Dialog or Drawer always opens with focus inside:** a control inside a closed `<details>` is no longer picked as the first control (Chromium reports it as focusable, but `focus()` on it did nothing, so focus stayed on the trigger and Escape didn't close the modal). The `<details>`'s own `<summary>` now counts as a control, so a body whose only control is a closed `<details>` opens on its summary. And focus is checked after each try: if a control doesn't take focus (or pass it on to a field inside it), the next one is tried, then the panel itself — focus is always in the modal, so Escape and the Tab trap work whatever the content. Command works the same way, and so does a Popover's first focus — which now also skips `tabindex="-1"` controls, so a Popover holding a SegmentedControl or Tabs opens on the selected item, as Tab would reach it. A summary is a control whether its `<details>` is open or closed, so a body that starts with an open `<details>` ("Advanced" above a form) now opens on its summary, the first Tab stop, rather than the first field.
192
+ - **The Tab trap skips closed `<details>` too:** Tab and Shift+Tab wrap between the controls you can see, the summary included, and never to a control in a closed `<details>`; opened, its controls are Tab stops again. Popover's and DatePicker's trap do the same and now count a summary as a stop, so Tab from it no longer leaves the popover. A workaround such as `closeProps={{ 'data-autofocus': true }}` can go.
193
+ - **A titled Alert is named by its title** (#170): an Alert with a `title` and a role (`alert`, `status`, `note` — the default is `alert` or `status` by tone) gets `aria-labelledby` pointing at the title, so a screen reader can say which notice it is when the banner takes focus or is read in browse mode. A passed `aria-label` or `aria-labelledby` wins; no title, or `role="none"`, stays unnamed. The title's id (set only when it names the Alert) is `titleId` if you pass one, else `id` + `-title`, else generated. The `/server` Alert can't generate ids: it uses `titleId` or `id` + `-title`, or for a plain-text title `aria-label`; give a rich title an `id` there. Drop a stand-in `aria-label={title}`.
194
+
195
+ ## 5.56 — Chamber-OS addendum 69 (#168)
196
+
197
+ - **AppShell's menu button is a 44px touch target:** the "Open navigation" button AppShell draws below 1024px is now 44×44 below 640px or on a touch screen (`(pointer: coarse)`), whatever the density — the same box as an IconButton with `touchHeight` — so it matches the 44px search and account buttons beside it. From 640px with a mouse it keeps its 32px size; from 1024px it is hidden, as before. The bar keeps its 56px height; on a phone the `header` content is 12px narrower, so a long title truncates a little sooner. No new prop.
198
+
189
199
  ## 5.55 — Chamber-OS addendum 68 (#167)
190
200
 
191
201
  - **Bar and footer buttons follow `touchHeight`'s query:** the 44px minimum for Buttons in an ActionBar (`children`, `start`) and in a Dialog or Drawer footer, and the 44px IconButton in a bar (not one in a footer), now apply below 640px **or on a touch screen** (`(pointer: coarse)`) at any width — the query `touchHeight` and the compact density use — so a `size="sm"` bulk-bar button on a tablet is 44px too. With a mouse from 640px nothing changes (36/32px in compact). Wrapping a footer's long label stays phone-only. Drop the `touchHeight` you kept on `size="sm"` bar and footer buttons for tablets.
@@ -331,6 +331,34 @@ window.Aura = (() => {
331
331
  return r2.current;
332
332
  };
333
333
  var FOCUSABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
334
+ function inClosedDetails(el) {
335
+ for (let p = el.parentElement; p; p = p.parentElement) {
336
+ if (p.tagName !== "DETAILS" || p.open) continue;
337
+ let s = null;
338
+ for (let c = p.firstElementChild; c; c = c.nextElementSibling)
339
+ if (c.tagName === "SUMMARY") {
340
+ s = c;
341
+ break;
342
+ }
343
+ if (!s || !(s === el || s.contains(el))) return true;
344
+ }
345
+ return false;
346
+ }
347
+ var TAB_STOPS = FOCUSABLE + ",details > summary";
348
+ function isTabStop(c, layoutless) {
349
+ return c.tabIndex >= 0 && (layoutless || c.getClientRects().length > 0) && !inClosedDetails(c);
350
+ }
351
+ function focusFirst(container, auto, lists, layoutless) {
352
+ const took = function(c) {
353
+ c.focus();
354
+ const a = document.activeElement;
355
+ return !!a && a !== container && container.contains(a);
356
+ };
357
+ if (auto && took(auto)) return true;
358
+ for (let l = 0; l < lists.length; l++)
359
+ for (let i = 0; i < lists[l].length; i++) if (isTabStop(lists[l][i], layoutless) && took(lists[l][i])) return true;
360
+ return false;
361
+ }
334
362
  function noopSubscribe() {
335
363
  return function() {
336
364
  };
@@ -348,9 +376,9 @@ window.Aura = (() => {
348
376
  function trapTab(e, container) {
349
377
  if (e.key !== "Tab" || !container) return;
350
378
  const list = Array.prototype.filter.call(
351
- container.querySelectorAll(FOCUSABLE),
379
+ container.querySelectorAll(TAB_STOPS),
352
380
  function(el) {
353
- return el.tabIndex >= 0 && el.offsetParent !== null;
381
+ return el.tabIndex >= 0 && el.offsetParent !== null && !inClosedDetails(el);
354
382
  }
355
383
  );
356
384
  if (!list.length) return;
@@ -623,19 +651,35 @@ window.Aura = (() => {
623
651
  warning: IconTriangleAlert,
624
652
  danger: IconCircleAlert
625
653
  };
626
- function alertElement(props, ref, close) {
654
+ function alertElement(props, ref, close, autoId) {
627
655
  const tone2 = props.tone || "info";
628
- const rest = omit(props, ["tone", "title", "children", "action", "onDismiss", "className", "role", "icon"]);
656
+ const rest = omit(props, [
657
+ "tone",
658
+ "title",
659
+ "titleId",
660
+ "children",
661
+ "action",
662
+ "onDismiss",
663
+ "className",
664
+ "role",
665
+ "icon"
666
+ ]);
667
+ const role = props.role || (tone2 === "danger" || tone2 === "warning" ? "alert" : "status");
668
+ const titled = !!props.title && role !== "none" && !props["aria-label"] && !props["aria-labelledby"];
669
+ const titleId = props.titleId || (props.id ? props.id + "-title" : autoId);
670
+ const text2 = typeof props.title === "string" || typeof props.title === "number" ? String(props.title) : void 0;
629
671
  return /* @__PURE__ */ React4.createElement(
630
672
  "div",
631
673
  {
632
674
  ...rest,
633
675
  ref,
634
676
  className: cx("aura-alert", "aura-alert--" + tone2, props.className),
635
- role: props.role || (tone2 === "danger" || tone2 === "warning" ? "alert" : "status")
677
+ role,
678
+ "aria-labelledby": titled && titleId ? titleId : props["aria-labelledby"],
679
+ "aria-label": titled && !titleId && text2 ? text2 : props["aria-label"]
636
680
  },
637
681
  /* @__PURE__ */ React4.createElement(Icon, { name: props.icon || h2(ALERT_ICON[tone2]), className: "aura-alert__icon" }),
638
- /* @__PURE__ */ React4.createElement("div", { className: "aura-alert__body" }, props.title ? /* @__PURE__ */ React4.createElement("p", { className: "aura-alert__title" }, props.title) : null, props.children ? /* @__PURE__ */ React4.createElement("div", { className: "aura-alert__text" }, props.children) : null, props.action ? /* @__PURE__ */ React4.createElement("div", { className: "aura-alert__action" }, props.action) : null),
682
+ /* @__PURE__ */ React4.createElement("div", { className: "aura-alert__body" }, props.title ? /* @__PURE__ */ React4.createElement("p", { className: "aura-alert__title", id: titled ? titleId : void 0 }, props.title) : null, props.children ? /* @__PURE__ */ React4.createElement("div", { className: "aura-alert__text" }, props.children) : null, props.action ? /* @__PURE__ */ React4.createElement("div", { className: "aura-alert__action" }, props.action) : null),
639
683
  close
640
684
  );
641
685
  }
@@ -4307,10 +4351,12 @@ window.Aura = (() => {
4307
4351
  var React23 = __toESM(require_react(), 1);
4308
4352
  var Alert = React23.forwardRef(function Alert2(props, ref) {
4309
4353
  const t = useStrings();
4354
+ const autoId = uid();
4310
4355
  return alertElement(
4311
4356
  props,
4312
4357
  ref,
4313
- props.onDismiss ? /* @__PURE__ */ React23.createElement(IconButton, { icon: /* @__PURE__ */ React23.createElement(IconX, null), label: t.dismiss, className: "aura-alert__close", onClick: props.onDismiss }) : null
4358
+ props.onDismiss ? /* @__PURE__ */ React23.createElement(IconButton, { icon: /* @__PURE__ */ React23.createElement(IconX, null), label: t.dismiss, className: "aura-alert__close", onClick: props.onDismiss }) : null,
4359
+ autoId
4314
4360
  );
4315
4361
  });
4316
4362
 
@@ -4793,8 +4839,8 @@ window.Aura = (() => {
4793
4839
  };
4794
4840
  }
4795
4841
  function tabbable(el) {
4796
- const all = Array.prototype.filter.call(el.querySelectorAll(FOCUSABLE), function(c) {
4797
- return c.tabIndex >= 0;
4842
+ const all = Array.prototype.filter.call(el.querySelectorAll(TAB_STOPS), function(c) {
4843
+ return c.tabIndex >= 0 && !inClosedDetails(c);
4798
4844
  });
4799
4845
  const shown = all.filter(function(c) {
4800
4846
  return c.getClientRects().length > 0;
@@ -4813,18 +4859,17 @@ window.Aura = (() => {
4813
4859
  prev.current = document.activeElement;
4814
4860
  const unlock = lockScroll();
4815
4861
  const el = ref.current;
4816
- const firstIn = function(scope) {
4817
- if (!scope || !el) return null;
4818
- const sel = FOCUSABLE.split(",").map(function(s) {
4862
+ const inPart = function(scope) {
4863
+ if (!scope || !el) return [];
4864
+ const sel = TAB_STOPS.split(",").map(function(s) {
4819
4865
  return scope + " " + s;
4820
4866
  }).join(",");
4821
- const list = el.querySelectorAll(sel);
4822
- for (let i = 0; i < list.length; i++)
4823
- if (list[i].tabIndex >= 0 && list[i].getClientRects().length > 0) return list[i];
4824
- return null;
4867
+ return el.querySelectorAll(sel);
4825
4868
  };
4826
- const target = el && (el.querySelector("[data-autofocus]") || firstIn(o.bodySelector) || firstIn(o.footSelector) || el);
4827
- if (target && o.autoFocus !== false) target.focus();
4869
+ if (el && o.autoFocus !== false) {
4870
+ const auto = el.querySelector("[data-autofocus]");
4871
+ if (!focusFirst(el, auto, [inPart(o.bodySelector), inPart(o.footSelector)])) el.focus();
4872
+ }
4828
4873
  return function() {
4829
4874
  unlock();
4830
4875
  const l = latest.current;
@@ -8474,6 +8519,7 @@ window.Aura = (() => {
8474
8519
  icon: /* @__PURE__ */ React48.createElement(IconMenu, null),
8475
8520
  label: props.menuLabel || t.openNav,
8476
8521
  size: "md",
8522
+ touchHeight: true,
8477
8523
  onClick: function() {
8478
8524
  setOpen(true);
8479
8525
  },
@@ -10390,8 +10436,15 @@ window.Aura = (() => {
10390
10436
  function() {
10391
10437
  if (!open || !mounted) return;
10392
10438
  if (props.autoFocus !== false && pop.current) {
10393
- const f = pop.current.querySelector("[data-autofocus]") || pop.current.querySelector(FOCUSABLE);
10394
- (f || pop.current).focus();
10439
+ const p = pop.current;
10440
+ const layoutless = document.documentElement.getClientRects().length === 0;
10441
+ if (!focusFirst(
10442
+ p,
10443
+ p.querySelector("[data-autofocus]"),
10444
+ [p.querySelectorAll(TAB_STOPS)],
10445
+ layoutless
10446
+ ))
10447
+ p.focus();
10395
10448
  }
10396
10449
  let timer2;
10397
10450
  function outside(e) {
@@ -313,6 +313,34 @@ var uid = React3.useId || function() {
313
313
  return r2.current;
314
314
  };
315
315
  var FOCUSABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
316
+ function inClosedDetails(el) {
317
+ for (let p = el.parentElement; p; p = p.parentElement) {
318
+ if (p.tagName !== "DETAILS" || p.open) continue;
319
+ let s = null;
320
+ for (let c = p.firstElementChild; c; c = c.nextElementSibling)
321
+ if (c.tagName === "SUMMARY") {
322
+ s = c;
323
+ break;
324
+ }
325
+ if (!s || !(s === el || s.contains(el))) return true;
326
+ }
327
+ return false;
328
+ }
329
+ var TAB_STOPS = FOCUSABLE + ",details > summary";
330
+ function isTabStop(c, layoutless) {
331
+ return c.tabIndex >= 0 && (layoutless || c.getClientRects().length > 0) && !inClosedDetails(c);
332
+ }
333
+ function focusFirst(container, auto, lists, layoutless) {
334
+ const took = function(c) {
335
+ c.focus();
336
+ const a = document.activeElement;
337
+ return !!a && a !== container && container.contains(a);
338
+ };
339
+ if (auto && took(auto)) return true;
340
+ for (let l = 0; l < lists.length; l++)
341
+ for (let i = 0; i < lists[l].length; i++) if (isTabStop(lists[l][i], layoutless) && took(lists[l][i])) return true;
342
+ return false;
343
+ }
316
344
  function noopSubscribe() {
317
345
  return function() {
318
346
  };
@@ -330,9 +358,9 @@ var useIsoLayoutEffect = typeof window !== "undefined" ? React3.useLayoutEffect
330
358
  function trapTab(e, container) {
331
359
  if (e.key !== "Tab" || !container) return;
332
360
  const list = Array.prototype.filter.call(
333
- container.querySelectorAll(FOCUSABLE),
361
+ container.querySelectorAll(TAB_STOPS),
334
362
  function(el) {
335
- return el.tabIndex >= 0 && el.offsetParent !== null;
363
+ return el.tabIndex >= 0 && el.offsetParent !== null && !inClosedDetails(el);
336
364
  }
337
365
  );
338
366
  if (!list.length) return;
@@ -605,19 +633,35 @@ var ALERT_ICON = {
605
633
  warning: IconTriangleAlert,
606
634
  danger: IconCircleAlert
607
635
  };
608
- function alertElement(props, ref, close) {
636
+ function alertElement(props, ref, close, autoId) {
609
637
  const tone2 = props.tone || "info";
610
- const rest = omit(props, ["tone", "title", "children", "action", "onDismiss", "className", "role", "icon"]);
638
+ const rest = omit(props, [
639
+ "tone",
640
+ "title",
641
+ "titleId",
642
+ "children",
643
+ "action",
644
+ "onDismiss",
645
+ "className",
646
+ "role",
647
+ "icon"
648
+ ]);
649
+ const role = props.role || (tone2 === "danger" || tone2 === "warning" ? "alert" : "status");
650
+ const titled = !!props.title && role !== "none" && !props["aria-label"] && !props["aria-labelledby"];
651
+ const titleId = props.titleId || (props.id ? props.id + "-title" : autoId);
652
+ const text2 = typeof props.title === "string" || typeof props.title === "number" ? String(props.title) : void 0;
611
653
  return /* @__PURE__ */ React4.createElement(
612
654
  "div",
613
655
  {
614
656
  ...rest,
615
657
  ref,
616
658
  className: cx("aura-alert", "aura-alert--" + tone2, props.className),
617
- role: props.role || (tone2 === "danger" || tone2 === "warning" ? "alert" : "status")
659
+ role,
660
+ "aria-labelledby": titled && titleId ? titleId : props["aria-labelledby"],
661
+ "aria-label": titled && !titleId && text2 ? text2 : props["aria-label"]
618
662
  },
619
663
  /* @__PURE__ */ React4.createElement(Icon, { name: props.icon || h2(ALERT_ICON[tone2]), className: "aura-alert__icon" }),
620
- /* @__PURE__ */ React4.createElement("div", { className: "aura-alert__body" }, props.title ? /* @__PURE__ */ React4.createElement("p", { className: "aura-alert__title" }, props.title) : null, props.children ? /* @__PURE__ */ React4.createElement("div", { className: "aura-alert__text" }, props.children) : null, props.action ? /* @__PURE__ */ React4.createElement("div", { className: "aura-alert__action" }, props.action) : null),
664
+ /* @__PURE__ */ React4.createElement("div", { className: "aura-alert__body" }, props.title ? /* @__PURE__ */ React4.createElement("p", { className: "aura-alert__title", id: titled ? titleId : void 0 }, props.title) : null, props.children ? /* @__PURE__ */ React4.createElement("div", { className: "aura-alert__text" }, props.children) : null, props.action ? /* @__PURE__ */ React4.createElement("div", { className: "aura-alert__action" }, props.action) : null),
621
665
  close
622
666
  );
623
667
  }
@@ -4289,10 +4333,12 @@ var Combobox = React22.forwardRef(
4289
4333
  var React23 = __toESM(require("react"), 1);
4290
4334
  var Alert = React23.forwardRef(function Alert2(props, ref) {
4291
4335
  const t = useStrings();
4336
+ const autoId = uid();
4292
4337
  return alertElement(
4293
4338
  props,
4294
4339
  ref,
4295
- props.onDismiss ? /* @__PURE__ */ React23.createElement(IconButton, { icon: /* @__PURE__ */ React23.createElement(IconX, null), label: t.dismiss, className: "aura-alert__close", onClick: props.onDismiss }) : null
4340
+ props.onDismiss ? /* @__PURE__ */ React23.createElement(IconButton, { icon: /* @__PURE__ */ React23.createElement(IconX, null), label: t.dismiss, className: "aura-alert__close", onClick: props.onDismiss }) : null,
4341
+ autoId
4296
4342
  );
4297
4343
  });
4298
4344
 
@@ -4775,8 +4821,8 @@ function lockScroll() {
4775
4821
  };
4776
4822
  }
4777
4823
  function tabbable(el) {
4778
- const all = Array.prototype.filter.call(el.querySelectorAll(FOCUSABLE), function(c) {
4779
- return c.tabIndex >= 0;
4824
+ const all = Array.prototype.filter.call(el.querySelectorAll(TAB_STOPS), function(c) {
4825
+ return c.tabIndex >= 0 && !inClosedDetails(c);
4780
4826
  });
4781
4827
  const shown = all.filter(function(c) {
4782
4828
  return c.getClientRects().length > 0;
@@ -4795,18 +4841,17 @@ function useModal(open, ref, opts) {
4795
4841
  prev.current = document.activeElement;
4796
4842
  const unlock = lockScroll();
4797
4843
  const el = ref.current;
4798
- const firstIn = function(scope) {
4799
- if (!scope || !el) return null;
4800
- const sel = FOCUSABLE.split(",").map(function(s) {
4844
+ const inPart = function(scope) {
4845
+ if (!scope || !el) return [];
4846
+ const sel = TAB_STOPS.split(",").map(function(s) {
4801
4847
  return scope + " " + s;
4802
4848
  }).join(",");
4803
- const list = el.querySelectorAll(sel);
4804
- for (let i = 0; i < list.length; i++)
4805
- if (list[i].tabIndex >= 0 && list[i].getClientRects().length > 0) return list[i];
4806
- return null;
4849
+ return el.querySelectorAll(sel);
4807
4850
  };
4808
- const target = el && (el.querySelector("[data-autofocus]") || firstIn(o.bodySelector) || firstIn(o.footSelector) || el);
4809
- if (target && o.autoFocus !== false) target.focus();
4851
+ if (el && o.autoFocus !== false) {
4852
+ const auto = el.querySelector("[data-autofocus]");
4853
+ if (!focusFirst(el, auto, [inPart(o.bodySelector), inPart(o.footSelector)])) el.focus();
4854
+ }
4810
4855
  return function() {
4811
4856
  unlock();
4812
4857
  const l = latest.current;
@@ -8456,6 +8501,7 @@ var AppShell = React48.forwardRef(function AppShell2(props, ref) {
8456
8501
  icon: /* @__PURE__ */ React48.createElement(IconMenu, null),
8457
8502
  label: props.menuLabel || t.openNav,
8458
8503
  size: "md",
8504
+ touchHeight: true,
8459
8505
  onClick: function() {
8460
8506
  setOpen(true);
8461
8507
  },
@@ -10372,8 +10418,15 @@ var Popover = React66.forwardRef(function Popover2(props, ref) {
10372
10418
  function() {
10373
10419
  if (!open || !mounted) return;
10374
10420
  if (props.autoFocus !== false && pop.current) {
10375
- const f = pop.current.querySelector("[data-autofocus]") || pop.current.querySelector(FOCUSABLE);
10376
- (f || pop.current).focus();
10421
+ const p = pop.current;
10422
+ const layoutless = document.documentElement.getClientRects().length === 0;
10423
+ if (!focusFirst(
10424
+ p,
10425
+ p.querySelector("[data-autofocus]"),
10426
+ [p.querySelectorAll(TAB_STOPS)],
10427
+ layoutless
10428
+ ))
10429
+ p.focus();
10377
10430
  }
10378
10431
  let timer2;
10379
10432
  function outside(e) {
package/dist/esm/Alert.js CHANGED
@@ -2,15 +2,18 @@
2
2
  import * as React from "react";
3
3
  import { IconButton } from "./IconButton.js";
4
4
  import { alertElement } from "./display.js";
5
+ import { uid } from "./internal.js";
5
6
  import { useStrings } from "./locale.js";
6
7
  import { IconX } from "./icons.js";
7
8
  import { ALERT_ICON } from "./display.js";
8
9
  const Alert = React.forwardRef(function Alert2(props, ref) {
9
10
  const t = useStrings();
11
+ const autoId = uid();
10
12
  return alertElement(
11
13
  props,
12
14
  ref,
13
- props.onDismiss ? /* @__PURE__ */ React.createElement(IconButton, { icon: /* @__PURE__ */ React.createElement(IconX, null), label: t.dismiss, className: "aura-alert__close", onClick: props.onDismiss }) : null
15
+ props.onDismiss ? /* @__PURE__ */ React.createElement(IconButton, { icon: /* @__PURE__ */ React.createElement(IconX, null), label: t.dismiss, className: "aura-alert__close", onClick: props.onDismiss }) : null,
16
+ autoId
14
17
  );
15
18
  });
16
19
  export {
@@ -138,6 +138,7 @@ const AppShell = React.forwardRef(function AppShell2(props, ref) {
138
138
  icon: /* @__PURE__ */ React.createElement(IconMenu, null),
139
139
  label: props.menuLabel || t.openNav,
140
140
  size: "md",
141
+ touchHeight: true,
141
142
  onClick: function() {
142
143
  setOpen(true);
143
144
  },
@@ -2,8 +2,9 @@
2
2
  import * as React from "react";
3
3
  import { IconButton } from "./IconButton.js";
4
4
  import {
5
- FOCUSABLE,
6
5
  cx,
6
+ focusFirst,
7
+ TAB_STOPS,
7
8
  trapTab,
8
9
  uid,
9
10
  useIsoLayoutEffect,
@@ -69,8 +70,15 @@ const Popover = React.forwardRef(function Popover2(props, ref) {
69
70
  function() {
70
71
  if (!open || !mounted) return;
71
72
  if (props.autoFocus !== false && pop.current) {
72
- const f = pop.current.querySelector("[data-autofocus]") || pop.current.querySelector(FOCUSABLE);
73
- (f || pop.current).focus();
73
+ const p = pop.current;
74
+ const layoutless = document.documentElement.getClientRects().length === 0;
75
+ if (!focusFirst(
76
+ p,
77
+ p.querySelector("[data-autofocus]"),
78
+ [p.querySelectorAll(TAB_STOPS)],
79
+ layoutless
80
+ ))
81
+ p.focus();
74
82
  }
75
83
  let timer;
76
84
  function outside(e) {
@@ -23,19 +23,35 @@ const ALERT_ICON = {
23
23
  warning: IconTriangleAlert,
24
24
  danger: IconCircleAlert
25
25
  };
26
- function alertElement(props, ref, close) {
26
+ function alertElement(props, ref, close, autoId) {
27
27
  const tone = props.tone || "info";
28
- const rest = omit(props, ["tone", "title", "children", "action", "onDismiss", "className", "role", "icon"]);
28
+ const rest = omit(props, [
29
+ "tone",
30
+ "title",
31
+ "titleId",
32
+ "children",
33
+ "action",
34
+ "onDismiss",
35
+ "className",
36
+ "role",
37
+ "icon"
38
+ ]);
39
+ const role = props.role || (tone === "danger" || tone === "warning" ? "alert" : "status");
40
+ const titled = !!props.title && role !== "none" && !props["aria-label"] && !props["aria-labelledby"];
41
+ const titleId = props.titleId || (props.id ? props.id + "-title" : autoId);
42
+ const text = typeof props.title === "string" || typeof props.title === "number" ? String(props.title) : void 0;
29
43
  return /* @__PURE__ */ React.createElement(
30
44
  "div",
31
45
  {
32
46
  ...rest,
33
47
  ref,
34
48
  className: cx("aura-alert", "aura-alert--" + tone, props.className),
35
- role: props.role || (tone === "danger" || tone === "warning" ? "alert" : "status")
49
+ role,
50
+ "aria-labelledby": titled && titleId ? titleId : props["aria-labelledby"],
51
+ "aria-label": titled && !titleId && text ? text : props["aria-label"]
36
52
  },
37
53
  /* @__PURE__ */ React.createElement(Icon, { name: props.icon || h(ALERT_ICON[tone]), className: "aura-alert__icon" }),
38
- /* @__PURE__ */ React.createElement("div", { className: "aura-alert__body" }, props.title ? /* @__PURE__ */ React.createElement("p", { className: "aura-alert__title" }, props.title) : null, props.children ? /* @__PURE__ */ React.createElement("div", { className: "aura-alert__text" }, props.children) : null, props.action ? /* @__PURE__ */ React.createElement("div", { className: "aura-alert__action" }, props.action) : null),
54
+ /* @__PURE__ */ React.createElement("div", { className: "aura-alert__body" }, props.title ? /* @__PURE__ */ React.createElement("p", { className: "aura-alert__title", id: titled ? titleId : void 0 }, props.title) : null, props.children ? /* @__PURE__ */ React.createElement("div", { className: "aura-alert__text" }, props.children) : null, props.action ? /* @__PURE__ */ React.createElement("div", { className: "aura-alert__action" }, props.action) : null),
39
55
  close
40
56
  );
41
57
  }
@@ -33,6 +33,34 @@ const uid = React.useId || function() {
33
33
  return r.current;
34
34
  };
35
35
  const FOCUSABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
36
+ function inClosedDetails(el) {
37
+ for (let p = el.parentElement; p; p = p.parentElement) {
38
+ if (p.tagName !== "DETAILS" || p.open) continue;
39
+ let s = null;
40
+ for (let c = p.firstElementChild; c; c = c.nextElementSibling)
41
+ if (c.tagName === "SUMMARY") {
42
+ s = c;
43
+ break;
44
+ }
45
+ if (!s || !(s === el || s.contains(el))) return true;
46
+ }
47
+ return false;
48
+ }
49
+ const TAB_STOPS = FOCUSABLE + ",details > summary";
50
+ function isTabStop(c, layoutless) {
51
+ return c.tabIndex >= 0 && (layoutless || c.getClientRects().length > 0) && !inClosedDetails(c);
52
+ }
53
+ function focusFirst(container, auto, lists, layoutless) {
54
+ const took = function(c) {
55
+ c.focus();
56
+ const a = document.activeElement;
57
+ return !!a && a !== container && container.contains(a);
58
+ };
59
+ if (auto && took(auto)) return true;
60
+ for (let l = 0; l < lists.length; l++)
61
+ for (let i = 0; i < lists[l].length; i++) if (isTabStop(lists[l][i], layoutless) && took(lists[l][i])) return true;
62
+ return false;
63
+ }
36
64
  function noopSubscribe() {
37
65
  return function() {
38
66
  };
@@ -50,9 +78,9 @@ const useIsoLayoutEffect = typeof window !== "undefined" ? React.useLayoutEffect
50
78
  function trapTab(e, container) {
51
79
  if (e.key !== "Tab" || !container) return;
52
80
  const list = Array.prototype.filter.call(
53
- container.querySelectorAll(FOCUSABLE),
81
+ container.querySelectorAll(TAB_STOPS),
54
82
  function(el) {
55
- return el.tabIndex >= 0 && el.offsetParent !== null;
83
+ return el.tabIndex >= 0 && el.offsetParent !== null && !inClosedDetails(el);
56
84
  }
57
85
  );
58
86
  if (!list.length) return;
@@ -84,10 +112,14 @@ const FilterSheet = React.createContext(false);
84
112
  export {
85
113
  FOCUSABLE,
86
114
  FilterSheet,
115
+ TAB_STOPS,
87
116
  collator,
88
117
  compare,
89
118
  cx,
90
119
  devWarnOnce,
120
+ focusFirst,
121
+ inClosedDetails,
122
+ isTabStop,
91
123
  omit,
92
124
  plainClick,
93
125
  tone,
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import * as React from "react";
3
- import { FOCUSABLE, useMounted } from "./internal.js";
3
+ import { focusFirst, inClosedDetails, TAB_STOPS, useMounted } from "./internal.js";
4
4
  let locks = 0, savedOverflow = "";
5
5
  function lockScroll() {
6
6
  const body = document.body;
@@ -16,8 +16,8 @@ function lockScroll() {
16
16
  };
17
17
  }
18
18
  function tabbable(el) {
19
- const all = Array.prototype.filter.call(el.querySelectorAll(FOCUSABLE), function(c) {
20
- return c.tabIndex >= 0;
19
+ const all = Array.prototype.filter.call(el.querySelectorAll(TAB_STOPS), function(c) {
20
+ return c.tabIndex >= 0 && !inClosedDetails(c);
21
21
  });
22
22
  const shown = all.filter(function(c) {
23
23
  return c.getClientRects().length > 0;
@@ -36,18 +36,17 @@ function useModal(open, ref, opts) {
36
36
  prev.current = document.activeElement;
37
37
  const unlock = lockScroll();
38
38
  const el = ref.current;
39
- const firstIn = function(scope) {
40
- if (!scope || !el) return null;
41
- const sel = FOCUSABLE.split(",").map(function(s) {
39
+ const inPart = function(scope) {
40
+ if (!scope || !el) return [];
41
+ const sel = TAB_STOPS.split(",").map(function(s) {
42
42
  return scope + " " + s;
43
43
  }).join(",");
44
- const list = el.querySelectorAll(sel);
45
- for (let i = 0; i < list.length; i++)
46
- if (list[i].tabIndex >= 0 && list[i].getClientRects().length > 0) return list[i];
47
- return null;
44
+ return el.querySelectorAll(sel);
48
45
  };
49
- const target = el && (el.querySelector("[data-autofocus]") || firstIn(o.bodySelector) || firstIn(o.footSelector) || el);
50
- if (target && o.autoFocus !== false) target.focus();
46
+ if (el && o.autoFocus !== false) {
47
+ const auto = el.querySelector("[data-autofocus]");
48
+ if (!focusFirst(el, auto, [inPart(o.bodySelector), inPart(o.footSelector)])) el.focus();
49
+ }
51
50
  return function() {
52
51
  unlock();
53
52
  const l = latest.current;
package/dist/index.d.cts CHANGED
@@ -707,7 +707,12 @@ export interface AlertProps extends Omit<React$1.HTMLAttributes<HTMLDivElement>,
707
707
  role?: "alert" | "status" | "note" | "none" | undefined;
708
708
  /** Replaces the tone's icon (5.8): a name or your own element; always hidden from screen readers. */
709
709
  icon?: IconInput | undefined;
710
+ /** With a `role` other than `none`, the Alert is named by its title (5.57, Chamber-OS 170: `aria-labelledby`), unless
711
+ * you pass `aria-label` or `aria-labelledby`. */
710
712
  title?: React$1.ReactNode | undefined;
713
+ /** id for the title element (default: generated, or `id` + `-title`). The server entry has no generated ids: pass
714
+ * `id` or `titleId` there for a title that isn't plain text. */
715
+ titleId?: string | undefined;
711
716
  children?: React$1.ReactNode | undefined;
712
717
  /** Usually a secondary Button. */
713
718
  action?: React$1.ReactNode | undefined;
package/dist/index.d.ts CHANGED
@@ -707,7 +707,12 @@ export interface AlertProps extends Omit<React$1.HTMLAttributes<HTMLDivElement>,
707
707
  role?: "alert" | "status" | "note" | "none" | undefined;
708
708
  /** Replaces the tone's icon (5.8): a name or your own element; always hidden from screen readers. */
709
709
  icon?: IconInput | undefined;
710
+ /** With a `role` other than `none`, the Alert is named by its title (5.57, Chamber-OS 170: `aria-labelledby`), unless
711
+ * you pass `aria-label` or `aria-labelledby`. */
710
712
  title?: React$1.ReactNode | undefined;
713
+ /** id for the title element (default: generated, or `id` + `-title`). The server entry has no generated ids: pass
714
+ * `id` or `titleId` there for a title that isn't plain text. */
715
+ titleId?: string | undefined;
711
716
  children?: React$1.ReactNode | undefined;
712
717
  /** Usually a secondary Button. */
713
718
  action?: React$1.ReactNode | undefined;
@@ -1179,19 +1179,35 @@ var ALERT_ICON = {
1179
1179
  warning: IconTriangleAlert,
1180
1180
  danger: IconCircleAlert
1181
1181
  };
1182
- function alertElement(props, ref, close) {
1182
+ function alertElement(props, ref, close, autoId) {
1183
1183
  const tone2 = props.tone || "info";
1184
- const rest = omit(props, ["tone", "title", "children", "action", "onDismiss", "className", "role", "icon"]);
1184
+ const rest = omit(props, [
1185
+ "tone",
1186
+ "title",
1187
+ "titleId",
1188
+ "children",
1189
+ "action",
1190
+ "onDismiss",
1191
+ "className",
1192
+ "role",
1193
+ "icon"
1194
+ ]);
1195
+ const role = props.role || (tone2 === "danger" || tone2 === "warning" ? "alert" : "status");
1196
+ const titled = !!props.title && role !== "none" && !props["aria-label"] && !props["aria-labelledby"];
1197
+ const titleId = props.titleId || (props.id ? props.id + "-title" : autoId);
1198
+ const text = typeof props.title === "string" || typeof props.title === "number" ? String(props.title) : void 0;
1185
1199
  return /* @__PURE__ */ React3.createElement(
1186
1200
  "div",
1187
1201
  {
1188
1202
  ...rest,
1189
1203
  ref,
1190
1204
  className: cx("aura-alert", "aura-alert--" + tone2, props.className),
1191
- role: props.role || (tone2 === "danger" || tone2 === "warning" ? "alert" : "status")
1205
+ role,
1206
+ "aria-labelledby": titled && titleId ? titleId : props["aria-labelledby"],
1207
+ "aria-label": titled && !titleId && text ? text : props["aria-label"]
1192
1208
  },
1193
1209
  /* @__PURE__ */ React3.createElement(Icon, { name: props.icon || h2(ALERT_ICON[tone2]), className: "aura-alert__icon" }),
1194
- /* @__PURE__ */ React3.createElement("div", { className: "aura-alert__body" }, props.title ? /* @__PURE__ */ React3.createElement("p", { className: "aura-alert__title" }, props.title) : null, props.children ? /* @__PURE__ */ React3.createElement("div", { className: "aura-alert__text" }, props.children) : null, props.action ? /* @__PURE__ */ React3.createElement("div", { className: "aura-alert__action" }, props.action) : null),
1210
+ /* @__PURE__ */ React3.createElement("div", { className: "aura-alert__body" }, props.title ? /* @__PURE__ */ React3.createElement("p", { className: "aura-alert__title", id: titled ? titleId : void 0 }, props.title) : null, props.children ? /* @__PURE__ */ React3.createElement("div", { className: "aura-alert__text" }, props.children) : null, props.action ? /* @__PURE__ */ React3.createElement("div", { className: "aura-alert__action" }, props.action) : null),
1195
1211
  close
1196
1212
  );
1197
1213
  }
@@ -63,7 +63,12 @@ export interface AlertProps extends Omit<React$1.HTMLAttributes<HTMLDivElement>,
63
63
  role?: "alert" | "status" | "note" | "none" | undefined;
64
64
  /** Replaces the tone's icon (5.8): a name or your own element; always hidden from screen readers. */
65
65
  icon?: IconInput | undefined;
66
+ /** With a `role` other than `none`, the Alert is named by its title (5.57, Chamber-OS 170: `aria-labelledby`), unless
67
+ * you pass `aria-label` or `aria-labelledby`. */
66
68
  title?: React$1.ReactNode | undefined;
69
+ /** id for the title element (default: generated, or `id` + `-title`). The server entry has no generated ids: pass
70
+ * `id` or `titleId` there for a title that isn't plain text. */
71
+ titleId?: string | undefined;
67
72
  children?: React$1.ReactNode | undefined;
68
73
  /** Usually a secondary Button. */
69
74
  action?: React$1.ReactNode | undefined;
@@ -63,7 +63,12 @@ export interface AlertProps extends Omit<React$1.HTMLAttributes<HTMLDivElement>,
63
63
  role?: "alert" | "status" | "note" | "none" | undefined;
64
64
  /** Replaces the tone's icon (5.8): a name or your own element; always hidden from screen readers. */
65
65
  icon?: IconInput | undefined;
66
+ /** With a `role` other than `none`, the Alert is named by its title (5.57, Chamber-OS 170: `aria-labelledby`), unless
67
+ * you pass `aria-label` or `aria-labelledby`. */
66
68
  title?: React$1.ReactNode | undefined;
69
+ /** id for the title element (default: generated, or `id` + `-title`). The server entry has no generated ids: pass
70
+ * `id` or `titleId` there for a title that isn't plain text. */
71
+ titleId?: string | undefined;
67
72
  children?: React$1.ReactNode | undefined;
68
73
  /** Usually a secondary Button. */
69
74
  action?: React$1.ReactNode | undefined;
@@ -1114,19 +1114,35 @@ var ALERT_ICON = {
1114
1114
  warning: IconTriangleAlert,
1115
1115
  danger: IconCircleAlert
1116
1116
  };
1117
- function alertElement(props, ref, close) {
1117
+ function alertElement(props, ref, close, autoId) {
1118
1118
  const tone2 = props.tone || "info";
1119
- const rest = omit(props, ["tone", "title", "children", "action", "onDismiss", "className", "role", "icon"]);
1119
+ const rest = omit(props, [
1120
+ "tone",
1121
+ "title",
1122
+ "titleId",
1123
+ "children",
1124
+ "action",
1125
+ "onDismiss",
1126
+ "className",
1127
+ "role",
1128
+ "icon"
1129
+ ]);
1130
+ const role = props.role || (tone2 === "danger" || tone2 === "warning" ? "alert" : "status");
1131
+ const titled = !!props.title && role !== "none" && !props["aria-label"] && !props["aria-labelledby"];
1132
+ const titleId = props.titleId || (props.id ? props.id + "-title" : autoId);
1133
+ const text = typeof props.title === "string" || typeof props.title === "number" ? String(props.title) : void 0;
1120
1134
  return /* @__PURE__ */ React3.createElement(
1121
1135
  "div",
1122
1136
  {
1123
1137
  ...rest,
1124
1138
  ref,
1125
1139
  className: cx("aura-alert", "aura-alert--" + tone2, props.className),
1126
- role: props.role || (tone2 === "danger" || tone2 === "warning" ? "alert" : "status")
1140
+ role,
1141
+ "aria-labelledby": titled && titleId ? titleId : props["aria-labelledby"],
1142
+ "aria-label": titled && !titleId && text ? text : props["aria-label"]
1127
1143
  },
1128
1144
  /* @__PURE__ */ React3.createElement(Icon, { name: props.icon || h2(ALERT_ICON[tone2]), className: "aura-alert__icon" }),
1129
- /* @__PURE__ */ React3.createElement("div", { className: "aura-alert__body" }, props.title ? /* @__PURE__ */ React3.createElement("p", { className: "aura-alert__title" }, props.title) : null, props.children ? /* @__PURE__ */ React3.createElement("div", { className: "aura-alert__text" }, props.children) : null, props.action ? /* @__PURE__ */ React3.createElement("div", { className: "aura-alert__action" }, props.action) : null),
1145
+ /* @__PURE__ */ React3.createElement("div", { className: "aura-alert__body" }, props.title ? /* @__PURE__ */ React3.createElement("p", { className: "aura-alert__title", id: titled ? titleId : void 0 }, props.title) : null, props.children ? /* @__PURE__ */ React3.createElement("div", { className: "aura-alert__text" }, props.children) : null, props.action ? /* @__PURE__ */ React3.createElement("div", { className: "aura-alert__action" }, props.action) : null),
1130
1146
  close
1131
1147
  );
1132
1148
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jirawatpyk/aura-react",
3
- "version": "5.55.0",
3
+ "version": "5.57.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",