@nxgt/material-vue 0.26.1 → 0.28.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.
Files changed (168) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/README.md +10 -7
  3. package/dist/Button-D1uzdbOJ.js +728 -0
  4. package/dist/{DropdownMenuTrigger-BHAhXGUO.js → DropdownMenuTrigger-BsuyNVzV.js} +48 -41
  5. package/dist/HoverCardTrigger-DaM5d4hE.js +510 -0
  6. package/dist/{camera-DvKuqT4L.js → camera-CxXijySa.js} +4 -4
  7. package/dist/components/base-content.css +139 -0
  8. package/dist/components/base.css +150 -0
  9. package/dist/components/components.css +2 -0
  10. package/dist/components/reduced-motion.css +56 -0
  11. package/dist/components/theme-tokens.css +116 -0
  12. package/dist/components/ui/actions/button-group/button-group.css +1 -1
  13. package/dist/components/ui/feedback/toast/toast.css +21 -0
  14. package/dist/components/ui/primitives/label/label.css +33 -0
  15. package/dist/{components-CCWod7cJ.js → components-Db9OdMfQ.js} +19053 -18364
  16. package/dist/components.js +7 -7
  17. package/dist/composables.js +3 -3
  18. package/dist/dnd.js +1 -1
  19. package/dist/field-name-attribute-Bl7LSVK0.js +4 -0
  20. package/dist/{form-scope-x-7TYn2s.js → form-scope-BILi3ke-.js} +1 -1
  21. package/dist/{i18n-provider-CaaZCmaR.js → i18n-provider-BPK68eqM.js} +1 -1
  22. package/dist/i18n.js +4 -4
  23. package/dist/icon-button-D60G5B_Z.js +262 -0
  24. package/dist/lib/components/ui/actions/button/Button.vue.d.ts +22 -4
  25. package/dist/lib/components/ui/actions/button/button-tones.d.ts +36 -0
  26. package/dist/lib/components/ui/actions/button/button.variants.d.ts +1 -1
  27. package/dist/lib/components/ui/actions/button/button.wrap.d.ts +8 -0
  28. package/dist/lib/components/ui/actions/button/label-text.d.ts +9 -0
  29. package/dist/lib/components/ui/actions/button/prepend-to-child.d.ts +18 -0
  30. package/dist/lib/components/ui/actions/button/use-click-lock.d.ts +14 -0
  31. package/dist/lib/components/ui/actions/copy-button/copy-button.vue.d.ts +6 -2
  32. package/dist/lib/components/ui/actions/icon-button/icon-button.compound-variants.d.ts +28 -0
  33. package/dist/lib/components/ui/actions/icon-button/icon-button.vue.d.ts +1 -1
  34. package/dist/lib/components/ui/actions/responsive-button/responsive-button.vue.d.ts +1 -3
  35. package/dist/lib/components/ui/collections/data-table/partials/data-table-empty.vue.d.ts +5 -1
  36. package/dist/lib/components/ui/collections/data-table/partials/data-table-scroll-region.vue.d.ts +28 -0
  37. package/dist/lib/components/ui/collections/data-table/partials/data-table-toolbar.d.ts +60 -0
  38. package/dist/lib/components/ui/collections/data-table/types.d.ts +9 -0
  39. package/dist/lib/components/ui/collections/data-table/use-scroll-edges.d.ts +22 -0
  40. package/dist/lib/components/ui/collections/pagination/PaginationFirst.vue.d.ts +1 -1
  41. package/dist/lib/components/ui/collections/pagination/PaginationItem.vue.d.ts +1 -1
  42. package/dist/lib/components/ui/collections/pagination/PaginationLast.vue.d.ts +1 -1
  43. package/dist/lib/components/ui/collections/pagination/PaginationNext.vue.d.ts +1 -1
  44. package/dist/lib/components/ui/collections/pagination/PaginationPrevious.vue.d.ts +1 -1
  45. package/dist/lib/components/ui/dates/calendar/Calendar.vue.d.ts +4 -4
  46. package/dist/lib/components/ui/dates/calendar/calendar-day-label.d.ts +17 -0
  47. package/dist/lib/components/ui/dates/calendar/calendar-years.d.ts +11 -0
  48. package/dist/lib/components/ui/dates/calendar/partials/calendar-month-select.vue.d.ts +18 -0
  49. package/dist/lib/components/ui/dates/calendar/partials/calendar-year-select.vue.d.ts +19 -0
  50. package/dist/lib/components/ui/dates/calendar/use-calendar-locale.d.ts +7 -0
  51. package/dist/lib/components/ui/dates/calendar/use-calendar-selected-hint.d.ts +17 -0
  52. package/dist/lib/components/ui/dates/date-range-field/partials/date-range-day.vue.d.ts +8 -0
  53. package/dist/lib/components/ui/display/cards/cards.gallery.d.ts +3 -3
  54. package/dist/lib/components/ui/display/list-tile/list-tile.vue.d.ts +10 -6
  55. package/dist/lib/components/ui/feedback/alert-dialog/AlertDialogContent.vue.d.ts +2 -2
  56. package/dist/lib/components/ui/feedback/sonner/index.d.ts +1 -0
  57. package/dist/lib/components/ui/feedback/toast/toast.d.ts +7 -0
  58. package/dist/lib/components/ui/feedback/toast/toast.vue.d.ts +18 -2
  59. package/dist/lib/components/ui/fields/contacts/consts.d.ts +1 -1
  60. package/dist/lib/components/ui/forms/use-field-blur.d.ts +3 -0
  61. package/dist/lib/components/ui/layout/app-shell/activity-content.vue.d.ts +9 -3
  62. package/dist/lib/components/ui/layout/app-shell/activity-layout.vue.d.ts +21 -4
  63. package/dist/lib/components/ui/layout/app-shell/app-header.vue.d.ts +7 -5
  64. package/dist/lib/components/ui/layout/app-shell/app-sidebar.vue.d.ts +4 -1
  65. package/dist/lib/components/ui/layout/app-shell/index.d.ts +2 -2
  66. package/dist/lib/components/ui/layout/app-shell/router-menu-link.d.ts +7 -0
  67. package/dist/lib/components/ui/layout/app-shell/types.d.ts +8 -0
  68. package/dist/lib/components/ui/layout/app-shell/use-content-scroll.d.ts +13 -0
  69. package/dist/lib/components/ui/layout/app-shell/use-navigation-sheet.d.ts +17 -0
  70. package/dist/lib/components/ui/layout/sidebar/sidebar-context.d.ts +15 -0
  71. package/dist/lib/components/ui/layout/sidebar/sidebar-menu-item.vue.d.ts +13 -4
  72. package/dist/lib/components/ui/media/carousel/carousel-next.vue.d.ts +1 -1
  73. package/dist/lib/components/ui/media/carousel/carousel-previous.vue.d.ts +1 -1
  74. package/dist/lib/components/ui/menu-item-variant.d.ts +4 -4
  75. package/dist/lib/components/ui/overlays/context-menu/ContextMenuContent.vue.d.ts +2 -2
  76. package/dist/lib/components/ui/overlays/dialog/DialogContent.vue.d.ts +2 -2
  77. package/dist/lib/components/ui/overlays/dialog/DialogScrollContent.vue.d.ts +2 -2
  78. package/dist/lib/components/ui/overlays/dialog/use-escape-forward.d.ts +17 -0
  79. package/dist/lib/components/ui/overlays/dialog/use-inert-others.d.ts +17 -0
  80. package/dist/lib/components/ui/overlays/dialog/use-modal-content.d.ts +32 -0
  81. package/dist/lib/components/ui/overlays/drawer/DrawerContent.vue.d.ts +2 -2
  82. package/dist/lib/components/ui/overlays/dropdown-menu/DropdownMenu.vue.d.ts +3 -1
  83. package/dist/lib/components/ui/overlays/dropdown-menu/DropdownMenuContent.vue.d.ts +2 -2
  84. package/dist/lib/components/ui/overlays/popover/PopoverContent.vue.d.ts +2 -2
  85. package/dist/lib/components/ui/overlays/sheet/SheetContent.vue.d.ts +2 -2
  86. package/dist/lib/components/ui/primitives/card/CardTitle.vue.d.ts +11 -4
  87. package/dist/lib/components/ui/primitives/card/index.d.ts +1 -0
  88. package/dist/lib/components/ui/primitives/tabs/Tabs.vue.d.ts +6 -2
  89. package/dist/lib/components/ui/primitives/tabs/TabsList.vue.d.ts +2 -2
  90. package/dist/lib/components/ui/primitives/tabs/TabsTrigger.vue.d.ts +6 -4
  91. package/dist/lib/components/ui/primitives/tabs/tabs-classes.d.ts +27 -0
  92. package/dist/lib/components/ui/primitives/tabs/tabs-indicator.vue.d.ts +25 -0
  93. package/dist/lib/components/ui/primitives/tabs/tabs-variant.d.ts +5 -8
  94. package/dist/lib/components/ui/primitives/toggle/Toggle.vue.d.ts +1 -1
  95. package/dist/lib/components/ui/primitives/toggle/index.d.ts +1 -1
  96. package/dist/lib/components/ui/selection/autocomplete/partials/autocomplete-box.vue.d.ts +2 -0
  97. package/dist/lib/components/ui/selection/select/SelectContent.vue.d.ts +2 -2
  98. package/dist/lib/components/ui/selection/select-chip-field/select-chip-field.vue.d.ts +1 -0
  99. package/dist/lib/components/ui/widgets/emoji/types.d.ts +1 -6
  100. package/dist/lib/i18n/resources/en/en.d.ts +23 -0
  101. package/dist/lib/i18n/resources/en/material.json.d.ts +28 -5
  102. package/dist/lib/i18n/resources/fr/fr.d.ts +23 -0
  103. package/dist/lib/i18n/resources/fr/material.json.d.ts +28 -5
  104. package/dist/lib/i18n/resources/resources.d.ts +46 -0
  105. package/dist/lib/i18n/translator.d.ts +1 -1
  106. package/dist/lib/lib/field-name-attribute.d.ts +7 -0
  107. package/dist/lib/lib/focus-field.d.ts +17 -0
  108. package/dist/lib/lib/index.d.ts +1 -0
  109. package/dist/lib.js +76 -74
  110. package/dist/main.js +13 -13
  111. package/dist/map.js +4 -4
  112. package/dist/material-vue.css +1 -1
  113. package/dist/{pdf-outline-sidebar-D3vq2qfS.js → pdf-outline-sidebar-BXGTDISI.js} +2 -2
  114. package/dist/{pdf-page-BrJbyjQj.js → pdf-page-BR4G03kN.js} +1 -1
  115. package/dist/{pdf-page-list-BCgYEszV.js → pdf-page-list-bjQm6_kf.js} +2 -2
  116. package/dist/{pdf-reader-BAzevSJU.js → pdf-reader-DGVEFNc0.js} +9 -9
  117. package/dist/{pdf-sidebar-DSh2Mend.js → pdf-sidebar-CiM6dwAm.js} +3 -3
  118. package/dist/{pdf-thumbnail-sidebar-D_EMcbKF.js → pdf-thumbnail-sidebar-fM3DGJzD.js} +3 -3
  119. package/dist/{pdf-toolbar-BcL7HlfS.js → pdf-toolbar-mWlGTWU1.js} +5 -5
  120. package/dist/pdf.js +2 -2
  121. package/dist/{sortable-provider-DMpmM7Ke.js → sortable-provider-Dml11nQY.js} +2 -2
  122. package/dist/{stored-language-Bxhsx4iB.js → stored-language-UnInBB6T.js} +56 -10
  123. package/dist/{stx-player-MldK_ZAZ.js → stx-player-BS7-CXFx.js} +2 -2
  124. package/dist/styles.css +10 -368
  125. package/dist/{translator-CLI_7Crg.js → translator-J3_vaV_A.js} +1 -1
  126. package/dist/{use-heading-spy-VPuFj2Gt.js → use-heading-spy-_dk8kGwL.js} +19 -19
  127. package/dist/use-inert-others-CtGqZKR8.js +22 -0
  128. package/dist/{use-material-translation-CONqD9wJ.js → use-material-translation-Do6ztdtV.js} +3 -3
  129. package/docs/components/buttons-and-actions.md +71 -16
  130. package/docs/components/data-display.md +13 -2
  131. package/docs/components/data-table-filter-and-lists.md +85 -5
  132. package/docs/components/date-and-time.md +25 -1
  133. package/docs/components/feedback-and-status.md +86 -11
  134. package/docs/components/form-fields.md +13 -4
  135. package/docs/components/media-and-files.md +7 -0
  136. package/docs/components/navigation-and-layout.md +19 -1
  137. package/docs/components/overlays-and-menus.md +20 -4
  138. package/docs/components/primitives.md +97 -20
  139. package/docs/components/rich-widgets.md +10 -0
  140. package/docs/components/selection-and-choice.md +12 -1
  141. package/docs/getting-started.md +2 -2
  142. package/docs/guides/app-shell.md +102 -6
  143. package/docs/guides/forms.md +58 -5
  144. package/docs/guides/helpers.md +2 -1
  145. package/docs/guides/i18n.md +24 -4
  146. package/docs/guides/ssr.md +1 -1
  147. package/docs/guides/theming.md +148 -6
  148. package/docs/previews/data-display-codegroup.webp +0 -0
  149. package/docs/previews/data-display-description.webp +0 -0
  150. package/docs/previews/feedback-status-toast.webp +0 -0
  151. package/docs/previews/primitives-tabs.webp +0 -0
  152. package/docs/previews/rich-widgets-emojipicker.webp +0 -0
  153. package/docs/roadmap.md +30 -1
  154. package/docs/troubleshooting.md +889 -12
  155. package/lib/components/base-content.css +139 -0
  156. package/lib/components/base.css +150 -0
  157. package/lib/components/components.css +2 -0
  158. package/lib/components/reduced-motion.css +56 -0
  159. package/lib/components/theme-tokens.css +116 -0
  160. package/lib/components/ui/actions/button-group/button-group.css +1 -1
  161. package/lib/components/ui/feedback/toast/toast.css +21 -0
  162. package/lib/components/ui/primitives/label/label.css +33 -0
  163. package/lib/styles.css +10 -368
  164. package/package.json +2 -2
  165. package/dist/Button-16J7vTpE.js +0 -665
  166. package/dist/HoverCardTrigger-C41002JS.js +0 -455
  167. package/dist/icon-button-CDdNsZuZ.js +0 -227
  168. package/dist/lib/components/ui/widgets/emoji/emoji.groups.d.ts +0 -6
@@ -18,21 +18,52 @@ 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
+ - [`Tabs` look different after upgrading](#tabs-look-different-after-upgrading)
65
+ - [A screen reader announces an icon-only tab as "tab", with no name](#a-screen-reader-announces-an-icon-only-tab-as-tab-with-no-name)
66
+
36
67
  **SSR & build**
37
68
 
38
69
  - [`ReferenceError: window is not defined`](#referenceerror-window-is-not-defined)
@@ -48,6 +79,8 @@ words in your error.
48
79
  - [`injection "Symbol(route location)" not found`](#injection-symbolroute-location-not-found)
49
80
  - [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
81
  - [`meta.touched` stays false after leaving a field](#metatouched-stays-false-after-leaving-a-field)
82
+ - [A failed submit does not focus the first invalid field](#a-failed-submit-does-not-focus-the-first-invalid-field)
83
+ - [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
84
  - [A multi-step form reopens at a later step](#a-multi-step-form-reopens-at-a-later-step)
52
85
  - [The form could not be loaded.](#the-form-could-not-be-loaded)
53
86
 
@@ -92,11 +125,14 @@ words in your error.
92
125
  - [A translation key is rendered instead of its text](#a-translation-key-is-rendered-instead-of-its-text)
93
126
  - [A label shows `{count}` or another placeholder](#a-label-shows-count-or-another-placeholder)
94
127
  - [Zod messages ignore the language, or your `material.zod.*` strings](#zod-messages-ignore-the-language-or-your-materialzod-strings)
128
+ - [A test asserting the old Zod string length text fails](#a-test-asserting-the-old-zod-string-length-text-fails)
129
+ - [A calendar's weekday row is in English on a French page](#a-calendars-weekday-row-is-in-english-on-a-french-page)
95
130
 
96
131
  **Runtime**
97
132
 
98
133
  - [`must be used within` / `must be used inside` (context providers)](#must-be-used-within--must-be-used-inside)
99
134
  - [`Clipboard is not available`](#clipboard-is-not-available)
135
+ - [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
136
  - [`Upload failed with status 403`](#upload-failed-with-status-403)
101
137
  - [`Network error during upload`](#network-error-during-upload)
102
138
  - [`Maximum number of presets (20) reached`](#maximum-number-of-presets-20-reached)
@@ -113,6 +149,7 @@ words in your error.
113
149
  - [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
150
  - [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
151
  - [An `AlertDialog` focuses its Cancel although `@open-auto-focus` prevented it](#an-alertdialog-focuses-its-cancel-although-open-auto-focus-prevented-it)
152
+ - [Focus moves into a dialog although `@open-auto-focus` prevented it](#focus-moves-into-a-dialog-although-open-auto-focus-prevented-it)
116
153
  - [A `CustomDialog` or `ConfirmationDialog` no longer opens from its `#trigger`](#a-customdialog-or-confirmationdialog-no-longer-opens-from-its-trigger)
117
154
  - [A form with a required `Switch` no longer submits](#a-form-with-a-required-switch-no-longer-submits)
118
155
  - [A `RichTextEditor` became editable after upgrading](#a-richtexteditor-became-editable-after-upgrading)
@@ -121,7 +158,12 @@ words in your error.
121
158
  - [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)
122
159
  - [A row does not navigate after `@row-click` ran](#a-row-does-not-navigate-after-row-click-ran)
123
160
  - [A `DataTable` Previous or Next button is disabled](#a-datatable-previous-or-next-button-is-disabled)
161
+ - [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)
124
162
  - [A `DataTable` shows a sorting or a selection the URL never got](#a-datatable-shows-a-sorting-or-a-selection-the-url-never-got)
163
+ - [A `DataTable` no longer scrolls inside a 400px box](#a-datatable-no-longer-scrolls-inside-a-400px-box)
164
+ - [A `DataTable` shows its header row on a phone](#a-datatable-shows-its-header-row-on-a-phone)
165
+ - [A test's `getByRole('region')` finds more than one region](#a-tests-getbyroleregion-finds-more-than-one-region)
166
+ - [A `DataTable` toolbar is gone](#a-datatable-toolbar-is-gone)
125
167
 
126
168
  **PDF reader**
127
169
 
@@ -397,6 +439,21 @@ const button = screen.getByRole('button', { name: 'New order' });
397
439
  expect(within(button).getByText('New order')).toHaveClass('sr-only'); // collapsed
398
440
  ```
399
441
 
442
+ ### A menu no longer blocks the page, or a `ResponsiveButton` has no `useIsMobile` behaviour
443
+
444
+ **When:** after upgrading, clicking outside an open `DropdownMenu` (or
445
+ `LanguageSwitcher`) reaches the element under the pointer and the page can
446
+ still scroll; or `ResponsiveButton` shows its label in a test whose
447
+ `matchMedia` says "mobile".
448
+ **Why:** `DropdownMenu` is non-modal by default now: Reka's modal menu marked
449
+ the rest of the page `aria-hidden` while its buttons and links stayed
450
+ focusable (axe `aria-hidden-focus`). `ResponsiveButton` and
451
+ `ResponsiveLinkButton` collapse with a CSS `max-md` media query instead of a
452
+ JS effect, so the label is in the DOM at every width and jsdom (which applies
453
+ no CSS) always shows it.
454
+ **Fix:** pass `modal` to `DropdownMenu` to restore the old behaviour; force a
455
+ collapsed button in a test with `:expanded="false"`.
456
+
400
457
  ### A menu item selected by `[data-variant=destructive]` stops matching
401
458
 
402
459
  **When:** your CSS, or a test, targets `[data-variant=destructive]` on a
@@ -512,6 +569,23 @@ await expect(page.getByRole('switch', { name: 'Notifications' })).toHaveAttribut
512
569
  await expect(page.getByLabel('Country')).toHaveAttribute('aria-invalid', 'true');
513
570
  ```
514
571
 
572
+ ### A test no longer finds `aria-invalid` on an `OtpField` slot
573
+
574
+ **When:** `[data-slot="input-otp-slot"][aria-invalid]` (or
575
+ `getAllByRole(…)` filtered on `aria-invalid`) finds nothing although the
576
+ `OtpField` has `error`.
577
+ **Why:** ARIA does not allow `aria-invalid` on a plain `div`, and a slot is
578
+ a presentational box: the one element a screen reader reads is the hidden
579
+ input, which keeps `aria-invalid="true"`. A slot shows the error with a red
580
+ border from `data-invalid`.
581
+ **Fix:** assert it on the input, found by the field's label:
582
+
583
+ ```ts
584
+ await expect(page.getByLabel('Enter OTP')).toHaveAttribute('aria-invalid', 'true');
585
+ // A slot's red border, if a test must see it:
586
+ await expect(page.locator('[data-slot="input-otp-slot"]').first()).toHaveAttribute('data-invalid');
587
+ ```
588
+
515
589
  ### A test no longer finds the breadcrumb's home button
516
590
 
517
591
  **When:** after upgrading, `getByRole('button', { name: 'Home' })` finds
@@ -529,6 +603,141 @@ await page.getByRole('navigation', { name: 'breadcrumb' })
529
603
  .click();
530
604
  ```
531
605
 
606
+ ### A test no longer finds a sidebar entry by the `button` role
607
+
608
+ **When:** after upgrading, `getByRole('button', { name: 'Invoices' })` finds
609
+ nothing among the entries of an `ActivityLayout`, `AppSidebar` or `AppHeader`'s
610
+ menu sheet.
611
+ **Why:** each entry used to be a `<button>` that navigated on click. It is now
612
+ a real `<a href>`, so its role is `link`, and the current page's entry carries
613
+ `aria-current="page"`. The icon inside is decorative (`aria-hidden`), so the
614
+ link's name is its label alone, "Invoices", and an exact name matches.
615
+ **Fix:** find it as a link:
616
+
617
+ ```ts
618
+ const invoices = page.getByRole('link', { name: 'Invoices' });
619
+ await invoices.click();
620
+ await expect(invoices).toHaveAttribute('aria-current', 'page');
621
+ ```
622
+
623
+ ### A test no longer finds a tooltip on an entry of an expanded sidebar
624
+
625
+ **When:** after upgrading, a test hovers or focuses an entry of an expanded
626
+ `ActivityLayout` or `AppSidebar` sidebar (or of the phone menu sheet) and
627
+ waits for `getByRole('tooltip', { name: 'Inbox' })`, which never appears.
628
+ **Why:** the shell's entries no longer pass their label as `tooltip`. A
629
+ `SidebarMenuItem` shows its label as a tooltip only while its `Sidebar` is
630
+ collapsed; expanded, the label is already on screen, and a tooltip repeating
631
+ it opened over the first entry as focus moved into the menu sheet, where it
632
+ took the first Escape. A `tooltip` you pass is still shown, collapsed or
633
+ expanded, and a collapsed entry with no `tooltip` now shows its label, where
634
+ it showed nothing before.
635
+ **Fix:** find the entry by the label it shows —
636
+ `getByRole('link', { name: 'Inbox' })` — or collapse the sidebar first when
637
+ the test is about the tooltip. Pass `tooltip` when an expanded entry of your
638
+ own `Sidebar` must have one:
639
+
640
+ ```vue
641
+ <SidebarMenuItem href="/inbox" tooltip="3 unread">Inbox</SidebarMenuItem>
642
+ ```
643
+
644
+ ### The app shell's page scrolls instead of its content, or a full-height screen collapses
645
+
646
+ **When:** after upgrading, the whole page scrolls under an `ActivityLayout`
647
+ rather than the box inside it; a test or a script that scrolled the shell's
648
+ inner box (`[data-slot=activity-layout] .overflow-auto`) finds nothing to
649
+ scroll; or a screen sized `h-full` to fill the shell (a map, a board) is only
650
+ as high as its content.
651
+ **Why:** the shell was `100lvw × 100lvh` and scrolled its content inside
652
+ itself: the page was a scrollbar's width wider than the window, and on a phone
653
+ the bottom of the content sat under the browser's toolbar. It is now `100%`
654
+ wide and at least `100dvh` high, and the document scrolls; the header and the
655
+ desktop sidebar are sticky.
656
+ **Fix:** scroll the window in a test (`window.scrollTo`, or let the browser
657
+ scroll an element into view). For a screen that manages its own height, opt
658
+ back into the in-shell scrolling with `scroll="container"`:
659
+
660
+ ```vue
661
+ <ActivityLayout :items="items" scroll="container">
662
+ ```
663
+
664
+ ### A test finds a new heading or a second `main`, or no longer finds a title in a `div`
665
+
666
+ **When:** after upgrading, `getAllByRole('heading')` returns more than it did,
667
+ a selector such as `div[data-slot=card-title]` finds nothing, or an
668
+ accessibility check reports more than one `main` landmark in an app that
669
+ renders its own `<main>` around `ActivityLayout`.
670
+ **Why:** `ActivityContent`'s title is now a heading, `h1` by default; a
671
+ `CardTitle` is an `h3`; and the shell's content region is the page's
672
+ `<main>`. The phone menu sheet was named "Open menu", like its button; it is
673
+ now named "Navigation" (`material.app-shell.navigation`):
674
+ `getByRole('dialog', { name: 'Navigation' })`.
675
+ **Fix:** find titles by role and level —
676
+ `getByRole('heading', { level: 1, name: 'Invoices' })` — and pick another
677
+ level with `title-as` or `CardTitle`'s `as` (`'div'` for no heading). An app
678
+ with its own `<main>` passes `content-as="div"`.
679
+
680
+ ### `[data-test="typogaphy"]` finds nothing
681
+
682
+ **When:** after upgrading, a selector on `data-test="typogaphy"` finds no
683
+ `Typography` or `HelperText`.
684
+ **Why:** the attribute was misspelled; it now reads `data-test="typography"`.
685
+ **Fix:** select `[data-test="typography"]` — or better, the text or the role
686
+ the user sees.
687
+
688
+ ### A `class` on `ActivityLayout` now styles the whole shell
689
+
690
+ **When:** after upgrading, the classes passed to `<ActivityLayout class="…">`
691
+ (a padding, a background, a gap) land around the sidebar and the header too,
692
+ not only around the content, or a selector on the inner box finds them
693
+ missing.
694
+ **Why:** `class` used to land on the inner layout box; it now reaches the
695
+ outer element, `[data-slot=activity-layout]`, with the other attributes — as
696
+ `className` does in `@nxgt/material`.
697
+ **Fix:** keep on `ActivityLayout` only the classes meant for the whole shell,
698
+ and move the rest to the screen: `ActivityContent`'s `class`, or an element
699
+ of your own inside it:
700
+
701
+ ```vue
702
+ <ActivityLayout :items="items" class="bg-muted">
703
+ <ActivityContent title="Invoices" class="gap-6">…</ActivityContent>
704
+ </ActivityLayout>
705
+ ```
706
+
707
+ ### A test no longer finds a calendar day by its number
708
+
709
+ **When:** after upgrading, `getByRole('gridcell', { name: '1' })` finds nothing
710
+ in a `Calendar`, `DateField`, `DateRangeField` or `EventCalendar`'s date picker,
711
+ while `getByRole('button', { name: 'Thursday, October 1, 2026' })` (the old
712
+ name, which is also the new one) still does; or
713
+ `getByRole('gridcell', { name: '1', exact: true })` used to match both the 1st
714
+ and the next month's 1st, shown as an outside day.
715
+ **Why:** a day is now named by its full localized date and nothing else:
716
+ "Thursday, October 1, 2026" (in French "jeudi 1 octobre 2026"), on the button
717
+ and on the cell (`gridcell`) around it, which used to be named by its text,
718
+ "1". The chosen day carries `aria-selected="true"` on its cell and a
719
+ "Selected" description on its button, today `aria-current="date"` on both. The
720
+ days of the neighbouring months are named by their own dates ("Wednesday,
721
+ September 30, 2026"), so a bare number matches no day at all. The language is
722
+ the calendar's `locale`, else the active `I18nProvider` language, else English:
723
+ under a French provider the names, the heading and the weekday row are now
724
+ French, so pass `locale="en"` to keep English.
725
+ **Fix:** pick the day by its date, exactly, and assert the state; compute
726
+ today's name instead of writing it:
727
+
728
+ ```ts
729
+ const name = (date: Date) =>
730
+ new Intl.DateTimeFormat('en', { dateStyle: 'full' }).format(date);
731
+
732
+ const day = page.getByRole('gridcell', { name: 'Thursday, October 1, 2026', exact: true });
733
+ await day.getByRole('button').click();
734
+ await expect(day).toHaveAttribute('aria-selected', 'true');
735
+ await expect(page.getByRole('button', { name: 'Thursday, October 1, 2026' })).toHaveAccessibleDescription('Selected');
736
+ await expect(page.getByRole('gridcell', { name: name(new Date()) })).toHaveAttribute('aria-current', 'date');
737
+ ```
738
+
739
+ `getByRole('button', { name: 'Thursday, October 1, 2026', exact: true })` finds the same day.
740
+
532
741
  ### A `QRCode`'s `aria-label` is no longer on its wrapper
533
742
 
534
743
  **When:** after upgrading, `div[aria-label="…"]` (or a
@@ -609,16 +818,22 @@ page.locator('[data-slot="table-container"]'); // the scrolling container
609
818
 
610
819
  **When:** after upgrading to 0.22.0, `getByRole('textbox', { name: 'Email*' })`
611
820
  (or any role query whose name ends in `*`) finds nothing for a field whose
612
- `Label` has `with-asterisk` (a `required` field). Queries on the label's text,
613
- `getByLabelText` and Playwright's `getByLabel`, still see the `*`: match it
614
- loosely in `getByLabelText` (`getByLabelText(/^Email/)`); Playwright's
615
- `getByLabel('Email')` already matches a substring.
616
- **Why:** screen readers used to read the asterisk ("Email star" or "Email*"),
617
- so it is now `aria-hidden`. The field's own `required` / `aria-required` says it is required,
618
- and every field of this library now sets it on the element a screen reader
619
- lands on (the input, the picker's combobox, the slider thumbs, the rating's
821
+ `Label` has `with-asterisk` (a `required` field). Since the release that drew the
822
+ asterisk with CSS, `getByLabelText('Email*')` and Playwright's
823
+ `getByLabel('Email *')` find nothing either; and the other way round,
824
+ `getByLabel('Email', { exact: true })` and `getByLabelText('Email')` now match
825
+ a required field, which a `*` in the label's text used to prevent.
826
+ **Why:** screen readers read the asterisk ("Email star" or "Email*"), and
827
+ Testing Library and Playwright compare the label's text, `aria-hidden` or not.
828
+ The `*` is now drawn by a CSS pseudo-element with an empty `content`
829
+ (`label.css`, imported by the stylesheet), so it is in no text and no name. The
830
+ field's own `required` / `aria-required` says it is required, and every field
831
+ of this library sets it on the element a screen reader lands on (the input, the
832
+ picker's combobox, the select's trigger, the slider thumbs, the rating's
620
833
  radiogroup, the editor's textbox, the list's listbox, or, for a file picker,
621
- a group described as "This field is required.").
834
+ a group described as "This field is required."). A `SelectField`'s trigger is
835
+ now named by its label (`aria-labelledby`), so `getByRole('combobox', { name:
836
+ 'Framework' })` finds it.
622
837
  **Fix:** find the field by its label alone, and check that it is required
623
838
  separately:
624
839
 
@@ -634,6 +849,434 @@ await expect(page.getByRole('textbox', { name: 'Email' })).toHaveAttribute('requ
634
849
  await expect(page.getByRole('combobox', { name: 'Delivery date' })).toHaveAttribute('aria-required', 'true');
635
850
  ```
636
851
 
852
+ Custom CSS that targeted the old `<span class="text-error">` should target
853
+ `[data-slot="label-text"][data-required]::after` instead; the recipe is in
854
+ [Label](components/primitives.md#label). The mark is drawn by the package stylesheet, so a
855
+ snapshot of the DOM no longer contains it, and it stays visible in
856
+ forced-colors mode.
857
+
858
+ ### An accessibility audit reports low contrast on a badge, a button, a toast or a menu entry
859
+
860
+ **When:** axe (or Lighthouse) reports `color-contrast` on a `Badge` with
861
+ `variant="success"`, `"info"`, `"warning"` or `"secondary"`; on a filled
862
+ `secondary`, `success`, `info` or `warning` `Button` or `IconButton` in the
863
+ dark theme; on a filled `Button` or `IconButton` while it is hovered, focused
864
+ or pressed; on muted text on a `bg-muted` surface (an `Alert`
865
+ `variant="foreground"`, the default `Timeline` marker, code) or on the
866
+ "paper" tint; on a toast's title or description while it enters, or its X
867
+ button; or on the muted text (a shortcut, the `LanguageSwitcher` code) of a
868
+ highlighted menu entry, or on a calendar's weekday row in the dark theme.
869
+ **Why:** the colours come from your tokens and the components' state tints.
870
+ Before this fix, concretely: the light `--secondary`,
871
+ `--success`, `--info` and `--warning` gave their near-white text 4.3:1,
872
+ 3.5:1, 2.5:1 and 3.4:1; the light `--muted-foreground` read 4.3:1 on
873
+ `--muted` and 4.4:1 on the paper tint, and the dark `--muted` was the light,
874
+ near-white value, so muted text on `bg-muted` failed in the dark theme too;
875
+ in the dark theme a filled `secondary`, `success`, `info` or `warning` button
876
+ gave 4.3:1, 3.5:1, 2.5:1 and 3.4:1, and its hover, focus and pressed tints
877
+ less; in the light theme a filled button faded toward the page when hovered,
878
+ focused or pressed (`/90`, `/80`, `/70`: down to 3.1:1 pressed); a library
879
+ toast started at opacity 0, so an audit run while it entered measured its
880
+ text faded; and the toast and the highlighted entry used the muted colour on
881
+ a background it does not reach 4.5:1 on; the dark `--muted-foreground` of a
882
+ calendar's weekday row reached 3.7:1 on a popover. If the audit still fails, your
883
+ stylesheet overrides one of those tokens, a `class` of your own puts the old
884
+ state tints back, or you still ship one of those versions.
885
+ **Fix:** update, remove the override, or pick a colour that keeps 4.5:1 with
886
+ its `-foreground`:
887
+
888
+ ```css
889
+ :root {
890
+ --info: oklch(0.5 0.165 254.624); /* 5.8:1 with --info-foreground */
891
+ }
892
+ ```
893
+
894
+ A toast the library renders, through `toast()` or `toasts.*` from
895
+ `@nxgt/material-vue` (vue-sonner's `toast.custom` path, marked
896
+ `data-styled="false"`), no longer fades its text in: the wrapper stays opaque
897
+ while it slides in, so an audit run while it enters measures the settled
898
+ colours. A toast vue-sonner styles itself still fades in from opacity 0: a
899
+ plain `toast('…')` or `toast.success('…')` from `vue-sonner`, `sonnerToast`
900
+ from this package, and, for now, the toasts `Notifications` shows with
901
+ `toast-on-arrival` and the messages of `AttributesField`. Show your own
902
+ through `toasts.*` instead. If a library toast still reports its title as low
903
+ contrast, an override of `[data-sonner-toast]` sets its `opacity` back to 0
904
+ on mount.
905
+
906
+ The muted text on a `bg-muted` surface (an `Alert` `variant="foreground"`, the
907
+ default `Timeline` marker) and on the "paper" tint failed in the shipped
908
+ palette too (4.3:1 and 4.4:1 in light; the dark `--muted` was the light one).
909
+ They pass since the `--muted` and `--muted-foreground` tokens changed: if
910
+ yours still fail, you override them.
911
+
912
+ In the dark theme, a filled `secondary` `Button` gave its near-white text
913
+ 4.3:1, a `tonal` secondary one 3.9:1 (3.4:1 on `--card`), and a filled
914
+ `success`, `info` or `warning` one 3.5:1, 2.5:1 and 3.4:1. The dark
915
+ `--secondary` is brighter (`oklch(0.66 ...)`, 6.5:1 as text on
916
+ `--background`), and the filled `secondary`, `success`, `info` and `warning`
917
+ `Button`s and `IconButton`s are tinted in the dark theme
918
+ (`dark:bg-secondary/70`, `dark:bg-success/60`, …) so their text keeps 4.5:1.
919
+ A filled button given a background of your own through `className` loses the
920
+ tint in dark mode: add a matching `dark:` one.
921
+
922
+ ### `ARIA hidden element must not be focusable or contain focusable elements`
923
+
924
+ **When:** an accessibility audit (axe, Lighthouse, the Storybook
925
+ accessibility panel) runs while a `Dialog`, `Sheet`, `Drawer`,
926
+ `AlertDialog`, `CommandDialog`, a `Select` list, a `ContextMenu`, or a
927
+ `modal` `DropdownMenu` or `Popover` is open, and reports `aria-hidden-focus`
928
+ on the page behind it: its buttons, links and fields.
929
+ **Why:** reka marks the rest of the page `aria-hidden` while a modal layer
930
+ is open and leaves it focusable; a control the tab order reaches inside an
931
+ `aria-hidden` subtree is what the rule reports. A version of @nxgt/material-vue that
932
+ reports it predates the inert background.
933
+ **Fix:** upgrade: the page behind a modal layer of this library is `inert`
934
+ as well — out of the tab order and of the accessibility tree — and the rule
935
+ passes. Live regions (the `Toaster`) stay reachable. Do not exclude
936
+ `[aria-hidden]` or `[data-aria-hidden]` from the audit: it hides real
937
+ failures. A layer of yours built on reka directly (not on these
938
+ components) still marks the page `aria-hidden` only; make the rest of the
939
+ page inert with core's `inertOthers`, which keeps live regions and counts
940
+ nested layers:
941
+
942
+ ```ts
943
+ import { inertOthers } from '@nxgt/material-core/inert-others';
944
+ import { nextTick, ref, watch } from 'vue';
945
+
946
+ const open = ref(false);
947
+ // A template ref on your layer's content: an element or a component.
948
+ const content = ref();
949
+
950
+ watch(open, (isOpen, _, onCleanup) => {
951
+ if (!isOpen) return;
952
+ let undo: (() => void) | undefined;
953
+ let closed = false;
954
+ nextTick(() => {
955
+ // Closed before the tick: do not mark the page.
956
+ if (!closed) undo = inertOthers(content.value?.$el ?? content.value);
957
+ });
958
+ onCleanup(() => {
959
+ closed = true;
960
+ undo?.();
961
+ });
962
+ });
963
+ ```
964
+
965
+ Elements mounted after the layer opens are not made inert: mount them
966
+ first, or pass them to `inertOthers` in `keep` when they must stay
967
+ reachable.
968
+
969
+ ### An accessibility audit reports low contrast on a tonal or outlined button, a selected list tile or a hovered badge link
970
+
971
+ **When:** axe (or Lighthouse) reports `color-contrast` on a `tonal` or
972
+ `outlined` `Button` or `IconButton` (`primary` in the dark theme, `error`,
973
+ `warning` and `success`), at rest or hovered, focused or pressed; on the
974
+ subtitle of a selected `ListTile`; or on a `Badge` link while it is hovered.
975
+ **Why:** those variants drew their text in the token itself, and some tokens
976
+ do not reach 4.5:1 as text on the page and on their own tint: the dark
977
+ `--primary` is a dark indigo (2.2:1 on `--card`), the light `--error` gave
978
+ 3.6:1 on its tonal tint, `--warning` 4.3:1, the dark `--success` 4.0:1 on
979
+ `--card`. The selected tile's tint brought the muted subtitle to 4.4:1, and a
980
+ `Badge` link faded toward the page on hover (`/90`: down to 3.0:1 for `info`
981
+ in the dark theme). A release that has the fix mixes those tones from the
982
+ tokens, toward black in the light theme and toward white in the dark one; if
983
+ the audit still fails, a text colour of your own replaces the tone, or a
984
+ token of yours is too light (or, in the dark theme, too dark) to read as text.
985
+ **Fix:** update, then drop the text colour you put on the button, or check
986
+ your token against the page at 4.5:1:
987
+
988
+ ```vue
989
+ <!-- before: the tone is replaced by the raw token -->
990
+ <Button variant="tonal" color="error" class="text-error">Delete</Button>
991
+ <!-- after -->
992
+ <Button variant="tonal" color="error">Delete</Button>
993
+ ```
994
+
995
+ A `Badge` link of your own with a hover fade such as `[a&]:hover:bg-error/90`
996
+ in its `class` puts the fade back: darken instead, as in
997
+ `[a&]:hover:bg-[color-mix(in_oklab,var(--color-error)_90%,black)]` (with
998
+ your badge's colour in place of `error`). A
999
+ subtitle colour of your own on a selected `ListTile` must reach 4.5:1 on
1000
+ `bg-primary/15`.
1001
+
1002
+ ### An accessibility audit reports low contrast on error text in the dark theme
1003
+
1004
+ **When:** an error message, a validation hint or a red value of your own, in
1005
+ the dark theme, fails 4.5:1 (about 4.2:1 on the page, 3.8:1 on a card); the
1006
+ catalogue's own field messages pass.
1007
+ **Why:** it is drawn with `text-error`, the fill colour. The dark `--error` is
1008
+ kept dark enough for near-white text on a filled error surface, which makes it
1009
+ too dark to read as text on the dark surfaces. Error text has its own token,
1010
+ `--error-text`, lightened in the dark theme (5.9:1 on `--background`, 5.3:1 on
1011
+ `--card`).
1012
+ **Fix:** draw text with `text-error-text`; keep `text-error` for fills,
1013
+ borders and icons that stand alone. On a `Button` or `IconButton` drop the
1014
+ class instead: the tone already colours the text:
1015
+
1016
+ ```vue
1017
+ <!-- before -->
1018
+ <p class="text-sm text-error">Enter a valid email</p>
1019
+ <!-- after -->
1020
+ <p class="text-sm text-error-text">Enter a valid email</p>
1021
+ ```
1022
+
1023
+ Setting `--error` or `--error-text` on an element below the root (a `style`
1024
+ on a section) changes nothing: `@theme` resolves `--color-error-text` on the
1025
+ root. Below the root, set `--color-error-text` (and `--color-error` for fills)
1026
+ on that element, or put the override on the root.
1027
+
1028
+ ### Dialogs, toasts or buttons no longer animate
1029
+
1030
+ **When:** a transition or an animation of a catalogue component does not run
1031
+ on your machine, while it does on a colleague's.
1032
+ **Why:** the operating system has "Reduce motion" on. `styles.css` honours
1033
+ `prefers-reduced-motion: reduce` for every component (dialogs, sheets, menus,
1034
+ toasts, buttons) and leaves only the `Spinner` and
1035
+ `animate-spin` elements turning.
1036
+ **Fix:** none is needed, this is the intended behaviour. To test the animated
1037
+ version, turn the setting off (or, in Chrome DevTools, Rendering, "Emulate CSS
1038
+ media feature prefers-reduced-motion": no-preference). To keep one animation
1039
+ for everyone, override it with an `!important` rule on that element inside
1040
+ `@media (prefers-reduced-motion: reduce)`.
1041
+
1042
+ ### A Button's label overflows a card on a phone
1043
+
1044
+ **When:** on a narrow screen, a `Button` with a long label sticks out of its
1045
+ card, dialog or column (a 349 px button in a 343 px card), or its label is
1046
+ clipped at both ends with no "…".
1047
+ **Why:** before this change, the button took its label's whole width
1048
+ (`width: max-content`) whatever its container. A button is now never wider
1049
+ than its container (`max-width: 100%`), and its text is put in a
1050
+ `<span data-slot="button-label">` (one span per run of text, so `Delete {name}?` is one piece) that shrinks and ends in "…". The reverse
1051
+ is the same change: a button that grew past its container now truncates.
1052
+ The ellipsis needs the text as the button's own child: with `as-child`, text
1053
+ inside the child element is clipped without one.
1054
+ **Fix:** update. Then pick, for each long label: leave the ellipsis (give a
1055
+ `title` so a sighted user can read it whole; the accessible name already
1056
+ is), let it break onto lines with `wrap`, or give the button a width of
1057
+ your own through `class`. With `as-child`, put the label in a `<span>`:
1058
+
1059
+ ```vue
1060
+ <template>
1061
+ <Button title="Continue to the payment details">Continue to the payment details</Button>
1062
+ <Button wrap>Continue to the payment details and confirm the order</Button>
1063
+ <Button class="w-full">Continue</Button>
1064
+ <Button as-child>
1065
+ <a href="/pay"><span>Continue to the payment details</span></a>
1066
+ </Button>
1067
+ </template>
1068
+ ```
1069
+
1070
+ ## Feedback
1071
+
1072
+ ### A toast with an action never goes away
1073
+
1074
+ **When:** a test, or a user, waits for a toast to disappear by itself, and one
1075
+ that carries an `action` (`toast({ action })`, or `toasts.info({ action })`
1076
+ and the other presets with your own action) is still there.
1077
+ **Why:** a toast with an `action` is persistent: `toast()` sets
1078
+ `options.duration` to `Infinity` (WCAG 2.2.1: a timed toast can vanish before
1079
+ a keyboard or screen-reader user reaches its button). Without an `action` a
1080
+ preset times out after 5 seconds.
1081
+ **Fix:** pass a duration, or close it:
1082
+
1083
+ ```ts
1084
+ toasts.info({ action, options: { duration: 8000 } }); // times out again
1085
+ toasts.dismiss(id); // or toasts.dismiss() for all of them
1086
+ ```
1087
+
1088
+ Users close it with its X button (named "Close", or "Fermer" in French),
1089
+ with Escape while focus is inside it, or with the action itself. In a test,
1090
+ click `getByRole('button', { name: 'Close' })` or call `toasts.dismiss()`.
1091
+
1092
+ ### A toast covers the form's button on a phone
1093
+
1094
+ **When:** on a phone, a toast shows across the bottom of the screen, over a
1095
+ form's submit button (or anything else kept at the bottom), until it goes.
1096
+ **Why:** under 600px wide, vue-sonner lays toasts out across the whole width,
1097
+ and its default `position` is `bottom-right`, so the bottom of the screen. The
1098
+ `Toaster` now shows them at the top (`top-center`) on a phone when it is given
1099
+ no `position`; it still covers the bottom when the app mounts vue-sonner's own
1100
+ `Toaster` (`import { Toaster } from 'vue-sonner'`), or passes a bottom
1101
+ `position`, which applies on every screen.
1102
+ **Fix:** mount the `Toaster` from `@nxgt/material-vue/components`, and drop a
1103
+ bottom `position` unless the bottom is what you want on phones too. When a
1104
+ sticky header must stay visible above the toasts, raise the phone offset's
1105
+ `top`:
1106
+
1107
+ ```vue
1108
+ <Toaster /> <!-- top-center on a phone -->
1109
+ <Toaster :mobile-offset="{ top: 72 }" />
1110
+ <Toaster position="bottom-center" /> <!-- bottom-center on every screen -->
1111
+ ```
1112
+
1113
+ ### A toast never goes away after a tap
1114
+
1115
+ **When:** on a phone (or any touch screen), a toast that should time out stays
1116
+ on the screen after the user tapped it, and only goes after a tap somewhere
1117
+ else.
1118
+ **Why:** a toast pauses while a mouse hovers it (WCAG 2.2.1). After a tap, the
1119
+ browser emulates a mouse that arrives over the toast and never leaves, and
1120
+ vue-sonner read that as a hover, so the toast stayed paused.
1121
+ **Fix:** update, and mount the `Toaster` from `@nxgt/material-vue/components`:
1122
+ it ignores the hover a tap emulates (`@nxgt/material-core/touch-hover`), so a
1123
+ tapped toast pauses while the finger is down and goes after its duration; a
1124
+ mouse still pauses it. vue-sonner's own `Toaster`
1125
+ (`import { Toaster } from 'vue-sonner'`) keeps the old behaviour. A toast with an
1126
+ `action` is meant to stay: see
1127
+ [A toast with an action never goes away](#a-toast-with-an-action-never-goes-away).
1128
+
1129
+ ### A toast's action cannot be clicked while a dialog is open
1130
+
1131
+ **When:** a toast with an `action` (or a close button) shows over an open
1132
+ `Dialog`, `AlertDialog`, `Sheet` or `Drawer`: the pointer does nothing on it,
1133
+ or a click on it closes the dialog.
1134
+ **Why:** a modal layer sets `pointer-events: none` on `<body>`, and the
1135
+ `Toaster` inherited it, so its toasts could not be clicked; the layer also
1136
+ counted a pointer down on a toast as outside the dialog.
1137
+ **Fix:** update `@nxgt/material-vue` and mount its `Toaster`: it sets
1138
+ `pointer-events: auto`, and the modal contents treat a pointer down or a focus
1139
+ inside the `Toaster` as inside, so the action runs and the dialog stays open.
1140
+ Sonner's own `Toaster` keeps the old behaviour.
1141
+
1142
+ ### A `ConfirmationDialog` reopened at once stays closed
1143
+
1144
+ **When:** a `ConfirmationDialog` with `v-model:open` (or `:open` and
1145
+ `@update:open`) closes on Confirm and your `update:open` handler sets `open`
1146
+ back to `true` in the same tick, to show the next step. `open` is `true`, the
1147
+ dialog is shut, and setting `open = true` again does nothing. Giving the
1148
+ dialog a `:key` per step made it go away.
1149
+ **Why:** the dialog kept its own copy of `open`, synced by a watch. Set to
1150
+ `false` then `true` before Vue flushed, `open` never changed as far as the
1151
+ watch could see, and the copy stayed `false`.
1152
+ **Fix:** update: a dialog whose parent listens to `update:open` now shows
1153
+ exactly what `open` says. No `:key` needed; one dialog whose `title` follows
1154
+ the step is enough:
1155
+
1156
+ ```vue
1157
+ <ConfirmationDialog
1158
+ v-model:open="open"
1159
+ :title="steps[step].title"
1160
+ @confirm="step === 0 ? (step = 1, (open = true)) : finish()"
1161
+ />
1162
+ ```
1163
+
1164
+ Reopened a little later (after an `await`), it already worked, and no overlay
1165
+ is left above the content: reka keeps the overlay and the content in one
1166
+ teleport, in order.
1167
+
1168
+ ### The first Escape after closing a nested dialog does nothing
1169
+
1170
+ **When:** an `AlertDialog` (a `ConfirmationDialog`, a `Dialog`, a `Sheet` or a
1171
+ `Drawer`) opened over another overlay — a `Sheet` with a Delete that asks for
1172
+ confirmation — is closed with Escape or Cancel, and the user presses Escape
1173
+ again at once to close the `Sheet` too. The `Sheet` stays open; a second
1174
+ Escape, a moment later, closes it. A test that presses Escape twice in a row
1175
+ fails the same way.
1176
+ **Why:** reka hands Escape to the highest overlay only, and a dialog stops
1177
+ being one when its content leaves the page, not when it closes (with
1178
+ `unmountOnHide`, its default). The closed alert stays on the page for its
1179
+ exit animation (200ms for a dialog, 300ms for a sheet, 500ms for a drawer):
1180
+ an Escape pressed meanwhile went to it, and it was already closed.
1181
+ **Fix:** update: `AlertDialogContent`, `DialogContent`, `SheetContent` and
1182
+ `DrawerContent` now press Escape again, for the overlay below, once a
1183
+ content that was closing has left. Nothing to change in your code. Your
1184
+ `@escape-key-down` still runs first, on both presses; preventing the default
1185
+ while yours is closing keeps the key from being forwarded:
1186
+
1187
+ ```vue
1188
+ <AlertDialog v-model:open="confirm">
1189
+ <!-- closing: the Sheet stays open -->
1190
+ <AlertDialogContent @escape-key-down="(event) => !confirm && event.preventDefault()">
1191
+ …
1192
+ </AlertDialogContent>
1193
+ </AlertDialog>
1194
+ ```
1195
+
1196
+ With no overlay below, the forwarded Escape reaches the page's own `keydown`
1197
+ handlers; an app shortcut that should ignore it can check `event.isTrusted` or
1198
+ `event.defaultPrevented`.
1199
+
1200
+ In a test, wait for the `Sheet` to leave the DOM (the forwarded Escape lands
1201
+ a few frames after the alert has gone) and look it up by selector rather
1202
+ than by role: the closing alert still hides the page from the accessibility
1203
+ tree.
1204
+
1205
+ ## Navigation
1206
+
1207
+ ### A `ListTile` link reloads the whole page
1208
+
1209
+ **When:** a `ListTile` with an `href` is clicked inside a router app: the
1210
+ browser loads the document again, the page flashes, and client state is lost.
1211
+ **Why:** by default the tile renders a plain `<a href>`, which needs no router
1212
+ and so cannot navigate on the client.
1213
+ **Fix:** pass `link-component`, with the same contract as the app shell's: a
1214
+ component that takes `href` and lets the other attributes fall through to its
1215
+ root. `RouterLink` takes `to`, so wrap it:
1216
+
1217
+ ```ts
1218
+ // app-link.ts
1219
+ import { defineComponent, h } from 'vue';
1220
+ import { RouterLink } from 'vue-router';
1221
+
1222
+ export const AppLink = defineComponent({
1223
+ props: { href: { type: String, required: true } },
1224
+ setup:
1225
+ (props, { slots, attrs }) =>
1226
+ () =>
1227
+ h(RouterLink, { ...attrs, to: props.href }, slots),
1228
+ });
1229
+ ```
1230
+
1231
+ ```vue
1232
+ <ListTile title="Notifications" href="/settings/notifications" :link-component="AppLink" />
1233
+ ```
1234
+
1235
+ ### The phone menu needs two Escapes to close, or a tooltip shows over it
1236
+
1237
+ **When:** on a phone, the menu sheet of `ActivityLayout` or `AppHeader` opens
1238
+ with a tooltip repeating the first entry's label over the menu, and a plain
1239
+ open sheet needs two Escapes to close: the first one closes the tooltip.
1240
+ **Why:** the entries of the sheet's expanded sidebar carried a tooltip, and
1241
+ focus lands on the first entry as the sheet opens, which opened its tooltip;
1242
+ the tooltip's own layer takes the first Escape.
1243
+ **Fix:** update `@nxgt/material-vue`. An entry of an expanded `Sidebar` now shows no
1244
+ tooltip, since it shows its label; a collapsed one keeps it, and an explicit
1245
+ `tooltip` is always shown. A `SidebarMenuItem` of your own needs no change.
1246
+
1247
+ ### `Tabs` look different after upgrading
1248
+
1249
+ **When:** a `Tabs` with no `variant`, or with `variant="bar"`, renders a row of
1250
+ tabs over a border with a bar under the active one, where it rendered
1251
+ one muted pill with the active trigger raised;
1252
+ one with `variant="line"` renders a rounded pill.
1253
+ **Why:** `underline` is now the default look. `bar` and `line` are deprecated
1254
+ aliases: `bar` renders as `underline`, `line` as `segmented`.
1255
+ **Fix:** write the look you want. `underline` needs no `variant`; for a pill,
1256
+ set `segmented`:
1257
+
1258
+ ```vue
1259
+ <Tabs default-value="week" variant="segmented">
1260
+ ```
1261
+
1262
+ A test that queried the old classes should query the state instead:
1263
+ `getByRole('tab', { selected: true })`, and `data-variant` on
1264
+ `[data-slot="tabs"]` for the look.
1265
+
1266
+ ### A screen reader announces an icon-only tab as "tab", with no name
1267
+
1268
+ **When:** a `TabsTrigger` holds only an icon; an accessibility audit reports a
1269
+ tab with no accessible name.
1270
+ **Why:** a tab is named by its text, and an icon has none.
1271
+ **Fix:** give the trigger `aria-label`, and `title` for a hover hint:
1272
+
1273
+ ```vue
1274
+ <TabsTrigger value="recent" :icon="Clock" aria-label="Recent" title="Recent" />
1275
+ ```
1276
+
1277
+ `EmojiList` and `EmojiPicker` name their group tabs themselves; update
1278
+ `@nxgt/material-vue` if yours read "tab" only.
1279
+
637
1280
  ## SSR & build
638
1281
 
639
1282
  ### `ReferenceError: window is not defined`
@@ -958,6 +1601,61 @@ const { value, handleBlur } = useField<string>('nickname');
958
1601
  What counts as leaving, per kind of field:
959
1602
  [forms](guides/forms.md#touched-on-blur).
960
1603
 
1604
+ ### A failed submit does not focus the first invalid field
1605
+
1606
+ **When:** `handleSubmit` fails validation (or you call `setFieldError`) and
1607
+ focus stays on the submit button, with the error out of view. There is no
1608
+ `shouldFocusError` option to turn on.
1609
+ **Why:** vee-validate never moves focus: `handleSubmit(onValid, onInvalid)`
1610
+ only reports `{ errors }`. (React Hook Form focuses by itself; that is a
1611
+ React-only behaviour.)
1612
+ **Fix:** from the release that lists this fix in its changelog, every
1613
+ `*FormField` marks its element with
1614
+ `data-field-name="<name>"`, and `focusFirstInvalid` / `focusField` from
1615
+ `@nxgt/material-vue/lib` use it to focus the first control a user can reach in
1616
+ the field, whatever its shape (an input, a select trigger, the first chip, a
1617
+ file picker's button):
1618
+
1619
+ ```vue
1620
+ <script setup lang="ts">
1621
+ import { focusField, focusFirstInvalid } from '@nxgt/material-vue/lib';
1622
+ import { useForm } from 'vee-validate';
1623
+
1624
+ const { handleSubmit, setFieldError } = useForm();
1625
+ const onSubmit = handleSubmit(save, ({ errors }) => focusFirstInvalid(errors));
1626
+
1627
+ // A server error on one field:
1628
+ // setFieldError('email', 'Already used'); focusField('email');
1629
+ </script>
1630
+ ```
1631
+
1632
+ `focusFirstInvalid(errors)` takes the first field **in page order** that has
1633
+ an error, not the first key of `errors`. Pass a `scope` element as the last
1634
+ argument to look inside one form of a page that has several. A field that is
1635
+ not mounted (a closed tab) cannot take focus: the function returns `false`.
1636
+
1637
+ ### A test that finds a chip by `aria-selected` or `getByRole('option')` no longer finds it
1638
+
1639
+ **When:** after upgrading past the release that lists this fix, a test (or
1640
+ an end-to-end locator) that picked a `SelectChipField` chip with
1641
+ `[aria-selected="true"]`, `getByRole('option', { name })` or
1642
+ `toHaveAttribute('aria-selected', 'true')` finds nothing, and the same for
1643
+ `SelectChipFormField`.
1644
+ **Why:** the chips are toggle buttons: `aria-selected` is not allowed on a
1645
+ button, so a chosen chip is now `aria-pressed="true"`, in `single` mode too
1646
+ (pressing the chosen chip clears it). The chips sit in a `role="group"`
1647
+ named by the label and described by the help text. `SelectCardField` is
1648
+ unchanged.
1649
+ **Fix:** query the group, then the chip as a button:
1650
+
1651
+ ```ts
1652
+ import { screen, within } from '@testing-library/vue';
1653
+
1654
+ const topics = screen.getByRole('group', { name: 'Topics' });
1655
+ const design = within(topics).getByRole('button', { name: 'Design' });
1656
+ expect(design).toHaveAttribute('aria-pressed', 'true');
1657
+ ```
1658
+
961
1659
  ### A multi-step form reopens at a later step
962
1660
 
963
1661
  **When:** a wizard built on `defineStepFormStore` opens at the step (and with
@@ -1558,8 +2256,58 @@ z.config({ customError: zodLocaleError({ language: () => requestLanguage() }) })
1558
2256
 
1559
2257
  See [helpers](guides/helpers.md#zodlocaleerror-limits).
1560
2258
 
2259
+ ### A calendar's weekday row is in English on a French page
2260
+
2261
+ **When:** under `<I18nProvider>` set to French, a `DateField`,
2262
+ `DateRangeField` or `Calendar` shows "S", "M", "T"… and starts the week on
2263
+ Sunday.
2264
+ **Why:** before the calendar followed the provider's language, it took reka's
2265
+ default `en-US` locale, which shapes the weekday row and the first day of the
2266
+ week.
2267
+ **Fix:** update: a `Calendar`'s `locale` prop, else the `I18nProvider`
2268
+ language, else English drives the whole calendar, weekday row included ("L",
2269
+ "M"…, Monday first, each header labelled "lundi"…). `DateField` and
2270
+ `DateRangeField` follow the provider; to force another language on one
2271
+ calendar, pass `locale`:
2272
+
2273
+ ```vue
2274
+ <Calendar v-model="date" locale="en" />
2275
+ ```
2276
+
2277
+ reka hides the weekday row from assistive tech (`<thead aria-hidden>`); in a
2278
+ test, find a day by its full date instead
2279
+ (`getByRole('gridcell', { name: 'jeudi 1 octobre 2026' })`).
2280
+
1561
2281
  ## Runtime
1562
2282
 
2283
+ ### A test asserting the old Zod string length text fails
2284
+
2285
+ **When:** after upgrading past the release that lists this fix, a test or a
2286
+ snapshot that asserted a string length message from `zodLocaleError` fails:
2287
+ it expected `This value is too small: it must be at least 3 characters.`
2288
+ (French `Cette valeur est trop petite : …`) and gets `Must contain at least 3
2289
+ characters.` (`Doit contenir au moins 3 caractères.`).
2290
+ **Why:** a string's `min` and `max` read as a length now, through
2291
+ the keys `material.zod.too_small_string_inclusive` / `_exclusive` and
2292
+ `material.zod.too_big_string_inclusive` / `_exclusive`, which pluralize the
2293
+ count: `z.string().min(1)` says `Must contain at least 1 character.` /
2294
+ `Doit contenir au moins 1 caractère.`. A number, a list, a set or a file
2295
+ keeps the "too large / too small" wording. A French `invalid_format` noun
2296
+ opens with a capital too (`Adresse e-mail invalide.`).
2297
+ **Fix:** update the expected text, or override the new keys in your app's
2298
+ `material.zod.*` resources to keep your own wording (an override of
2299
+ `too_small_inclusive` no longer applies to strings):
2300
+
2301
+ ```json
2302
+ {
2303
+ "material": {
2304
+ "zod": {
2305
+ "too_small_string_inclusive": "At least {minimum, plural, one {# character} other {# characters}}, please."
2306
+ }
2307
+ }
2308
+ }
2309
+ ```
2310
+
1563
2311
  ### `must be used within` / `must be used inside`
1564
2312
 
1565
2313
  **When:** on render, from a part of a widget or a context composable used
@@ -1642,8 +2390,34 @@ try {
1642
2390
  ```
1643
2391
 
1644
2392
  Serve the app over `https` (or `localhost`) in any environment where copying
1645
- must work. `CopyButton` does not throw: it uses `@vueuse/core`'s
1646
- `useClipboard`, and in an insecure context it silently copies nothing.
2393
+ must work. `CopyButton` does not throw: it awaits the same write and, in an insecure
2394
+ context or when the clipboard refuses it, shows "Copy failed" ([details](#a-test-no-longer-finds-copybutton-by-copied-or-its-name-changes)).
2395
+
2396
+ ### A test no longer finds `CopyButton` by "Copied", or its name changes
2397
+
2398
+ **When:** a test clicks a `CopyButton` and looks for a button named
2399
+ `Copied`, or reads the tooltip, or the copy "succeeds" in a page where the
2400
+ clipboard is not available.
2401
+ **Why:** the button's accessible name is now always its `label` (`Copy`). The
2402
+ outcome is a visually hidden `role="status"` (polite) region next to the
2403
+ button, so a screen reader announces it without a rename. "Copied" shows only
2404
+ after the clipboard write resolved; a rejected write (permission denied, an
2405
+ insecure origin, no Clipboard API) shows "Copy failed" (`failed-label`,
2406
+ `material.copy.failed`).
2407
+ **Fix:** stub the clipboard and read the status next to the button. A bare
2408
+ `getByRole('status')` is ambiguous: every `CopyButton` renders one, and so do
2409
+ `Banner`, `DataTable` and the skeletons.
2410
+
2411
+ ```ts
2412
+ Object.defineProperty(navigator, 'clipboard', {
2413
+ configurable: true,
2414
+ value: { writeText: vi.fn().mockResolvedValue(undefined) }, // or mockRejectedValue(new Error('denied'))
2415
+ });
2416
+ const button = screen.getByRole('button', { name: 'Copy' });
2417
+ await user.click(button);
2418
+ // the parent is the CopyButton's own root
2419
+ expect(await within(button.parentElement!).findByRole('status')).toHaveTextContent('Copied');
2420
+ ```
1647
2421
 
1648
2422
  ### `Upload failed with status 403`
1649
2423
 
@@ -1973,7 +2747,9 @@ await expect(volume).toHaveAttribute('aria-invalid', 'true');
1973
2747
  **When:** with 0.21.0 or earlier, an `AlertDialogContent` whose
1974
2748
  `@open-auto-focus` calls `event.preventDefault()` still moves focus to its
1975
2749
  `AlertDialogCancel` a moment after opening, where `DialogContent`,
1976
- `SheetContent` and `DrawerContent` keep focus where it was.
2750
+ `SheetContent` and `DrawerContent` leave it to you (where focus then goes
2751
+ behind a modal layer: [Focus moves into a dialog although
2752
+ `@open-auto-focus` prevented it](#focus-moves-into-a-dialog-although-open-auto-focus-prevented-it)).
1977
2753
  **Why:** reka focuses the Cancel a tick after the alert dialog opens without
1978
2754
  checking whether the event was prevented, and the dialog's focus trap then
1979
2755
  keeps focus inside.
@@ -1990,6 +2766,29 @@ takes no focus until the next task: focus it from a `setTimeout` if you want it.
1990
2766
  <AlertDialogContent @open-auto-focus="(event) => event.preventDefault()">
1991
2767
  ```
1992
2768
 
2769
+ ### Focus moves into a dialog although `@open-auto-focus` prevented it
2770
+
2771
+ **When:** a modal `Dialog`, `Sheet`, `Drawer` or `AlertDialog` whose
2772
+ `@open-auto-focus` calls `event.preventDefault()` and focuses nothing: focus
2773
+ lands on the layer's content, not on the element that had it before it
2774
+ opened.
2775
+ **Why:** the page behind a modal layer is `inert`, and an inert element
2776
+ cannot keep focus: the browser would drop it on `<body>`, where a keyboard
2777
+ or screen-reader user is nowhere. The layer takes it instead (its content
2778
+ has `tabindex="-1"`); closing still hands focus back to the element that
2779
+ opened it.
2780
+ **Fix:** focus what you want inside the layer in the handler; to keep
2781
+ focus on the page behind, make the layer non-modal (`:modal="false"` on
2782
+ `Dialog`, `Sheet` or `Drawer`):
2783
+
2784
+ ```vue
2785
+ <DialogContent
2786
+ @open-auto-focus="(event) => { event.preventDefault(); nameInput?.focus(); }"
2787
+ >
2788
+ <input ref="nameInput" aria-label="Name" />
2789
+ </DialogContent>
2790
+ ```
2791
+
1993
2792
  ### A `CustomDialog` or `ConfirmationDialog` no longer opens from its `#trigger`
1994
2793
 
1995
2794
  **When:** after upgrading to 0.22.0, clicking what you put in the `#trigger`
@@ -2164,6 +2963,41 @@ Anything else that must not make the row follow its link on a click (a custom wi
2164
2963
  />
2165
2964
  ```
2166
2965
 
2966
+ ### A double click or Enter submits a form twice, or a test no longer finds a loading `Button` disabled
2967
+
2968
+ **When:** a sign-in or save form is submitted twice by a quick double click,
2969
+ or by Enter pressed twice in a field, although its submit button has
2970
+ `loading`; or `<Button as-child loading><a @click="save">` still calls
2971
+ `save`. Or, after upgrading, a test expecting a loading `Button` to be
2972
+ `disabled` (or to skip it with Tab) fails.
2973
+ **Why:** a `loading` that turns true only after an `await` (a mutation's
2974
+ pending flag behind an async check) left a window for the second click. And a
2975
+ loading `Button` used to set the native `disabled`, which drops focus to
2976
+ `<body>` while the form works. It now sets `aria-busy="true"` and
2977
+ `aria-disabled="true"` and cancels every click before your `@click` — the
2978
+ click Enter in a field sends to the form's submit button included — so
2979
+ neither your handler, an `as-child` child's nor an ancestor's runs, and stays
2980
+ focusable, as `@nxgt/material`'s does. With `as-child` the busy attributes,
2981
+ the click guard and the spinner land on the child element: the spinner goes
2982
+ inside it, and the child stays the same element (focus, your template ref)
2983
+ while `loading` turns on and off.
2984
+ **Fix:** pass `useForm`'s `isSubmitting`, which is true as soon as
2985
+ `handleSubmit` starts; for a `loading` that turns true later, add
2986
+ `lock-on-click`. In a test, check the busy state, not `disabled`:
2987
+
2988
+ ```vue
2989
+ <Button type="submit" :loading="isSubmitting">Sign in</Button>
2990
+ <Button type="submit" :loading="isPending" lock-on-click>Sign in</Button>
2991
+ ```
2992
+
2993
+ ```ts
2994
+ expect(screen.getByRole('button', { name: 'Sign in' })).toHaveAttribute('aria-busy', 'true');
2995
+ ```
2996
+
2997
+ A `form.requestSubmit()` of your own fires no click, so the guard does not
2998
+ see it: check `isSubmitting` before calling it (see
2999
+ [Forms](./guides/forms.md)).
3000
+
2167
3001
  ### A `DataTable` shows a sorting or a selection the URL never got
2168
3002
 
2169
3003
  **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.
@@ -2181,6 +3015,49 @@ Anything else that must not make the row follow its link on a click (a custom wi
2181
3015
  />
2182
3016
  ```
2183
3017
 
3018
+ ### A `DataTable` no longer scrolls inside a 400px box
3019
+
3020
+ **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.
3021
+ **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.
3022
+ **Fix:** pass `max-height` (a number of pixels, or any CSS length) where the table should scroll inside a box:
3023
+
3024
+ ```vue
3025
+ <DataTable :data="rows" :columns="columns" :max-height="400" />
3026
+ ```
3027
+
3028
+ `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.
3029
+
3030
+ ### A `DataTable` shows its header row on a phone
3031
+
3032
+ **When:** after upgrading, a `DataTable` under 768px wide shows its header row, which it used to hide.
3033
+ **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.
3034
+ **Fix:** nothing, usually. To hide it again, pass `show-header` — `false` everywhere, or your own breakpoint:
3035
+
3036
+ ```vue
3037
+ <DataTable :data="rows" :columns="columns" :show-header="false" />
3038
+ <DataTable :data="rows" :columns="columns" :show-header="!isPhone" />
3039
+ ```
3040
+
3041
+ ### A test's `getByRole('region')` finds more than one region
3042
+
3043
+ **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.
3044
+ **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.
3045
+ **Fix:** query your region by name, and give the table a name of its own where a page holds several:
3046
+
3047
+ ```vue
3048
+ <DataTable :data="rows" :columns="columns" scroll-label="Invoices" />
3049
+ ```
3050
+
3051
+ ```ts
3052
+ page.getByRole('region', { name: 'Invoices' });
3053
+ ```
3054
+
3055
+ ### A `DataTable` toolbar is gone
3056
+
3057
+ **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"]`).
3058
+ **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.
3059
+ **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.
3060
+
2184
3061
  ---
2185
3062
 
2186
3063
  ---