diff --git a/packages/components/table/src/table.ts b/packages/components/table/src/table.ts
index 12274792..e773f799 100644
--- a/packages/components/table/src/table.ts
+++ b/packages/components/table/src/table.ts
@@ -2,12 +2,34 @@ import { buildProps } from '@puik/utils'
import type { ExtractPropTypes, PropType } from 'vue'
import type Table from './table.vue'
+export enum PuikTableSortOrder {
+ Asc = 'ASC',
+ Desc = 'DESC',
+}
+
+export enum PuikTableSortIcon {
+ Default = 'unfold_more',
+ Asc = 'expand_more',
+ Desc = 'expand_less',
+}
+
+export enum PuikTableScrollBarPosistion {
+ Left = 'left',
+ Right = 'right',
+ IsScrolling = 'isScrolling',
+}
+
+export type sortOption = {
+ sortBy?: string
+ sortOrder?: PuikTableSortOrder
+}
export interface PuikTableHeader {
value: string
text?: string
size?: 'sm' | 'md' | 'lg'
align?: 'left' | 'center' | 'right'
width?: string
+ sortable?: boolean
}
export const tableProps = buildProps({
@@ -35,6 +57,11 @@ export const tableProps = buildProps({
required: false,
default: () => [],
},
+ sortFromServer: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
fullWidth: {
type: Boolean,
required: false,
diff --git a/packages/components/table/src/table.vue b/packages/components/table/src/table.vue
index 396bf713..6f1ba93d 100644
--- a/packages/components/table/src/table.vue
+++ b/packages/components/table/src/table.vue
@@ -11,8 +11,9 @@
{
'puik-table__head__row__item--sticky-scroll':
stickyFirstCol &&
- (ScrollBarPosition === 'isScrolling' ||
- ScrollBarPosition === 'right'),
+ (ScrollBarPosition ===
+ PuikTableScrollBarPosistion.IsScrolling ||
+ ScrollBarPosition === PuikTableScrollBarPosistion.Right),
},
{ 'puik-table__head__row__item--selection': selectable },
{ 'puik-table__head__row__item--expandable': expandable },
@@ -40,34 +41,55 @@
[`puik-table__head__row__item--${header.size}`]:
header?.size && !header?.width,
},
+ {
+ 'puik-table__head__row__item--sortable': header?.sortable,
+ },
{ 'puik-table__head__row__item--sticky': isSticky(index) },
{
'puik-table__head__row__item--sticky-scroll':
- isSticky(index) && ScrollBarPosition === 'isScrolling',
+ isSticky(index) &&
+ ScrollBarPosition === PuikTableScrollBarPosistion.IsScrolling,
},
{
'puik-table__head__row__item--sticky-left':
- isSticky(index) && ScrollBarPosition === 'left',
+ isSticky(index) &&
+ ScrollBarPosition === PuikTableScrollBarPosistion.Left,
},
{
'puik-table__head__row__item--sticky-right':
- isSticky(index) && ScrollBarPosition === 'right',
+ isSticky(index) &&
+ ScrollBarPosition === PuikTableScrollBarPosistion.Right,
},
]"
:style="{ minWidth: header.width, width: header.width }"
>
-
- {{ header.text }}
-
+
+
+
+
+ {{ header?.text || header?.value }}
+
+
+
+
+
-
+
@@ -121,25 +147,33 @@
{ 'puik-table__body__row__item--sticky': isSticky(colIndex) },
{
'puik-table__body__row__item--sticky-scroll':
- isSticky(colIndex) && ScrollBarPosition == 'isScrolling',
+ isSticky(colIndex) &&
+ ScrollBarPosition ==
+ PuikTableScrollBarPosistion.IsScrolling,
},
{
'puik-table__body__row__item--sticky-left':
- isSticky(colIndex) && ScrollBarPosition == 'left',
+ isSticky(colIndex) &&
+ ScrollBarPosition == PuikTableScrollBarPosistion.Left,
},
{
'puik-table__body__row__item--sticky-right':
- isSticky(colIndex) && ScrollBarPosition == 'right',
+ isSticky(colIndex) &&
+ ScrollBarPosition == PuikTableScrollBarPosistion.Right,
},
]"
>
-
- {{ item[header.value] }}
-
+
+
+
+ {{ item[header.value] }}
+
+
+
|
|
+
-
+
{{ item }}
|
+ |
@@ -170,37 +250,101 @@
import { computed, ref, watch } from 'vue'
import { useLocale } from '@puik/hooks'
import PuikCheckbox from '../../checkbox/src/checkbox.vue'
+import PuikButton from '../../button/src/button.vue'
import PuikIcon from '../../icon/src/icon.vue'
-import { tableProps } from './table'
+import {
+ tableProps,
+ PuikTableSortOrder,
+ PuikTableSortIcon,
+ PuikTableScrollBarPosistion,
+} from './table'
+import type { sortOption } from './table'
defineOptions({
name: 'PuikTable',
})
const props = defineProps(tableProps)
+
const emit = defineEmits<{
(e: 'select', index: number): void
(e: 'select:all'): void
(e: 'update:selection', value: number[]): void
+ (e: 'sortColumn', column: sortOption): void
}>()
const { t } = useLocale()
const checked = ref(props.selection)
const expandedRows = ref([])
const ScrollBarPosition = ref('left')
-let lastScrollLeft = 0
+const lastScrollLeft = ref(0)
+const sortOrder = ref([])
+const sortIcon = ref({})
+const data = ref([...props.items])
+const currentSortCol = ref('')
+
+const resetSortIcons = () => {
+ for (const col in sortIcon.value) {
+ sortIcon.value[col] = PuikTableSortIcon.Default
+ }
+}
+const setSortOrderAndIcon = (headerCol: string) => {
+ if (sortOrder.value[headerCol]) {
+ sortOrder.value[headerCol] =
+ sortOrder.value[headerCol] === PuikTableSortOrder.Asc &&
+ currentSortCol.value === headerCol
+ ? PuikTableSortOrder.Desc
+ : PuikTableSortOrder.Asc
+ sortIcon.value[headerCol] =
+ sortOrder.value[headerCol] === PuikTableSortOrder.Asc
+ ? PuikTableSortIcon.Asc
+ : PuikTableSortIcon.Desc
+ } else {
+ sortOrder.value[headerCol] = PuikTableSortOrder.Asc
+ sortIcon.value[headerCol] = PuikTableSortIcon.Asc
+ }
+}
+const sortDataLocally = (headerCol: string) => {
+ const order = sortOrder.value[headerCol] === PuikTableSortOrder.Asc ? 1 : -1
+ data.value.sort((a, b) => {
+ const aValue =
+ typeof a[headerCol] === 'string'
+ ? a[headerCol].toLowerCase()
+ : a[headerCol]
+ const bValue =
+ typeof b[headerCol] === 'string'
+ ? b[headerCol].toLowerCase()
+ : b[headerCol]
+ return order * (aValue < bValue ? -1 : aValue > bValue ? 1 : 0)
+ })
+}
+const sortTable = (headerCol: string) => {
+ if (!props.sortFromServer) {
+ sortDataLocally(headerCol)
+ }
+ resetSortIcons()
+ setSortOrderAndIcon(headerCol)
+
+ const options = {
+ sortBy: headerCol,
+ sortOrder: sortOrder.value[headerCol],
+ } as sortOption
+ emit('sortColumn', options)
+ currentSortCol.value = headerCol
+ return data.value
+}
const getScrollBarPosition = async (event: Event) => {
const target = event.target as HTMLElement
if (target.scrollLeft === 0) {
- ScrollBarPosition.value = 'left'
+ ScrollBarPosition.value = PuikTableScrollBarPosistion.Left
} else if (
- Math.abs(target.scrollLeft + target.offsetWidth - target.scrollWidth) < 10
+ Math.abs(target.scrollLeft + target.offsetWidth - target.scrollWidth) < 20
) {
- ScrollBarPosition.value = 'right'
+ ScrollBarPosition.value = PuikTableScrollBarPosistion.Right
} else {
- ScrollBarPosition.value = 'isScrolling'
+ ScrollBarPosition.value = PuikTableScrollBarPosistion.IsScrolling
}
- lastScrollLeft = target.scrollLeft
+ lastScrollLeft.value = target.scrollLeft
}
const isSticky = (
diff --git a/packages/components/table/stories/table.stories.ts b/packages/components/table/stories/table.stories.ts
index 9052f774..8dbf3bab 100644
--- a/packages/components/table/stories/table.stories.ts
+++ b/packages/components/table/stories/table.stories.ts
@@ -1,5 +1,6 @@
import { ref } from 'vue'
import PuikButton from '../../button/src/button.vue'
+import PuikIcon from '../../icon/src/icon.vue'
import PuikTable from './../src/table.vue'
import type { PuikTableHeader } from '../src/table'
import type { Meta, StoryFn, StoryObj, Args } from '@storybook/vue3'
@@ -51,6 +52,7 @@ export default {
size: 'sm' | 'md' | 'lg' | undefined
width: string | undefined
align: 'left' | 'center' | 'right' | undefined
+ sortable: boolean | undefined
}
`,
},
@@ -142,7 +144,7 @@ export default {
},
},
},
- '`expanded-row-${rowIndex}`': {
+ 'expanded-row': {
control: 'none',
description: 'slot for expanded row content',
table: {
@@ -179,6 +181,23 @@ export default {
},
},
},
+ sortColumn: {
+ control: 'none',
+ description: 'Event emitted when sorting a column',
+ table: {
+ type: {
+ summary: 'sortOption',
+ detail: `
+import type { sortOption } from '@prestashopcorp/puik/es/components/table/src/table'
+
+type sortOption = {
+ sortBy?: string
+ sortOrder?: PuikTableSortOrder
+}
+`,
+ },
+ },
+ },
},
args: {
selectable: false,
@@ -239,6 +258,60 @@ const Template: StoryFn = (args: Args) => ({
`,
})
+const SortableTemplate: StoryFn = (args: Args) => ({
+ components: {
+ PuikTable,
+ PuikButton,
+ PuikIcon,
+ },
+ setup() {
+ const selection = ref([])
+ const items = generateData()
+ const headers: PuikTableHeader[] = [
+ {
+ text: 'Nom',
+ value: 'lastname',
+ size: 'md',
+ sortable: true,
+ },
+ {
+ text: 'Prénom',
+ value: 'firstname',
+ size: 'md',
+ sortable: true,
+ },
+ {
+ text: 'Age',
+ value: 'age',
+ size: 'sm',
+ align: 'center',
+ sortable: true,
+ },
+ {
+ text: 'Email',
+ value: 'email',
+ align: 'right',
+ },
+ {
+ value: 'actions',
+ size: 'sm',
+ },
+ ]
+ return { args, headers, items, selection }
+ },
+ template: `
+
+
+
+
+
+ `,
+})
+
export const Default: StoryObj = {
render: Template,
args: {},
@@ -667,6 +740,126 @@ export const Expandable: StoryObj = {
},
}
+export const Sortable: StoryObj = {
+ render: SortableTemplate,
+ args: {},
+ parameters: {
+ docs: {
+ source: {
+ code: `
+
+ const headers: PuikTableHeader[] = [
+ {
+ text: 'Nom',
+ value: 'lastname',
+ size: 'md',
+ sortable: true,
+ },
+ {
+ text: 'Prénom',
+ value: 'firstname',
+ size: 'md',
+ sortable: true,
+ },
+ {
+ text: 'Age',
+ value: 'age',
+ size: 'sm',
+ align: 'center',
+ sortable: true,
+ },
+ {
+ text: 'Email',
+ value: 'email',
+ align: 'right',
+ },
+ {
+ value: 'actions',
+ size: 'sm',
+ },
+ ]
+
+
+
+
+
+
+
+
+
+
+
+ |
+ Nom |
+ Prénom |
+ Age |
+ Email |
+ |
+
+
+
+
+
+
+ |
+ lastname0 |
+ firstname0 |
+ 40 |
+ lastname0.firstname0@email.com |
+
+
+ |
+
+
+
+
+
+ |
+ lastname1 |
+ firstname1 |
+ 40 |
+ lastname1.firstname1@email.com |
+
+
+ |
+
+
+
+
+ |
+ lastname2 |
+ firstname2 |
+ 40 |
+ lastname2.firstname2@email.com |
+
+
+ |
+
+
+
+ `,
+ language: 'html',
+ },
+ },
+ },
+}
+
export const FullWidth: StoryObj = {
render: Template,
args: {
diff --git a/packages/components/table/test/table.spec.ts b/packages/components/table/test/table.spec.ts
index 1c2a7ac9..7dbff48b 100644
--- a/packages/components/table/test/table.spec.ts
+++ b/packages/components/table/test/table.spec.ts
@@ -3,8 +3,12 @@ import { describe, it, expect } from 'vitest'
import { faker } from '@faker-js/faker'
import { locales } from '@puik/locale'
import PuikTable from '../src/table.vue'
+import {
+ PuikTableSortOrder,
+ type PuikTableHeader,
+ type sortOption,
+} from '../src/table'
import type { MountingOptions, VueWrapper } from '@vue/test-utils'
-import type { PuikTableHeader } from '../src/table'
const defaultItems = Array(5)
.fill(null)
@@ -85,7 +89,7 @@ describe('Table tests', () => {
it('should display headers without text', () => {
const headers: PuikTableHeader[] = [{ value: 'firstname' }]
factory({ headers })
- expect(getHeaders()[0].text()).toBe('')
+ expect(getHeaders()[0].text()).toBeUndefined
})
it('should display 5 items', () => {
const headers: PuikTableHeader[] = [{ value: 'firstname' }]
@@ -314,4 +318,19 @@ describe('Table tests', () => {
'puik-table__head__row__item--expandable'
)
})
+
+ it('should emit sortColum event', async () => {
+ const headers: PuikTableHeader[] = [{ value: 'firstname', sortable: true }]
+ factory({ headers })
+ const header = getHeaders()[0]
+ const SortButton = header.find('.puik-button')
+ const payload: sortOption = {
+ sortBy: 'firstname',
+ sortOrder: PuikTableSortOrder.Asc,
+ }
+ expect(SortButton.classes()).toContain('puik-button')
+ await SortButton.trigger('click')
+ expect(wrapper.emitted('sortColumn')).toBeTruthy()
+ expect(wrapper.emitted('sortColumn')?.[0]?.[0]).toStrictEqual(payload)
+ })
})
diff --git a/packages/theme/src/table.scss b/packages/theme/src/table.scss
index e22be884..a2aeeec3 100644
--- a/packages/theme/src/table.scss
+++ b/packages/theme/src/table.scss
@@ -30,15 +30,42 @@
}
}
- @mixin puik-table-text-align() {
+ @mixin puik-table-align() {
+ > .puik-table__head__row__item__container,
+ .puik-table__body__row__item__container {
+ @apply flex;
+ }
&--left {
- @apply text-left;
+ > .puik-table__head__row__item__container,
+ .puik-table__body__row__item__container {
+ @apply justify-start;
+ }
+ > .puik-table__head__row__item__content,
+ .puik-table__body__row__item__content {
+ @apply text-left;
+ }
}
&--center {
- @apply text-center;
+ > .puik-table__head__row__item__container,
+ .puik-table__body__row__item__container {
+ @apply flex;
+ @apply justify-center;
+ }
+ > .puik-table__head__row__item__content,
+ .puik-table__body__row__item__content {
+ @apply text-center;
+ }
}
&--right {
- @apply text-right;
+ > .puik-table__head__row__item__container,
+ .puik-table__body__row__item__container {
+ @apply flex;
+ @apply justify-end;
+ }
+ > .puik-table__head__row__item__content,
+ .puik-table__body__row__item__content {
+ @apply text-right;
+ }
}
}
@@ -51,7 +78,7 @@
@extend .puik-body-small;
@apply font-normal uppercase text-primary-600;
@include puik-table-item();
- @include puik-table-text-align();
+ @include puik-table-align();
}
}
}
@@ -71,7 +98,7 @@
@extend .puik-body-default;
@apply text-primary-800;
@include puik-table-item();
- @include puik-table-text-align();
+ @include puik-table-align();
}
}
}
@@ -79,7 +106,7 @@
tr {
.puik-table__head__row__item--sticky,
.puik-table__body__row__item--sticky {
- @apply sticky bg-white;
+ @apply sticky bg-white z-10;
}
.puik-table__head__row__item--sticky-scroll,
@@ -129,6 +156,7 @@
.puik-table__body__row__item--expanded {
@apply p-0;
}
+
.puik-table__body__row__item__container {
@apply flex items-center space-x-3;
.puik-icon {
@@ -138,4 +166,12 @@
@apply -rotate-180 transform transition-transform duration-100 ease-in-out;
}
}
+
+ .puik-table__head__row__item__content {
+ @apply flex items-center space-x-1;
+ }
+
+ .puik-table__head__row__item--right.puik-table__head__row__item--sortable {
+ @apply pr-2;
+ }
}