Theme Builder: accordion panels across the full config surface, vertical-tab nav, reflective gallery

This commit is contained in:
2026-06-21 17:14:58 +03:00
parent 318a0919c6
commit d76a7a5014
24 changed files with 2420 additions and 845 deletions
+152 -7
View File
@@ -143,6 +143,13 @@ describe('AxesControls', () => {
act(() => setNativeValue(numberInput('Label angle'), '-45'));
expect(at('axis', 'labelAngle')).toBe(-45);
});
test('ticks "Hidden" writes axis.ticks false', () => {
render();
open({}, 'Axes & grid');
pick('Ticks', 'Hidden');
expect(at('axis', 'ticks')).toBe(false);
});
});
describe('LegendControls', () => {
@@ -159,20 +166,158 @@ describe('LegendControls', () => {
act(() => setNativeValue(numberInput('Symbol size'), '120'));
expect(at('legend', 'symbolSize')).toBe(120);
});
test('direction "Horizontal" writes legend.direction', () => {
render();
open({}, 'Legend');
pick('Direction', 'Horizontal');
expect(at('legend', 'direction')).toBe('horizontal');
});
});
describe('accordion panels', () => {
// Every panel groups its controls into the single-expand accordion. Verify the
// structure on the Color panel: each section is a collapsible header, the first
// is open, the rest collapsed.
const headers = () =>
[...container.querySelectorAll('button')].filter((b) =>
b.hasAttribute('data-accordion-header'),
);
test('a converted panel renders collapsible sections, first open', () => {
render();
open({}, 'Color');
const hs = headers();
expect(hs).toHaveLength(4); // categorical / mark color / sequential / diverging
expect(hs[0].textContent).toContain('Categorical palette');
expect(hs[3].textContent).toContain('Diverging gradient');
expect(hs[0].getAttribute('aria-expanded')).toBe('true');
expect(hs[1].getAttribute('aria-expanded')).toBe('false');
});
test('a section header shows a count badge of the properties set within it', () => {
render();
// Two axis-grid keys set → the Grid section badge reads 2.
open({ axis: { grid: false, gridColor: '#fff' } }, 'Axes & grid');
const grid = headers().find((h) => h.textContent?.includes('Grid'))!;
expect(grid.querySelector('[aria-label="2 set"]')?.textContent).toBe('2');
});
});
describe('MarksControls', () => {
// The per-type accordion sections start collapsed except the first ("All
// marks"); open a section by clicking its header before reaching its controls.
const openSection = (title: string) => {
const header = [...container.querySelectorAll('button')].find(
(b) => b.hasAttribute('data-accordion-header') && b.textContent?.includes(title),
)!;
act(() => header.click());
};
test('bar corner radius writes the bar-only path, not the generic mark', () => {
render();
open({}, 'Marks');
openSection('Bars');
act(() => setNativeValue(numberInput('Bar corner radius'), '4'));
expect(at('bar', 'cornerRadiusEnd')).toBe(4);
expect(at('mark', 'cornerRadius')).toBeUndefined();
});
test('clearing a bar control prunes the emptied bar object (minimal diff)', () => {
render();
open({ bar: { cornerRadiusEnd: 4 } }, 'Marks');
openSection('Bars');
act(() => setNativeValue(numberInput('Bar corner radius'), ''));
expect(config().bar).toBeUndefined();
});
test('line curve writes line.interpolate', () => {
render();
open({}, 'Marks');
openSection('Lines & areas');
pick('Line curve', 'Monotone');
expect(at('line', 'interpolate')).toBe('monotone');
});
test('arc donut hole writes arc.innerRadius', () => {
render();
open({}, 'Marks');
openSection('Arc');
act(() => setNativeValue(numberInput('Donut hole radius'), '40'));
expect(at('arc', 'innerRadius')).toBe(40);
});
test('tooltips tri-state "Off" writes mark.tooltip false', () => {
render();
open({}, 'Marks');
pick('Tooltips', 'Off'); // "All marks" section is open by default
expect(at('mark', 'tooltip')).toBe(false);
});
});
describe('TypeControls', () => {
test('title weight "Bold" writes title.fontWeight 700', () => {
render();
open({}, 'Type');
pick('Title weight', 'Bold');
expect(at('title', 'fontWeight')).toBe(700);
});
test('axis label size writes axis.labelFontSize', () => {
render();
open({}, 'Type');
act(() => setNativeValue(numberInput('Axis label size'), '9'));
expect(at('axis', 'labelFontSize')).toBe(9);
});
test('axis title weight "Bold" (shared WeightRow) writes 700', () => {
render();
open({}, 'Type');
pick('Axis title weight', 'Bold');
expect(at('axis', 'titleFontWeight')).toBe(700);
});
});
describe('TitleControls', () => {
test('alignment "Left" writes title.anchor start', () => {
render();
open({}, 'Title');
pick('Alignment', 'Left');
expect(at('title', 'anchor')).toBe('start');
});
test('title weight "Bold" writes title.fontWeight 700 (relocated from Type)', () => {
render();
open({}, 'Title');
pick('Title weight', 'Bold');
expect(at('title', 'fontWeight')).toBe(700);
});
test('subtitle size writes title.subtitleFontSize', () => {
render();
open({}, 'Title');
act(() => setNativeValue(numberInput('Subtitle size'), '11'));
expect(at('title', 'subtitleFontSize')).toBe(11);
});
});
describe('FormatControls', () => {
test('a number preset chip fills numberFormat; clearing the field removes it', () => {
render();
open({}, 'Formats');
act(() => button('$1,234.00')!.click());
expect(config().numberFormat).toBe('$,.2f');
act(() => setNativeValue(container.querySelector('input[aria-label="Number format"]')!, ''));
expect(config().numberFormat).toBeUndefined();
});
test('date format typed free-text writes timeFormat', () => {
render();
open({}, 'Formats');
act(() => setNativeValue(container.querySelector('input[aria-label="Date format"]')!, '%Y'));
expect(config().timeFormat).toBe('%Y');
});
});
describe('HeaderControls', () => {
test('facet title size writes header.titleFontSize', () => {
render();
open({}, 'Headers');
act(() => setNativeValue(numberInput('Header title size'), '13'));
expect(at('header', 'titleFontSize')).toBe(13);
});
});