clickgo 6.6.0 → 6.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/demo.cga +0 -0
- package/dist/app/task.cga +0 -0
- package/dist/control/common.cgc +0 -0
- package/dist/index.js +1 -1
- package/dist/theme/compact-dark.cgt +0 -0
- package/dist/theme/compact-light.cgt +0 -0
- package/doc/clickgo-rag.md +456 -247
- package/package.json +1 -1
- package/test/dock-lifecycle.mjs +111 -3
- package/test/locale-integrity.mjs +2 -2
- package/test/rtl-archive.mjs +12 -5
- package/test/sidebar-toggle.mjs +75 -0
- package/test/toolbox.mjs +88 -0
package/package.json
CHANGED
package/test/dock-lifecycle.mjs
CHANGED
|
@@ -8,7 +8,8 @@ const sizeUnwatches = [];
|
|
|
8
8
|
|
|
9
9
|
globalThis.document = {
|
|
10
10
|
'documentElement': {
|
|
11
|
-
'clientWidth': 360
|
|
11
|
+
'clientWidth': 360,
|
|
12
|
+
'clientHeight': 800
|
|
12
13
|
}
|
|
13
14
|
};
|
|
14
15
|
|
|
@@ -32,6 +33,14 @@ class AbstractControl {
|
|
|
32
33
|
return Boolean(this.props[name]);
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
propNumber(name) {
|
|
37
|
+
return Number(this.props[name]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
parentByName(name) {
|
|
41
|
+
return this.parents?.[name] ?? null;
|
|
42
|
+
}
|
|
43
|
+
|
|
35
44
|
emit() {}
|
|
36
45
|
}
|
|
37
46
|
|
|
@@ -56,6 +65,9 @@ const clickgo = new SyntheticModule(['control', 'dom'], function() {
|
|
|
56
65
|
},
|
|
57
66
|
isRtl(element) {
|
|
58
67
|
return element.rtl;
|
|
68
|
+
},
|
|
69
|
+
index(element) {
|
|
70
|
+
return element.order;
|
|
59
71
|
}
|
|
60
72
|
});
|
|
61
73
|
});
|
|
@@ -146,6 +158,10 @@ group.dock = {
|
|
|
146
158
|
group.updateFloatLayout();
|
|
147
159
|
assert.equal(group.floatMaxWidth, 280, 'LTR left Dock must use only the viewport space on its right');
|
|
148
160
|
assert.equal(group.floatStyle['max-width'], '280px');
|
|
161
|
+
assert.equal(group.floatStyle.height, undefined, 'short floating panels must size to their content');
|
|
162
|
+
group.props.grow = true;
|
|
163
|
+
assert.equal(group.floatStyle.height, '300px', 'grow floating panels fill the available height');
|
|
164
|
+
group.props.grow = false;
|
|
149
165
|
|
|
150
166
|
group.element.rtl = true;
|
|
151
167
|
areaRect.left = 320;
|
|
@@ -159,14 +175,106 @@ areaRect.right = 40;
|
|
|
159
175
|
group.updateFloatLayout();
|
|
160
176
|
assert.equal(group.floatMaxWidth, 320, 'RTL right Dock must open toward the viewport space on its right');
|
|
161
177
|
|
|
178
|
+
group.element.rtl = false;
|
|
179
|
+
group.dock.getFloatBounds = () => ({ 'left': 30, 'right': 330, 'top': 60, 'bottom': 220 });
|
|
180
|
+
areaRect.left = 300;
|
|
181
|
+
areaRect.right = 340;
|
|
182
|
+
group.updateFloatLayout();
|
|
183
|
+
assert.equal(group.floatMaxWidth, 270, 'floating panels must stay inside their Form, not only the viewport');
|
|
184
|
+
assert.equal(group.floatMaxHeight, '160px');
|
|
185
|
+
assert.equal(group.floatTop, 60);
|
|
186
|
+
group.dock.positionData = 'left';
|
|
187
|
+
group.updateFloatLayout();
|
|
188
|
+
assert.equal(group.floatMaxWidth, 0, 'zero available width must remain zero rather than use a fallback');
|
|
189
|
+
assert.equal(group.floatStyle['max-width'], '0px');
|
|
190
|
+
|
|
191
|
+
const columnMod = new SourceTextModule(await readFile(new URL('../dist/sources/control/dock-column/code.js', import.meta.url), 'utf8'));
|
|
192
|
+
await columnMod.link(() => clickgo);
|
|
193
|
+
await columnMod.evaluate();
|
|
194
|
+
const DockColumn = columnMod.namespace.default;
|
|
195
|
+
|
|
196
|
+
document.documentElement.clientWidth = 1200;
|
|
197
|
+
document.documentElement.clientHeight = 900;
|
|
198
|
+
const multiForm = {
|
|
199
|
+
'element': {
|
|
200
|
+
'isConnected': true,
|
|
201
|
+
'offsetWidth': 1100,
|
|
202
|
+
'getBoundingClientRect': () => ({ 'left': 60, 'right': 1160, 'top': 30, 'bottom': 930 })
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const multiDock = new Dock(multiForm, 600);
|
|
206
|
+
await multiDock.onMounted();
|
|
207
|
+
assert.deepEqual(multiDock.getFloatBounds(), { 'left': 60, 'right': 1160, 'top': 30, 'bottom': 900 });
|
|
208
|
+
|
|
209
|
+
const projectColumn = new DockColumn(multiForm, 600);
|
|
210
|
+
const mainColumn = new DockColumn(multiForm, 600);
|
|
211
|
+
projectColumn.parents = mainColumn.parents = { 'dock': multiDock };
|
|
212
|
+
projectColumn.element = { 'order': 0 };
|
|
213
|
+
mainColumn.element = { 'order': 1 };
|
|
214
|
+
projectColumn.props.width = 220;
|
|
215
|
+
mainColumn.props.width = 340;
|
|
216
|
+
// Child groups mount before their column: both columns must allocate through the same root.
|
|
217
|
+
const projectIndex = projectColumn.registerGroup();
|
|
218
|
+
const mainIndex = mainColumn.registerGroup();
|
|
219
|
+
assert.notEqual(projectIndex, mainIndex);
|
|
220
|
+
await projectColumn.onMounted();
|
|
221
|
+
await mainColumn.onMounted();
|
|
222
|
+
assert.equal(projectColumn.expandedData, true);
|
|
223
|
+
assert.equal(mainColumn.expandedData, true);
|
|
224
|
+
|
|
225
|
+
function resizeMultiForm(width) {
|
|
226
|
+
multiForm.element.offsetWidth = width;
|
|
227
|
+
multiDock.access.formSizeWatch.handler();
|
|
228
|
+
}
|
|
229
|
+
resizeMultiForm(850);
|
|
230
|
+
assert.equal(projectColumn.autoCollapsed, true, 'the auxiliary column collapses first');
|
|
231
|
+
assert.equal(mainColumn.autoCollapsed, false, 'keep the main column open when it still fits');
|
|
232
|
+
assert.equal(projectColumn.expandedPreference, true, 'automatic collapse must preserve user preference');
|
|
233
|
+
assert.equal(projectColumn.props.expanded, true);
|
|
234
|
+
assert.match(multiDock.widthComp, /var\(--dock-collapsed-width\).*340px/);
|
|
235
|
+
|
|
236
|
+
resizeMultiForm(650);
|
|
237
|
+
assert.equal(projectColumn.expandedData, false);
|
|
238
|
+
assert.equal(mainColumn.expandedData, false, 'collapse the main column too when workspace space is insufficient');
|
|
239
|
+
resizeMultiForm(1100);
|
|
240
|
+
assert.equal(projectColumn.expandedData, true);
|
|
241
|
+
assert.equal(mainColumn.expandedData, true);
|
|
242
|
+
|
|
243
|
+
document.documentElement.clientWidth = 600;
|
|
244
|
+
multiDock.access.viewportSizeWatch.handler();
|
|
245
|
+
assert.equal(projectColumn.expandedData, false);
|
|
246
|
+
assert.equal(mainColumn.expandedData, false, 'viewport clipping must also limit multiple expanded columns');
|
|
247
|
+
document.documentElement.clientWidth = 1200;
|
|
248
|
+
multiDock.access.viewportSizeWatch.handler();
|
|
249
|
+
assert.equal(mainColumn.expandedData, true);
|
|
250
|
+
projectColumn.toggle();
|
|
251
|
+
assert.equal(projectColumn.expandedPreference, false);
|
|
252
|
+
resizeMultiForm(650);
|
|
253
|
+
resizeMultiForm(1100);
|
|
254
|
+
assert.equal(projectColumn.expandedData, false, 'a column the user closed must remain closed after resizing');
|
|
255
|
+
assert.equal(mainColumn.expandedData, true);
|
|
256
|
+
|
|
257
|
+
multiDock.toggleFloat(projectIndex);
|
|
258
|
+
multiDock.toggleFloat(mainIndex);
|
|
259
|
+
assert.equal(multiDock.floatGroup, mainIndex, 'only one floating group may be open across columns');
|
|
260
|
+
mainColumn.unregisterGroup(mainIndex);
|
|
261
|
+
assert.equal(multiDock.floatGroup, -1);
|
|
262
|
+
multiDock.toggleFloat(projectIndex);
|
|
263
|
+
projectColumn.onUnmounted();
|
|
264
|
+
assert.equal(multiDock.columns.length, 1);
|
|
265
|
+
assert.equal(multiDock.floatGroup, -1, 'column removal closes its floating panel');
|
|
266
|
+
mainColumn.onUnmounted();
|
|
267
|
+
multiDock.onUnmounted();
|
|
268
|
+
|
|
162
269
|
const dockStyle = await readFile(new URL('../dist/sources/control/dock/style.scss', import.meta.url), 'utf8');
|
|
163
270
|
assert.match(dockStyle, /border-inline-end/, 'left Dock border must follow the inline end edge');
|
|
164
271
|
assert.match(dockStyle, /border-inline-start/, 'right Dock border must follow the inline start edge');
|
|
165
|
-
|
|
272
|
+
const toggleStyle = await readFile(new URL('../dist/sources/control/sidebar-toggle/style.scss', import.meta.url), 'utf8');
|
|
273
|
+
assert.match(toggleStyle, /:dir\(rtl\)[\s\S]*scaleX\(-1\)/, 'shared sidebar chevron must mirror in RTL');
|
|
166
274
|
|
|
167
275
|
const groupStyle = await readFile(new URL('../dist/sources/control/dock-group/style.scss', import.meta.url), 'utf8');
|
|
168
276
|
assert.match(groupStyle, /inset-inline-start:\s*100%/, 'left Dock floating panel must open toward inline end');
|
|
169
277
|
assert.match(groupStyle, /inset-inline-end:\s*100%/, 'right Dock floating panel must open toward inline start');
|
|
170
278
|
assert.match(groupStyle, /max-width:\s*calc\(100vw - 40px\)/, 'floating panel must reserve the collapsed Dock width');
|
|
171
279
|
|
|
172
|
-
console.log('Dock lifecycle, RTL
|
|
280
|
+
console.log('Dock lifecycle, multiple columns, preference restoration, RTL and Form/viewport bounds passed.');
|
|
@@ -140,7 +140,7 @@ for (const path of localeSources) {
|
|
|
140
140
|
tables.push({ ...table, path });
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
-
assert.equal(tables.length,
|
|
143
|
+
assert.equal(tables.length, 26, 'all 26 built-in TypeScript locale tables must be present');
|
|
144
144
|
for (const { node, path, line } of tables) {
|
|
145
145
|
const tableCodes = node.properties.map(getPropertyName);
|
|
146
146
|
assert.deepEqual([...tableCodes].sort(), [...codes].sort(), `${path}:${line} must contain exactly 16 locale entries`);
|
|
@@ -186,4 +186,4 @@ const demoLocaleCodes = [...demoLocaleData.matchAll(/'([^']+)'/g)].map((match) =
|
|
|
186
186
|
assert.deepEqual(demoLocaleCodes, codes, 'demo global locale selector must expose all 16 locales in registry order');
|
|
187
187
|
|
|
188
188
|
dom.window.close();
|
|
189
|
-
console.log(
|
|
189
|
+
console.log(`Locale registry, ${tables.length} TypeScript tables, 16 task app locale JSON files and the Demo selector are complete.`);
|
package/test/rtl-archive.mjs
CHANGED
|
@@ -14,12 +14,12 @@ const codes = [
|
|
|
14
14
|
/** --- 包含 localeData 的全部内置控件 --- */
|
|
15
15
|
const localeControls = [
|
|
16
16
|
'arteditor', 'calendar', 'captcha', 'date', 'datepanel', 'daterange', 'empty', 'jodit', 'label',
|
|
17
|
-
'levelselect', 'map', 'monaco', 'number', 'page', 'palette', 'property', 'select', 'text', 'tuieditor',
|
|
17
|
+
'levelselect', 'map', 'monaco', 'number', 'page', 'palette', 'property', 'select', 'sidebar-toggle', 'text', 'toolbox', 'tuieditor',
|
|
18
18
|
];
|
|
19
19
|
/** --- 合并到 common.cgc 的多语言控件 --- */
|
|
20
20
|
const commonControls = new Set([
|
|
21
21
|
'calendar', 'date', 'datepanel', 'daterange', 'empty', 'label', 'levelselect', 'number', 'palette',
|
|
22
|
-
'select', 'text',
|
|
22
|
+
'select', 'sidebar-toggle', 'text', 'toolbox',
|
|
23
23
|
]);
|
|
24
24
|
/** --- 全部内置主题 --- */
|
|
25
25
|
const themes = ['admin', 'cyber', 'dark', 'light', 'modern', 'modern-light', 'compact-dark', 'compact-light'];
|
|
@@ -335,7 +335,7 @@ for (const code of codes) {
|
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
// ---
|
|
338
|
+
// --- 所有多语言控件的归档语言表必须与 TypeScript 源码一致 ---
|
|
339
339
|
const archivedControlCode = new Map();
|
|
340
340
|
for (const control of localeControls) {
|
|
341
341
|
const packageName = commonControls.has(control) ? 'common' : control;
|
|
@@ -365,6 +365,10 @@ const rtlCodeChecks = [
|
|
|
365
365
|
['dist/control/common.cgc', 'date', ['autoPosition', 'autoScroll']],
|
|
366
366
|
['dist/control/common.cgc', 'daterange', ['autoPosition', 'autoScroll']],
|
|
367
367
|
['dist/control/common.cgc', 'dock-group', ['isRtl', 'floatMaxWidth']],
|
|
368
|
+
['dist/control/common.cgc', 'dock', ['updateColumns', 'getFloatBounds']],
|
|
369
|
+
['dist/control/common.cgc', 'dock-column', ['registerGroup', 'expandedPreference']],
|
|
370
|
+
['dist/control/common.cgc', 'toolbox', ['getBoundingClientRect', 'toggleColumns']],
|
|
371
|
+
['dist/control/common.cgc', 'sidebar-toggle', ['chevronRight', 'tipLabelComp', 'update:expanded']],
|
|
368
372
|
['dist/control/table.cgc', 'table', ['localeDirection', 'getScrollLeft', 'setScrollInlineOffset']],
|
|
369
373
|
['dist/control/desc.cgc', 'desc', ['getScrollInlineOffset', 'setScrollInlineOffset']],
|
|
370
374
|
['dist/control/iconview.cgc', 'iconview', ['isRtl']],
|
|
@@ -387,8 +391,11 @@ const rtlStyleChecks = [
|
|
|
387
391
|
['dist/control/common.cgc', 'list/style.css', 'dist/sources/control/list/style.css', [':dir(rtl)', 'margin-inline-end']],
|
|
388
392
|
['dist/control/common.cgc', 'tab/style.css', 'dist/sources/control/tab/style.css', ['border-inline-start-width', 'border-inline-end-width']],
|
|
389
393
|
['dist/control/common.cgc', 'timeline/style.css', 'dist/sources/control/timeline/style.css', ['border-inline-start']],
|
|
390
|
-
['dist/control/common.cgc', 'dock/style.css', 'dist/sources/control/dock/style.css', ['
|
|
394
|
+
['dist/control/common.cgc', 'dock/style.css', 'dist/sources/control/dock/style.css', ['border-inline-end']],
|
|
391
395
|
['dist/control/common.cgc', 'dock-group/style.css', 'dist/sources/control/dock-group/style.css', ['inset-inline-start', 'max-width:calc(100vw - 40px)']],
|
|
396
|
+
['dist/control/common.cgc', 'dock-column/style.css', 'dist/sources/control/dock-column/style.css', ['border-inline-start']],
|
|
397
|
+
['dist/control/common.cgc', 'toolbox/style.css', 'dist/sources/control/toolbox/style.css', ['border-inline-end']],
|
|
398
|
+
['dist/control/common.cgc', 'sidebar-toggle/style.css', 'dist/sources/control/sidebar-toggle/style.css', [':dir(rtl)', 'border-bottom', '--sidebar-toggle-background']],
|
|
392
399
|
['dist/control/table.cgc', 'table/style.css', 'dist/sources/control/table/style.css', ['border-inline-end', 'inset-inline-end']],
|
|
393
400
|
['dist/control/xterm.cgc', 'xterm/style.css', 'dist/sources/control/xterm/style.css', ['direction:ltr']],
|
|
394
401
|
];
|
|
@@ -417,4 +424,4 @@ for (const theme of themes) {
|
|
|
417
424
|
);
|
|
418
425
|
}
|
|
419
426
|
|
|
420
|
-
console.log(`Archive check passed: task.cga has 16 locales,
|
|
427
|
+
console.log(`Archive check passed: task.cga has 16 locales, ${localeControls.length} control locale tables are current, and RTL output is present in all ${themes.length} themes.`);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Run after TypeScript compilation: node --experimental-vm-modules test/sidebar-toggle.mjs
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { SourceTextModule, SyntheticModule } from 'node:vm';
|
|
5
|
+
|
|
6
|
+
class AbstractControl {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.emitted = [];
|
|
9
|
+
this.locale = 'sc';
|
|
10
|
+
}
|
|
11
|
+
propBoolean(name) {
|
|
12
|
+
return [true, 'true', ''].includes(this.props[name]);
|
|
13
|
+
}
|
|
14
|
+
l(key) {
|
|
15
|
+
return this.localeData[this.locale][key];
|
|
16
|
+
}
|
|
17
|
+
emit(name, value) {
|
|
18
|
+
this.emitted.push([name, value]);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const clickgo = new SyntheticModule(['control'], function() {
|
|
23
|
+
this.setExport('control', { AbstractControl });
|
|
24
|
+
});
|
|
25
|
+
const mod = new SourceTextModule(await readFile(new URL('../dist/sources/control/sidebar-toggle/code.js', import.meta.url), 'utf8'));
|
|
26
|
+
await mod.link(() => clickgo);
|
|
27
|
+
await mod.evaluate();
|
|
28
|
+
const SidebarToggle = mod.namespace.default;
|
|
29
|
+
const toggle = new SidebarToggle();
|
|
30
|
+
|
|
31
|
+
for (const [position, expanded, pointsRight] of [
|
|
32
|
+
['left', false, true], ['left', true, false], ['right', false, false], ['right', true, true],
|
|
33
|
+
]) {
|
|
34
|
+
toggle.props.position = position;
|
|
35
|
+
toggle.props.expanded = expanded;
|
|
36
|
+
assert.equal(toggle.chevronRight, pointsRight, `${position}, expanded=${expanded}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
toggle.props.expanded = 'false';
|
|
40
|
+
toggle.props.label = 'Project';
|
|
41
|
+
assert.equal(toggle.tipLabelComp, '展开侧栏 · Project');
|
|
42
|
+
toggle.props.expanded = 'true';
|
|
43
|
+
assert.equal(toggle.tipLabelComp, '收起侧栏 · Project');
|
|
44
|
+
toggle.locale = 'ar';
|
|
45
|
+
assert.equal(toggle.tipLabelComp, 'طي الشريط الجانبي · Project');
|
|
46
|
+
toggle.props.tipLabel = 'Use one tool column';
|
|
47
|
+
assert.equal(toggle.tipLabelComp, 'Use one tool column', 'Toolbox keeps its column-specific operation');
|
|
48
|
+
|
|
49
|
+
for (const key of ['Enter', ' ']) {
|
|
50
|
+
toggle.props.expanded = false;
|
|
51
|
+
const count = toggle.emitted.length;
|
|
52
|
+
let prevented = false;
|
|
53
|
+
let stopped = false;
|
|
54
|
+
const event = {
|
|
55
|
+
key, 'repeat': false,
|
|
56
|
+
'preventDefault': () => { prevented = true; },
|
|
57
|
+
'stopPropagation': () => { stopped = true; },
|
|
58
|
+
};
|
|
59
|
+
toggle.keydown(event);
|
|
60
|
+
assert.equal(prevented && stopped, true);
|
|
61
|
+
assert.deepEqual(toggle.emitted[count], ['update:expanded', true]);
|
|
62
|
+
assert.equal(toggle.props.expanded, false, 'the owner applies the requested state');
|
|
63
|
+
toggle.keydown({ ...event, 'repeat': true });
|
|
64
|
+
assert.equal(toggle.emitted.length, count + 1, 'holding a key must not repeatedly flip the sidebar');
|
|
65
|
+
}
|
|
66
|
+
toggle.keydown({
|
|
67
|
+
'key': 'Tab',
|
|
68
|
+
'preventDefault': () => assert.fail('Tab keeps native focus navigation'),
|
|
69
|
+
'stopPropagation': () => assert.fail('Other keys are not consumed'),
|
|
70
|
+
});
|
|
71
|
+
toggle.props.expanded = true;
|
|
72
|
+
toggle.toggle();
|
|
73
|
+
assert.deepEqual(toggle.emitted.at(-1), ['update:expanded', false]);
|
|
74
|
+
|
|
75
|
+
console.log('Shared sidebar toggle direction, localized tips, controlled state and keyboard activation passed.');
|
package/test/toolbox.mjs
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Run after TypeScript compilation: node --experimental-vm-modules test/toolbox.mjs
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { SourceTextModule, SyntheticModule } from 'node:vm';
|
|
5
|
+
import { JSDOM } from 'jsdom';
|
|
6
|
+
|
|
7
|
+
const dom = new JSDOM('<div id="tools"></div>');
|
|
8
|
+
globalThis.HTMLElement = dom.window.HTMLElement;
|
|
9
|
+
const tools = dom.window.document.getElementById('tools');
|
|
10
|
+
class AbstractControl {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.refs = { tools };
|
|
13
|
+
this.emitted = [];
|
|
14
|
+
}
|
|
15
|
+
emit(name, value) {
|
|
16
|
+
this.emitted.push([name, value]);
|
|
17
|
+
}
|
|
18
|
+
propNumber(name) {
|
|
19
|
+
return Number(this.props[name]);
|
|
20
|
+
}
|
|
21
|
+
watch(name, callback, options) {
|
|
22
|
+
if (options?.immediate) {
|
|
23
|
+
callback();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const clickgo = new SyntheticModule(['control'], function() {
|
|
28
|
+
this.setExport('control', { AbstractControl });
|
|
29
|
+
});
|
|
30
|
+
const mod = new SourceTextModule(await readFile(new URL('../dist/sources/control/toolbox/code.js', import.meta.url), 'utf8'));
|
|
31
|
+
await mod.link(() => clickgo);
|
|
32
|
+
await mod.evaluate();
|
|
33
|
+
const Toolbox = mod.namespace.default;
|
|
34
|
+
const toolbox = new Toolbox();
|
|
35
|
+
toolbox.props.columns = '2';
|
|
36
|
+
toolbox.onMounted();
|
|
37
|
+
assert.equal(toolbox.columnsData, 2);
|
|
38
|
+
toolbox.toggleColumns();
|
|
39
|
+
assert.equal(toolbox.columnsData, 1);
|
|
40
|
+
assert.deepEqual(toolbox.emitted, [['update:columns', 1]]);
|
|
41
|
+
|
|
42
|
+
function button(x, y, { disabled = false, hidden = false } = {}) {
|
|
43
|
+
const item = dom.window.document.createElement('div');
|
|
44
|
+
item.setAttribute('data-cg-control', 'button');
|
|
45
|
+
item.setAttribute('role', 'button');
|
|
46
|
+
item.setAttribute('tabindex', '0');
|
|
47
|
+
if (disabled) {
|
|
48
|
+
item.setAttribute('data-cg-disabled', '');
|
|
49
|
+
}
|
|
50
|
+
Object.defineProperty(item, 'offsetHeight', { 'value': hidden ? 0 : 24 });
|
|
51
|
+
item.getBoundingClientRect = () => ({ 'left': x, 'top': y, 'width': 24, 'height': 24 });
|
|
52
|
+
tools.append(item);
|
|
53
|
+
return item;
|
|
54
|
+
}
|
|
55
|
+
// A one-tool group precedes a two-column group; DOM index arithmetic would pick the wrong row.
|
|
56
|
+
const browse = button(0, 0);
|
|
57
|
+
const paint = button(0, 40);
|
|
58
|
+
const erase = button(30, 40);
|
|
59
|
+
button(0, 80, { 'disabled': true });
|
|
60
|
+
button(30, 80, { 'hidden': true });
|
|
61
|
+
const select = button(0, 120);
|
|
62
|
+
|
|
63
|
+
function move(from, key, expected) {
|
|
64
|
+
from.focus();
|
|
65
|
+
let prevented = false;
|
|
66
|
+
toolbox.keydown({ 'target': from, key, 'preventDefault': () => { prevented = true; } });
|
|
67
|
+
assert.equal(prevented, true);
|
|
68
|
+
assert.equal(dom.window.document.activeElement, expected, key);
|
|
69
|
+
}
|
|
70
|
+
move(browse, 'ArrowDown', paint);
|
|
71
|
+
move(paint, 'ArrowRight', erase);
|
|
72
|
+
move(erase, 'ArrowLeft', paint);
|
|
73
|
+
move(paint, 'ArrowDown', select);
|
|
74
|
+
move(select, 'Home', browse);
|
|
75
|
+
move(browse, 'End', select);
|
|
76
|
+
move(browse, 'ArrowUp', browse);
|
|
77
|
+
|
|
78
|
+
// RTL changes the rendered physical positions; navigation must follow those positions.
|
|
79
|
+
paint.getBoundingClientRect = () => ({ 'left': 30, 'top': 40, 'width': 24, 'height': 24 });
|
|
80
|
+
erase.getBoundingClientRect = () => ({ 'left': 0, 'top': 40, 'width': 24, 'height': 24 });
|
|
81
|
+
move(paint, 'ArrowLeft', erase);
|
|
82
|
+
move(erase, 'ArrowRight', paint);
|
|
83
|
+
|
|
84
|
+
const outside = dom.window.document.createElement('div');
|
|
85
|
+
outside.setAttribute('data-cg-control', 'button');
|
|
86
|
+
outside.setAttribute('role', 'button');
|
|
87
|
+
toolbox.keydown({ 'target': outside, 'key': 'ArrowDown', 'preventDefault': () => assert.fail('header/outside buttons are not tools') });
|
|
88
|
+
console.log('Toolbox column binding and keyboard navigation across groups, hidden/disabled tools and RTL passed.');
|