mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-02-03 05:37:56 +01:00
feat(Table): expand row (#1036)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
49
docs/components/content/examples/TableExampleExpandable.vue
Normal file
49
docs/components/content/examples/TableExampleExpandable.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<script setup>
|
||||||
|
const people = [{
|
||||||
|
id: 1,
|
||||||
|
name: 'Lindsay Walton',
|
||||||
|
title: 'Front-end Developer',
|
||||||
|
email: 'lindsay.walton@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 2,
|
||||||
|
name: 'Courtney Henry',
|
||||||
|
title: 'Designer',
|
||||||
|
email: 'courtney.henry@example.com',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 3,
|
||||||
|
name: 'Tom Cook',
|
||||||
|
title: 'Director of Product',
|
||||||
|
email: 'tom.cook@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}, {
|
||||||
|
id: 4,
|
||||||
|
name: 'Whitney Francis',
|
||||||
|
title: 'Copywriter',
|
||||||
|
email: 'whitney.francis@example.com',
|
||||||
|
role: 'Admin'
|
||||||
|
}, {
|
||||||
|
id: 5,
|
||||||
|
name: 'Leonard Krasner',
|
||||||
|
title: 'Senior Designer',
|
||||||
|
email: 'leonard.krasner@example.com',
|
||||||
|
role: 'Owner'
|
||||||
|
}, {
|
||||||
|
id: 6,
|
||||||
|
name: 'Floyd Miles',
|
||||||
|
title: 'Principal Designer',
|
||||||
|
email: 'floyd.miles@example.com',
|
||||||
|
role: 'Member'
|
||||||
|
}]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTable :rows="people">
|
||||||
|
<template #expand="{ row }">
|
||||||
|
<div class="p-4">
|
||||||
|
<pre>{{ row }}</pre>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
</template>
|
||||||
@@ -301,6 +301,19 @@ componentProps:
|
|||||||
---
|
---
|
||||||
::
|
::
|
||||||
|
|
||||||
|
### Expandable :u-badge{label="New" class="align-middle ml-2 !rounded-full" variant="subtle"}
|
||||||
|
|
||||||
|
You can use the `expand` slot to display extra information about a row. You will have access to the `row` property in the slot scope.
|
||||||
|
|
||||||
|
::component-example{class="grid"}
|
||||||
|
---
|
||||||
|
padding: false
|
||||||
|
component: 'table-example-expandable'
|
||||||
|
componentProps:
|
||||||
|
class: 'flex-1'
|
||||||
|
---
|
||||||
|
::
|
||||||
|
|
||||||
### Loading
|
### Loading
|
||||||
|
|
||||||
Use the `loading` prop to indicate that data is currently loading with an indeterminate [Progress](/components/progress#indeterminate) bar.
|
Use the `loading` prop to indicate that data is currently loading with an indeterminate [Progress](/components/progress#indeterminate) bar.
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
<UCheckbox :model-value="indeterminate || selected.length === rows.length" :indeterminate="indeterminate" v-bind="ui.default.checkbox" aria-label="Select all" @change="onChange" />
|
<UCheckbox :model-value="indeterminate || selected.length === rows.length" :indeterminate="indeterminate" v-bind="ui.default.checkbox" aria-label="Select all" @change="onChange" />
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
|
<th v-if="$slots.expand" scope="col" :class="ui.tr.base">
|
||||||
|
<span class="sr-only">Expand</span>
|
||||||
|
</th>
|
||||||
|
|
||||||
<th
|
<th
|
||||||
v-for="(column, index) in columns"
|
v-for="(column, index) in columns"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -66,17 +70,39 @@
|
|||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<tr v-for="(row, index) in rows" :key="index" :class="[ui.tr.base, isSelected(row) && ui.tr.selected, $attrs.onSelect && ui.tr.active, row?.class]" @click="() => onSelect(row)">
|
<template v-for="(row, index) in rows" :key="index">
|
||||||
<td v-if="modelValue" :class="ui.checkbox.padding">
|
<tr :class="[ui.tr.base, isSelected(row) && ui.tr.selected, $attrs.onSelect && ui.tr.active, row?.class]" @click="() => onSelect(row)">
|
||||||
<UCheckbox v-model="selected" :value="row" v-bind="ui.default.checkbox" aria-label="Select row" @click.stop />
|
<td v-if="modelValue" :class="ui.checkbox.padding">
|
||||||
</td>
|
<UCheckbox v-model="selected" :value="row" v-bind="ui.default.checkbox" aria-label="Select row" @click.stop />
|
||||||
|
</td>
|
||||||
|
|
||||||
<td v-for="(column, subIndex) in columns" :key="subIndex" :class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size, column?.rowClass, row[column.key]?.class]">
|
<td
|
||||||
<slot :name="`${column.key}-data`" :column="column" :row="row" :index="index" :get-row-data="(defaultValue) => getRowData(row, column.key, defaultValue)">
|
v-if="$slots.expand"
|
||||||
{{ getRowData(row, column.key) }}
|
:class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size]"
|
||||||
</slot>
|
>
|
||||||
</td>
|
<UButton
|
||||||
</tr>
|
v-bind="{ ...(ui.default.expandButton || {}), ...expandButton }"
|
||||||
|
:ui="{ icon: { base: [ui.expand.icon, openedRows.includes(index) && 'rotate-180'] } }"
|
||||||
|
@click="toggleOpened(index)"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td v-for="(column, subIndex) in columns" :key="subIndex" :class="[ui.td.base, ui.td.padding, ui.td.color, ui.td.font, ui.td.size, column?.rowClass, row[column.key]?.class]">
|
||||||
|
<slot :name="`${column.key}-data`" :column="column" :row="row" :index="index" :get-row-data="(defaultValue) => getRowData(row, column.key, defaultValue)">
|
||||||
|
{{ getRowData(row, column.key) }}
|
||||||
|
</slot>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="openedRows.includes(index)">
|
||||||
|
<td colspan="100%">
|
||||||
|
<slot
|
||||||
|
name="expand"
|
||||||
|
:row="row"
|
||||||
|
:index="index"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -84,7 +110,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, toRaw, toRef } from 'vue'
|
import { ref, computed, defineComponent, toRaw, toRef } from 'vue'
|
||||||
import type { PropType, AriaAttributes } from 'vue'
|
import type { PropType, AriaAttributes } from 'vue'
|
||||||
import { upperFirst } from 'scule'
|
import { upperFirst } from 'scule'
|
||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
@@ -174,6 +200,10 @@ export default defineComponent({
|
|||||||
type: String,
|
type: String,
|
||||||
default: () => config.default.sortDescIcon
|
default: () => config.default.sortDescIcon
|
||||||
},
|
},
|
||||||
|
expandButton: {
|
||||||
|
type: Object as PropType<Button>,
|
||||||
|
default: () => config.default.expandButton as Button
|
||||||
|
},
|
||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
@@ -211,6 +241,8 @@ export default defineComponent({
|
|||||||
|
|
||||||
const sort = useVModel(props, 'sort', emit, { passive: true, defaultValue: defu({}, props.sort, { column: null, direction: 'asc' }) })
|
const sort = useVModel(props, 'sort', emit, { passive: true, defaultValue: defu({}, props.sort, { column: null, direction: 'asc' }) })
|
||||||
|
|
||||||
|
const openedRows = ref([])
|
||||||
|
|
||||||
const savedSort = { column: sort.value.column, direction: null }
|
const savedSort = { column: sort.value.column, direction: null }
|
||||||
|
|
||||||
const rows = computed(() => {
|
const rows = computed(() => {
|
||||||
@@ -314,6 +346,14 @@ export default defineComponent({
|
|||||||
return get(row, rowKey, defaultValue)
|
return get(row, rowKey, defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleOpened (index: number) {
|
||||||
|
if (openedRows.value.includes(index)) {
|
||||||
|
openedRows.value = openedRows.value.filter((i) => i !== index)
|
||||||
|
} else {
|
||||||
|
openedRows.value.push(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getAriaSort (column: Column): AriaAttributes['aria-sort'] {
|
function getAriaSort (column: Column): AriaAttributes['aria-sort'] {
|
||||||
if (!column.sortable) {
|
if (!column.sortable) {
|
||||||
return undefined
|
return undefined
|
||||||
@@ -350,11 +390,13 @@ export default defineComponent({
|
|||||||
emptyState,
|
emptyState,
|
||||||
// eslint-disable-next-line vue/no-dupe-keys
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
loadingState,
|
loadingState,
|
||||||
|
openedRows,
|
||||||
isSelected,
|
isSelected,
|
||||||
onSort,
|
onSort,
|
||||||
onSelect,
|
onSelect,
|
||||||
onChange,
|
onChange,
|
||||||
getRowData,
|
getRowData,
|
||||||
|
toggleOpened,
|
||||||
getAriaSort
|
getAriaSort
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ export default {
|
|||||||
label: 'text-sm text-center text-gray-900 dark:text-white',
|
label: 'text-sm text-center text-gray-900 dark:text-white',
|
||||||
icon: 'w-6 h-6 mx-auto text-gray-400 dark:text-gray-500 mb-4'
|
icon: 'w-6 h-6 mx-auto text-gray-400 dark:text-gray-500 mb-4'
|
||||||
},
|
},
|
||||||
|
expand: {
|
||||||
|
icon: 'transform transition-transform duration-200'
|
||||||
|
},
|
||||||
progress: {
|
progress: {
|
||||||
wrapper: 'absolute inset-x-0 -bottom-[0.5px] p-0'
|
wrapper: 'absolute inset-x-0 -bottom-[0.5px] p-0'
|
||||||
},
|
},
|
||||||
@@ -51,6 +54,13 @@ export default {
|
|||||||
variant: 'ghost' as const,
|
variant: 'ghost' as const,
|
||||||
class: '-m-1.5'
|
class: '-m-1.5'
|
||||||
},
|
},
|
||||||
|
expandButton: {
|
||||||
|
icon: 'i-heroicons-chevron-down',
|
||||||
|
color: 'gray' as const,
|
||||||
|
variant: 'ghost' as const,
|
||||||
|
size: 'xs' as const,
|
||||||
|
class: '-my-1.5 align-middle'
|
||||||
|
},
|
||||||
checkbox: {
|
checkbox: {
|
||||||
color: 'primary' as const
|
color: 'primary' as const
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user