Skip to content

Commit 4a63990

Browse files
fix(resizable, box): DLT-3445 accept full layout token set (#1293)
1 parent 3cb4282 commit 4a63990

10 files changed

Lines changed: 293 additions & 47 deletions

File tree

packages/dialtone-css/lib/build/less/components/box.less

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@
6262
}
6363

6464
._box-layout(@prop) {
65+
&-0 { @{prop}: 0; }
66+
6567
&-1px { @{prop}: var(--dt-layout-1px); }
6668
&-2px { @{prop}: var(--dt-layout-2px); }
6769
&-8px { @{prop}: var(--dt-layout-8px); }
@@ -70,9 +72,15 @@
7072
&-24px { @{prop}: var(--dt-layout-24px); }
7173
&-50 { @{prop}: var(--dt-layout-50); }
7274
&-75 { @{prop}: var(--dt-layout-75); }
73-
each(range(0, 1600, 100), {
75+
76+
each(range(100, 200, 25), {
77+
&-@{value} { @{prop}: ~"var(--dt-layout-@{value})"; }
78+
});
79+
80+
each(range(250, 1600, 50), {
7481
&-@{value} { @{prop}: ~"var(--dt-layout-@{value})"; }
7582
});
83+
7684
// Percentage tokens
7785
each(@box-layout-percent-values, {
7886
&-@{value}p { @{prop}: ~"var(--dt-layout-@{value}-percent)"; }

packages/dialtone-vue/common/constants/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export const DEFAULT_PREFIX = 'dt';
8383

8484
// Component size scale
8585
export { COMPONENT_SIZES, TEXT_HEADLINE_SIZES } from './sizes.js';
86+
export { LAYOUT_SIZE_VALUES, LAYOUT_PERCENT_VALUES, LAYOUT_VALUES } from './layout.js';
8687

8788
export default {
8889
VALIDATION_MESSAGE_TYPES,
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Layout sizing values accepted by Vue sizing props.
3+
* Includes 0, which resolves to raw CSS 0 rather than a --dt-layout-* token.
4+
* Remaining values must stay in sync with LAYOUT_STOPS in
5+
* packages/dialtone-css/postcss/constants.cjs.
6+
*
7+
* Non-zero bare integers are scale-indexed on the 64px layout base:
8+
* value_in_px = stop * 64 / 100.
9+
* '*px' values are literal off-scale pixel exceptions.
10+
* @type {string[]}
11+
*/
12+
export const LAYOUT_SIZE_VALUES = [
13+
'0',
14+
'1px', '2px', '8px', '25', '20px', '24px', '50', '75', '100', '125', '150',
15+
'175', '200', '250', '300', '350', '400', '450', '500', '550', '600', '650',
16+
'700', '750', '800', '850', '900', '950', '1000', '1050', '1100', '1150',
17+
'1200', '1250', '1300', '1350', '1400', '1450', '1500', '1550', '1600',
18+
];
19+
20+
/**
21+
* Percentage layout values accepted by DtBox layout props.
22+
* @type {string[]}
23+
*/
24+
export const LAYOUT_PERCENT_VALUES = [
25+
'10p', '20p', '25p', '30p', '33p', '40p', '50p',
26+
'60p', '66p', '70p', '75p', '80p', '90p', '95p', '100p',
27+
];
28+
29+
/**
30+
* Layout token and percentage values accepted by DtBox layout props.
31+
* @type {string[]}
32+
*/
33+
export const LAYOUT_VALUES = [
34+
...LAYOUT_SIZE_VALUES,
35+
...LAYOUT_PERCENT_VALUES,
36+
];
37+
38+
export default {
39+
LAYOUT_SIZE_VALUES,
40+
LAYOUT_PERCENT_VALUES,
41+
LAYOUT_VALUES,
42+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import cssConstants from '../../../dialtone-css/postcss/constants.cjs';
2+
import { LAYOUT_SIZE_VALUES, LAYOUT_PERCENT_VALUES, LAYOUT_VALUES } from '@/common/constants';
3+
4+
describe('layout constants', () => {
5+
it('builds size values from CSS layout token stops', () => {
6+
expect(LAYOUT_SIZE_VALUES).toEqual(['0', ...cssConstants.LAYOUT_STOPS.map(String)]);
7+
});
8+
9+
it('builds layout values from size and percentage values', () => {
10+
expect(LAYOUT_VALUES).toEqual([
11+
...LAYOUT_SIZE_VALUES,
12+
...LAYOUT_PERCENT_VALUES,
13+
]);
14+
});
15+
});

packages/dialtone-vue/components/Box/Box.test.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { mount } from '@vue/test-utils';
2+
import { vi } from 'vitest';
23
import DtBox from './Box.vue';
34
import {
45
DT_BOX_AS_VALUES,
@@ -7,11 +8,17 @@ import {
78
DT_BOX_BORDER_WIDTH_VALUES,
89
DT_BOX_BORDER_RADIUS_VALUES,
910
DT_BOX_SHADOW_VALUES,
11+
DT_BOX_LAYOUT_VALUES,
1012
DT_BOX_OVERFLOW_VALUES,
1113
} from './BoxConstants.js';
1214

1315
describe('DtBox', () => {
1416
const slotContent = 'Box content';
17+
const EXPANDED_LAYOUT_VALUES = DT_BOX_LAYOUT_VALUES.filter((value) => {
18+
const token = Number(value);
19+
// Intermediate stops between 100-multiple base stops, such as 125, 350, and 1550.
20+
return Number.isInteger(token) && token > 100 && token % 100 !== 0;
21+
});
1522
let wrapper;
1623

1724
const mountComponent = (props = {}, attrs = {}, slots = {}) => {
@@ -22,12 +29,19 @@ describe('DtBox', () => {
2229
default: slotContent,
2330
...slots,
2431
},
32+
global: {
33+
directives: {
34+
// Vue resolves the directive during render even when the scrollbar branch is not mounted.
35+
'dt-scrollbar': {},
36+
},
37+
},
2538
});
2639
return wrapper;
2740
};
2841

2942
afterEach(() => {
3043
wrapper?.unmount();
44+
vi.restoreAllMocks();
3145
});
3246

3347
// ── Presentation ──────────────────────────────────────────
@@ -340,6 +354,41 @@ describe('DtBox', () => {
340354
expect(wrapper.classes()).toContain('d-box--max-bls-600');
341355
});
342356

357+
it.each(EXPANDED_LAYOUT_VALUES)('applies inlineSize class for expanded layout token %s', (value) => {
358+
const wrapper = mountComponent({ inlineSize: value });
359+
360+
expect(wrapper.classes()).toContain(`d-box--is-${value}`);
361+
});
362+
363+
it.each(EXPANDED_LAYOUT_VALUES)('does not warn for expanded layout token %s', (value) => {
364+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
365+
366+
mountComponent({ inlineSize: value });
367+
368+
expect(warnSpy).not.toHaveBeenCalled();
369+
});
370+
371+
it.each([
372+
['inlineSize', '350', 'd-box--is-350'],
373+
['blockSize', '650', 'd-box--bls-650'],
374+
['minInlineSize', '750', 'd-box--min-is-750'],
375+
['maxInlineSize', '350', 'd-box--max-is-350'],
376+
['minBlockSize', '650', 'd-box--min-bls-650'],
377+
['maxBlockSize', '750', 'd-box--max-bls-750'],
378+
])('applies %s modifier class for expanded layout token %s', (prop, value, expectedClass) => {
379+
const wrapper = mountComponent({ [prop]: value });
380+
381+
expect(wrapper.classes()).toContain(expectedClass);
382+
});
383+
384+
it('warns when layout token is invalid', () => {
385+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
386+
387+
mountComponent({ inlineSize: '225' });
388+
389+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('[DtBox] Invalid prop value: "225"'));
390+
});
391+
343392
// ── Overflow ──────────────────────────────────────────────
344393

345394
it('applies overflow modifier class', () => {

packages/dialtone-vue/components/Box/Box.vue

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -176,37 +176,37 @@ const props = defineProps({
176176
177177
/**
178178
* Block size (aka height). Maps to --dt-layout-* tokens.
179-
* @values 0, 1px, 2px, 8px, 25, 20px, 24px, 50, 75, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1600, 10p, 20p, 25p, 30p, 33p, 40p, 50p, 60p, 66p, 70p, 75p, 80p, 90p, 95p, 100p
179+
* @values 0, 1px, 2px, 8px, 25, 20px, 24px, 50, 75, 100, 125, 150, 175, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950, 1000, 1050, 1100, 1150, 1200, 1250, 1300, 1350, 1400, 1450, 1500, 1550, 1600, 10p, 20p, 25p, 30p, 33p, 40p, 50p, 60p, 66p, 70p, 75p, 80p, 90p, 95p, 100p
180180
*/
181181
blockSize: { type: String, default: undefined, validator: layoutValidator },
182182
183183
/**
184184
* Inline size (aka width). Maps to --dt-layout-* tokens.
185-
* @values 0, 1px, 2px, 8px, 25, 20px, 24px, 50, 75, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1600, 10p, 20p, 25p, 30p, 33p, 40p, 50p, 60p, 66p, 70p, 75p, 80p, 90p, 95p, 100p
185+
* @values 0, 1px, 2px, 8px, 25, 20px, 24px, 50, 75, 100, 125, 150, 175, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950, 1000, 1050, 1100, 1150, 1200, 1250, 1300, 1350, 1400, 1450, 1500, 1550, 1600, 10p, 20p, 25p, 30p, 33p, 40p, 50p, 60p, 66p, 70p, 75p, 80p, 90p, 95p, 100p
186186
*/
187187
inlineSize: { type: String, default: undefined, validator: layoutValidator },
188188
189189
/**
190190
* Maximum block size. Maps to --dt-layout-* tokens.
191-
* @values 0, 1px, 2px, 8px, 25, 20px, 24px, 50, 75, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1600, 10p, 20p, 25p, 30p, 33p, 40p, 50p, 60p, 66p, 70p, 75p, 80p, 90p, 95p, 100p
191+
* @values 0, 1px, 2px, 8px, 25, 20px, 24px, 50, 75, 100, 125, 150, 175, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950, 1000, 1050, 1100, 1150, 1200, 1250, 1300, 1350, 1400, 1450, 1500, 1550, 1600, 10p, 20p, 25p, 30p, 33p, 40p, 50p, 60p, 66p, 70p, 75p, 80p, 90p, 95p, 100p
192192
*/
193193
maxBlockSize: { type: String, default: undefined, validator: layoutValidator },
194194
195195
/**
196196
* Minimum block size. Maps to --dt-layout-* tokens.
197-
* @values 0, 1px, 2px, 8px, 25, 20px, 24px, 50, 75, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1600, 10p, 20p, 25p, 30p, 33p, 40p, 50p, 60p, 66p, 70p, 75p, 80p, 90p, 95p, 100p
197+
* @values 0, 1px, 2px, 8px, 25, 20px, 24px, 50, 75, 100, 125, 150, 175, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950, 1000, 1050, 1100, 1150, 1200, 1250, 1300, 1350, 1400, 1450, 1500, 1550, 1600, 10p, 20p, 25p, 30p, 33p, 40p, 50p, 60p, 66p, 70p, 75p, 80p, 90p, 95p, 100p
198198
*/
199199
minBlockSize: { type: String, default: undefined, validator: layoutValidator },
200200
201201
/**
202202
* Maximum inline size. Maps to --dt-layout-* tokens.
203-
* @values 0, 1px, 2px, 8px, 25, 20px, 24px, 50, 75, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1600, 10p, 20p, 25p, 30p, 33p, 40p, 50p, 60p, 66p, 70p, 75p, 80p, 90p, 95p, 100p
203+
* @values 0, 1px, 2px, 8px, 25, 20px, 24px, 50, 75, 100, 125, 150, 175, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950, 1000, 1050, 1100, 1150, 1200, 1250, 1300, 1350, 1400, 1450, 1500, 1550, 1600, 10p, 20p, 25p, 30p, 33p, 40p, 50p, 60p, 66p, 70p, 75p, 80p, 90p, 95p, 100p
204204
*/
205205
maxInlineSize: { type: String, default: undefined, validator: layoutValidator },
206206
207207
/**
208208
* Minimum inline size. Maps to --dt-layout-* tokens.
209-
* @values 0, 1px, 2px, 8px, 25, 20px, 24px, 50, 75, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1600, 10p, 20p, 25p, 30p, 33p, 40p, 50p, 60p, 66p, 70p, 75p, 80p, 90p, 95p, 100p
209+
* @values 0, 1px, 2px, 8px, 25, 20px, 24px, 50, 75, 100, 125, 150, 175, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950, 1000, 1050, 1100, 1150, 1200, 1250, 1300, 1350, 1400, 1450, 1500, 1550, 1600, 10p, 20p, 25p, 30p, 33p, 40p, 50p, 60p, 66p, 70p, 75p, 80p, 90p, 95p, 100p
210210
*/
211211
minInlineSize: { type: String, default: undefined, validator: layoutValidator },
212212

packages/dialtone-vue/components/Box/BoxConstants.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { LAYOUT_VALUES } from '@/common/constants';
2+
13
/**
24
* Valid `as` elements for the box component.
35
* @type {string[]}
@@ -62,11 +64,7 @@ export const DT_BOX_SHADOW_VALUES = ['small', 'medium', 'large', 'extra-large',
6264
* Layout token scale (for sizing props: inlineSize, blockSize, min/max variants).
6365
* @type {string[]}
6466
*/
65-
export const DT_BOX_LAYOUT_VALUES = [
66-
'0', '1px', '2px', '8px', '25', '20px', '24px', '50', '75', '100', '200', '300', '400', '500', '600', '700', '800', '900', '1000', '1100', '1200', '1300', '1400', '1500', '1600',
67-
'10p', '20p', '25p', '30p', '33p', '40p', '50p',
68-
'60p', '66p', '70p', '75p', '80p', '90p', '95p', '100p',
69-
];
67+
export const DT_BOX_LAYOUT_VALUES = LAYOUT_VALUES;
7068

7169
/**
7270
* Overflow values.

packages/dialtone-vue/components/Resizable/Resizable.test.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import DtResizable from './Resizable.vue';
1212
import DtResizablePanel from './ResizablePanel.vue';
1313
import DtResizableHandle from './ResizableHandle.vue';
1414
import { RESIZABLE_CONTEXT_KEY, RESIZABLE_HANDLE_CENTER_OFFSET_PX } from './ResizableConstants';
15+
import { isValidSizing, parseSizeToPixels } from './ResizableUtils';
16+
import { LAYOUT_SIZE_VALUES, LAYOUT_VALUES } from '@/common/constants';
1517

1618
// Mock ResizeObserver for test environment
1719
global.ResizeObserver = vi.fn().mockImplementation(() => ({
@@ -59,10 +61,48 @@ const InjectionReader = defineComponent({
5961
},
6062
});
6163

64+
const PanelSizeValidationLayout = defineComponent({
65+
name: 'PanelSizeValidationLayout',
66+
components: { DtResizable, DtResizablePanel, DtResizableHandle },
67+
props: {
68+
panelProps: {
69+
type: Object,
70+
default: () => ({}),
71+
},
72+
},
73+
74+
template: `
75+
<div style="width: 1000px; height: 400px;">
76+
<dt-resizable>
77+
<dt-resizable-panel id="left" v-bind="panelProps" />
78+
<dt-resizable-handle />
79+
<dt-resizable-panel id="right" initial-size="50p" />
80+
</dt-resizable>
81+
</div>
82+
`,
83+
});
84+
6285
let wrapper;
6386

6487
const CONTAINER_WIDTH = 1000;
6588
const PANEL_BOUNDARY = CONTAINER_WIDTH / 2;
89+
const LAYOUT_BASE_PX = 64;
90+
const REPRESENTATIVE_LAYOUT_VALUES = LAYOUT_SIZE_VALUES.filter(value => ['350', '650', '750'].includes(value));
91+
const RESIZABLE_SIZE_PROPS = ['initialSize', 'userMinSize', 'userMaxSize', 'systemMinSize', 'systemMaxSize', 'collapseSize'];
92+
const EXPANDED_LAYOUT_TOKEN_VALUES = LAYOUT_SIZE_VALUES.filter((value) => {
93+
const token = Number(value);
94+
// Intermediate stops between 100-multiple base stops, such as 125, 350, and 1550.
95+
return Number.isInteger(token) && token > 100 && token % 100 !== 0;
96+
});
97+
const REPRESENTATIVE_LAYOUT_TOKEN_PIXEL_CASES = REPRESENTATIVE_LAYOUT_VALUES.map(value => [
98+
value,
99+
(Number(value) * LAYOUT_BASE_PX) / 100,
100+
]);
101+
const PRESERVED_SIZE_VALUES = [
102+
...LAYOUT_SIZE_VALUES.filter(value => ['0', '1px', '2px', '8px', '20px', '24px'].includes(value)),
103+
...LAYOUT_VALUES.filter(value => value === '50p'),
104+
];
105+
const INVALID_SIZE_VALUES = ['72', '225', '9999', '101p'];
66106

67107
const _setWrapper = (props = {}, slots = {}) => {
68108
wrapper = mount(DtResizable, {
@@ -76,6 +116,7 @@ describe('DtResizable Tests', () => {
76116
afterEach(() => {
77117
wrapper?.unmount();
78118
restoreClientWidth();
119+
vi.restoreAllMocks();
79120
});
80121

81122
describe('Presentation', () => {
@@ -187,6 +228,88 @@ describe('DtResizable Tests', () => {
187228
});
188229
});
189230

231+
describe('Sizing validation', () => {
232+
it.each(EXPANDED_LAYOUT_TOKEN_VALUES)('should accept expanded layout token %s', (value) => {
233+
expect(isValidSizing(value)).toBe(true);
234+
});
235+
236+
it.each(EXPANDED_LAYOUT_TOKEN_VALUES)('should not warn when validating expanded layout token %s', (value) => {
237+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
238+
239+
isValidSizing(value);
240+
241+
expect(warnSpy).not.toHaveBeenCalled();
242+
});
243+
244+
it.each(REPRESENTATIVE_LAYOUT_TOKEN_PIXEL_CASES)('should resolve layout token %s to %ipx', (value, expectedPixels) => {
245+
expect(parseSizeToPixels(value, 2000)).toBe(expectedPixels);
246+
});
247+
248+
it.each(PRESERVED_SIZE_VALUES)('should preserve existing valid size value %s', (value) => {
249+
expect(isValidSizing(value)).toBe(true);
250+
});
251+
252+
it.each(INVALID_SIZE_VALUES)('should reject invalid size value %s', (value) => {
253+
expect(isValidSizing(value)).toBe(false);
254+
});
255+
256+
it.each(INVALID_SIZE_VALUES)('should fall back to zero for invalid size value %s', (value) => {
257+
vi.spyOn(console, 'warn').mockImplementation(() => {});
258+
259+
expect(parseSizeToPixels(value, 1000)).toBe(0);
260+
});
261+
262+
it.each(INVALID_SIZE_VALUES)('should warn for invalid size value %s', (value) => {
263+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
264+
265+
parseSizeToPixels(value, 1000);
266+
267+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('[resizable] Invalid ResizableSizeValue'));
268+
});
269+
270+
it.each(
271+
RESIZABLE_SIZE_PROPS.flatMap(prop => REPRESENTATIVE_LAYOUT_VALUES.map(value => [prop, value])),
272+
)('should not error when %s uses expanded layout token %s', async (prop, value) => {
273+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
274+
mockClientWidth(CONTAINER_WIDTH);
275+
276+
wrapper = mount(PanelSizeValidationLayout, {
277+
props: { panelProps: { [prop]: value } },
278+
attachTo: document.body,
279+
});
280+
await wrapper.vm.$nextTick();
281+
282+
expect(errorSpy).not.toHaveBeenCalled();
283+
});
284+
285+
it.each(
286+
RESIZABLE_SIZE_PROPS.flatMap(prop => REPRESENTATIVE_LAYOUT_VALUES.map(value => [prop, value])),
287+
)('should not warn when %s uses expanded layout token %s', async (prop, value) => {
288+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
289+
mockClientWidth(CONTAINER_WIDTH);
290+
291+
wrapper = mount(PanelSizeValidationLayout, {
292+
props: { panelProps: { [prop]: value } },
293+
attachTo: document.body,
294+
});
295+
await wrapper.vm.$nextTick();
296+
297+
expect(warnSpy).not.toHaveBeenCalled();
298+
});
299+
300+
it('should warn when a panel size prop uses an invalid value', () => {
301+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
302+
vi.spyOn(console, 'warn').mockImplementation(() => {});
303+
304+
wrapper = mount(PanelSizeValidationLayout, {
305+
props: { panelProps: { userMinSize: '225' } },
306+
attachTo: document.body,
307+
});
308+
309+
expect(errorSpy).toHaveBeenCalledWith('[DtResizablePanel] Invalid userMinSize: "225".');
310+
});
311+
});
312+
190313
describe('Event emissions', () => {
191314
const FullLayout = defineComponent({
192315
name: 'FullLayout',

0 commit comments

Comments
 (0)