@nxgt/material-core 0.4.0 → 0.6.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 +28 -0
- package/README.md +171 -7
- package/dist/lib/described-by.d.ts +6 -0
- package/dist/lib/described-by.d.ts.map +1 -0
- package/dist/lib/described-by.js +10 -0
- package/dist/lib/described-by.js.map +10 -0
- 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/escape-forward.d.ts +20 -0
- package/dist/lib/escape-forward.d.ts.map +1 -0
- package/dist/lib/escape-forward.js +50 -0
- package/dist/lib/escape-forward.js.map +10 -0
- package/dist/lib/focus-control.d.ts +27 -0
- package/dist/lib/focus-control.d.ts.map +1 -0
- package/dist/lib/focus-control.js +43 -0
- package/dist/lib/focus-control.js.map +10 -0
- package/dist/lib/inert-others.d.ts +20 -0
- package/dist/lib/inert-others.d.ts.map +1 -0
- package/dist/lib/inert-others.js +116 -0
- package/dist/lib/inert-others.js.map +10 -0
- package/dist/lib/touch-hover.d.ts +24 -0
- package/dist/lib/touch-hover.d.ts.map +1 -0
- package/dist/lib/touch-hover.js +39 -0
- package/dist/lib/touch-hover.js.map +10 -0
- package/dist/lib/zod-issue-messages.d.ts +39 -0
- package/dist/lib/zod-issue-messages.d.ts.map +1 -0
- package/dist/lib/zod-issue-messages.js +44 -0
- package/dist/lib/zod-issue-messages.js.map +10 -0
- package/docs/roadmap.md +21 -0
- package/package.json +44 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @nxgt/material-core
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#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.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [#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.
|
|
12
|
+
|
|
13
|
+
## 0.5.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- [#456](https://github.com/softistx/nxgt-material/pull/456) [`7200d8c`](https://github.com/softistx/nxgt-material/commit/7200d8c05e8b94de10910ccb3609be3ed1b59061) Thanks [@SteveGT96](https://github.com/SteveGT96)! - New subpath `@nxgt/material-core/escape-forward`: `forwardEscape(content, event)`, called from an overlay content's escape handler, presses Escape again for the overlay below once a content that was closing (`data-state="closed"`, still on the page for its exit animation) has left. Radix's and reka's dismissable layers give Escape to the highest layer only and drop a dialog's layer when it unmounts, not when it closes, so an Escape pressed during the animation was lost. Both libraries' dialog, alert dialog, sheet and drawer contents use it.
|
|
18
|
+
|
|
19
|
+
- [#450](https://github.com/softistx/nxgt-material/pull/450) [`8e568c3`](https://github.com/softistx/nxgt-material/commit/8e568c3544327c1ba273482963ec13e1930d3be6) Thanks [@SteveGT96](https://github.com/SteveGT96)! - New subpath `@nxgt/material-core/focus-control`: `firstControl(roots)` returns the first control of a field a user can reach (one they can Tab to that is rendered, not a `display: none` input a library keeps), and `focusHandle(roots)` builds the stand-in element a form library focuses (`focus()`, `select()`, `setCustomValidity()`, `reportValidity()`), for a field made of several controls. Both libraries use it: `@nxgt/material`'s fields hand it to React Hook Form as `field.ref`, and `@nxgt/material-vue`'s `focusField` / `focusFirstInvalid` look the control up with it.
|
|
20
|
+
|
|
21
|
+
New subpath `@nxgt/material-core/zod-issue-messages`: `sizeMessage(issue, { translate, lookup })` and `invalidFormatMessage(issue, { translate, lookup })`, the text of a zod `too_big` / `too_small` and `invalid_format` issue, which both libraries' zod error maps use. Core reads the issue by shape and has no zod dependency.
|
|
22
|
+
|
|
23
|
+
New subpath `@nxgt/material-core/described-by`: `describedBy(own, ...ids)`, a field's `aria-describedby` value — the caller's ids first, then the field's help or error ids, the falsy ones left out, `undefined` when none is left. Both libraries' text-like fields use it.
|
|
24
|
+
|
|
25
|
+
- [#461](https://github.com/softistx/nxgt-material/pull/461) [`f6cb264`](https://github.com/softistx/nxgt-material/commit/f6cb2649cbdae34201eba18d0ec69846a281deba) Thanks [@SteveGT96](https://github.com/SteveGT96)! - New subpath `@nxgt/material-core/inert-others`: `inertOthers(content, keep?)` makes everything in `<body>` but a modal layer's content `inert` — out of the tab order and of the accessibility tree — and returns the undo. It marks what `aria-hidden`'s `hideOthers` marks `aria-hidden` (the siblings of the content and of its ancestors) and keeps what `hideOthers` keeps (`[aria-live], script`) and `keep` reachable. Elements mounted after the call are not made inert. Elements are counted per layer, so nested layers undo in any order; an element already `inert` is left as it was; focus left on the page behind goes to the content; the undo also runs by itself when the content leaves the document. Marked elements carry `data-nxgt-inert` (`INERT_MARKER`). Both libraries' modal layers use it.
|
|
26
|
+
|
|
27
|
+
- [#457](https://github.com/softistx/nxgt-material/pull/457) [`025bf28`](https://github.com/softistx/nxgt-material/commit/025bf28872d1954596cf7d7054245646309e4525) Thanks [@SteveGT96](https://github.com/SteveGT96)! - New subpath `@nxgt/material-core/touch-hover`: `ignoreTouchHover(document, selector)` keeps a tap from hovering whatever matches `selector`. After a tap, a browser emulates a mouse that arrives over what was tapped (`mouseover`, `mouseenter`, `mousemove`) and does not leave until the next tap elsewhere; while the last pointer was a finger, those three events are stopped in the capture phase, before any handler inside the element hears them. A mouse or a pen hovers as before, and `mouseout` / `mouseleave` always pass. It returns the function that removes its listeners. Both libraries' `Toaster` use it, so a toast a finger tapped no longer stays paused.
|
|
28
|
+
|
|
29
|
+
The subpath also exports `TOASTER_PHONE_QUERY` and `TOASTER_PHONE_POSITION`, the phone width and default position both libraries' `Toaster` share.
|
|
30
|
+
|
|
3
31
|
## 0.4.0
|
|
4
32
|
|
|
5
33
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -5,17 +5,27 @@ 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
|
|
9
|
-
|
|
10
|
-
|
|
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
|
+
(`/described-by`), the page behind a modal layer made inert
|
|
12
|
+
(`/inert-others`), Escape pressed while an overlay closes
|
|
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
|
|
15
|
+
messages (`/zod-issue-messages`), the Vite plugin that bundles pdfkit's
|
|
16
|
+
standard fonts, and the Node-only resolution of those fonts that the Next.js
|
|
17
|
+
helper and the Nuxt module run at build time.
|
|
11
18
|
|
|
12
19
|
**You usually do not install it yourself.** Both libraries depend on it and
|
|
13
20
|
re-export what they always exported, at the same subpaths:
|
|
14
21
|
`@nxgt/material/print`, `@nxgt/material-vue/pdf`, `/lib` and `/vite` read
|
|
15
|
-
from here, and so do their `PdfReader`, `CurrencyField`, form fields
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
from here, and so do their `PdfReader`, `CurrencyField`, form fields (their
|
|
23
|
+
focus, blur, `aria-describedby` and zod messages), file list and upload
|
|
24
|
+
field (`formatFileSize`), modal layers (the inert page behind, Escape while
|
|
25
|
+
closing), `Toaster`, `DataTable` rows, `@nxgt/material/next` and
|
|
26
|
+
`@nxgt/material-vue/nuxt`. Import it directly when you want the model or a
|
|
27
|
+
formatter without a UI library, or one of these helpers in a component of
|
|
28
|
+
your own.
|
|
19
29
|
|
|
20
30
|
```ts
|
|
21
31
|
import { heading, pdfDocument, text } from '@nxgt/material-core/pdf/model';
|
|
@@ -70,9 +80,16 @@ built-in.
|
|
|
70
80
|
| `/pdf-viewer` | The PDF viewer's pure half: page geometry and fit (`readerFitScale`, `placeholderSize`, `rotatedSize`, `PdfPageSize`, `FitMode`), page sizes (`readPageSizes`, `createPageSizesLoader`), the scroll spy (`createScrollSpy`, `revealHit`, `scrollIntoArea`), the opening page's pin (`pinToPage`, whose optional `onRelease` runs once when it lets go, and `pinHolds`, whether the view is still where it puts the page), text search (`pageTextFromItems`, `findTextMatches`, `searchDocument`), `escapeHtml`, `mapPdfOutline` |
|
|
71
81
|
| `/currency-digits` | `currencyFractionDigits` — the decimals `CurrencyField` shows |
|
|
72
82
|
| `/field-blur-layers` | `layerOf`, `openerOf`, `FieldLayers` — whose overlay a layer of the page is, for a field's blur |
|
|
83
|
+
| `/focus-control` | `firstControl`, `focusHandle`, `FocusHandle` — the first control of a field a user can reach, and a stand-in element that focuses it for a form library |
|
|
84
|
+
| `/escape-forward` | `forwardEscape` — Escape pressed while an overlay closes, pressed again for the overlay below once it has gone |
|
|
85
|
+
| `/described-by` | `describedBy` — an `aria-describedby` value from the caller's ids and a field's help and error ids |
|
|
86
|
+
| `/inert-others` | `inertOthers`, `INERT_MARKER` — the rest of the page `inert` behind a modal layer, live regions kept, nested layers counted |
|
|
87
|
+
| `/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 |
|
|
88
|
+
| `/zod-issue-messages` | `sizeMessage`, `invalidFormatMessage`, `IssueText` — the translated text of a zod `too_big` / `too_small` and `invalid_format` issue |
|
|
73
89
|
| `/row-activation` | `isRowActivation`, `followRowLink`, `ROW_CONTROL_SELECTOR`, `ROW_LINK_SELECTOR`, `RowClick` — whether a click on a table row follows its link, and following it |
|
|
74
90
|
| `/format-file-size` | `formatFileSize` — a byte count as `'1.5 KB'` |
|
|
75
91
|
| `/slider-ticks` | `tickValues` — where a slider's tick marks sit |
|
|
92
|
+
| `/emoji-groups` | `groupKey`, `groupEmojis`, `searchEmojis`, `groupName`, `GROUP_NAMES` and the type `EmojiItem` — how the emoji list groups, searches and names its emojis |
|
|
76
93
|
| `/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 |
|
|
77
94
|
| `/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 |
|
|
78
95
|
| `/vite` | `pdfkitStandardFonts()` — see below |
|
|
@@ -356,6 +373,22 @@ tickValues(0, 100, true, 1).length; // 21 — capped
|
|
|
356
373
|
tickValues(5, 5, true); // [5] — min === max
|
|
357
374
|
```
|
|
358
375
|
|
|
376
|
+
### The emoji list's groups: `/emoji-groups`
|
|
377
|
+
|
|
378
|
+
How both libraries' `EmojiList` groups, searches and names its emojis:
|
|
379
|
+
`groupKey` turns a catalog group into a key, `groupEmojis` builds the tabs
|
|
380
|
+
(`all`, `recent`, then one per group), `searchEmojis` filters by name or
|
|
381
|
+
shortcode, `groupName` gives a key's English name (a key it does not know
|
|
382
|
+
reads as its words).
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
import { groupName, searchEmojis } from '@nxgt/material-core/emoji-groups';
|
|
386
|
+
|
|
387
|
+
groupName('people_body'); // 'People & body'
|
|
388
|
+
groupName('hand_signs'); // 'Hand signs'
|
|
389
|
+
searchEmojis([{ name: 'fire' }], 'FIR'); // [{ name: 'fire' }]
|
|
390
|
+
```
|
|
391
|
+
|
|
359
392
|
### A date range's presets and times: `/date-range`
|
|
360
393
|
|
|
361
394
|
The pure half of both libraries' `DateRangeField` (`presets`, `withTime` /
|
|
@@ -466,6 +499,137 @@ field.addEventListener('focusout', (event) => {
|
|
|
466
499
|
});
|
|
467
500
|
```
|
|
468
501
|
|
|
502
|
+
### Focusing a field made of several controls: `/focus-control`
|
|
503
|
+
|
|
504
|
+
`firstControl(roots)` returns the first control in the roots a user can
|
|
505
|
+
reach (one they can Tab to that is rendered, not a `display: none` input a
|
|
506
|
+
library keeps), or `null`. `focusHandle(roots)` builds a stand-in element for
|
|
507
|
+
a form library that focuses an invalid field by calling `focus()` on what the
|
|
508
|
+
field registered, as React Hook Form does with `field.ref`: `focus()` focuses
|
|
509
|
+
that first control, looked up when it is called (and once more a turn later,
|
|
510
|
+
when a re-render replaced it). Pass a live collection — the `Set` of the
|
|
511
|
+
field's roots — and both read the DOM only when called.
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
import { firstControl, focusHandle } from '@nxgt/material-core/focus-control';
|
|
515
|
+
|
|
516
|
+
const roots = new Set<Element>([document.querySelector('#delivery-dates')!]);
|
|
517
|
+
firstControl(roots)?.focus(); // the first date input, not a hidden one
|
|
518
|
+
field.ref(focusHandle(roots)); // React Hook Form: shouldFocusError reaches it
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### Escape while an overlay closes: `/escape-forward`
|
|
522
|
+
|
|
523
|
+
Radix's and reka's dismissable layers give Escape to the highest layer only,
|
|
524
|
+
and a dialog's layer leaves their stack when its content unmounts, not when
|
|
525
|
+
it closes: an Escape pressed during the exit animation went to a dialog that
|
|
526
|
+
was already closed, and the overlay below stayed open.
|
|
527
|
+
`forwardEscape(content, event)`, called from the content's escape handler
|
|
528
|
+
after the caller's, does nothing unless `content` has `data-state="closed"`
|
|
529
|
+
and the default was not prevented. Then it prevents the default and presses
|
|
530
|
+
Escape again (a new `keydown` on what has focus) once `content` has left the
|
|
531
|
+
page, two frames later, so the layer below has taken over. It returns
|
|
532
|
+
whether it did. A content that is still there after about two seconds (a
|
|
533
|
+
`forceMount`) is no longer waited on.
|
|
534
|
+
|
|
535
|
+
```ts
|
|
536
|
+
import { forwardEscape } from '@nxgt/material-core/escape-forward';
|
|
537
|
+
|
|
538
|
+
const contentElement = document.querySelector<HTMLElement>('[role="dialog"]');
|
|
539
|
+
const own = (event: KeyboardEvent) => console.log(event.key); // the caller's handler
|
|
540
|
+
|
|
541
|
+
function onEscapeKeyDown(event: KeyboardEvent) {
|
|
542
|
+
own(event); // the caller's handler first
|
|
543
|
+
forwardEscape(contentElement, event);
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### A field's `aria-describedby`: `/described-by`
|
|
548
|
+
|
|
549
|
+
`describedBy(own, ...ids)` joins the ids a field's control is described by:
|
|
550
|
+
the caller's own `aria-describedby` first, then the field's (its help or
|
|
551
|
+
error text), with `undefined`, `false` and empty ids left out. It returns
|
|
552
|
+
`undefined` when none is left, so the attribute is not rendered at all.
|
|
553
|
+
|
|
554
|
+
```ts
|
|
555
|
+
import { describedBy } from '@nxgt/material-core/described-by';
|
|
556
|
+
|
|
557
|
+
describedBy('hint', error && 'email-error'); // 'hint email-error', or 'hint'
|
|
558
|
+
describedBy(undefined, false); // undefined: no attribute
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
### The page behind a modal layer: `/inert-others`
|
|
562
|
+
|
|
563
|
+
`inertOthers(content, keep?)` makes everything in `<body>` but a modal
|
|
564
|
+
layer's content `inert` — out of the tab order and of the accessibility tree
|
|
565
|
+
— and returns the undo. It marks the elements `aria-hidden`'s `hideOthers`
|
|
566
|
+
marks `aria-hidden` (the siblings of the content and of its ancestors), so
|
|
567
|
+
the two libraries' modal layers pass axe's `aria-hidden-focus`. Live regions
|
|
568
|
+
and scripts stay reachable — the selector `hideOthers` keeps,
|
|
569
|
+
`[aria-live], script`, so a `role="status"` without `aria-live` does not —
|
|
570
|
+
and so does anything in `keep`. Each element is counted per
|
|
571
|
+
layer: nested layers (a select in a dialog) undo in any order, and an element
|
|
572
|
+
that was already `inert` is left as it was. Focus still on the page behind
|
|
573
|
+
goes to the content a microtask later, unless something moved it into the
|
|
574
|
+
layer by then. The undo runs once, by your call or by itself when the content
|
|
575
|
+
leaves the document; marked elements carry `data-nxgt-inert`
|
|
576
|
+
(`INERT_MARKER`). It reads the DOM only when called, and does nothing
|
|
577
|
+
without a content in the document (on the server). Elements mounted after
|
|
578
|
+
the call are not made inert: mount them before the layer opens, or pass them
|
|
579
|
+
in `keep` when they must stay reachable.
|
|
580
|
+
|
|
581
|
+
```ts
|
|
582
|
+
import { inertOthers } from '@nxgt/material-core/inert-others';
|
|
583
|
+
|
|
584
|
+
const undo = inertOthers(dialogContent);
|
|
585
|
+
// … the dialog closes:
|
|
586
|
+
undo();
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### A tap that does not hover: `/touch-hover`
|
|
590
|
+
|
|
591
|
+
After a tap, a browser emulates a mouse: `mouseover`, `mouseenter` and
|
|
592
|
+
`mousemove` arrive on what was tapped, and no `mouseleave` follows until the
|
|
593
|
+
next tap lands somewhere else. Anything that pauses while hovered (a toast,
|
|
594
|
+
under WCAG 2.2.1) stays paused. `ignoreTouchHover(document, selector)`
|
|
595
|
+
listens in the capture phase and, while the last pointer was a finger
|
|
596
|
+
(`pointerType: 'touch'`), stops those three events before any handler inside
|
|
597
|
+
an element matching `selector` hears them. A mouse or a pen hovers as before
|
|
598
|
+
(its first `pointermove` ends the touch), and `mouseout` / `mouseleave`
|
|
599
|
+
always pass. After a tap, bubble-phase listeners over a toast no longer see the
|
|
600
|
+
emulated `mouseover`, `mouseenter` and `mousemove`. It returns the function that
|
|
601
|
+
removes its listeners.
|
|
602
|
+
|
|
603
|
+
```ts
|
|
604
|
+
import { ignoreTouchHover } from '@nxgt/material-core/touch-hover';
|
|
605
|
+
|
|
606
|
+
const stop = ignoreTouchHover(document, '[data-sonner-toaster]');
|
|
607
|
+
// …on unmount
|
|
608
|
+
stop();
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
It also holds the two constants both libraries' `Toaster` share: `TOASTER_PHONE_QUERY`
|
|
612
|
+
(`'(max-width: 600px)'`, the width under which sonner lays toasts out across the
|
|
613
|
+
screen) and `TOASTER_PHONE_POSITION` (`'top-center'`, where toasts go on a phone
|
|
614
|
+
when the caller names no `position`).
|
|
615
|
+
|
|
616
|
+
### Zod issue messages: `/zod-issue-messages`
|
|
617
|
+
|
|
618
|
+
The two libraries' zod error maps turn an issue into text in the language
|
|
619
|
+
they resolved. The two issues with several shapes are formatted here:
|
|
620
|
+
`sizeMessage(issue, { translate, lookup })` for `too_big` / `too_small` (a
|
|
621
|
+
string reads as a length, anything else as a size with its unit) and
|
|
622
|
+
`invalidFormatMessage(issue, { translate, lookup })` for `invalid_format`.
|
|
623
|
+
`translate` returns the message at a key (the key itself when missing),
|
|
624
|
+
`lookup` returns it or `undefined`. A zod issue is passed as is: core reads it
|
|
625
|
+
by shape and has no zod dependency.
|
|
626
|
+
|
|
627
|
+
```ts
|
|
628
|
+
import { sizeMessage } from '@nxgt/material-core/zod-issue-messages';
|
|
629
|
+
|
|
630
|
+
sizeMessage(issue, { translate, lookup }); // "Must contain at most 80 characters."
|
|
631
|
+
```
|
|
632
|
+
|
|
469
633
|
### A clickable table row: `/row-activation`
|
|
470
634
|
|
|
471
635
|
A row whose first cell links to its page should follow that link on a click
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An `aria-describedby` value: the caller's own ids first, then the field's
|
|
3
|
+
* (its help or error text), the falsy ones left out; `undefined` when none.
|
|
4
|
+
*/
|
|
5
|
+
export declare function describedBy(own?: string, ...ids: (string | undefined | false)[]): string | undefined;
|
|
6
|
+
//# sourceMappingURL=described-by.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"described-by.d.ts","sourceRoot":"","sources":["../../src/lib/described-by.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,WAAW,CAC1B,GAAG,CAAC,EAAE,MAAM,EACZ,GAAG,GAAG,EAAE,CAAC,MAAM,GAAG,SAAS,GAAG,KAAK,CAAC,EAAE,GACpC,MAAM,GAAG,SAAS,CAEpB"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/described-by.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * An `aria-describedby` value: the caller's own ids first, then the field's\n * (its help or error text), the falsy ones left out; `undefined` when none.\n */\nexport function describedBy(\n\town?: string,\n\t...ids: (string | undefined | false)[]\n): string | undefined {\n\treturn [own, ...ids].filter(Boolean).join(' ') || undefined;\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AAIO,SAAS,WAAW,CAC1B,QACG,KACkB;AAAA,EACrB,OAAO,CAAC,KAAK,GAAG,GAAG,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,KAAK;AAAA;",
|
|
8
|
+
"debugId": "A732F6C9ECA1AE0864756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -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,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape pressed while an overlay is closing, handed on to the overlay below.
|
|
3
|
+
*
|
|
4
|
+
* Radix's and reka's `DismissableLayer` give Escape to the highest layer of
|
|
5
|
+
* their stack only, and a dialog's layer leaves that stack when its content
|
|
6
|
+
* unmounts — not when it closes. A content still on the page for its exit
|
|
7
|
+
* animation therefore takes every Escape pressed meanwhile and does nothing
|
|
8
|
+
* with it (it is already closed): the Sheet under a just-closed AlertDialog
|
|
9
|
+
* stays open, and the user has to press Escape again.
|
|
10
|
+
*
|
|
11
|
+
* Called from the content's `onEscapeKeyDown` (after the caller's own
|
|
12
|
+
* handler), `forwardEscape` does nothing unless `content` is closing
|
|
13
|
+
* (`data-state="closed"`) and the default was not prevented. Then it
|
|
14
|
+
* prevents the default (so the closing layer does nothing either) and
|
|
15
|
+
* presses Escape again — a new `keydown` on what has focus — once `content`
|
|
16
|
+
* has left the page and the layer below listens again. It returns whether
|
|
17
|
+
* it did.
|
|
18
|
+
*/
|
|
19
|
+
export declare function forwardEscape(content: HTMLElement | null | undefined, event: KeyboardEvent): boolean;
|
|
20
|
+
//# sourceMappingURL=escape-forward.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escape-forward.d.ts","sourceRoot":"","sources":["../../src/lib/escape-forward.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AA4BH,wBAAgB,aAAa,CAC5B,OAAO,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,EACvC,KAAK,EAAE,aAAa,GAClB,OAAO,CA6BT"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// src/lib/escape-forward.ts
|
|
2
|
+
var MAX_FRAMES = 120;
|
|
3
|
+
function keyInit(event) {
|
|
4
|
+
return {
|
|
5
|
+
key: event.key,
|
|
6
|
+
code: event.code,
|
|
7
|
+
location: event.location,
|
|
8
|
+
repeat: event.repeat,
|
|
9
|
+
altKey: event.altKey,
|
|
10
|
+
ctrlKey: event.ctrlKey,
|
|
11
|
+
metaKey: event.metaKey,
|
|
12
|
+
shiftKey: event.shiftKey,
|
|
13
|
+
bubbles: true,
|
|
14
|
+
cancelable: true,
|
|
15
|
+
composed: true
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function forwardEscape(content, event) {
|
|
19
|
+
if (event.defaultPrevented || content?.dataset.state !== "closed") {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const document = content.ownerDocument;
|
|
23
|
+
const view = document.defaultView;
|
|
24
|
+
if (!view)
|
|
25
|
+
return false;
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
const init = keyInit(event);
|
|
28
|
+
let frames = 0;
|
|
29
|
+
const tick = () => {
|
|
30
|
+
if (content.dataset.state !== "closed")
|
|
31
|
+
return;
|
|
32
|
+
if (content.isConnected) {
|
|
33
|
+
if (++frames < MAX_FRAMES)
|
|
34
|
+
view.requestAnimationFrame(tick);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
view.requestAnimationFrame(() => view.requestAnimationFrame(() => {
|
|
38
|
+
const target = document.activeElement ?? document.body;
|
|
39
|
+
target.dispatchEvent(new view.KeyboardEvent("keydown", init));
|
|
40
|
+
}));
|
|
41
|
+
};
|
|
42
|
+
view.requestAnimationFrame(tick);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
forwardEscape
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
//# debugId=6802D3D345478C3C64756E2164756E21
|
|
50
|
+
//# sourceMappingURL=escape-forward.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/escape-forward.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Escape pressed while an overlay is closing, handed on to the overlay below.\n *\n * Radix's and reka's `DismissableLayer` give Escape to the highest layer of\n * their stack only, and a dialog's layer leaves that stack when its content\n * unmounts — not when it closes. A content still on the page for its exit\n * animation therefore takes every Escape pressed meanwhile and does nothing\n * with it (it is already closed): the Sheet under a just-closed AlertDialog\n * stays open, and the user has to press Escape again.\n *\n * Called from the content's `onEscapeKeyDown` (after the caller's own\n * handler), `forwardEscape` does nothing unless `content` is closing\n * (`data-state=\"closed\"`) and the default was not prevented. Then it\n * prevents the default (so the closing layer does nothing either) and\n * presses Escape again — a new `keydown` on what has focus — once `content`\n * has left the page and the layer below listens again. It returns whether\n * it did.\n */\n\n// A content that never leaves (a `forceMount`) stops being waited on: about\n// two seconds at 60 frames per second.\nconst MAX_FRAMES = 120;\n\n// The window of the content's document: its frames, and its `KeyboardEvent`\n// (an iframe's, when the content is in one).\ntype Frames = Pick<Window, 'requestAnimationFrame'> & {\n\tKeyboardEvent: typeof KeyboardEvent;\n};\n\nfunction keyInit(event: KeyboardEvent): KeyboardEventInit {\n\treturn {\n\t\tkey: event.key,\n\t\tcode: event.code,\n\t\tlocation: event.location,\n\t\trepeat: event.repeat,\n\t\taltKey: event.altKey,\n\t\tctrlKey: event.ctrlKey,\n\t\tmetaKey: event.metaKey,\n\t\tshiftKey: event.shiftKey,\n\t\tbubbles: true,\n\t\tcancelable: true,\n\t\tcomposed: true,\n\t};\n}\n\nexport function forwardEscape(\n\tcontent: HTMLElement | null | undefined,\n\tevent: KeyboardEvent,\n): boolean {\n\tif (event.defaultPrevented || content?.dataset.state !== 'closed') {\n\t\treturn false;\n\t}\n\tconst document = content.ownerDocument;\n\tconst view = document.defaultView as Frames | null;\n\tif (!view) return false;\n\tevent.preventDefault();\n\tconst init = keyInit(event);\n\tlet frames = 0;\n\tconst tick = () => {\n\t\t// Reopened meanwhile: it is a live layer again, and Escape is its own.\n\t\tif (content.dataset.state !== 'closed') return;\n\t\tif (content.isConnected) {\n\t\t\tif (++frames < MAX_FRAMES) view.requestAnimationFrame(tick);\n\t\t\treturn;\n\t\t}\n\t\t// The layer leaves the stack in an unmount cleanup; with Radix, the\n\t\t// layer below then re-renders and listens again in an effect of its\n\t\t// own. Two frames cover both.\n\t\tview.requestAnimationFrame(() =>\n\t\t\tview.requestAnimationFrame(() => {\n\t\t\t\tconst target = document.activeElement ?? document.body;\n\t\t\t\ttarget.dispatchEvent(new view.KeyboardEvent('keydown', init));\n\t\t\t}),\n\t\t);\n\t};\n\tview.requestAnimationFrame(tick);\n\treturn true;\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AAqBA,IAAM,aAAa;AAQnB,SAAS,OAAO,CAAC,OAAyC;AAAA,EACzD,OAAO;AAAA,IACN,KAAK,MAAM;AAAA,IACX,MAAM,MAAM;AAAA,IACZ,UAAU,MAAM;AAAA,IAChB,QAAQ,MAAM;AAAA,IACd,QAAQ,MAAM;AAAA,IACd,SAAS,MAAM;AAAA,IACf,SAAS,MAAM;AAAA,IACf,UAAU,MAAM;AAAA,IAChB,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,UAAU;AAAA,EACX;AAAA;AAGM,SAAS,aAAa,CAC5B,SACA,OACU;AAAA,EACV,IAAI,MAAM,oBAAoB,SAAS,QAAQ,UAAU,UAAU;AAAA,IAClE,OAAO;AAAA,EACR;AAAA,EACA,MAAM,WAAW,QAAQ;AAAA,EACzB,MAAM,OAAO,SAAS;AAAA,EACtB,IAAI,CAAC;AAAA,IAAM,OAAO;AAAA,EAClB,MAAM,eAAe;AAAA,EACrB,MAAM,OAAO,QAAQ,KAAK;AAAA,EAC1B,IAAI,SAAS;AAAA,EACb,MAAM,OAAO,MAAM;AAAA,IAElB,IAAI,QAAQ,QAAQ,UAAU;AAAA,MAAU;AAAA,IACxC,IAAI,QAAQ,aAAa;AAAA,MACxB,IAAI,EAAE,SAAS;AAAA,QAAY,KAAK,sBAAsB,IAAI;AAAA,MAC1D;AAAA,IACD;AAAA,IAIA,KAAK,sBAAsB,MAC1B,KAAK,sBAAsB,MAAM;AAAA,MAChC,MAAM,SAAS,SAAS,iBAAiB,SAAS;AAAA,MAClD,OAAO,cAAc,IAAI,KAAK,cAAc,WAAW,IAAI,CAAC;AAAA,KAC5D,CACF;AAAA;AAAA,EAED,KAAK,sBAAsB,IAAI;AAAA,EAC/B,OAAO;AAAA;",
|
|
8
|
+
"debugId": "6802D3D345478C3C64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first control of `roots` a user can reach, in document order: one a
|
|
3
|
+
* user can Tab to that is rendered — not a `display: none` input a library
|
|
4
|
+
* keeps for forms. A root that is itself such a control is its own first
|
|
5
|
+
* control. `null` when none is. Reads the DOM when called.
|
|
6
|
+
*/
|
|
7
|
+
export declare function firstControl(roots: Iterable<Element>): HTMLElement | null;
|
|
8
|
+
/** What a form library calls on the element a field registered. */
|
|
9
|
+
export type FocusHandle = {
|
|
10
|
+
focus: () => void;
|
|
11
|
+
select: () => void;
|
|
12
|
+
setCustomValidity: (message: string) => void;
|
|
13
|
+
reportValidity: () => boolean;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A stand-in element for a field made of several controls, for a form
|
|
17
|
+
* library that focuses an invalid field by calling `focus()` on what the
|
|
18
|
+
* field registered (React Hook Form's `field.ref`). `focus()` focuses the
|
|
19
|
+
* first control of `roots` a user can reach, looked up when it is called —
|
|
20
|
+
* `roots` is read live, so pass the field's own set — and again a turn
|
|
21
|
+
* later when focus is not in the field by then: the error that triggered it
|
|
22
|
+
* may re-render the field and replace its control. `select()` selects that
|
|
23
|
+
* control's text when it has any. Native validation is not used:
|
|
24
|
+
* `setCustomValidity` does nothing and `reportValidity` returns `true`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function focusHandle(roots: Iterable<Element>): FocusHandle;
|
|
27
|
+
//# sourceMappingURL=focus-control.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focus-control.d.ts","sourceRoot":"","sources":["../../src/lib/focus-control.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,WAAW,GAAG,IAAI,CAUzE;AAED,mEAAmE;AACnE,MAAM,MAAM,WAAW,GAAG;IACzB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,iBAAiB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,cAAc,EAAE,MAAM,OAAO,CAAC;CAC9B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,WAAW,CAqBjE"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/lib/focus-control.ts
|
|
2
|
+
var TABBABLE = 'input:not([type="hidden"]):not(:disabled):not([tabindex="-1"]), textarea:not(:disabled):not([tabindex="-1"]), select:not(:disabled):not([tabindex="-1"]), button:not(:disabled):not([tabindex="-1"]), [contenteditable="true"], [tabindex]:not([tabindex="-1"])';
|
|
3
|
+
function firstControl(roots) {
|
|
4
|
+
for (const root of roots) {
|
|
5
|
+
const controls = root.matches(TABBABLE) ? [root] : root.querySelectorAll(TABBABLE);
|
|
6
|
+
for (const control of controls) {
|
|
7
|
+
if (control.getClientRects().length)
|
|
8
|
+
return control;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
function focusHandle(roots) {
|
|
14
|
+
return {
|
|
15
|
+
focus: () => {
|
|
16
|
+
firstControl(roots)?.focus();
|
|
17
|
+
setTimeout(() => {
|
|
18
|
+
const list = [...roots];
|
|
19
|
+
const active = list[0]?.ownerDocument.activeElement ?? null;
|
|
20
|
+
if (!list.some((root) => root.contains(active))) {
|
|
21
|
+
firstControl(list)?.focus();
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
select: () => {
|
|
26
|
+
const control = firstControl(roots);
|
|
27
|
+
if (control && "select" in control) {
|
|
28
|
+
control.select();
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
setCustomValidity: () => {
|
|
32
|
+
return;
|
|
33
|
+
},
|
|
34
|
+
reportValidity: () => true
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
firstControl,
|
|
39
|
+
focusHandle
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
//# debugId=F6055B3989727FBE64756E2164756E21
|
|
43
|
+
//# sourceMappingURL=focus-control.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/focus-control.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/** What a user can Tab to inside a field. */\nconst TABBABLE =\n\t'input:not([type=\"hidden\"]):not(:disabled):not([tabindex=\"-1\"]), textarea:not(:disabled):not([tabindex=\"-1\"]), select:not(:disabled):not([tabindex=\"-1\"]), button:not(:disabled):not([tabindex=\"-1\"]), [contenteditable=\"true\"], [tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * The first control of `roots` a user can reach, in document order: one a\n * user can Tab to that is rendered — not a `display: none` input a library\n * keeps for forms. A root that is itself such a control is its own first\n * control. `null` when none is. Reads the DOM when called.\n */\nexport function firstControl(roots: Iterable<Element>): HTMLElement | null {\n\tfor (const root of roots) {\n\t\tconst controls = root.matches(TABBABLE)\n\t\t\t? [root as HTMLElement]\n\t\t\t: root.querySelectorAll<HTMLElement>(TABBABLE);\n\t\tfor (const control of controls) {\n\t\t\tif (control.getClientRects().length) return control;\n\t\t}\n\t}\n\treturn null;\n}\n\n/** What a form library calls on the element a field registered. */\nexport type FocusHandle = {\n\tfocus: () => void;\n\tselect: () => void;\n\tsetCustomValidity: (message: string) => void;\n\treportValidity: () => boolean;\n};\n\n/**\n * A stand-in element for a field made of several controls, for a form\n * library that focuses an invalid field by calling `focus()` on what the\n * field registered (React Hook Form's `field.ref`). `focus()` focuses the\n * first control of `roots` a user can reach, looked up when it is called —\n * `roots` is read live, so pass the field's own set — and again a turn\n * later when focus is not in the field by then: the error that triggered it\n * may re-render the field and replace its control. `select()` selects that\n * control's text when it has any. Native validation is not used:\n * `setCustomValidity` does nothing and `reportValidity` returns `true`.\n */\nexport function focusHandle(roots: Iterable<Element>): FocusHandle {\n\treturn {\n\t\tfocus: () => {\n\t\t\tfirstControl(roots)?.focus();\n\t\t\tsetTimeout(() => {\n\t\t\t\tconst list = [...roots];\n\t\t\t\tconst active = list[0]?.ownerDocument.activeElement ?? null;\n\t\t\t\tif (!list.some((root) => root.contains(active))) {\n\t\t\t\t\tfirstControl(list)?.focus();\n\t\t\t\t}\n\t\t\t});\n\t\t},\n\t\tselect: () => {\n\t\t\tconst control = firstControl(roots);\n\t\t\tif (control && 'select' in control) {\n\t\t\t\t(control as HTMLInputElement).select();\n\t\t\t}\n\t\t},\n\t\tsetCustomValidity: () => undefined,\n\t\treportValidity: () => true,\n\t};\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AACA,IAAM,WACL;AAQM,SAAS,YAAY,CAAC,OAA8C;AAAA,EAC1E,WAAW,QAAQ,OAAO;AAAA,IACzB,MAAM,WAAW,KAAK,QAAQ,QAAQ,IACnC,CAAC,IAAmB,IACpB,KAAK,iBAA8B,QAAQ;AAAA,IAC9C,WAAW,WAAW,UAAU;AAAA,MAC/B,IAAI,QAAQ,eAAe,EAAE;AAAA,QAAQ,OAAO;AAAA,IAC7C;AAAA,EACD;AAAA,EACA,OAAO;AAAA;AAsBD,SAAS,WAAW,CAAC,OAAuC;AAAA,EAClE,OAAO;AAAA,IACN,OAAO,MAAM;AAAA,MACZ,aAAa,KAAK,GAAG,MAAM;AAAA,MAC3B,WAAW,MAAM;AAAA,QAChB,MAAM,OAAO,CAAC,GAAG,KAAK;AAAA,QACtB,MAAM,SAAS,KAAK,IAAI,cAAc,iBAAiB;AAAA,QACvD,IAAI,CAAC,KAAK,KAAK,CAAC,SAAS,KAAK,SAAS,MAAM,CAAC,GAAG;AAAA,UAChD,aAAa,IAAI,GAAG,MAAM;AAAA,QAC3B;AAAA,OACA;AAAA;AAAA,IAEF,QAAQ,MAAM;AAAA,MACb,MAAM,UAAU,aAAa,KAAK;AAAA,MAClC,IAAI,WAAW,YAAY,SAAS;AAAA,QAClC,QAA6B,OAAO;AAAA,MACtC;AAAA;AAAA,IAED,mBAAmB,MAAG;AAAA,MAAG;AAAA;AAAA,IACzB,gBAAgB,MAAM;AAAA,EACvB;AAAA;",
|
|
8
|
+
"debugId": "F6055B3989727FBE64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** The attribute on every element `inertOthers` made `inert`. */
|
|
2
|
+
export declare const INERT_MARKER = "data-nxgt-inert";
|
|
3
|
+
/**
|
|
4
|
+
* Makes everything in `<body>` but `target` (a modal layer's content) and
|
|
5
|
+
* `keep` `inert` — out of the tab order and out of the accessibility tree —
|
|
6
|
+
* and returns the undo. The elements marked are the siblings of `target`
|
|
7
|
+
* and of each of its ancestors, the ones `aria-hidden`'s `hideOthers` marks
|
|
8
|
+
* `aria-hidden`: live regions and scripts are kept too, and so are their
|
|
9
|
+
* ancestors. Every element is counted per layer, so nested layers (a select
|
|
10
|
+
* open in a dialog) undo in any order: an element stays inert while one
|
|
11
|
+
* layer that marked it is open. An element already `inert` is left as it
|
|
12
|
+
* was. Focus still on an element made inert goes to `target`, which a
|
|
13
|
+
* layer's content takes (`tabindex="-1"`). The undo runs once — by your
|
|
14
|
+
* call, or by itself when `target` leaves the document — and a second call
|
|
15
|
+
* does nothing. Elements mounted after the call are not made inert. Does nothing, and
|
|
16
|
+
* returns a no-op, when `target` is missing or not in its document's body
|
|
17
|
+
* (on the server). Reads and writes the DOM only when called.
|
|
18
|
+
*/
|
|
19
|
+
export declare function inertOthers(target: Element | null | undefined, keep?: Iterable<Element | null | undefined>): () => void;
|
|
20
|
+
//# sourceMappingURL=inert-others.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inert-others.d.ts","sourceRoot":"","sources":["../../src/lib/inert-others.ts"],"names":[],"mappings":"AASA,iEAAiE;AACjE,eAAO,MAAM,YAAY,oBAAoB,CAAC;AA6C9C;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CAC1B,MAAM,EAAE,OAAO,GAAG,IAAI,GAAG,SAAS,EAClC,IAAI,GAAE,QAAQ,CAAC,OAAO,GAAG,IAAI,GAAG,SAAS,CAAM,GAC7C,MAAM,IAAI,CAqCZ"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// src/lib/inert-others.ts
|
|
2
|
+
var KEEP = "[aria-live], script";
|
|
3
|
+
var INERT_MARKER = "data-nxgt-inert";
|
|
4
|
+
var counts = new WeakMap;
|
|
5
|
+
var inertBefore = new WeakSet;
|
|
6
|
+
function keptPath(targets, body) {
|
|
7
|
+
const kept = new Set;
|
|
8
|
+
for (const target of targets) {
|
|
9
|
+
for (let node = target;node && node !== body && !kept.has(node); node = node.parentNode) {
|
|
10
|
+
kept.add(node);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return kept;
|
|
14
|
+
}
|
|
15
|
+
function mark(element) {
|
|
16
|
+
const count = (counts.get(element) ?? 0) + 1;
|
|
17
|
+
counts.set(element, count);
|
|
18
|
+
if (count > 1)
|
|
19
|
+
return;
|
|
20
|
+
if (element.hasAttribute("inert")) {
|
|
21
|
+
inertBefore.add(element);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
element.setAttribute("inert", "");
|
|
25
|
+
element.setAttribute(INERT_MARKER, "");
|
|
26
|
+
}
|
|
27
|
+
function unmark(element) {
|
|
28
|
+
const count = (counts.get(element) ?? 1) - 1;
|
|
29
|
+
if (count > 0) {
|
|
30
|
+
counts.set(element, count);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
counts.delete(element);
|
|
34
|
+
if (inertBefore.delete(element))
|
|
35
|
+
return;
|
|
36
|
+
element.removeAttribute("inert");
|
|
37
|
+
element.removeAttribute(INERT_MARKER);
|
|
38
|
+
}
|
|
39
|
+
function inertOthers(target, keep = []) {
|
|
40
|
+
const body = target?.ownerDocument?.body;
|
|
41
|
+
if (!target || !body?.contains(target))
|
|
42
|
+
return () => {
|
|
43
|
+
return;
|
|
44
|
+
};
|
|
45
|
+
const targets = [
|
|
46
|
+
target,
|
|
47
|
+
...[...keep].filter((element) => Boolean(element && body.contains(element))),
|
|
48
|
+
...Array.from(body.querySelectorAll(KEEP))
|
|
49
|
+
];
|
|
50
|
+
const kept = keptPath(targets, body);
|
|
51
|
+
const stops = new Set(targets);
|
|
52
|
+
const marked = [];
|
|
53
|
+
const active = target.ownerDocument.activeElement;
|
|
54
|
+
const visit = (parent) => {
|
|
55
|
+
for (const child of Array.from(parent.children)) {
|
|
56
|
+
if (stops.has(child))
|
|
57
|
+
continue;
|
|
58
|
+
if (kept.has(child)) {
|
|
59
|
+
visit(child);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
mark(child);
|
|
63
|
+
marked.push(child);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
visit(body);
|
|
67
|
+
let undone = false;
|
|
68
|
+
keepFocus(target, active, marked, () => !undone);
|
|
69
|
+
const undo = () => {
|
|
70
|
+
if (undone)
|
|
71
|
+
return;
|
|
72
|
+
undone = true;
|
|
73
|
+
watch?.disconnect();
|
|
74
|
+
for (const element of marked)
|
|
75
|
+
unmark(element);
|
|
76
|
+
};
|
|
77
|
+
const watch = watchRemoval(body, target, undo);
|
|
78
|
+
return undo;
|
|
79
|
+
}
|
|
80
|
+
function keepFocus(target, active, marked, open) {
|
|
81
|
+
const doc = target.ownerDocument;
|
|
82
|
+
if (!active || !marked.some((element) => element.contains(active)))
|
|
83
|
+
return;
|
|
84
|
+
queueMicrotask(() => {
|
|
85
|
+
if (!open() || target.contains(doc.activeElement))
|
|
86
|
+
return;
|
|
87
|
+
target.focus?.({ preventScroll: true });
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function watchRemoval(body, target, undo) {
|
|
91
|
+
const Observer = body.ownerDocument.defaultView?.MutationObserver;
|
|
92
|
+
if (!Observer)
|
|
93
|
+
return;
|
|
94
|
+
const observer = new Observer(() => {
|
|
95
|
+
if (!target.isConnected)
|
|
96
|
+
return undo();
|
|
97
|
+
observer.disconnect();
|
|
98
|
+
observeAncestors(observer, body, target);
|
|
99
|
+
});
|
|
100
|
+
observeAncestors(observer, body, target);
|
|
101
|
+
return observer;
|
|
102
|
+
}
|
|
103
|
+
function observeAncestors(observer, body, target) {
|
|
104
|
+
for (let node = target.parentNode;node; node = node.parentNode) {
|
|
105
|
+
observer.observe(node, { childList: true });
|
|
106
|
+
if (node === body)
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export {
|
|
111
|
+
INERT_MARKER,
|
|
112
|
+
inertOthers
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
//# debugId=DB32588292C8994B64756E2164756E21
|
|
116
|
+
//# sourceMappingURL=inert-others.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/inert-others.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * What stays reachable behind a modal layer, the selector `aria-hidden`'s\n * `hideOthers` keeps: elements with `aria-live` — a toaster announces what\n * happens while the layer is open, and its toasts stay usable — and scripts.\n * A `role=\"status\"` or `role=\"alert\"` without `aria-live` is not kept, as it\n * is not there.\n */\nconst KEEP = '[aria-live], script';\n\n/** The attribute on every element `inertOthers` made `inert`. */\nexport const INERT_MARKER = 'data-nxgt-inert';\n\n/** How many open layers made each element inert. */\nconst counts = new WeakMap<Element, number>();\n/** Elements that were already `inert` before the first layer: left as found. */\nconst inertBefore = new WeakSet<Element>();\n\nfunction keptPath(targets: Element[], body: Element): Set<Node> {\n\tconst kept = new Set<Node>();\n\tfor (const target of targets) {\n\t\tfor (\n\t\t\tlet node: Node | null = target;\n\t\t\tnode && node !== body && !kept.has(node);\n\t\t\tnode = node.parentNode\n\t\t) {\n\t\t\tkept.add(node);\n\t\t}\n\t}\n\treturn kept;\n}\n\nfunction mark(element: Element) {\n\tconst count = (counts.get(element) ?? 0) + 1;\n\tcounts.set(element, count);\n\tif (count > 1) return;\n\tif (element.hasAttribute('inert')) {\n\t\tinertBefore.add(element);\n\t\treturn;\n\t}\n\telement.setAttribute('inert', '');\n\telement.setAttribute(INERT_MARKER, '');\n}\n\nfunction unmark(element: Element) {\n\tconst count = (counts.get(element) ?? 1) - 1;\n\tif (count > 0) {\n\t\tcounts.set(element, count);\n\t\treturn;\n\t}\n\tcounts.delete(element);\n\tif (inertBefore.delete(element)) return;\n\telement.removeAttribute('inert');\n\telement.removeAttribute(INERT_MARKER);\n}\n\n/**\n * Makes everything in `<body>` but `target` (a modal layer's content) and\n * `keep` `inert` — out of the tab order and out of the accessibility tree —\n * and returns the undo. The elements marked are the siblings of `target`\n * and of each of its ancestors, the ones `aria-hidden`'s `hideOthers` marks\n * `aria-hidden`: live regions and scripts are kept too, and so are their\n * ancestors. Every element is counted per layer, so nested layers (a select\n * open in a dialog) undo in any order: an element stays inert while one\n * layer that marked it is open. An element already `inert` is left as it\n * was. Focus still on an element made inert goes to `target`, which a\n * layer's content takes (`tabindex=\"-1\"`). The undo runs once — by your\n * call, or by itself when `target` leaves the document — and a second call\n * does nothing. Elements mounted after the call are not made inert. Does nothing, and\n * returns a no-op, when `target` is missing or not in its document's body\n * (on the server). Reads and writes the DOM only when called.\n */\nexport function inertOthers(\n\ttarget: Element | null | undefined,\n\tkeep: Iterable<Element | null | undefined> = [],\n): () => void {\n\tconst body = target?.ownerDocument?.body;\n\tif (!target || !body?.contains(target)) return () => undefined;\n\tconst targets = [\n\t\ttarget,\n\t\t...[...keep].filter((element): element is Element =>\n\t\t\tBoolean(element && body.contains(element)),\n\t\t),\n\t\t...Array.from(body.querySelectorAll(KEEP)),\n\t];\n\tconst kept = keptPath(targets, body);\n\tconst stops = new Set<Node>(targets);\n\tconst marked: Element[] = [];\n\t// Read before anything turns inert: a browser may blur what it marks.\n\tconst active = target.ownerDocument.activeElement;\n\tconst visit = (parent: Element) => {\n\t\tfor (const child of Array.from(parent.children)) {\n\t\t\tif (stops.has(child)) continue;\n\t\t\tif (kept.has(child)) {\n\t\t\t\tvisit(child);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tmark(child);\n\t\t\tmarked.push(child);\n\t\t}\n\t};\n\tvisit(body);\n\tlet undone = false;\n\tkeepFocus(target, active, marked, () => !undone);\n\tconst undo = () => {\n\t\tif (undone) return;\n\t\tundone = true;\n\t\twatch?.disconnect();\n\t\tfor (const element of marked) unmark(element);\n\t};\n\tconst watch = watchRemoval(body, target, undo);\n\treturn undo;\n}\n\n/**\n * Focus left on the page behind (an `onOpenAutoFocus` that prevented the\n * default) would be lost once it is inert: it goes to the layer instead, a\n * microtask later, unless something moved it into the layer by then (a\n * focus fallback of the layer's own).\n */\nfunction keepFocus(\n\ttarget: Element,\n\tactive: Element | null,\n\tmarked: Element[],\n\topen: () => boolean,\n) {\n\tconst doc = target.ownerDocument;\n\tif (!active || !marked.some((element) => element.contains(active))) return;\n\tqueueMicrotask(() => {\n\t\tif (!open() || target.contains(doc.activeElement)) return;\n\t\t(target as HTMLElement).focus?.({ preventScroll: true });\n\t});\n}\n\n/**\n * Calls `undo` once `target` leaves the document: Radix's Select drops its\n * content on close without detaching the ref a wrapper holds, so the\n * wrapper never hears it went. Only the children of `target`'s ancestors,\n * up to `body`, are watched: removing `target` or one of them is the only\n * way it leaves, and the rest of the page's mutations never wake it.\n */\nfunction watchRemoval(body: Element, target: Element, undo: () => void) {\n\tconst Observer = body.ownerDocument.defaultView?.MutationObserver;\n\tif (!Observer) return undefined;\n\tconst observer = new Observer(() => {\n\t\tif (!target.isConnected) return undo();\n\t\t// Re-parented: watch the ancestors it has now.\n\t\tobserver.disconnect();\n\t\tobserveAncestors(observer, body, target);\n\t});\n\tobserveAncestors(observer, body, target);\n\treturn observer;\n}\n\nfunction observeAncestors(\n\tobserver: MutationObserver,\n\tbody: Element,\n\ttarget: Element,\n) {\n\tfor (let node = target.parentNode; node; node = node.parentNode) {\n\t\tobserver.observe(node, { childList: true });\n\t\tif (node === body) break;\n\t}\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AAOA,IAAM,OAAO;AAGN,IAAM,eAAe;AAG5B,IAAM,SAAS,IAAI;AAEnB,IAAM,cAAc,IAAI;AAExB,SAAS,QAAQ,CAAC,SAAoB,MAA0B;AAAA,EAC/D,MAAM,OAAO,IAAI;AAAA,EACjB,WAAW,UAAU,SAAS;AAAA,IAC7B,SACK,OAAoB,OACxB,QAAQ,SAAS,QAAQ,CAAC,KAAK,IAAI,IAAI,GACvC,OAAO,KAAK,YACX;AAAA,MACD,KAAK,IAAI,IAAI;AAAA,IACd;AAAA,EACD;AAAA,EACA,OAAO;AAAA;AAGR,SAAS,IAAI,CAAC,SAAkB;AAAA,EAC/B,MAAM,SAAS,OAAO,IAAI,OAAO,KAAK,KAAK;AAAA,EAC3C,OAAO,IAAI,SAAS,KAAK;AAAA,EACzB,IAAI,QAAQ;AAAA,IAAG;AAAA,EACf,IAAI,QAAQ,aAAa,OAAO,GAAG;AAAA,IAClC,YAAY,IAAI,OAAO;AAAA,IACvB;AAAA,EACD;AAAA,EACA,QAAQ,aAAa,SAAS,EAAE;AAAA,EAChC,QAAQ,aAAa,cAAc,EAAE;AAAA;AAGtC,SAAS,MAAM,CAAC,SAAkB;AAAA,EACjC,MAAM,SAAS,OAAO,IAAI,OAAO,KAAK,KAAK;AAAA,EAC3C,IAAI,QAAQ,GAAG;AAAA,IACd,OAAO,IAAI,SAAS,KAAK;AAAA,IACzB;AAAA,EACD;AAAA,EACA,OAAO,OAAO,OAAO;AAAA,EACrB,IAAI,YAAY,OAAO,OAAO;AAAA,IAAG;AAAA,EACjC,QAAQ,gBAAgB,OAAO;AAAA,EAC/B,QAAQ,gBAAgB,YAAY;AAAA;AAmB9B,SAAS,WAAW,CAC1B,QACA,OAA6C,CAAC,GACjC;AAAA,EACb,MAAM,OAAO,QAAQ,eAAe;AAAA,EACpC,IAAI,CAAC,UAAU,CAAC,MAAM,SAAS,MAAM;AAAA,IAAG,OAAO,MAAG;AAAA,MAAG;AAAA;AAAA,EACrD,MAAM,UAAU;AAAA,IACf;AAAA,IACA,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,YACpB,QAAQ,WAAW,KAAK,SAAS,OAAO,CAAC,CAC1C;AAAA,IACA,GAAG,MAAM,KAAK,KAAK,iBAAiB,IAAI,CAAC;AAAA,EAC1C;AAAA,EACA,MAAM,OAAO,SAAS,SAAS,IAAI;AAAA,EACnC,MAAM,QAAQ,IAAI,IAAU,OAAO;AAAA,EACnC,MAAM,SAAoB,CAAC;AAAA,EAE3B,MAAM,SAAS,OAAO,cAAc;AAAA,EACpC,MAAM,QAAQ,CAAC,WAAoB;AAAA,IAClC,WAAW,SAAS,MAAM,KAAK,OAAO,QAAQ,GAAG;AAAA,MAChD,IAAI,MAAM,IAAI,KAAK;AAAA,QAAG;AAAA,MACtB,IAAI,KAAK,IAAI,KAAK,GAAG;AAAA,QACpB,MAAM,KAAK;AAAA,QACX;AAAA,MACD;AAAA,MACA,KAAK,KAAK;AAAA,MACV,OAAO,KAAK,KAAK;AAAA,IAClB;AAAA;AAAA,EAED,MAAM,IAAI;AAAA,EACV,IAAI,SAAS;AAAA,EACb,UAAU,QAAQ,QAAQ,QAAQ,MAAM,CAAC,MAAM;AAAA,EAC/C,MAAM,OAAO,MAAM;AAAA,IAClB,IAAI;AAAA,MAAQ;AAAA,IACZ,SAAS;AAAA,IACT,OAAO,WAAW;AAAA,IAClB,WAAW,WAAW;AAAA,MAAQ,OAAO,OAAO;AAAA;AAAA,EAE7C,MAAM,QAAQ,aAAa,MAAM,QAAQ,IAAI;AAAA,EAC7C,OAAO;AAAA;AASR,SAAS,SAAS,CACjB,QACA,QACA,QACA,MACC;AAAA,EACD,MAAM,MAAM,OAAO;AAAA,EACnB,IAAI,CAAC,UAAU,CAAC,OAAO,KAAK,CAAC,YAAY,QAAQ,SAAS,MAAM,CAAC;AAAA,IAAG;AAAA,EACpE,eAAe,MAAM;AAAA,IACpB,IAAI,CAAC,KAAK,KAAK,OAAO,SAAS,IAAI,aAAa;AAAA,MAAG;AAAA,IAClD,OAAuB,QAAQ,EAAE,eAAe,KAAK,CAAC;AAAA,GACvD;AAAA;AAUF,SAAS,YAAY,CAAC,MAAe,QAAiB,MAAkB;AAAA,EACvE,MAAM,WAAW,KAAK,cAAc,aAAa;AAAA,EACjD,IAAI,CAAC;AAAA,IAAU;AAAA,EACf,MAAM,WAAW,IAAI,SAAS,MAAM;AAAA,IACnC,IAAI,CAAC,OAAO;AAAA,MAAa,OAAO,KAAK;AAAA,IAErC,SAAS,WAAW;AAAA,IACpB,iBAAiB,UAAU,MAAM,MAAM;AAAA,GACvC;AAAA,EACD,iBAAiB,UAAU,MAAM,MAAM;AAAA,EACvC,OAAO;AAAA;AAGR,SAAS,gBAAgB,CACxB,UACA,MACA,QACC;AAAA,EACD,SAAS,OAAO,OAAO,WAAY,MAAM,OAAO,KAAK,YAAY;AAAA,IAChE,SAAS,QAAQ,MAAM,EAAE,WAAW,KAAK,CAAC;AAAA,IAC1C,IAAI,SAAS;AAAA,MAAM;AAAA,EACpB;AAAA;",
|
|
8
|
+
"debugId": "DB32588292C8994B64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A phone, as sonner sees one: under 600px wide it lays the toasts out
|
|
3
|
+
* across the whole width (`@media (max-width: 600px)` in its styles).
|
|
4
|
+
*/
|
|
5
|
+
export declare const TOASTER_PHONE_QUERY = "(max-width: 600px)";
|
|
6
|
+
/** Where toasts go on a phone when the caller names no `position`. */
|
|
7
|
+
export declare const TOASTER_PHONE_POSITION = "top-center";
|
|
8
|
+
type Listen = Pick<Document, 'addEventListener' | 'removeEventListener'>;
|
|
9
|
+
/**
|
|
10
|
+
* Keeps a tap from hovering whatever matches `selector`: while the last
|
|
11
|
+
* pointer was a finger (`pointerType: 'touch'`), the hover events the browser
|
|
12
|
+
* emulates for it (`mouseover`, `mouseenter`, `mousemove`) stop at `doc`, in
|
|
13
|
+
* the capture phase, before any handler inside the matching element hears
|
|
14
|
+
* them. A mouse or a pen hovers as before: its first `pointermove` ends the
|
|
15
|
+
* touch. `mouseout` and `mouseleave` always pass, so a hover that did start
|
|
16
|
+
* still ends.
|
|
17
|
+
*
|
|
18
|
+
* Toasts pause while hovered (WCAG 2.2.1), and a tap on one used to leave it
|
|
19
|
+
* "hovered", so paused, until the next tap elsewhere. Returns the function
|
|
20
|
+
* that removes the listeners; nothing is read before an event arrives.
|
|
21
|
+
*/
|
|
22
|
+
export declare function ignoreTouchHover(doc: Listen, selector: string): () => void;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=touch-hover.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"touch-hover.d.ts","sourceRoot":"","sources":["../../src/lib/touch-hover.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,mBAAmB,uBAAuB,CAAC;AAExD,sEAAsE;AACtE,eAAO,MAAM,sBAAsB,eAAe,CAAC;AAYnD,KAAK,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,kBAAkB,GAAG,qBAAqB,CAAC,CAAC;AAEzE;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,IAAI,CAqB1E"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/lib/touch-hover.ts
|
|
2
|
+
var TOASTER_PHONE_QUERY = "(max-width: 600px)";
|
|
3
|
+
var TOASTER_PHONE_POSITION = "top-center";
|
|
4
|
+
var HOVER_EVENTS = ["mouseover", "mouseenter", "mousemove"];
|
|
5
|
+
var POINTER_EVENTS = ["pointerdown", "pointermove", "pointerup"];
|
|
6
|
+
function ignoreTouchHover(doc, selector) {
|
|
7
|
+
let touch = false;
|
|
8
|
+
const onPointer = (event) => {
|
|
9
|
+
touch = event.pointerType === "touch";
|
|
10
|
+
};
|
|
11
|
+
const onHover = (event) => {
|
|
12
|
+
if (!touch)
|
|
13
|
+
return;
|
|
14
|
+
const target = event.target;
|
|
15
|
+
if (typeof target?.closest !== "function")
|
|
16
|
+
return;
|
|
17
|
+
if (target.closest(selector))
|
|
18
|
+
event.stopPropagation();
|
|
19
|
+
};
|
|
20
|
+
const options = { capture: true, passive: true };
|
|
21
|
+
for (const type of POINTER_EVENTS)
|
|
22
|
+
doc.addEventListener(type, onPointer, options);
|
|
23
|
+
for (const type of HOVER_EVENTS)
|
|
24
|
+
doc.addEventListener(type, onHover, options);
|
|
25
|
+
return () => {
|
|
26
|
+
for (const type of POINTER_EVENTS)
|
|
27
|
+
doc.removeEventListener(type, onPointer, options);
|
|
28
|
+
for (const type of HOVER_EVENTS)
|
|
29
|
+
doc.removeEventListener(type, onHover, options);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
TOASTER_PHONE_POSITION,
|
|
34
|
+
TOASTER_PHONE_QUERY,
|
|
35
|
+
ignoreTouchHover
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
//# debugId=F1500A469841898064756E2164756E21
|
|
39
|
+
//# sourceMappingURL=touch-hover.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/touch-hover.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * A phone, as sonner sees one: under 600px wide it lays the toasts out\n * across the whole width (`@media (max-width: 600px)` in its styles).\n */\nexport const TOASTER_PHONE_QUERY = '(max-width: 600px)';\n\n/** Where toasts go on a phone when the caller names no `position`. */\nexport const TOASTER_PHONE_POSITION = 'top-center';\n\n/**\n * The mouse events a browser emulates after a tap (\"compatibility mouse\n * events\"): a tap reads as a mouse arriving over what was tapped and staying\n * there, with no `mouseleave` until the next tap lands somewhere else.\n */\nconst HOVER_EVENTS = ['mouseover', 'mouseenter', 'mousemove'] as const;\n\n/** The pointer events that say which kind of pointer is in use. */\nconst POINTER_EVENTS = ['pointerdown', 'pointermove', 'pointerup'] as const;\n\ntype Listen = Pick<Document, 'addEventListener' | 'removeEventListener'>;\n\n/**\n * Keeps a tap from hovering whatever matches `selector`: while the last\n * pointer was a finger (`pointerType: 'touch'`), the hover events the browser\n * emulates for it (`mouseover`, `mouseenter`, `mousemove`) stop at `doc`, in\n * the capture phase, before any handler inside the matching element hears\n * them. A mouse or a pen hovers as before: its first `pointermove` ends the\n * touch. `mouseout` and `mouseleave` always pass, so a hover that did start\n * still ends.\n *\n * Toasts pause while hovered (WCAG 2.2.1), and a tap on one used to leave it\n * \"hovered\", so paused, until the next tap elsewhere. Returns the function\n * that removes the listeners; nothing is read before an event arrives.\n */\nexport function ignoreTouchHover(doc: Listen, selector: string): () => void {\n\tlet touch = false;\n\tconst onPointer = (event: Event) => {\n\t\ttouch = (event as PointerEvent).pointerType === 'touch';\n\t};\n\tconst onHover = (event: Event) => {\n\t\tif (!touch) return;\n\t\tconst target = event.target as Partial<Element> | null;\n\t\tif (typeof target?.closest !== 'function') return;\n\t\tif (target.closest(selector)) event.stopPropagation();\n\t};\n\tconst options = { capture: true, passive: true };\n\tfor (const type of POINTER_EVENTS)\n\t\tdoc.addEventListener(type, onPointer, options);\n\tfor (const type of HOVER_EVENTS) doc.addEventListener(type, onHover, options);\n\treturn () => {\n\t\tfor (const type of POINTER_EVENTS)\n\t\t\tdoc.removeEventListener(type, onPointer, options);\n\t\tfor (const type of HOVER_EVENTS)\n\t\t\tdoc.removeEventListener(type, onHover, options);\n\t};\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AAIO,IAAM,sBAAsB;AAG5B,IAAM,yBAAyB;AAOtC,IAAM,eAAe,CAAC,aAAa,cAAc,WAAW;AAG5D,IAAM,iBAAiB,CAAC,eAAe,eAAe,WAAW;AAiB1D,SAAS,gBAAgB,CAAC,KAAa,UAA8B;AAAA,EAC3E,IAAI,QAAQ;AAAA,EACZ,MAAM,YAAY,CAAC,UAAiB;AAAA,IACnC,QAAS,MAAuB,gBAAgB;AAAA;AAAA,EAEjD,MAAM,UAAU,CAAC,UAAiB;AAAA,IACjC,IAAI,CAAC;AAAA,MAAO;AAAA,IACZ,MAAM,SAAS,MAAM;AAAA,IACrB,IAAI,OAAO,QAAQ,YAAY;AAAA,MAAY;AAAA,IAC3C,IAAI,OAAO,QAAQ,QAAQ;AAAA,MAAG,MAAM,gBAAgB;AAAA;AAAA,EAErD,MAAM,UAAU,EAAE,SAAS,MAAM,SAAS,KAAK;AAAA,EAC/C,WAAW,QAAQ;AAAA,IAClB,IAAI,iBAAiB,MAAM,WAAW,OAAO;AAAA,EAC9C,WAAW,QAAQ;AAAA,IAAc,IAAI,iBAAiB,MAAM,SAAS,OAAO;AAAA,EAC5E,OAAO,MAAM;AAAA,IACZ,WAAW,QAAQ;AAAA,MAClB,IAAI,oBAAoB,MAAM,WAAW,OAAO;AAAA,IACjD,WAAW,QAAQ;AAAA,MAClB,IAAI,oBAAoB,MAAM,SAAS,OAAO;AAAA;AAAA;",
|
|
8
|
+
"debugId": "F1500A469841898064756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** The values a message interpolates (a count, a unit, a pattern…). */
|
|
2
|
+
export type IssueContext = Record<string, string | number>;
|
|
3
|
+
/**
|
|
4
|
+
* The part of a zod issue these messages read, by shape: core has no zod
|
|
5
|
+
* dependency, and a zod issue is assignable to it.
|
|
6
|
+
*/
|
|
7
|
+
export type ZodIssueShape = {
|
|
8
|
+
code: string;
|
|
9
|
+
origin?: string;
|
|
10
|
+
inclusive?: boolean;
|
|
11
|
+
maximum?: unknown;
|
|
12
|
+
minimum?: unknown;
|
|
13
|
+
format?: string;
|
|
14
|
+
prefix?: unknown;
|
|
15
|
+
suffix?: unknown;
|
|
16
|
+
includes?: unknown;
|
|
17
|
+
pattern?: unknown;
|
|
18
|
+
};
|
|
19
|
+
/** How `zodLocaleError` formats one issue in the language it resolved. */
|
|
20
|
+
export type IssueText = {
|
|
21
|
+
/** The message at `key` (the key itself when the bundle lacks it). */
|
|
22
|
+
translate: (key: string, context?: IssueContext) => string;
|
|
23
|
+
/** The message at `key`, or `undefined` when the bundle lacks it. */
|
|
24
|
+
lookup: (key: string, context?: IssueContext) => string | undefined;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* A `too_big` or `too_small` issue. A string reads as a length ("Must
|
|
28
|
+
* contain at most 80 characters.", the count passed as a number so the
|
|
29
|
+
* message can pluralize it); anything else (a number, a list, a set, a file)
|
|
30
|
+
* as a size, with its unit when `material.zod.sizing.<origin>.unit` has one.
|
|
31
|
+
*/
|
|
32
|
+
export declare function sizeMessage(issue: ZodIssueShape, { translate, lookup }: IssueText): string;
|
|
33
|
+
/**
|
|
34
|
+
* An `invalid_format` issue: a prefix, a suffix, an included text or a
|
|
35
|
+
* pattern is named; any other format (`email`, `url`, …) by its noun, which
|
|
36
|
+
* opens the sentence ("{noun} invalide.") and so is capitalized.
|
|
37
|
+
*/
|
|
38
|
+
export declare function invalidFormatMessage(issue: ZodIssueShape, { translate, lookup }: IssueText): string;
|
|
39
|
+
//# sourceMappingURL=zod-issue-messages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zod-issue-messages.d.ts","sourceRoot":"","sources":["../../src/lib/zod-issue-messages.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;AAE3D;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,0EAA0E;AAC1E,MAAM,MAAM,SAAS,GAAG;IACvB,sEAAsE;IACtE,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,KAAK,MAAM,CAAC;IAC3D,qEAAqE;IACrE,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,KAAK,MAAM,GAAG,SAAS,CAAC;CACpE,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,WAAW,CAC1B,KAAK,EAAE,aAAa,EACpB,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,SAAS,UAmBhC;AAWD;;;;GAIG;AACH,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,aAAa,EACpB,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,SAAS,UAYhC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// src/lib/zod-issue-messages.ts
|
|
2
|
+
function sizeMessage(issue, { translate, lookup }) {
|
|
3
|
+
const code = issue.code === "too_big" ? "too_big" : "too_small";
|
|
4
|
+
const bound = code === "too_big" ? "maximum" : "minimum";
|
|
5
|
+
const adj = issue.inclusive ? "inclusive" : "exclusive";
|
|
6
|
+
if (issue.origin === "string") {
|
|
7
|
+
const text = lookup(`material.zod.${code}_string_${adj}`, {
|
|
8
|
+
[bound]: Number(issue[bound])
|
|
9
|
+
});
|
|
10
|
+
if (text)
|
|
11
|
+
return text;
|
|
12
|
+
}
|
|
13
|
+
const unit = lookup(`material.zod.sizing.${issue.origin}.unit`);
|
|
14
|
+
const context = {
|
|
15
|
+
origin: issue.origin ?? "value",
|
|
16
|
+
[bound]: String(issue[bound])
|
|
17
|
+
};
|
|
18
|
+
return unit ? translate(`material.zod.${code}_${adj}`, { ...context, unit }) : translate(`material.zod.${code}_no_unit_${adj}`, context);
|
|
19
|
+
}
|
|
20
|
+
var FORMAT_VALUES = {
|
|
21
|
+
starts_with: "prefix",
|
|
22
|
+
ends_with: "suffix",
|
|
23
|
+
includes: "includes",
|
|
24
|
+
regex: "pattern"
|
|
25
|
+
};
|
|
26
|
+
function invalidFormatMessage(issue, { translate, lookup }) {
|
|
27
|
+
const format = issue.format ?? "";
|
|
28
|
+
const value = FORMAT_VALUES[format];
|
|
29
|
+
if (value) {
|
|
30
|
+
return translate(`material.zod.invalid_format.${format}`, {
|
|
31
|
+
[value]: String(issue[value])
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const noun = lookup(`material.zod.nouns.${format}`) ?? format;
|
|
35
|
+
const message = translate("material.zod.invalid_format.default", { noun });
|
|
36
|
+
return message.charAt(0).toLocaleUpperCase() + message.slice(1);
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
invalidFormatMessage,
|
|
40
|
+
sizeMessage
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
//# debugId=5949BD7BC175EE9164756E2164756E21
|
|
44
|
+
//# sourceMappingURL=zod-issue-messages.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/zod-issue-messages.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/** The values a message interpolates (a count, a unit, a pattern…). */\nexport type IssueContext = Record<string, string | number>;\n\n/**\n * The part of a zod issue these messages read, by shape: core has no zod\n * dependency, and a zod issue is assignable to it.\n */\nexport type ZodIssueShape = {\n\tcode: string;\n\torigin?: string;\n\tinclusive?: boolean;\n\tmaximum?: unknown;\n\tminimum?: unknown;\n\tformat?: string;\n\tprefix?: unknown;\n\tsuffix?: unknown;\n\tincludes?: unknown;\n\tpattern?: unknown;\n};\n\n/** How `zodLocaleError` formats one issue in the language it resolved. */\nexport type IssueText = {\n\t/** The message at `key` (the key itself when the bundle lacks it). */\n\ttranslate: (key: string, context?: IssueContext) => string;\n\t/** The message at `key`, or `undefined` when the bundle lacks it. */\n\tlookup: (key: string, context?: IssueContext) => string | undefined;\n};\n\n/**\n * A `too_big` or `too_small` issue. A string reads as a length (\"Must\n * contain at most 80 characters.\", the count passed as a number so the\n * message can pluralize it); anything else (a number, a list, a set, a file)\n * as a size, with its unit when `material.zod.sizing.<origin>.unit` has one.\n */\nexport function sizeMessage(\n\tissue: ZodIssueShape,\n\t{ translate, lookup }: IssueText,\n) {\n\tconst code = issue.code === 'too_big' ? 'too_big' : 'too_small';\n\tconst bound = code === 'too_big' ? 'maximum' : 'minimum';\n\tconst adj = issue.inclusive ? 'inclusive' : 'exclusive';\n\tif (issue.origin === 'string') {\n\t\tconst text = lookup(`material.zod.${code}_string_${adj}`, {\n\t\t\t[bound]: Number(issue[bound]),\n\t\t});\n\t\tif (text) return text;\n\t}\n\tconst unit = lookup(`material.zod.sizing.${issue.origin}.unit`);\n\tconst context = {\n\t\torigin: issue.origin ?? 'value',\n\t\t[bound]: String(issue[bound]),\n\t};\n\treturn unit\n\t\t? translate(`material.zod.${code}_${adj}`, { ...context, unit })\n\t\t: translate(`material.zod.${code}_no_unit_${adj}`, context);\n}\n\n/** The value each `invalid_format` message of a fixed shape interpolates. */\ntype FormatValue = 'prefix' | 'suffix' | 'includes' | 'pattern';\nconst FORMAT_VALUES: Record<string, FormatValue> = {\n\tstarts_with: 'prefix',\n\tends_with: 'suffix',\n\tincludes: 'includes',\n\tregex: 'pattern',\n};\n\n/**\n * An `invalid_format` issue: a prefix, a suffix, an included text or a\n * pattern is named; any other format (`email`, `url`, …) by its noun, which\n * opens the sentence (\"{noun} invalide.\") and so is capitalized.\n */\nexport function invalidFormatMessage(\n\tissue: ZodIssueShape,\n\t{ translate, lookup }: IssueText,\n) {\n\tconst format = issue.format ?? '';\n\tconst value = FORMAT_VALUES[format];\n\tif (value) {\n\t\treturn translate(`material.zod.invalid_format.${format}`, {\n\t\t\t[value]: String(issue[value as FormatValue]),\n\t\t});\n\t}\n\tconst noun = lookup(`material.zod.nouns.${format}`) ?? format;\n\tconst message = translate('material.zod.invalid_format.default', { noun });\n\treturn message.charAt(0).toLocaleUpperCase() + message.slice(1);\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AAkCO,SAAS,WAAW,CAC1B,SACE,WAAW,UACZ;AAAA,EACD,MAAM,OAAO,MAAM,SAAS,YAAY,YAAY;AAAA,EACpD,MAAM,QAAQ,SAAS,YAAY,YAAY;AAAA,EAC/C,MAAM,MAAM,MAAM,YAAY,cAAc;AAAA,EAC5C,IAAI,MAAM,WAAW,UAAU;AAAA,IAC9B,MAAM,OAAO,OAAO,gBAAgB,eAAe,OAAO;AAAA,OACxD,QAAQ,OAAO,MAAM,MAAM;AAAA,IAC7B,CAAC;AAAA,IACD,IAAI;AAAA,MAAM,OAAO;AAAA,EAClB;AAAA,EACA,MAAM,OAAO,OAAO,uBAAuB,MAAM,aAAa;AAAA,EAC9D,MAAM,UAAU;AAAA,IACf,QAAQ,MAAM,UAAU;AAAA,KACvB,QAAQ,OAAO,MAAM,MAAM;AAAA,EAC7B;AAAA,EACA,OAAO,OACJ,UAAU,gBAAgB,QAAQ,OAAO,KAAK,SAAS,KAAK,CAAC,IAC7D,UAAU,gBAAgB,gBAAgB,OAAO,OAAO;AAAA;AAK5D,IAAM,gBAA6C;AAAA,EAClD,aAAa;AAAA,EACb,WAAW;AAAA,EACX,UAAU;AAAA,EACV,OAAO;AACR;AAOO,SAAS,oBAAoB,CACnC,SACE,WAAW,UACZ;AAAA,EACD,MAAM,SAAS,MAAM,UAAU;AAAA,EAC/B,MAAM,QAAQ,cAAc;AAAA,EAC5B,IAAI,OAAO;AAAA,IACV,OAAO,UAAU,+BAA+B,UAAU;AAAA,OACxD,QAAQ,OAAO,MAAM,MAAqB;AAAA,IAC5C,CAAC;AAAA,EACF;AAAA,EACA,MAAM,OAAO,OAAO,sBAAsB,QAAQ,KAAK;AAAA,EACvD,MAAM,UAAU,UAAU,uCAAuC,EAAE,KAAK,CAAC;AAAA,EACzE,OAAO,QAAQ,OAAO,CAAC,EAAE,kBAAkB,IAAI,QAAQ,MAAM,CAAC;AAAA;",
|
|
8
|
+
"debugId": "5949BD7BC175EE9164756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/docs/roadmap.md
CHANGED
|
@@ -31,6 +31,25 @@ Nothing in progress right now; see Next.
|
|
|
31
31
|
|
|
32
32
|
## Shipped
|
|
33
33
|
|
|
34
|
+
- **A field's first reachable control** (`/focus-control`) — the control of a
|
|
35
|
+
field made of several a user can reach, and a stand-in a form library can
|
|
36
|
+
focus, so both libraries focus the first invalid field. 0.5.0.
|
|
37
|
+
- **A field's `aria-describedby`** (`/described-by`) — the caller's ids and
|
|
38
|
+
the field's help or error ids joined into one value, for both libraries'
|
|
39
|
+
text-like fields. 0.5.0.
|
|
40
|
+
- **The page behind a modal layer made inert** (`/inert-others`) — everything
|
|
41
|
+
but the layer out of the tab order and the accessibility tree, live
|
|
42
|
+
regions kept, nested layers counted, for both libraries' dialogs, sheets,
|
|
43
|
+
drawers, selects and menus. 0.5.0.
|
|
44
|
+
- **Escape while an overlay closes** (`/escape-forward`) — an Escape pressed
|
|
45
|
+
during a layer's exit animation pressed again for the layer below once it
|
|
46
|
+
has gone, for both libraries' dialogs, sheets and drawers. 0.5.0.
|
|
47
|
+
- **A tap that does not hover** (`/touch-hover`) — a browser's emulated hover
|
|
48
|
+
after a tap ignored, so a toast a finger touched does not stay paused, and
|
|
49
|
+
where the `Toaster` puts toasts on a phone. 0.5.0.
|
|
50
|
+
- **Zod issue messages** (`/zod-issue-messages`) — the translated text of a
|
|
51
|
+
zod size or format issue, which both libraries' zod error maps read from
|
|
52
|
+
here. 0.5.0.
|
|
34
53
|
- **Clickable table rows** (`/row-activation`) — whether a click on a row
|
|
35
54
|
should follow its link (not on a control, not in a portal, not the end of
|
|
36
55
|
a text selection) and following it with the click's modifier keys, for
|
|
@@ -45,6 +64,8 @@ Nothing in progress right now; see Next.
|
|
|
45
64
|
- **Slider ticks** (`/slider-ticks`) — the values a slider's tick marks sit
|
|
46
65
|
at, which both libraries' `SliderField` (`ticks`) read from here instead of
|
|
47
66
|
keeping a copy each. 0.3.0.
|
|
67
|
+
- **Emoji groups** (`/emoji-groups`) — how the emoji list groups, searches
|
|
68
|
+
and names its emojis, which both libraries' `EmojiList` read from here. 0.6.0.
|
|
48
69
|
- **`formatFileSize` and the pdfkit fonts resolution** — the byte-count
|
|
49
70
|
formatter both libraries' file list and upload field use
|
|
50
71
|
(`/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, 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.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",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"email": "stevegires@gmail.com"
|
|
@@ -93,6 +93,36 @@
|
|
|
93
93
|
"import": "./dist/lib/field-blur-layers.js",
|
|
94
94
|
"default": "./dist/lib/field-blur-layers.js"
|
|
95
95
|
},
|
|
96
|
+
"./focus-control": {
|
|
97
|
+
"types": "./dist/lib/focus-control.d.ts",
|
|
98
|
+
"import": "./dist/lib/focus-control.js",
|
|
99
|
+
"default": "./dist/lib/focus-control.js"
|
|
100
|
+
},
|
|
101
|
+
"./escape-forward": {
|
|
102
|
+
"types": "./dist/lib/escape-forward.d.ts",
|
|
103
|
+
"import": "./dist/lib/escape-forward.js",
|
|
104
|
+
"default": "./dist/lib/escape-forward.js"
|
|
105
|
+
},
|
|
106
|
+
"./described-by": {
|
|
107
|
+
"types": "./dist/lib/described-by.d.ts",
|
|
108
|
+
"import": "./dist/lib/described-by.js",
|
|
109
|
+
"default": "./dist/lib/described-by.js"
|
|
110
|
+
},
|
|
111
|
+
"./touch-hover": {
|
|
112
|
+
"types": "./dist/lib/touch-hover.d.ts",
|
|
113
|
+
"import": "./dist/lib/touch-hover.js",
|
|
114
|
+
"default": "./dist/lib/touch-hover.js"
|
|
115
|
+
},
|
|
116
|
+
"./zod-issue-messages": {
|
|
117
|
+
"types": "./dist/lib/zod-issue-messages.d.ts",
|
|
118
|
+
"import": "./dist/lib/zod-issue-messages.js",
|
|
119
|
+
"default": "./dist/lib/zod-issue-messages.js"
|
|
120
|
+
},
|
|
121
|
+
"./inert-others": {
|
|
122
|
+
"types": "./dist/lib/inert-others.d.ts",
|
|
123
|
+
"import": "./dist/lib/inert-others.js",
|
|
124
|
+
"default": "./dist/lib/inert-others.js"
|
|
125
|
+
},
|
|
96
126
|
"./row-activation": {
|
|
97
127
|
"types": "./dist/lib/row-activation.d.ts",
|
|
98
128
|
"import": "./dist/lib/row-activation.js",
|
|
@@ -108,6 +138,11 @@
|
|
|
108
138
|
"import": "./dist/lib/slider-ticks.js",
|
|
109
139
|
"default": "./dist/lib/slider-ticks.js"
|
|
110
140
|
},
|
|
141
|
+
"./emoji-groups": {
|
|
142
|
+
"types": "./dist/lib/emoji-groups.d.ts",
|
|
143
|
+
"import": "./dist/lib/emoji-groups.js",
|
|
144
|
+
"default": "./dist/lib/emoji-groups.js"
|
|
145
|
+
},
|
|
111
146
|
"./date-range": {
|
|
112
147
|
"types": "./dist/lib/date-range.d.ts",
|
|
113
148
|
"import": "./dist/lib/date-range.js",
|
|
@@ -177,9 +212,16 @@
|
|
|
177
212
|
"src/lib/pdf-download.ts",
|
|
178
213
|
"src/lib/currency-digits.ts",
|
|
179
214
|
"src/lib/field-blur-layers.ts",
|
|
215
|
+
"src/lib/focus-control.ts",
|
|
216
|
+
"src/lib/escape-forward.ts",
|
|
217
|
+
"src/lib/described-by.ts",
|
|
218
|
+
"src/lib/touch-hover.ts",
|
|
219
|
+
"src/lib/zod-issue-messages.ts",
|
|
220
|
+
"src/lib/inert-others.ts",
|
|
180
221
|
"src/lib/row-activation.ts",
|
|
181
222
|
"src/lib/format-file-size.ts",
|
|
182
223
|
"src/lib/slider-ticks.ts",
|
|
224
|
+
"src/lib/emoji-groups.ts",
|
|
183
225
|
"src/lib/date-range.ts",
|
|
184
226
|
"src/file-preview/index.ts",
|
|
185
227
|
"src/pdf-viewer/index.ts",
|