@nxgt/material-core 0.6.0 → 0.7.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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @nxgt/material-core
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#475](https://github.com/softistx/nxgt-material/pull/475) [`2fcfa35`](https://github.com/softistx/nxgt-material/commit/2fcfa35d5bff9d46392247440a82f0bc3638f104) Thanks [@SteveGT96](https://github.com/SteveGT96)! - New `/toast-focus` subpath: `focusOrigin(event, origin)` records where the focus was before it entered a toast, and `handFocusBack(origin)` gives it back there before the toast's action runs (or blurs the toast's control when that element has gone), so a dialog the action opens keeps the focus. `trackToasterOrigin()` records, per `Toaster`, where the focus entered it, so a toast reached from another toast hands the focus back to the same control. `TOASTER_SELECTOR` is `'[data-sonner-toaster]'`. Both libraries' toasts use it ([#473](https://github.com/softistx/nxgt-material/issues/473)).
8
+
9
+ ### Patch Changes
10
+
11
+ - [#476](https://github.com/softistx/nxgt-material/pull/476) [`464883e`](https://github.com/softistx/nxgt-material/commit/464883e825e4601f17395c9586d0ac01dd392162) Thanks [@SteveGT96](https://github.com/SteveGT96)! - The roadmap names the release that ships the toast action and focus fixes.
12
+
3
13
  ## 0.6.0
4
14
 
5
15
  ### Minor Changes
package/README.md CHANGED
@@ -11,7 +11,8 @@ rule, a field's first reachable control (`/focus-control`) and its `aria-describ
11
11
  (`/described-by`), the page behind a modal layer made inert
12
12
  (`/inert-others`), Escape pressed while an overlay closes
13
13
  (`/escape-forward`), the Toaster's guard against a tap's emulated hover
14
- (`/touch-hover`), a table row's activation, the zod error map's issue
14
+ (`/touch-hover`), the focus a toast's action hands back (`/toast-focus`), a
15
+ table row's activation, the zod error map's issue
15
16
  messages (`/zod-issue-messages`), the Vite plugin that bundles pdfkit's
16
17
  standard fonts, and the Node-only resolution of those fonts that the Next.js
17
18
  helper and the Nuxt module run at build time.
@@ -85,6 +86,7 @@ built-in.
85
86
  | `/described-by` | `describedBy` — an `aria-describedby` value from the caller's ids and a field's help and error ids |
86
87
  | `/inert-others` | `inertOthers`, `INERT_MARKER` — the rest of the page `inert` behind a modal layer, live regions kept, nested layers counted |
87
88
  | `/touch-hover` | `ignoreTouchHover`, `TOASTER_PHONE_QUERY`, `TOASTER_PHONE_POSITION` — keeps a tap from hovering an element, so a toast a finger touched does not stay paused; where the `Toaster` puts toasts on a phone |
89
+ | `/toast-focus` | `focusOrigin`, `handFocusBack`, `trackToasterOrigin`, `TOASTER_SELECTOR` — where the focus was before it entered a toast, and handing it back there before the toast's action runs, so a dialog the action opens keeps the focus |
88
90
  | `/zod-issue-messages` | `sizeMessage`, `invalidFormatMessage`, `IssueText` — the translated text of a zod `too_big` / `too_small` and `invalid_format` issue |
89
91
  | `/row-activation` | `isRowActivation`, `followRowLink`, `ROW_CONTROL_SELECTOR`, `ROW_LINK_SELECTOR`, `RowClick` — whether a click on a table row follows its link, and following it |
90
92
  | `/format-file-size` | `formatFileSize` — a byte count as `'1.5 KB'` |
@@ -613,6 +615,41 @@ It also holds the two constants both libraries' `Toaster` share: `TOASTER_PHONE_
613
615
  screen) and `TOASTER_PHONE_POSITION` (`'top-center'`, where toasts go on a phone
614
616
  when the caller names no `position`).
615
617
 
618
+ ### The focus a toast's action hands back: `/toast-focus`
619
+
620
+ sonner focuses again the control that had the focus before it entered the
621
+ `Toaster` as soon as the focus leaves it, so an action that opened a dialog
622
+ saw the focus pulled back out of the dialog; and a dialog opened from a
623
+ toast's button gave the focus back, once closed, to a button that had gone
624
+ with its toast. Each toast records where the focus came from with
625
+ `focusOrigin(event, origin)` on its `focusin` (focus moving between toasts
626
+ keeps the first origin, or else the one its `Toaster` recorded), and its action calls `handFocusBack(origin)` before
627
+ running: it focuses `origin` (else the `Toaster`'s), or blurs the toast's control when `origin` has
628
+ gone, and does nothing when the focus is not in the `Toaster`
629
+ (`TOASTER_SELECTOR`, `'[data-sonner-toaster]'`). The list item around a
630
+ toast takes the focus first from the keyboard, and no toast hears it:
631
+ `trackToasterOrigin()` (once, when a toast mounts) listens on the document so
632
+ the `Toaster` knows the page control the focus left, and the next toast, which
633
+ never had the focus from the page, still hands it back there.
634
+
635
+ ```ts
636
+ import {
637
+ focusOrigin,
638
+ handFocusBack,
639
+ trackToasterOrigin,
640
+ } from '@nxgt/material-core/toast-focus';
641
+
642
+ trackToasterOrigin();
643
+ let origin: Element | null = null;
644
+ toastElement.addEventListener('focusin', (event) => {
645
+ origin = focusOrigin(event, origin);
646
+ });
647
+ actionButton.addEventListener('click', () => {
648
+ handFocusBack(origin);
649
+ openDialog();
650
+ });
651
+ ```
652
+
616
653
  ### Zod issue messages: `/zod-issue-messages`
617
654
 
618
655
  The two libraries' zod error maps turn an issue into text in the language
@@ -0,0 +1,41 @@
1
+ /** The element sonner and vue-sonner render each toast list as. */
2
+ export declare const TOASTER_SELECTOR = "[data-sonner-toaster]";
3
+ /**
4
+ * Where the focus came from as it entered a toast, for a `focusin` heard on
5
+ * the toast: the element it left (`relatedTarget`), unless that was already
6
+ * in a Toaster, as when the focus moves from one toast to the next; then
7
+ * `origin`, what the toast had recorded so far, is kept, or else what the
8
+ * Toaster recorded when the focus first entered it (a toast that never had
9
+ * the focus from outside, as the next one in the list, still has an origin).
10
+ */
11
+ export declare function focusOrigin(event: Pick<FocusEvent, 'relatedTarget'> & Partial<Pick<FocusEvent, 'target'>>, origin: Element | null): Element | null;
12
+ /**
13
+ * Records, for each Toaster, where the focus came from as it entered it.
14
+ * A toast hears the focus only once it reaches one of its own controls, and
15
+ * with a keyboard it first lands on the list item that wraps the toast
16
+ * (sonner's and vue-sonner's `li` take `tabindex="0"`), which no toast hears:
17
+ * this listens on the document, once, so the Toaster knows the page control
18
+ * the focus left. Call it when a toast mounts; calling it again does nothing.
19
+ */
20
+ export declare function trackToasterOrigin(doc?: Pick<Document, 'addEventListener'>): void;
21
+ /**
22
+ * Gives the focus back before a toast's action runs.
23
+ *
24
+ * sonner remembers the element focused before the focus entered its Toaster,
25
+ * and focuses it again the moment the focus leaves (the list's `onBlur`, and
26
+ * once more when the list unmounts). An action that opens a dialog moves the
27
+ * focus into the dialog, which counts as leaving: the restore pulled it back
28
+ * out, and the dialog lost the control it had focused (WCAG 2.4.3).
29
+ * vue-sonner does not restore (its `blur` does not bubble from the toast's
30
+ * buttons), so a dialog opened there gave the focus back, once closed, to a
31
+ * button that had gone with its toast.
32
+ *
33
+ * Called first by the action, it focuses `origin` (from {@link focusOrigin}),
34
+ * else the origin its Toaster recorded, or blurs the toast's control when
35
+ * neither is left (gone, or inert: `focus()` then silently does nothing).
36
+ * Only while the focus is in the Toaster: an action run from elsewhere leaves
37
+ * it alone. sonner then has nothing left to restore; the action moves the focus where it wants, and
38
+ * a dialog it opens returns the focus to `origin` when it closes.
39
+ */
40
+ export declare function handFocusBack(origin: Element | null, doc?: Pick<Document, 'activeElement'>): void;
41
+ //# sourceMappingURL=toast-focus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toast-focus.d.ts","sourceRoot":"","sources":["../../src/lib/toast-focus.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,eAAO,MAAM,gBAAgB,0BAA0B,CAAC;AAoBxD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAC1B,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,eAAe,CAAC,GACvC,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC,EACpC,MAAM,EAAE,OAAO,GAAG,IAAI,GACpB,OAAO,GAAG,IAAI,CAQhB;AAID;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CACjC,GAAG,GAAE,IAAI,CAAC,QAAQ,EAAE,kBAAkB,CAAY,GAChD,IAAI,CAIN;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,aAAa,CAC5B,MAAM,EAAE,OAAO,GAAG,IAAI,EACtB,GAAG,GAAE,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAY,GAC7C,IAAI,CAUN"}
@@ -0,0 +1,41 @@
1
+ // src/lib/toast-focus.ts
2
+ var TOASTER_SELECTOR = "[data-sonner-toaster]";
3
+ var asFocusable = (value) => typeof value?.closest === "function" && typeof value.focus === "function" ? value : null;
4
+ var toasterOrigins = new WeakMap;
5
+ function focusOrigin(event, origin) {
6
+ const toaster = asFocusable(event.target)?.closest(TOASTER_SELECTOR);
7
+ const from = asFocusable(event.relatedTarget);
8
+ if (from && !from.closest(TOASTER_SELECTOR)) {
9
+ if (toaster)
10
+ toasterOrigins.set(toaster, from);
11
+ return from;
12
+ }
13
+ return origin ?? (toaster ? toasterOrigins.get(toaster) ?? null : null);
14
+ }
15
+ var tracked = new WeakSet;
16
+ function trackToasterOrigin(doc = document) {
17
+ if (tracked.has(doc))
18
+ return;
19
+ tracked.add(doc);
20
+ doc.addEventListener("focusin", (event) => focusOrigin(event, null), true);
21
+ }
22
+ function handFocusBack(origin, doc = document) {
23
+ const active = asFocusable(doc.activeElement);
24
+ if (!active?.closest(TOASTER_SELECTOR))
25
+ return;
26
+ const toaster = active.closest(TOASTER_SELECTOR);
27
+ const candidates = [origin, toaster && toasterOrigins.get(toaster)];
28
+ const target = candidates.map(asFocusable).find((el) => el && el.isConnected !== false);
29
+ target?.focus({ preventScroll: true });
30
+ if (asFocusable(doc.activeElement) === active)
31
+ active.blur();
32
+ }
33
+ export {
34
+ TOASTER_SELECTOR,
35
+ focusOrigin,
36
+ handFocusBack,
37
+ trackToasterOrigin
38
+ };
39
+
40
+ //# debugId=0763A78D43B386CF64756E2164756E21
41
+ //# sourceMappingURL=toast-focus.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/lib/toast-focus.ts"],
4
+ "sourcesContent": [
5
+ "/** The element sonner and vue-sonner render each toast list as. */\nexport const TOASTER_SELECTOR = '[data-sonner-toaster]';\n\n/** What these helpers read of an element: the DOM's own `HTMLElement` fits. */\ntype Focusable = Pick<HTMLElement, 'closest' | 'focus' | 'blur'> & {\n\treadonly isConnected?: boolean;\n};\n\nconst asFocusable = (value: unknown): Focusable | null =>\n\ttypeof (value as Focusable | null)?.closest === 'function' &&\n\ttypeof (value as Focusable).focus === 'function'\n\t\t? (value as Focusable)\n\t\t: null;\n\n/**\n * Where the focus was before it entered each Toaster, keyed by the Toaster's\n * element: a toast reached from another toast has no origin of its own, and\n * falls back to this one.\n */\nconst toasterOrigins = new WeakMap<Element, Element>();\n\n/**\n * Where the focus came from as it entered a toast, for a `focusin` heard on\n * the toast: the element it left (`relatedTarget`), unless that was already\n * in a Toaster, as when the focus moves from one toast to the next; then\n * `origin`, what the toast had recorded so far, is kept, or else what the\n * Toaster recorded when the focus first entered it (a toast that never had\n * the focus from outside, as the next one in the list, still has an origin).\n */\nexport function focusOrigin(\n\tevent: Pick<FocusEvent, 'relatedTarget'> &\n\t\tPartial<Pick<FocusEvent, 'target'>>,\n\torigin: Element | null,\n): Element | null {\n\tconst toaster = asFocusable(event.target)?.closest(TOASTER_SELECTOR);\n\tconst from = asFocusable(event.relatedTarget);\n\tif (from && !from.closest(TOASTER_SELECTOR)) {\n\t\tif (toaster) toasterOrigins.set(toaster, from as unknown as Element);\n\t\treturn from as unknown as Element;\n\t}\n\treturn origin ?? (toaster ? (toasterOrigins.get(toaster) ?? null) : null);\n}\n\nconst tracked = new WeakSet<object>();\n\n/**\n * Records, for each Toaster, where the focus came from as it entered it.\n * A toast hears the focus only once it reaches one of its own controls, and\n * with a keyboard it first lands on the list item that wraps the toast\n * (sonner's and vue-sonner's `li` take `tabindex=\"0\"`), which no toast hears:\n * this listens on the document, once, so the Toaster knows the page control\n * the focus left. Call it when a toast mounts; calling it again does nothing.\n */\nexport function trackToasterOrigin(\n\tdoc: Pick<Document, 'addEventListener'> = document,\n): void {\n\tif (tracked.has(doc)) return;\n\ttracked.add(doc);\n\tdoc.addEventListener('focusin', (event) => focusOrigin(event, null), true);\n}\n\n/**\n * Gives the focus back before a toast's action runs.\n *\n * sonner remembers the element focused before the focus entered its Toaster,\n * and focuses it again the moment the focus leaves (the list's `onBlur`, and\n * once more when the list unmounts). An action that opens a dialog moves the\n * focus into the dialog, which counts as leaving: the restore pulled it back\n * out, and the dialog lost the control it had focused (WCAG 2.4.3).\n * vue-sonner does not restore (its `blur` does not bubble from the toast's\n * buttons), so a dialog opened there gave the focus back, once closed, to a\n * button that had gone with its toast.\n *\n * Called first by the action, it focuses `origin` (from {@link focusOrigin}),\n * else the origin its Toaster recorded, or blurs the toast's control when\n * neither is left (gone, or inert: `focus()` then silently does nothing).\n * Only while the focus is in the Toaster: an action run from elsewhere leaves\n * it alone. sonner then has nothing left to restore; the action moves the focus where it wants, and\n * a dialog it opens returns the focus to `origin` when it closes.\n */\nexport function handFocusBack(\n\torigin: Element | null,\n\tdoc: Pick<Document, 'activeElement'> = document,\n): void {\n\tconst active = asFocusable(doc.activeElement);\n\tif (!active?.closest(TOASTER_SELECTOR)) return;\n\tconst toaster = active.closest(TOASTER_SELECTOR);\n\tconst candidates = [origin, toaster && toasterOrigins.get(toaster)];\n\tconst target = candidates\n\t\t.map(asFocusable)\n\t\t.find((el) => el && el.isConnected !== false);\n\ttarget?.focus({ preventScroll: true });\n\tif (asFocusable(doc.activeElement) === active) active.blur();\n}\n"
6
+ ],
7
+ "mappings": ";AACO,IAAM,mBAAmB;AAOhC,IAAM,cAAc,CAAC,UACpB,OAAQ,OAA4B,YAAY,cAChD,OAAQ,MAAoB,UAAU,aAClC,QACD;AAOJ,IAAM,iBAAiB,IAAI;AAUpB,SAAS,WAAW,CAC1B,OAEA,QACiB;AAAA,EACjB,MAAM,UAAU,YAAY,MAAM,MAAM,GAAG,QAAQ,gBAAgB;AAAA,EACnE,MAAM,OAAO,YAAY,MAAM,aAAa;AAAA,EAC5C,IAAI,QAAQ,CAAC,KAAK,QAAQ,gBAAgB,GAAG;AAAA,IAC5C,IAAI;AAAA,MAAS,eAAe,IAAI,SAAS,IAA0B;AAAA,IACnE,OAAO;AAAA,EACR;AAAA,EACA,OAAO,WAAW,UAAW,eAAe,IAAI,OAAO,KAAK,OAAQ;AAAA;AAGrE,IAAM,UAAU,IAAI;AAUb,SAAS,kBAAkB,CACjC,MAA0C,UACnC;AAAA,EACP,IAAI,QAAQ,IAAI,GAAG;AAAA,IAAG;AAAA,EACtB,QAAQ,IAAI,GAAG;AAAA,EACf,IAAI,iBAAiB,WAAW,CAAC,UAAU,YAAY,OAAO,IAAI,GAAG,IAAI;AAAA;AAsBnE,SAAS,aAAa,CAC5B,QACA,MAAuC,UAChC;AAAA,EACP,MAAM,SAAS,YAAY,IAAI,aAAa;AAAA,EAC5C,IAAI,CAAC,QAAQ,QAAQ,gBAAgB;AAAA,IAAG;AAAA,EACxC,MAAM,UAAU,OAAO,QAAQ,gBAAgB;AAAA,EAC/C,MAAM,aAAa,CAAC,QAAQ,WAAW,eAAe,IAAI,OAAO,CAAC;AAAA,EAClE,MAAM,SAAS,WACb,IAAI,WAAW,EACf,KAAK,CAAC,OAAO,MAAM,GAAG,gBAAgB,KAAK;AAAA,EAC7C,QAAQ,MAAM,EAAE,eAAe,KAAK,CAAC;AAAA,EACrC,IAAI,YAAY,IAAI,aAAa,MAAM;AAAA,IAAQ,OAAO,KAAK;AAAA;",
8
+ "debugId": "0763A78D43B386CF64756E2164756E21",
9
+ "names": []
10
+ }
package/docs/roadmap.md CHANGED
@@ -31,6 +31,9 @@ Nothing in progress right now; see Next.
31
31
 
32
32
  ## Shipped
33
33
 
34
+ - **The focus a toast's action hands back** (`/toast-focus`) — where the
35
+ focus was before it entered a toast, given back before the action runs, so
36
+ a dialog the action opens keeps the focus in both libraries. 0.7.0.
34
37
  - **A field's first reachable control** (`/focus-control`) — the control of a
35
38
  field made of several a user can reach, and a stand-in a form library can
36
39
  focus, so both libraries focus the first invalid field. 0.5.0.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nxgt/material-core",
3
- "version": "0.6.0",
4
- "description": "The framework-free core of @nxgt/material and @nxgt/material-vue: the PDF document model and its react-pdf renderer, print formatters, PDF file helpers, the PDF viewer's pure modules, the currency digits, the file-size formatter, the field-blur layers, a field's first focusable control, the aria-describedby joiner, the inert background of a modal layer, Escape forwarded while an overlay closes, the Toaster's touch-hover guard, the zod issue messages, the date range presets, the file preview's kind detection and text reader, the pdfkit fonts Vite plugin and the Node-only pdfkit fonts resolution for Next.js and Nuxt builds",
3
+ "version": "0.7.0",
4
+ "description": "The framework-free core of @nxgt/material and @nxgt/material-vue: the PDF document model and its react-pdf renderer, print formatters, PDF file helpers, the PDF viewer's pure modules, the currency digits, the file-size formatter, the field-blur layers, a field's first focusable control, the aria-describedby joiner, the inert background of a modal layer, Escape forwarded while an overlay closes, the Toaster's touch-hover guard and its focus hand-back, the zod issue messages, the date range presets, the file preview's kind detection and text reader, the pdfkit fonts Vite plugin and the Node-only pdfkit fonts resolution for Next.js and Nuxt builds",
5
5
  "license": "MIT",
6
6
  "bugs": {
7
7
  "email": "stevegires@gmail.com"
@@ -113,6 +113,11 @@
113
113
  "import": "./dist/lib/touch-hover.js",
114
114
  "default": "./dist/lib/touch-hover.js"
115
115
  },
116
+ "./toast-focus": {
117
+ "types": "./dist/lib/toast-focus.d.ts",
118
+ "import": "./dist/lib/toast-focus.js",
119
+ "default": "./dist/lib/toast-focus.js"
120
+ },
116
121
  "./zod-issue-messages": {
117
122
  "types": "./dist/lib/zod-issue-messages.d.ts",
118
123
  "import": "./dist/lib/zod-issue-messages.js",
@@ -216,6 +221,7 @@
216
221
  "src/lib/escape-forward.ts",
217
222
  "src/lib/described-by.ts",
218
223
  "src/lib/touch-hover.ts",
224
+ "src/lib/toast-focus.ts",
219
225
  "src/lib/zod-issue-messages.ts",
220
226
  "src/lib/inert-others.ts",
221
227
  "src/lib/row-activation.ts",