@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
package/docs/troubleshooting.md
CHANGED
|
@@ -18,21 +18,50 @@ words in your error.
|
|
|
18
18
|
|
|
19
19
|
- [Components render unstyled, with no error](#components-render-unstyled-with-no-error)
|
|
20
20
|
- [`<Icon>` renders nothing](#icon-renders-nothing)
|
|
21
|
+
- [An accessibility audit reports low contrast on a badge, a button, a toast or a menu entry](#an-accessibility-audit-reports-low-contrast-on-a-badge-a-button-a-toast-or-a-menu-entry)
|
|
22
|
+
- [`ARIA hidden element must not be focusable or contain focusable elements`](#aria-hidden-element-must-not-be-focusable-or-contain-focusable-elements)
|
|
23
|
+
- [An accessibility audit reports low contrast on a tonal or outlined button, a selected list tile or a hovered badge link](#an-accessibility-audit-reports-low-contrast-on-a-tonal-or-outlined-button-a-selected-list-tile-or-a-hovered-badge-link)
|
|
24
|
+
- [An accessibility audit reports low contrast on error text in the dark theme](#an-accessibility-audit-reports-low-contrast-on-error-text-in-the-dark-theme)
|
|
25
|
+
- [Dialogs, toasts or buttons no longer animate](#dialogs-toasts-or-buttons-no-longer-animate)
|
|
26
|
+
- [A Button's label overflows a card on a phone](#a-buttons-label-overflows-a-card-on-a-phone)
|
|
21
27
|
- [`JsBarcode is not a function`](#jsbarcode-is-not-a-function)
|
|
22
28
|
- [A `LinkButton` selected as a `button` stops matching](#a-linkbutton-selected-as-a-button-stops-matching)
|
|
23
29
|
- [A `Toc`, `Pager`, `SeeAlso` or `ScrollProgress` inside `Doc` selected by `data-slot` stops matching](#a-toc-pager-seealso-or-scrollprogress-inside-doc-selected-by-data-slot-stops-matching)
|
|
24
30
|
- [A `ResponsiveButton` selected by `[aria-expanded]` stops matching](#a-responsivebutton-selected-by-aria-expanded-stops-matching)
|
|
31
|
+
- [A menu no longer blocks the page, or a `ResponsiveButton` has no `useIsMobile` behaviour](#a-menu-no-longer-blocks-the-page-or-a-responsivebutton-has-no-useismobile-behaviour)
|
|
25
32
|
- [A menu item selected by `[data-variant=destructive]` stops matching](#a-menu-item-selected-by-data-variantdestructive-stops-matching)
|
|
26
33
|
- [A `Stepper` step selected by `#step-N` stops matching](#a-stepper-step-selected-by-step-n-stops-matching)
|
|
27
34
|
- [A test no longer finds a date or time picker by the `button` role, or a slider thumb by the exact name `Minimum`](#a-test-no-longer-finds-a-date-or-time-picker-by-the-button-role-or-a-slider-thumb-by-the-exact-name-minimum)
|
|
28
35
|
- [An end-to-end test no longer finds a required field by a name ending in `*`](#an-end-to-end-test-no-longer-finds-a-required-field-by-a-name-ending-in-)
|
|
29
36
|
- [A test no longer finds `aria-invalid` on an upload field's group or drop zone](#a-test-no-longer-finds-aria-invalid-on-an-upload-fields-group-or-drop-zone)
|
|
30
37
|
- [A test no longer finds `aria-invalid` on a `Switch` row or an `Autocomplete` box](#a-test-no-longer-finds-aria-invalid-on-a-switch-row-or-an-autocomplete-box)
|
|
38
|
+
- [A test no longer finds `aria-invalid` on an `OtpField` slot](#a-test-no-longer-finds-aria-invalid-on-an-otpfield-slot)
|
|
31
39
|
- [A test no longer finds the breadcrumb's home button](#a-test-no-longer-finds-the-breadcrumbs-home-button)
|
|
40
|
+
- [A test no longer finds a sidebar entry by the `button` role](#a-test-no-longer-finds-a-sidebar-entry-by-the-button-role)
|
|
41
|
+
- [A test no longer finds a tooltip on an entry of an expanded sidebar](#a-test-no-longer-finds-a-tooltip-on-an-entry-of-an-expanded-sidebar)
|
|
42
|
+
- [The app shell's page scrolls instead of its content, or a full-height screen collapses](#the-app-shells-page-scrolls-instead-of-its-content-or-a-full-height-screen-collapses)
|
|
43
|
+
- [A test finds a new heading or a second `main`, or no longer finds a title in a `div`](#a-test-finds-a-new-heading-or-a-second-main-or-no-longer-finds-a-title-in-a-div)
|
|
44
|
+
- [`[data-test="typogaphy"]` finds nothing](#data-testtypogaphy-finds-nothing)
|
|
45
|
+
- [A `class` on `ActivityLayout` now styles the whole shell](#a-class-on-activitylayout-now-styles-the-whole-shell)
|
|
46
|
+
- [A test no longer finds a calendar day by its number](#a-test-no-longer-finds-a-calendar-day-by-its-number)
|
|
32
47
|
- [A `QRCode`'s `aria-label` is no longer on its wrapper](#a-qrcodes-aria-label-is-no-longer-on-its-wrapper)
|
|
33
48
|
- [A `CheckboxGroup` or `RadioGroupField` option selected by `#<id>-<value>` stops matching](#a-checkboxgroup-or-radiogroupfield-option-selected-by-id-value-stops-matching)
|
|
34
49
|
- [A `Table`'s `id`, `aria-*` or `style` is no longer on its scrolling container](#a-tables-id-aria--or-style-is-no-longer-on-its-scrolling-container)
|
|
35
50
|
|
|
51
|
+
**Feedback**
|
|
52
|
+
|
|
53
|
+
- [A toast with an action never goes away](#a-toast-with-an-action-never-goes-away)
|
|
54
|
+
- [A toast covers the form's button on a phone](#a-toast-covers-the-forms-button-on-a-phone)
|
|
55
|
+
- [A toast never goes away after a tap](#a-toast-never-goes-away-after-a-tap)
|
|
56
|
+
- [A toast's action cannot be clicked while a dialog is open](#a-toasts-action-cannot-be-clicked-while-a-dialog-is-open)
|
|
57
|
+
- [A `ConfirmationDialog` reopened at once stays closed](#a-confirmationdialog-reopened-at-once-stays-closed)
|
|
58
|
+
- [The first Escape after closing a nested dialog does nothing](#the-first-escape-after-closing-a-nested-dialog-does-nothing)
|
|
59
|
+
|
|
60
|
+
**Navigation**
|
|
61
|
+
|
|
62
|
+
- [A `ListTile` link reloads the whole page](#a-listtile-link-reloads-the-whole-page)
|
|
63
|
+
- [The phone menu needs two Escapes to close, or a tooltip shows over it](#the-phone-menu-needs-two-escapes-to-close-or-a-tooltip-shows-over-it)
|
|
64
|
+
|
|
36
65
|
**SSR & build**
|
|
37
66
|
|
|
38
67
|
- [`ReferenceError: window is not defined`](#referenceerror-window-is-not-defined)
|
|
@@ -48,6 +77,8 @@ words in your error.
|
|
|
48
77
|
- [`injection "Symbol(route location)" not found`](#injection-symbolroute-location-not-found)
|
|
49
78
|
- [A `*FormField` is missing from the submitted values, or never shows an error](#a-formfield-is-missing-from-the-submitted-values-or-never-shows-an-error)
|
|
50
79
|
- [`meta.touched` stays false after leaving a field](#metatouched-stays-false-after-leaving-a-field)
|
|
80
|
+
- [A failed submit does not focus the first invalid field](#a-failed-submit-does-not-focus-the-first-invalid-field)
|
|
81
|
+
- [A test that finds a chip by `aria-selected` or `getByRole('option')` no longer finds it](#a-test-that-finds-a-chip-by-aria-selected-or-getbyroleoption-no-longer-finds-it)
|
|
51
82
|
- [A multi-step form reopens at a later step](#a-multi-step-form-reopens-at-a-later-step)
|
|
52
83
|
- [The form could not be loaded.](#the-form-could-not-be-loaded)
|
|
53
84
|
|
|
@@ -92,11 +123,14 @@ words in your error.
|
|
|
92
123
|
- [A translation key is rendered instead of its text](#a-translation-key-is-rendered-instead-of-its-text)
|
|
93
124
|
- [A label shows `{count}` or another placeholder](#a-label-shows-count-or-another-placeholder)
|
|
94
125
|
- [Zod messages ignore the language, or your `material.zod.*` strings](#zod-messages-ignore-the-language-or-your-materialzod-strings)
|
|
126
|
+
- [A test asserting the old Zod string length text fails](#a-test-asserting-the-old-zod-string-length-text-fails)
|
|
127
|
+
- [A calendar's weekday row is in English on a French page](#a-calendars-weekday-row-is-in-english-on-a-french-page)
|
|
95
128
|
|
|
96
129
|
**Runtime**
|
|
97
130
|
|
|
98
131
|
- [`must be used within` / `must be used inside` (context providers)](#must-be-used-within--must-be-used-inside)
|
|
99
132
|
- [`Clipboard is not available`](#clipboard-is-not-available)
|
|
133
|
+
- [A test no longer finds `CopyButton` by "Copied", or its name changes](#a-test-no-longer-finds-copybutton-by-copied-or-its-name-changes)
|
|
100
134
|
- [`Upload failed with status 403`](#upload-failed-with-status-403)
|
|
101
135
|
- [`Network error during upload`](#network-error-during-upload)
|
|
102
136
|
- [`Maximum number of presets (20) reached`](#maximum-number-of-presets-20-reached)
|
|
@@ -113,11 +147,21 @@ words in your error.
|
|
|
113
147
|
- [A test no longer finds the label or helper text inside a date or time picker's trigger](#a-test-no-longer-finds-the-label-or-helper-text-inside-a-date-or-time-pickers-trigger)
|
|
114
148
|
- [A test no longer finds a `DurationField` input by the field's label, or a `Slider`'s `aria-label` on its root](#a-test-no-longer-finds-a-durationfield-input-by-the-fields-label-or-a-sliders-aria-label-on-its-root)
|
|
115
149
|
- [An `AlertDialog` focuses its Cancel although `@open-auto-focus` prevented it](#an-alertdialog-focuses-its-cancel-although-open-auto-focus-prevented-it)
|
|
150
|
+
- [Focus moves into a dialog although `@open-auto-focus` prevented it](#focus-moves-into-a-dialog-although-open-auto-focus-prevented-it)
|
|
116
151
|
- [A `CustomDialog` or `ConfirmationDialog` no longer opens from its `#trigger`](#a-customdialog-or-confirmationdialog-no-longer-opens-from-its-trigger)
|
|
117
152
|
- [A form with a required `Switch` no longer submits](#a-form-with-a-required-switch-no-longer-submits)
|
|
118
153
|
- [A `RichTextEditor` became editable after upgrading](#a-richtexteditor-became-editable-after-upgrading)
|
|
119
154
|
- [Focus falls to `<body>` when a controlled `Drawer` closes](#focus-falls-to-body-when-a-controlled-drawer-closes)
|
|
120
155
|
- [A `TimePicker` test expects AM / PM to add 12 hours, or a click on the pressed half to switch to PM](#a-timepicker-test-expects-am--pm-to-add-12-hours-or-a-click-on-the-pressed-half-to-switch-to-pm)
|
|
156
|
+
- [A link or button in the first column of a `DataTable` with `getRowHref`](#a-link-or-button-in-the-first-column-of-a-datatable-with-getrowhref)
|
|
157
|
+
- [A row does not navigate after `@row-click` ran](#a-row-does-not-navigate-after-row-click-ran)
|
|
158
|
+
- [A `DataTable` Previous or Next button is disabled](#a-datatable-previous-or-next-button-is-disabled)
|
|
159
|
+
- [A double click or Enter submits a form twice, or a test no longer finds a loading `Button` disabled](#a-double-click-or-enter-submits-a-form-twice-or-a-test-no-longer-finds-a-loading-button-disabled)
|
|
160
|
+
- [A `DataTable` shows a sorting or a selection the URL never got](#a-datatable-shows-a-sorting-or-a-selection-the-url-never-got)
|
|
161
|
+
- [A `DataTable` no longer scrolls inside a 400px box](#a-datatable-no-longer-scrolls-inside-a-400px-box)
|
|
162
|
+
- [A `DataTable` shows its header row on a phone](#a-datatable-shows-its-header-row-on-a-phone)
|
|
163
|
+
- [A test's `getByRole('region')` finds more than one region](#a-tests-getbyroleregion-finds-more-than-one-region)
|
|
164
|
+
- [A `DataTable` toolbar is gone](#a-datatable-toolbar-is-gone)
|
|
121
165
|
|
|
122
166
|
**PDF reader**
|
|
123
167
|
|
|
@@ -393,6 +437,21 @@ const button = screen.getByRole('button', { name: 'New order' });
|
|
|
393
437
|
expect(within(button).getByText('New order')).toHaveClass('sr-only'); // collapsed
|
|
394
438
|
```
|
|
395
439
|
|
|
440
|
+
### A menu no longer blocks the page, or a `ResponsiveButton` has no `useIsMobile` behaviour
|
|
441
|
+
|
|
442
|
+
**When:** after upgrading, clicking outside an open `DropdownMenu` (or
|
|
443
|
+
`LanguageSwitcher`) reaches the element under the pointer and the page can
|
|
444
|
+
still scroll; or `ResponsiveButton` shows its label in a test whose
|
|
445
|
+
`matchMedia` says "mobile".
|
|
446
|
+
**Why:** `DropdownMenu` is non-modal by default now: Reka's modal menu marked
|
|
447
|
+
the rest of the page `aria-hidden` while its buttons and links stayed
|
|
448
|
+
focusable (axe `aria-hidden-focus`). `ResponsiveButton` and
|
|
449
|
+
`ResponsiveLinkButton` collapse with a CSS `max-md` media query instead of a
|
|
450
|
+
JS effect, so the label is in the DOM at every width and jsdom (which applies
|
|
451
|
+
no CSS) always shows it.
|
|
452
|
+
**Fix:** pass `modal` to `DropdownMenu` to restore the old behaviour; force a
|
|
453
|
+
collapsed button in a test with `:expanded="false"`.
|
|
454
|
+
|
|
396
455
|
### A menu item selected by `[data-variant=destructive]` stops matching
|
|
397
456
|
|
|
398
457
|
**When:** your CSS, or a test, targets `[data-variant=destructive]` on a
|
|
@@ -508,6 +567,23 @@ await expect(page.getByRole('switch', { name: 'Notifications' })).toHaveAttribut
|
|
|
508
567
|
await expect(page.getByLabel('Country')).toHaveAttribute('aria-invalid', 'true');
|
|
509
568
|
```
|
|
510
569
|
|
|
570
|
+
### A test no longer finds `aria-invalid` on an `OtpField` slot
|
|
571
|
+
|
|
572
|
+
**When:** `[data-slot="input-otp-slot"][aria-invalid]` (or
|
|
573
|
+
`getAllByRole(…)` filtered on `aria-invalid`) finds nothing although the
|
|
574
|
+
`OtpField` has `error`.
|
|
575
|
+
**Why:** ARIA does not allow `aria-invalid` on a plain `div`, and a slot is
|
|
576
|
+
a presentational box: the one element a screen reader reads is the hidden
|
|
577
|
+
input, which keeps `aria-invalid="true"`. A slot shows the error with a red
|
|
578
|
+
border from `data-invalid`.
|
|
579
|
+
**Fix:** assert it on the input, found by the field's label:
|
|
580
|
+
|
|
581
|
+
```ts
|
|
582
|
+
await expect(page.getByLabel('Enter OTP')).toHaveAttribute('aria-invalid', 'true');
|
|
583
|
+
// A slot's red border, if a test must see it:
|
|
584
|
+
await expect(page.locator('[data-slot="input-otp-slot"]').first()).toHaveAttribute('data-invalid');
|
|
585
|
+
```
|
|
586
|
+
|
|
511
587
|
### A test no longer finds the breadcrumb's home button
|
|
512
588
|
|
|
513
589
|
**When:** after upgrading, `getByRole('button', { name: 'Home' })` finds
|
|
@@ -525,6 +601,141 @@ await page.getByRole('navigation', { name: 'breadcrumb' })
|
|
|
525
601
|
.click();
|
|
526
602
|
```
|
|
527
603
|
|
|
604
|
+
### A test no longer finds a sidebar entry by the `button` role
|
|
605
|
+
|
|
606
|
+
**When:** after upgrading, `getByRole('button', { name: 'Invoices' })` finds
|
|
607
|
+
nothing among the entries of an `ActivityLayout`, `AppSidebar` or `AppHeader`'s
|
|
608
|
+
menu sheet.
|
|
609
|
+
**Why:** each entry used to be a `<button>` that navigated on click. It is now
|
|
610
|
+
a real `<a href>`, so its role is `link`, and the current page's entry carries
|
|
611
|
+
`aria-current="page"`. The icon inside is decorative (`aria-hidden`), so the
|
|
612
|
+
link's name is its label alone, "Invoices", and an exact name matches.
|
|
613
|
+
**Fix:** find it as a link:
|
|
614
|
+
|
|
615
|
+
```ts
|
|
616
|
+
const invoices = page.getByRole('link', { name: 'Invoices' });
|
|
617
|
+
await invoices.click();
|
|
618
|
+
await expect(invoices).toHaveAttribute('aria-current', 'page');
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
### A test no longer finds a tooltip on an entry of an expanded sidebar
|
|
622
|
+
|
|
623
|
+
**When:** after upgrading, a test hovers or focuses an entry of an expanded
|
|
624
|
+
`ActivityLayout` or `AppSidebar` sidebar (or of the phone menu sheet) and
|
|
625
|
+
waits for `getByRole('tooltip', { name: 'Inbox' })`, which never appears.
|
|
626
|
+
**Why:** the shell's entries no longer pass their label as `tooltip`. A
|
|
627
|
+
`SidebarMenuItem` shows its label as a tooltip only while its `Sidebar` is
|
|
628
|
+
collapsed; expanded, the label is already on screen, and a tooltip repeating
|
|
629
|
+
it opened over the first entry as focus moved into the menu sheet, where it
|
|
630
|
+
took the first Escape. A `tooltip` you pass is still shown, collapsed or
|
|
631
|
+
expanded, and a collapsed entry with no `tooltip` now shows its label, where
|
|
632
|
+
it showed nothing before.
|
|
633
|
+
**Fix:** find the entry by the label it shows —
|
|
634
|
+
`getByRole('link', { name: 'Inbox' })` — or collapse the sidebar first when
|
|
635
|
+
the test is about the tooltip. Pass `tooltip` when an expanded entry of your
|
|
636
|
+
own `Sidebar` must have one:
|
|
637
|
+
|
|
638
|
+
```vue
|
|
639
|
+
<SidebarMenuItem href="/inbox" tooltip="3 unread">Inbox</SidebarMenuItem>
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
### The app shell's page scrolls instead of its content, or a full-height screen collapses
|
|
643
|
+
|
|
644
|
+
**When:** after upgrading, the whole page scrolls under an `ActivityLayout`
|
|
645
|
+
rather than the box inside it; a test or a script that scrolled the shell's
|
|
646
|
+
inner box (`[data-slot=activity-layout] .overflow-auto`) finds nothing to
|
|
647
|
+
scroll; or a screen sized `h-full` to fill the shell (a map, a board) is only
|
|
648
|
+
as high as its content.
|
|
649
|
+
**Why:** the shell was `100lvw × 100lvh` and scrolled its content inside
|
|
650
|
+
itself: the page was a scrollbar's width wider than the window, and on a phone
|
|
651
|
+
the bottom of the content sat under the browser's toolbar. It is now `100%`
|
|
652
|
+
wide and at least `100dvh` high, and the document scrolls; the header and the
|
|
653
|
+
desktop sidebar are sticky.
|
|
654
|
+
**Fix:** scroll the window in a test (`window.scrollTo`, or let the browser
|
|
655
|
+
scroll an element into view). For a screen that manages its own height, opt
|
|
656
|
+
back into the in-shell scrolling with `scroll="container"`:
|
|
657
|
+
|
|
658
|
+
```vue
|
|
659
|
+
<ActivityLayout :items="items" scroll="container">
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
### A test finds a new heading or a second `main`, or no longer finds a title in a `div`
|
|
663
|
+
|
|
664
|
+
**When:** after upgrading, `getAllByRole('heading')` returns more than it did,
|
|
665
|
+
a selector such as `div[data-slot=card-title]` finds nothing, or an
|
|
666
|
+
accessibility check reports more than one `main` landmark in an app that
|
|
667
|
+
renders its own `<main>` around `ActivityLayout`.
|
|
668
|
+
**Why:** `ActivityContent`'s title is now a heading, `h1` by default; a
|
|
669
|
+
`CardTitle` is an `h3`; and the shell's content region is the page's
|
|
670
|
+
`<main>`. The phone menu sheet was named "Open menu", like its button; it is
|
|
671
|
+
now named "Navigation" (`material.app-shell.navigation`):
|
|
672
|
+
`getByRole('dialog', { name: 'Navigation' })`.
|
|
673
|
+
**Fix:** find titles by role and level —
|
|
674
|
+
`getByRole('heading', { level: 1, name: 'Invoices' })` — and pick another
|
|
675
|
+
level with `title-as` or `CardTitle`'s `as` (`'div'` for no heading). An app
|
|
676
|
+
with its own `<main>` passes `content-as="div"`.
|
|
677
|
+
|
|
678
|
+
### `[data-test="typogaphy"]` finds nothing
|
|
679
|
+
|
|
680
|
+
**When:** after upgrading, a selector on `data-test="typogaphy"` finds no
|
|
681
|
+
`Typography` or `HelperText`.
|
|
682
|
+
**Why:** the attribute was misspelled; it now reads `data-test="typography"`.
|
|
683
|
+
**Fix:** select `[data-test="typography"]` — or better, the text or the role
|
|
684
|
+
the user sees.
|
|
685
|
+
|
|
686
|
+
### A `class` on `ActivityLayout` now styles the whole shell
|
|
687
|
+
|
|
688
|
+
**When:** after upgrading, the classes passed to `<ActivityLayout class="…">`
|
|
689
|
+
(a padding, a background, a gap) land around the sidebar and the header too,
|
|
690
|
+
not only around the content, or a selector on the inner box finds them
|
|
691
|
+
missing.
|
|
692
|
+
**Why:** `class` used to land on the inner layout box; it now reaches the
|
|
693
|
+
outer element, `[data-slot=activity-layout]`, with the other attributes — as
|
|
694
|
+
`className` does in `@nxgt/material`.
|
|
695
|
+
**Fix:** keep on `ActivityLayout` only the classes meant for the whole shell,
|
|
696
|
+
and move the rest to the screen: `ActivityContent`'s `class`, or an element
|
|
697
|
+
of your own inside it:
|
|
698
|
+
|
|
699
|
+
```vue
|
|
700
|
+
<ActivityLayout :items="items" class="bg-muted">
|
|
701
|
+
<ActivityContent title="Invoices" class="gap-6">…</ActivityContent>
|
|
702
|
+
</ActivityLayout>
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### A test no longer finds a calendar day by its number
|
|
706
|
+
|
|
707
|
+
**When:** after upgrading, `getByRole('gridcell', { name: '1' })` finds nothing
|
|
708
|
+
in a `Calendar`, `DateField`, `DateRangeField` or `EventCalendar`'s date picker,
|
|
709
|
+
while `getByRole('button', { name: 'Thursday, October 1, 2026' })` (the old
|
|
710
|
+
name, which is also the new one) still does; or
|
|
711
|
+
`getByRole('gridcell', { name: '1', exact: true })` used to match both the 1st
|
|
712
|
+
and the next month's 1st, shown as an outside day.
|
|
713
|
+
**Why:** a day is now named by its full localized date and nothing else:
|
|
714
|
+
"Thursday, October 1, 2026" (in French "jeudi 1 octobre 2026"), on the button
|
|
715
|
+
and on the cell (`gridcell`) around it, which used to be named by its text,
|
|
716
|
+
"1". The chosen day carries `aria-selected="true"` on its cell and a
|
|
717
|
+
"Selected" description on its button, today `aria-current="date"` on both. The
|
|
718
|
+
days of the neighbouring months are named by their own dates ("Wednesday,
|
|
719
|
+
September 30, 2026"), so a bare number matches no day at all. The language is
|
|
720
|
+
the calendar's `locale`, else the active `I18nProvider` language, else English:
|
|
721
|
+
under a French provider the names, the heading and the weekday row are now
|
|
722
|
+
French, so pass `locale="en"` to keep English.
|
|
723
|
+
**Fix:** pick the day by its date, exactly, and assert the state; compute
|
|
724
|
+
today's name instead of writing it:
|
|
725
|
+
|
|
726
|
+
```ts
|
|
727
|
+
const name = (date: Date) =>
|
|
728
|
+
new Intl.DateTimeFormat('en', { dateStyle: 'full' }).format(date);
|
|
729
|
+
|
|
730
|
+
const day = page.getByRole('gridcell', { name: 'Thursday, October 1, 2026', exact: true });
|
|
731
|
+
await day.getByRole('button').click();
|
|
732
|
+
await expect(day).toHaveAttribute('aria-selected', 'true');
|
|
733
|
+
await expect(page.getByRole('button', { name: 'Thursday, October 1, 2026' })).toHaveAccessibleDescription('Selected');
|
|
734
|
+
await expect(page.getByRole('gridcell', { name: name(new Date()) })).toHaveAttribute('aria-current', 'date');
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
`getByRole('button', { name: 'Thursday, October 1, 2026', exact: true })` finds the same day.
|
|
738
|
+
|
|
528
739
|
### A `QRCode`'s `aria-label` is no longer on its wrapper
|
|
529
740
|
|
|
530
741
|
**When:** after upgrading, `div[aria-label="…"]` (or a
|
|
@@ -605,16 +816,22 @@ page.locator('[data-slot="table-container"]'); // the scrolling container
|
|
|
605
816
|
|
|
606
817
|
**When:** after upgrading to 0.22.0, `getByRole('textbox', { name: 'Email*' })`
|
|
607
818
|
(or any role query whose name ends in `*`) finds nothing for a field whose
|
|
608
|
-
`Label` has `with-asterisk` (a `required` field).
|
|
609
|
-
`getByLabelText` and Playwright's
|
|
610
|
-
|
|
611
|
-
`getByLabel('Email')`
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
819
|
+
`Label` has `with-asterisk` (a `required` field). Since the release that drew the
|
|
820
|
+
asterisk with CSS, `getByLabelText('Email*')` and Playwright's
|
|
821
|
+
`getByLabel('Email *')` find nothing either; and the other way round,
|
|
822
|
+
`getByLabel('Email', { exact: true })` and `getByLabelText('Email')` now match
|
|
823
|
+
a required field, which a `*` in the label's text used to prevent.
|
|
824
|
+
**Why:** screen readers read the asterisk ("Email star" or "Email*"), and
|
|
825
|
+
Testing Library and Playwright compare the label's text, `aria-hidden` or not.
|
|
826
|
+
The `*` is now drawn by a CSS pseudo-element with an empty `content`
|
|
827
|
+
(`label.css`, imported by the stylesheet), so it is in no text and no name. The
|
|
828
|
+
field's own `required` / `aria-required` says it is required, and every field
|
|
829
|
+
of this library sets it on the element a screen reader lands on (the input, the
|
|
830
|
+
picker's combobox, the select's trigger, the slider thumbs, the rating's
|
|
616
831
|
radiogroup, the editor's textbox, the list's listbox, or, for a file picker,
|
|
617
|
-
a group described as "This field is required.").
|
|
832
|
+
a group described as "This field is required."). A `SelectField`'s trigger is
|
|
833
|
+
now named by its label (`aria-labelledby`), so `getByRole('combobox', { name:
|
|
834
|
+
'Framework' })` finds it.
|
|
618
835
|
**Fix:** find the field by its label alone, and check that it is required
|
|
619
836
|
separately:
|
|
620
837
|
|
|
@@ -630,6 +847,401 @@ await expect(page.getByRole('textbox', { name: 'Email' })).toHaveAttribute('requ
|
|
|
630
847
|
await expect(page.getByRole('combobox', { name: 'Delivery date' })).toHaveAttribute('aria-required', 'true');
|
|
631
848
|
```
|
|
632
849
|
|
|
850
|
+
Custom CSS that targeted the old `<span class="text-error">` should target
|
|
851
|
+
`[data-slot="label-text"][data-required]::after` instead; the recipe is in
|
|
852
|
+
[Label](components/primitives.md#label). The mark is drawn by the package stylesheet, so a
|
|
853
|
+
snapshot of the DOM no longer contains it, and it stays visible in
|
|
854
|
+
forced-colors mode.
|
|
855
|
+
|
|
856
|
+
### An accessibility audit reports low contrast on a badge, a button, a toast or a menu entry
|
|
857
|
+
|
|
858
|
+
**When:** axe (or Lighthouse) reports `color-contrast` on a `Badge` with
|
|
859
|
+
`variant="success"`, `"info"`, `"warning"` or `"secondary"`; on a filled
|
|
860
|
+
`secondary`, `success`, `info` or `warning` `Button` or `IconButton` in the
|
|
861
|
+
dark theme; on a filled `Button` or `IconButton` while it is hovered, focused
|
|
862
|
+
or pressed; on muted text on a `bg-muted` surface (an `Alert`
|
|
863
|
+
`variant="foreground"`, the default `Timeline` marker, code) or on the
|
|
864
|
+
"paper" tint; on a toast's title or description while it enters, or its X
|
|
865
|
+
button; or on the muted text (a shortcut, the `LanguageSwitcher` code) of a
|
|
866
|
+
highlighted menu entry, or on a calendar's weekday row in the dark theme.
|
|
867
|
+
**Why:** the colours come from your tokens and the components' state tints.
|
|
868
|
+
Before this fix, concretely: the light `--secondary`,
|
|
869
|
+
`--success`, `--info` and `--warning` gave their near-white text 4.3:1,
|
|
870
|
+
3.5:1, 2.5:1 and 3.4:1; the light `--muted-foreground` read 4.3:1 on
|
|
871
|
+
`--muted` and 4.4:1 on the paper tint, and the dark `--muted` was the light,
|
|
872
|
+
near-white value, so muted text on `bg-muted` failed in the dark theme too;
|
|
873
|
+
in the dark theme a filled `secondary`, `success`, `info` or `warning` button
|
|
874
|
+
gave 4.3:1, 3.5:1, 2.5:1 and 3.4:1, and its hover, focus and pressed tints
|
|
875
|
+
less; in the light theme a filled button faded toward the page when hovered,
|
|
876
|
+
focused or pressed (`/90`, `/80`, `/70`: down to 3.1:1 pressed); a library
|
|
877
|
+
toast started at opacity 0, so an audit run while it entered measured its
|
|
878
|
+
text faded; and the toast and the highlighted entry used the muted colour on
|
|
879
|
+
a background it does not reach 4.5:1 on; the dark `--muted-foreground` of a
|
|
880
|
+
calendar's weekday row reached 3.7:1 on a popover. If the audit still fails, your
|
|
881
|
+
stylesheet overrides one of those tokens, a `class` of your own puts the old
|
|
882
|
+
state tints back, or you still ship one of those versions.
|
|
883
|
+
**Fix:** update, remove the override, or pick a colour that keeps 4.5:1 with
|
|
884
|
+
its `-foreground`:
|
|
885
|
+
|
|
886
|
+
```css
|
|
887
|
+
:root {
|
|
888
|
+
--info: oklch(0.5 0.165 254.624); /* 5.8:1 with --info-foreground */
|
|
889
|
+
}
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
A toast the library renders, through `toast()` or `toasts.*` from
|
|
893
|
+
`@nxgt/material-vue` (vue-sonner's `toast.custom` path, marked
|
|
894
|
+
`data-styled="false"`), no longer fades its text in: the wrapper stays opaque
|
|
895
|
+
while it slides in, so an audit run while it enters measures the settled
|
|
896
|
+
colours. A toast vue-sonner styles itself still fades in from opacity 0: a
|
|
897
|
+
plain `toast('…')` or `toast.success('…')` from `vue-sonner`, `sonnerToast`
|
|
898
|
+
from this package, and, for now, the toasts `Notifications` shows with
|
|
899
|
+
`toast-on-arrival` and the messages of `AttributesField`. Show your own
|
|
900
|
+
through `toasts.*` instead. If a library toast still reports its title as low
|
|
901
|
+
contrast, an override of `[data-sonner-toast]` sets its `opacity` back to 0
|
|
902
|
+
on mount.
|
|
903
|
+
|
|
904
|
+
The muted text on a `bg-muted` surface (an `Alert` `variant="foreground"`, the
|
|
905
|
+
default `Timeline` marker) and on the "paper" tint failed in the shipped
|
|
906
|
+
palette too (4.3:1 and 4.4:1 in light; the dark `--muted` was the light one).
|
|
907
|
+
They pass since the `--muted` and `--muted-foreground` tokens changed: if
|
|
908
|
+
yours still fail, you override them.
|
|
909
|
+
|
|
910
|
+
In the dark theme, a filled `secondary` `Button` gave its near-white text
|
|
911
|
+
4.3:1, a `tonal` secondary one 3.9:1 (3.4:1 on `--card`), and a filled
|
|
912
|
+
`success`, `info` or `warning` one 3.5:1, 2.5:1 and 3.4:1. The dark
|
|
913
|
+
`--secondary` is brighter (`oklch(0.66 ...)`, 6.5:1 as text on
|
|
914
|
+
`--background`), and the filled `secondary`, `success`, `info` and `warning`
|
|
915
|
+
`Button`s and `IconButton`s are tinted in the dark theme
|
|
916
|
+
(`dark:bg-secondary/70`, `dark:bg-success/60`, …) so their text keeps 4.5:1.
|
|
917
|
+
A filled button given a background of your own through `className` loses the
|
|
918
|
+
tint in dark mode: add a matching `dark:` one.
|
|
919
|
+
|
|
920
|
+
### `ARIA hidden element must not be focusable or contain focusable elements`
|
|
921
|
+
|
|
922
|
+
**When:** an accessibility audit (axe, Lighthouse, the Storybook
|
|
923
|
+
accessibility panel) runs while a `Dialog`, `Sheet`, `Drawer`,
|
|
924
|
+
`AlertDialog`, `CommandDialog`, a `Select` list, a `ContextMenu`, or a
|
|
925
|
+
`modal` `DropdownMenu` or `Popover` is open, and reports `aria-hidden-focus`
|
|
926
|
+
on the page behind it: its buttons, links and fields.
|
|
927
|
+
**Why:** reka marks the rest of the page `aria-hidden` while a modal layer
|
|
928
|
+
is open and leaves it focusable; a control the tab order reaches inside an
|
|
929
|
+
`aria-hidden` subtree is what the rule reports. A version of @nxgt/material-vue that
|
|
930
|
+
reports it predates the inert background.
|
|
931
|
+
**Fix:** upgrade: the page behind a modal layer of this library is `inert`
|
|
932
|
+
as well — out of the tab order and of the accessibility tree — and the rule
|
|
933
|
+
passes. Live regions (the `Toaster`) stay reachable. Do not exclude
|
|
934
|
+
`[aria-hidden]` or `[data-aria-hidden]` from the audit: it hides real
|
|
935
|
+
failures. A layer of yours built on reka directly (not on these
|
|
936
|
+
components) still marks the page `aria-hidden` only; make the rest of the
|
|
937
|
+
page inert with core's `inertOthers`, which keeps live regions and counts
|
|
938
|
+
nested layers:
|
|
939
|
+
|
|
940
|
+
```ts
|
|
941
|
+
import { inertOthers } from '@nxgt/material-core/inert-others';
|
|
942
|
+
import { nextTick, ref, watch } from 'vue';
|
|
943
|
+
|
|
944
|
+
const open = ref(false);
|
|
945
|
+
// A template ref on your layer's content: an element or a component.
|
|
946
|
+
const content = ref();
|
|
947
|
+
|
|
948
|
+
watch(open, (isOpen, _, onCleanup) => {
|
|
949
|
+
if (!isOpen) return;
|
|
950
|
+
let undo: (() => void) | undefined;
|
|
951
|
+
let closed = false;
|
|
952
|
+
nextTick(() => {
|
|
953
|
+
// Closed before the tick: do not mark the page.
|
|
954
|
+
if (!closed) undo = inertOthers(content.value?.$el ?? content.value);
|
|
955
|
+
});
|
|
956
|
+
onCleanup(() => {
|
|
957
|
+
closed = true;
|
|
958
|
+
undo?.();
|
|
959
|
+
});
|
|
960
|
+
});
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
Elements mounted after the layer opens are not made inert: mount them
|
|
964
|
+
first, or pass them to `inertOthers` in `keep` when they must stay
|
|
965
|
+
reachable.
|
|
966
|
+
|
|
967
|
+
### An accessibility audit reports low contrast on a tonal or outlined button, a selected list tile or a hovered badge link
|
|
968
|
+
|
|
969
|
+
**When:** axe (or Lighthouse) reports `color-contrast` on a `tonal` or
|
|
970
|
+
`outlined` `Button` or `IconButton` (`primary` in the dark theme, `error`,
|
|
971
|
+
`warning` and `success`), at rest or hovered, focused or pressed; on the
|
|
972
|
+
subtitle of a selected `ListTile`; or on a `Badge` link while it is hovered.
|
|
973
|
+
**Why:** those variants drew their text in the token itself, and some tokens
|
|
974
|
+
do not reach 4.5:1 as text on the page and on their own tint: the dark
|
|
975
|
+
`--primary` is a dark indigo (2.2:1 on `--card`), the light `--error` gave
|
|
976
|
+
3.6:1 on its tonal tint, `--warning` 4.3:1, the dark `--success` 4.0:1 on
|
|
977
|
+
`--card`. The selected tile's tint brought the muted subtitle to 4.4:1, and a
|
|
978
|
+
`Badge` link faded toward the page on hover (`/90`: down to 3.0:1 for `info`
|
|
979
|
+
in the dark theme). A release that has the fix mixes those tones from the
|
|
980
|
+
tokens, toward black in the light theme and toward white in the dark one; if
|
|
981
|
+
the audit still fails, a text colour of your own replaces the tone, or a
|
|
982
|
+
token of yours is too light (or, in the dark theme, too dark) to read as text.
|
|
983
|
+
**Fix:** update, then drop the text colour you put on the button, or check
|
|
984
|
+
your token against the page at 4.5:1:
|
|
985
|
+
|
|
986
|
+
```vue
|
|
987
|
+
<!-- before: the tone is replaced by the raw token -->
|
|
988
|
+
<Button variant="tonal" color="error" class="text-error">Delete</Button>
|
|
989
|
+
<!-- after -->
|
|
990
|
+
<Button variant="tonal" color="error">Delete</Button>
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
A `Badge` link of your own with a hover fade such as `[a&]:hover:bg-error/90`
|
|
994
|
+
in its `class` puts the fade back: darken instead, as in
|
|
995
|
+
`[a&]:hover:bg-[color-mix(in_oklab,var(--color-error)_90%,black)]` (with
|
|
996
|
+
your badge's colour in place of `error`). A
|
|
997
|
+
subtitle colour of your own on a selected `ListTile` must reach 4.5:1 on
|
|
998
|
+
`bg-primary/15`.
|
|
999
|
+
|
|
1000
|
+
### An accessibility audit reports low contrast on error text in the dark theme
|
|
1001
|
+
|
|
1002
|
+
**When:** an error message, a validation hint or a red value of your own, in
|
|
1003
|
+
the dark theme, fails 4.5:1 (about 4.2:1 on the page, 3.8:1 on a card); the
|
|
1004
|
+
catalogue's own field messages pass.
|
|
1005
|
+
**Why:** it is drawn with `text-error`, the fill colour. The dark `--error` is
|
|
1006
|
+
kept dark enough for near-white text on a filled error surface, which makes it
|
|
1007
|
+
too dark to read as text on the dark surfaces. Error text has its own token,
|
|
1008
|
+
`--error-text`, lightened in the dark theme (5.9:1 on `--background`, 5.3:1 on
|
|
1009
|
+
`--card`).
|
|
1010
|
+
**Fix:** draw text with `text-error-text`; keep `text-error` for fills,
|
|
1011
|
+
borders and icons that stand alone. On a `Button` or `IconButton` drop the
|
|
1012
|
+
class instead: the tone already colours the text:
|
|
1013
|
+
|
|
1014
|
+
```vue
|
|
1015
|
+
<!-- before -->
|
|
1016
|
+
<p class="text-sm text-error">Enter a valid email</p>
|
|
1017
|
+
<!-- after -->
|
|
1018
|
+
<p class="text-sm text-error-text">Enter a valid email</p>
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
Setting `--error` or `--error-text` on an element below the root (a `style`
|
|
1022
|
+
on a section) changes nothing: `@theme` resolves `--color-error-text` on the
|
|
1023
|
+
root. Below the root, set `--color-error-text` (and `--color-error` for fills)
|
|
1024
|
+
on that element, or put the override on the root.
|
|
1025
|
+
|
|
1026
|
+
### Dialogs, toasts or buttons no longer animate
|
|
1027
|
+
|
|
1028
|
+
**When:** a transition or an animation of a catalogue component does not run
|
|
1029
|
+
on your machine, while it does on a colleague's.
|
|
1030
|
+
**Why:** the operating system has "Reduce motion" on. `styles.css` honours
|
|
1031
|
+
`prefers-reduced-motion: reduce` for every component (dialogs, sheets, menus,
|
|
1032
|
+
toasts, buttons) and leaves only the `Spinner` and
|
|
1033
|
+
`animate-spin` elements turning.
|
|
1034
|
+
**Fix:** none is needed, this is the intended behaviour. To test the animated
|
|
1035
|
+
version, turn the setting off (or, in Chrome DevTools, Rendering, "Emulate CSS
|
|
1036
|
+
media feature prefers-reduced-motion": no-preference). To keep one animation
|
|
1037
|
+
for everyone, override it with an `!important` rule on that element inside
|
|
1038
|
+
`@media (prefers-reduced-motion: reduce)`.
|
|
1039
|
+
|
|
1040
|
+
### A Button's label overflows a card on a phone
|
|
1041
|
+
|
|
1042
|
+
**When:** on a narrow screen, a `Button` with a long label sticks out of its
|
|
1043
|
+
card, dialog or column (a 349 px button in a 343 px card), or its label is
|
|
1044
|
+
clipped at both ends with no "…".
|
|
1045
|
+
**Why:** before this change, the button took its label's whole width
|
|
1046
|
+
(`width: max-content`) whatever its container. A button is now never wider
|
|
1047
|
+
than its container (`max-width: 100%`), and its text is put in a
|
|
1048
|
+
`<span data-slot="button-label">` (one span per run of text, so `Delete {name}?` is one piece) that shrinks and ends in "…". The reverse
|
|
1049
|
+
is the same change: a button that grew past its container now truncates.
|
|
1050
|
+
The ellipsis needs the text as the button's own child: with `as-child`, text
|
|
1051
|
+
inside the child element is clipped without one.
|
|
1052
|
+
**Fix:** update. Then pick, for each long label: leave the ellipsis (give a
|
|
1053
|
+
`title` so a sighted user can read it whole; the accessible name already
|
|
1054
|
+
is), let it break onto lines with `wrap`, or give the button a width of
|
|
1055
|
+
your own through `class`. With `as-child`, put the label in a `<span>`:
|
|
1056
|
+
|
|
1057
|
+
```vue
|
|
1058
|
+
<template>
|
|
1059
|
+
<Button title="Continue to the payment details">Continue to the payment details</Button>
|
|
1060
|
+
<Button wrap>Continue to the payment details and confirm the order</Button>
|
|
1061
|
+
<Button class="w-full">Continue</Button>
|
|
1062
|
+
<Button as-child>
|
|
1063
|
+
<a href="/pay"><span>Continue to the payment details</span></a>
|
|
1064
|
+
</Button>
|
|
1065
|
+
</template>
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
## Feedback
|
|
1069
|
+
|
|
1070
|
+
### A toast with an action never goes away
|
|
1071
|
+
|
|
1072
|
+
**When:** a test, or a user, waits for a toast to disappear by itself, and one
|
|
1073
|
+
that carries an `action` (`toast({ action })`, or `toasts.info({ action })`
|
|
1074
|
+
and the other presets with your own action) is still there.
|
|
1075
|
+
**Why:** a toast with an `action` is persistent: `toast()` sets
|
|
1076
|
+
`options.duration` to `Infinity` (WCAG 2.2.1: a timed toast can vanish before
|
|
1077
|
+
a keyboard or screen-reader user reaches its button). Without an `action` a
|
|
1078
|
+
preset times out after 5 seconds.
|
|
1079
|
+
**Fix:** pass a duration, or close it:
|
|
1080
|
+
|
|
1081
|
+
```ts
|
|
1082
|
+
toasts.info({ action, options: { duration: 8000 } }); // times out again
|
|
1083
|
+
toasts.dismiss(id); // or toasts.dismiss() for all of them
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
Users close it with its X button (named "Close", or "Fermer" in French),
|
|
1087
|
+
with Escape while focus is inside it, or with the action itself. In a test,
|
|
1088
|
+
click `getByRole('button', { name: 'Close' })` or call `toasts.dismiss()`.
|
|
1089
|
+
|
|
1090
|
+
### A toast covers the form's button on a phone
|
|
1091
|
+
|
|
1092
|
+
**When:** on a phone, a toast shows across the bottom of the screen, over a
|
|
1093
|
+
form's submit button (or anything else kept at the bottom), until it goes.
|
|
1094
|
+
**Why:** under 600px wide, vue-sonner lays toasts out across the whole width,
|
|
1095
|
+
and its default `position` is `bottom-right`, so the bottom of the screen. The
|
|
1096
|
+
`Toaster` now shows them at the top (`top-center`) on a phone when it is given
|
|
1097
|
+
no `position`; it still covers the bottom when the app mounts vue-sonner's own
|
|
1098
|
+
`Toaster` (`import { Toaster } from 'vue-sonner'`), or passes a bottom
|
|
1099
|
+
`position`, which applies on every screen.
|
|
1100
|
+
**Fix:** mount the `Toaster` from `@nxgt/material-vue/components`, and drop a
|
|
1101
|
+
bottom `position` unless the bottom is what you want on phones too. When a
|
|
1102
|
+
sticky header must stay visible above the toasts, raise the phone offset's
|
|
1103
|
+
`top`:
|
|
1104
|
+
|
|
1105
|
+
```vue
|
|
1106
|
+
<Toaster /> <!-- top-center on a phone -->
|
|
1107
|
+
<Toaster :mobile-offset="{ top: 72 }" />
|
|
1108
|
+
<Toaster position="bottom-center" /> <!-- bottom-center on every screen -->
|
|
1109
|
+
```
|
|
1110
|
+
|
|
1111
|
+
### A toast never goes away after a tap
|
|
1112
|
+
|
|
1113
|
+
**When:** on a phone (or any touch screen), a toast that should time out stays
|
|
1114
|
+
on the screen after the user tapped it, and only goes after a tap somewhere
|
|
1115
|
+
else.
|
|
1116
|
+
**Why:** a toast pauses while a mouse hovers it (WCAG 2.2.1). After a tap, the
|
|
1117
|
+
browser emulates a mouse that arrives over the toast and never leaves, and
|
|
1118
|
+
vue-sonner read that as a hover, so the toast stayed paused.
|
|
1119
|
+
**Fix:** update, and mount the `Toaster` from `@nxgt/material-vue/components`:
|
|
1120
|
+
it ignores the hover a tap emulates (`@nxgt/material-core/touch-hover`), so a
|
|
1121
|
+
tapped toast pauses while the finger is down and goes after its duration; a
|
|
1122
|
+
mouse still pauses it. vue-sonner's own `Toaster`
|
|
1123
|
+
(`import { Toaster } from 'vue-sonner'`) keeps the old behaviour. A toast with an
|
|
1124
|
+
`action` is meant to stay: see
|
|
1125
|
+
[A toast with an action never goes away](#a-toast-with-an-action-never-goes-away).
|
|
1126
|
+
|
|
1127
|
+
### A toast's action cannot be clicked while a dialog is open
|
|
1128
|
+
|
|
1129
|
+
**When:** a toast with an `action` (or a close button) shows over an open
|
|
1130
|
+
`Dialog`, `AlertDialog`, `Sheet` or `Drawer`: the pointer does nothing on it,
|
|
1131
|
+
or a click on it closes the dialog.
|
|
1132
|
+
**Why:** a modal layer sets `pointer-events: none` on `<body>`, and the
|
|
1133
|
+
`Toaster` inherited it, so its toasts could not be clicked; the layer also
|
|
1134
|
+
counted a pointer down on a toast as outside the dialog.
|
|
1135
|
+
**Fix:** update `@nxgt/material-vue` and mount its `Toaster`: it sets
|
|
1136
|
+
`pointer-events: auto`, and the modal contents treat a pointer down or a focus
|
|
1137
|
+
inside the `Toaster` as inside, so the action runs and the dialog stays open.
|
|
1138
|
+
Sonner's own `Toaster` keeps the old behaviour.
|
|
1139
|
+
|
|
1140
|
+
### A `ConfirmationDialog` reopened at once stays closed
|
|
1141
|
+
|
|
1142
|
+
**When:** a `ConfirmationDialog` with `v-model:open` (or `:open` and
|
|
1143
|
+
`@update:open`) closes on Confirm and your `update:open` handler sets `open`
|
|
1144
|
+
back to `true` in the same tick, to show the next step. `open` is `true`, the
|
|
1145
|
+
dialog is shut, and setting `open = true` again does nothing. Giving the
|
|
1146
|
+
dialog a `:key` per step made it go away.
|
|
1147
|
+
**Why:** the dialog kept its own copy of `open`, synced by a watch. Set to
|
|
1148
|
+
`false` then `true` before Vue flushed, `open` never changed as far as the
|
|
1149
|
+
watch could see, and the copy stayed `false`.
|
|
1150
|
+
**Fix:** update: a dialog whose parent listens to `update:open` now shows
|
|
1151
|
+
exactly what `open` says. No `:key` needed; one dialog whose `title` follows
|
|
1152
|
+
the step is enough:
|
|
1153
|
+
|
|
1154
|
+
```vue
|
|
1155
|
+
<ConfirmationDialog
|
|
1156
|
+
v-model:open="open"
|
|
1157
|
+
:title="steps[step].title"
|
|
1158
|
+
@confirm="step === 0 ? (step = 1, (open = true)) : finish()"
|
|
1159
|
+
/>
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
Reopened a little later (after an `await`), it already worked, and no overlay
|
|
1163
|
+
is left above the content: reka keeps the overlay and the content in one
|
|
1164
|
+
teleport, in order.
|
|
1165
|
+
|
|
1166
|
+
### The first Escape after closing a nested dialog does nothing
|
|
1167
|
+
|
|
1168
|
+
**When:** an `AlertDialog` (a `ConfirmationDialog`, a `Dialog`, a `Sheet` or a
|
|
1169
|
+
`Drawer`) opened over another overlay — a `Sheet` with a Delete that asks for
|
|
1170
|
+
confirmation — is closed with Escape or Cancel, and the user presses Escape
|
|
1171
|
+
again at once to close the `Sheet` too. The `Sheet` stays open; a second
|
|
1172
|
+
Escape, a moment later, closes it. A test that presses Escape twice in a row
|
|
1173
|
+
fails the same way.
|
|
1174
|
+
**Why:** reka hands Escape to the highest overlay only, and a dialog stops
|
|
1175
|
+
being one when its content leaves the page, not when it closes (with
|
|
1176
|
+
`unmountOnHide`, its default). The closed alert stays on the page for its
|
|
1177
|
+
exit animation (200ms for a dialog, 300ms for a sheet, 500ms for a drawer):
|
|
1178
|
+
an Escape pressed meanwhile went to it, and it was already closed.
|
|
1179
|
+
**Fix:** update: `AlertDialogContent`, `DialogContent`, `SheetContent` and
|
|
1180
|
+
`DrawerContent` now press Escape again, for the overlay below, once a
|
|
1181
|
+
content that was closing has left. Nothing to change in your code. Your
|
|
1182
|
+
`@escape-key-down` still runs first, on both presses; preventing the default
|
|
1183
|
+
while yours is closing keeps the key from being forwarded:
|
|
1184
|
+
|
|
1185
|
+
```vue
|
|
1186
|
+
<AlertDialog v-model:open="confirm">
|
|
1187
|
+
<!-- closing: the Sheet stays open -->
|
|
1188
|
+
<AlertDialogContent @escape-key-down="(event) => !confirm && event.preventDefault()">
|
|
1189
|
+
…
|
|
1190
|
+
</AlertDialogContent>
|
|
1191
|
+
</AlertDialog>
|
|
1192
|
+
```
|
|
1193
|
+
|
|
1194
|
+
With no overlay below, the forwarded Escape reaches the page's own `keydown`
|
|
1195
|
+
handlers; an app shortcut that should ignore it can check `event.isTrusted` or
|
|
1196
|
+
`event.defaultPrevented`.
|
|
1197
|
+
|
|
1198
|
+
In a test, wait for the `Sheet` to leave the DOM (the forwarded Escape lands
|
|
1199
|
+
a few frames after the alert has gone) and look it up by selector rather
|
|
1200
|
+
than by role: the closing alert still hides the page from the accessibility
|
|
1201
|
+
tree.
|
|
1202
|
+
|
|
1203
|
+
## Navigation
|
|
1204
|
+
|
|
1205
|
+
### A `ListTile` link reloads the whole page
|
|
1206
|
+
|
|
1207
|
+
**When:** a `ListTile` with an `href` is clicked inside a router app: the
|
|
1208
|
+
browser loads the document again, the page flashes, and client state is lost.
|
|
1209
|
+
**Why:** by default the tile renders a plain `<a href>`, which needs no router
|
|
1210
|
+
and so cannot navigate on the client.
|
|
1211
|
+
**Fix:** pass `link-component`, with the same contract as the app shell's: a
|
|
1212
|
+
component that takes `href` and lets the other attributes fall through to its
|
|
1213
|
+
root. `RouterLink` takes `to`, so wrap it:
|
|
1214
|
+
|
|
1215
|
+
```ts
|
|
1216
|
+
// app-link.ts
|
|
1217
|
+
import { defineComponent, h } from 'vue';
|
|
1218
|
+
import { RouterLink } from 'vue-router';
|
|
1219
|
+
|
|
1220
|
+
export const AppLink = defineComponent({
|
|
1221
|
+
props: { href: { type: String, required: true } },
|
|
1222
|
+
setup:
|
|
1223
|
+
(props, { slots, attrs }) =>
|
|
1224
|
+
() =>
|
|
1225
|
+
h(RouterLink, { ...attrs, to: props.href }, slots),
|
|
1226
|
+
});
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
```vue
|
|
1230
|
+
<ListTile title="Notifications" href="/settings/notifications" :link-component="AppLink" />
|
|
1231
|
+
```
|
|
1232
|
+
|
|
1233
|
+
### The phone menu needs two Escapes to close, or a tooltip shows over it
|
|
1234
|
+
|
|
1235
|
+
**When:** on a phone, the menu sheet of `ActivityLayout` or `AppHeader` opens
|
|
1236
|
+
with a tooltip repeating the first entry's label over the menu, and a plain
|
|
1237
|
+
open sheet needs two Escapes to close: the first one closes the tooltip.
|
|
1238
|
+
**Why:** the entries of the sheet's expanded sidebar carried a tooltip, and
|
|
1239
|
+
focus lands on the first entry as the sheet opens, which opened its tooltip;
|
|
1240
|
+
the tooltip's own layer takes the first Escape.
|
|
1241
|
+
**Fix:** update `@nxgt/material-vue`. An entry of an expanded `Sidebar` now shows no
|
|
1242
|
+
tooltip, since it shows its label; a collapsed one keeps it, and an explicit
|
|
1243
|
+
`tooltip` is always shown. A `SidebarMenuItem` of your own needs no change.
|
|
1244
|
+
|
|
633
1245
|
## SSR & build
|
|
634
1246
|
|
|
635
1247
|
### `ReferenceError: window is not defined`
|
|
@@ -954,6 +1566,61 @@ const { value, handleBlur } = useField<string>('nickname');
|
|
|
954
1566
|
What counts as leaving, per kind of field:
|
|
955
1567
|
[forms](guides/forms.md#touched-on-blur).
|
|
956
1568
|
|
|
1569
|
+
### A failed submit does not focus the first invalid field
|
|
1570
|
+
|
|
1571
|
+
**When:** `handleSubmit` fails validation (or you call `setFieldError`) and
|
|
1572
|
+
focus stays on the submit button, with the error out of view. There is no
|
|
1573
|
+
`shouldFocusError` option to turn on.
|
|
1574
|
+
**Why:** vee-validate never moves focus: `handleSubmit(onValid, onInvalid)`
|
|
1575
|
+
only reports `{ errors }`. (React Hook Form focuses by itself; that is a
|
|
1576
|
+
React-only behaviour.)
|
|
1577
|
+
**Fix:** from the release that lists this fix in its changelog, every
|
|
1578
|
+
`*FormField` marks its element with
|
|
1579
|
+
`data-field-name="<name>"`, and `focusFirstInvalid` / `focusField` from
|
|
1580
|
+
`@nxgt/material-vue/lib` use it to focus the first control a user can reach in
|
|
1581
|
+
the field, whatever its shape (an input, a select trigger, the first chip, a
|
|
1582
|
+
file picker's button):
|
|
1583
|
+
|
|
1584
|
+
```vue
|
|
1585
|
+
<script setup lang="ts">
|
|
1586
|
+
import { focusField, focusFirstInvalid } from '@nxgt/material-vue/lib';
|
|
1587
|
+
import { useForm } from 'vee-validate';
|
|
1588
|
+
|
|
1589
|
+
const { handleSubmit, setFieldError } = useForm();
|
|
1590
|
+
const onSubmit = handleSubmit(save, ({ errors }) => focusFirstInvalid(errors));
|
|
1591
|
+
|
|
1592
|
+
// A server error on one field:
|
|
1593
|
+
// setFieldError('email', 'Already used'); focusField('email');
|
|
1594
|
+
</script>
|
|
1595
|
+
```
|
|
1596
|
+
|
|
1597
|
+
`focusFirstInvalid(errors)` takes the first field **in page order** that has
|
|
1598
|
+
an error, not the first key of `errors`. Pass a `scope` element as the last
|
|
1599
|
+
argument to look inside one form of a page that has several. A field that is
|
|
1600
|
+
not mounted (a closed tab) cannot take focus: the function returns `false`.
|
|
1601
|
+
|
|
1602
|
+
### A test that finds a chip by `aria-selected` or `getByRole('option')` no longer finds it
|
|
1603
|
+
|
|
1604
|
+
**When:** after upgrading past the release that lists this fix, a test (or
|
|
1605
|
+
an end-to-end locator) that picked a `SelectChipField` chip with
|
|
1606
|
+
`[aria-selected="true"]`, `getByRole('option', { name })` or
|
|
1607
|
+
`toHaveAttribute('aria-selected', 'true')` finds nothing, and the same for
|
|
1608
|
+
`SelectChipFormField`.
|
|
1609
|
+
**Why:** the chips are toggle buttons: `aria-selected` is not allowed on a
|
|
1610
|
+
button, so a chosen chip is now `aria-pressed="true"`, in `single` mode too
|
|
1611
|
+
(pressing the chosen chip clears it). The chips sit in a `role="group"`
|
|
1612
|
+
named by the label and described by the help text. `SelectCardField` is
|
|
1613
|
+
unchanged.
|
|
1614
|
+
**Fix:** query the group, then the chip as a button:
|
|
1615
|
+
|
|
1616
|
+
```ts
|
|
1617
|
+
import { screen, within } from '@testing-library/vue';
|
|
1618
|
+
|
|
1619
|
+
const topics = screen.getByRole('group', { name: 'Topics' });
|
|
1620
|
+
const design = within(topics).getByRole('button', { name: 'Design' });
|
|
1621
|
+
expect(design).toHaveAttribute('aria-pressed', 'true');
|
|
1622
|
+
```
|
|
1623
|
+
|
|
957
1624
|
### A multi-step form reopens at a later step
|
|
958
1625
|
|
|
959
1626
|
**When:** a wizard built on `defineStepFormStore` opens at the step (and with
|
|
@@ -1554,8 +2221,58 @@ z.config({ customError: zodLocaleError({ language: () => requestLanguage() }) })
|
|
|
1554
2221
|
|
|
1555
2222
|
See [helpers](guides/helpers.md#zodlocaleerror-limits).
|
|
1556
2223
|
|
|
2224
|
+
### A calendar's weekday row is in English on a French page
|
|
2225
|
+
|
|
2226
|
+
**When:** under `<I18nProvider>` set to French, a `DateField`,
|
|
2227
|
+
`DateRangeField` or `Calendar` shows "S", "M", "T"… and starts the week on
|
|
2228
|
+
Sunday.
|
|
2229
|
+
**Why:** before the calendar followed the provider's language, it took reka's
|
|
2230
|
+
default `en-US` locale, which shapes the weekday row and the first day of the
|
|
2231
|
+
week.
|
|
2232
|
+
**Fix:** update: a `Calendar`'s `locale` prop, else the `I18nProvider`
|
|
2233
|
+
language, else English drives the whole calendar, weekday row included ("L",
|
|
2234
|
+
"M"…, Monday first, each header labelled "lundi"…). `DateField` and
|
|
2235
|
+
`DateRangeField` follow the provider; to force another language on one
|
|
2236
|
+
calendar, pass `locale`:
|
|
2237
|
+
|
|
2238
|
+
```vue
|
|
2239
|
+
<Calendar v-model="date" locale="en" />
|
|
2240
|
+
```
|
|
2241
|
+
|
|
2242
|
+
reka hides the weekday row from assistive tech (`<thead aria-hidden>`); in a
|
|
2243
|
+
test, find a day by its full date instead
|
|
2244
|
+
(`getByRole('gridcell', { name: 'jeudi 1 octobre 2026' })`).
|
|
2245
|
+
|
|
1557
2246
|
## Runtime
|
|
1558
2247
|
|
|
2248
|
+
### A test asserting the old Zod string length text fails
|
|
2249
|
+
|
|
2250
|
+
**When:** after upgrading past the release that lists this fix, a test or a
|
|
2251
|
+
snapshot that asserted a string length message from `zodLocaleError` fails:
|
|
2252
|
+
it expected `This value is too small: it must be at least 3 characters.`
|
|
2253
|
+
(French `Cette valeur est trop petite : …`) and gets `Must contain at least 3
|
|
2254
|
+
characters.` (`Doit contenir au moins 3 caractères.`).
|
|
2255
|
+
**Why:** a string's `min` and `max` read as a length now, through
|
|
2256
|
+
the keys `material.zod.too_small_string_inclusive` / `_exclusive` and
|
|
2257
|
+
`material.zod.too_big_string_inclusive` / `_exclusive`, which pluralize the
|
|
2258
|
+
count: `z.string().min(1)` says `Must contain at least 1 character.` /
|
|
2259
|
+
`Doit contenir au moins 1 caractère.`. A number, a list, a set or a file
|
|
2260
|
+
keeps the "too large / too small" wording. A French `invalid_format` noun
|
|
2261
|
+
opens with a capital too (`Adresse e-mail invalide.`).
|
|
2262
|
+
**Fix:** update the expected text, or override the new keys in your app's
|
|
2263
|
+
`material.zod.*` resources to keep your own wording (an override of
|
|
2264
|
+
`too_small_inclusive` no longer applies to strings):
|
|
2265
|
+
|
|
2266
|
+
```json
|
|
2267
|
+
{
|
|
2268
|
+
"material": {
|
|
2269
|
+
"zod": {
|
|
2270
|
+
"too_small_string_inclusive": "At least {minimum, plural, one {# character} other {# characters}}, please."
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
```
|
|
2275
|
+
|
|
1559
2276
|
### `must be used within` / `must be used inside`
|
|
1560
2277
|
|
|
1561
2278
|
**When:** on render, from a part of a widget or a context composable used
|
|
@@ -1638,8 +2355,34 @@ try {
|
|
|
1638
2355
|
```
|
|
1639
2356
|
|
|
1640
2357
|
Serve the app over `https` (or `localhost`) in any environment where copying
|
|
1641
|
-
must work. `CopyButton` does not throw: it
|
|
1642
|
-
|
|
2358
|
+
must work. `CopyButton` does not throw: it awaits the same write and, in an insecure
|
|
2359
|
+
context or when the clipboard refuses it, shows "Copy failed" ([details](#a-test-no-longer-finds-copybutton-by-copied-or-its-name-changes)).
|
|
2360
|
+
|
|
2361
|
+
### A test no longer finds `CopyButton` by "Copied", or its name changes
|
|
2362
|
+
|
|
2363
|
+
**When:** a test clicks a `CopyButton` and looks for a button named
|
|
2364
|
+
`Copied`, or reads the tooltip, or the copy "succeeds" in a page where the
|
|
2365
|
+
clipboard is not available.
|
|
2366
|
+
**Why:** the button's accessible name is now always its `label` (`Copy`). The
|
|
2367
|
+
outcome is a visually hidden `role="status"` (polite) region next to the
|
|
2368
|
+
button, so a screen reader announces it without a rename. "Copied" shows only
|
|
2369
|
+
after the clipboard write resolved; a rejected write (permission denied, an
|
|
2370
|
+
insecure origin, no Clipboard API) shows "Copy failed" (`failed-label`,
|
|
2371
|
+
`material.copy.failed`).
|
|
2372
|
+
**Fix:** stub the clipboard and read the status next to the button. A bare
|
|
2373
|
+
`getByRole('status')` is ambiguous: every `CopyButton` renders one, and so do
|
|
2374
|
+
`Banner`, `DataTable` and the skeletons.
|
|
2375
|
+
|
|
2376
|
+
```ts
|
|
2377
|
+
Object.defineProperty(navigator, 'clipboard', {
|
|
2378
|
+
configurable: true,
|
|
2379
|
+
value: { writeText: vi.fn().mockResolvedValue(undefined) }, // or mockRejectedValue(new Error('denied'))
|
|
2380
|
+
});
|
|
2381
|
+
const button = screen.getByRole('button', { name: 'Copy' });
|
|
2382
|
+
await user.click(button);
|
|
2383
|
+
// the parent is the CopyButton's own root
|
|
2384
|
+
expect(await within(button.parentElement!).findByRole('status')).toHaveTextContent('Copied');
|
|
2385
|
+
```
|
|
1643
2386
|
|
|
1644
2387
|
### `Upload failed with status 403`
|
|
1645
2388
|
|
|
@@ -1969,7 +2712,9 @@ await expect(volume).toHaveAttribute('aria-invalid', 'true');
|
|
|
1969
2712
|
**When:** with 0.21.0 or earlier, an `AlertDialogContent` whose
|
|
1970
2713
|
`@open-auto-focus` calls `event.preventDefault()` still moves focus to its
|
|
1971
2714
|
`AlertDialogCancel` a moment after opening, where `DialogContent`,
|
|
1972
|
-
`SheetContent` and `DrawerContent`
|
|
2715
|
+
`SheetContent` and `DrawerContent` leave it to you (where focus then goes
|
|
2716
|
+
behind a modal layer: [Focus moves into a dialog although
|
|
2717
|
+
`@open-auto-focus` prevented it](#focus-moves-into-a-dialog-although-open-auto-focus-prevented-it)).
|
|
1973
2718
|
**Why:** reka focuses the Cancel a tick after the alert dialog opens without
|
|
1974
2719
|
checking whether the event was prevented, and the dialog's focus trap then
|
|
1975
2720
|
keeps focus inside.
|
|
@@ -1986,6 +2731,29 @@ takes no focus until the next task: focus it from a `setTimeout` if you want it.
|
|
|
1986
2731
|
<AlertDialogContent @open-auto-focus="(event) => event.preventDefault()">
|
|
1987
2732
|
```
|
|
1988
2733
|
|
|
2734
|
+
### Focus moves into a dialog although `@open-auto-focus` prevented it
|
|
2735
|
+
|
|
2736
|
+
**When:** a modal `Dialog`, `Sheet`, `Drawer` or `AlertDialog` whose
|
|
2737
|
+
`@open-auto-focus` calls `event.preventDefault()` and focuses nothing: focus
|
|
2738
|
+
lands on the layer's content, not on the element that had it before it
|
|
2739
|
+
opened.
|
|
2740
|
+
**Why:** the page behind a modal layer is `inert`, and an inert element
|
|
2741
|
+
cannot keep focus: the browser would drop it on `<body>`, where a keyboard
|
|
2742
|
+
or screen-reader user is nowhere. The layer takes it instead (its content
|
|
2743
|
+
has `tabindex="-1"`); closing still hands focus back to the element that
|
|
2744
|
+
opened it.
|
|
2745
|
+
**Fix:** focus what you want inside the layer in the handler; to keep
|
|
2746
|
+
focus on the page behind, make the layer non-modal (`:modal="false"` on
|
|
2747
|
+
`Dialog`, `Sheet` or `Drawer`):
|
|
2748
|
+
|
|
2749
|
+
```vue
|
|
2750
|
+
<DialogContent
|
|
2751
|
+
@open-auto-focus="(event) => { event.preventDefault(); nameInput?.focus(); }"
|
|
2752
|
+
>
|
|
2753
|
+
<input ref="nameInput" aria-label="Name" />
|
|
2754
|
+
</DialogContent>
|
|
2755
|
+
```
|
|
2756
|
+
|
|
1989
2757
|
### A `CustomDialog` or `ConfirmationDialog` no longer opens from its `#trigger`
|
|
1990
2758
|
|
|
1991
2759
|
**When:** after upgrading to 0.22.0, clicking what you put in the `#trigger`
|
|
@@ -2101,6 +2869,162 @@ expect(onChange).toHaveBeenLastCalledWith(new Date(2026, 0, 1, 21, 30)); // from
|
|
|
2101
2869
|
await userEvent.type(canvas.getByRole('spinbutton', { name: 'Hours' }), '10');
|
|
2102
2870
|
```
|
|
2103
2871
|
|
|
2872
|
+
### A link or button in the first column of a `DataTable` with `getRowHref`
|
|
2873
|
+
|
|
2874
|
+
**When:** a `DataTable` has `getRowHref`, and the cell of its first data column (the first visible column with an accessor; without one, the first that is not `select` or `expand`) renders a link or a button of its own.
|
|
2875
|
+
**Why:** with `getRowHref` that cell's content is wrapped in the row's link, so a link or button inside it is interactive content nested in a link: a nested `<a>` is invalid HTML, so server and client markup can disagree (a hydration warning in an SSR app), and a click or Enter lands on the inner control or the outer link depending on the target. The row's click handling skips clicks on any control, so the row does not follow its link from there either.
|
|
2876
|
+
**Fix:** keep that column plain, and put the control in another column:
|
|
2877
|
+
|
|
2878
|
+
```vue
|
|
2879
|
+
<DataTable
|
|
2880
|
+
manual
|
|
2881
|
+
:data="rows"
|
|
2882
|
+
:columns="columns"
|
|
2883
|
+
:get-row-href="(student) => `/students/${student.id}`"
|
|
2884
|
+
:link-component="RouterLink"
|
|
2885
|
+
/>
|
|
2886
|
+
```
|
|
2887
|
+
```ts
|
|
2888
|
+
// columns: the first accessor column renders plain content
|
|
2889
|
+
columnHelper.accessor('name', { header: 'Name' }),
|
|
2890
|
+
// the control goes in a column of its own, after it
|
|
2891
|
+
columnHelper.display({
|
|
2892
|
+
id: 'open',
|
|
2893
|
+
cell: ({ row }) => h(Button, { onClick: () => openSheet(row.original) }, () => 'Quick view'),
|
|
2894
|
+
}),
|
|
2895
|
+
```
|
|
2896
|
+
|
|
2897
|
+
Anything else that must not make the row follow its link on a click (a custom widget that is not a button, link or field) can carry `data-row-click-ignore`.
|
|
2898
|
+
|
|
2899
|
+
### A row does not navigate after `@row-click` ran
|
|
2900
|
+
|
|
2901
|
+
**When:** a `DataTable` has both `getRowHref` and `@row-click`; the handler runs, and a mouse click does not go to the row's page.
|
|
2902
|
+
**Why:** the handler runs first, and the row follows its link only if the click's `defaultPrevented` is still false after it. A handler that calls `event.preventDefault()` therefore stops the navigation by design, which is how a handler cancels it (for a row that opens a sheet instead). Ctrl/Cmd+click on the link, and Enter on it, do not go through the row handler.
|
|
2903
|
+
**Fix:** do not call `preventDefault()` in the handler when the row should still navigate:
|
|
2904
|
+
|
|
2905
|
+
```vue
|
|
2906
|
+
<DataTable
|
|
2907
|
+
manual
|
|
2908
|
+
:data="rows"
|
|
2909
|
+
:columns="columns"
|
|
2910
|
+
:get-row-href="(row) => `/students/${row.id}`"
|
|
2911
|
+
@row-click="(row, event) => openSheet(row)"
|
|
2912
|
+
/>
|
|
2913
|
+
```
|
|
2914
|
+
|
|
2915
|
+
### A `DataTable` Previous or Next button is disabled
|
|
2916
|
+
|
|
2917
|
+
**When:** a server-mode `DataTable` (`manual`) renders its footer with Previous or Next greyed out, though the server has more pages.
|
|
2918
|
+
**Why:** each button is enabled only when `pagination.previous` / `pagination.next` is an href string or a function, and the table is not `loading`. An absent or `null` value gives a disabled button, and both buttons are disabled while `loading` is true, so a `loading` that stays on (a flag never reset after the navigation ended) leaves them disabled. A `pagination` that is not passed renders no buttons at all.
|
|
2919
|
+
**Fix:** pass a target on each side that has a page, and turn `loading` off when the page has arrived:
|
|
2920
|
+
|
|
2921
|
+
```vue
|
|
2922
|
+
<DataTable
|
|
2923
|
+
manual
|
|
2924
|
+
:data="rows"
|
|
2925
|
+
:columns="columns"
|
|
2926
|
+
:loading="navigating"
|
|
2927
|
+
:pagination="{ previous: hasPrevious ? previousHref : null, next: hasNext ? nextHref : null, totalCount, offset }"
|
|
2928
|
+
/>
|
|
2929
|
+
```
|
|
2930
|
+
|
|
2931
|
+
### A double click or Enter submits a form twice, or a test no longer finds a loading `Button` disabled
|
|
2932
|
+
|
|
2933
|
+
**When:** a sign-in or save form is submitted twice by a quick double click,
|
|
2934
|
+
or by Enter pressed twice in a field, although its submit button has
|
|
2935
|
+
`loading`; or `<Button as-child loading><a @click="save">` still calls
|
|
2936
|
+
`save`. Or, after upgrading, a test expecting a loading `Button` to be
|
|
2937
|
+
`disabled` (or to skip it with Tab) fails.
|
|
2938
|
+
**Why:** a `loading` that turns true only after an `await` (a mutation's
|
|
2939
|
+
pending flag behind an async check) left a window for the second click. And a
|
|
2940
|
+
loading `Button` used to set the native `disabled`, which drops focus to
|
|
2941
|
+
`<body>` while the form works. It now sets `aria-busy="true"` and
|
|
2942
|
+
`aria-disabled="true"` and cancels every click before your `@click` — the
|
|
2943
|
+
click Enter in a field sends to the form's submit button included — so
|
|
2944
|
+
neither your handler, an `as-child` child's nor an ancestor's runs, and stays
|
|
2945
|
+
focusable, as `@nxgt/material`'s does. With `as-child` the busy attributes,
|
|
2946
|
+
the click guard and the spinner land on the child element: the spinner goes
|
|
2947
|
+
inside it, and the child stays the same element (focus, your template ref)
|
|
2948
|
+
while `loading` turns on and off.
|
|
2949
|
+
**Fix:** pass `useForm`'s `isSubmitting`, which is true as soon as
|
|
2950
|
+
`handleSubmit` starts; for a `loading` that turns true later, add
|
|
2951
|
+
`lock-on-click`. In a test, check the busy state, not `disabled`:
|
|
2952
|
+
|
|
2953
|
+
```vue
|
|
2954
|
+
<Button type="submit" :loading="isSubmitting">Sign in</Button>
|
|
2955
|
+
<Button type="submit" :loading="isPending" lock-on-click>Sign in</Button>
|
|
2956
|
+
```
|
|
2957
|
+
|
|
2958
|
+
```ts
|
|
2959
|
+
expect(screen.getByRole('button', { name: 'Sign in' })).toHaveAttribute('aria-busy', 'true');
|
|
2960
|
+
```
|
|
2961
|
+
|
|
2962
|
+
A `form.requestSubmit()` of your own fires no click, so the guard does not
|
|
2963
|
+
see it: check `isSubmitting` before calling it (see
|
|
2964
|
+
[Forms](./guides/forms.md)).
|
|
2965
|
+
|
|
2966
|
+
### A `DataTable` shows a sorting or a selection the URL never got
|
|
2967
|
+
|
|
2968
|
+
**When:** a `DataTable` has `:sorting` (or `:row-selection`) bound to state you derive from the URL, with no `@update:sorting` (or `@update:row-selection`), and clicking a header changes the arrow while the rows and the URL stay as they were.
|
|
2969
|
+
**Why:** `sorting` and `rowSelection` are `defineModel`s. Vue keeps a local copy and applies a change to it unless the parent binds both the prop and its `update:` listener, so a one-way `:sorting` is a starting value the table then overrides, until the prop changes.
|
|
2970
|
+
**Fix:** bind both halves, and write the new value where the data comes from:
|
|
2971
|
+
|
|
2972
|
+
```vue
|
|
2973
|
+
<DataTable
|
|
2974
|
+
manual
|
|
2975
|
+
:data="rows"
|
|
2976
|
+
:columns="columns"
|
|
2977
|
+
:sorting="sorting"
|
|
2978
|
+
@update:sorting="(next) => router.push({ query: { ...route.query, sort: toQuery(next) } })"
|
|
2979
|
+
v-model:row-selection="rowSelection"
|
|
2980
|
+
/>
|
|
2981
|
+
```
|
|
2982
|
+
|
|
2983
|
+
### A `DataTable` no longer scrolls inside a 400px box
|
|
2984
|
+
|
|
2985
|
+
**When:** after upgrading, a long `DataTable` is as tall as its rows and the page scrolls, where its rows used to scroll inside a box about 400px tall.
|
|
2986
|
+
**Why:** the table capped its own height (`max-h-[400px]` on `[data-slot="table-wrapper"]`, and viewport-based caps on it and on `[data-slot="data-table"]` in the stylesheet). On a phone that made a scroll inside the page's scroll, so the cap is now opt-in.
|
|
2987
|
+
**Fix:** pass `max-height` (a number of pixels, or any CSS length) where the table should scroll inside a box:
|
|
2988
|
+
|
|
2989
|
+
```vue
|
|
2990
|
+
<DataTable :data="rows" :columns="columns" :max-height="400" />
|
|
2991
|
+
```
|
|
2992
|
+
|
|
2993
|
+
`max-height` is the supported way: it also keeps the header row in view while the rows scroll. A `max-height` rule of your own on `[data-slot="table-wrapper"]` still works, and the box still becomes the named, focusable scroll region once it overflows.
|
|
2994
|
+
|
|
2995
|
+
### A `DataTable` shows its header row on a phone
|
|
2996
|
+
|
|
2997
|
+
**When:** after upgrading, a `DataTable` under 768px wide shows its header row, which it used to hide.
|
|
2998
|
+
**Why:** without the header a phone lost the column names, sorting and "Select all", so the header now shows at every width. The table scrolls sideways, in its own region, when the columns do not fit.
|
|
2999
|
+
**Fix:** nothing, usually. To hide it again, pass `show-header` — `false` everywhere, or your own breakpoint:
|
|
3000
|
+
|
|
3001
|
+
```vue
|
|
3002
|
+
<DataTable :data="rows" :columns="columns" :show-header="false" />
|
|
3003
|
+
<DataTable :data="rows" :columns="columns" :show-header="!isPhone" />
|
|
3004
|
+
```
|
|
3005
|
+
|
|
3006
|
+
### A test's `getByRole('region')` finds more than one region
|
|
3007
|
+
|
|
3008
|
+
**When:** a test or an axe check runs on a page with a `DataTable` wider than its box, and a query for the `region` role now also finds the table.
|
|
3009
|
+
**Why:** while its content overflows, the table's scroll box (`[data-slot="table-wrapper"]`) is a keyboard-focusable `region` named "Scrollable table" (`material.table.labels.scroll-region`), so a keyboard user can scroll it (axe `scrollable-region-focusable`). It is not a region while it fits, nor on the server.
|
|
3010
|
+
**Fix:** query your region by name, and give the table a name of its own where a page holds several:
|
|
3011
|
+
|
|
3012
|
+
```vue
|
|
3013
|
+
<DataTable :data="rows" :columns="columns" scroll-label="Invoices" />
|
|
3014
|
+
```
|
|
3015
|
+
|
|
3016
|
+
```ts
|
|
3017
|
+
page.getByRole('region', { name: 'Invoices' });
|
|
3018
|
+
```
|
|
3019
|
+
|
|
3020
|
+
### A `DataTable` toolbar is gone
|
|
3021
|
+
|
|
3022
|
+
**When:** a `DataTable` is given `actions` or an `#actions` slot that renders nothing (a `v-if` left out, `null`), and a test looked for the toolbar's `fieldset` (`[data-slot="header"]`).
|
|
3023
|
+
**Why:** the toolbar is rendered only when it has content (yours, or `show-view-options`): an empty `<fieldset>` is a nameless group to a screen reader.
|
|
3024
|
+
**Fix:** look for what the toolbar holds (a filter by its label, the view options by "View"), not for the `fieldset` itself. A component of yours that renders nothing still counts as content: render it conditionally instead.
|
|
3025
|
+
|
|
3026
|
+
---
|
|
3027
|
+
|
|
2104
3028
|
---
|
|
2105
3029
|
|
|
2106
3030
|
## PDF reader
|