@nxgt/material-core 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # @nxgt/material-core
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#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.
8
+
9
+ - [#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.
10
+
11
+ 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.
12
+
13
+ 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.
14
+
15
+ - [#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.
16
+
17
+ - [#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.
18
+
19
+ The subpath also exports `TOASTER_PHONE_QUERY` and `TOASTER_PHONE_POSITION`, the phone width and default position both libraries' `Toaster` share.
20
+
21
+ ## 0.4.0
22
+
23
+ ### Minor Changes
24
+
25
+ - [#431](https://github.com/softistx/nxgt-material/pull/431) [`bd58a6f`](https://github.com/softistx/nxgt-material/commit/bd58a6f67bdc25d4b4d0d7d88c52784869ad1529) Thanks [@SteveGT96](https://github.com/SteveGT96)! - `DataTable` renders server-side data: one page from a loader, with the query in the URL. Everything is opt-in; a table that sets none of the new props renders and behaves as before.
26
+
27
+ - `manual`: `data` is one server page, rendered as given — no client paging, sorting or filtering — and the footer becomes `DataTableServerFooter` (exported).
28
+ - `pagination={{ totalCount, offset, previous, next }}`: the server footer's range ("41–60 of 133", `material.table.pagination.range`) and Previous / Next, each an href (a real link, `rel="prev"` / `rel="next"`, that works with no JavaScript) or a callback (a button); disabled when absent and while `loading`.
29
+ - Controlled sorting: `sorting` + `onSortingChange(sorting)` in React, `v-model:sorting` in Vue. The headers show the sorting they are given; the callback receives the new `SortingState`, not an updater.
30
+ - `getRowId(row, index)` and controlled selection by id: `rowSelection` + `onRowSelectionChange(selection)` in React, `v-model:row-selection` in Vue. Ids of other pages are kept, and the server footer counts them all.
31
+ - `getRowHref(row)`: the first data cell becomes a link to the row's page, and a click anywhere else on the row (but on a control, in a portal, or at the end of a text selection) follows it. `onRowClick(row, event)` (`@row-click` in Vue) makes the row focusable and activatable with Enter.
32
+ - `linkComponent`: the link rendered for row and page links, given `href` (wrap React Router's `Link` or vue-router's `RouterLink`, which take `to`). Default: a plain `<a>`.
33
+ - `footer`: a render function (React, Vue) or the `#footer` slot (Vue) replaces the footer; `footer={false}` removes it.
34
+ - New `material.table.pagination.*` strings, in English and French: `label`, `range`, `range-open`, `total`, `selected`.
35
+
36
+ `@nxgt/material-core` gains `/row-activation` (`isRowActivation`, `followRowLink`, `ROW_CONTROL_SELECTOR`, `ROW_LINK_SELECTOR`, `RowClick`): the clickable-row rule both libraries' `DataTable` use.
37
+
3
38
  ## 0.3.2
4
39
 
5
40
  ### Patch 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 fields' blur rule, the Vite plugin that bundles
9
- pdfkit's standard fonts, and the Node-only resolution of those fonts that the
10
- Next.js helper and the Nuxt module run at build time.
8
+ the file-size formatter, the slider's tick marks, the date range presets, the
9
+ file preview's kind detection, the fields' blur rule, a field's first
10
+ reachable control (`/focus-control`) and its `aria-describedby`
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, file
16
- list and upload field (`formatFileSize`), `@nxgt/material/next` and
17
- `@nxgt/material-vue/nuxt`. Import it directly when you want the model or a formatter without
18
- a UI library.
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,6 +80,13 @@ 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 |
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 |
73
90
  | `/format-file-size` | `formatFileSize` — a byte count as `'1.5 KB'` |
74
91
  | `/slider-ticks` | `tickValues` — where a slider's tick marks sit |
75
92
  | `/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 |
@@ -465,6 +482,155 @@ field.addEventListener('focusout', (event) => {
465
482
  });
466
483
  ```
467
484
 
485
+ ### Focusing a field made of several controls: `/focus-control`
486
+
487
+ `firstControl(roots)` returns the first control in the roots a user can
488
+ reach (one they can Tab to that is rendered, not a `display: none` input a
489
+ library keeps), or `null`. `focusHandle(roots)` builds a stand-in element for
490
+ a form library that focuses an invalid field by calling `focus()` on what the
491
+ field registered, as React Hook Form does with `field.ref`: `focus()` focuses
492
+ that first control, looked up when it is called (and once more a turn later,
493
+ when a re-render replaced it). Pass a live collection — the `Set` of the
494
+ field's roots — and both read the DOM only when called.
495
+
496
+ ```ts
497
+ import { firstControl, focusHandle } from '@nxgt/material-core/focus-control';
498
+
499
+ const roots = new Set<Element>([document.querySelector('#delivery-dates')!]);
500
+ firstControl(roots)?.focus(); // the first date input, not a hidden one
501
+ field.ref(focusHandle(roots)); // React Hook Form: shouldFocusError reaches it
502
+ ```
503
+
504
+ ### Escape while an overlay closes: `/escape-forward`
505
+
506
+ Radix's and reka's dismissable layers give Escape to the highest layer only,
507
+ and a dialog's layer leaves their stack when its content unmounts, not when
508
+ it closes: an Escape pressed during the exit animation went to a dialog that
509
+ was already closed, and the overlay below stayed open.
510
+ `forwardEscape(content, event)`, called from the content's escape handler
511
+ after the caller's, does nothing unless `content` has `data-state="closed"`
512
+ and the default was not prevented. Then it prevents the default and presses
513
+ Escape again (a new `keydown` on what has focus) once `content` has left the
514
+ page, two frames later, so the layer below has taken over. It returns
515
+ whether it did. A content that is still there after about two seconds (a
516
+ `forceMount`) is no longer waited on.
517
+
518
+ ```ts
519
+ import { forwardEscape } from '@nxgt/material-core/escape-forward';
520
+
521
+ const contentElement = document.querySelector<HTMLElement>('[role="dialog"]');
522
+ const own = (event: KeyboardEvent) => console.log(event.key); // the caller's handler
523
+
524
+ function onEscapeKeyDown(event: KeyboardEvent) {
525
+ own(event); // the caller's handler first
526
+ forwardEscape(contentElement, event);
527
+ }
528
+ ```
529
+
530
+ ### A field's `aria-describedby`: `/described-by`
531
+
532
+ `describedBy(own, ...ids)` joins the ids a field's control is described by:
533
+ the caller's own `aria-describedby` first, then the field's (its help or
534
+ error text), with `undefined`, `false` and empty ids left out. It returns
535
+ `undefined` when none is left, so the attribute is not rendered at all.
536
+
537
+ ```ts
538
+ import { describedBy } from '@nxgt/material-core/described-by';
539
+
540
+ describedBy('hint', error && 'email-error'); // 'hint email-error', or 'hint'
541
+ describedBy(undefined, false); // undefined: no attribute
542
+ ```
543
+
544
+ ### The page behind a modal layer: `/inert-others`
545
+
546
+ `inertOthers(content, keep?)` makes everything in `<body>` but a modal
547
+ layer's content `inert` — out of the tab order and of the accessibility tree
548
+ — and returns the undo. It marks the elements `aria-hidden`'s `hideOthers`
549
+ marks `aria-hidden` (the siblings of the content and of its ancestors), so
550
+ the two libraries' modal layers pass axe's `aria-hidden-focus`. Live regions
551
+ and scripts stay reachable — the selector `hideOthers` keeps,
552
+ `[aria-live], script`, so a `role="status"` without `aria-live` does not —
553
+ and so does anything in `keep`. Each element is counted per
554
+ layer: nested layers (a select in a dialog) undo in any order, and an element
555
+ that was already `inert` is left as it was. Focus still on the page behind
556
+ goes to the content a microtask later, unless something moved it into the
557
+ layer by then. The undo runs once, by your call or by itself when the content
558
+ leaves the document; marked elements carry `data-nxgt-inert`
559
+ (`INERT_MARKER`). It reads the DOM only when called, and does nothing
560
+ without a content in the document (on the server). Elements mounted after
561
+ the call are not made inert: mount them before the layer opens, or pass them
562
+ in `keep` when they must stay reachable.
563
+
564
+ ```ts
565
+ import { inertOthers } from '@nxgt/material-core/inert-others';
566
+
567
+ const undo = inertOthers(dialogContent);
568
+ // … the dialog closes:
569
+ undo();
570
+ ```
571
+
572
+ ### A tap that does not hover: `/touch-hover`
573
+
574
+ After a tap, a browser emulates a mouse: `mouseover`, `mouseenter` and
575
+ `mousemove` arrive on what was tapped, and no `mouseleave` follows until the
576
+ next tap lands somewhere else. Anything that pauses while hovered (a toast,
577
+ under WCAG 2.2.1) stays paused. `ignoreTouchHover(document, selector)`
578
+ listens in the capture phase and, while the last pointer was a finger
579
+ (`pointerType: 'touch'`), stops those three events before any handler inside
580
+ an element matching `selector` hears them. A mouse or a pen hovers as before
581
+ (its first `pointermove` ends the touch), and `mouseout` / `mouseleave`
582
+ always pass. After a tap, bubble-phase listeners over a toast no longer see the
583
+ emulated `mouseover`, `mouseenter` and `mousemove`. It returns the function that
584
+ removes its listeners.
585
+
586
+ ```ts
587
+ import { ignoreTouchHover } from '@nxgt/material-core/touch-hover';
588
+
589
+ const stop = ignoreTouchHover(document, '[data-sonner-toaster]');
590
+ // …on unmount
591
+ stop();
592
+ ```
593
+
594
+ It also holds the two constants both libraries' `Toaster` share: `TOASTER_PHONE_QUERY`
595
+ (`'(max-width: 600px)'`, the width under which sonner lays toasts out across the
596
+ screen) and `TOASTER_PHONE_POSITION` (`'top-center'`, where toasts go on a phone
597
+ when the caller names no `position`).
598
+
599
+ ### Zod issue messages: `/zod-issue-messages`
600
+
601
+ The two libraries' zod error maps turn an issue into text in the language
602
+ they resolved. The two issues with several shapes are formatted here:
603
+ `sizeMessage(issue, { translate, lookup })` for `too_big` / `too_small` (a
604
+ string reads as a length, anything else as a size with its unit) and
605
+ `invalidFormatMessage(issue, { translate, lookup })` for `invalid_format`.
606
+ `translate` returns the message at a key (the key itself when missing),
607
+ `lookup` returns it or `undefined`. A zod issue is passed as is: core reads it
608
+ by shape and has no zod dependency.
609
+
610
+ ```ts
611
+ import { sizeMessage } from '@nxgt/material-core/zod-issue-messages';
612
+
613
+ sizeMessage(issue, { translate, lookup }); // "Must contain at most 80 characters."
614
+ ```
615
+
616
+ ### A clickable table row: `/row-activation`
617
+
618
+ A row whose first cell links to its page should follow that link on a click
619
+ anywhere else on the row — but not on its checkbox or its buttons, not from
620
+ a menu its cell opened into `<body>`, and not when the click ends a text
621
+ selection. `isRowActivation` says whether a click is one; `followRowLink`
622
+ clicks the row's link (`ROW_LINK_SELECTOR`) with the click's modifier keys,
623
+ so a router's link navigates as it does on its own click. Both read the DOM
624
+ when called, never at import.
625
+
626
+ ```ts
627
+ import { followRowLink, isRowActivation } from '@nxgt/material-core/row-activation';
628
+
629
+ row.addEventListener('click', (event) => {
630
+ if (isRowActivation(event, row)) followRowLink(row, event);
631
+ });
632
+ ```
633
+
468
634
  ### Bundling pdfkit's fonts: `/vite`
469
635
 
470
636
  An SSR server bundled into a single file with no `node_modules` needs
@@ -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
+ // src/lib/described-by.ts
2
+ function describedBy(own, ...ids) {
3
+ return [own, ...ids].filter(Boolean).join(" ") || undefined;
4
+ }
5
+ export {
6
+ describedBy
7
+ };
8
+
9
+ //# debugId=A732F6C9ECA1AE0864756E2164756E21
10
+ //# sourceMappingURL=described-by.js.map
@@ -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
+ /**
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,30 @@
1
+ /** What a click inside a row must not activate the row from: its controls
2
+ * (checkboxes, buttons, links, menus, fields) and anything marked
3
+ * `data-row-click-ignore`. */
4
+ export declare const ROW_CONTROL_SELECTOR: string;
5
+ /** The row's own link, which a click on the row follows: the element marked
6
+ * `data-slot="data-table-row-link"`, else the first link of the cell marked
7
+ * `data-row-link-cell` (for a link component that drops the `data-slot` it
8
+ * is given). */
9
+ export declare const ROW_LINK_SELECTOR = "[data-slot=\"data-table-row-link\"],[data-row-link-cell] a[href]";
10
+ /** The parts of a click this module reads. */
11
+ export type RowClick = {
12
+ target: EventTarget | null;
13
+ button: number;
14
+ defaultPrevented: boolean;
15
+ ctrlKey?: boolean;
16
+ metaKey?: boolean;
17
+ shiftKey?: boolean;
18
+ altKey?: boolean;
19
+ };
20
+ /** Whether `event`, a click that reached `row`, activates the row: a primary
21
+ * click, not handled yet, inside the row's own DOM (not in a portal its
22
+ * cells opened), not on one of its controls, and not the end of a text
23
+ * selection in it. */
24
+ export declare function isRowActivation(event: RowClick, row: Element): boolean;
25
+ /** Follows the row's link (`ROW_LINK_SELECTOR`) as if it had been clicked,
26
+ * with the modifier keys of `event`: a router's link navigates as it does on
27
+ * its own click, Ctrl/Cmd+click opens a tab where the browser allows it.
28
+ * `false` when the row has no link. */
29
+ export declare function followRowLink(row: Element, event: RowClick): boolean;
30
+ //# sourceMappingURL=row-activation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"row-activation.d.ts","sourceRoot":"","sources":["../../src/lib/row-activation.ts"],"names":[],"mappings":"AAIA;;8BAE8B;AAC9B,eAAO,MAAM,oBAAoB,QAgBtB,CAAC;AAEZ;;;gBAGgB;AAChB,eAAO,MAAM,iBAAiB,qEACmC,CAAC;AAElE,8CAA8C;AAC9C,MAAM,MAAM,QAAQ,GAAG;IACtB,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,OAAO,CAAC;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAMF;;;sBAGsB;AACtB,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAgBtE;AAED;;;uCAGuC;AACvC,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAgBpE"}
@@ -0,0 +1,62 @@
1
+ // src/lib/row-activation.ts
2
+ var ROW_CONTROL_SELECTOR = [
3
+ "a[href]",
4
+ "button",
5
+ "input",
6
+ "select",
7
+ "textarea",
8
+ "label",
9
+ "summary",
10
+ '[contenteditable="true"]',
11
+ '[role="button"]',
12
+ '[role="checkbox"]',
13
+ '[role="link"]',
14
+ '[role="menuitem"]',
15
+ '[role="switch"]',
16
+ '[role="combobox"]',
17
+ "[data-row-click-ignore]"
18
+ ].join(",");
19
+ var ROW_LINK_SELECTOR = '[data-slot="data-table-row-link"],[data-row-link-cell] a[href]';
20
+ function isElement(target) {
21
+ return target !== null && typeof target.closest === "function";
22
+ }
23
+ function isRowActivation(event, row) {
24
+ if (event.defaultPrevented || event.button !== 0)
25
+ return false;
26
+ const { target } = event;
27
+ if (!isElement(target) || !row.contains(target))
28
+ return false;
29
+ const control = target.closest(ROW_CONTROL_SELECTOR);
30
+ if (control && control !== row && row.contains(control))
31
+ return false;
32
+ const selection = row.ownerDocument?.getSelection?.();
33
+ if (selection && !selection.isCollapsed && selection.anchorNode && row.contains(selection.anchorNode)) {
34
+ return false;
35
+ }
36
+ return true;
37
+ }
38
+ function followRowLink(row, event) {
39
+ const link = row.querySelector(ROW_LINK_SELECTOR);
40
+ const view = row.ownerDocument?.defaultView;
41
+ if (!link || !view)
42
+ return false;
43
+ link.dispatchEvent(new view.MouseEvent("click", {
44
+ bubbles: true,
45
+ cancelable: true,
46
+ button: 0,
47
+ ctrlKey: event.ctrlKey,
48
+ metaKey: event.metaKey,
49
+ shiftKey: event.shiftKey,
50
+ altKey: event.altKey
51
+ }));
52
+ return true;
53
+ }
54
+ export {
55
+ ROW_CONTROL_SELECTOR,
56
+ ROW_LINK_SELECTOR,
57
+ followRowLink,
58
+ isRowActivation
59
+ };
60
+
61
+ //# debugId=B5C72BCA21047D6B64756E2164756E21
62
+ //# sourceMappingURL=row-activation.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/lib/row-activation.ts"],
4
+ "sourcesContent": [
5
+ "// A clickable table row: a click anywhere on the row, but on a control of its\n// own, does what the row's link (or its click handler) does. Shared by both\n// libraries' `DataTable`. Reads the DOM only when called, never at import.\n\n/** What a click inside a row must not activate the row from: its controls\n * (checkboxes, buttons, links, menus, fields) and anything marked\n * `data-row-click-ignore`. */\nexport const ROW_CONTROL_SELECTOR = [\n\t'a[href]',\n\t'button',\n\t'input',\n\t'select',\n\t'textarea',\n\t'label',\n\t'summary',\n\t'[contenteditable=\"true\"]',\n\t'[role=\"button\"]',\n\t'[role=\"checkbox\"]',\n\t'[role=\"link\"]',\n\t'[role=\"menuitem\"]',\n\t'[role=\"switch\"]',\n\t'[role=\"combobox\"]',\n\t'[data-row-click-ignore]',\n].join(',');\n\n/** The row's own link, which a click on the row follows: the element marked\n * `data-slot=\"data-table-row-link\"`, else the first link of the cell marked\n * `data-row-link-cell` (for a link component that drops the `data-slot` it\n * is given). */\nexport const ROW_LINK_SELECTOR =\n\t'[data-slot=\"data-table-row-link\"],[data-row-link-cell] a[href]';\n\n/** The parts of a click this module reads. */\nexport type RowClick = {\n\ttarget: EventTarget | null;\n\tbutton: number;\n\tdefaultPrevented: boolean;\n\tctrlKey?: boolean;\n\tmetaKey?: boolean;\n\tshiftKey?: boolean;\n\taltKey?: boolean;\n};\n\nfunction isElement(target: EventTarget | null): target is Element {\n\treturn target !== null && typeof (target as Element).closest === 'function';\n}\n\n/** Whether `event`, a click that reached `row`, activates the row: a primary\n * click, not handled yet, inside the row's own DOM (not in a portal its\n * cells opened), not on one of its controls, and not the end of a text\n * selection in it. */\nexport function isRowActivation(event: RowClick, row: Element): boolean {\n\tif (event.defaultPrevented || event.button !== 0) return false;\n\tconst { target } = event;\n\tif (!isElement(target) || !row.contains(target)) return false;\n\tconst control = target.closest(ROW_CONTROL_SELECTOR);\n\tif (control && control !== row && row.contains(control)) return false;\n\tconst selection = row.ownerDocument?.getSelection?.();\n\tif (\n\t\tselection &&\n\t\t!selection.isCollapsed &&\n\t\tselection.anchorNode &&\n\t\trow.contains(selection.anchorNode)\n\t) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\n/** Follows the row's link (`ROW_LINK_SELECTOR`) as if it had been clicked,\n * with the modifier keys of `event`: a router's link navigates as it does on\n * its own click, Ctrl/Cmd+click opens a tab where the browser allows it.\n * `false` when the row has no link. */\nexport function followRowLink(row: Element, event: RowClick): boolean {\n\tconst link = row.querySelector(ROW_LINK_SELECTOR);\n\tconst view = row.ownerDocument?.defaultView;\n\tif (!link || !view) return false;\n\tlink.dispatchEvent(\n\t\tnew view.MouseEvent('click', {\n\t\t\tbubbles: true,\n\t\t\tcancelable: true,\n\t\t\tbutton: 0,\n\t\t\tctrlKey: event.ctrlKey,\n\t\t\tmetaKey: event.metaKey,\n\t\t\tshiftKey: event.shiftKey,\n\t\t\taltKey: event.altKey,\n\t\t}),\n\t);\n\treturn true;\n}\n"
6
+ ],
7
+ "mappings": ";AAOO,IAAM,uBAAuB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,EAAE,KAAK,GAAG;AAMH,IAAM,oBACZ;AAaD,SAAS,SAAS,CAAC,QAA+C;AAAA,EACjE,OAAO,WAAW,QAAQ,OAAQ,OAAmB,YAAY;AAAA;AAO3D,SAAS,eAAe,CAAC,OAAiB,KAAuB;AAAA,EACvE,IAAI,MAAM,oBAAoB,MAAM,WAAW;AAAA,IAAG,OAAO;AAAA,EACzD,QAAQ,WAAW;AAAA,EACnB,IAAI,CAAC,UAAU,MAAM,KAAK,CAAC,IAAI,SAAS,MAAM;AAAA,IAAG,OAAO;AAAA,EACxD,MAAM,UAAU,OAAO,QAAQ,oBAAoB;AAAA,EACnD,IAAI,WAAW,YAAY,OAAO,IAAI,SAAS,OAAO;AAAA,IAAG,OAAO;AAAA,EAChE,MAAM,YAAY,IAAI,eAAe,eAAe;AAAA,EACpD,IACC,aACA,CAAC,UAAU,eACX,UAAU,cACV,IAAI,SAAS,UAAU,UAAU,GAChC;AAAA,IACD,OAAO;AAAA,EACR;AAAA,EACA,OAAO;AAAA;AAOD,SAAS,aAAa,CAAC,KAAc,OAA0B;AAAA,EACrE,MAAM,OAAO,IAAI,cAAc,iBAAiB;AAAA,EAChD,MAAM,OAAO,IAAI,eAAe;AAAA,EAChC,IAAI,CAAC,QAAQ,CAAC;AAAA,IAAM,OAAO;AAAA,EAC3B,KAAK,cACJ,IAAI,KAAK,WAAW,SAAS;AAAA,IAC5B,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,SAAS,MAAM;AAAA,IACf,SAAS,MAAM;AAAA,IACf,UAAU,MAAM;AAAA,IAChB,QAAQ,MAAM;AAAA,EACf,CAAC,CACF;AAAA,EACA,OAAO;AAAA;",
8
+ "debugId": "B5C72BCA21047D6B64756E2164756E21",
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,29 @@ 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.
53
+ - **Clickable table rows** (`/row-activation`) — whether a click on a row
54
+ should follow its link (not on a control, not in a portal, not the end of
55
+ a text selection) and following it with the click's modifier keys, for
56
+ both libraries' `DataTable`. 0.4.0.
34
57
  - **Date range presets and times** (`/date-range`) — the built-in presets'
35
58
  ranges, whether a field's rules rule a range out, and the time of day on
36
59
  each end, for both libraries' `DateRangeField`. 0.3.0.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nxgt/material-core",
3
- "version": "0.3.2",
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.5.0",
4
+ "description": "The framework-free core of @nxgt/material and @nxgt/material-vue: the PDF document model and its react-pdf renderer, print formatters, PDF file helpers, the PDF viewer's pure modules, the currency digits, the file-size formatter, the field-blur layers, a field's first focusable control, the aria-describedby joiner, the inert background of a modal layer, Escape forwarded while an overlay closes, the Toaster's touch-hover guard, the zod issue messages, the date range presets, the file preview's kind detection and text reader, the pdfkit fonts Vite plugin and the Node-only pdfkit fonts resolution for Next.js and Nuxt builds",
5
5
  "license": "MIT",
6
6
  "bugs": {
7
7
  "email": "stevegires@gmail.com"
@@ -93,6 +93,41 @@
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
+ },
126
+ "./row-activation": {
127
+ "types": "./dist/lib/row-activation.d.ts",
128
+ "import": "./dist/lib/row-activation.js",
129
+ "default": "./dist/lib/row-activation.js"
130
+ },
96
131
  "./format-file-size": {
97
132
  "types": "./dist/lib/format-file-size.d.ts",
98
133
  "import": "./dist/lib/format-file-size.js",
@@ -172,6 +207,13 @@
172
207
  "src/lib/pdf-download.ts",
173
208
  "src/lib/currency-digits.ts",
174
209
  "src/lib/field-blur-layers.ts",
210
+ "src/lib/focus-control.ts",
211
+ "src/lib/escape-forward.ts",
212
+ "src/lib/described-by.ts",
213
+ "src/lib/touch-hover.ts",
214
+ "src/lib/zod-issue-messages.ts",
215
+ "src/lib/inert-others.ts",
216
+ "src/lib/row-activation.ts",
175
217
  "src/lib/format-file-size.ts",
176
218
  "src/lib/slider-ticks.ts",
177
219
  "src/lib/date-range.ts",