@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 +20 -0
- package/README.md +58 -4
- package/dist/lib/emoji-groups.d.ts +20 -0
- package/dist/lib/emoji-groups.d.ts.map +1 -0
- package/dist/lib/emoji-groups.js +58 -0
- package/dist/lib/emoji-groups.js.map +10 -0
- package/dist/lib/toast-focus.d.ts +41 -0
- package/dist/lib/toast-focus.d.ts.map +1 -0
- package/dist/lib/toast-focus.js +41 -0
- package/dist/lib/toast-focus.js.map +10 -0
- package/docs/roadmap.md +5 -0
- package/package.json +14 -2
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
|
|
9
|
-
file preview's kind detection, the fields' blur
|
|
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
|
|
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.
|
|
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",
|