@nxgt/material-core 0.5.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,25 @@
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
+
13
+ ## 0.6.0
14
+
15
+ ### Minor Changes
16
+
17
+ - [#468](https://github.com/softistx/nxgt-material/pull/468) [`5f984fa`](https://github.com/softistx/nxgt-material/commit/5f984fa95a2944e6c71acd81d0272730d87c2c0a) Thanks [@SteveGT96](https://github.com/SteveGT96)! - New subpath `@nxgt/material-core/emoji-groups`: `groupKey`, `groupEmojis`, `searchEmojis`, `groupName`, `GROUP_NAMES` and the type `EmojiItem`, the emoji list's grouping, search and group names that both libraries kept a copy of.
18
+
19
+ ### Patch Changes
20
+
21
+ - [#470](https://github.com/softistx/nxgt-material/pull/470) [`853d093`](https://github.com/softistx/nxgt-material/commit/853d093f0620dc6940ea1ce69e025b61e3b0094d) Thanks [@SteveGT96](https://github.com/SteveGT96)! - The roadmap names the release that ships the two Tabs looks and the emoji groups.
22
+
3
23
  ## 0.5.0
4
24
 
5
25
  ### Minor Changes
package/README.md CHANGED
@@ -5,13 +5,14 @@ The framework-free core that [`@nxgt/material`](https://www.npmjs.com/package/@n
5
5
  (Vue) share, as one copy: the PDF document model and its react-pdf renderer,
6
6
  the print formatters, the PDF file helpers, the PDF viewer's framework-free
7
7
  half (page geometry, scroll spy, text search), the currency field's decimals,
8
- the file-size formatter, the slider's tick marks, the date range presets, the
9
- file preview's kind detection, the fields' blur rule, a field's first
10
- reachable control (`/focus-control`) and its `aria-describedby`
8
+ the file-size formatter, the slider's tick marks, the emoji list's groups,
9
+ the date range presets, the file preview's kind detection, the fields' blur
10
+ rule, a field's first reachable control (`/focus-control`) and its `aria-describedby`
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,10 +86,12 @@ 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'` |
91
93
  | `/slider-ticks` | `tickValues` — where a slider's tick marks sit |
94
+ | `/emoji-groups` | `groupKey`, `groupEmojis`, `searchEmojis`, `groupName`, `GROUP_NAMES` and the type `EmojiItem` — how the emoji list groups, searches and names its emojis |
92
95
  | `/date-range` | `DATE_RANGE_PRESET_IDS`, `presetRange`, `resolvePresetRange`, `isSameDayRange`, `isRangeDisabled`, `withTimeOf`, `endOfDayTime`, `clampRangeEnd`, `keepRangeTimes` and the types `DayRange`, `PartialDayRange`, `DateRangePreset`, `DateRangePresetId`, `DateRangePresetOption`, `DateRangeRules` — `DateRangeField`'s presets and times |
93
96
  | `/file-preview` | `fileKind`, `describeFile`, `safePreviewUrl`, `readTextPreview`, `FilePreviewHttpError`, `FilePreviewUnsafeUrlError`, `TEXT_PREVIEW_MAX_BYTES`, `fileExtension`, `fileNameFromUrl`, `dataUrlType`, `kindOfMimeType`, `normaliseMimeType`, `isTextFileKind`, `TEXT_FILE_KINDS` and the types `FileKind`, `FileKindInput`, `DescribedFile`, `FilePreviewSource`, `TextPreview` — `FilePreview`'s kind detection, URL allow-list and text reader |
94
97
  | `/vite` | `pdfkitStandardFonts()` — see below |
@@ -372,6 +375,22 @@ tickValues(0, 100, true, 1).length; // 21 — capped
372
375
  tickValues(5, 5, true); // [5] — min === max
373
376
  ```
374
377
 
378
+ ### The emoji list's groups: `/emoji-groups`
379
+
380
+ How both libraries' `EmojiList` groups, searches and names its emojis:
381
+ `groupKey` turns a catalog group into a key, `groupEmojis` builds the tabs
382
+ (`all`, `recent`, then one per group), `searchEmojis` filters by name or
383
+ shortcode, `groupName` gives a key's English name (a key it does not know
384
+ reads as its words).
385
+
386
+ ```ts
387
+ import { groupName, searchEmojis } from '@nxgt/material-core/emoji-groups';
388
+
389
+ groupName('people_body'); // 'People & body'
390
+ groupName('hand_signs'); // 'Hand signs'
391
+ searchEmojis([{ name: 'fire' }], 'FIR'); // [{ name: 'fire' }]
392
+ ```
393
+
375
394
  ### A date range's presets and times: `/date-range`
376
395
 
377
396
  The pure half of both libraries' `DateRangeField` (`presets`, `withTime` /
@@ -596,6 +615,41 @@ It also holds the two constants both libraries' `Toaster` share: `TOASTER_PHONE_
596
615
  screen) and `TOASTER_PHONE_POSITION` (`'top-center'`, where toasts go on a phone
597
616
  when the caller names no `position`).
598
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
+
599
653
  ### Zod issue messages: `/zod-issue-messages`
600
654
 
601
655
  The two libraries' zod error maps turn an issue into text in the language
@@ -0,0 +1,20 @@
1
+ /** What the grouping functions read of an emoji. */
2
+ export type EmojiItem = {
3
+ name: string;
4
+ emoji?: string;
5
+ group?: string | null;
6
+ shortcodes?: string[];
7
+ };
8
+ export declare function groupKey(group?: string | null): string;
9
+ export declare function groupEmojis(emojis: EmojiItem[], recent: string[]): {
10
+ [k: string]: EmojiItem[];
11
+ };
12
+ export declare function searchEmojis(emojis: EmojiItem[], query: string): EmojiItem[] | null;
13
+ export declare const GROUP_NAMES: Record<string, string>;
14
+ /**
15
+ * A group's English name, the fallback of `material.emoji.groups.<key>`: the
16
+ * name of the icon-only tab that shows it. A key the bundles do not name
17
+ * (`hand_signs`) reads as its words (`Hand signs`).
18
+ */
19
+ export declare function groupName(key: string): string;
20
+ //# sourceMappingURL=emoji-groups.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emoji-groups.d.ts","sourceRoot":"","sources":["../../src/lib/emoji-groups.ts"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,MAAM,MAAM,SAAS,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB,CAAC;AAEF,wBAAgB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,UAI7C;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE;;EAehE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,MAAM,sBAO9D;AAED,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAa9C,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,UAGpC"}
@@ -0,0 +1,58 @@
1
+ // src/lib/emoji-groups.ts
2
+ function groupKey(group) {
3
+ const raw = group?.trim();
4
+ if (!raw)
5
+ return "general";
6
+ return raw.toLowerCase().replace(/ & /g, "_").replace(/\s+/g, "_");
7
+ }
8
+ function groupEmojis(emojis, recent) {
9
+ const groups = {
10
+ all: emojis
11
+ };
12
+ const recentItems = recent.map((name) => emojis.find((item) => item.name === name)).filter((item) => Boolean(item));
13
+ if (recentItems.length > 0)
14
+ groups.recent = recentItems;
15
+ for (const item of emojis) {
16
+ const key = groupKey(item.group);
17
+ groups[key] = [...groups[key] ?? [], item];
18
+ }
19
+ return Object.fromEntries(Object.entries(groups).filter(([, items]) => items.length > 0));
20
+ }
21
+ function searchEmojis(emojis, query) {
22
+ const needle = query.trim().toLowerCase();
23
+ if (!needle)
24
+ return null;
25
+ return emojis.filter((item) => {
26
+ if (item.name.toLowerCase().includes(needle))
27
+ return true;
28
+ return item.shortcodes?.some((code) => code.toLowerCase().includes(needle));
29
+ });
30
+ }
31
+ var GROUP_NAMES = {
32
+ all: "All",
33
+ recent: "Recent",
34
+ general: "General",
35
+ people_body: "People & body",
36
+ components: "Components",
37
+ animals_nature: "Animals & nature",
38
+ food_drink: "Food & drink",
39
+ travel_places: "Travel & places",
40
+ activities: "Activities",
41
+ objects: "Objects",
42
+ symbols: "Symbols",
43
+ flags: "Flags"
44
+ };
45
+ function groupName(key) {
46
+ const words = key.replace(/_/g, " ");
47
+ return GROUP_NAMES[key] ?? words.charAt(0).toUpperCase() + words.slice(1);
48
+ }
49
+ export {
50
+ GROUP_NAMES,
51
+ groupEmojis,
52
+ groupKey,
53
+ groupName,
54
+ searchEmojis
55
+ };
56
+
57
+ //# debugId=78A2C18812F7925A64756E2164756E21
58
+ //# sourceMappingURL=emoji-groups.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/lib/emoji-groups.ts"],
4
+ "sourcesContent": [
5
+ "/** What the grouping functions read of an emoji. */\nexport type EmojiItem = {\n\tname: string;\n\temoji?: string;\n\tgroup?: string | null;\n\tshortcodes?: string[];\n};\n\nexport function groupKey(group?: string | null) {\n\tconst raw = group?.trim();\n\tif (!raw) return 'general';\n\treturn raw.toLowerCase().replace(/ & /g, '_').replace(/\\s+/g, '_');\n}\n\nexport function groupEmojis(emojis: EmojiItem[], recent: string[]) {\n\tconst groups: Record<string, EmojiItem[]> = {\n\t\tall: emojis,\n\t};\n\tconst recentItems = recent\n\t\t.map((name) => emojis.find((item) => item.name === name))\n\t\t.filter((item): item is EmojiItem => Boolean(item));\n\tif (recentItems.length > 0) groups.recent = recentItems;\n\tfor (const item of emojis) {\n\t\tconst key = groupKey(item.group);\n\t\tgroups[key] = [...(groups[key] ?? []), item];\n\t}\n\treturn Object.fromEntries(\n\t\tObject.entries(groups).filter(([, items]) => items.length > 0),\n\t);\n}\n\nexport function searchEmojis(emojis: EmojiItem[], query: string) {\n\tconst needle = query.trim().toLowerCase();\n\tif (!needle) return null;\n\treturn emojis.filter((item) => {\n\t\tif (item.name.toLowerCase().includes(needle)) return true;\n\t\treturn item.shortcodes?.some((code) => code.toLowerCase().includes(needle));\n\t});\n}\n\nexport const GROUP_NAMES: Record<string, string> = {\n\tall: 'All',\n\trecent: 'Recent',\n\tgeneral: 'General',\n\tpeople_body: 'People & body',\n\tcomponents: 'Components',\n\tanimals_nature: 'Animals & nature',\n\tfood_drink: 'Food & drink',\n\ttravel_places: 'Travel & places',\n\tactivities: 'Activities',\n\tobjects: 'Objects',\n\tsymbols: 'Symbols',\n\tflags: 'Flags',\n};\n\n/**\n * A group's English name, the fallback of `material.emoji.groups.<key>`: the\n * name of the icon-only tab that shows it. A key the bundles do not name\n * (`hand_signs`) reads as its words (`Hand signs`).\n */\nexport function groupName(key: string) {\n\tconst words = key.replace(/_/g, ' ');\n\treturn GROUP_NAMES[key] ?? words.charAt(0).toUpperCase() + words.slice(1);\n}\n"
6
+ ],
7
+ "mappings": ";AAQO,SAAS,QAAQ,CAAC,OAAuB;AAAA,EAC/C,MAAM,MAAM,OAAO,KAAK;AAAA,EACxB,IAAI,CAAC;AAAA,IAAK,OAAO;AAAA,EACjB,OAAO,IAAI,YAAY,EAAE,QAAQ,QAAQ,GAAG,EAAE,QAAQ,QAAQ,GAAG;AAAA;AAG3D,SAAS,WAAW,CAAC,QAAqB,QAAkB;AAAA,EAClE,MAAM,SAAsC;AAAA,IAC3C,KAAK;AAAA,EACN;AAAA,EACA,MAAM,cAAc,OAClB,IAAI,CAAC,SAAS,OAAO,KAAK,CAAC,SAAS,KAAK,SAAS,IAAI,CAAC,EACvD,OAAO,CAAC,SAA4B,QAAQ,IAAI,CAAC;AAAA,EACnD,IAAI,YAAY,SAAS;AAAA,IAAG,OAAO,SAAS;AAAA,EAC5C,WAAW,QAAQ,QAAQ;AAAA,IAC1B,MAAM,MAAM,SAAS,KAAK,KAAK;AAAA,IAC/B,OAAO,OAAO,CAAC,GAAI,OAAO,QAAQ,CAAC,GAAI,IAAI;AAAA,EAC5C;AAAA,EACA,OAAO,OAAO,YACb,OAAO,QAAQ,MAAM,EAAE,OAAO,IAAI,WAAW,MAAM,SAAS,CAAC,CAC9D;AAAA;AAGM,SAAS,YAAY,CAAC,QAAqB,OAAe;AAAA,EAChE,MAAM,SAAS,MAAM,KAAK,EAAE,YAAY;AAAA,EACxC,IAAI,CAAC;AAAA,IAAQ,OAAO;AAAA,EACpB,OAAO,OAAO,OAAO,CAAC,SAAS;AAAA,IAC9B,IAAI,KAAK,KAAK,YAAY,EAAE,SAAS,MAAM;AAAA,MAAG,OAAO;AAAA,IACrD,OAAO,KAAK,YAAY,KAAK,CAAC,SAAS,KAAK,YAAY,EAAE,SAAS,MAAM,CAAC;AAAA,GAC1E;AAAA;AAGK,IAAM,cAAsC;AAAA,EAClD,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,gBAAgB;AAAA,EAChB,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,SAAS;AAAA,EACT,OAAO;AACR;AAOO,SAAS,SAAS,CAAC,KAAa;AAAA,EACtC,MAAM,QAAQ,IAAI,QAAQ,MAAM,GAAG;AAAA,EACnC,OAAO,YAAY,QAAQ,MAAM,OAAO,CAAC,EAAE,YAAY,IAAI,MAAM,MAAM,CAAC;AAAA;",
8
+ "debugId": "78A2C18812F7925A64756E2164756E21",
9
+ "names": []
10
+ }
@@ -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.
@@ -64,6 +67,8 @@ Nothing in progress right now; see Next.
64
67
  - **Slider ticks** (`/slider-ticks`) — the values a slider's tick marks sit
65
68
  at, which both libraries' `SliderField` (`ticks`) read from here instead of
66
69
  keeping a copy each. 0.3.0.
70
+ - **Emoji groups** (`/emoji-groups`) — how the emoji list groups, searches
71
+ and names its emojis, which both libraries' `EmojiList` read from here. 0.6.0.
67
72
  - **`formatFileSize` and the pdfkit fonts resolution** — the byte-count
68
73
  formatter both libraries' file list and upload field use
69
74
  (`/format-file-size`), and the Node-only resolution of pdfkit's standard
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nxgt/material-core",
3
- "version": "0.5.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",
@@ -138,6 +143,11 @@
138
143
  "import": "./dist/lib/slider-ticks.js",
139
144
  "default": "./dist/lib/slider-ticks.js"
140
145
  },
146
+ "./emoji-groups": {
147
+ "types": "./dist/lib/emoji-groups.d.ts",
148
+ "import": "./dist/lib/emoji-groups.js",
149
+ "default": "./dist/lib/emoji-groups.js"
150
+ },
141
151
  "./date-range": {
142
152
  "types": "./dist/lib/date-range.d.ts",
143
153
  "import": "./dist/lib/date-range.js",
@@ -211,11 +221,13 @@
211
221
  "src/lib/escape-forward.ts",
212
222
  "src/lib/described-by.ts",
213
223
  "src/lib/touch-hover.ts",
224
+ "src/lib/toast-focus.ts",
214
225
  "src/lib/zod-issue-messages.ts",
215
226
  "src/lib/inert-others.ts",
216
227
  "src/lib/row-activation.ts",
217
228
  "src/lib/format-file-size.ts",
218
229
  "src/lib/slider-ticks.ts",
230
+ "src/lib/emoji-groups.ts",
219
231
  "src/lib/date-range.ts",
220
232
  "src/file-preview/index.ts",
221
233
  "src/pdf-viewer/index.ts",