Skip to content

Commit 02617ab

Browse files
committed
feat: table sticky columns
1 parent f8f395d commit 02617ab

File tree

8 files changed

+245
-17
lines changed

8 files changed

+245
-17
lines changed

.changeset/olive-masks-compete.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@casual-ui/svelte': minor
3+
---
4+
5+
feat: table sticky columns

packages/docs/src/routes/features/components/data-presentation/table/+page.md

+71-5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,55 @@ componentName:
3232
<CTable data={fruits} {columns} />
3333
```
3434

35+
## Sticky columns
36+
37+
```svelte live
38+
<script>
39+
import { CTable } from '@casual-ui/svelte'
40+
import CustomColumn from '/src/routes/features/components/data-presentation/table/CustomColumn.svelte'
41+
42+
const columns = [
43+
{
44+
title: 'Icon',
45+
field: 'icon',
46+
cell: CustomColumn,
47+
sticky: 'left',
48+
width: '100px'
49+
},
50+
{
51+
title: 'Name',
52+
field: 'name',
53+
width: '100px',
54+
sticky: 'left',
55+
},
56+
{
57+
title: 'Color',
58+
field: 'color',
59+
width: '300px',
60+
},
61+
{
62+
title: 'Shape',
63+
field: 'shape',
64+
width: '300px'
65+
},
66+
{
67+
title: 'Description',
68+
field: 'description',
69+
width: '200px',
70+
sticky: 'right',
71+
},
72+
]
73+
74+
const fruits = [
75+
{ name: 'Apple', color: 'red', shape: 'circle', description: 'Apple is red' },
76+
{ name: 'Banana', color: 'yellow', shape: 'long', description: 'Banana is yellow' },
77+
{ name: 'Grapes', color: 'purple', shape: 'many circles', description: 'Grapes is purple' },
78+
]
79+
</script>
80+
81+
<CTable data={fruits} {columns} />
82+
```
83+
3584
## Custom column
3685

3786
A custom column component can hold these props:
@@ -40,8 +89,14 @@ A custom column component can hold these props:
4089
* `row`: The row data
4190
* `rowIdex`: The row index number (from 0).
4291

92+
<Tabs activeName="CustomColumn.svelte">
93+
<TabPanel name="CustomColumn.svelte">
94+
4395
@code(./CustomColumn.svelte)
4496

97+
</TabPanel>
98+
<TabPanel name="Live">
99+
45100
```svelte live
46101
<script lang="ts">
47102
import { CTable } from '@casual-ui/svelte'
@@ -63,24 +118,31 @@ A custom column component can hold these props:
63118
const fruits = [
64119
{ name: 'Apple', description: 'Apple is red' },
65120
{ name: 'Banana', description: 'Banana is yellow' },
66-
{ name: 'Grapes', description: 'Grapes is purple' },
121+
{ name: 'Grapes', description: 'Grapes are purple' },
67122
]
68123
</script>
69124
70125
<CTable data={fruits} {columns} />
71126
```
72127

128+
</TabPanel>
129+
</Tabs>
130+
131+
132+
73133
## Custom title
74134

75135
A custom title component can hold these props:
76136

77137
* `col`: The column config
78138

79-
The CustomTitle.svelte content is shown below
80-
139+
<Tabs activeName="CustomTitle.svelte">
140+
<TabPanel name="CustomTitle.svelte">
141+
81142
@code(./CustomTitle.svelte)
82143

83-
Toggle Dark mode to see title color change
144+
</TabPanel>
145+
<TabPanel name="Live">
84146

85147
```svelte live
86148
<script lang="ts">
@@ -107,4 +169,8 @@ Toggle Dark mode to see title color change
107169
</script>
108170
109171
<CTable data={fruits} {columns} />
110-
```
172+
```
173+
174+
</TabPanel>
175+
</Tabs>
176+

packages/docs/src/routes/features/components/data-presentation/table/CustomColumn.svelte

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
export let row: {
44
name: string
55
}
6+
7+
$$restProps
68
</script>
79

810
<div class="text-10">

packages/ui/src/actions/createClickOutsideAction.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Action } from 'svelte/types/runtime/action'
1+
import type { Action } from 'svelte/action'
22

33
export default ({ cbInside, cbOutside }: {
44
cbInside: () => void

packages/ui/src/components/table/CTable.svelte

+89-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
<script context="module">
2+
export const DEFAULT_WIDTH = '100px'
3+
</script>
4+
15
<script>
6+
import { onMount } from 'svelte'
7+
28
import bem from '../../utils/bem'
39
import CTd from './CTd.svelte'
410
import CTh from './CTh.svelte'
511
import CTr from './CTr.svelte'
12+
import { EStickyPosition } from './types'
613
714
/**
815
* Determine whether the table is striped or not.
@@ -12,7 +19,7 @@
1219
1320
/**
1421
* The columns config array.
15-
* @type {Array<{ field: string; width?: string; title?: string | import('svelte').ComponentType; cell?: import('svelte').ComponentType }>}
22+
* @type {Array<import('./types').Column>}
1623
*/
1724
export let columns = []
1825
@@ -21,23 +28,90 @@
2128
* @type {Array<any>}
2229
*/
2330
export let data = []
31+
32+
const stickyLeftPositions = []
33+
const stickyRightPositions = []
34+
let maxStickyLeftIdx = -1
35+
let minStickyRightIdx = -1
36+
37+
function compute() {
38+
let previousLeftIndex = -1
39+
let previousRightIndex = -1
40+
columns.forEach((c, i) => {
41+
if (c.sticky === EStickyPosition.Left) {
42+
if (previousLeftIndex === -1) {
43+
stickyLeftPositions[i] = 0
44+
} else {
45+
stickyLeftPositions[i] =
46+
columns[previousLeftIndex].width || DEFAULT_WIDTH
47+
}
48+
49+
previousLeftIndex = i
50+
maxStickyLeftIdx = i
51+
} else if (c.sticky === EStickyPosition.Right) {
52+
if (previousRightIndex === -1) {
53+
stickyRightPositions[i] = 0
54+
minStickyRightIdx = i
55+
} else {
56+
stickyRightPositions[i] =
57+
columns[previousRightIndex].width || DEFAULT_WIDTH
58+
}
59+
previousRightIndex = i
60+
} else {
61+
stickyLeftPositions[i] = 0
62+
stickyRightPositions[i] = 0
63+
}
64+
})
65+
}
66+
67+
compute()
68+
69+
/**
70+
* @type {HTMLDivElement}
71+
*/
72+
let tableWrapper
73+
74+
let scrollLeft = 0
75+
let clientWidth = 0
76+
let scrollWidth = 0
77+
78+
$: showMaxLeftStickyShadow = scrollLeft > 0
79+
$: showMaxRightStickyShadow = scrollLeft < scrollWidth - clientWidth
80+
81+
function handleScroll(e) {
82+
scrollLeft = e.target.scrollLeft
83+
}
84+
85+
onMount(() => {
86+
scrollWidth = tableWrapper.scrollWidth
87+
})
2488
</script>
2589

2690
<div
2791
class={bem('table', {
2892
striped,
2993
})}
94+
bind:clientWidth
95+
on:scroll={handleScroll}
96+
bind:this={tableWrapper}
3097
>
3198
<table class="c-table--table">
3299
<colgroup>
33-
{#each columns as _col}
34-
<col />
100+
{#each columns as col}
101+
<col style={{ width: col.width ?? DEFAULT_WIDTH }} />
35102
{/each}
36103
</colgroup>
37104
<thead>
38105
<CTr>
39-
{#each columns as col}
40-
<CTh width={col.width}>
106+
{#each columns as col, i}
107+
<CTh
108+
width={col.width}
109+
sticky={col.sticky}
110+
stickyLeft={stickyLeftPositions[i]}
111+
stickyRight={stickyRightPositions[i]}
112+
stickyLeftMax={maxStickyLeftIdx === i && showMaxLeftStickyShadow}
113+
stickyRightMin={minStickyRightIdx === i && showMaxRightStickyShadow}
114+
>
41115
{#if typeof col.title === 'string'}
42116
{col.title}
43117
{:else}
@@ -50,8 +124,16 @@
50124
<tbody>
51125
{#each data as row, i}
52126
<CTr>
53-
{#each columns as col}
54-
<CTd width={col.width}>
127+
{#each columns as col, j}
128+
<CTd
129+
width={col.width}
130+
sticky={col.sticky}
131+
stickyLeft={stickyLeftPositions[j]}
132+
stickyRight={stickyRightPositions[j]}
133+
stickyLeftMax={maxStickyLeftIdx === j && showMaxLeftStickyShadow}
134+
stickyRightMin={minStickyRightIdx === j &&
135+
showMaxRightStickyShadow}
136+
>
55137
{#if col.cell}
56138
<svelte:component this={col.cell} {col} {row} rowIndex={i} />
57139
{:else}
+24-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
11
<script>
2+
import clsx from '../../utils/clsx'
3+
import { DEFAULT_WIDTH } from './CTable.svelte'
4+
25
/**
36
* The width of cell.
47
* @type {string}
58
*/
6-
export let width = 'auto'
9+
export let width = DEFAULT_WIDTH
10+
11+
/**
12+
* @type {import('./types').EStickyPosition}
13+
*/
14+
export let sticky
15+
16+
export let stickyLeft = '0'
17+
export let stickyRight = '0'
18+
export let stickyLeftMax = false
19+
export let stickyRightMin = false
720
</script>
821

9-
<td class="c-table--td" style={`width: ${width};`}>
22+
<td
23+
class={clsx('c-table--td', {
24+
[`c-table--td--sticky-${sticky}`]: sticky,
25+
})}
26+
class:c-table--td--sticky-max-left={stickyLeftMax}
27+
class:c-table--td--sticky-min-right={stickyRightMin}
28+
style={`width: ${width};`}
29+
style:--c-table-sticky-left={stickyLeft}
30+
style:--c-table-sticky-right={stickyRight}
31+
>
1032
<slot />
1133
</td>
+24-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
11
<script>
2+
import clsx from '../../utils/clsx'
3+
import { DEFAULT_WIDTH } from './CTable.svelte'
4+
25
/**
36
* The width of th.
47
* @type {string}
58
*/
6-
export let width = 'auto'
9+
export let width = DEFAULT_WIDTH
10+
11+
/**
12+
* @type {import('./types').EStickyPosition}
13+
*/
14+
export let sticky
15+
16+
export let stickyLeft = '0'
17+
export let stickyRight = '0'
18+
export let stickyLeftMax = false
19+
export let stickyRightMin = false
720
</script>
821

9-
<th class="c-table--td" style={`width: ${width};`}>
22+
<th
23+
class={clsx('c-table--th', {
24+
[`c-table--th--sticky-${sticky}`]: sticky,
25+
})}
26+
class:c-table--td--sticky-max-left={stickyLeftMax}
27+
class:c-table--td--sticky-min-right={stickyRightMin}
28+
style={`width: ${width};`}
29+
style:--c-table-sticky-left={stickyLeft}
30+
style:--c-table-sticky-right={stickyRight}
31+
>
1032
<slot />
1133
</th>
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { ComponentType } from 'svelte'
2+
3+
export interface Column {
4+
/**
5+
* The field of row data
6+
*/
7+
field: string
8+
/**
9+
* The column width
10+
*/
11+
width?: string
12+
/**
13+
* The title content. Can be a svelte component
14+
*/
15+
title?: string | ComponentType
16+
/**
17+
* Customize the cell content with svelte component
18+
*/
19+
cell?: ComponentType
20+
/**
21+
* Make the column sticky to left or right
22+
*/
23+
sticky?: EStickyPosition
24+
}
25+
26+
export enum EStickyPosition {
27+
Left = 'left',
28+
Right = 'right',
29+
}

0 commit comments

Comments
 (0)