Skip to content

Commit 4216aef

Browse files
committed
feat(createDataGrid): add column layout with sizing, pinning, resizing, reordering
1 parent f3e70bc commit 4216aef

2 files changed

Lines changed: 625 additions & 0 deletions

File tree

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { createColumnLayout } from './layout'
4+
5+
describe('createColumnLayout', () => {
6+
describe('auto-distribute sizes', () => {
7+
it('gives 4 equal columns 25% each', () => {
8+
const layout = createColumnLayout([
9+
{ key: 'a' },
10+
{ key: 'b' },
11+
{ key: 'c' },
12+
{ key: 'd' },
13+
])
14+
15+
const cols = layout.columns.value
16+
for (const col of cols) {
17+
expect(col.size).toBe(25)
18+
}
19+
})
20+
21+
it('splits remainder evenly among unsized columns', () => {
22+
// 'a' takes 40, remaining 60 split between b and c
23+
const layout = createColumnLayout([
24+
{ key: 'a', size: 40 },
25+
{ key: 'b' },
26+
{ key: 'c' },
27+
])
28+
29+
const cols = layout.columns.value
30+
expect(cols.find(c => c.key === 'a')!.size).toBe(40)
31+
expect(cols.find(c => c.key === 'b')!.size).toBe(30)
32+
expect(cols.find(c => c.key === 'c')!.size).toBe(30)
33+
})
34+
35+
it('keeps explicit sizes when all specified', () => {
36+
const layout = createColumnLayout([
37+
{ key: 'a', size: 60 },
38+
{ key: 'b', size: 40 },
39+
])
40+
41+
const cols = layout.columns.value
42+
expect(cols.find(c => c.key === 'a')!.size).toBe(60)
43+
expect(cols.find(c => c.key === 'b')!.size).toBe(40)
44+
})
45+
})
46+
47+
describe('offset computation', () => {
48+
it('computes cumulative offsets within scrollable region', () => {
49+
const layout = createColumnLayout([
50+
{ key: 'a', size: 30 },
51+
{ key: 'b', size: 40 },
52+
{ key: 'c', size: 30 },
53+
])
54+
55+
const cols = layout.columns.value
56+
expect(cols.find(c => c.key === 'a')!.offset).toBe(0)
57+
expect(cols.find(c => c.key === 'b')!.offset).toBe(30)
58+
expect(cols.find(c => c.key === 'c')!.offset).toBe(70)
59+
})
60+
})
61+
62+
describe('leaf extraction from nested columns', () => {
63+
it('extracts leaves from nested defs', () => {
64+
const layout = createColumnLayout([
65+
{ key: 'name', size: 30 },
66+
{
67+
key: 'contact',
68+
children: [
69+
{ key: 'email', size: 35 },
70+
{ key: 'phone', size: 35 },
71+
],
72+
},
73+
])
74+
75+
const cols = layout.columns.value
76+
expect(cols).toHaveLength(3)
77+
expect(cols.map(c => c.key)).toEqual(['name', 'email', 'phone'])
78+
})
79+
80+
it('auto-distributes remainder across nested leaves', () => {
81+
const layout = createColumnLayout([
82+
{ key: 'name' },
83+
{
84+
key: 'contact',
85+
children: [
86+
{ key: 'email' },
87+
{ key: 'phone' },
88+
],
89+
},
90+
])
91+
92+
// 3 leaves, each gets 100/3
93+
const cols = layout.columns.value
94+
expect(cols).toHaveLength(3)
95+
const total = cols.reduce((sum, c) => sum + c.size, 0)
96+
expect(total).toBeCloseTo(100)
97+
})
98+
})
99+
100+
describe('pinning', () => {
101+
it('splits columns into left/scrollable/right regions from options', () => {
102+
const layout = createColumnLayout([
103+
{ key: 'a', size: 20, pinned: 'left' },
104+
{ key: 'b', size: 60 },
105+
{ key: 'c', size: 20, pinned: 'right' },
106+
])
107+
108+
const { left, scrollable, right } = layout.pinned.value
109+
expect(left.map(c => c.key)).toEqual(['a'])
110+
expect(scrollable.map(c => c.key)).toEqual(['b'])
111+
expect(right.map(c => c.key)).toEqual(['c'])
112+
})
113+
114+
it('pin mutation moves a column to the specified region', () => {
115+
const layout = createColumnLayout([
116+
{ key: 'a', size: 30 },
117+
{ key: 'b', size: 40 },
118+
{ key: 'c', size: 30 },
119+
])
120+
121+
layout.pin('a', 'left')
122+
123+
const { left, scrollable } = layout.pinned.value
124+
expect(left.map(c => c.key)).toEqual(['a'])
125+
expect(scrollable.map(c => c.key)).toEqual(['b', 'c'])
126+
})
127+
128+
it('unpin moves column back to scrollable', () => {
129+
const layout = createColumnLayout([
130+
{ key: 'a', size: 30, pinned: 'left' },
131+
{ key: 'b', size: 40 },
132+
{ key: 'c', size: 30 },
133+
])
134+
135+
layout.pin('a', false)
136+
137+
const { left, scrollable } = layout.pinned.value
138+
expect(left).toHaveLength(0)
139+
expect(scrollable.map(c => c.key)).toEqual(['a', 'b', 'c'])
140+
})
141+
142+
it('computes offsets independently per region', () => {
143+
const layout = createColumnLayout([
144+
{ key: 'a', size: 20, pinned: 'left' },
145+
{ key: 'b', size: 20, pinned: 'left' },
146+
{ key: 'c', size: 30 },
147+
{ key: 'd', size: 30 },
148+
])
149+
150+
const { left, scrollable } = layout.pinned.value
151+
152+
// Left region offsets start at 0
153+
expect(left.find(c => c.key === 'a')!.offset).toBe(0)
154+
expect(left.find(c => c.key === 'b')!.offset).toBe(20)
155+
156+
// Scrollable region offsets start at 0 independently
157+
expect(scrollable.find(c => c.key === 'c')!.offset).toBe(0)
158+
expect(scrollable.find(c => c.key === 'd')!.offset).toBe(30)
159+
})
160+
})
161+
162+
describe('resize', () => {
163+
it('adjusts target and neighbor by delta', () => {
164+
const layout = createColumnLayout([
165+
{ key: 'a', size: 50 },
166+
{ key: 'b', size: 50 },
167+
])
168+
169+
layout.resize('a', 10)
170+
171+
const cols = layout.columns.value
172+
expect(cols.find(c => c.key === 'a')!.size).toBe(60)
173+
expect(cols.find(c => c.key === 'b')!.size).toBe(40)
174+
})
175+
176+
it('clamps at minSize', () => {
177+
const layout = createColumnLayout([
178+
{ key: 'a', size: 50, minSize: 20 },
179+
{ key: 'b', size: 50, minSize: 20 },
180+
])
181+
182+
// Try to shrink 'a' below its min
183+
layout.resize('a', -40)
184+
185+
const cols = layout.columns.value
186+
expect(cols.find(c => c.key === 'a')!.size).toBe(20)
187+
expect(cols.find(c => c.key === 'b')!.size).toBe(80)
188+
})
189+
190+
it('clamps at maxSize', () => {
191+
const layout = createColumnLayout([
192+
{ key: 'a', size: 50, maxSize: 60 },
193+
{ key: 'b', size: 50, minSize: 20 },
194+
])
195+
196+
layout.resize('a', 30)
197+
198+
const cols = layout.columns.value
199+
expect(cols.find(c => c.key === 'a')!.size).toBe(60)
200+
expect(cols.find(c => c.key === 'b')!.size).toBe(40)
201+
})
202+
203+
it('no-op on last column in its region', () => {
204+
const layout = createColumnLayout([
205+
{ key: 'a', size: 50 },
206+
{ key: 'b', size: 50 },
207+
])
208+
209+
layout.resize('b', 10)
210+
211+
const cols = layout.columns.value
212+
expect(cols.find(c => c.key === 'a')!.size).toBe(50)
213+
expect(cols.find(c => c.key === 'b')!.size).toBe(50)
214+
})
215+
216+
it('resizes within pin region only', () => {
217+
const layout = createColumnLayout([
218+
{ key: 'a', size: 20, pinned: 'left' },
219+
{ key: 'b', size: 20, pinned: 'left' },
220+
{ key: 'c', size: 30 },
221+
{ key: 'd', size: 30 },
222+
])
223+
224+
layout.resize('a', 5)
225+
226+
const cols = layout.columns.value
227+
// a grows, b shrinks (left region)
228+
expect(cols.find(c => c.key === 'a')!.size).toBe(25)
229+
expect(cols.find(c => c.key === 'b')!.size).toBe(15)
230+
// scrollable region unchanged
231+
expect(cols.find(c => c.key === 'c')!.size).toBe(30)
232+
expect(cols.find(c => c.key === 'd')!.size).toBe(30)
233+
})
234+
})
235+
236+
describe('reorder', () => {
237+
it('moves a column from one position to another', () => {
238+
const layout = createColumnLayout([
239+
{ key: 'a' },
240+
{ key: 'b' },
241+
{ key: 'c' },
242+
])
243+
244+
// Move 'a' (index 0) to index 2
245+
layout.reorder(0, 2)
246+
247+
expect(layout.columns.value.map(c => c.key)).toEqual(['b', 'c', 'a'])
248+
})
249+
250+
it('no-op for out-of-bounds from index', () => {
251+
const layout = createColumnLayout([
252+
{ key: 'a' },
253+
{ key: 'b' },
254+
])
255+
256+
layout.reorder(5, 0)
257+
258+
expect(layout.columns.value.map(c => c.key)).toEqual(['a', 'b'])
259+
})
260+
})
261+
262+
describe('reset', () => {
263+
it('restores initial sizes', () => {
264+
const layout = createColumnLayout([
265+
{ key: 'a', size: 60 },
266+
{ key: 'b', size: 40 },
267+
])
268+
269+
layout.resize('a', -20)
270+
layout.reset()
271+
272+
const cols = layout.columns.value
273+
expect(cols.find(c => c.key === 'a')!.size).toBe(60)
274+
expect(cols.find(c => c.key === 'b')!.size).toBe(40)
275+
})
276+
277+
it('restores initial order', () => {
278+
const layout = createColumnLayout([
279+
{ key: 'a' },
280+
{ key: 'b' },
281+
{ key: 'c' },
282+
])
283+
284+
layout.reorder(0, 2)
285+
layout.reset()
286+
287+
expect(layout.columns.value.map(c => c.key)).toEqual(['a', 'b', 'c'])
288+
})
289+
290+
it('restores initial pins', () => {
291+
const layout = createColumnLayout([
292+
{ key: 'a', size: 30, pinned: 'left' },
293+
{ key: 'b', size: 40 },
294+
{ key: 'c', size: 30 },
295+
])
296+
297+
layout.pin('a', false)
298+
layout.reset()
299+
300+
const { left } = layout.pinned.value
301+
expect(left.map(c => c.key)).toEqual(['a'])
302+
})
303+
})
304+
305+
describe('distribute', () => {
306+
it('sets sizes from array and normalizes to 100', () => {
307+
const layout = createColumnLayout([
308+
{ key: 'a' },
309+
{ key: 'b' },
310+
{ key: 'c' },
311+
])
312+
313+
layout.distribute([50, 30, 20])
314+
315+
const cols = layout.columns.value
316+
expect(cols.find(c => c.key === 'a')!.size).toBe(50)
317+
expect(cols.find(c => c.key === 'b')!.size).toBe(30)
318+
expect(cols.find(c => c.key === 'c')!.size).toBe(20)
319+
const total = cols.reduce((sum, c) => sum + c.size, 0)
320+
expect(total).toBeCloseTo(100)
321+
})
322+
323+
it('no-op when array length mismatches', () => {
324+
const layout = createColumnLayout([
325+
{ key: 'a', size: 50 },
326+
{ key: 'b', size: 50 },
327+
])
328+
329+
layout.distribute([100])
330+
331+
const cols = layout.columns.value
332+
expect(cols.find(c => c.key === 'a')!.size).toBe(50)
333+
expect(cols.find(c => c.key === 'b')!.size).toBe(50)
334+
})
335+
336+
it('normalizes values that do not sum to 100', () => {
337+
const layout = createColumnLayout([
338+
{ key: 'a' },
339+
{ key: 'b' },
340+
])
341+
342+
layout.distribute([30, 30])
343+
344+
const cols = layout.columns.value
345+
const total = cols.reduce((sum, c) => sum + c.size, 0)
346+
expect(total).toBeCloseTo(100)
347+
})
348+
})
349+
})

0 commit comments

Comments
 (0)