Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/api-generator/src/locale/en/VNavRail.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"description": "A vertical navigation rail, typically anchored to the start edge of the application, for switching between top-level destinations.",
"props": {
"location": "Controls the edge of the screen the rail is attached to.",
"width": "Sets the width of the rail.",
"align": "Aligns the rail items along the main (vertical) axis.",
"active": "Controls whether the rail is shown and reserves layout space for it."
},
"slots": {
"default": "The default slot for `v-nav-rail-item` destinations.",
"prepend": "A slot at the top of the rail, typically for a menu button or FAB.",
"append": "A slot at the bottom of the rail, typically for settings or account actions."
},
"events": {
"update:active": "Event that is emitted when the active state changes."
}
}
10 changes: 10 additions & 0 deletions packages/api-generator/src/locale/en/VNavRailItem.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"description": "A single destination within a [v-nav-rail](/components/nav-rails), rendering an icon button with a label below it.",
"props": {
"title": "The label shown beneath the icon.",
"icon": "The icon shown for the inactive (and default) state.",
"activeIcon": "The icon shown when the item is selected. Falls back to `icon` when not provided.",
"divider": "Renders a horizontal divider after the item.",
"verticalDivider": "Renders a vertical divider after the item."
}
}
1 change: 1 addition & 0 deletions packages/docs/src/data/page-to-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
"components/mask-inputs": ["VMaskInput"],
"components/menus": ["VMenu"],
"components/navigation-drawers": ["VNavigationDrawer"],
"components/nav-rails": ["VNavRail", "VNavRailItem"],
"components/no-ssr": ["VNoSsr"],
"components/month-pickers": ["VMonthPicker"],
"components/number-inputs": ["VNumberInput"],
Expand Down
162 changes: 162 additions & 0 deletions packages/docs/src/examples/v-data-table/virtual-expanded-rows.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<template>
<v-container>
<v-data-table-virtual
:headers="headers"
:items="groups"
height="400"
item-value="id"
fixed-header
show-expand
>
<template v-slot:item.count="{ item }">
<v-chip :text="`${item.children.length} items`" size="small"></v-chip>
</template>

<template v-slot:item.data-table-expand="{ item, internalItem, isExpanded, toggleExpand }">
<v-icon-btn
v-if="item.children.length"
:icon="isExpanded(internalItem) ? '$collapse' : '$expand'"
icon-size="20"
variant="text"
@click="toggleExpand(internalItem)"
></v-icon-btn>

<v-icon-btn
v-else
icon="$plus"
icon-size="20"
@click="addChild(item); isExpanded(internalItem) || toggleExpand(internalItem)"
></v-icon-btn>
</template>

<template v-slot:expanded-row="{ columns, item }">
<v-data-table-row
v-for="child in item.children"
:key="child.id"
:item="asRow(child)"
class="bg-surface-light"
>
<template v-slot:item.name="{ value }">
<span class="pl-4">{{ value }}</span>
</template>

<template v-slot:item.data-table-expand>
<v-icon-btn
icon="$close"
icon-color="red"
icon-size="20"
variant="text"
@click="removeChild(item, child)"
></v-icon-btn>
</template>
</v-data-table-row>

<tr>
<td
:colspan="columns.length"
class="pa-0 border-dashed border-sm border-opacity-50"
style="height: 32px"
>
<v-btn
height="32"
icon="$plus"
size="small"
text="row"
variant="text"
block
tile
@click="addChild(item)"
></v-btn>
</td>
</tr>
</template>
</v-data-table-virtual>
</v-container>
</template>

<script setup>
import { ref } from 'vue'

let nextId = 0

const newChild = () => ({ id: `child-${nextId++}`, name: `Item ${nextId}`, type: 'child' })

const headers = [
{ title: 'Name', key: 'name', width: 170 },
{ title: 'Type', key: 'type', width: 'auto' },
{ title: 'Children', key: 'count', width: 140, sortable: false },
{ key: 'data-table-expand', width: 96, align: 'end' },
]

const groups = ref(Array.from({ length: 200 }, (_, i) => ({
id: i,
name: `Group ${i + 1}`,
type: 'group',
children: Array.from({ length: i % 7 ? 2 + (i % 4) : 0 }, newChild),
})))

function addChild (group) {
group.children.push(newChild())
}

function removeChild (group, child) {
group.children.splice(group.children.indexOf(child), 1)
}

function asRow (child) {
return {
type: 'item',
key: child.id,
value: child.id,
raw: child,
columns: { name: child.name, type: child.type },
selectable: false,
}
}
</script>

<script>
export default {
data: () => ({
nextId: 0,
groups: [],
headers: [
{ title: 'Name', key: 'name', width: 170 },
{ title: 'Type', key: 'type', width: 'auto' },
{ title: 'Children', key: 'count', width: 140, sortable: false },
{ key: 'data-table-expand', width: 96, align: 'end' },
],
}),

created () {
this.groups = Array.from({ length: 200 }, (_, i) => ({
id: i,
name: `Group ${i + 1}`,
type: 'group',
children: Array.from({ length: i % 7 ? 2 + (i % 4) : 0 }, () => this.newChild()),
}))
},

methods: {
newChild () {
return { id: `child-${this.nextId++}`, name: `Item ${this.nextId}`, type: 'child' }
},
addChild (group) {
group.children.push(this.newChild())
},
removeChild (group, child) {
group.children.splice(group.children.indexOf(child), 1)
},
asRow (child) {
return {
type: 'item',
key: child.id,
value: child.id,
raw: child,
columns: { name: child.name, type: child.type },
selectable: false,
}
},
},
}
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ meta:

# Creating a highly optimized Vuetify 3 project with Nuxt and UnoCSS

Although traditionally we tend to think about Vuetify as a great fit for large projects, it can also serve smaller projects that tend to prioritize performance. When scaffolding new projects it comes with tree-shaking out-of-the-box, but there is still some room for improvement as main CSS bundle ships hundreds of kB of CSS we might not need. If the users like your page loading animation and you can afford a bit of overhead, there is nothing wrong with simplified setup that does the job and let's you focus on the business logic or UX. That said, integrating UnoCSS (atomic CSS engine) will help us trim the bundle and enable dynamic utilities. With it's superpowers we not only avoid writing plain CSS (most of the time), but can also make use of TailwindCSS tooling (IDE extensions) or standardize certain utilities.
Although traditionally we tend to think about Vuetify as a great fit for large projects, it can also serve smaller projects that tend to prioritize performance. When scaffolding new projects it comes with tree-shaking out-of-the-box, but there is still some room for improvement as main CSS bundle ships hundreds of kB of CSS we might not need. If the users like your page loading animation and you can afford a bit of overhead, there is nothing wrong with simplified setup that does the job and lets you focus on the business logic or UX. That said, integrating UnoCSS (atomic CSS engine) will help us trim the bundle and enable dynamic utilities. With its superpowers we not only avoid writing plain CSS (most of the time), but can also make use of TailwindCSS tooling (IDE extensions) or standardize certain utilities.

The article will walk through scaffolding a starter project, installing and wiring UnoCSS, customizing fonts, handling light/dark mode and aligning breakpoints. Finally, "CSS layers" section shows how to order Vuetify, UnoCSS, and app‑specific styles to avoid specificity conflicts.

Expand Down Expand Up @@ -213,7 +213,7 @@ bun add -D @unocss/preset-wind4

:::

The preset called [Wind4](https://unocss.dev/presets/wind4) is an official configuration builder providing making it easy to expose all the utilities from TailwindCSS v4 in your project. There are 2 configuration points - an object passed to the main `presetWind4` method and `theme` field next to the `presets`. Unlike using pure TailwindCSS v4 it let's us configure stuff in JavaScript code.
The preset called [Wind4](https://unocss.dev/presets/wind4) is an official configuration builder providing making it easy to expose all the utilities from TailwindCSS v4 in your project. There are 2 configuration points - an object passed to the main `presetWind4` method and `theme` field next to the `presets`. Unlike using pure TailwindCSS v4 it lets us configure stuff in JavaScript code.

```ts { resource="nuxt.config.ts" }
import presetWind4 from '@unocss/preset-wind4'
Expand Down
2 changes: 1 addition & 1 deletion packages/docs/src/pages/en/components/alerts.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ The `v-alert` has 6 style variants, **elevated**, **flat**, **tonal**, **outline

#### Closable

The **closable** prop adds a [v-icon](/components/icons) on the far right, after the main content. This control hides the `v-alert` when clicked, setting it's internal model to **false**. Manually control the visibility of the alert by binding **v-model** or using **model-value**. The following example uses a dynamic model that shows and hides the `v-alert` component:
The **closable** prop adds a [v-icon](/components/icons) on the far right, after the main content. This control hides the `v-alert` when clicked, setting its internal model to **false**. Manually control the visibility of the alert by binding **v-model** or using **model-value**. The following example uses a dynamic model that shows and hides the `v-alert` component:

<ExamplesExample file="v-alert/prop-closable" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,9 @@ The v-data-table-virtual component relies on all data being available locally. B
When customizing rows with the `#item` slot, you must bind the provided `itemRef` to your `<tr>`. This ensures that the virtual scroller can correctly measure and recycle rows.

<ExamplesExample file="v-data-table/virtual-custom" />

### Expanded rows

The `expanded-row` slot may hold any number of rows. Use `v-data-table-row` instead of plain `<tr>` for rows that should line up with the parent columns.

<ExamplesExample file="v-data-table/virtual-expanded-rows" />
2 changes: 1 addition & 1 deletion packages/docs/src/pages/en/features/aliasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Although treeshaking is automatically applied during production builds, it is ad

## Virtual component defaults

Virtual components have access to the Vuetify [Global configuration](/features/global-configuration/). Default settings for aliases are defined the same as built-in components with no extra steps required by you. In the following example, **MyButton** uses [v-btn props](/api/v-btn/#props) to change it's default **variant**:
Virtual components have access to the Vuetify [Global configuration](/features/global-configuration/). Default settings for aliases are defined the same as built-in components with no extra steps required by you. In the following example, **MyButton** uses [v-btn props](/api/v-btn/#props) to change its default **variant**:

```js { resource="src/plugins/vuetify.js"}
import { createVuetify } from 'vuetify'
Expand Down
2 changes: 1 addition & 1 deletion packages/vuetify/src/components/VCalendar/modes/stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const WIDTH_MULTIPLIER = 1.7
* whitespace is reduced. If there is a hole in columns the event width is
* scaled up so it intersects with the next column. The columns have equal
* width in the space they are given. If the event doesn't have any to the
* right of it that intersect with it's content it's right side is extended
* right of it that intersect with its content, its right side is extended
* to the right side.
*/

Expand Down
63 changes: 45 additions & 18 deletions packages/vuetify/src/components/VDataTable/VDataTableVirtual.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ import { MaybeTransition } from '@/composables/transition'
import { makeVirtualProps, useVirtual } from '@/composables/virtual'

// Utilities
import { cloneVNode, computed, shallowRef, toRef, toRefs, watch } from 'vue'
import { cloneVNode, computed, Fragment, isVNode, nextTick, shallowRef, toRef, toRefs, watch } from 'vue'
import { convertToUnit, genericComponent, omit, pickWithRest, propsFactory, useRender } from '@/util'

// Types
import type { DeepReadonly } from 'vue'
import type { DeepReadonly, VNode, VNodeArrayChildren } from 'vue'
import type { VDataTableSlotProps } from './VDataTable'
import type { VDataTableHeadersSlots } from './VDataTableHeaders'
import type { VDataTableRowsSlots } from './VDataTableRows'
Expand Down Expand Up @@ -67,6 +67,16 @@ export const makeVDataTableVirtualProps = propsFactory({

type ItemType<T> = T extends readonly (infer U)[] ? U : never

function elementNodes (nodes?: VNodeArrayChildren): VNode[] {
return (nodes ?? []).flatMap(node =>
Array.isArray(node) ? elementNodes(node)
: !isVNode(node) ? []
: node.type === Fragment ? elementNodes(node.children as VNodeArrayChildren)
: typeof node.type === 'symbol' ? []
: [node]
)
}

export const VDataTableVirtual = genericComponent<new <T extends readonly any[], V>(
props: {
items?: T
Expand Down Expand Up @@ -163,18 +173,32 @@ export const VDataTableVirtual = genericComponent<new <T extends readonly any[],
}))
)

// one virtual size per index = item row + optional expanded row
// one virtual size per index = item row + optional expanded rows
const rowHeights = new Map<number, number>()
const expandedHeights = new Map<number, number>()
const expandedHeights = new Map<number, number[]>()

function updateSize (index: number) {
handleItemResize(index, (rowHeights.get(index) ?? 0) + (expandedHeights.get(index) ?? 0))
const expanded = expandedHeights.get(index)?.reduce((sum, height) => sum + (height || 0), 0) ?? 0
handleItemResize(index, (rowHeights.get(index) ?? 0) + expanded)
}

function setRowHeight (index: number, height: number) {
rowHeights.set(index, height)
updateSize(index)
}
function setExpandedHeight (index: number, height: number) {
expandedHeights.set(index, height)

function setExpandedHeight (index: number, row: number, height: number) {
const heights = expandedHeights.get(index) ?? []
heights[row] = height
expandedHeights.set(index, heights)
updateSize(index)
}

function trimExpandedHeights (index: number, count: number) {
const heights = expandedHeights.get(index)
if (!heights || heights.length <= count) return

heights.length = count
updateSize(index)
}

Expand Down Expand Up @@ -295,6 +319,14 @@ export const VDataTableVirtual = genericComponent<new <T extends readonly any[],
const { props: rowProps, ...itemSlot } = itemSlotProps
const index = itemSlotProps.internalItem.virtualIndex ?? itemSlotProps.internalItem.index
const itemExpanded = isExpanded(itemSlotProps.internalItem)
const expandedRows = props.showExpand && itemExpanded && slots['expanded-row']
? elementNodes(slots['expanded-row'](itemSlot))
: []

// rows that stay never resize, so a shrunk slot reports nothing
if (slots['expanded-row'] && (expandedHeights.get(index)?.length ?? 0) > expandedRows.length) {
nextTick(() => trimExpandedHeights(index, expandedRows.length))
}

return (
<>
Expand All @@ -318,25 +350,20 @@ export const VDataTableVirtual = genericComponent<new <T extends readonly any[],

{ props.showExpand && (
slots['expanded-row']
? itemExpanded && (
? expandedRows.map((node, row) => (
<VVirtualScrollItem
key={ `${index}-expanded` }
key={ `${index}-expanded-${row}` }
renderless
onUpdate:height={ height => setExpandedHeight(index, height) }
onUpdate:height={ height => setExpandedHeight(index, row, height) }
>
{ ({ itemRef }) => {
const nodes = slots['expanded-row']!(itemSlot)
return nodes?.length
? cloneVNode(nodes[0], { ref: itemRef }, true)
: undefined
}}
{ ({ itemRef }) => cloneVNode(node, { ref: itemRef }, true) }
</VVirtualScrollItem>
)
))
: slots.expanded && (
<VVirtualScrollItem
key={ `${index}-expanded` }
renderless
onUpdate:height={ height => setExpandedHeight(index, height) }
onUpdate:height={ height => setExpandedHeight(index, 0, height) }
>
{ ({ itemRef }) => (
<tr class="v-data-table__tr--expanded" ref={ itemRef }>
Expand Down
Loading
Loading