@nxgt/material-vue 0.26.0 → 0.27.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 +181 -0
- package/README.md +8 -5
- package/dist/Button-DEUkxkrJ.js +728 -0
- package/dist/{DropdownMenuTrigger-BHAhXGUO.js → DropdownMenuTrigger-BsuyNVzV.js} +48 -41
- package/dist/HoverCardTrigger-TDll55LP.js +510 -0
- package/dist/{camera-DvKuqT4L.js → camera-UJXnnFeb.js} +4 -4
- package/dist/components/base-content.css +139 -0
- package/dist/components/base.css +150 -0
- package/dist/components/components.css +2 -0
- package/dist/components/reduced-motion.css +56 -0
- package/dist/components/theme-tokens.css +116 -0
- package/dist/components/ui/actions/button-group/button-group.css +1 -1
- package/dist/components/ui/feedback/toast/toast.css +21 -0
- package/dist/components/ui/primitives/label/label.css +33 -0
- package/dist/{components-CCWod7cJ.js → components-BZ1XAK37.js} +19198 -18568
- package/dist/components.js +7 -7
- package/dist/composables.js +3 -3
- package/dist/dnd.js +1 -1
- package/dist/field-name-attribute-Bl7LSVK0.js +4 -0
- package/dist/{form-scope-x-7TYn2s.js → form-scope-BFGca0h6.js} +1 -1
- package/dist/{i18n-provider-CaaZCmaR.js → i18n-provider-DBusa725.js} +1 -1
- package/dist/i18n.js +4 -4
- package/dist/icon-button-jHB4MhQ5.js +262 -0
- package/dist/lib/components/ui/actions/button/Button.vue.d.ts +22 -4
- package/dist/lib/components/ui/actions/button/button-tones.d.ts +36 -0
- package/dist/lib/components/ui/actions/button/button.variants.d.ts +1 -1
- package/dist/lib/components/ui/actions/button/button.wrap.d.ts +8 -0
- package/dist/lib/components/ui/actions/button/label-text.d.ts +9 -0
- package/dist/lib/components/ui/actions/button/prepend-to-child.d.ts +18 -0
- package/dist/lib/components/ui/actions/button/use-click-lock.d.ts +14 -0
- package/dist/lib/components/ui/actions/copy-button/copy-button.vue.d.ts +6 -2
- package/dist/lib/components/ui/actions/icon-button/icon-button.compound-variants.d.ts +28 -0
- package/dist/lib/components/ui/actions/icon-button/icon-button.vue.d.ts +1 -1
- package/dist/lib/components/ui/actions/responsive-button/responsive-button.vue.d.ts +1 -3
- package/dist/lib/components/ui/collections/data-table/partials/data-table-empty.vue.d.ts +5 -1
- package/dist/lib/components/ui/collections/data-table/partials/data-table-scroll-region.vue.d.ts +28 -0
- package/dist/lib/components/ui/collections/data-table/partials/data-table-toolbar.d.ts +60 -0
- package/dist/lib/components/ui/collections/data-table/types.d.ts +9 -0
- package/dist/lib/components/ui/collections/data-table/use-scroll-edges.d.ts +22 -0
- package/dist/lib/components/ui/collections/pagination/PaginationFirst.vue.d.ts +1 -1
- package/dist/lib/components/ui/collections/pagination/PaginationItem.vue.d.ts +1 -1
- package/dist/lib/components/ui/collections/pagination/PaginationLast.vue.d.ts +1 -1
- package/dist/lib/components/ui/collections/pagination/PaginationNext.vue.d.ts +1 -1
- package/dist/lib/components/ui/collections/pagination/PaginationPrevious.vue.d.ts +1 -1
- package/dist/lib/components/ui/dates/calendar/Calendar.vue.d.ts +4 -4
- package/dist/lib/components/ui/dates/calendar/calendar-day-label.d.ts +17 -0
- package/dist/lib/components/ui/dates/calendar/calendar-years.d.ts +11 -0
- package/dist/lib/components/ui/dates/calendar/partials/calendar-month-select.vue.d.ts +18 -0
- package/dist/lib/components/ui/dates/calendar/partials/calendar-year-select.vue.d.ts +19 -0
- package/dist/lib/components/ui/dates/calendar/use-calendar-locale.d.ts +7 -0
- package/dist/lib/components/ui/dates/calendar/use-calendar-selected-hint.d.ts +17 -0
- package/dist/lib/components/ui/dates/date-range-field/partials/date-range-day.vue.d.ts +8 -0
- package/dist/lib/components/ui/display/cards/cards.gallery.d.ts +3 -3
- package/dist/lib/components/ui/display/list-tile/list-tile.vue.d.ts +10 -6
- package/dist/lib/components/ui/feedback/alert-dialog/AlertDialogContent.vue.d.ts +2 -2
- package/dist/lib/components/ui/feedback/sonner/index.d.ts +1 -0
- package/dist/lib/components/ui/feedback/toast/toast.d.ts +7 -0
- package/dist/lib/components/ui/feedback/toast/toast.vue.d.ts +18 -2
- package/dist/lib/components/ui/fields/contacts/consts.d.ts +1 -1
- package/dist/lib/components/ui/forms/use-field-blur.d.ts +3 -0
- package/dist/lib/components/ui/layout/app-shell/activity-content.vue.d.ts +9 -3
- package/dist/lib/components/ui/layout/app-shell/activity-layout.vue.d.ts +21 -4
- package/dist/lib/components/ui/layout/app-shell/app-header.vue.d.ts +7 -5
- package/dist/lib/components/ui/layout/app-shell/app-sidebar.vue.d.ts +4 -1
- package/dist/lib/components/ui/layout/app-shell/index.d.ts +2 -2
- package/dist/lib/components/ui/layout/app-shell/router-menu-link.d.ts +7 -0
- package/dist/lib/components/ui/layout/app-shell/types.d.ts +8 -0
- package/dist/lib/components/ui/layout/app-shell/use-content-scroll.d.ts +13 -0
- package/dist/lib/components/ui/layout/app-shell/use-navigation-sheet.d.ts +17 -0
- package/dist/lib/components/ui/layout/sidebar/sidebar-context.d.ts +15 -0
- package/dist/lib/components/ui/layout/sidebar/sidebar-menu-item.vue.d.ts +13 -4
- package/dist/lib/components/ui/media/carousel/carousel-next.vue.d.ts +1 -1
- package/dist/lib/components/ui/media/carousel/carousel-previous.vue.d.ts +1 -1
- package/dist/lib/components/ui/menu-item-variant.d.ts +4 -4
- package/dist/lib/components/ui/overlays/context-menu/ContextMenuContent.vue.d.ts +2 -2
- package/dist/lib/components/ui/overlays/dialog/DialogContent.vue.d.ts +2 -2
- package/dist/lib/components/ui/overlays/dialog/DialogScrollContent.vue.d.ts +2 -2
- package/dist/lib/components/ui/overlays/dialog/use-escape-forward.d.ts +17 -0
- package/dist/lib/components/ui/overlays/dialog/use-inert-others.d.ts +17 -0
- package/dist/lib/components/ui/overlays/dialog/use-modal-content.d.ts +32 -0
- package/dist/lib/components/ui/overlays/drawer/DrawerContent.vue.d.ts +2 -2
- package/dist/lib/components/ui/overlays/dropdown-menu/DropdownMenu.vue.d.ts +3 -1
- package/dist/lib/components/ui/overlays/dropdown-menu/DropdownMenuContent.vue.d.ts +2 -2
- package/dist/lib/components/ui/overlays/popover/PopoverContent.vue.d.ts +2 -2
- package/dist/lib/components/ui/overlays/sheet/SheetContent.vue.d.ts +2 -2
- package/dist/lib/components/ui/primitives/card/CardTitle.vue.d.ts +11 -4
- package/dist/lib/components/ui/primitives/card/index.d.ts +1 -0
- package/dist/lib/components/ui/primitives/toggle/Toggle.vue.d.ts +1 -1
- package/dist/lib/components/ui/primitives/toggle/index.d.ts +1 -1
- package/dist/lib/components/ui/selection/autocomplete/partials/autocomplete-box.vue.d.ts +2 -0
- package/dist/lib/components/ui/selection/select/SelectContent.vue.d.ts +2 -2
- package/dist/lib/components/ui/selection/select-chip-field/select-chip-field.vue.d.ts +1 -0
- package/dist/lib/i18n/resources/en/en.d.ts +9 -0
- package/dist/lib/i18n/resources/en/material.json.d.ts +13 -4
- package/dist/lib/i18n/resources/fr/fr.d.ts +9 -0
- package/dist/lib/i18n/resources/fr/material.json.d.ts +13 -4
- package/dist/lib/i18n/resources/resources.d.ts +18 -0
- package/dist/lib/i18n/translator.d.ts +1 -1
- package/dist/lib/lib/field-name-attribute.d.ts +7 -0
- package/dist/lib/lib/focus-field.d.ts +17 -0
- package/dist/lib/lib/index.d.ts +1 -0
- package/dist/lib.js +76 -74
- package/dist/main.js +13 -13
- package/dist/map.js +4 -4
- package/dist/material-vue.css +1 -1
- package/dist/{pdf-outline-sidebar-D3vq2qfS.js → pdf-outline-sidebar-ChjAXoUX.js} +2 -2
- package/dist/{pdf-page-BrJbyjQj.js → pdf-page-D4OTPcpF.js} +1 -1
- package/dist/{pdf-page-list-BCgYEszV.js → pdf-page-list-CFgKYLur.js} +2 -2
- package/dist/{pdf-reader-BAzevSJU.js → pdf-reader-B1hw81cG.js} +9 -9
- package/dist/{pdf-sidebar-DSh2Mend.js → pdf-sidebar-Do5HzrDq.js} +3 -3
- package/dist/{pdf-thumbnail-sidebar-D_EMcbKF.js → pdf-thumbnail-sidebar-CMge0C9-.js} +3 -3
- package/dist/{pdf-toolbar-BcL7HlfS.js → pdf-toolbar-BBio9baU.js} +5 -5
- package/dist/pdf.js +2 -2
- package/dist/{sortable-provider-DMpmM7Ke.js → sortable-provider-DMVbbmUU.js} +2 -2
- package/dist/{stored-language-Bxhsx4iB.js → stored-language-AJpwOMsA.js} +26 -8
- package/dist/{stx-player-MldK_ZAZ.js → stx-player-gtyStl7e.js} +2 -2
- package/dist/styles.css +10 -368
- package/dist/{translator-CLI_7Crg.js → translator-D2_swsMz.js} +1 -1
- package/dist/{use-heading-spy-VPuFj2Gt.js → use-heading-spy-BbEbAfPK.js} +19 -19
- package/dist/use-inert-others-CtGqZKR8.js +22 -0
- package/dist/{use-material-translation-CONqD9wJ.js → use-material-translation-BHtgyMz3.js} +3 -3
- package/docs/components/buttons-and-actions.md +71 -16
- package/docs/components/data-display.md +12 -1
- package/docs/components/data-table-filter-and-lists.md +85 -5
- package/docs/components/date-and-time.md +25 -1
- package/docs/components/feedback-and-status.md +86 -11
- package/docs/components/form-fields.md +13 -4
- package/docs/components/media-and-files.md +7 -0
- package/docs/components/navigation-and-layout.md +19 -1
- package/docs/components/overlays-and-menus.md +20 -4
- package/docs/components/primitives.md +16 -2
- package/docs/components/selection-and-choice.md +12 -1
- package/docs/getting-started.md +2 -2
- package/docs/guides/app-shell.md +102 -6
- package/docs/guides/forms.md +58 -5
- package/docs/guides/helpers.md +2 -1
- package/docs/guides/i18n.md +5 -2
- package/docs/guides/ssr.md +1 -1
- package/docs/guides/theming.md +134 -6
- package/docs/previews/data-display-description.webp +0 -0
- package/docs/previews/feedback-status-toast.webp +0 -0
- package/docs/roadmap.md +21 -0
- package/docs/troubleshooting.md +936 -12
- package/lib/components/base-content.css +139 -0
- package/lib/components/base.css +150 -0
- package/lib/components/components.css +2 -0
- package/lib/components/reduced-motion.css +56 -0
- package/lib/components/theme-tokens.css +116 -0
- package/lib/components/ui/actions/button-group/button-group.css +1 -1
- package/lib/components/ui/feedback/toast/toast.css +21 -0
- package/lib/components/ui/primitives/label/label.css +33 -0
- package/lib/styles.css +10 -368
- package/package.json +2 -2
- package/dist/Button-16J7vTpE.js +0 -665
- package/dist/HoverCardTrigger-C41002JS.js +0 -455
- package/dist/icon-button-CDdNsZuZ.js +0 -227
|
@@ -1,34 +1,28 @@
|
|
|
1
|
-
import { a as e } from "./i18n-provider-
|
|
1
|
+
import { a as e } from "./i18n-provider-DBusa725.js";
|
|
2
2
|
import { c as t, d as n, f as r, l as i, n as a, u as o } from "./number.utils-Cy1z6qI7.js";
|
|
3
3
|
import { computed as s, getCurrentInstance as c, onMounted as l, ref as u, toValue as d } from "vue";
|
|
4
4
|
import { useIntersectionObserver as f, useMediaQuery as p, useSSRWidth as m, useStorage as h } from "@vueuse/core";
|
|
5
|
-
//#region lib/composables/use-ssr-media-query.ts
|
|
6
|
-
var g = 1024;
|
|
7
|
-
function _(e) {
|
|
8
|
-
return c() ? p(e, { ssrWidth: m() ?? g }) : p(e);
|
|
9
|
-
}
|
|
10
|
-
//#endregion
|
|
11
|
-
//#region lib/composables/use-mobile.ts
|
|
12
|
-
var v = 768;
|
|
13
|
-
function y() {
|
|
14
|
-
return _(`(max-width: ${v - 1}px)`);
|
|
15
|
-
}
|
|
16
|
-
//#endregion
|
|
17
5
|
//#region lib/composables/resolve-theme.ts
|
|
18
|
-
function
|
|
6
|
+
function g(e, t) {
|
|
19
7
|
let n = e === "light" || e === "dark" ? e : "system";
|
|
20
8
|
return {
|
|
21
9
|
theme: n === "system" ? t ? "dark" : "light" : n,
|
|
22
10
|
preference: n
|
|
23
11
|
};
|
|
24
12
|
}
|
|
25
|
-
function
|
|
13
|
+
function _(e) {
|
|
26
14
|
typeof document > "u" || (document.documentElement.classList.toggle("dark", e === "dark"), document.documentElement.style.colorScheme = e);
|
|
27
15
|
}
|
|
28
16
|
//#endregion
|
|
17
|
+
//#region lib/composables/use-ssr-media-query.ts
|
|
18
|
+
var v = 1024;
|
|
19
|
+
function y(e) {
|
|
20
|
+
return c() ? p(e, { ssrWidth: m() ?? v }) : p(e);
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
29
23
|
//#region lib/composables/use-theme.ts
|
|
30
|
-
function
|
|
31
|
-
let e =
|
|
24
|
+
function b() {
|
|
25
|
+
let e = y("(prefers-color-scheme: dark)"), t = h("theme", null, void 0, { initOnMounted: !0 }), n = s(() => g(t.value, !!e.value));
|
|
32
26
|
return {
|
|
33
27
|
get theme() {
|
|
34
28
|
return n.value.theme;
|
|
@@ -37,11 +31,17 @@ function S() {
|
|
|
37
31
|
return n.value.preference;
|
|
38
32
|
},
|
|
39
33
|
updateTheme: (n) => {
|
|
40
|
-
n === "system" ? t.value = null : t.value = n,
|
|
34
|
+
n === "system" ? t.value = null : t.value = n, _(g(n === "system" ? void 0 : n, !!e.value).theme);
|
|
41
35
|
}
|
|
42
36
|
};
|
|
43
37
|
}
|
|
44
38
|
//#endregion
|
|
39
|
+
//#region lib/composables/use-mobile.ts
|
|
40
|
+
var x = 768;
|
|
41
|
+
function S() {
|
|
42
|
+
return y(`(max-width: ${x - 1}px)`);
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
45
|
//#region ../../node_modules/date-fns/locale/fr/_lib/formatDistance.js
|
|
46
46
|
var C = {
|
|
47
47
|
lessThanXSeconds: {
|
|
@@ -477,4 +477,4 @@ function z(e, t, n) {
|
|
|
477
477
|
});
|
|
478
478
|
}
|
|
479
479
|
//#endregion
|
|
480
|
-
export {
|
|
480
|
+
export { y as a, b as i, I as n, _ as o, S as r, g as s, z as t };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { nextTick as e, useTemplateRef as t, watch as n } from "vue";
|
|
2
|
+
import { unrefElement as r } from "@vueuse/core";
|
|
3
|
+
import { inertOthers as i } from "@nxgt/material-core/inert-others";
|
|
4
|
+
//#region lib/components/ui/overlays/dialog/use-inert-others.ts
|
|
5
|
+
function a(a, o) {
|
|
6
|
+
let s = typeof a == "string" ? t(a) : a;
|
|
7
|
+
n(o, (t, n, a) => {
|
|
8
|
+
if (!t || typeof document > "u") return;
|
|
9
|
+
let o, c = !1;
|
|
10
|
+
e(() => {
|
|
11
|
+
let e = r(s);
|
|
12
|
+
!c && e instanceof Element && (o = i(e));
|
|
13
|
+
}), a(() => {
|
|
14
|
+
c = !0, o?.();
|
|
15
|
+
});
|
|
16
|
+
}, {
|
|
17
|
+
immediate: !0,
|
|
18
|
+
flush: "post"
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { a as t };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import "./stored-language-
|
|
2
|
-
import { a as e } from "./i18n-provider-
|
|
3
|
-
import { i as t, r as n } from "./translator-
|
|
1
|
+
import "./stored-language-AJpwOMsA.js";
|
|
2
|
+
import { a as e } from "./i18n-provider-DBusa725.js";
|
|
3
|
+
import { i as t, r as n } from "./translator-D2_swsMz.js";
|
|
4
4
|
//#region lib/i18n/translate-material.ts
|
|
5
5
|
function r(e, r, i, a) {
|
|
6
6
|
if (e) for (let t of /* @__PURE__ */ new Set([e, e.split("-")[0]])) {
|
|
@@ -54,17 +54,71 @@ function save() {
|
|
|
54
54
|
| `variant` | `'filled' \| 'tonal' \| 'outlined' \| 'ghost' \| 'link'` | `'filled'` | the style |
|
|
55
55
|
| `color` | `'primary' \| 'secondary' \| 'success' \| 'info' \| 'warning' \| 'error' \| 'default'` | `'primary'` | the colour; `default` is the foreground colour |
|
|
56
56
|
| `size` | `'default' \| 'xs' \| 'sm' \| 'lg' \| 'icon' \| 'icon-xs' \| 'icon-sm' \| 'icon-lg'` | — (36 px) | height; the `icon*` sizes are square |
|
|
57
|
-
| `loading` | `boolean` | — |
|
|
57
|
+
| `loading` | `boolean` | — | busy: a spinner, `aria-busy="true"` and `aria-disabled="true"`, and every click cancelled before your `@click` — a submit button submits neither on a click nor on Enter in a field. It stays focusable (no native `disabled`), so focus is not lost |
|
|
58
|
+
| `lock-on-click` | `boolean` | — | a click makes the button busy at once, until `loading` has turned true and back (or for one second when it never does): for a `loading` that turns true only after an `await` |
|
|
58
59
|
| `spinner-variant` | `SpinnerProps['variant']` | `'2'` | which spinner `loading` shows |
|
|
60
|
+
| `wrap` | `boolean` | — | the label breaks onto as many lines as it needs, centred, instead of being cut with “…”: the button grows taller, never shorter than its `size`. Ignored by the `icon*` sizes |
|
|
59
61
|
| `tooltip` | `string` | — | wraps the button in a `Tooltip` |
|
|
60
|
-
| `as-child` / `as` | `boolean` / tag or component | `false` / `'button'` | styles its only child, or renders another element |
|
|
62
|
+
| `as-child` / `as` | `boolean` / tag or component | `false` / `'button'` | styles its only child, or renders another element. With `as-child` the child keeps its own attributes (`class`, `style` and listeners merge) and its slot content; while `loading`, the spinner goes inside the child, `aria-busy` and `aria-disabled` land on it, and the child stays the same element, keeping its focus, ref and directives. A component child with no slot content shows only the spinner while busy |
|
|
61
63
|
| `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | not `submit` by default — pass it inside a form |
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
loading). The classes are exported as `buttonVariants` for styling your own
|
|
65
|
-
element.
|
|
65
|
+
The classes are exported as `buttonVariants` for styling your own element.
|
|
66
66
|
|
|
67
|
-
**
|
|
67
|
+
**On a phone.** A button is never wider than its container, and keeps its
|
|
68
|
+
content's width in a column: a label too
|
|
69
|
+
long for it (a card on a phone) is cut with “…” on one line, and its
|
|
70
|
+
accessible name — and a `title` you give it — keep the whole text. Give a
|
|
71
|
+
label that must be read in full `wrap`, or a width of your own (`w-full`).
|
|
72
|
+
The text is put in a `<span data-slot="button-label">` (one per run of
|
|
73
|
+
text) that shrinks and ends in the ellipsis; it needs the text to be the button's own slot content; with
|
|
74
|
+
`as-child`, put the label in a `<span>` inside the child element.
|
|
75
|
+
|
|
76
|
+
```vue
|
|
77
|
+
<template>
|
|
78
|
+
<Button title="Continue to the payment details">Continue to the payment details</Button>
|
|
79
|
+
<Button wrap>Continue to the payment details and confirm the order</Button>
|
|
80
|
+
</template>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Submitting once.** Give a submit button `:loading="isSubmitting"` from
|
|
84
|
+
`useForm`: vee-validate sets it as `handleSubmit` starts, so the button is
|
|
85
|
+
busy from the first click and a double click, or Enter pressed twice,
|
|
86
|
+
submits once. When `loading` comes from something that turns true later — a
|
|
87
|
+
mutation's pending flag after an `await` — the second click comes first: add
|
|
88
|
+
`lock-on-click`.
|
|
89
|
+
|
|
90
|
+
```vue
|
|
91
|
+
<script setup lang="ts">
|
|
92
|
+
import { useForm } from 'vee-validate';
|
|
93
|
+
import { Button, EmailFormField } from '@nxgt/material-vue/components';
|
|
94
|
+
|
|
95
|
+
const { handleSubmit, isSubmitting } = useForm();
|
|
96
|
+
const onSubmit = handleSubmit(async (values) => {
|
|
97
|
+
// …
|
|
98
|
+
});
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<template>
|
|
102
|
+
<form @submit="onSubmit">
|
|
103
|
+
<EmailFormField name="email" label="Email" />
|
|
104
|
+
<Button type="submit" :loading="isSubmitting">Sign in</Button>
|
|
105
|
+
</form>
|
|
106
|
+
</template>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**What the guard covers.** Clicks on this button, Space and Enter on it,
|
|
110
|
+
and Enter in a field when it is the form's first submit button (the default
|
|
111
|
+
one, which the browser clicks). `form.requestSubmit()` and `form.submit()`
|
|
112
|
+
fire no click, so a submit your code starts — an OTP step that submits on its
|
|
113
|
+
last digit, a keyboard shortcut — must check `isSubmitting` itself.
|
|
114
|
+
With `lock-on-click`, a `loading` that never turns true releases the lock after
|
|
115
|
+
one second.
|
|
116
|
+
|
|
117
|
+
A test finds a busy button by its state, not by `disabled`:
|
|
118
|
+
`expect(button).toHaveAttribute('aria-busy', 'true')` (`toBeDisabled()` is
|
|
119
|
+
false: it is still focusable).
|
|
120
|
+
|
|
121
|
+
**Storybook:** `Buttons & actions/Button` (`Submits Once`, `Lock On Click`), `Buttons & actions/Button/Phone` · **React:** `Button`
|
|
68
122
|
|
|
69
123
|
---
|
|
70
124
|
|
|
@@ -204,7 +258,7 @@ function save(options?: { draft: boolean }) {
|
|
|
204
258
|
| --- | --- | --- | --- |
|
|
205
259
|
| `#menu` slot | — | — | the `DropdownMenu*` items shown by the chevron |
|
|
206
260
|
| `menu-label` | `string` | `"More actions"` (translated) | the chevron's `aria-label` |
|
|
207
|
-
| `variant`, `color`, `loading`, `disabled` | as `Button` | as `Button` |
|
|
261
|
+
| `variant`, `color`, `loading`, `disabled` | as `Button` | as `Button` | `variant`, `color` and `disabled` apply to both halves; `loading` makes the main half busy (spinner, clicks ignored, still focusable) and disables the chevron (native `disabled`) |
|
|
208
262
|
|
|
209
263
|
Other `Button` props and `@click` go to the main half.
|
|
210
264
|
|
|
@@ -216,7 +270,7 @@ Other `Button` props and `@click` go to the main half.
|
|
|
216
270
|
|
|
217
271
|

|
|
218
272
|
|
|
219
|
-
Shows its label
|
|
273
|
+
Shows its label from 768 px up and collapses to icon-only below. The collapse is a CSS media query, not a JS effect, so the server HTML is the same everywhere and nothing flashes.
|
|
220
274
|
|
|
221
275
|
```vue
|
|
222
276
|
<script setup lang="ts">
|
|
@@ -235,10 +289,10 @@ import { Plus } from '@lucide/vue';
|
|
|
235
289
|
| Prop | Type | Default | Effect |
|
|
236
290
|
| --- | --- | --- | --- |
|
|
237
291
|
| `#icon` slot | — | — | always shown |
|
|
238
|
-
| `expanded` | `boolean` |
|
|
292
|
+
| `expanded` | `boolean` | by width (`max-md` CSS) | forces the label on or off at every width |
|
|
239
293
|
|
|
240
|
-
Every `Button` prop applies.
|
|
241
|
-
|
|
294
|
+
Every `Button` prop applies. The label is always in the DOM, so the accessible
|
|
295
|
+
name is the same at every width; collapsed, it is screen-reader text; an `aria-label` would
|
|
242
296
|
replace that name, so pass one only when it should differ from the label.
|
|
243
297
|
`expanded` is layout, not disclosure: the button has no `aria-expanded` of its
|
|
244
298
|
own, so it is not announced as "collapsed". Used as a popover, drawer or menu
|
|
@@ -297,7 +351,7 @@ Needs `vue-router` installed on the app (`app.use(router)`).
|
|
|
297
351
|
|
|
298
352
|

|
|
299
353
|
|
|
300
|
-
Copies a value to the clipboard and briefly shows a check mark.
|
|
354
|
+
Copies a value to the clipboard and briefly shows a check mark (or a cross when the clipboard refuses).
|
|
301
355
|
|
|
302
356
|
```vue
|
|
303
357
|
<script setup lang="ts">
|
|
@@ -315,11 +369,12 @@ import { CopyButton } from '@nxgt/material-vue/components';
|
|
|
315
369
|
| Prop | Type | Default | Effect |
|
|
316
370
|
| --- | --- | --- | --- |
|
|
317
371
|
| `value` | `string \| (() => string)` | — (required) | what is copied; a function is read at click time |
|
|
318
|
-
| `label`
|
|
372
|
+
| `label` | `string` | `"Copy"` (translated) | the button's accessible name; it never changes after a click |
|
|
373
|
+
| `copied-label` / `failed-label` | `string` | `"Copied"` / `"Copy failed"` (translated) | announced in the status region (and shown in the tooltip) once the write succeeded or failed |
|
|
319
374
|
| `#icon` slot | — | a copy icon | replaces the idle icon |
|
|
320
375
|
| `variant`, `color` | as `IconButton` | `'ghost'`, `'default'` | the button style |
|
|
321
376
|
|
|
322
|
-
The
|
|
377
|
+
The write is awaited: "Copied" appears only once the clipboard accepted the text, and a refusal (no permission, an insecure page, no Clipboard API) shows "Copy failed" instead. The outcome is announced through a visually hidden `role="status"` (polite) region rendered next to the button, not by renaming the button: `getByRole('button', { name: 'Copy' })` keeps matching, and a test reads the outcome with `within(button.parentElement!).getByRole('status')` (a bare `getByRole('status')` is ambiguous: every `CopyButton` renders a `status` region, and so do `Banner`, `DataTable` and the skeletons). Its one root is a `span` with `display: contents` (no box) around the button and the status region, so `v-show`, `Transition` and `$el` work, and a class still goes to the button. The result stays for 1.5 s. `copyToClipboard(text)` is the same copy
|
|
323
378
|
without a button.
|
|
324
379
|
|
|
325
380
|
`CopyValue` shows the value in a monospace chip with the button beside it:
|
|
@@ -399,7 +454,7 @@ const emit = defineEmits<{ next: [] }>();
|
|
|
399
454
|
| `label` | `string` | `"Load more"` (translated) | tooltip, `aria-label` and expanded label |
|
|
400
455
|
| `#icon` slot | — | a rotate icon | replaces it |
|
|
401
456
|
| `variant` | as `Button` | `'tonal'` | the style |
|
|
402
|
-
| `loading` | `boolean` | — | spinner,
|
|
457
|
+
| `loading` | `boolean` | — | spinner, clicks ignored |
|
|
403
458
|
|
|
404
459
|
Icon-only by default because it sits in a pagination bar next to other icon
|
|
405
460
|
buttons.
|
|
@@ -522,7 +577,7 @@ on load.
|
|
|
522
577
|
|
|
523
578
|

|
|
524
579
|
|
|
525
|
-
Globe button with a menu of the `I18nProvider` languages.
|
|
580
|
+
Globe button with a menu of the `I18nProvider` languages. Its menu is non-modal (see [DropdownMenu](overlays-and-menus.md#dropdownmenu)).
|
|
526
581
|
|
|
527
582
|
```vue
|
|
528
583
|
<script setup lang="ts">
|
|
@@ -399,7 +399,7 @@ const user = { name: 'Ada Lovelace', phone: '' };
|
|
|
399
399
|
|
|
400
400
|
`label` and `value` (both `string`) are its only props; the default slot is
|
|
401
401
|
rendered under the value. A falsy `value` renders nothing, so optional fields
|
|
402
|
-
need no `v-if`.
|
|
402
|
+
need no `v-if`. The label is a small muted caption above the value (not a title). `Description` takes one cell of the grid it is in; pass `class="col-start-full"` for a full-width row.
|
|
403
403
|
|
|
404
404
|
**Storybook:** `Data display/Description` · **React:** `Description`
|
|
405
405
|
|
|
@@ -462,12 +462,23 @@ import { Icon, ListTile } from '@nxgt/material-vue/components';
|
|
|
462
462
|
| `title` | `string` | — | first line |
|
|
463
463
|
| `subtitle` | `string` | — | second, muted line |
|
|
464
464
|
| `href` | `string` | — | renders an `<a>`; without it the row is a `<button>` |
|
|
465
|
+
| `linkComponent` / `link-component` | `Component` | plain `<a>` | the link element, as in the app shell: a component that takes `href` and lets the other attributes fall through |
|
|
465
466
|
| `selected` | `boolean` | `false` | highlighted row (`aria-current="page"` on a link) |
|
|
466
467
|
| `size` | `'sm' \| 'md'` | `'md'` | `sm` is the dense navigation row |
|
|
467
468
|
| `disabled` | `boolean` | — | disables the link or button |
|
|
468
469
|
| `@click` | `(event: MouseEvent) => void` | — | fired by both shapes |
|
|
469
470
|
| `#leading`, `#trailing` | slots | — | icons, avatars, actions |
|
|
470
471
|
|
|
472
|
+
With a router, pass `link-component` so a click navigates on the client instead of reloading the page (`RouterLink` takes `to`, so wrap it in a component that takes `href`; `AppLink` below is the one defined in [A `ListTile` link reloads the whole page](../troubleshooting.md#a-listtile-link-reloads-the-whole-page)):
|
|
473
|
+
|
|
474
|
+
```vue
|
|
475
|
+
<ListTile title="Notifications" href="/settings/notifications" :link-component="AppLink" />
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
A disabled tile never renders the link component: it is a plain `<a>` without `href`, with `aria-disabled="true"`.
|
|
479
|
+
|
|
480
|
+
The root carries `data-slot="item-detail"` (a tile that is a button too).
|
|
481
|
+
|
|
471
482
|
**Storybook:** `Data display/ListTile` · **React:** `ListTile`
|
|
472
483
|
|
|
473
484
|
---
|
|
@@ -23,7 +23,7 @@ each other, so either can be used alone.
|
|
|
23
23
|
|
|
24
24
|

|
|
25
25
|
|
|
26
|
-
TanStack Table v9 with sorting, filtering, selection, expandable rows, pagination or load-more, column visibility, skeleton rows while loading and your own empty state — or one server page at a time, with controlled sorting and selection, cursor Previous / Next links and row links.
|
|
26
|
+
TanStack Table v9 with sorting, filtering, selection, expandable rows, pagination or load-more, column visibility, skeleton rows while loading and your own empty state — or one server page at a time, with controlled sorting and selection, cursor Previous / Next links and row links. On a phone it keeps its header row and scrolls sideways in a keyboard-reachable region; the page does not.
|
|
27
27
|
|
|
28
28
|
```vue
|
|
29
29
|
<script setup lang="ts">
|
|
@@ -79,7 +79,9 @@ table enables, for typing your own column defs.
|
|
|
79
79
|
| `#actions="{ table }"` / `actions` | slot / `(table) => VNode` | — | toolbar content with access to the table (selected rows…) |
|
|
80
80
|
| `onLoadMore` / `isLoadingMore` | `() => void` / `boolean` | — | adds a "load more" button under the rows |
|
|
81
81
|
| `options` | `{ showPage?: boolean; showSelected?: boolean }` | both shown | parts of the footer |
|
|
82
|
-
| `showHeader` | `boolean` |
|
|
82
|
+
| `showHeader` | `boolean` | shown | header row (column names, sorting, "Select all") at every width; `false` hides it |
|
|
83
|
+
| `maxHeight` | `number \| string` | none | caps the table's height (`400`, `'60vh'`); the rows scroll inside it |
|
|
84
|
+
| `scrollLabel` | `string` | "Scrollable table" | the name of the scroll region the table becomes while it overflows |
|
|
83
85
|
| `loading` | `boolean` | `false` | skeleton rows, or the current rows dimmed; `aria-busy`; controls disabled |
|
|
84
86
|
| `loadingRows` | `number` | the page size | how many skeleton rows |
|
|
85
87
|
| `#empty="{ filtered }"` | slot | icon and "No data available" | content of the empty row, spanning every visible column |
|
|
@@ -142,6 +144,72 @@ default, or the `#empty` slot, told whether rows exist but none matches the
|
|
|
142
144
|
filters (`filtered`); content that renders nothing gets the default.
|
|
143
145
|
`DataTableEmptyState` types the slot props.
|
|
144
146
|
|
|
147
|
+
### Phones and narrow screens
|
|
148
|
+
|
|
149
|
+
```vue
|
|
150
|
+
<template>
|
|
151
|
+
<!-- The rows scroll inside 60% of the viewport; the page scrolls otherwise. -->
|
|
152
|
+
<DataTable
|
|
153
|
+
:data="invoices"
|
|
154
|
+
:columns="columns"
|
|
155
|
+
max-height="60vh"
|
|
156
|
+
scroll-label="Invoices"
|
|
157
|
+
/>
|
|
158
|
+
</template>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The table is as tall as its rows, and the page scrolls: there is no height
|
|
162
|
+
cap unless you pass `max-height`, so a phone never gets a scroll inside the
|
|
163
|
+
page's. The header row shows at every width (`:show-header="false"` hides
|
|
164
|
+
it). With `max-height`, the rows scroll under the header row, which stays in
|
|
165
|
+
view on an opaque background.
|
|
166
|
+
|
|
167
|
+
When the columns are wider than the table, the table scrolls sideways in
|
|
168
|
+
one box (`[data-slot="table-wrapper"]`), and the page does not. While it
|
|
169
|
+
overflows, that box is a `region` named "Scrollable table"
|
|
170
|
+
(`material.table.labels.scroll-region`, or `scroll-label`) with
|
|
171
|
+
`tabindex="0"`, so a keyboard user can reach it and scroll it with the arrow
|
|
172
|
+
keys, and an edge fades on each side where columns are hidden
|
|
173
|
+
(`data-scroll-start` / `data-scroll-end` on its parent,
|
|
174
|
+
`[data-slot="table-scroll"]`). It is not a region while it fits, nor on the
|
|
175
|
+
server; once focused, it stays a tab stop until focus leaves, even if it
|
|
176
|
+
stops overflowing. The empty state is centred in the part of the table you
|
|
177
|
+
can see (the box's measured width, `--table-visible-width`), not across its
|
|
178
|
+
whole scroll width. The table needs no width from its parent: inside a
|
|
179
|
+
content-sized one (`w-fit`, `inline-flex`, an `auto` grid track, a popover)
|
|
180
|
+
it is as wide as its columns.
|
|
181
|
+
|
|
182
|
+
The `select` column's checkboxes keep their 16px box, with a 24px target
|
|
183
|
+
around it (WCAG 2.5.8). The toolbar renders only when it has content:
|
|
184
|
+
the `actions` prop or the `#actions` slot that render nothing (`null`,
|
|
185
|
+
`false`, whitespace, a `v-if` left out) leave no empty `<fieldset>` behind.
|
|
186
|
+
|
|
187
|
+
Selection actions do not move the rows. With `actions` or the `#actions`
|
|
188
|
+
slot and the `select` column (`predefinedColumns().select`, id `select`),
|
|
189
|
+
the toolbar's height is reserved — an empty, hidden placeholder
|
|
190
|
+
(`[data-slot="header-placeholder"]`) while they render nothing, the same
|
|
191
|
+
minimum height once they do — so the "Delete selected" that appears on the
|
|
192
|
+
first selection does not push the tapped row down. The reserved height is
|
|
193
|
+
one row of default-size buttons (`h-9`); taller or wrapping actions still
|
|
194
|
+
grow the toolbar.
|
|
195
|
+
|
|
196
|
+
```vue
|
|
197
|
+
<template>
|
|
198
|
+
<DataTable :data="people" :columns="[select, ...columns]">
|
|
199
|
+
<template #actions="{ table }">
|
|
200
|
+
<Button
|
|
201
|
+
v-if="table.getSelectedRowModel().rows.length > 0"
|
|
202
|
+
@click="handleDelete(table.getSelectedRowModel().rows)()"
|
|
203
|
+
>
|
|
204
|
+
Delete selected
|
|
205
|
+
</Button>
|
|
206
|
+
</template>
|
|
207
|
+
</DataTable>
|
|
208
|
+
</template>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Rows stay rows on a phone; a card layout is not built in.
|
|
212
|
+
|
|
145
213
|
### Server-side data
|
|
146
214
|
|
|
147
215
|
When the server filters, sorts and pages, hand the table **one page** and
|
|
@@ -263,7 +331,16 @@ async function archive() {
|
|
|
263
331
|
index of the page's first row) make the range: "41–60 of 133"
|
|
264
332
|
(`material.table.pagination.range`), "41–60" without a total, "133 rows"
|
|
265
333
|
without an offset. Cursor paging knows the offset from the trail's length
|
|
266
|
-
times the page size; leave it out if pages can be short.
|
|
334
|
+
times the page size; leave it out if pages can be short. The range is a
|
|
335
|
+
polite live region (`role="status"`, `aria-atomic="true"`, named "Rows
|
|
336
|
+
shown" by `material.table.pagination.range-label`) that is in the DOM from
|
|
337
|
+
the first render whenever `pagination` is given, so a screen reader
|
|
338
|
+
announces "41–60 of 133" when the page changes. With neither `offset` nor
|
|
339
|
+
`totalCount` the region is empty, and is kept in the DOM so a later change
|
|
340
|
+
is still announced. The table already has a visually hidden status for
|
|
341
|
+
"Loading data…" (and the page may have a toast region), so a test finds the
|
|
342
|
+
range with `getByRole('status', { name: 'Rows shown' })`, not a bare
|
|
343
|
+
`getByRole('status')`.
|
|
267
344
|
- **Sorting** is `v-model:sorting`: the headers show it, and
|
|
268
345
|
`update:sorting` hands you the new `SortingState` to put in the URL.
|
|
269
346
|
Filters work the same way: render them in `#actions` and keep their values
|
|
@@ -288,7 +365,10 @@ async function archive() {
|
|
|
288
365
|
navigation (opening a sheet). Alone, it makes the row focusable and Enter
|
|
289
366
|
calls it; with `getRowHref`, the link stays the keyboard target, and
|
|
290
367
|
calling `event.preventDefault()` in it stops a mouse click from following
|
|
291
|
-
the link.
|
|
368
|
+
the link. The row itself is never a second control: with `getRowHref` it
|
|
369
|
+
has no role, no `tabindex` and no `aria-label`, and the one link is named
|
|
370
|
+
by the cell's text, so `getByRole('link', { name: 'Ada', exact: true })`
|
|
371
|
+
is unique. A focusable row has no role of its own: for anything a screen
|
|
292
372
|
reader user must reach, prefer `getRowHref` or a button in a cell.
|
|
293
373
|
- **Your own footer.** The `#footer="{ table }"` slot or the `footer` prop
|
|
294
374
|
(`(table) => VNode`) replaces the footer — `DataTableServerFooter` is the
|
|
@@ -297,7 +377,7 @@ async function archive() {
|
|
|
297
377
|
page is fetched, and the current rows stay in place, dimmed.
|
|
298
378
|
|
|
299
379
|
The server footer's strings are `material.table.pagination.*`
|
|
300
|
-
(`previous`, `next`, `label`, `range`, `range-open`, `total`, `selected`),
|
|
380
|
+
(`previous`, `next`, `label`, `range`, `range-open`, `range-label`, `total`, `selected`),
|
|
301
381
|
in English and French.
|
|
302
382
|
|
|
303
383
|
For the TanStack API itself (column helpers, features, row models), see
|
|
@@ -58,9 +58,33 @@ under `I18nProvider`), and so are `DateRangeField`'s; an `aria-label` on
|
|
|
58
58
|
| `layout` | `'month-and-year' \| 'month-only' \| 'year-only'` | — | month / year selects in the heading instead of plain text |
|
|
59
59
|
| `year-range` | `DateValue[]` | 100 years back, 10 ahead | the years in the year select |
|
|
60
60
|
| `default-placeholder` | `DateValue` | today | the month shown first |
|
|
61
|
-
| `locale` | `string` | `'en'` | month and weekday names |
|
|
61
|
+
| `locale` | `string` | `I18nProvider` language, else `'en'` | month and weekday names, and the language of the day names |
|
|
62
62
|
|
|
63
63
|
**Emits:** `update:modelValue` with `DateValue | DateValue[] | undefined`.
|
|
64
|
+
|
|
65
|
+
**Day names.** Every day is named by its full date in the calendar's language
|
|
66
|
+
(the `locale` prop, else the active `I18nProvider` language, else English):
|
|
67
|
+
"Thursday, October 1, 2026", "jeudi 1 octobre 2026". The name is on the button
|
|
68
|
+
and, for Testing Library, on the `gridcell` around it. The chosen day has
|
|
69
|
+
`aria-selected="true"` on its cell and its button is described by "Selected"
|
|
70
|
+
(`material.calendar.selected`, translated); today has `aria-current="date"` on
|
|
71
|
+
both. The days of the neighbouring months are named by their own dates, so a
|
|
72
|
+
number alone matches no day. "Selected" comes from the `Calendar` itself, which
|
|
73
|
+
renders the hidden hint; a calendar hand-built from the exported parts names its
|
|
74
|
+
cells and buttons the same way but has no "Selected". `DateRangeField`'s
|
|
75
|
+
calendar does the same:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
await page.getByRole('gridcell', { name: 'Thursday, October 1, 2026', exact: true }).getByRole('button').click();
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Weekday row.** It follows the same language: "S", "M"… in English, "L",
|
|
82
|
+
"M"… under a French `I18nProvider`, with the week starting on the locale's
|
|
83
|
+
first day (Monday in French). Each header is labelled by its full weekday
|
|
84
|
+
("lundi"), as in `@nxgt/material`; reka hides the row from assistive tech
|
|
85
|
+
(`<thead aria-hidden>`), since every day's name already carries its weekday.
|
|
86
|
+
It reads at 4.5:1 or more on the popover in the light and the dark theme.
|
|
87
|
+
|
|
64
88
|
No form wrapper — use `DateFormField`.
|
|
65
89
|
**Storybook:** `Date & time/Calendar` · **React:** `Calendar`, `CalendarDayButton`
|
|
66
90
|
|
|
@@ -52,6 +52,8 @@ import { CircleAlert } from '@lucide/vue';
|
|
|
52
52
|
|
|
53
53
|
The default slot is rendered under the description.
|
|
54
54
|
|
|
55
|
+
The root carries `data-slot="alert"`, the title `data-slot="alert-title"` and the description `data-slot="alert-description"`, for tests and selectors.
|
|
56
|
+
|
|
55
57
|
**Storybook:** `Feedback & status/Alert` · **React:** `Alert`
|
|
56
58
|
|
|
57
59
|
---
|
|
@@ -157,7 +159,8 @@ the content brings both.
|
|
|
157
159
|
On open, `AlertDialogContent` focuses `AlertDialogCancel`, the least
|
|
158
160
|
destructive action (its first control when there is no Cancel, the content
|
|
159
161
|
when there is none); an `@open-auto-focus` that calls `event.preventDefault()`
|
|
160
|
-
keeps focus
|
|
162
|
+
keeps focus off the Cancel, on the content: the page behind an alert dialog is
|
|
163
|
+
`inert` while it is open ([Overlays & menus](overlays-and-menus.md)). After that opt-out the Cancel takes no focus until the next
|
|
161
164
|
task: focus it from a `setTimeout`, not in the handler.
|
|
162
165
|
|
|
163
166
|
**Emits:** `update:open` on `AlertDialog` — `boolean`.
|
|
@@ -239,7 +242,7 @@ function archive() {}
|
|
|
239
242
|
| Prop (`ConfirmationDialog`) | Type | Default | Effect |
|
|
240
243
|
| --- | --- | --- | --- |
|
|
241
244
|
| `#trigger` slot | — | — | becomes the dialog trigger (`as-child`, no extra button): pass one element, such as a `Button` |
|
|
242
|
-
| `v-model:open` | `boolean` | uncontrolled | open state;
|
|
245
|
+
| `v-model:open` | `boolean` | uncontrolled | open state: with `v-model:open`, or `:open` together with `@update:open`, the dialog shows exactly what `open` says; otherwise it keeps its own state (`:open` alone only opens it, `@update:open` alone only listens; an `open` that is still `undefined`, as `ref<boolean>()`, counts as not set and the dialog keeps its own state, so it still closes) |
|
|
243
246
|
| `@confirm` | `(values?) => void` | — | emitted on confirm, with the form's parsed values when there is one; the dialog then closes |
|
|
244
247
|
| `form-options` | `ConfirmationDialogFormOptions` (`{ schema, defaultValues? }`) | — | a form in the dialog, below the default slot — see below |
|
|
245
248
|
| `color` | `IconButton` colour | `'warning'` | icon and both buttons |
|
|
@@ -247,6 +250,11 @@ function archive() {}
|
|
|
247
250
|
| `#icon` slot | — | info icon, in a decorative `aria-hidden` badge | replaces the badge: keep yours decorative (`<IconButton as="span" aria-hidden="true">`), not a button that does nothing |
|
|
248
251
|
| `confirm-text` / `cancel-text` | `string` | "Confirm" / "Cancel", translated (`material.confirm-dialog.confirm` / `.cancel`) | button text |
|
|
249
252
|
|
|
253
|
+
**Several steps, one dialog.** Confirm closes it; to show the next step, change
|
|
254
|
+
its `title` (and `description`) and set `open` back to `true`, at once (from
|
|
255
|
+
`@confirm` or `update:open`) or after an `await`. No `:key` per step is needed,
|
|
256
|
+
and the reopened dialog's buttons take the click.
|
|
257
|
+
|
|
250
258
|
`form-options` puts a form in the confirmation, as React's `formOptions`:
|
|
251
259
|
`schema` is a Zod 4 schema (any Standard Schema, or a vee-validate typed
|
|
252
260
|
schema), `defaultValues` its initial values, and the `#form` slot renders the
|
|
@@ -564,16 +572,83 @@ async function upload() {
|
|
|
564
572
|
| --- | --- | --- | --- |
|
|
565
573
|
| `title` / `description` | `string` | preset title for `success`, `error`, `loading` | toast text |
|
|
566
574
|
| `icon` | a VNode (`toast`) · icon name (`toasts.*`) | preset icon | leading icon |
|
|
567
|
-
| `action` | `{ label, onClick? }` or `(dismiss) => VNode` | a translated
|
|
568
|
-
| `
|
|
575
|
+
| `action` | `{ label, onClick? }` or `(dismiss) => VNode` | a translated “Close” for `success`, `info`, `error`; a preset keeps the caller's own | trailing button: `onClick`, then the toast closes; or what the function renders, given `dismiss`. Brings the X button and a persistent toast, see below |
|
|
576
|
+
| `dismissLabel` | `string` | the translated “Close” when an `action` is given | accessible name of the X icon button beside the action; `''` hides it |
|
|
577
|
+
| `role` | `'status' \| 'alert'` | none (`toasts.error`: `'alert'`) | live-region role. Toasts are already announced politely by the `Toaster`'s region; `alert` interrupts, and may be read twice by some screen readers |
|
|
578
|
+
| `options` | vue-sonner `ExternalToast` | `duration: 5000` for presets, `Infinity` for `loading` and for any toast with your own `action` (a preset's fallback "Close" still times out at 5000) unless a `duration` is passed | passed to Sonner (`id`, `duration`, `position`…) |
|
|
569
579
|
|
|
570
580
|
The toast id defaults to the title, so two toasts with the same title
|
|
571
|
-
replace each other; pass `options: { id }` to show both. `Toaster`
|
|
572
|
-
vue-sonner's and takes its props (`position`, `expand`, `rich-colors`…)
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
581
|
+
replace each other; pass `options: { id }` to show both. `Toaster` wraps
|
|
582
|
+
vue-sonner's and takes its props (`position`, `expand`, `rich-colors`…), all
|
|
583
|
+
forwarded; mount the one from `@nxgt/material-vue/components`, not
|
|
584
|
+
vue-sonner's own, or neither phone change below applies. vue-sonner's
|
|
585
|
+
stylesheet comes with this package's (`styles.css`, or the compiled
|
|
586
|
+
stylesheet). `Toast` is the component each toast renders, and `sonnerToast`
|
|
587
|
+
is vue-sonner's own `toast`, for anything the presets do not cover.
|
|
588
|
+
|
|
589
|
+
**Types:** `ToasterProps` (vue-sonner's), from `@nxgt/material-vue/components`.
|
|
590
|
+
|
|
591
|
+
**On a phone** (under 600px wide, where vue-sonner lays toasts out across the
|
|
592
|
+
screen), a `Toaster` with no `position` shows its toasts at the top
|
|
593
|
+
(`top-center`): across the bottom they covered what a form keeps there, its
|
|
594
|
+
submit button first. A `position` you pass applies on every screen. The server
|
|
595
|
+
and the client's first render answer for a desktop (`bottom-right`, or for the
|
|
596
|
+
width `provideSSRWidth()` gives), and a phone moves the toasts to the top once
|
|
597
|
+
mounted. The phone offset is vue-sonner's `mobile-offset` (16px by default);
|
|
598
|
+
raise its `top` when a sticky header must stay visible:
|
|
599
|
+
`<Toaster :mobile-offset="{ top: 72 }" />`.
|
|
600
|
+
|
|
601
|
+
```vue
|
|
602
|
+
<Toaster /> <!-- bottom-right on a desktop, top-center on a phone -->
|
|
603
|
+
<Toaster position="bottom-center" /> <!-- bottom-center everywhere -->
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
**An action keeps the toast open.** `toast()` itself does it, so a preset and a
|
|
607
|
+
direct call behave alike: any toast with an `action` is persistent by default
|
|
608
|
+
(`options.duration: Infinity`, WCAG 2.2.1: a timed toast can vanish before a
|
|
609
|
+
keyboard or screen-reader user reaches its button) unless you pass
|
|
610
|
+
`options.duration`, and shows a small close (X) icon button beside the action,
|
|
611
|
+
named by `material.toast.buttons.close` ("Close" / "Fermer") unless you pass
|
|
612
|
+
your own `dismissLabel` (`''` hides it). The presets keep the `action` you pass
|
|
613
|
+
(`toasts.info({ description: 'A new version is available', action: { label:
|
|
614
|
+
'Reload', onClick } })`); without an `action`, the preset's text "Close" button
|
|
615
|
+
is unchanged, on a timed toast. Sonner pauses its timer while a mouse hovers a
|
|
616
|
+
toast, while a finger is down on it and while the page is hidden, but not on
|
|
617
|
+
keyboard focus, which is why the default is persistent. A tap does not hover:
|
|
618
|
+
the mouse a browser emulates after a tap (which never leaves until the next tap
|
|
619
|
+
elsewhere) is ignored, so a tapped toast still goes after its duration. On a
|
|
620
|
+
touch screen a tap no longer fans out a stack of two or more toasts either: pass
|
|
621
|
+
`expand` to keep the stack open. Sonner does not close a toast on Escape (it
|
|
622
|
+
only collapses the stack); this `Toast` does when focus is inside it, with or
|
|
623
|
+
without an action. The close button is reached with Tab. **Focus after a
|
|
624
|
+
dismiss:** the dismissed toast leaves the page with the focused control in it,
|
|
625
|
+
so focus falls back to `document.body`; move it yourself (`onClick`, or after
|
|
626
|
+
`toasts.dismiss()`) when the flow needs it elsewhere.
|
|
627
|
+
|
|
628
|
+
**Announced to screen readers:** `Toaster` renders a polite live region
|
|
629
|
+
(`<section aria-live="polite">`) that is in the page before the first toast,
|
|
630
|
+
so every toast inserted into it is read out. A toast therefore has no `role`
|
|
631
|
+
of its own: one on each toast would nest a second live region. `toasts.error`
|
|
632
|
+
is `role="alert"` (assertive), so a failure interrupts; pass `role` to
|
|
633
|
+
`toast()` or to a preset to choose another (a caller's wins). Mount `Toaster`
|
|
634
|
+
once, early, and not conditionally after the first toast. A `Toast` rendered
|
|
635
|
+
outside a `Toaster` is not in a live region: give it its own.
|
|
636
|
+
|
|
637
|
+
**In tests:** find a toast by its text within the region, and an error toast
|
|
638
|
+
by role. A `status` or `alert` takes no name from its content, so read the
|
|
639
|
+
message with `toHaveTextContent`.
|
|
640
|
+
|
|
641
|
+
```ts
|
|
642
|
+
const alert = await screen.findByRole('alert'); // toasts.error
|
|
643
|
+
expect(alert).toHaveTextContent('Save failed');
|
|
644
|
+
const saved = await screen.findByText('Item saved'); // toasts.success / info
|
|
645
|
+
expect(saved.closest('section[aria-live="polite"]')).not.toBeNull();
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
Wait with `findBy*` or `waitFor`, never a fixed delay. A toast stays in the
|
|
649
|
+
DOM until it is dismissed or times out (a toast with an action never times
|
|
650
|
+
out), so queries in one test file can see the previous
|
|
651
|
+
test's toasts: call `toasts.dismiss()` first.
|
|
577
652
|
|
|
578
653
|
**Storybook:** `Feedback & status/Toast` · **React:** `Toaster`, `toast`, `toasts`, `Toast` (`ToastAction` type)
|
|
579
654
|
|
|
@@ -640,7 +715,7 @@ import { HydrateLoading } from '@nxgt/material-vue/components';
|
|
|
640
715
|
| --- | --- | --- | --- |
|
|
641
716
|
| `#icon` slot | — | `<Spinner class="size-8" />` | replaces the spinner |
|
|
642
717
|
| default slot | — | — | shown above the spinner |
|
|
643
|
-
| `class` | `string` | — | the root fills the viewport (`
|
|
718
|
+
| `class` | `string` | — | the root fills the viewport (`w-lvw h-lvh`); override to box it |
|
|
644
719
|
|
|
645
720
|
**Storybook:** `Feedback & status/HydrateLoading` · **React:** `HydrateLoading`
|
|
646
721
|
|
|
@@ -498,8 +498,8 @@ const code = ref('');
|
|
|
498
498
|
| `length` | `number` | `6` | number of slots, split in two groups |
|
|
499
499
|
| `v-model` | `string` | — | the digits typed so far |
|
|
500
500
|
| `@complete` | `(value: string) => void` | — | once every slot is filled (from `vue-input-otp`) |
|
|
501
|
-
| `error` | `boolean` | — | error colour on the slots |
|
|
502
|
-
| `id` | `string` | from `useId()` | the input's id, which the label points to |
|
|
501
|
+
| `error` | `boolean` | — | error colour on the slots (`data-invalid` on each); `aria-invalid` goes on the input only, the one element a screen reader reads |
|
|
502
|
+
| `id` | `string` | from `useId()` | the input's id, which the label points to; it differs per field — `OtpFormField` too (not the field `name`), so two code forms on one page each keep their name |
|
|
503
503
|
|
|
504
504
|
`InputOTP`, `InputOTPGroup`, `InputOTPSlot` and `InputOTPSeparator` are the
|
|
505
505
|
unlabelled primitives it is built from, for a hand-built layout:
|
|
@@ -527,10 +527,19 @@ const pin = ref('');
|
|
|
527
527
|
</template>
|
|
528
528
|
```
|
|
529
529
|
|
|
530
|
+
**On a phone.** The slots are 36 px squares that shrink with a narrow
|
|
531
|
+
field, kept square: six fit a 320 px-wide card, each above the 24 px
|
|
532
|
+
target of WCAG 2.5.8, and the gaps between them narrow under the `sm`
|
|
533
|
+
breakpoint. A slot you build yourself with `InputOTPSlot` does the same.
|
|
534
|
+
|
|
535
|
+
**In a test**, find the code by its input — `getByLabelText('Enter OTP')` —
|
|
536
|
+
and an error by `toHaveAttribute('aria-invalid', 'true')` on it; a slot
|
|
537
|
+
carries `data-invalid`, not `aria-invalid`.
|
|
538
|
+
|
|
530
539
|
**Emits:** `update:modelValue` with `string` (the digits typed so far).
|
|
531
540
|
**Form wrapper:** `OtpFormField` — `name` inside a vee-validate form, see
|
|
532
541
|
[forms](../guides/forms.md#the-pair) and [what each field emits](../guides/forms.md#what-each-field-emits).
|
|
533
|
-
**Storybook:** `Form fields/OtpField` · **React:** `OtpField`, `OtpFormField`, `InputOTP` (same parts)
|
|
542
|
+
**Storybook:** `Form fields/OtpField`, `Form fields/OtpField/Phone` · **React:** `OtpField`, `OtpFormField`, `InputOTP` (same parts)
|
|
534
543
|
|
|
535
544
|
---
|
|
536
545
|
|
|
@@ -818,7 +827,7 @@ a screen reader says "Notifications, switch, Email me when an order ships". An
|
|
|
818
827
|
`aria-label` or `aria-labelledby` you pass wins, and an `aria-describedby` you
|
|
819
828
|
pass is kept beside the row text. The `helper-text` (the validation message,
|
|
820
829
|
under `SwitchFormField`) describes the switch too. With `required`, the label's
|
|
821
|
-
asterisk is
|
|
830
|
+
asterisk is drawn by CSS, so the name and the label text stay "Terms", not
|
|
822
831
|
"Terms*". Without an `id` one is generated. The row is
|
|
823
832
|
tinted while the switch is on, from `v-model` or, uncontrolled, from
|
|
824
833
|
`default-value` and each toggle, and carries `data-checked="true"` /
|