@nxgt/material 2.7.1 → 2.9.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 (223) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +9 -6
  3. package/dist/{_Uint8Array-JRUmWcRr.js → _Uint8Array-azThpjEP.js} +1 -1
  4. package/dist/{_overRest-DvC5rm5s.js → _overRest-DMn4Dt71.js} +1 -1
  5. package/dist/{abnfDiagram-O67JEVCF-Cl4ukf6a.js → abnfDiagram-O67JEVCF-D-rmT4BY.js} +4 -4
  6. package/dist/{architectureDiagram-NJMV4G6O-WmFlBQF5.js → architectureDiagram-NJMV4G6O-DOc_c_T9.js} +7 -7
  7. package/dist/{async-slice-OftEOtMA.js → async-slice-DhZDjEtw.js} +2 -2
  8. package/dist/{blockDiagram-BEXU5L5S-BM-gkWAO.js → blockDiagram-BEXU5L5S-BOsyK4E_.js} +11 -11
  9. package/dist/{c4Diagram-YGBWAQC7-D9xSt2QO.js → c4Diagram-YGBWAQC7-El4SkIzX.js} +8 -8
  10. package/dist/{channel-Dg92Hoym.js → channel-CnLB6790.js} +1 -1
  11. package/dist/{chunk-3FUC2YCW-Cf18L00V.js → chunk-3FUC2YCW-BCsKm3ZT.js} +8 -8
  12. package/dist/{chunk-5DYCD2WN-Cja1INgd.js → chunk-5DYCD2WN-Cd1zazNp.js} +3 -3
  13. package/dist/{chunk-6AEJRKK7-3YWRP4aV.js → chunk-6AEJRKK7-BLBlw8ui.js} +2 -2
  14. package/dist/{chunk-742MDFTN-Cs1qGiTK.js → chunk-742MDFTN-CsUjFyzK.js} +2 -2
  15. package/dist/{chunk-7INBJB4K-BY5n77ka.js → chunk-7INBJB4K-k-I67NSP.js} +6 -6
  16. package/dist/{chunk-7M6MHVWA-C3x6E3yT.js → chunk-7M6MHVWA-C3qpW8zM.js} +9 -9
  17. package/dist/{chunk-7PRAP22T-_Av9vcXz.js → chunk-7PRAP22T-0C6_H8Li.js} +1 -1
  18. package/dist/{chunk-GNY47TPC-D1E6DHDV.js → chunk-GNY47TPC-CzPNLW5k.js} +11 -11
  19. package/dist/{chunk-GTNCS2PH-BZ9WMD1v.js → chunk-GTNCS2PH-AJi9PVT7.js} +3 -3
  20. package/dist/{chunk-GWA4HPMP-Co-A6dtV.js → chunk-GWA4HPMP-B8jXI0Tl.js} +3 -3
  21. package/dist/{chunk-MBY4JIJT-C3RQP_Nk.js → chunk-MBY4JIJT-BFIqOe-8.js} +4 -4
  22. package/dist/{chunk-NETBCI7D-DJUuN4EY.js → chunk-NETBCI7D-tNinefdn.js} +1 -1
  23. package/dist/{chunk-O7XYJQB3-A66QSOPA.js → chunk-O7XYJQB3-BChweQ_V.js} +1 -1
  24. package/dist/{chunk-UA2S7LBM-BNeHeMx9.js → chunk-UA2S7LBM-ChuksCDg.js} +6 -6
  25. package/dist/{chunk-WEXAMYUT-CucJfehv.js → chunk-WEXAMYUT-CbFOhEve.js} +2 -2
  26. package/dist/{chunk-XXDRQBXY-FgXU14Bh.js → chunk-XXDRQBXY-dKXxu_aj.js} +1 -1
  27. package/dist/{chunk-Z7XXMR3K-DMTBxZxM.js → chunk-Z7XXMR3K-C9P-XjM2.js} +8 -8
  28. package/dist/{chunk-ZIGJFQKS-C3xUAXJq.js → chunk-ZIGJFQKS-9VXArDKF.js} +3 -3
  29. package/dist/{classDiagram-v2-NBCMYWYE-BDaxYvDy.js → classDiagram-v2-NBCMYWYE-D00ZobEC.js} +14 -14
  30. package/dist/clsx-CbxlCXZ3.js +16 -0
  31. package/dist/components/base-content.css +139 -0
  32. package/dist/components/base.css +150 -0
  33. package/dist/components/components.css +2 -0
  34. package/dist/components/reduced-motion.css +56 -0
  35. package/dist/components/theme-tokens.css +116 -0
  36. package/dist/components/ui/actions/button-group/button-group.css +1 -1
  37. package/dist/components/ui/feedback/toast/toast.css +21 -0
  38. package/dist/components/ui/primitives/label/label.css +33 -0
  39. package/dist/{components-DrStgM4J.js → components-km60bAdr.js} +34658 -34218
  40. package/dist/components.js +6 -6
  41. package/dist/{cose-bilkent-JH36ORCC-DRUT3qBb.js → cose-bilkent-JH36ORCC-P94vkHv_.js} +2 -2
  42. package/dist/{cynefinDiagram-VND7K2PF-B9HrdQW8.js → cynefinDiagram-VND7K2PF-EIpXbLXm.js} +4 -4
  43. package/dist/{dagre-6A5THRUB-DZF0nICj.js → dagre-6A5THRUB-D_F3_LlW.js} +20 -20
  44. package/dist/{diagram-22UHCM2B-CVZYpeev.js → diagram-22UHCM2B-CsxOExpW.js} +15 -15
  45. package/dist/{diagram-3UASUU5V-D7MmBJbM.js → diagram-3UASUU5V-Dl7AETV1.js} +7 -7
  46. package/dist/{diagram-ATOU4E4O-CNY-8kl0.js → diagram-ATOU4E4O-isL7nwg2.js} +3 -3
  47. package/dist/{diagram-CDSNMT55-C8Mk1smi.js → diagram-CDSNMT55-Bnhlo5at.js} +5 -5
  48. package/dist/{diagram-MLGK6HIB-CpfeILwW.js → diagram-MLGK6HIB-DvBcyz8v.js} +4 -4
  49. package/dist/{diagram-MPIPVDR6-Do7bUnTd.js → diagram-MPIPVDR6-iTevwmNE.js} +4 -4
  50. package/dist/{dist-C3YK_ean.js → dist-B6_-RPZ3.js} +1 -1
  51. package/dist/{dist-CRbPtqwD.js → dist-BvUQHUs_.js} +4 -4
  52. package/dist/dnd.js +1 -1
  53. package/dist/{ebnfDiagram-ZINNZB2B-1GewX9Zq.js → ebnfDiagram-ZINNZB2B-RVuQIpm0.js} +4 -4
  54. package/dist/{elk-276RUBZZ-DI4eicFj.js → elk-276RUBZZ-BK-NMLiI.js} +26 -26
  55. package/dist/{erDiagram-OPXOYQCR-CUbpVns3.js → erDiagram-OPXOYQCR-DC1GgS4l.js} +14 -14
  56. package/dist/field-blur--x_n3oMy.js +3637 -0
  57. package/dist/flowDiagram-KWPJA3E3-BUSlDNQP.js +17 -0
  58. package/dist/{ganttDiagram-FUAMR5RP-D3vmspam.js → ganttDiagram-FUAMR5RP-DOp1p_x0.js} +6 -6
  59. package/dist/{gitGraphDiagram-X574FWY7-80yOq8dE.js → gitGraphDiagram-X574FWY7-DerJL1Q-.js} +3 -3
  60. package/dist/{graphlib-DLJn7qS3.js → graphlib-DLjE3QPd.js} +6 -6
  61. package/dist/hooks.js +3 -3
  62. package/dist/{i18n-provider-DBFRz5uH.js → i18n-provider-CQL7fqLy.js} +1 -1
  63. package/dist/i18n.js +6 -6
  64. package/dist/icon-button-G9zdQ3Ix.js +2753 -0
  65. package/dist/{infoDiagram-VRGFBTTK-Cboh-B9n.js → infoDiagram-VRGFBTTK-CtVsLyCQ.js} +3 -3
  66. package/dist/{isArrayLikeObject-CO5xeuaJ.js → isArrayLikeObject-CO-5KAzG.js} +3 -3
  67. package/dist/{isEmpty-DOC38Ces.js → isEmpty-Bl1drJE7.js} +3 -3
  68. package/dist/{ishikawaDiagram-OU5B5YK6-DwTd1YxL.js → ishikawaDiagram-OU5B5YK6-D1n8vp8C.js} +4 -4
  69. package/dist/{journeyDiagram-ZHPQQLJL-Dxvl3oeN.js → journeyDiagram-ZHPQQLJL-DuQ9Th0y.js} +4 -4
  70. package/dist/{kanban-definition-PNTS6WVX-S8plI4wn.js → kanban-definition-PNTS6WVX-D1-Owjil.js} +10 -10
  71. package/dist/lib/components/ui/actions/button/busy-click-capture.d.ts +11 -0
  72. package/dist/lib/components/ui/actions/button/button-tones.d.ts +36 -0
  73. package/dist/lib/components/ui/actions/button/button.d.ts +19 -0
  74. package/dist/lib/components/ui/actions/button/button.wrap.d.ts +8 -0
  75. package/dist/lib/components/ui/actions/button/label-text.d.ts +10 -0
  76. package/dist/lib/components/ui/actions/button/use-click-lock.d.ts +18 -0
  77. package/dist/lib/components/ui/actions/copy-button/copy-button.d.ts +5 -1
  78. package/dist/lib/components/ui/actions/icon-button/icon-button.compound-variants.d.ts +114 -0
  79. package/dist/lib/components/ui/collections/data-table/hooks.d.ts +13 -13
  80. package/dist/lib/components/ui/collections/data-table/partials/data-table-empty-row.d.ts +5 -1
  81. package/dist/lib/components/ui/collections/data-table/partials/data-table-scroll-region.d.ts +16 -0
  82. package/dist/lib/components/ui/collections/data-table/partials/data-table-toolbar.d.ts +24 -0
  83. package/dist/lib/components/ui/collections/data-table/select-checkbox.d.ts +10 -0
  84. package/dist/lib/components/ui/collections/data-table/types.d.ts +9 -0
  85. package/dist/lib/components/ui/collections/data-table/use-data-table.d.ts +12 -1
  86. package/dist/lib/components/ui/collections/data-table/use-scroll-edges.d.ts +22 -0
  87. package/dist/lib/components/ui/dates/calendar/calendar-day.d.ts +8 -0
  88. package/dist/lib/components/ui/dates/calendar/calendar.d.ts +3 -3
  89. package/dist/lib/components/ui/dates/calendar/calendar.utils.d.ts +8 -0
  90. package/dist/lib/components/ui/dates/calendar/use-calendar-labels.d.ts +22 -0
  91. package/dist/lib/components/ui/display/description/description.d.ts +2 -1
  92. package/dist/lib/components/ui/display/list-tile/list-tile.d.ts +9 -1
  93. package/dist/lib/components/ui/display/list-tile/partials/list-tile-body.d.ts +11 -0
  94. package/dist/lib/components/ui/display/list-tile/partials/list-tile-classes.d.ts +10 -0
  95. package/dist/lib/components/ui/feedback/alert-dialog/alert-dialog.d.ts +1 -1
  96. package/dist/lib/components/ui/feedback/toast/partials/toast-actions.d.ts +3 -0
  97. package/dist/lib/components/ui/feedback/toast/toast.d.ts +27 -3
  98. package/dist/lib/components/ui/feedback/toast/toaster.d.ts +19 -0
  99. package/dist/lib/components/ui/feedback/toast/toasts.d.ts +3 -0
  100. package/dist/lib/components/ui/forms/field-blur.d.ts +7 -2
  101. package/dist/lib/components/ui/forms/use-field-blur.d.ts +8 -1
  102. package/dist/lib/components/ui/layout/app-shell/activity-content.d.ts +10 -1
  103. package/dist/lib/components/ui/layout/app-shell/activity-layout.d.ts +26 -2
  104. package/dist/lib/components/ui/layout/app-shell/app-header.d.ts +8 -2
  105. package/dist/lib/components/ui/layout/app-shell/app-sidebar.d.ts +8 -2
  106. package/dist/lib/components/ui/layout/app-shell/types.d.ts +3 -0
  107. package/dist/lib/components/ui/layout/app-shell/use-content-scroll.d.ts +10 -0
  108. package/dist/lib/components/ui/layout/app-shell/use-navigation-sheet.d.ts +19 -0
  109. package/dist/lib/components/ui/layout/sidebar/index.d.ts +5 -0
  110. package/dist/lib/components/ui/layout/sidebar/sidebar-content.d.ts +2 -0
  111. package/dist/lib/components/ui/layout/sidebar/sidebar-context.d.ts +7 -0
  112. package/dist/lib/components/ui/layout/sidebar/sidebar-footer.d.ts +2 -0
  113. package/dist/lib/components/ui/layout/sidebar/sidebar-header.d.ts +2 -0
  114. package/dist/lib/components/ui/layout/sidebar/sidebar-menu-item-tooltip.d.ts +7 -0
  115. package/dist/lib/components/ui/layout/sidebar/sidebar-menu-item.d.ts +31 -0
  116. package/dist/lib/components/ui/layout/sidebar/sidebar-menu-label.d.ts +4 -0
  117. package/dist/lib/components/ui/layout/sidebar/sidebar.d.ts +1 -12
  118. package/dist/lib/components/ui/overlays/context-menu/context-menu-items.d.ts +10 -0
  119. package/dist/lib/components/ui/overlays/context-menu/context-menu-sub.d.ts +8 -0
  120. package/dist/lib/components/ui/overlays/context-menu/context-menu.d.ts +4 -14
  121. package/dist/lib/components/ui/overlays/dialog/dialog-outside.d.ts +6 -0
  122. package/dist/lib/components/ui/overlays/dialog/dialog-scroll-content.d.ts +1 -1
  123. package/dist/lib/components/ui/overlays/dialog/dialog.d.ts +1 -1
  124. package/dist/lib/components/ui/overlays/dialog/use-return-focus.d.ts +3 -2
  125. package/dist/lib/components/ui/overlays/drawer/drawer.d.ts +1 -1
  126. package/dist/lib/components/ui/overlays/dropdown-menu/dropdown-menu-items.d.ts +11 -0
  127. package/dist/lib/components/ui/overlays/dropdown-menu/dropdown-menu-sub.d.ts +8 -0
  128. package/dist/lib/components/ui/overlays/dropdown-menu/dropdown-menu.d.ts +6 -21
  129. package/dist/lib/components/ui/overlays/popover/popover.d.ts +1 -1
  130. package/dist/lib/components/ui/overlays/sheet/sheet.d.ts +1 -1
  131. package/dist/lib/components/ui/primitives/card/card.d.ts +10 -1
  132. package/dist/lib/components/ui/primitives/link/link-component.d.ts +14 -0
  133. package/dist/lib/components/ui/primitives/tabs/partials/tabs-content.d.ts +3 -0
  134. package/dist/lib/components/ui/primitives/tabs/partials/tabs-indicator.d.ts +13 -0
  135. package/dist/lib/components/ui/primitives/tabs/partials/tabs-list.d.ts +3 -0
  136. package/dist/lib/components/ui/primitives/tabs/partials/tabs-trigger.d.ts +6 -0
  137. package/dist/lib/components/ui/primitives/tabs/tabs-classes.d.ts +27 -0
  138. package/dist/lib/components/ui/primitives/tabs/tabs-variant.d.ts +5 -0
  139. package/dist/lib/components/ui/primitives/tabs/tabs.d.ts +11 -7
  140. package/dist/lib/components/ui/selection/select/select.d.ts +1 -1
  141. package/dist/lib/components/ui/selection/select-chip-field/select-chip-field.d.ts +1 -1
  142. package/dist/lib/components/ui/widgets/emoji/types.d.ts +1 -6
  143. package/dist/lib/i18n/resources/en/en.d.ts +23 -0
  144. package/dist/lib/i18n/resources/en/material.json.d.ts +28 -5
  145. package/dist/lib/i18n/resources/fr/fr.d.ts +23 -0
  146. package/dist/lib/i18n/resources/fr/material.json.d.ts +28 -5
  147. package/dist/lib/i18n/resources/resources.d.ts +46 -0
  148. package/dist/lib/i18n/translator.d.ts +1 -1
  149. package/dist/lib/lib/redux/status-slice.d.ts +1 -1
  150. package/dist/lib.js +39 -55
  151. package/dist/main.js +13 -13
  152. package/dist/map.js +35 -35
  153. package/dist/material.css +1 -1
  154. package/dist/{merge-CuDTNgpm.js → merge-CwwtONfp.js} +4 -4
  155. package/dist/{mermaid.core-O7mtb1-p.js → mermaid.core-Dfr4zfuP.js} +49 -49
  156. package/dist/{mindmap-definition-NLK3R4M7-CO0E97EZ.js → mindmap-definition-NLK3R4M7-_qKWGSrk.js} +13 -13
  157. package/dist/{pegDiagram-GJSIUBJH-CAO6aYvj.js → pegDiagram-GJSIUBJH-CWKobqLl.js} +4 -4
  158. package/dist/{pieDiagram-5QR66LMP-BXxVfv5K.js → pieDiagram-5QR66LMP-B6at48Bd.js} +5 -5
  159. package/dist/print.js +2 -2
  160. package/dist/{quadrantDiagram-O4NWA36T-BbidZDM8.js → quadrantDiagram-O4NWA36T-Cr9bA0xJ.js} +3 -3
  161. package/dist/{railroadDiagram-XR7U4H2S-DmKDXg8V.js → railroadDiagram-XR7U4H2S-fsZ7oqPG.js} +4 -4
  162. package/dist/{requirementDiagram-PLB6GJNP-wFvgSWJP.js → requirementDiagram-PLB6GJNP-BBs-lByV.js} +13 -13
  163. package/dist/rolldown-runtime-BFFgcFxR.js +30 -0
  164. package/dist/{sankeyDiagram-IPEJSGJF-Cka5t09q.js → sankeyDiagram-IPEJSGJF-BzX37jGY.js} +3 -3
  165. package/dist/{sequenceDiagram-PO4LG4MO-B2qb55A5.js → sequenceDiagram-PO4LG4MO-Bgx1kuGA.js} +5 -5
  166. package/dist/{sortable-provider-C5VYhY8u.js → sortable-provider-WZT5I34N.js} +24 -24
  167. package/dist/{src-C9kQSo6K.js → src-CsLQInsN.js} +1 -1
  168. package/dist/{stateDiagram-v2-GCMORJYK-EMa7q3ju.js → stateDiagram-v2-GCMORJYK-Chc9Z6Mr.js} +14 -14
  169. package/dist/{stored-language-B0o4cBh8.js → stored-language-BW32owje.js} +56 -10
  170. package/dist/styles.css +10 -368
  171. package/dist/{swimlanes-2SLR337P-BtZ7Di5u.js → swimlanes-2SLR337P-D0yLvfot.js} +12 -12
  172. package/dist/swimlanesDiagram-TC7HE7FX-BOd0Czfs.js +28 -0
  173. package/dist/{timeline-definition-EJHVYXUP-2Z6UABw8.js → timeline-definition-EJHVYXUP-nDNR-Dt-.js} +5 -5
  174. package/dist/{translate-material-BCwQrt2G.js → translate-material-ZEzF-LKH.js} +2 -2
  175. package/dist/{translator-CwpmpI24.js → translator-BfqVlNGW.js} +1 -1
  176. package/dist/{use-heading-spy-BtBJiU63.js → use-heading-spy-DvZVu15k.js} +37 -37
  177. package/dist/{use-material-translation-DG_7z3Q7.js → use-material-translation-B2GUtxUY.js} +2 -2
  178. package/dist/{usecaseDiagram-POWQR4AR-DT56cp_B.js → usecaseDiagram-POWQR4AR-Bwen99ay.js} +13 -13
  179. package/dist/{utils-UBn-46YY.js → utils-DC8bw9A_.js} +346 -360
  180. package/dist/{vennDiagram-UO4OBE2U-CO0yFCI8.js → vennDiagram-UO4OBE2U-BpgP__1T.js} +4 -4
  181. package/dist/{wardleyDiagram-VNRHLVJA-QkwghAYo.js → wardleyDiagram-VNRHLVJA-CYqKMb2f.js} +4 -4
  182. package/dist/{xychartDiagram-PMCCYNJV-B2aZlrno.js → xychartDiagram-PMCCYNJV-b-Quuxoi.js} +7 -7
  183. package/docs/components/buttons-and-actions.md +60 -10
  184. package/docs/components/data-display.md +24 -2
  185. package/docs/components/data-table-filter-and-lists.md +88 -10
  186. package/docs/components/date-and-time.md +24 -0
  187. package/docs/components/feedback-and-status.md +85 -8
  188. package/docs/components/form-fields.md +14 -5
  189. package/docs/components/media-and-files.md +7 -0
  190. package/docs/components/navigation-and-layout.md +21 -3
  191. package/docs/components/overlays-and-menus.md +21 -6
  192. package/docs/components/primitives.md +96 -7
  193. package/docs/components/rich-widgets.md +10 -0
  194. package/docs/components/selection-and-choice.md +13 -2
  195. package/docs/getting-started.md +4 -2
  196. package/docs/guides/app-shell.md +97 -4
  197. package/docs/guides/forms.md +54 -5
  198. package/docs/guides/helpers.md +2 -2
  199. package/docs/guides/i18n.md +24 -4
  200. package/docs/guides/ssr.md +1 -1
  201. package/docs/guides/theming.md +147 -5
  202. package/docs/previews/data-display-codegroup.webp +0 -0
  203. package/docs/previews/data-display-description.webp +0 -0
  204. package/docs/previews/feedback-status-toast.webp +0 -0
  205. package/docs/previews/primitives-tabs.webp +0 -0
  206. package/docs/previews/rich-widgets-emojipicker.webp +0 -0
  207. package/docs/roadmap.md +29 -1
  208. package/docs/troubleshooting.md +937 -11
  209. package/lib/components/base-content.css +139 -0
  210. package/lib/components/base.css +150 -0
  211. package/lib/components/components.css +2 -0
  212. package/lib/components/reduced-motion.css +56 -0
  213. package/lib/components/theme-tokens.css +116 -0
  214. package/lib/components/ui/actions/button-group/button-group.css +1 -1
  215. package/lib/components/ui/feedback/toast/toast.css +21 -0
  216. package/lib/components/ui/primitives/label/label.css +33 -0
  217. package/lib/styles.css +10 -368
  218. package/package.json +2 -2
  219. package/dist/field-blur-DWQtxiAe.js +0 -3483
  220. package/dist/flowDiagram-KWPJA3E3-DZxumFfu.js +0 -17
  221. package/dist/icon-button-BxySFfrr.js +0 -2718
  222. package/dist/lib/components/ui/widgets/emoji/emoji.groups.d.ts +0 -6
  223. package/dist/swimlanesDiagram-TC7HE7FX-CROh8GTF.js +0 -28
@@ -19,6 +19,28 @@ words in your error.
19
19
 
20
20
  - [Components render unstyled, with no error](#components-render-unstyled-with-no-error)
21
21
  - [`<Icon>` renders nothing](#icon-renders-nothing)
22
+ - [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)
23
+ - [`ARIA hidden element must not be focusable or contain focusable elements`](#aria-hidden-element-must-not-be-focusable-or-contain-focusable-elements)
24
+ - [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)
25
+ - [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)
26
+ - [Dialogs, toasts or buttons no longer animate](#dialogs-toasts-or-buttons-no-longer-animate)
27
+ - [A Button's label overflows a card on a phone](#a-buttons-label-overflows-a-card-on-a-phone)
28
+
29
+ **Feedback**
30
+
31
+ - [A toast with an action never goes away](#a-toast-with-an-action-never-goes-away)
32
+ - [A toast covers the form's button on a phone](#a-toast-covers-the-forms-button-on-a-phone)
33
+ - [A toast never goes away after a tap](#a-toast-never-goes-away-after-a-tap)
34
+ - [A toast's action cannot be clicked while a dialog is open](#a-toasts-action-cannot-be-clicked-while-a-dialog-is-open)
35
+ - [A `ConfirmationDialog` reopened with new content blocks clicks](#a-confirmationdialog-reopened-with-new-content-blocks-clicks)
36
+ - [The first Escape after closing a nested dialog does nothing](#the-first-escape-after-closing-a-nested-dialog-does-nothing)
37
+
38
+ **Navigation**
39
+
40
+ - [A `ListTile` link reloads the whole page](#a-listtile-link-reloads-the-whole-page)
41
+ - [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)
42
+ - [`Tabs` look different after upgrading](#tabs-look-different-after-upgrading)
43
+ - [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)
22
44
 
23
45
  **SSR & build**
24
46
 
@@ -73,18 +95,24 @@ words in your error.
73
95
  - [A translation key is rendered instead of its text](#a-translation-key-is-rendered-instead-of-its-text)
74
96
  - [A label shows `{count}` or another placeholder](#a-label-shows-count-or-another-placeholder)
75
97
  - [Zod messages ignore the language, or your `material.zod.*` strings](#zod-messages-ignore-the-language-or-your-materialzod-strings)
98
+ - [A test asserting the old Zod string length text fails](#a-test-asserting-the-old-zod-string-length-text-fails)
99
+ - [A calendar's weekday row is in English on a French page](#a-calendars-weekday-row-is-in-english-on-a-french-page)
76
100
 
77
101
  **Runtime**
78
102
 
79
103
  - [`must be used within` / `must be used inside` (context providers)](#must-be-used-within--must-be-used-inside)
80
104
  - [`Camera components must be used inside <Camera>`](#camera-components-must-be-used-inside-camera)
81
105
  - [`Clipboard is not available`](#clipboard-is-not-available)
106
+ - [A test no longer finds `CopyButton` by "Copied", or its name changes](#a-test-no-longer-finds-copybutton-by-copied-or-its-name-changes)
82
107
  - [`Upload failed with status 403`](#upload-failed-with-status-403)
83
108
  - [`Network error during upload`](#network-error-during-upload)
84
109
  - [`Maximum number of presets (20) reached`](#maximum-number-of-presets-20-reached)
110
+ - [`` Cannot update a component (`DataTable`) while rendering a different component ``](#cannot-update-a-component-datatable-while-rendering-a-different-component)
85
111
  - [`Failed to execute 'exitFullscreen' on 'Document': Document not active`](#failed-to-execute-exitfullscreen-on-document-document-not-active)
86
112
  - [A `SwitchFormField` shows no validation message](#a-switchformfield-shows-no-validation-message)
87
113
  - [A `TimeFormField` shows its error on a desktop but not on a phone](#a-timeformfield-shows-its-error-on-a-desktop-but-not-on-a-phone)
114
+ - [`setError(…, { shouldFocus: true })` or a failed submit does not focus the field](#seterror--shouldfocus-true--or-a-failed-submit-does-not-focus-the-field)
115
+ - [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)
88
116
  - [`touchedFields` stays empty after leaving a field, or `mode: 'onBlur'` never validates](#touchedfields-stays-empty-after-leaving-a-field-or-mode-onblur-never-validates)
89
117
  - [`TypeError: values.map is not a function` (a `SliderFormField`)](#typeerror-valuesmap-is-not-a-function)
90
118
  - [An end-to-end test no longer finds `aria-expanded` on a `ResponsiveButton`](#an-end-to-end-test-no-longer-finds-aria-expanded-on-a-responsivebutton)
@@ -104,20 +132,36 @@ words in your error.
104
132
  - [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)
105
133
  - [A test no longer finds `aria-invalid` on a `CheckboxGroup`](#a-test-no-longer-finds-aria-invalid-on-a-checkboxgroup)
106
134
  - [A test no longer finds `aria-invalid` on a `Switch` row or an `Autocomplete`'s chips](#a-test-no-longer-finds-aria-invalid-on-a-switch-row-or-an-autocompletes-chips)
135
+ - [A test no longer finds `aria-invalid` on an `OtpField` slot](#a-test-no-longer-finds-aria-invalid-on-an-otpfield-slot)
107
136
  - [A test no longer finds the breadcrumb's home button](#a-test-no-longer-finds-the-breadcrumbs-home-button)
137
+ - [A test no longer finds a sidebar entry by the `button` role](#a-test-no-longer-finds-a-sidebar-entry-by-the-button-role)
138
+ - [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)
139
+ - [A `className` on `ActivityLayout` now styles the whole shell](#a-classname-on-activitylayout-now-styles-the-whole-shell)
140
+ - [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)
141
+ - [`[data-test="typogaphy"]` finds nothing](#data-testtypogaphy-finds-nothing)
142
+ - [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)
143
+ - [A test no longer finds a calendar day by its number, or by "Today" or "selected"](#a-test-no-longer-finds-a-calendar-day-by-its-number-or-by-today-or-selected)
108
144
  - [`#<name>` no longer finds a `SwitchFormField` or `CheckboxFormField`](#name-no-longer-finds-a-switchformfield-or-checkboxformfield)
109
145
  - [`#<name>` or `#<option value>` no longer finds a radio or checkbox group or one of its options](#name-or-option-value-no-longer-finds-a-radio-or-checkbox-group-or-one-of-its-options)
146
+ - [`#<name>` no longer finds a text, OTP, number, date or picker form field, or a second form's field has no name](#name-no-longer-finds-a-text-otp-number-date-or-picker-form-field-or-a-second-forms-field-has-no-name)
147
+ - [A double click or Enter submits a form twice, or a test finds `aria-busy` in an idle `Button`](#a-double-click-or-enter-submits-a-form-twice-or-a-test-finds-aria-busy-in-an-idle-button)
110
148
  - [An empty `UploadField` or `TimeField` is no longer tinted](#an-empty-uploadfield-or-timefield-is-no-longer-tinted)
111
149
  - [`ConfirmDialog` buttons stay in English](#confirmdialog-buttons-stay-in-english)
112
150
  - [A `ConfirmationDialog` opened with `open` no longer closes](#a-confirmationdialog-opened-with-open-no-longer-closes)
113
151
  - [A menu item selected by `[data-variant=destructive]` stops matching](#a-menu-item-selected-by-data-variantdestructive-stops-matching)
152
+ - [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)
114
153
  - [A `Stepper` step selected by `#step-N` stops matching](#a-stepper-step-selected-by-step-n-stops-matching)
115
154
  - [Keys typed right after opening a dialog, sheet or drawer land inside it](#keys-typed-right-after-opening-a-dialog-sheet-or-drawer-land-inside-it)
155
+ - [Focus moves into a dialog although `onOpenAutoFocus` prevented it](#focus-moves-into-a-dialog-although-onopenautofocus-prevented-it)
116
156
  - [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)
117
157
  - [Focus is no longer in a `Tree` after it renders](#focus-is-no-longer-in-a-tree-after-it-renders)
118
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)
119
159
  - [A row does not navigate after `onRowClick` ran](#a-row-does-not-navigate-after-onrowclick-ran)
120
160
  - [A `DataTable` Previous or Next button is disabled](#a-datatable-previous-or-next-button-is-disabled)
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
 
@@ -332,6 +376,419 @@ public/assets/icons/sprites/sharp-solid.svg ← <Icon name="atom" family="sharp"
332
376
 
333
377
  Serve one sheet per style you use, from your own Font Awesome Pro kit.
334
378
 
379
+ ### An accessibility audit reports low contrast on a badge, a button, a toast or a menu entry
380
+
381
+ **When:** axe (or Lighthouse) reports `color-contrast` on a `Badge` with
382
+ `variant="success"`, `"info"`, `"warning"` or `"secondary"`; on a filled
383
+ `secondary`, `success`, `info` or `warning` `Button` or `IconButton` in the
384
+ dark theme; on a filled `Button` or `IconButton` while it is hovered, focused
385
+ or pressed; on muted text on a `bg-muted` surface (an `Alert`
386
+ `variant="foreground"`, the default `Timeline` marker, code) or on the
387
+ "paper" tint; on a toast's title or description while it enters, or its X
388
+ button; or on the muted text (a shortcut, the `LanguageSwitcher` code) of a
389
+ highlighted menu entry, or on a calendar's weekday row in the dark theme.
390
+ **Why:** the colours come from your tokens and the components' state tints.
391
+ Before this fix, concretely: the light `--secondary`,
392
+ `--success`, `--info` and `--warning` gave their near-white text 4.3:1,
393
+ 3.5:1, 2.5:1 and 3.4:1; the light `--muted-foreground` read 4.3:1 on
394
+ `--muted` and 4.4:1 on the paper tint, and the dark `--muted` was the light,
395
+ near-white value, so muted text on `bg-muted` failed in the dark theme too;
396
+ in the dark theme a filled `secondary`, `success`, `info` or `warning` button
397
+ gave 4.3:1, 3.5:1, 2.5:1 and 3.4:1, and its hover, focus and pressed tints
398
+ less; in the light theme a filled button faded toward the page when hovered,
399
+ focused or pressed (`/90`, `/80`, `/70`: down to 3.1:1 pressed); a library
400
+ toast started at opacity 0, so an audit run while it entered measured its
401
+ text faded; and the toast and the highlighted entry used the muted colour on
402
+ a background it does not reach 4.5:1 on; the dark `--muted-foreground` of a
403
+ calendar's weekday row reached 3.7:1 on a popover. If the audit still fails, your
404
+ stylesheet overrides one of those tokens, a `className` of your own puts the old
405
+ state tints back, or you still ship one of those versions.
406
+ **Fix:** update, remove the override, or pick a colour that keeps 4.5:1 with
407
+ its `-foreground`:
408
+
409
+ ```css
410
+ :root {
411
+ --info: oklch(0.5 0.165 254.624); /* 5.8:1 with --info-foreground */
412
+ }
413
+ ```
414
+
415
+ A toast the library renders, through `toast()` or `toasts.*` from
416
+ `@nxgt/material` (sonner's `toast.custom` path, marked
417
+ `data-styled="false"`), no longer fades its text in: the wrapper stays opaque
418
+ while it slides in, so an audit run while it enters measures the settled
419
+ colours. A toast sonner styles itself still fades in from opacity 0: a plain
420
+ `toast('…')` or `toast.success('…')` from `sonner`, or `sonnerToast` from this
421
+ package. Show it through `toasts.*` instead. If a library toast still reports
422
+ its title as low contrast, an override of `[data-sonner-toast]` sets its
423
+ `opacity` back to 0 on mount.
424
+
425
+ The muted text on a `bg-muted` surface (an `Alert` `variant="foreground"`, the
426
+ default `Timeline` marker) and on the "paper" tint failed in the shipped
427
+ palette too (4.3:1 and 4.4:1 in light; the dark `--muted` was the light one).
428
+ They pass since the `--muted` and `--muted-foreground` tokens changed: if
429
+ yours still fail, you override them.
430
+
431
+ In the dark theme, a filled `secondary` `Button` gave its near-white text
432
+ 4.3:1, a `tonal` secondary one 3.9:1 (3.4:1 on `--card`), and a filled
433
+ `success`, `info` or `warning` one 3.5:1, 2.5:1 and 3.4:1. The dark
434
+ `--secondary` is brighter (`oklch(0.66 ...)`, 6.5:1 as text on
435
+ `--background`), and the filled `secondary`, `success`, `info` and `warning`
436
+ `Button`s and `IconButton`s are tinted in the dark theme
437
+ (`dark:bg-secondary/70`, `dark:bg-success/60`, …) so their text keeps 4.5:1.
438
+ A filled button given a background of your own through `className` loses the
439
+ tint in dark mode: add a matching `dark:` one.
440
+
441
+ ### `ARIA hidden element must not be focusable or contain focusable elements`
442
+
443
+ **When:** an accessibility audit (axe, Lighthouse, the Storybook
444
+ accessibility panel) runs while a `Dialog`, `Sheet`, `Drawer`,
445
+ `AlertDialog`, `CommandDialog`, a `Select` list, a `ContextMenu`, or a
446
+ `modal` `DropdownMenu` or `Popover` is open, and reports `aria-hidden-focus`
447
+ on the page behind it: its buttons, links and fields.
448
+ **Why:** Radix marks the rest of the page `aria-hidden` while a modal layer
449
+ is open and leaves it focusable; a control the tab order reaches inside an
450
+ `aria-hidden` subtree is what the rule reports. A version of @nxgt/material that
451
+ reports it predates the inert background.
452
+ **Fix:** upgrade: the page behind a modal layer of this library is `inert`
453
+ as well — out of the tab order and of the accessibility tree — and the rule
454
+ passes. Live regions (the `Toaster`) stay reachable. Do not exclude
455
+ `[aria-hidden]` or `[data-aria-hidden]` from the audit: it hides real
456
+ failures. A layer of yours built on Radix directly (not on these
457
+ components) still marks the page `aria-hidden` only; make the rest of the
458
+ page inert with core's `inertOthers`, which keeps live regions and counts
459
+ nested layers:
460
+
461
+ ```tsx
462
+ import { inertOthers } from '@nxgt/material-core/inert-others';
463
+
464
+ // content: your layer's content element, in state (a callback ref).
465
+ useEffect(
466
+ () => (open && content ? inertOthers(content) : undefined),
467
+ [open, content],
468
+ );
469
+ ```
470
+
471
+ Elements mounted after the layer opens are not made inert: mount them
472
+ first, or pass them to `inertOthers` in `keep` when they must stay
473
+ reachable.
474
+
475
+ ### An accessibility audit reports low contrast on a tonal or outlined button, a selected list tile or a hovered badge link
476
+
477
+ **When:** axe (or Lighthouse) reports `color-contrast` on a `tonal` or
478
+ `outlined` `Button` or `IconButton` (`primary` in the dark theme, `error`,
479
+ `warning` and `success`), at rest or hovered, focused or pressed; on the
480
+ subtitle of a selected `ListTile`; or on a `Badge` link while it is hovered.
481
+ **Why:** those variants drew their text in the token itself, and some tokens
482
+ do not reach 4.5:1 as text on the page and on their own tint: the dark
483
+ `--primary` is a dark indigo (2.2:1 on `--card`), the light `--error` gave
484
+ 3.6:1 on its tonal tint, `--warning` 4.3:1, the dark `--success` 4.0:1 on
485
+ `--card`. The selected tile's tint brought the muted subtitle to 4.4:1, and a
486
+ `Badge` link faded toward the page on hover (`/90`: down to 3.0:1 for `info`
487
+ in the dark theme). A release that has the fix mixes those tones from the
488
+ tokens, toward black in the light theme and toward white in the dark one; if
489
+ the audit still fails, a text colour of your own replaces the tone, or a
490
+ token of yours is too light (or, in the dark theme, too dark) to read as text.
491
+ **Fix:** update, then drop the text colour you put on the button, or check
492
+ your token against the page at 4.5:1:
493
+
494
+ ```tsx
495
+ // before: the tone is replaced by the raw token
496
+ <Button variant="tonal" color="error" className="text-error">Delete</Button>
497
+ // after
498
+ <Button variant="tonal" color="error">Delete</Button>
499
+ ```
500
+
501
+ A `Badge` link of your own with a hover fade such as `[a&]:hover:bg-error/90`
502
+ in its `className` puts the fade back: darken instead, as in
503
+ `[a&]:hover:bg-[color-mix(in_oklab,var(--color-error)_90%,black)]` (with
504
+ your badge's colour in place of `error`). A
505
+ subtitle colour of your own on a selected `ListTile` must reach 4.5:1 on
506
+ `bg-primary/15`.
507
+
508
+ ### An accessibility audit reports low contrast on error text in the dark theme
509
+
510
+ **When:** an error message, a validation hint or a red value of your own, in
511
+ the dark theme, fails 4.5:1 (about 4.2:1 on the page, 3.8:1 on a card); the
512
+ catalogue's own field messages pass.
513
+ **Why:** it is drawn with `text-error`, the fill colour. The dark `--error` is
514
+ kept dark enough for near-white text on a filled error surface, which makes it
515
+ too dark to read as text on the dark surfaces. Error text has its own token,
516
+ `--error-text`, lightened in the dark theme (5.9:1 on `--background`, 5.3:1 on
517
+ `--card`).
518
+ **Fix:** draw text with `text-error-text`; keep `text-error` for fills,
519
+ borders and icons that stand alone. On a `Button` or `IconButton` drop the
520
+ class instead: the tone already colours the text:
521
+
522
+ ```tsx
523
+ // before
524
+ <p className="text-sm text-error">Enter a valid email</p>
525
+ // after
526
+ <p className="text-sm text-error-text">Enter a valid email</p>
527
+ ```
528
+
529
+ Setting `--error` or `--error-text` on an element below the root (a `style`
530
+ on a section) changes nothing: `@theme` resolves `--color-error-text` on the
531
+ root. Below the root, set `--color-error-text` (and `--color-error` for fills)
532
+ on that element, or put the override on the root.
533
+
534
+ ### Dialogs, toasts or buttons no longer animate
535
+
536
+ **When:** a transition or an animation of a catalogue component does not run
537
+ on your machine, while it does on a colleague's.
538
+ **Why:** the operating system has "Reduce motion" on. `styles.css` honours
539
+ `prefers-reduced-motion: reduce` for every component (dialogs, sheets, menus,
540
+ toasts, buttons) and leaves only the `Spinner` and
541
+ `animate-spin` elements turning.
542
+ **Fix:** none is needed, this is the intended behaviour. To test the animated
543
+ version, turn the setting off (or, in Chrome DevTools, Rendering, "Emulate CSS
544
+ media feature prefers-reduced-motion": no-preference). To keep one animation
545
+ for everyone, override it with an `!important` rule on that element inside
546
+ `@media (prefers-reduced-motion: reduce)`.
547
+
548
+ ### A Button's label overflows a card on a phone
549
+
550
+ **When:** on a narrow screen, a `Button` with a long label sticks out of its
551
+ card, dialog or column (a 349 px button in a 343 px card), or its label is
552
+ clipped at both ends with no "…".
553
+ **Why:** before this change, the button took its label's whole width
554
+ (`width: max-content`) whatever its container. A button is now never wider
555
+ than its container (`max-width: 100%`), and its text is put in a
556
+ `<span data-slot="button-label">` (one span per run of text, so `Delete {name}?` is one piece) that shrinks and ends in "…". The reverse
557
+ is the same change: a button that grew past its container now truncates.
558
+ The ellipsis needs the text as the button's own child: with `asChild`, text
559
+ inside the child element is clipped without one.
560
+ **Fix:** update. Then pick, for each long label: leave the ellipsis (give a
561
+ `title` so a sighted user can read it whole; the accessible name already
562
+ is), let it break onto lines with `wrap`, or give the button a width of
563
+ your own through `className`. With `asChild`, put the label in a `<span>`:
564
+
565
+ ```tsx
566
+ <Button title="Continue to the payment details">Continue to the payment details</Button>
567
+ <Button wrap>Continue to the payment details and confirm the order</Button>
568
+ <Button className="w-full">Continue</Button>
569
+ <Button asChild>
570
+ <a href="/pay"><span>Continue to the payment details</span></a>
571
+ </Button>
572
+ ```
573
+
574
+ ## Feedback
575
+
576
+ ### A toast with an action never goes away
577
+
578
+ **When:** a test, or a user, waits for a toast to disappear by itself, and one
579
+ that carries an `action` (`toast({ action })`, or `toasts.info({ action })`
580
+ and the other presets with your own action) is still there.
581
+ **Why:** a toast with an `action` is persistent: `toast()` sets
582
+ `options.duration` to `Infinity` (WCAG 2.2.1: a timed toast can vanish before
583
+ a keyboard or screen-reader user reaches its button). Without an `action` a
584
+ preset times out after 5 seconds.
585
+ **Fix:** pass a duration, or close it:
586
+
587
+ ```ts
588
+ toasts.info({ action, options: { duration: 8000 } }); // times out again
589
+ toasts.dismiss(id); // or toasts.dismiss() for all of them
590
+ ```
591
+
592
+ Users close it with its X button (named "Close", or "Fermer" in French),
593
+ with Escape while focus is inside it, or with the action itself. In a test,
594
+ click `getByRole('button', { name: 'Close' })` or call `toasts.dismiss()`.
595
+
596
+ ### A toast covers the form's button on a phone
597
+
598
+ **When:** on a phone, a toast shows across the bottom of the screen, over a
599
+ form's submit button (or anything else kept at the bottom), until it goes.
600
+ **Why:** under 600px wide, Sonner lays toasts out across the whole width, and
601
+ its default `position` is `bottom-right`, so the bottom of the screen. The
602
+ `Toaster` now shows them at the top (`top-center`) on a phone when it is given
603
+ no `position`; it still covers the bottom when the app mounts Sonner's own
604
+ `Toaster` (`import { Toaster } from 'sonner'`), or passes a bottom `position`,
605
+ which applies on every screen.
606
+ **Fix:** mount the `Toaster` from `@nxgt/material/components`, and drop a bottom
607
+ `position` unless the bottom is what you want on phones too. When a sticky
608
+ header must stay visible above the toasts, raise the phone offset's `top`:
609
+
610
+ ```tsx
611
+ <Toaster /> // top-center on a phone
612
+ <Toaster mobileOffset={{ top: 72 }} />
613
+ <Toaster position="bottom-center" /> // bottom-center on every screen
614
+ ```
615
+
616
+ ### A toast never goes away after a tap
617
+
618
+ **When:** on a phone (or any touch screen), a toast that should time out stays
619
+ on the screen after the user tapped it, and only goes after a tap somewhere
620
+ else.
621
+ **Why:** a toast pauses while a mouse hovers it (WCAG 2.2.1). After a tap, the
622
+ browser emulates a mouse that arrives over the toast and never leaves, and
623
+ Sonner read that as a hover, so the toast stayed paused.
624
+ **Fix:** update, and mount the `Toaster` from `@nxgt/material/components`: it
625
+ ignores the hover a tap emulates (`@nxgt/material-core/touch-hover`), so a
626
+ tapped toast pauses while the finger is down and goes after its duration; a
627
+ mouse still pauses it. Sonner's own `Toaster`
628
+ (`import { Toaster } from 'sonner'`) keeps the old behaviour. A toast with an
629
+ `action` is meant to stay: see
630
+ [A toast with an action never goes away](#a-toast-with-an-action-never-goes-away).
631
+
632
+ ### A toast's action cannot be clicked while a dialog is open
633
+
634
+ **When:** a toast with an `action` (or a close button) shows over an open
635
+ `Dialog`, `AlertDialog`, `Sheet` or `Drawer`: the pointer does nothing on it,
636
+ or a click on it closes the dialog.
637
+ **Why:** a modal layer sets `pointer-events: none` on `<body>`, and the
638
+ `Toaster` inherited it, so its toasts could not be clicked; the layer also
639
+ counted a pointer down on a toast as outside the dialog.
640
+ **Fix:** update `@nxgt/material` and mount its `Toaster`: it sets
641
+ `pointer-events: auto`, and the modal contents treat a pointer down or a focus
642
+ inside the `Toaster` as inside, so the action runs and the dialog stays open.
643
+ Sonner's own `Toaster` keeps the old behaviour.
644
+
645
+ ### A `ConfirmationDialog` reopened with new content blocks clicks
646
+
647
+ **When:** a `ConfirmationDialog` or a `ConfirmDialog` (or a `Dialog`,
648
+ `AlertDialog` or `Sheet` of your own) closes on Confirm and your page opens it
649
+ again straight away with the next step's title and text. The second dialog
650
+ shows, but its Confirm does nothing: a click lands on the dimmed overlay.
651
+ Giving the dialog a `key` per step made it go away.
652
+ **Why:** Radix portals the overlay and the content to `<body>` separately, and
653
+ each leaves when its own exit animation ends: the overlay's (150ms) before the
654
+ content's (200ms for a dialog, 300ms for a sheet). Reopened between the two,
655
+ the new overlay was appended after the content still on the page and, at the
656
+ same `z-index`, painted over it. Nothing was left behind once both closed:
657
+ `<body>` got its `pointer-events` back.
658
+ **Fix:** update: the overlay is now kept under its content. The `key` is no
659
+ longer needed; one dialog whose `title` follows the step is enough:
660
+
661
+ ```tsx
662
+ const [step, setStep] = useState(0);
663
+ const [open, setOpen] = useState(false);
664
+
665
+ <ConfirmationDialog
666
+ open={open}
667
+ onOpenChange={setOpen}
668
+ title={steps[step].title}
669
+ onConfirm={async () => {
670
+ if (step === 0) {
671
+ setStep(1);
672
+ await saveDraft(); // the dialog has closed; reopen it with step 2
673
+ setOpen(true);
674
+ }
675
+ }}
676
+ />
677
+ ```
678
+
679
+ ### The first Escape after closing a nested dialog does nothing
680
+
681
+ **When:** an `AlertDialog` (a `ConfirmationDialog`, a `Dialog`, a `Sheet` or a
682
+ `Drawer`) opened over another overlay — a `Sheet` with a Delete that asks for
683
+ confirmation — is closed with Escape or Cancel, and the user presses Escape
684
+ again at once to close the `Sheet` too. The `Sheet` stays open; a second
685
+ Escape, a moment later, closes it. A test that presses Escape twice in a row
686
+ fails the same way.
687
+ **Why:** Radix hands Escape to the highest overlay only, and an overlay stops
688
+ being one when its content leaves the page, not when it closes. The closed
689
+ alert stays on the page for its exit animation (200ms for a dialog, 300ms for
690
+ a sheet, 500ms for a drawer): an Escape pressed meanwhile went to it, and it
691
+ was already closed. Moving focus back to the `Sheet` sooner would not help:
692
+ Radix listens for Escape on the document, wherever focus is.
693
+ **Fix:** update: `AlertDialogContent`, `DialogContent`, `SheetContent` and
694
+ `DrawerContent` now press Escape again, for the overlay below, once a
695
+ content that was closing has left. Nothing to change in your code. Your
696
+ `onEscapeKeyDown` still runs first, on both presses; preventing the default
697
+ while yours is closing keeps the key from being forwarded:
698
+
699
+ ```tsx
700
+ const [confirm, setConfirm] = useState(false);
701
+
702
+ <AlertDialog open={confirm} onOpenChange={setConfirm}>
703
+ <AlertDialogContent
704
+ onEscapeKeyDown={(event) => {
705
+ if (!confirm) event.preventDefault(); // closing: the Sheet stays open
706
+ }}
707
+ >
708
+ …
709
+ </AlertDialogContent>
710
+ </AlertDialog>
711
+ ```
712
+
713
+ With no overlay below, the forwarded Escape reaches the page's own `keydown`
714
+ handlers; an app shortcut that should ignore it can check `event.isTrusted` or
715
+ `event.defaultPrevented`.
716
+
717
+ In a test, wait for the `Sheet` to leave the DOM (the forwarded Escape lands
718
+ a few frames after the alert has gone) and look it up by selector rather
719
+ than by role: the closing alert still hides the page from the accessibility
720
+ tree.
721
+
722
+ ## Navigation
723
+
724
+ ### A `ListTile` link reloads the whole page
725
+
726
+ **When:** a `ListTile` with an `href` is clicked inside a router app: the
727
+ browser loads the document again, the page flashes, and client state is lost.
728
+ **Why:** by default the tile renders a plain `<a href>`, which needs no router
729
+ and so cannot navigate on the client.
730
+ **Fix:** pass `linkComponent`, with the same contract as the app shell's: it
731
+ takes `href`, spreads every other prop and forwards `ref`. React Router's
732
+ `Link` takes `to`, so wrap it:
733
+
734
+ ```tsx
735
+ import { Link } from 'react-router';
736
+ import type { AppShellLinkComponent } from '@nxgt/material/components';
737
+ import { ListTile } from '@nxgt/material/components';
738
+
739
+ // Hoisted, outside the render: not a new component on every render.
740
+ const RouterLink: AppShellLinkComponent = ({ href, ...props }) => (
741
+ <Link to={href} {...props} />
742
+ );
743
+
744
+ <ListTile title="Notifications" href="/settings/notifications" linkComponent={RouterLink} />
745
+ ```
746
+
747
+ ### The phone menu needs two Escapes to close, or a tooltip shows over it
748
+
749
+ **When:** on a phone, the menu sheet of `ActivityLayout` or `AppHeader` opens
750
+ with a tooltip repeating the first entry's label over the menu, and a plain
751
+ open sheet needs two Escapes to close: the first one closes the tooltip.
752
+ **Why:** the entries of the sheet's expanded sidebar carried a tooltip, and
753
+ focus lands on the first entry as the sheet opens, which opened its tooltip;
754
+ the tooltip's own layer takes the first Escape.
755
+ **Fix:** update `@nxgt/material`. An entry of an expanded `Sidebar` now shows no
756
+ tooltip, since it shows its label; a collapsed one keeps it, and an explicit
757
+ `tooltip` is always shown. A `SidebarMenuItem` of your own needs no change.
758
+
759
+ ### `Tabs` look different after upgrading
760
+
761
+ **When:** a `Tabs` with no `variant`, or with `variant="bar"`, renders a row of
762
+ tabs over a border with a bar under the active one, where it rendered
763
+ boxed tabs, a primary ring around the active one;
764
+ one with `variant="line"` renders a rounded pill.
765
+ **Why:** `underline` is now the default look. `bar` and `line` are deprecated
766
+ aliases: `bar` renders as `underline`, `line` as `segmented`.
767
+ **Fix:** write the look you want. `underline` needs no `variant`; for a pill,
768
+ set `segmented`:
769
+
770
+ ```tsx
771
+ <Tabs defaultValue="week" variant="segmented">
772
+ ```
773
+
774
+ A test that queried the old classes should query the state instead:
775
+ `getByRole('tab', { selected: true })`, and `data-variant` on
776
+ `[data-slot="tabs"]` for the look.
777
+
778
+ ### A screen reader announces an icon-only tab as "tab", with no name
779
+
780
+ **When:** a `TabsTrigger` holds only an icon; an accessibility audit reports a
781
+ tab with no accessible name.
782
+ **Why:** a tab is named by its text, and an icon has none.
783
+ **Fix:** give the trigger `aria-label`, and `title` for a hover hint:
784
+
785
+ ```tsx
786
+ <TabsTrigger value="recent" icon={<Clock />} aria-label="Recent" title="Recent" />
787
+ ```
788
+
789
+ `EmojiList` and `EmojiPicker` name their group tabs themselves; update
790
+ `@nxgt/material` if yours read "tab" only.
791
+
335
792
  ## SSR & build
336
793
 
337
794
  ### `ReferenceError: window is not defined`
@@ -1184,8 +1641,59 @@ z.config({ customError: zodLocaleError({ storageKey: 'lang', translate }) });
1184
1641
  z.config({ customError: zodLocaleError({ language: () => requestLanguage() }) });
1185
1642
  ```
1186
1643
 
1644
+ ### A calendar's weekday row is in English on a French page
1645
+
1646
+ **When:** under `<I18nProvider>` set to French, a `DateField`,
1647
+ `DateRangeField` or `Calendar` names its days in French but its weekday row
1648
+ reads "Su", "Mo"… and the week starts on Sunday.
1649
+ **Why:** before the calendar followed the provider's language, it took
1650
+ react-day-picker's default English `locale`, which shapes the weekday row
1651
+ ("cccccc"), its labels ("Monday") and the first day of the week.
1652
+ **Fix:** update: the `locale` prop, else the `I18nProvider` language, else
1653
+ English drives the whole calendar, weekday row included ("lu", "ma"…, Monday
1654
+ first, each header labelled "lundi"…). To force another language on one
1655
+ calendar, pass its date-fns locale:
1656
+
1657
+ ```tsx
1658
+ import { enUS } from 'react-day-picker/locale';
1659
+
1660
+ <DateField locale={enUS} label="Delivery date" value={date} onValueChange={setDate} />
1661
+ ```
1662
+
1663
+ react-day-picker hides the weekday row from assistive tech
1664
+ (`<thead aria-hidden>`); in a test, find a day by its full date instead
1665
+ (`getByRole('gridcell', { name: 'jeudi 1 octobre 2026' })`).
1666
+
1187
1667
  ## Runtime
1188
1668
 
1669
+ ### A test asserting the old Zod string length text fails
1670
+
1671
+ **When:** after upgrading past the release that lists this fix, a test or a
1672
+ snapshot that asserted a string length message from `zodLocaleError` fails:
1673
+ it expected `This value is too small: it must be at least 3 characters.`
1674
+ (French `Cette valeur est trop petite : …`) and gets `Must contain at least 3
1675
+ characters.` (`Doit contenir au moins 3 caractères.`).
1676
+ **Why:** a string's `min` and `max` read as a length now, through
1677
+ the keys `material.zod.too_small_string_inclusive` / `_exclusive` and
1678
+ `material.zod.too_big_string_inclusive` / `_exclusive`, which pluralize the
1679
+ count: `z.string().min(1)` says `Must contain at least 1 character.` /
1680
+ `Doit contenir au moins 1 caractère.`. A number, a list, a set or a file
1681
+ keeps the "too large / too small" wording. A French `invalid_format` noun
1682
+ opens with a capital too (`Adresse e-mail invalide.`).
1683
+ **Fix:** update the expected text, or override the new keys in your app's
1684
+ `material.zod.*` resources to keep your own wording (an override of
1685
+ `too_small_inclusive` no longer applies to strings):
1686
+
1687
+ ```json
1688
+ {
1689
+ "material": {
1690
+ "zod": {
1691
+ "too_small_string_inclusive": "At least {minimum, plural, one {# character} other {# characters}}, please."
1692
+ }
1693
+ }
1694
+ }
1695
+ ```
1696
+
1189
1697
  ### `must be used within` / `must be used inside`
1190
1698
 
1191
1699
  **When:** on render, from a sub-component or a context hook used outside the
@@ -1299,7 +1807,34 @@ try {
1299
1807
  ```
1300
1808
 
1301
1809
  Serve the app over `https` (or `localhost`) in any environment where copying
1302
- must work. `CopyButton` does not throw. It uses a different clipboard fallback.
1810
+ must work. `CopyButton` does not throw: it awaits the same write and, when the clipboard
1811
+ refuses it, shows "Copy failed" ([details](#a-test-no-longer-finds-copybutton-by-copied-or-its-name-changes)).
1812
+
1813
+ ### A test no longer finds `CopyButton` by "Copied", or its name changes
1814
+
1815
+ **When:** a test clicks a `CopyButton` and looks for a button named
1816
+ `Copied`, or reads the tooltip, or the copy "succeeds" in a page where the
1817
+ clipboard is not available.
1818
+ **Why:** the button's accessible name is now always its `label` (`Copy`). The
1819
+ outcome is a visually hidden `role="status"` (polite) region next to the
1820
+ button, so a screen reader announces it without a rename. "Copied" shows only
1821
+ after the clipboard write resolved; a rejected write (permission denied, an
1822
+ insecure origin, no Clipboard API) shows "Copy failed" (`failedLabel`,
1823
+ `material.copy.failed`).
1824
+ **Fix:** stub the clipboard and read the status next to the button. A bare
1825
+ `getByRole('status')` is ambiguous: every `CopyButton` renders one, and so do
1826
+ `Banner`, `DataTable` and the skeletons.
1827
+
1828
+ ```ts
1829
+ Object.defineProperty(navigator, 'clipboard', {
1830
+ configurable: true,
1831
+ value: { writeText: vi.fn().mockResolvedValue(undefined) }, // or mockRejectedValue(new Error('denied'))
1832
+ });
1833
+ const button = screen.getByRole('button', { name: 'Copy' });
1834
+ await user.click(button);
1835
+ // the parent holds the button and its status; wrap a CopyButton that has siblings
1836
+ expect(await within(button.parentElement!).findByRole('status')).toHaveTextContent('Copied');
1837
+ ```
1303
1838
 
1304
1839
  ### `Upload failed with status 403`
1305
1840
 
@@ -1342,6 +1877,30 @@ Apply it on the bucket, with your own origin.
1342
1877
  <FilterProvider schema={schema} presetConfig={{ enabled: true, maxPresets: 50 }}>
1343
1878
  ```
1344
1879
 
1880
+ ### `` Cannot update a component (`DataTable`) while rendering a different component ``
1881
+
1882
+ **When:** in the dev server's console, in an end-to-end run, now and then,
1883
+ mostly in Firefox under Playwright, after a `DataTable` got new `data` (a page,
1884
+ a search). The other component varies (`CheckboxProvider`, `Presence`, a
1885
+ `DataTableRow`, a column header), and a production build names them with
1886
+ minified names (a minified name stands for `DataTable`). The same run may log
1887
+ it for `RouterProvider` too.
1888
+
1889
+ **Why:** TanStack Table resets the expanded rows in a microtask after `data`
1890
+ changes. Before the fix, `DataTable` set its state for that reset even when no
1891
+ row was expanded; in a browser that runs pending microtasks while React
1892
+ renders, that update landed during another component's render and React
1893
+ warned. The `RouterProvider` one is React Router's route discovery resolving
1894
+ at the same moment: the same browser behaviour, not something the app or the
1895
+ table does.
1896
+
1897
+ **Fix:** upgrade to the `@nxgt/material` release that lists this fix in its
1898
+ changelog: `DataTable` no longer sets state when no row is expanded, so the
1899
+ reset cannot land as an update during another component's render. Nothing to
1900
+ change in the app when no row is expanded; with a row expanded, a new `data`
1901
+ still resets it, and the warning can still appear. If it still names
1902
+ `RouterProvider`, it is harmless and outside `DataTable`.
1903
+
1345
1904
  ### `Failed to execute 'exitFullscreen' on 'Document': Document not active`
1346
1905
 
1347
1906
  **When:** as an unhandled promise rejection in the console (or a failing test
@@ -1394,6 +1953,63 @@ on a desktop and on a phone alike: closing the picker with OK hands focus back
1394
1953
  to the trigger, and the next Tab or click elsewhere touches it. See the next
1395
1954
  entry.
1396
1955
 
1956
+ ### `setError(…, { shouldFocus: true })` or a failed submit does not focus the field
1957
+
1958
+ **When:** a form wired with `*FormField`s: `handleSubmit` fails validation, or
1959
+ you call `form.setError('email', { message }, { shouldFocus: true })`, and
1960
+ focus stays where it was. Before the release that lists this fix only: every
1961
+ `*FormField` (and the config-driven `FormField`), the plain text inputs
1962
+ included, ignored `shouldFocusError`, `setError(…, { shouldFocus: true })` and
1963
+ `setFocus(name)`.
1964
+ **Why:** React Hook Form focuses a field by calling `focus()` on the `ref`
1965
+ that `Controller` gives to `field.ref`. The wrappers did not hand it to any
1966
+ element, so React Hook Form had nothing to focus (and `setFocus('email')` did
1967
+ nothing either).
1968
+ **Fix:** upgrade to the release that lists this fix in its changelog: every `*FormField` (and `FormField`) forwards
1969
+ `field.ref`, to its input, or, for a field made of several controls, to a
1970
+ handle that focuses the first one a user can reach (the select's trigger, the
1971
+ first chip, the file picker's button, the first of the two date inputs). For
1972
+ a field of your own, give `field.ref` to the control:
1973
+
1974
+ ```tsx
1975
+ <Controller
1976
+ control={control}
1977
+ name="nickname"
1978
+ render={({ field }) => <input {...field} />}
1979
+ />
1980
+ ```
1981
+
1982
+ A form whose error is shown by a field that is not mounted (a closed tab, a
1983
+ collapsed section) cannot take focus: React Hook Form skips it silently, so
1984
+ open the section before calling `setError`.
1985
+
1986
+ `setFocus(name)` uses the same `ref`, so a sign-in step that calls
1987
+ `setFocus('password')` or `setFocus('code')` on mount puts focus in its
1988
+ `PasswordFormField` or `OtpFormField` (Storybook: `Guides/Forms/Focus on
1989
+ error`, `Set Focus Password` and `Set Focus Otp`).
1990
+
1991
+ ### A test that finds a chip by `aria-selected` or `getByRole('option')` no longer finds it
1992
+
1993
+ **When:** after upgrading past the release that lists this fix, a test (or
1994
+ an end-to-end locator) that picked a `SelectChipField` chip with
1995
+ `[aria-selected="true"]`, `getByRole('option', { name })` or
1996
+ `toHaveAttribute('aria-selected', 'true')` finds nothing, and the same for
1997
+ `SelectChipFormField` and the config-driven `FormField` (`chips-single`, `chips-multiple`).
1998
+ **Why:** the chips are toggle buttons: `aria-selected` is not allowed on a
1999
+ button, so a chosen chip is now `aria-pressed="true"`, in `single` mode too
2000
+ (pressing the chosen chip clears it). The chips sit in a `role="group"`
2001
+ named by the label and described by the help text. `SelectCardField` is
2002
+ unchanged.
2003
+ **Fix:** query the group, then the chip as a button:
2004
+
2005
+ ```ts
2006
+ import { screen, within } from '@testing-library/react';
2007
+
2008
+ const topics = screen.getByRole('group', { name: 'Topics' });
2009
+ const design = within(topics).getByRole('button', { name: 'Design' });
2010
+ expect(design).toHaveAttribute('aria-pressed', 'true');
2011
+ ```
2012
+
1397
2013
  ### `touchedFields` stays empty after leaving a field, or `mode: 'onBlur'` never validates
1398
2014
 
1399
2015
  **When:** a `*FormField` keeps `formState.touchedFields` (and
@@ -1543,16 +2159,22 @@ await page.getByRole('slider', { name: 'Price Minimum' }).press('ArrowRight');
1543
2159
 
1544
2160
  **When:** after upgrading to 2.2.0, `getByRole('textbox', { name: 'Email*' })`
1545
2161
  (or any role query whose name ends in `*`) finds nothing for a field whose
1546
- `Label` has `withAsterisk` (a `required` field). Queries on the label's text,
1547
- `getByLabelText` and Playwright's `getByLabel`, still see the `*`: match it
1548
- loosely in `getByLabelText` (`getByLabelText(/^Email/)`); Playwright's
1549
- `getByLabel('Email')` already matches a substring.
1550
- **Why:** screen readers used to read the asterisk ("Email star" or "Email*"),
1551
- so it is now `aria-hidden`. The field's own `required` / `aria-required` says
1552
- it is required, and every field of this library now sets it on the element a
1553
- screen reader lands on (the input, the picker's combobox, the slider thumbs, the rating's
2162
+ `Label` has `withAsterisk` (a `required` field). Since the release that drew the
2163
+ asterisk with CSS, `getByLabelText('Email*')` and Playwright's
2164
+ `getByLabel('Email *')` find nothing either; and the other way round,
2165
+ `getByLabel('Email', { exact: true })` and `getByLabelText('Email')` now match
2166
+ a required field, which a `*` in the label's text used to prevent.
2167
+ **Why:** screen readers read the asterisk ("Email star" or "Email*"), and
2168
+ Testing Library and Playwright compare the label's text, `aria-hidden` or not.
2169
+ The `*` is now drawn by a CSS pseudo-element with an empty `content`
2170
+ (`label.css`, imported by the stylesheet), so it is in no text and no name. The
2171
+ field's own `required` / `aria-required` says it is required, and every field
2172
+ of this library sets it on the element a screen reader lands on (the input, the
2173
+ picker's combobox, the select's trigger, the slider thumbs, the rating's
1554
2174
  radiogroup, the editor's textbox, the list's listbox, or, for a file picker,
1555
- a group described as "This field is required.").
2175
+ a group described as "This field is required."). A `SelectField`'s trigger is
2176
+ now named by its label (`aria-labelledby`), so `getByRole('combobox', { name:
2177
+ 'Framework' })` finds it.
1556
2178
  **Fix:** find the field by its label alone, and check that it is required
1557
2179
  separately:
1558
2180
 
@@ -1568,6 +2190,12 @@ await expect(page.getByRole('textbox', { name: 'Email' })).toHaveAttribute('requ
1568
2190
  await expect(page.getByRole('combobox', { name: 'Delivery date' })).toHaveAttribute('aria-required', 'true');
1569
2191
  ```
1570
2192
 
2193
+ Custom CSS that targeted the old `<span class="text-error">` should target
2194
+ `[data-slot="label-text"][data-required]::after` instead; the recipe is in
2195
+ [Label](components/primitives.md#label). The mark is drawn by the package stylesheet, so a
2196
+ snapshot of the DOM no longer contains it, and it stays visible in
2197
+ forced-colors mode.
2198
+
1571
2199
  ### An end-to-end test no longer finds `aria-expanded` on a `ResponsiveButton`
1572
2200
 
1573
2201
  **When:** after upgrading to 2.2.0, a spec that selects
@@ -1888,6 +2516,23 @@ await expect(page.getByRole('switch', { name: 'Notifications' })).toHaveAttribut
1888
2516
  await expect(page.getByRole('combobox', { name: 'Country' })).toHaveAttribute('aria-invalid', 'true');
1889
2517
  ```
1890
2518
 
2519
+ ### A test no longer finds `aria-invalid` on an `OtpField` slot
2520
+
2521
+ **When:** `[data-slot="input-otp-slot"][aria-invalid]` (or
2522
+ `getAllByRole(…)` filtered on `aria-invalid`) finds nothing although the
2523
+ `OtpField` has `error`.
2524
+ **Why:** ARIA does not allow `aria-invalid` on a plain `div`, and a slot is
2525
+ a presentational box: the one element a screen reader reads is the hidden
2526
+ input, which keeps `aria-invalid="true"`. A slot shows the error with a red
2527
+ border from `data-invalid`.
2528
+ **Fix:** assert it on the input, found by the field's label:
2529
+
2530
+ ```ts
2531
+ await expect(screen.getByLabelText('Enter OTP')).toHaveAttribute('aria-invalid', 'true');
2532
+ // A slot's red border, if a test must see it:
2533
+ await expect(container.querySelector('[data-slot="input-otp-slot"]')).toHaveAttribute('data-invalid');
2534
+ ```
2535
+
1891
2536
  ### A test no longer finds the breadcrumb's home button
1892
2537
 
1893
2538
  **When:** after upgrading, `getByRole('button', { name: 'Home' })` finds
@@ -1906,6 +2551,137 @@ await page.getByRole('navigation', { name: 'breadcrumb' })
1906
2551
  .click();
1907
2552
  ```
1908
2553
 
2554
+ ### A test no longer finds a sidebar entry by the `button` role
2555
+
2556
+ **When:** after upgrading, `getByRole('button', { name: 'Invoices' })` finds
2557
+ nothing among the entries of an `ActivityLayout`, `AppSidebar` or `AppHeader`'s
2558
+ menu sheet.
2559
+ **Why:** each entry used to be a `<button>` that navigated on click. It is now
2560
+ a real `<a href>`, so its role is `link`, and the current page's entry carries
2561
+ `aria-current="page"`. The icon inside is decorative (`aria-hidden`), so the
2562
+ link's name is its label alone, "Invoices", and an exact name matches.
2563
+ **Fix:** find it as a link:
2564
+
2565
+ ```ts
2566
+ const invoices = page.getByRole('link', { name: 'Invoices' });
2567
+ await invoices.click();
2568
+ await expect(invoices).toHaveAttribute('aria-current', 'page');
2569
+ ```
2570
+
2571
+ ### The app shell's page scrolls instead of its content, or a full-height screen collapses
2572
+
2573
+ **When:** after upgrading, the whole page scrolls under an `ActivityLayout`
2574
+ rather than the box inside it; a test or a script that scrolled the shell's
2575
+ inner box (`[data-slot=activity-layout] .overflow-auto`) finds nothing to
2576
+ scroll; or a screen sized `h-full` to fill the shell (a map, a board) is only
2577
+ as high as its content.
2578
+ **Why:** the shell was `100lvw × 100lvh` and scrolled its content inside
2579
+ itself: the page was a scrollbar's width wider than the window, and on a phone
2580
+ the bottom of the content sat under the browser's toolbar. It is now `100%`
2581
+ wide and at least `100dvh` high, and the document scrolls; the header and the
2582
+ desktop sidebar are sticky.
2583
+ **Fix:** scroll the window in a test (`window.scrollTo`, or let the browser
2584
+ scroll an element into view). For a screen that manages its own height, opt
2585
+ back into the in-shell scrolling with `scroll="container"`:
2586
+
2587
+ ```tsx
2588
+ <ActivityLayout items={items} scroll="container">
2589
+ ```
2590
+
2591
+ ### A `className` on `ActivityLayout` now styles the whole shell
2592
+
2593
+ **When:** after upgrading, the classes passed to `<ActivityLayout className="…">`
2594
+ (a padding, a background, a gap) land around the sidebar and the header too,
2595
+ not only around the content, or a selector on the inner box finds them
2596
+ missing.
2597
+ **Why:** `className` used to land on the inner layout box; it now reaches the
2598
+ outer element, `[data-slot=activity-layout]`, with the other props.
2599
+ **Fix:** keep on `ActivityLayout` only the classes meant for the whole shell,
2600
+ and move the rest to the screen: `ActivityContent`'s `className`, or an
2601
+ element of your own inside it:
2602
+
2603
+ ```tsx
2604
+ <ActivityLayout items={items} className="bg-muted">
2605
+ <ActivityContent title="Invoices" className="gap-6">…</ActivityContent>
2606
+ </ActivityLayout>
2607
+ ```
2608
+
2609
+ ### A test finds a new heading or a second `main`, or no longer finds a title in a `div`
2610
+
2611
+ **When:** after upgrading, `getAllByRole('heading')` returns more than it did,
2612
+ a selector such as `div[data-slot=card-title]` finds nothing, or an
2613
+ accessibility check reports more than one `main` landmark in an app that
2614
+ renders its own `<main>` around `ActivityLayout`.
2615
+ **Why:** `ActivityContent`'s title is now a heading, `h1` by default; a
2616
+ `CardTitle` is an `h3` (it was a `div`); and the shell's content region is the
2617
+ page's `<main>`. The phone menu sheet had no name; it is now named
2618
+ "Navigation" (`material.app-shell.navigation`):
2619
+ `getByRole('dialog', { name: 'Navigation' })`.
2620
+ **Fix:** find titles by role and level —
2621
+ `getByRole('heading', { level: 1, name: 'Invoices' })` — and pick another
2622
+ level with `titleAs` or `CardTitle`'s `as` (`'div'` for no heading). An app
2623
+ with its own `<main>` passes `contentAs="div"`.
2624
+
2625
+ ### `[data-test="typogaphy"]` finds nothing
2626
+
2627
+ **When:** after upgrading, a selector on `data-test="typogaphy"` finds no
2628
+ `Typography`.
2629
+ **Why:** the attribute was misspelled; it now reads `data-test="typography"`.
2630
+ **Fix:** select `[data-test="typography"]` — or better, the text or the role
2631
+ the user sees.
2632
+
2633
+ ### A test no longer finds a tooltip on an entry of an expanded sidebar
2634
+
2635
+ **When:** after upgrading, a test hovers or focuses a `SidebarMenuItem` of an
2636
+ expanded `Sidebar` (or an entry of the phone menu sheet) and waits for
2637
+ `getByRole('tooltip', { name: 'Inbox' })`, which never appears.
2638
+ **Why:** an entry's label is its tooltip only while the sidebar is collapsed;
2639
+ expanded, the label is already on screen, and a tooltip repeating it opened
2640
+ over the first entry as focus moved into the menu sheet. A `tooltip` you pass
2641
+ is still shown, collapsed or expanded.
2642
+ **Fix:** find the entry by the label it shows —
2643
+ `getByRole('link', { name: 'Inbox' })` — or collapse the sidebar first when
2644
+ the test is about the tooltip. Pass `tooltip` when an expanded entry must
2645
+ have one:
2646
+
2647
+ ```tsx
2648
+ <SidebarMenuItem icon={<Inbox />} tooltip="3 unread">Inbox</SidebarMenuItem>
2649
+ ```
2650
+
2651
+ ### A test no longer finds a calendar day by its number, or by "Today" or "selected"
2652
+
2653
+ **When:** after upgrading, `getByRole('gridcell', { name: '1' })` or
2654
+ `getByRole('button', { name: 'Thursday, October 1st, 2026' })` finds nothing in
2655
+ a `Calendar`, `DateField`, `DateRangeField` or `EventCalendar`'s date picker; so
2656
+ do names with "Today, " or ", selected" in them; or
2657
+ `getByRole('gridcell', { name: '1', exact: true })` used to match both the 1st
2658
+ and the next month's 1st, shown as an outside day.
2659
+ **Why:** a day is now named by its full localized date and nothing else:
2660
+ "Thursday, October 1, 2026" (in French "jeudi 1 octobre 2026"), on the button
2661
+ and on the cell (`gridcell`) around it. "Today" and "selected" are no longer in
2662
+ the name: the chosen day carries `aria-selected="true"` on its cell and a
2663
+ "Selected" description on its button, today `aria-current="date"` on both. The
2664
+ days of the neighbouring months are named by their own dates ("Wednesday,
2665
+ September 30, 2026"), so a bare number matches no day at all. The language is
2666
+ the calendar's `locale`, else the active `I18nProvider` language, else English:
2667
+ under a French provider the names, the heading and the weekday row are now
2668
+ French, so pass `locale={enUS}` (from `react-day-picker/locale`) to keep English.
2669
+ **Fix:** pick the day by its date, exactly, and assert the state; compute
2670
+ today's name instead of writing it:
2671
+
2672
+ ```ts
2673
+ const name = (date: Date) =>
2674
+ new Intl.DateTimeFormat('en', { dateStyle: 'full' }).format(date);
2675
+
2676
+ const day = page.getByRole('gridcell', { name: 'Thursday, October 1, 2026', exact: true });
2677
+ await day.getByRole('button').click();
2678
+ await expect(day).toHaveAttribute('aria-selected', 'true');
2679
+ await expect(page.getByRole('button', { name: 'Thursday, October 1, 2026' })).toHaveAccessibleDescription('Selected');
2680
+ await expect(page.getByRole('gridcell', { name: name(new Date()) })).toHaveAttribute('aria-current', 'date');
2681
+ ```
2682
+
2683
+ `getByRole('button', { name: 'Thursday, October 1, 2026', exact: true })` finds the same day.
2684
+
1909
2685
  ### `#<name>` no longer finds a `SwitchFormField` or `CheckboxFormField`
1910
2686
 
1911
2687
  **When:** after upgrading, a selector such as `#notifications`, or a
@@ -1950,6 +2726,68 @@ await page.locator('#married').getByRole('radio', { name: 'Yes' }).check();
1950
2726
  await page.locator('#married-0').check(); // the first option
1951
2727
  ```
1952
2728
 
2729
+ ### `#<name>` no longer finds a text, OTP, number, date or picker form field, or a second form's field has no name
2730
+
2731
+ **When:** two forms on one page have a field with the same `name` (a sign-in
2732
+ and a sign-up form both with `email`, two steps of a code check both with
2733
+ `code`), and the second field has no accessible name: its label points at the
2734
+ first form's input. Or, after upgrading, a selector such as `#email`, or a
2735
+ `<label htmlFor="email">` of your own, no longer reaches the input of
2736
+ `<TextFormField name="email">`.
2737
+ **Why:** the text-like wrappers (`TextFormField`, `TextareaFormField`,
2738
+ `EmailFormField`, `PasswordFormField`, `UrlFormField`, `NumberFormField`,
2739
+ `CurrencyFormField`, `PercentFormField`, `InputGroupFormField`,
2740
+ `PhoneFormField`, `OtpFormField`, `ColorFormField`, `TimezoneFormField`,
2741
+ `RatingFormField`, the date and time wrappers, `ComboboxFormField`,
2742
+ `AutocompleteFormField`, `TreeSelectFormField`, `FilesFormField`,
2743
+ `LocationFormField`) gave the control the field's `name` as its `id`, so two
2744
+ forms sharing a name had two elements with one `id`. Without an `id` of yours,
2745
+ the control now gets a generated one (`useId`), as the bare fields always did
2746
+ and as `@nxgt/material-vue` does.
2747
+ **Fix:** select by role and name, by `name`, or pass the `id` you rely on:
2748
+
2749
+ ```tsx
2750
+ await page.getByRole('textbox', { name: 'Email' }).fill('ada@example.com');
2751
+ await page.locator('[name="email"]').fill('ada@example.com');
2752
+ // or keep a fixed id
2753
+ <TextFormField control={control} name="email" id="sign-in-email" label="Email" />
2754
+ ```
2755
+
2756
+ ### A double click or Enter submits a form twice, or a test finds `aria-busy` in an idle `Button`
2757
+
2758
+ **When:** a sign-in or save form is submitted twice by a quick double click,
2759
+ or by Enter pressed twice in a field, although its submit button has
2760
+ `loading`; or `<Button asChild loading><a onClick={save}>` still calls
2761
+ `save`. Or, after upgrading, a markup assertion such as
2762
+ `expect(html).not.toContain('aria-busy')` fails on a `Button` that is not
2763
+ loading.
2764
+ **Why:** `loading` blocked clicks only once the busy state had rendered, a
2765
+ `loading` that turns true after an `await` (a mutation's `isPending` behind an
2766
+ async resolver) left a window for the second click, and an `asChild` child's
2767
+ own `onClick` ran before the guard. A loading `Button` now sets
2768
+ `aria-busy="true"` and `aria-disabled="true"` (it set neither before) and
2769
+ cancels every click in the capture phase — the click Enter in a field sends to
2770
+ the form's submit button included — so neither your handler, the child's nor
2771
+ an ancestor's runs. It still sets no native `disabled` and stays focusable.
2772
+ Busy buttons show a progress cursor: the class string of every `Button` now
2773
+ holds `aria-busy:cursor-progress`, which a plain `toContain('aria-busy')`
2774
+ matches.
2775
+ **Fix:** pass React Hook Form's `formState.isSubmitting`, which is true as soon
2776
+ as `handleSubmit` starts; for a `loading` that turns true later, add
2777
+ `lockOnClick`. In a test, check the attribute, not the string:
2778
+
2779
+ ```tsx
2780
+ <Button type="submit" loading={formState.isSubmitting}>Sign in</Button>
2781
+ <Button type="submit" loading={mutation.isPending} lockOnClick>Sign in</Button>
2782
+
2783
+ expect(screen.getByRole('button', { name: 'Sign in' })).toHaveAttribute('aria-busy', 'true');
2784
+ expect(renderToStaticMarkup(<Button>Save</Button>)).not.toMatch(/\saria-busy=/);
2785
+ ```
2786
+
2787
+ A `form.requestSubmit()` of your own fires no click, so the guard does not
2788
+ see it: check `formState.isSubmitting` before calling it (see
2789
+ [Forms](./guides/forms.md)).
2790
+
1953
2791
  ### An empty `UploadField` or `TimeField` is no longer tinted
1954
2792
 
1955
2793
  **When:** after upgrading, an `UploadField` showing its `placeholder`, or a
@@ -2023,6 +2861,21 @@ render `data-variant="error"`, and both use the `error` colour.
2023
2861
  [data-slot='menubar-item'][data-variant='error'] { font-weight: 600; }
2024
2862
  ```
2025
2863
 
2864
+ ### A menu no longer blocks the page, or a `ResponsiveButton` has no `useIsMobile` behaviour
2865
+
2866
+ **When:** after upgrading, clicking outside an open `DropdownMenu` (or
2867
+ `LanguageSwitcher`) reaches the element under the pointer and the page can
2868
+ still scroll; or `ResponsiveButton` shows its label in a test whose
2869
+ `matchMedia` says "mobile".
2870
+ **Why:** `DropdownMenu` is non-modal by default now: Radix's modal menu marked
2871
+ the rest of the page `aria-hidden` while its buttons and links stayed
2872
+ focusable (axe `aria-hidden-focus`). `ResponsiveButton` and
2873
+ `ResponsiveLinkButton` collapse with a CSS `max-md` media query instead of a
2874
+ JS effect, so the label is in the DOM at every width and jsdom (which applies
2875
+ no CSS) always shows it.
2876
+ **Fix:** pass `modal` to `DropdownMenu` to restore the old behaviour; force a
2877
+ collapsed button in a test with `expanded={false}`.
2878
+
2026
2879
  ### A `Stepper` step selected by `#step-N` stops matching
2027
2880
 
2028
2881
  **When:** after upgrading to 2.5.0, a test or a stylesheet that targets a
@@ -2075,7 +2928,9 @@ await dialog.getByRole('button', { name: 'Save' }).click();
2075
2928
 
2076
2929
  To put focus on something else when an overlay opens, prevent the default in
2077
2930
  `onOpenAutoFocus` and focus it yourself; to keep focus where it was (a
2078
- non-modal panel the user keeps typing behind), only prevent the default:
2931
+ non-modal panel the user keeps typing behind), only prevent the default — on
2932
+ a modal one focus then goes to its content, as the page behind is inert
2933
+ ([details](#focus-moves-into-a-dialog-although-onopenautofocus-prevented-it)):
2079
2934
 
2080
2935
  ```tsx
2081
2936
  const saveButtonRef = useRef<HTMLButtonElement>(null);
@@ -2094,6 +2949,34 @@ const saveButtonRef = useRef<HTMLButtonElement>(null);
2094
2949
  A `Drawer` also takes Vaul's `autoFocus={false}`, which does the same as
2095
2950
  preventing the default.
2096
2951
 
2952
+ ### Focus moves into a dialog although `onOpenAutoFocus` prevented it
2953
+
2954
+ **When:** a modal `Dialog`, `Sheet`, `Drawer` or `AlertDialog` whose
2955
+ `onOpenAutoFocus` calls `event.preventDefault()` and focuses nothing: focus
2956
+ lands on the layer's content, not on the element that had it before it
2957
+ opened.
2958
+ **Why:** the page behind a modal layer is `inert`, and an inert element
2959
+ cannot keep focus: the browser would drop it on `<body>`, where a keyboard
2960
+ or screen-reader user is nowhere. The layer takes it instead (its content
2961
+ has `tabindex="-1"`); closing still hands focus back to the element that
2962
+ opened it.
2963
+ **Fix:** focus what you want inside the layer in the handler; to keep
2964
+ focus on the page behind, make the layer non-modal (`modal={false}` on
2965
+ `Dialog`, `Sheet` or `Drawer`):
2966
+
2967
+ ```tsx
2968
+ const nameRef = useRef<HTMLInputElement>(null);
2969
+
2970
+ <DialogContent
2971
+ onOpenAutoFocus={(event) => {
2972
+ event.preventDefault();
2973
+ nameRef.current?.focus();
2974
+ }}
2975
+ >
2976
+ <Input ref={nameRef} aria-label="Name" />
2977
+ </DialogContent>
2978
+ ```
2979
+
2097
2980
  ### A `TimePicker` test expects AM / PM to add 12 hours, or a click on the pressed half to switch to PM
2098
2981
 
2099
2982
  **When:** after upgrading to 2.6.0, a test (or code of yours) around a
@@ -2199,6 +3082,49 @@ Anything else that must not make the row follow its link on a click (a custom wi
2199
3082
  />
2200
3083
  ```
2201
3084
 
3085
+ ### A `DataTable` no longer scrolls inside a 400px box
3086
+
3087
+ **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.
3088
+ **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.
3089
+ **Fix:** pass `maxHeight` (a number of pixels, or any CSS length) where the table should scroll inside a box:
3090
+
3091
+ ```tsx
3092
+ <DataTable data={rows} columns={columns} maxHeight={400} />
3093
+ ```
3094
+
3095
+ `maxHeight` 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.
3096
+
3097
+ ### A `DataTable` shows its header row on a phone
3098
+
3099
+ **When:** after upgrading, a `DataTable` under 768px wide shows its header row, which it used to hide.
3100
+ **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.
3101
+ **Fix:** nothing, usually. To hide it again, pass `showHeader` — `false` everywhere, or your own breakpoint:
3102
+
3103
+ ```tsx
3104
+ <DataTable data={rows} columns={columns} showHeader={false} />
3105
+ <DataTable data={rows} columns={columns} showHeader={!isPhone} />
3106
+ ```
3107
+
3108
+ ### A test's `getByRole('region')` finds more than one region
3109
+
3110
+ **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.
3111
+ **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.
3112
+ **Fix:** query your region by name, and give the table a name of its own where a page holds several:
3113
+
3114
+ ```tsx
3115
+ <DataTable data={rows} columns={columns} scrollLabel="Invoices" />
3116
+ ```
3117
+
3118
+ ```ts
3119
+ page.getByRole('region', { name: 'Invoices' });
3120
+ ```
3121
+
3122
+ ### A `DataTable` toolbar is gone
3123
+
3124
+ **When:** a `DataTable` is given `actions` that render nothing (`() => null`, `false`), and a test looked for the toolbar's `fieldset` (`[data-slot="header"]`).
3125
+ **Why:** the toolbar is rendered only when it has content (yours, or `showViewOptions`): an empty `<fieldset>` is a nameless group to a screen reader.
3126
+ **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.
3127
+
2202
3128
  ---
2203
3129
 
2204
3130
  ---