@nxgt/material-core 0.10.0 → 0.11.1
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 +75 -0
- package/README.md +41 -1
- package/dist/chunks/index-407b6zs0.js +41 -0
- package/dist/chunks/index-407b6zs0.js.map +10 -0
- package/dist/lib/focus-control.js +7 -39
- package/dist/lib/focus-control.js.map +3 -4
- package/dist/lib/repeatable-group.d.ts +70 -0
- package/dist/lib/repeatable-group.d.ts.map +1 -0
- package/dist/lib/repeatable-group.js +59 -0
- package/dist/lib/repeatable-group.js.map +10 -0
- package/dist/lib/touch-hover.d.ts +7 -0
- package/dist/lib/touch-hover.d.ts.map +1 -1
- package/dist/lib/touch-hover.js +3 -1
- package/dist/lib/touch-hover.js.map +3 -3
- package/docs/roadmap.md +4 -0
- package/package.json +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,80 @@
|
|
|
1
1
|
# @nxgt/material-core
|
|
2
2
|
|
|
3
|
+
## 0.11.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#607](https://github.com/softistx/nxgt-material/pull/607) [`3b671a5`](https://github.com/softistx/nxgt-material/commit/3b671a5b8d0109fa645d0de52bc716104f7f8831) Thanks [@SteveGT96](https://github.com/SteveGT96)! - Two fixes seen on a phone and on the collapsed sidebar (softistx/nxgt-material#605).
|
|
8
|
+
|
|
9
|
+
- **`SidebarMenuItem` no tooltip beside a visible label.** The collapsed rail showed each label under its icon and also opened a tooltip repeating it, above the entry, over the entry above it (the selected pill). An entry that shows its label, collapsed or expanded, now has no tooltip; its accessible name is the label. A `tooltip` you pass is still shown, now to the right of the entry (`side="right"`) so it never covers a neighbour, and an icon-only entry (no label, an `aria-label`) shows its `aria-label` there. Keyboard focus shows it as before. `Tooltip` (React) takes a `side` prop. In `@nxgt/material-vue`, `SidebarMenuItem` now binds its attributes (`aria-label`, `onClick`...) to the link or button itself.
|
|
10
|
+
- **`Toaster` on a phone starts below the app bar.** Under 600px with no `position`, toasts sat at the very top and covered the app shell's sticky bar (menu button, title, account control), which could not be reached while a toast showed. They now start 80px down (`mobileOffset` `{ top: 80 }`, from `@nxgt/material-core/touch-hover`). Desktop is unchanged, and a `position` or a `mobileOffset` you pass is kept as is.
|
|
11
|
+
|
|
12
|
+
Migration: an app that relied on the toast at the very top of a phone, or on the collapsed rail's label tooltip, sets `<Toaster mobileOffset={{ top: 16 }} />` (Vue: `:mobile-offset="{ top: 16 }"`) or passes `tooltip` on the entry.
|
|
13
|
+
|
|
14
|
+
## 0.11.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- [#574](https://github.com/softistx/nxgt-material/pull/574) [`c00f8c3`](https://github.com/softistx/nxgt-material/commit/c00f8c366e35b7769560bb86b964ae0705bcc60f) Thanks [@SteveGT96](https://github.com/SteveGT96)! - Add `RepeatableGroup` and `RepeatableGroupFormField` (Storybook
|
|
19
|
+
`Form fields/RepeatableGroup`): rows of fields a user adds, removes and, with
|
|
20
|
+
`reorderable`, moves up and down. `RepeatableGroupFormField` wraps the form
|
|
21
|
+
library's `useFieldArray` (react-hook-form in React, vee-validate in Vue) and
|
|
22
|
+
renders each row from `renderRow` (React) or the `row` slot (Vue), whose
|
|
23
|
+
`path` names the row's fields. The rows are a list (`ul` / `li`) in a group
|
|
24
|
+
named by `label`; each row's buttons are named after it ("Remove period 2",
|
|
25
|
+
"Move period 2 up"), from the new `material.repeatable-group.*` keys in
|
|
26
|
+
English and French. An added row's first field takes the focus, a removed row
|
|
27
|
+
hands it to its neighbour or to Add, and a moved row keeps it on its move
|
|
28
|
+
button; each change is announced politely. `max` makes Add `aria-disabled`,
|
|
29
|
+
described by "Maximum reached (n)"; `min` disables Remove; `emptyTitle` /
|
|
30
|
+
`emptyDescription` show an `EmptyState` with no row. The array's own error
|
|
31
|
+
shows under the rows and describes the group; each row's fields show their
|
|
32
|
+
own. One column on a phone, a responsive grid wider up, no inline style.
|
|
33
|
+
|
|
34
|
+
`@nxgt/material-core` gains `/repeatable-group`: the focus rules
|
|
35
|
+
(`focusAfterRemove`, `focusAfterMove`, `focusRowTarget`), the limits
|
|
36
|
+
(`canAddRow`, `canRemoveRow`, `rowMoveTarget`) and the `RowAnnouncement`
|
|
37
|
+
descriptor both libraries use.
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
<RepeatableGroupFormField
|
|
41
|
+
control={control}
|
|
42
|
+
name="periods"
|
|
43
|
+
label="Periods"
|
|
44
|
+
rowLabel="period"
|
|
45
|
+
max={6}
|
|
46
|
+
reorderable
|
|
47
|
+
newRow={() => ({ name: '' })}
|
|
48
|
+
renderRow={({ index }) => (
|
|
49
|
+
<TextFormField control={control} name={`periods.${index}.name`} label="Name" />
|
|
50
|
+
)}
|
|
51
|
+
/>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```vue
|
|
55
|
+
<RepeatableGroupFormField
|
|
56
|
+
name="periods"
|
|
57
|
+
label="Periods"
|
|
58
|
+
row-label="period"
|
|
59
|
+
:max="6"
|
|
60
|
+
reorderable
|
|
61
|
+
:new-row="() => ({ name: '' })"
|
|
62
|
+
>
|
|
63
|
+
<template #row="{ path }">
|
|
64
|
+
<TextFormField :name="`${path}.name`" label="Name" />
|
|
65
|
+
</template>
|
|
66
|
+
</RepeatableGroupFormField>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Fixes [#516](https://github.com/softistx/nxgt-material/issues/516).
|
|
70
|
+
|
|
71
|
+
Migration: an app with a local field-array list marked temporary for this
|
|
72
|
+
request ([#516](https://github.com/softistx/nxgt-material/issues/516)) — a `useFieldArray` whose rows are mapped by hand, with its own
|
|
73
|
+
Add and per-row Remove buttons — can delete it and render
|
|
74
|
+
`RepeatableGroupFormField` instead: the array's `name`, `newRow` for what Add
|
|
75
|
+
appends, and the row's fields moved into `renderRow` / the `row` slot. Pass no
|
|
76
|
+
`shouldFocus` to `append`: the group moves the focus itself.
|
|
77
|
+
|
|
3
78
|
## 0.10.0
|
|
4
79
|
|
|
5
80
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -8,7 +8,8 @@ half (page geometry, scroll spy, text search), the currency field's decimals,
|
|
|
8
8
|
the file-size formatter, the slider's tick marks, the emoji list's groups,
|
|
9
9
|
the date range presets, a debounce, accent-insensitive text matching and a race-free async search (`/debounce`, `/text-match`, `/async-search`), the file preview's kind detection, the fields' blur
|
|
10
10
|
rule, a field's first reachable control (`/focus-control`) and its `aria-describedby`
|
|
11
|
-
(`/described-by`), the
|
|
11
|
+
(`/described-by`), where the focus goes in a repeatable row group
|
|
12
|
+
(`/repeatable-group`), the page behind a modal layer made inert
|
|
12
13
|
(`/inert-others`), Escape pressed while an overlay closes
|
|
13
14
|
(`/escape-forward`), the Toaster's guard against a tap's emulated hover
|
|
14
15
|
(`/touch-hover`), the focus a toast's action hands back (`/toast-focus`), a
|
|
@@ -88,6 +89,7 @@ built-in.
|
|
|
88
89
|
| `/currency-digits` | `currencyFractionDigits` — the decimals `CurrencyField` shows |
|
|
89
90
|
| `/field-blur-layers` | `layerOf`, `openerOf`, `FieldLayers` — whose overlay a layer of the page is, for a field's blur |
|
|
90
91
|
| `/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 |
|
|
92
|
+
| `/repeatable-group` | `focusAfterRemove`, `focusAfterMove`, `focusRowTarget`, `rowMoveTarget`, `canAddRow`, `canRemoveRow`, `RowFocusTarget`, `RowMoveDirection`, `RowAnnouncement`, `ROW_SLOT`, `ROW_FIELDS_SLOT`, `ADD_ATTRIBUTE` — where the focus goes after a row of a `RepeatableGroup` is added, removed or moved, and what its live region says |
|
|
91
93
|
| `/escape-forward` | `forwardEscape` — Escape pressed while an overlay closes, pressed again for the overlay below once it has gone |
|
|
92
94
|
| `/described-by` | `describedBy` — an `aria-describedby` value from the caller's ids and a field's help and error ids |
|
|
93
95
|
| `/inert-others` | `inertOthers`, `INERT_MARKER` — the rest of the page `inert` behind a modal layer, live regions kept, nested layers counted |
|
|
@@ -811,6 +813,44 @@ firstControl(roots)?.focus(); // the first date input, not a hidden one
|
|
|
811
813
|
field.ref(focusHandle(roots)); // React Hook Form: shouldFocusError reaches it
|
|
812
814
|
```
|
|
813
815
|
|
|
816
|
+
### Rows added, removed and moved: `/repeatable-group`
|
|
817
|
+
|
|
818
|
+
What both libraries' `RepeatableGroup` decides, as plain functions.
|
|
819
|
+
`focusAfterRemove(removed, remaining)` names where the focus goes once a row
|
|
820
|
+
is gone: the row that took its place, else the one before, else the Add
|
|
821
|
+
button. `focusAfterMove(to, direction, count)` keeps it on the move button
|
|
822
|
+
that was pressed, or on the row's other one when the row reached either end.
|
|
823
|
+
`focusRowTarget(root, target)` moves the focus there, reading the DOM when
|
|
824
|
+
called (rows are `[data-slot="repeatable-group-row"]`, the move buttons carry
|
|
825
|
+
`data-move`, the Add button `data-repeatable-group-add`). `canAddRow(count, max)`, `canRemoveRow(count, min)` and
|
|
826
|
+
`rowMoveTarget(index, direction, count)` are the limits (`RowMoveDirection`
|
|
827
|
+
is `'up' | 'down'`), and `RowAnnouncement` is what the live region says
|
|
828
|
+
(1-based positions). The DOM markers are exported as `ROW_SLOT`
|
|
829
|
+
(`'repeatable-group-row'`), `ROW_FIELDS_SLOT` (`'repeatable-group-fields'`)
|
|
830
|
+
and `ADD_ATTRIBUTE` (`'data-repeatable-group-add'`).
|
|
831
|
+
|
|
832
|
+
```ts
|
|
833
|
+
import {
|
|
834
|
+
canAddRow,
|
|
835
|
+
canRemoveRow,
|
|
836
|
+
focusAfterRemove,
|
|
837
|
+
focusRowTarget,
|
|
838
|
+
type RowAnnouncement,
|
|
839
|
+
rowMoveTarget,
|
|
840
|
+
} from '@nxgt/material-core/repeatable-group';
|
|
841
|
+
|
|
842
|
+
const addable = canAddRow(rows.length, 6); // false at 6 rows
|
|
843
|
+
const removable = canRemoveRow(rows.length, 1); // false at 1 row
|
|
844
|
+
const up = rowMoveTarget(index, 'up', rows.length); // null for the first row
|
|
845
|
+
|
|
846
|
+
remove(index);
|
|
847
|
+
await nextTick(); // or after React commits
|
|
848
|
+
focusRowTarget(groupElement, focusAfterRemove(index, rows.length));
|
|
849
|
+
|
|
850
|
+
const said: RowAnnouncement = { kind: 'removed', index: index + 1 };
|
|
851
|
+
status.textContent = said.kind === 'removed' ? `Removed period ${said.index}` : '';
|
|
852
|
+
```
|
|
853
|
+
|
|
814
854
|
### Escape while an overlay closes: `/escape-forward`
|
|
815
855
|
|
|
816
856
|
Radix's and reka's dismissable layers give Escape to the highest layer only,
|
|
@@ -0,0 +1,41 @@
|
|
|
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 firstControl2(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 focusHandle2(roots) {
|
|
14
|
+
return {
|
|
15
|
+
focus: () => {
|
|
16
|
+
firstControl2(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
|
+
firstControl2(list)?.focus();
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
select: () => {
|
|
26
|
+
const control = firstControl2(roots);
|
|
27
|
+
if (control && "select" in control) {
|
|
28
|
+
control.select();
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
setCustomValidity: () => {
|
|
32
|
+
return;
|
|
33
|
+
},
|
|
34
|
+
reportValidity: () => true
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { firstControl2, focusHandle2 };
|
|
39
|
+
|
|
40
|
+
//# debugId=084F2A6FA5DCD68964756E2164756E21
|
|
41
|
+
//# sourceMappingURL=index-407b6zs0.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,aAAY,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,YAAW,CAAC,OAAuC;AAAA,EAClE,OAAO;AAAA,IACN,OAAO,MAAM;AAAA,MACZ,cAAa,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,cAAa,IAAI,GAAG,MAAM;AAAA,QAC3B;AAAA,OACA;AAAA;AAAA,IAEF,QAAQ,MAAM;AAAA,MACb,MAAM,UAAU,cAAa,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": "084F2A6FA5DCD68964756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -1,43 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
firstControl2,
|
|
3
|
+
focusHandle2
|
|
4
|
+
} from "../chunks/index-407b6zs0.js";
|
|
37
5
|
export {
|
|
38
|
-
firstControl,
|
|
39
|
-
focusHandle
|
|
6
|
+
firstControl2 as firstControl,
|
|
7
|
+
focusHandle2 as focusHandle
|
|
40
8
|
};
|
|
41
9
|
|
|
42
|
-
//# debugId=
|
|
10
|
+
//# debugId=3CD7CAE6F251A45964756E2164756E21
|
|
43
11
|
//# sourceMappingURL=focus-control.js.map
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": [
|
|
3
|
+
"sources": [],
|
|
4
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
5
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
6
|
+
"mappings": "",
|
|
7
|
+
"debugId": "3CD7CAE6F251A45964756E2164756E21",
|
|
9
8
|
"names": []
|
|
10
9
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** Which way a row moves: towards the first row, or towards the last. */
|
|
2
|
+
export type RowMoveDirection = 'up' | 'down';
|
|
3
|
+
/**
|
|
4
|
+
* Where the focus goes once a RepeatableGroup's rows have changed: a row's
|
|
5
|
+
* first field, the Add button, or one of a row's move buttons. Indexes are
|
|
6
|
+
* 0-based, in the rows as they are after the change.
|
|
7
|
+
*/
|
|
8
|
+
export type RowFocusTarget = {
|
|
9
|
+
kind: 'field';
|
|
10
|
+
index: number;
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'add';
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'move';
|
|
15
|
+
index: number;
|
|
16
|
+
direction: RowMoveDirection;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* What the group's polite live region says about a change. `index`, `from`
|
|
20
|
+
* and `to` are 1-based: they are what a user reads ("period 2").
|
|
21
|
+
*/
|
|
22
|
+
export type RowAnnouncement = {
|
|
23
|
+
kind: 'added';
|
|
24
|
+
index: number;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'removed';
|
|
27
|
+
index: number;
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'moved';
|
|
30
|
+
from: number;
|
|
31
|
+
to: number;
|
|
32
|
+
};
|
|
33
|
+
/** Another row can be added: no `max`, or fewer rows than it. */
|
|
34
|
+
export declare const canAddRow: (count: number, max?: number) => boolean;
|
|
35
|
+
/** A row can be removed: no `min`, or more rows than it. */
|
|
36
|
+
export declare const canRemoveRow: (count: number, min?: number) => boolean;
|
|
37
|
+
/**
|
|
38
|
+
* The index the row at `index` moves to, or `null` when it cannot move that
|
|
39
|
+
* way (the first row up, the last one down).
|
|
40
|
+
*/
|
|
41
|
+
export declare function rowMoveTarget(index: number, direction: RowMoveDirection, count: number): number | null;
|
|
42
|
+
/**
|
|
43
|
+
* Where the focus goes once the row at `removed` is gone and `remaining`
|
|
44
|
+
* rows are left: the row that took its place, else the row before it (the
|
|
45
|
+
* last one was removed), else the Add button (no row is left).
|
|
46
|
+
*/
|
|
47
|
+
export declare function focusAfterRemove(removed: number, remaining: number): RowFocusTarget;
|
|
48
|
+
/**
|
|
49
|
+
* Where the focus goes once a row moved `direction` to `to`: the button that
|
|
50
|
+
* moved it, so the user can press it again — unless the row can no longer
|
|
51
|
+
* move that way (it reached the first or the last place, and that button is
|
|
52
|
+
* now disabled), then the other move button of the same row.
|
|
53
|
+
*/
|
|
54
|
+
export declare function focusAfterMove(to: number, direction: RowMoveDirection, count: number): RowFocusTarget;
|
|
55
|
+
/** The `data-slot` of each row and of its fields. */
|
|
56
|
+
export declare const ROW_SLOT = "repeatable-group-row";
|
|
57
|
+
export declare const ROW_FIELDS_SLOT = "repeatable-group-fields";
|
|
58
|
+
/**
|
|
59
|
+
* The attribute that marks the Add button: an attribute of its own, as the
|
|
60
|
+
* button keeps the `data-slot` of the library's Button.
|
|
61
|
+
*/
|
|
62
|
+
export declare const ADD_ATTRIBUTE = "data-repeatable-group-add";
|
|
63
|
+
/**
|
|
64
|
+
* Moves the focus to `target` inside the group's `root`: the first control a
|
|
65
|
+
* user can reach in the row's fields, the Add button (`data-repeatable-group-add`), or the row's move
|
|
66
|
+
* button (`data-move="up"` / `"down"`). Reads the DOM when called; returns
|
|
67
|
+
* whether something took the focus.
|
|
68
|
+
*/
|
|
69
|
+
export declare function focusRowTarget(root: Element, target: RowFocusTarget): boolean;
|
|
70
|
+
//# sourceMappingURL=repeatable-group.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repeatable-group.d.ts","sourceRoot":"","sources":["../../src/lib/repeatable-group.ts"],"names":[],"mappings":"AAEA,yEAAyE;AACzE,MAAM,MAAM,gBAAgB,GAAG,IAAI,GAAG,MAAM,CAAC;AAE7C;;;;GAIG;AACH,MAAM,MAAM,cAAc,GACvB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,GACf;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAEhE;;;GAGG;AACH,MAAM,MAAM,eAAe,GACxB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/C,iEAAiE;AACjE,eAAO,MAAM,SAAS,GAAI,OAAO,MAAM,EAAE,MAAM,MAAM,KAAG,OACvB,CAAC;AAElC,4DAA4D;AAC5D,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,EAAE,MAAM,MAAM,KAAG,OAC1B,CAAC;AAElC;;;GAGG;AACH,wBAAgB,aAAa,CAC5B,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,gBAAgB,EAC3B,KAAK,EAAE,MAAM,GACX,MAAM,GAAG,IAAI,CAGf;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC/B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GACf,cAAc,CAGhB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC7B,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,gBAAgB,EAC3B,KAAK,EAAE,MAAM,GACX,cAAc,CAIhB;AAED,qDAAqD;AACrD,eAAO,MAAM,QAAQ,yBAAyB,CAAC;AAC/C,eAAO,MAAM,eAAe,4BAA4B,CAAC;AACzD;;;GAGG;AACH,eAAO,MAAM,aAAa,8BAA8B,CAAC;AAEzD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAK7E"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
firstControl2
|
|
3
|
+
} from "../chunks/index-407b6zs0.js";
|
|
4
|
+
|
|
5
|
+
// src/lib/repeatable-group.ts
|
|
6
|
+
var canAddRow = (count, max) => max === undefined || count < max;
|
|
7
|
+
var canRemoveRow = (count, min) => min === undefined || count > min;
|
|
8
|
+
function rowMoveTarget(index, direction, count) {
|
|
9
|
+
const to = direction === "up" ? index - 1 : index + 1;
|
|
10
|
+
return to < 0 || to >= count ? null : to;
|
|
11
|
+
}
|
|
12
|
+
function focusAfterRemove(removed, remaining) {
|
|
13
|
+
if (remaining <= 0)
|
|
14
|
+
return { kind: "add" };
|
|
15
|
+
return { kind: "field", index: Math.min(removed, remaining - 1) };
|
|
16
|
+
}
|
|
17
|
+
function focusAfterMove(to, direction, count) {
|
|
18
|
+
const stays = rowMoveTarget(to, direction, count) !== null;
|
|
19
|
+
const other = direction === "up" ? "down" : "up";
|
|
20
|
+
return { kind: "move", index: to, direction: stays ? direction : other };
|
|
21
|
+
}
|
|
22
|
+
var ROW_SLOT = "repeatable-group-row";
|
|
23
|
+
var ROW_FIELDS_SLOT = "repeatable-group-fields";
|
|
24
|
+
var ADD_ATTRIBUTE = "data-repeatable-group-add";
|
|
25
|
+
function focusRowTarget(root, target) {
|
|
26
|
+
const el = targetElement(root, target);
|
|
27
|
+
if (!el)
|
|
28
|
+
return false;
|
|
29
|
+
el.focus();
|
|
30
|
+
return el.ownerDocument.activeElement === el;
|
|
31
|
+
}
|
|
32
|
+
function targetElement(root, target) {
|
|
33
|
+
if (target.kind === "add") {
|
|
34
|
+
return root.querySelector(`[${ADD_ATTRIBUTE}]`);
|
|
35
|
+
}
|
|
36
|
+
const rows = root.querySelectorAll(`[data-slot="${ROW_SLOT}"]`);
|
|
37
|
+
const row = rows[target.index];
|
|
38
|
+
if (!row)
|
|
39
|
+
return null;
|
|
40
|
+
if (target.kind === "move") {
|
|
41
|
+
return row.querySelector(`[data-move="${target.direction}"]`);
|
|
42
|
+
}
|
|
43
|
+
const fields = row.querySelector(`[data-slot="${ROW_FIELDS_SLOT}"]`);
|
|
44
|
+
return firstControl2([fields ?? row]);
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
ADD_ATTRIBUTE,
|
|
48
|
+
ROW_FIELDS_SLOT,
|
|
49
|
+
ROW_SLOT,
|
|
50
|
+
canAddRow,
|
|
51
|
+
canRemoveRow,
|
|
52
|
+
focusAfterMove,
|
|
53
|
+
focusAfterRemove,
|
|
54
|
+
focusRowTarget,
|
|
55
|
+
rowMoveTarget
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
//# debugId=EF31DAFB676F33B564756E2164756E21
|
|
59
|
+
//# sourceMappingURL=repeatable-group.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/repeatable-group.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { firstControl } from './focus-control';\n\n/** Which way a row moves: towards the first row, or towards the last. */\nexport type RowMoveDirection = 'up' | 'down';\n\n/**\n * Where the focus goes once a RepeatableGroup's rows have changed: a row's\n * first field, the Add button, or one of a row's move buttons. Indexes are\n * 0-based, in the rows as they are after the change.\n */\nexport type RowFocusTarget =\n\t| { kind: 'field'; index: number }\n\t| { kind: 'add' }\n\t| { kind: 'move'; index: number; direction: RowMoveDirection };\n\n/**\n * What the group's polite live region says about a change. `index`, `from`\n * and `to` are 1-based: they are what a user reads (\"period 2\").\n */\nexport type RowAnnouncement =\n\t| { kind: 'added'; index: number }\n\t| { kind: 'removed'; index: number }\n\t| { kind: 'moved'; from: number; to: number };\n\n/** Another row can be added: no `max`, or fewer rows than it. */\nexport const canAddRow = (count: number, max?: number): boolean =>\n\tmax === undefined || count < max;\n\n/** A row can be removed: no `min`, or more rows than it. */\nexport const canRemoveRow = (count: number, min?: number): boolean =>\n\tmin === undefined || count > min;\n\n/**\n * The index the row at `index` moves to, or `null` when it cannot move that\n * way (the first row up, the last one down).\n */\nexport function rowMoveTarget(\n\tindex: number,\n\tdirection: RowMoveDirection,\n\tcount: number,\n): number | null {\n\tconst to = direction === 'up' ? index - 1 : index + 1;\n\treturn to < 0 || to >= count ? null : to;\n}\n\n/**\n * Where the focus goes once the row at `removed` is gone and `remaining`\n * rows are left: the row that took its place, else the row before it (the\n * last one was removed), else the Add button (no row is left).\n */\nexport function focusAfterRemove(\n\tremoved: number,\n\tremaining: number,\n): RowFocusTarget {\n\tif (remaining <= 0) return { kind: 'add' };\n\treturn { kind: 'field', index: Math.min(removed, remaining - 1) };\n}\n\n/**\n * Where the focus goes once a row moved `direction` to `to`: the button that\n * moved it, so the user can press it again — unless the row can no longer\n * move that way (it reached the first or the last place, and that button is\n * now disabled), then the other move button of the same row.\n */\nexport function focusAfterMove(\n\tto: number,\n\tdirection: RowMoveDirection,\n\tcount: number,\n): RowFocusTarget {\n\tconst stays = rowMoveTarget(to, direction, count) !== null;\n\tconst other: RowMoveDirection = direction === 'up' ? 'down' : 'up';\n\treturn { kind: 'move', index: to, direction: stays ? direction : other };\n}\n\n/** The `data-slot` of each row and of its fields. */\nexport const ROW_SLOT = 'repeatable-group-row';\nexport const ROW_FIELDS_SLOT = 'repeatable-group-fields';\n/**\n * The attribute that marks the Add button: an attribute of its own, as the\n * button keeps the `data-slot` of the library's Button.\n */\nexport const ADD_ATTRIBUTE = 'data-repeatable-group-add';\n\n/**\n * Moves the focus to `target` inside the group's `root`: the first control a\n * user can reach in the row's fields, the Add button (`data-repeatable-group-add`), or the row's move\n * button (`data-move=\"up\"` / `\"down\"`). Reads the DOM when called; returns\n * whether something took the focus.\n */\nexport function focusRowTarget(root: Element, target: RowFocusTarget): boolean {\n\tconst el = targetElement(root, target);\n\tif (!el) return false;\n\tel.focus();\n\treturn el.ownerDocument.activeElement === el;\n}\n\nfunction targetElement(\n\troot: Element,\n\ttarget: RowFocusTarget,\n): HTMLElement | null {\n\tif (target.kind === 'add') {\n\t\treturn root.querySelector<HTMLElement>(`[${ADD_ATTRIBUTE}]`);\n\t}\n\tconst rows = root.querySelectorAll(`[data-slot=\"${ROW_SLOT}\"]`);\n\tconst row = rows[target.index];\n\tif (!row) return null;\n\tif (target.kind === 'move') {\n\t\treturn row.querySelector<HTMLElement>(`[data-move=\"${target.direction}\"]`);\n\t}\n\tconst fields = row.querySelector(`[data-slot=\"${ROW_FIELDS_SLOT}\"]`);\n\treturn firstControl([fields ?? row]);\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";;;;;AAyBO,IAAM,YAAY,CAAC,OAAe,QACxC,QAAQ,aAAa,QAAQ;AAGvB,IAAM,eAAe,CAAC,OAAe,QAC3C,QAAQ,aAAa,QAAQ;AAMvB,SAAS,aAAa,CAC5B,OACA,WACA,OACgB;AAAA,EAChB,MAAM,KAAK,cAAc,OAAO,QAAQ,IAAI,QAAQ;AAAA,EACpD,OAAO,KAAK,KAAK,MAAM,QAAQ,OAAO;AAAA;AAQhC,SAAS,gBAAgB,CAC/B,SACA,WACiB;AAAA,EACjB,IAAI,aAAa;AAAA,IAAG,OAAO,EAAE,MAAM,MAAM;AAAA,EACzC,OAAO,EAAE,MAAM,SAAS,OAAO,KAAK,IAAI,SAAS,YAAY,CAAC,EAAE;AAAA;AAS1D,SAAS,cAAc,CAC7B,IACA,WACA,OACiB;AAAA,EACjB,MAAM,QAAQ,cAAc,IAAI,WAAW,KAAK,MAAM;AAAA,EACtD,MAAM,QAA0B,cAAc,OAAO,SAAS;AAAA,EAC9D,OAAO,EAAE,MAAM,QAAQ,OAAO,IAAI,WAAW,QAAQ,YAAY,MAAM;AAAA;AAIjE,IAAM,WAAW;AACjB,IAAM,kBAAkB;AAKxB,IAAM,gBAAgB;AAQtB,SAAS,cAAc,CAAC,MAAe,QAAiC;AAAA,EAC9E,MAAM,KAAK,cAAc,MAAM,MAAM;AAAA,EACrC,IAAI,CAAC;AAAA,IAAI,OAAO;AAAA,EAChB,GAAG,MAAM;AAAA,EACT,OAAO,GAAG,cAAc,kBAAkB;AAAA;AAG3C,SAAS,aAAa,CACrB,MACA,QACqB;AAAA,EACrB,IAAI,OAAO,SAAS,OAAO;AAAA,IAC1B,OAAO,KAAK,cAA2B,IAAI,gBAAgB;AAAA,EAC5D;AAAA,EACA,MAAM,OAAO,KAAK,iBAAiB,eAAe,YAAY;AAAA,EAC9D,MAAM,MAAM,KAAK,OAAO;AAAA,EACxB,IAAI,CAAC;AAAA,IAAK,OAAO;AAAA,EACjB,IAAI,OAAO,SAAS,QAAQ;AAAA,IAC3B,OAAO,IAAI,cAA2B,eAAe,OAAO,aAAa;AAAA,EAC1E;AAAA,EACA,MAAM,SAAS,IAAI,cAAc,eAAe,mBAAmB;AAAA,EACnE,OAAO,cAAa,CAAC,UAAU,GAAG,CAAC;AAAA;",
|
|
8
|
+
"debugId": "EF31DAFB676F33B564756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
export declare const TOASTER_PHONE_QUERY = "(max-width: 600px)";
|
|
6
6
|
/** Where toasts go on a phone when the caller names no `position`. */
|
|
7
7
|
export declare const TOASTER_PHONE_POSITION = "top-center";
|
|
8
|
+
/**
|
|
9
|
+
* How far below the top of a phone the toasts start when the caller names no
|
|
10
|
+
* `position`: past the app shell's sticky bar (64px high, 8px from the top),
|
|
11
|
+
* so the menu button, the title and the account control stay reachable while
|
|
12
|
+
* a toast shows. The caller's own `mobileOffset` replaces it.
|
|
13
|
+
*/
|
|
14
|
+
export declare const TOASTER_PHONE_OFFSET_TOP = 80;
|
|
8
15
|
type Listen = Pick<Document, 'addEventListener' | 'removeEventListener'>;
|
|
9
16
|
/**
|
|
10
17
|
* Keeps a tap from hovering whatever matches `selector`: while the last
|
|
@@ -1 +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;
|
|
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;AAEnD;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAY3C,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"}
|
package/dist/lib/touch-hover.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/lib/touch-hover.ts
|
|
2
2
|
var TOASTER_PHONE_QUERY = "(max-width: 600px)";
|
|
3
3
|
var TOASTER_PHONE_POSITION = "top-center";
|
|
4
|
+
var TOASTER_PHONE_OFFSET_TOP = 80;
|
|
4
5
|
var HOVER_EVENTS = ["mouseover", "mouseenter", "mousemove"];
|
|
5
6
|
var POINTER_EVENTS = ["pointerdown", "pointermove", "pointerup"];
|
|
6
7
|
function ignoreTouchHover(doc, selector) {
|
|
@@ -30,10 +31,11 @@ function ignoreTouchHover(doc, selector) {
|
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
export {
|
|
34
|
+
TOASTER_PHONE_OFFSET_TOP,
|
|
33
35
|
TOASTER_PHONE_POSITION,
|
|
34
36
|
TOASTER_PHONE_QUERY,
|
|
35
37
|
ignoreTouchHover
|
|
36
38
|
};
|
|
37
39
|
|
|
38
|
-
//# debugId=
|
|
40
|
+
//# debugId=5B9CEF7EA94C41D764756E2164756E21
|
|
39
41
|
//# sourceMappingURL=touch-hover.js.map
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/lib/touch-hover.ts"],
|
|
4
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"
|
|
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 * How far below the top of a phone the toasts start when the caller names no\n * `position`: past the app shell's sticky bar (64px high, 8px from the top),\n * so the menu button, the title and the account control stay reachable while\n * a toast shows. The caller's own `mobileOffset` replaces it.\n */\nexport const TOASTER_PHONE_OFFSET_TOP = 80;\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
6
|
],
|
|
7
|
-
"mappings": ";AAIO,IAAM,sBAAsB;AAG5B,IAAM,yBAAyB;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": ";AAIO,IAAM,sBAAsB;AAG5B,IAAM,yBAAyB;AAQ/B,IAAM,2BAA2B;AAOxC,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": "5B9CEF7EA94C41D764756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/docs/roadmap.md
CHANGED
|
@@ -76,6 +76,10 @@ Nothing is in progress.
|
|
|
76
76
|
|
|
77
77
|
## Shipped
|
|
78
78
|
|
|
79
|
+
- **A repeatable row group's focus rules** (`/repeatable-group`) — where the
|
|
80
|
+
focus goes after a row is added, removed or moved, the add and remove
|
|
81
|
+
limits, and what the live region announces, behind both libraries'
|
|
82
|
+
`RepeatableGroup`. 0.11.0.
|
|
79
83
|
- **Two-step verification helpers and printing one element** (`/two-factor`,
|
|
80
84
|
`/print-only`) — a TOTP secret grouped to be typed by hand, the recovery
|
|
81
85
|
codes as a text file, saving a text file, and printing one element of the
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxgt/material-core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "The framework-free core of @nxgt/material and @nxgt/material-vue: the PDF document model and its react-pdf renderer, print formatters, PDF file helpers, the PDF viewer's pure modules, the currency digits, the file-size formatter, the field-blur layers, a field's first focusable control, the aria-describedby joiner, the inert background of a modal layer, Escape forwarded while an overlay closes, the Toaster's touch-hover guard and its focus hand-back, the page's Content Security Policy nonce, an object built on first use, the zod issue messages, the date range presets, the debounce, accent-insensitive text matching and the race-free async search behind Autocomplete and SearchField, the submit guard, the code error map, the two-factor helpers (a grouped TOTP secret, the recovery codes' file, a text download), printing one element of the page, the file preview's kind detection and text reader, the pdfkit fonts Vite plugin, a CSP-safe Vite assetsInlineLimit and the Node-only pdfkit fonts resolution for Next.js and Nuxt builds",
|
|
3
|
+
"version": "0.11.1",
|
|
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, a repeatable row group's focus and announcement rules, the aria-describedby joiner, the inert background of a modal layer, Escape forwarded while an overlay closes, the Toaster's touch-hover guard and its focus hand-back, the page's Content Security Policy nonce, an object built on first use, the zod issue messages, the date range presets, the debounce, accent-insensitive text matching and the race-free async search behind Autocomplete and SearchField, the submit guard, the code error map, the two-factor helpers (a grouped TOTP secret, the recovery codes' file, a text download), printing one element of the page, the file preview's kind detection and text reader, the pdfkit fonts Vite plugin, a CSP-safe Vite assetsInlineLimit 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"
|
|
@@ -103,6 +103,11 @@
|
|
|
103
103
|
"import": "./dist/lib/focus-control.js",
|
|
104
104
|
"default": "./dist/lib/focus-control.js"
|
|
105
105
|
},
|
|
106
|
+
"./repeatable-group": {
|
|
107
|
+
"types": "./dist/lib/repeatable-group.d.ts",
|
|
108
|
+
"import": "./dist/lib/repeatable-group.js",
|
|
109
|
+
"default": "./dist/lib/repeatable-group.js"
|
|
110
|
+
},
|
|
106
111
|
"./escape-forward": {
|
|
107
112
|
"types": "./dist/lib/escape-forward.d.ts",
|
|
108
113
|
"import": "./dist/lib/escape-forward.js",
|
|
@@ -268,6 +273,7 @@
|
|
|
268
273
|
"src/lib/currency-digits.ts",
|
|
269
274
|
"src/lib/field-blur-layers.ts",
|
|
270
275
|
"src/lib/focus-control.ts",
|
|
276
|
+
"src/lib/repeatable-group.ts",
|
|
271
277
|
"src/lib/escape-forward.ts",
|
|
272
278
|
"src/lib/described-by.ts",
|
|
273
279
|
"src/lib/touch-hover.ts",
|