@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
@@ -399,7 +399,7 @@ const user = { name: 'Ada Lovelace', phone: '' };
399
399
 
400
400
  `label` and `value` (both `string`) are its only props; the default slot is
401
401
  rendered under the value. A falsy `value` renders nothing, so optional fields
402
- need no `v-if`.
402
+ need no `v-if`. The label is a small muted caption above the value (not a title). `Description` takes one cell of the grid it is in; pass `class="col-start-full"` for a full-width row.
403
403
 
404
404
  **Storybook:** `Data display/Description` · **React:** `Description`
405
405
 
@@ -462,12 +462,23 @@ import { Icon, ListTile } from '@nxgt/material-vue/components';
462
462
  | `title` | `string` | — | first line |
463
463
  | `subtitle` | `string` | — | second, muted line |
464
464
  | `href` | `string` | — | renders an `<a>`; without it the row is a `<button>` |
465
+ | `linkComponent` / `link-component` | `Component` | plain `<a>` | the link element, as in the app shell: a component that takes `href` and lets the other attributes fall through |
465
466
  | `selected` | `boolean` | `false` | highlighted row (`aria-current="page"` on a link) |
466
467
  | `size` | `'sm' \| 'md'` | `'md'` | `sm` is the dense navigation row |
467
468
  | `disabled` | `boolean` | — | disables the link or button |
468
469
  | `@click` | `(event: MouseEvent) => void` | — | fired by both shapes |
469
470
  | `#leading`, `#trailing` | slots | — | icons, avatars, actions |
470
471
 
472
+ With a router, pass `link-component` so a click navigates on the client instead of reloading the page (`RouterLink` takes `to`, so wrap it in a component that takes `href`; `AppLink` below is the one defined in [A `ListTile` link reloads the whole page](../troubleshooting.md#a-listtile-link-reloads-the-whole-page)):
473
+
474
+ ```vue
475
+ <ListTile title="Notifications" href="/settings/notifications" :link-component="AppLink" />
476
+ ```
477
+
478
+ A disabled tile never renders the link component: it is a plain `<a>` without `href`, with `aria-disabled="true"`.
479
+
480
+ The root carries `data-slot="item-detail"` (a tile that is a button too).
481
+
471
482
  **Storybook:** `Data display/ListTile` · **React:** `ListTile`
472
483
 
473
484
  ---
@@ -620,7 +631,7 @@ function openEvent() {
620
631
 
621
632
  ![CodeBlock](../previews/data-display-codeblock.webp)
622
633
 
623
- Highlighted code with line numbers and copy, a Monaco diff, a collapsible JSON tree, and tabbed samples (`Tabs variant="line"`).
634
+ Highlighted code with line numbers and copy, a Monaco diff, a collapsible JSON tree, and tabbed samples (`Tabs`, underlined).
624
635
 
625
636
  <img src="../previews/data-display-codegroup.webp" height="96" alt="CodeGroup">
626
637
  <img src="../previews/data-display-jsonviewer.webp" height="96" alt="JsonViewer">
@@ -23,7 +23,7 @@ each other, so either can be used alone.
23
23
 
24
24
  ![DataTable](../previews/data-table-filter-lists-datatable.webp)
25
25
 
26
- TanStack Table v9 with sorting, filtering, selection, expandable rows, pagination or load-more, column visibility, skeleton rows while loading and your own empty state — or one server page at a time, with controlled sorting and selection, cursor Previous / Next links and row links.
26
+ TanStack Table v9 with sorting, filtering, selection, expandable rows, pagination or load-more, column visibility, skeleton rows while loading and your own empty state — or one server page at a time, with controlled sorting and selection, cursor Previous / Next links and row links. On a phone it keeps its header row and scrolls sideways in a keyboard-reachable region; the page does not.
27
27
 
28
28
  ```vue
29
29
  <script setup lang="ts">
@@ -79,7 +79,9 @@ table enables, for typing your own column defs.
79
79
  | `#actions="{ table }"` / `actions` | slot / `(table) => VNode` | — | toolbar content with access to the table (selected rows…) |
80
80
  | `onLoadMore` / `isLoadingMore` | `() => void` / `boolean` | — | adds a "load more" button under the rows |
81
81
  | `options` | `{ showPage?: boolean; showSelected?: boolean }` | both shown | parts of the footer |
82
- | `showHeader` | `boolean` | hidden on mobile | header row |
82
+ | `showHeader` | `boolean` | shown | header row (column names, sorting, "Select all") at every width; `false` hides it |
83
+ | `maxHeight` | `number \| string` | none | caps the table's height (`400`, `'60vh'`); the rows scroll inside it |
84
+ | `scrollLabel` | `string` | "Scrollable table" | the name of the scroll region the table becomes while it overflows |
83
85
  | `loading` | `boolean` | `false` | skeleton rows, or the current rows dimmed; `aria-busy`; controls disabled |
84
86
  | `loadingRows` | `number` | the page size | how many skeleton rows |
85
87
  | `#empty="{ filtered }"` | slot | icon and "No data available" | content of the empty row, spanning every visible column |
@@ -142,6 +144,72 @@ default, or the `#empty` slot, told whether rows exist but none matches the
142
144
  filters (`filtered`); content that renders nothing gets the default.
143
145
  `DataTableEmptyState` types the slot props.
144
146
 
147
+ ### Phones and narrow screens
148
+
149
+ ```vue
150
+ <template>
151
+ <!-- The rows scroll inside 60% of the viewport; the page scrolls otherwise. -->
152
+ <DataTable
153
+ :data="invoices"
154
+ :columns="columns"
155
+ max-height="60vh"
156
+ scroll-label="Invoices"
157
+ />
158
+ </template>
159
+ ```
160
+
161
+ The table is as tall as its rows, and the page scrolls: there is no height
162
+ cap unless you pass `max-height`, so a phone never gets a scroll inside the
163
+ page's. The header row shows at every width (`:show-header="false"` hides
164
+ it). With `max-height`, the rows scroll under the header row, which stays in
165
+ view on an opaque background.
166
+
167
+ When the columns are wider than the table, the table scrolls sideways in
168
+ one box (`[data-slot="table-wrapper"]`), and the page does not. While it
169
+ overflows, that box is a `region` named "Scrollable table"
170
+ (`material.table.labels.scroll-region`, or `scroll-label`) with
171
+ `tabindex="0"`, so a keyboard user can reach it and scroll it with the arrow
172
+ keys, and an edge fades on each side where columns are hidden
173
+ (`data-scroll-start` / `data-scroll-end` on its parent,
174
+ `[data-slot="table-scroll"]`). It is not a region while it fits, nor on the
175
+ server; once focused, it stays a tab stop until focus leaves, even if it
176
+ stops overflowing. The empty state is centred in the part of the table you
177
+ can see (the box's measured width, `--table-visible-width`), not across its
178
+ whole scroll width. The table needs no width from its parent: inside a
179
+ content-sized one (`w-fit`, `inline-flex`, an `auto` grid track, a popover)
180
+ it is as wide as its columns.
181
+
182
+ The `select` column's checkboxes keep their 16px box, with a 24px target
183
+ around it (WCAG 2.5.8). The toolbar renders only when it has content:
184
+ the `actions` prop or the `#actions` slot that render nothing (`null`,
185
+ `false`, whitespace, a `v-if` left out) leave no empty `<fieldset>` behind.
186
+
187
+ Selection actions do not move the rows. With `actions` or the `#actions`
188
+ slot and the `select` column (`predefinedColumns().select`, id `select`),
189
+ the toolbar's height is reserved — an empty, hidden placeholder
190
+ (`[data-slot="header-placeholder"]`) while they render nothing, the same
191
+ minimum height once they do — so the "Delete selected" that appears on the
192
+ first selection does not push the tapped row down. The reserved height is
193
+ one row of default-size buttons (`h-9`); taller or wrapping actions still
194
+ grow the toolbar.
195
+
196
+ ```vue
197
+ <template>
198
+ <DataTable :data="people" :columns="[select, ...columns]">
199
+ <template #actions="{ table }">
200
+ <Button
201
+ v-if="table.getSelectedRowModel().rows.length > 0"
202
+ @click="handleDelete(table.getSelectedRowModel().rows)()"
203
+ >
204
+ Delete selected
205
+ </Button>
206
+ </template>
207
+ </DataTable>
208
+ </template>
209
+ ```
210
+
211
+ Rows stay rows on a phone; a card layout is not built in.
212
+
145
213
  ### Server-side data
146
214
 
147
215
  When the server filters, sorts and pages, hand the table **one page** and
@@ -263,7 +331,16 @@ async function archive() {
263
331
  index of the page's first row) make the range: "41–60 of 133"
264
332
  (`material.table.pagination.range`), "41–60" without a total, "133 rows"
265
333
  without an offset. Cursor paging knows the offset from the trail's length
266
- times the page size; leave it out if pages can be short.
334
+ times the page size; leave it out if pages can be short. The range is a
335
+ polite live region (`role="status"`, `aria-atomic="true"`, named "Rows
336
+ shown" by `material.table.pagination.range-label`) that is in the DOM from
337
+ the first render whenever `pagination` is given, so a screen reader
338
+ announces "41–60 of 133" when the page changes. With neither `offset` nor
339
+ `totalCount` the region is empty, and is kept in the DOM so a later change
340
+ is still announced. The table already has a visually hidden status for
341
+ "Loading data…" (and the page may have a toast region), so a test finds the
342
+ range with `getByRole('status', { name: 'Rows shown' })`, not a bare
343
+ `getByRole('status')`.
267
344
  - **Sorting** is `v-model:sorting`: the headers show it, and
268
345
  `update:sorting` hands you the new `SortingState` to put in the URL.
269
346
  Filters work the same way: render them in `#actions` and keep their values
@@ -288,7 +365,10 @@ async function archive() {
288
365
  navigation (opening a sheet). Alone, it makes the row focusable and Enter
289
366
  calls it; with `getRowHref`, the link stays the keyboard target, and
290
367
  calling `event.preventDefault()` in it stops a mouse click from following
291
- the link. A focusable row has no role of its own: for anything a screen
368
+ the link. The row itself is never a second control: with `getRowHref` it
369
+ has no role, no `tabindex` and no `aria-label`, and the one link is named
370
+ by the cell's text, so `getByRole('link', { name: 'Ada', exact: true })`
371
+ is unique. A focusable row has no role of its own: for anything a screen
292
372
  reader user must reach, prefer `getRowHref` or a button in a cell.
293
373
  - **Your own footer.** The `#footer="{ table }"` slot or the `footer` prop
294
374
  (`(table) => VNode`) replaces the footer — `DataTableServerFooter` is the
@@ -297,7 +377,7 @@ async function archive() {
297
377
  page is fetched, and the current rows stay in place, dimmed.
298
378
 
299
379
  The server footer's strings are `material.table.pagination.*`
300
- (`previous`, `next`, `label`, `range`, `range-open`, `total`, `selected`),
380
+ (`previous`, `next`, `label`, `range`, `range-open`, `range-label`, `total`, `selected`),
301
381
  in English and French.
302
382
 
303
383
  For the TanStack API itself (column helpers, features, row models), see
@@ -58,9 +58,33 @@ under `I18nProvider`), and so are `DateRangeField`'s; an `aria-label` on
58
58
  | `layout` | `'month-and-year' \| 'month-only' \| 'year-only'` | — | month / year selects in the heading instead of plain text |
59
59
  | `year-range` | `DateValue[]` | 100 years back, 10 ahead | the years in the year select |
60
60
  | `default-placeholder` | `DateValue` | today | the month shown first |
61
- | `locale` | `string` | `'en'` | month and weekday names |
61
+ | `locale` | `string` | `I18nProvider` language, else `'en'` | month and weekday names, and the language of the day names |
62
62
 
63
63
  **Emits:** `update:modelValue` with `DateValue | DateValue[] | undefined`.
64
+
65
+ **Day names.** Every day is named by its full date in the calendar's language
66
+ (the `locale` prop, else the active `I18nProvider` language, else English):
67
+ "Thursday, October 1, 2026", "jeudi 1 octobre 2026". The name is on the button
68
+ and, for Testing Library, on the `gridcell` around it. The chosen day has
69
+ `aria-selected="true"` on its cell and its button is described by "Selected"
70
+ (`material.calendar.selected`, translated); today has `aria-current="date"` on
71
+ both. The days of the neighbouring months are named by their own dates, so a
72
+ number alone matches no day. "Selected" comes from the `Calendar` itself, which
73
+ renders the hidden hint; a calendar hand-built from the exported parts names its
74
+ cells and buttons the same way but has no "Selected". `DateRangeField`'s
75
+ calendar does the same:
76
+
77
+ ```ts
78
+ await page.getByRole('gridcell', { name: 'Thursday, October 1, 2026', exact: true }).getByRole('button').click();
79
+ ```
80
+
81
+ **Weekday row.** It follows the same language: "S", "M"… in English, "L",
82
+ "M"… under a French `I18nProvider`, with the week starting on the locale's
83
+ first day (Monday in French). Each header is labelled by its full weekday
84
+ ("lundi"), as in `@nxgt/material`; reka hides the row from assistive tech
85
+ (`<thead aria-hidden>`), since every day's name already carries its weekday.
86
+ It reads at 4.5:1 or more on the popover in the light and the dark theme.
87
+
64
88
  No form wrapper — use `DateFormField`.
65
89
  **Storybook:** `Date & time/Calendar` · **React:** `Calendar`, `CalendarDayButton`
66
90
 
@@ -52,6 +52,8 @@ import { CircleAlert } from '@lucide/vue';
52
52
 
53
53
  The default slot is rendered under the description.
54
54
 
55
+ The root carries `data-slot="alert"`, the title `data-slot="alert-title"` and the description `data-slot="alert-description"`, for tests and selectors.
56
+
55
57
  **Storybook:** `Feedback & status/Alert` · **React:** `Alert`
56
58
 
57
59
  ---
@@ -157,7 +159,8 @@ the content brings both.
157
159
  On open, `AlertDialogContent` focuses `AlertDialogCancel`, the least
158
160
  destructive action (its first control when there is no Cancel, the content
159
161
  when there is none); an `@open-auto-focus` that calls `event.preventDefault()`
160
- keeps focus where it was. After that opt-out the Cancel takes no focus until the next
162
+ keeps focus off the Cancel, on the content: the page behind an alert dialog is
163
+ `inert` while it is open ([Overlays & menus](overlays-and-menus.md)). After that opt-out the Cancel takes no focus until the next
161
164
  task: focus it from a `setTimeout`, not in the handler.
162
165
 
163
166
  **Emits:** `update:open` on `AlertDialog` — `boolean`.
@@ -239,7 +242,7 @@ function archive() {}
239
242
  | Prop (`ConfirmationDialog`) | Type | Default | Effect |
240
243
  | --- | --- | --- | --- |
241
244
  | `#trigger` slot | — | — | becomes the dialog trigger (`as-child`, no extra button): pass one element, such as a `Button` |
242
- | `v-model:open` | `boolean` | uncontrolled | open state; without it the dialog keeps its own |
245
+ | `v-model:open` | `boolean` | uncontrolled | open state: with `v-model:open`, or `:open` together with `@update:open`, the dialog shows exactly what `open` says; otherwise it keeps its own state (`:open` alone only opens it, `@update:open` alone only listens; an `open` that is still `undefined`, as `ref<boolean>()`, counts as not set and the dialog keeps its own state, so it still closes) |
243
246
  | `@confirm` | `(values?) => void` | — | emitted on confirm, with the form's parsed values when there is one; the dialog then closes |
244
247
  | `form-options` | `ConfirmationDialogFormOptions` (`{ schema, defaultValues? }`) | — | a form in the dialog, below the default slot — see below |
245
248
  | `color` | `IconButton` colour | `'warning'` | icon and both buttons |
@@ -247,6 +250,11 @@ function archive() {}
247
250
  | `#icon` slot | — | info icon, in a decorative `aria-hidden` badge | replaces the badge: keep yours decorative (`<IconButton as="span" aria-hidden="true">`), not a button that does nothing |
248
251
  | `confirm-text` / `cancel-text` | `string` | "Confirm" / "Cancel", translated (`material.confirm-dialog.confirm` / `.cancel`) | button text |
249
252
 
253
+ **Several steps, one dialog.** Confirm closes it; to show the next step, change
254
+ its `title` (and `description`) and set `open` back to `true`, at once (from
255
+ `@confirm` or `update:open`) or after an `await`. No `:key` per step is needed,
256
+ and the reopened dialog's buttons take the click.
257
+
250
258
  `form-options` puts a form in the confirmation, as React's `formOptions`:
251
259
  `schema` is a Zod 4 schema (any Standard Schema, or a vee-validate typed
252
260
  schema), `defaultValues` its initial values, and the `#form` slot renders the
@@ -564,16 +572,83 @@ async function upload() {
564
572
  | --- | --- | --- | --- |
565
573
  | `title` / `description` | `string` | preset title for `success`, `error`, `loading` | toast text |
566
574
  | `icon` | a VNode (`toast`) · icon name (`toasts.*`) | preset icon | leading icon |
567
- | `action` | `{ label, onClick? }` or `(dismiss) => VNode` | a translated "Close" for `success`, `info`, `error` | trailing button |
568
- | `options` | vue-sonner `ExternalToast` | `duration: 5000` for presets, `Infinity` for `loading` | passed to Sonner (`id`, `duration`, `position`…) |
575
+ | `action` | `{ label, onClick? }` or `(dismiss) => VNode` | a translated “Close” for `success`, `info`, `error`; a preset keeps the caller's own | trailing button: `onClick`, then the toast closes; or what the function renders, given `dismiss`. Brings the X button and a persistent toast, see below |
576
+ | `dismissLabel` | `string` | the translated “Close” when an `action` is given | accessible name of the X icon button beside the action; `''` hides it |
577
+ | `role` | `'status' \| 'alert'` | none (`toasts.error`: `'alert'`) | live-region role. Toasts are already announced politely by the `Toaster`'s region; `alert` interrupts, and may be read twice by some screen readers |
578
+ | `options` | vue-sonner `ExternalToast` | `duration: 5000` for presets, `Infinity` for `loading` and for any toast with your own `action` (a preset's fallback "Close" still times out at 5000) unless a `duration` is passed | passed to Sonner (`id`, `duration`, `position`…) |
569
579
 
570
580
  The toast id defaults to the title, so two toasts with the same title
571
- replace each other; pass `options: { id }` to show both. `Toaster` is
572
- vue-sonner's and takes its props (`position`, `expand`, `rich-colors`…);
573
- vue-sonner's stylesheet comes with this package's (`styles.css`, or the
574
- compiled stylesheet). `Toast` is the component each toast
575
- renders, and `sonnerToast` is vue-sonner's own `toast`, for anything the
576
- presets do not cover.
581
+ replace each other; pass `options: { id }` to show both. `Toaster` wraps
582
+ vue-sonner's and takes its props (`position`, `expand`, `rich-colors`…), all
583
+ forwarded; mount the one from `@nxgt/material-vue/components`, not
584
+ vue-sonner's own, or neither phone change below applies. vue-sonner's
585
+ stylesheet comes with this package's (`styles.css`, or the compiled
586
+ stylesheet). `Toast` is the component each toast renders, and `sonnerToast`
587
+ is vue-sonner's own `toast`, for anything the presets do not cover.
588
+
589
+ **Types:** `ToasterProps` (vue-sonner's), from `@nxgt/material-vue/components`.
590
+
591
+ **On a phone** (under 600px wide, where vue-sonner lays toasts out across the
592
+ screen), a `Toaster` with no `position` shows its toasts at the top
593
+ (`top-center`): across the bottom they covered what a form keeps there, its
594
+ submit button first. A `position` you pass applies on every screen. The server
595
+ and the client's first render answer for a desktop (`bottom-right`, or for the
596
+ width `provideSSRWidth()` gives), and a phone moves the toasts to the top once
597
+ mounted. The phone offset is vue-sonner's `mobile-offset` (16px by default);
598
+ raise its `top` when a sticky header must stay visible:
599
+ `<Toaster :mobile-offset="{ top: 72 }" />`.
600
+
601
+ ```vue
602
+ <Toaster /> <!-- bottom-right on a desktop, top-center on a phone -->
603
+ <Toaster position="bottom-center" /> <!-- bottom-center everywhere -->
604
+ ```
605
+
606
+ **An action keeps the toast open.** `toast()` itself does it, so a preset and a
607
+ direct call behave alike: any toast with an `action` is persistent by default
608
+ (`options.duration: Infinity`, WCAG 2.2.1: a timed toast can vanish before a
609
+ keyboard or screen-reader user reaches its button) unless you pass
610
+ `options.duration`, and shows a small close (X) icon button beside the action,
611
+ named by `material.toast.buttons.close` ("Close" / "Fermer") unless you pass
612
+ your own `dismissLabel` (`''` hides it). The presets keep the `action` you pass
613
+ (`toasts.info({ description: 'A new version is available', action: { label:
614
+ 'Reload', onClick } })`); without an `action`, the preset's text "Close" button
615
+ is unchanged, on a timed toast. Sonner pauses its timer while a mouse hovers a
616
+ toast, while a finger is down on it and while the page is hidden, but not on
617
+ keyboard focus, which is why the default is persistent. A tap does not hover:
618
+ the mouse a browser emulates after a tap (which never leaves until the next tap
619
+ elsewhere) is ignored, so a tapped toast still goes after its duration. On a
620
+ touch screen a tap no longer fans out a stack of two or more toasts either: pass
621
+ `expand` to keep the stack open. Sonner does not close a toast on Escape (it
622
+ only collapses the stack); this `Toast` does when focus is inside it, with or
623
+ without an action. The close button is reached with Tab. **Focus after a
624
+ dismiss:** the dismissed toast leaves the page with the focused control in it,
625
+ so focus falls back to `document.body`; move it yourself (`onClick`, or after
626
+ `toasts.dismiss()`) when the flow needs it elsewhere.
627
+
628
+ **Announced to screen readers:** `Toaster` renders a polite live region
629
+ (`<section aria-live="polite">`) that is in the page before the first toast,
630
+ so every toast inserted into it is read out. A toast therefore has no `role`
631
+ of its own: one on each toast would nest a second live region. `toasts.error`
632
+ is `role="alert"` (assertive), so a failure interrupts; pass `role` to
633
+ `toast()` or to a preset to choose another (a caller's wins). Mount `Toaster`
634
+ once, early, and not conditionally after the first toast. A `Toast` rendered
635
+ outside a `Toaster` is not in a live region: give it its own.
636
+
637
+ **In tests:** find a toast by its text within the region, and an error toast
638
+ by role. A `status` or `alert` takes no name from its content, so read the
639
+ message with `toHaveTextContent`.
640
+
641
+ ```ts
642
+ const alert = await screen.findByRole('alert'); // toasts.error
643
+ expect(alert).toHaveTextContent('Save failed');
644
+ const saved = await screen.findByText('Item saved'); // toasts.success / info
645
+ expect(saved.closest('section[aria-live="polite"]')).not.toBeNull();
646
+ ```
647
+
648
+ Wait with `findBy*` or `waitFor`, never a fixed delay. A toast stays in the
649
+ DOM until it is dismissed or times out (a toast with an action never times
650
+ out), so queries in one test file can see the previous
651
+ test's toasts: call `toasts.dismiss()` first.
577
652
 
578
653
  **Storybook:** `Feedback & status/Toast` · **React:** `Toaster`, `toast`, `toasts`, `Toast` (`ToastAction` type)
579
654
 
@@ -640,7 +715,7 @@ import { HydrateLoading } from '@nxgt/material-vue/components';
640
715
  | --- | --- | --- | --- |
641
716
  | `#icon` slot | — | `<Spinner class="size-8" />` | replaces the spinner |
642
717
  | default slot | — | — | shown above the spinner |
643
- | `class` | `string` | — | the root fills the viewport (`size-lv`); override to box it |
718
+ | `class` | `string` | — | the root fills the viewport (`w-lvw h-lvh`); override to box it |
644
719
 
645
720
  **Storybook:** `Feedback & status/HydrateLoading` · **React:** `HydrateLoading`
646
721
 
@@ -498,8 +498,8 @@ const code = ref('');
498
498
  | `length` | `number` | `6` | number of slots, split in two groups |
499
499
  | `v-model` | `string` | — | the digits typed so far |
500
500
  | `@complete` | `(value: string) => void` | — | once every slot is filled (from `vue-input-otp`) |
501
- | `error` | `boolean` | — | error colour on the slots |
502
- | `id` | `string` | from `useId()` | the input's id, which the label points to |
501
+ | `error` | `boolean` | — | error colour on the slots (`data-invalid` on each); `aria-invalid` goes on the input only, the one element a screen reader reads |
502
+ | `id` | `string` | from `useId()` | the input's id, which the label points to; it differs per field — `OtpFormField` too (not the field `name`), so two code forms on one page each keep their name |
503
503
 
504
504
  `InputOTP`, `InputOTPGroup`, `InputOTPSlot` and `InputOTPSeparator` are the
505
505
  unlabelled primitives it is built from, for a hand-built layout:
@@ -527,10 +527,19 @@ const pin = ref('');
527
527
  </template>
528
528
  ```
529
529
 
530
+ **On a phone.** The slots are 36 px squares that shrink with a narrow
531
+ field, kept square: six fit a 320 px-wide card, each above the 24 px
532
+ target of WCAG 2.5.8, and the gaps between them narrow under the `sm`
533
+ breakpoint. A slot you build yourself with `InputOTPSlot` does the same.
534
+
535
+ **In a test**, find the code by its input — `getByLabelText('Enter OTP')` —
536
+ and an error by `toHaveAttribute('aria-invalid', 'true')` on it; a slot
537
+ carries `data-invalid`, not `aria-invalid`.
538
+
530
539
  **Emits:** `update:modelValue` with `string` (the digits typed so far).
531
540
  **Form wrapper:** `OtpFormField` — `name` inside a vee-validate form, see
532
541
  [forms](../guides/forms.md#the-pair) and [what each field emits](../guides/forms.md#what-each-field-emits).
533
- **Storybook:** `Form fields/OtpField` · **React:** `OtpField`, `OtpFormField`, `InputOTP` (same parts)
542
+ **Storybook:** `Form fields/OtpField`, `Form fields/OtpField/Phone` · **React:** `OtpField`, `OtpFormField`, `InputOTP` (same parts)
534
543
 
535
544
  ---
536
545
 
@@ -818,7 +827,7 @@ a screen reader says "Notifications, switch, Email me when an order ships". An
818
827
  `aria-label` or `aria-labelledby` you pass wins, and an `aria-describedby` you
819
828
  pass is kept beside the row text. The `helper-text` (the validation message,
820
829
  under `SwitchFormField`) describes the switch too. With `required`, the label's
821
- asterisk is hidden from screen readers, so the name stays "Terms", not
830
+ asterisk is drawn by CSS, so the name and the label text stay "Terms", not
822
831
  "Terms*". Without an `id` one is generated. The row is
823
832
  tinted while the switch is on, from `v-model` or, uncontrolled, from
824
833
  `default-value` and each toggle, and carries `data-checked="true"` /
@@ -751,6 +751,13 @@ Playback uses the browser's `<video>` / `<audio>`: a format the browser cannot
751
751
  decode (H.264 MP4 in a Chromium build without proprietary codecs) shows the
752
752
  player's error state with a Retry button. `StxPlayer` mounts on the client only.
753
753
 
754
+ The speed and captions menus are radio groups: each entry is a
755
+ `menuitemradio` and the chosen one `aria-checked="true"` (captions: "Off" or
756
+ one track's `label`). `c` toggles the captions; turned on before a language
757
+ was chosen, they show the current track's first caption track, which the menu
758
+ then checks. In a test, pick one with
759
+ `getByRole('menuitemradio', { name: 'English' })`.
760
+
754
761
  **Storybook:** `Media & files/StxPlayer` · **React:** `StxPlayer` (same parts)
755
762
 
756
763
  ---
@@ -72,11 +72,14 @@ which work alone; `findActiveMenuItem`, `isMenuItemActive` and
72
72
  | Prop | Type | Default | Effect |
73
73
  | --- | --- | --- | --- |
74
74
  | `items` | `MenuItem[]` | — | `label` (already translated), `url`, `icon` (a Font Awesome name), `footer` |
75
+ | `linkComponent` | `Component` | `RouterLink` | The link each menu entry renders, given `href`; pass it as `link-component` (see the [app shell guide](../guides/app-shell.md)) |
75
76
  | `avatarSrc` | `string` | — | avatar image in the header |
76
77
  | `#brand` | slot | — | sidebar header |
77
78
  | `#header-actions`, `#avatar` | slots | — | header, before the avatar; `#avatar` replaces it |
78
79
  | `title`, `subtitle`, `#actions` | `string`, `string`, slot | — | `ActivityContent`'s header card |
79
80
  | `#header` / `hideHeader` | slot / `boolean` | — / `false` | `ActivityContent`: replace or drop the header card |
81
+ | `contentAs` / `scroll` | `'main' \| 'div'` / `'document' \| 'container'` | `'main'` / `'document'` | the content region is the page's `<main>`; the document scrolls (see the [app shell guide](../guides/app-shell.md#landmarks-headings-and-scrolling)) |
82
+ | `titleAs` | `'h1'` … `'h6'` \| `'div'` | `'h1'` | `ActivityContent`: the title's heading level |
80
83
 
81
84
  The active entry is the longest match across the whole menu, segment-aware.
82
85
  `AppBreadcrumb` reads `meta.breadcrumb` (a string, or a function of the
@@ -86,6 +89,11 @@ to the router's root, so a history base is kept; a crumb with params links to
86
89
  its path with them filled in) and the current page is text with
87
90
  `aria-current="page"`.
88
91
 
92
+ `AppSidebar` gives each entry its label as a tooltip **only while it is
93
+ collapsed**: expanded (and in the phone menu sheet) the label is on screen
94
+ and no tooltip opens. A `SidebarMenuItem` of your own shows the `tooltip` you
95
+ pass, collapsed or expanded.
96
+
89
97
  **Storybook:** `Navigation & layout/AppShell` · **React:** `ActivityLayout`, `ActivityContent`, `AppSidebar`, `AppHeader`, `AppBreadcrumb`
90
98
 
91
99
  ---
@@ -135,9 +143,19 @@ const expanded = ref(false);
135
143
  ```
136
144
 
137
145
  Parts: `Sidebar`, `SidebarHeader`, `SidebarContent`, `SidebarFooter`,
138
- `SidebarMenuLabel` (`#actions` slot), `SidebarMenuItem` (a button: `#icon`
146
+ `SidebarMenuLabel` (`#actions` slot), `SidebarMenuItem` (a button, or a link with `href` and `link-component`: `#icon`
139
147
  slot, `active`, `tooltip`, `@click`).
140
148
 
149
+ A `SidebarMenuItem` stays a `<button>` without `href`. With `href` it renders a
150
+ real link, a plain `<a>` unless you pass `link-component` (given the `href`):
151
+
152
+ ```vue
153
+ <SidebarMenuItem href="/inbox" :link-component="MyLink">
154
+ <template #icon><Icon name="inbox" /></template>
155
+ Inbox
156
+ </SidebarMenuItem>
157
+ ```
158
+
141
159
  | Prop | Type | Default | Effect |
142
160
  | --- | --- | --- | --- |
143
161
  | `v-model:expanded` | `boolean` | `false` | labels beside the icons; collapsed shows icons with tooltips |
@@ -9,6 +9,19 @@ uncontrolled. Dialogs that exist to confirm something live on
9
9
  [Feedback & status](feedback-and-status.md). Everything is imported from
10
10
  `@nxgt/material-vue/components`.
11
11
 
12
+ **The page behind a modal layer is inert.** While a `Dialog`, `Sheet`,
13
+ `Drawer`, `AlertDialog` or `CommandDialog` is open — and a `Select` list, a
14
+ `ContextMenu`, or a `DropdownMenu` or `Popover` given `modal` — everything else
15
+ on the page is `inert` as well as `aria-hidden`: its controls leave the tab
16
+ order and the accessibility tree, so a keyboard or screen-reader user stays in
17
+ the layer and axe's `aria-hidden-focus` passes. Live regions stay reachable:
18
+ a toast shown over a dialog is announced, and its action can be used. Layers
19
+ stack: a `Select` open in a `Dialog` makes the dialog inert too, until it
20
+ closes. Focus left on the page when the layer opens (an `@open-auto-focus`
21
+ that prevents the default and focuses nothing) goes to the layer's content.
22
+ Non-modal layers (`:modal="false"`, and `DropdownMenu` and `Popover` by
23
+ default) leave the page as it is.
24
+
12
25
  | | Component | What it does |
13
26
  | --- | --- | --- |
14
27
  | <img src="../previews/overlays-menus-dialog.webp" width="96" alt=""> | [Dialog](#dialog) | Modal dialog with a close button |
@@ -74,7 +87,7 @@ overlay, for a dialog taller than the screen).
74
87
  | Prop | Type | Default | Effect |
75
88
  | --- | --- | --- | --- |
76
89
  | `default-open` / `v-model:open` (`Dialog`) | `boolean` | `false` | open state, uncontrolled / controlled |
77
- | `modal` (`Dialog`) | `boolean` | `true` | `false` drops the backdrop and lets the page stay interactive |
90
+ | `modal` (`Dialog`) | `boolean` | `true` | `false` drops the backdrop and lets the page stay interactive; `true` makes the page behind `inert` while it is open |
78
91
  | `as-child` (`DialogTrigger`) | `boolean` | `false` | the trigger is its child — pass it with a `Button`, or the trigger renders its own `<button>` around it |
79
92
  | `show-close-button` (`DialogContent`) | `boolean` | `true` | the top-right X, named "Close" (`material.buttons.close`, translated) |
80
93
  | `show-close-button` (`DialogFooter`) | `boolean` | `false` | adds an outlined "Close" button (`material.buttons.close`, translated) |
@@ -86,7 +99,10 @@ Unlike React, `DialogContent` renders the X by itself (turn it off with
86
99
  On open, `DialogContent` moves focus into the dialog: to its first control
87
100
  (the built-in X when there is nothing before it), or to the content itself
88
101
  when it has none. An `@open-auto-focus` that calls `event.preventDefault()`
89
- keeps focus where it was. `SheetContent`, `DrawerContent` and
102
+ keeps focus off that control: a modal dialog then focuses its content, as the
103
+ page behind it is inert, and a non-modal one leaves focus where it was
104
+ ([details](../troubleshooting.md#focus-moves-into-a-dialog-although-open-auto-focus-prevented-it)).
105
+ `SheetContent`, `DrawerContent` and
90
106
  `AlertDialogContent` do the same; an alert dialog focuses its
91
107
  `AlertDialogCancel`.
92
108
 
@@ -371,7 +387,7 @@ shows.
371
387
 
372
388
  ![DropdownMenu](../previews/overlays-menus-dropdownmenu.webp)
373
389
 
374
- Menu with checkbox and radio items, submenus, shortcuts and a destructive item.
390
+ Menu with checkbox and radio items, submenus, shortcuts and a destructive item. It is non-modal by default: Reka's modal menu marks the rest of the page `aria-hidden` while its buttons and links stay focusable, which accessibility audits (axe `aria-hidden-focus`) report; a non-modal menu hides nothing and still closes on Escape, an outside click or Tab. Pass `modal` for a modal one: the page behind it is then `inert` too, so its controls leave the tab order and axe passes.
375
391
 
376
392
  ```vue
377
393
  <script setup lang="ts">
@@ -421,7 +437,7 @@ Parts: `DropdownMenu`, `DropdownMenuTrigger`, `DropdownMenuContent`,
421
437
  | `variant` (`DropdownMenuItem`) | `'default' \| 'error'` | `'default'` | error colour for a destructive item; `'destructive'` is a deprecated alias of `'error'`; both render `data-variant="error"`, so style or test with `[data-variant=error]` |
422
438
  | `inset` (item, label, sub-trigger) | `boolean` | — | indents to line up with checkbox and radio items |
423
439
  | `@select` (`DropdownMenuItem`) | `(event: Event) => void` | — | the item's action; `event.preventDefault()` keeps the menu open |
424
- | `default-open` / `v-model:open`, `modal` (`DropdownMenu`) | `boolean` | —, `true` | open state; a modal menu blocks the page behind it |
440
+ | `default-open` / `v-model:open`, `modal` (`DropdownMenu`) | `boolean` | —, `false` | open state; `modal` brings back the scroll lock, swallowed outside clicks and an `aria-hidden`, `inert` page behind the menu |
425
441
  | `to` (`DropdownMenuContent`) | `string \| HTMLElement` | `'body'` | where the menu is teleported; for a menu opened in fullscreen, pass the fullscreen element, read when the menu opens (see [troubleshooting](../troubleshooting.md#a-dropdownmenu-opened-in-fullscreen-does-not-show)) |
426
442
 
427
443
  Vue's `DropdownMenuItem` has no `active` prop. CSS or a test that selected