@dsh-workbench/shared 0.2.12 → 0.2.13

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.
Files changed (37) hide show
  1. package/dist/components/ChoiceGroup.css +53 -3
  2. package/dist/components/ChoiceGroup.d.ts +5 -0
  3. package/dist/components/ChoiceGroup.js +15 -5
  4. package/dist/components/FormRows.css +53 -0
  5. package/dist/components/FormRows.d.ts +14 -0
  6. package/dist/components/FormRows.js +11 -0
  7. package/dist/components/SelectField.css +12 -4
  8. package/dist/components/SelectField.d.ts +1 -0
  9. package/dist/components/SelectField.js +9 -7
  10. package/dist/components/Spinner.css +6 -0
  11. package/dist/components/Spinner.d.ts +6 -0
  12. package/dist/components/Spinner.js +7 -0
  13. package/dist/components/TextField.css +2 -2
  14. package/dist/host.d.ts +3 -3
  15. package/dist/host.js +3 -3
  16. package/dist/index.d.ts +7 -4
  17. package/dist/index.js +7 -4
  18. package/dist/lib/form-problem.d.ts +2 -0
  19. package/dist/lib/form-problem.js +20 -0
  20. package/package.json +2 -1
  21. package/src/components/ChoiceGroup.css +53 -3
  22. package/src/components/ChoiceGroup.test.tsx +88 -0
  23. package/src/components/ChoiceGroup.tsx +70 -28
  24. package/src/components/FormRows.css +53 -0
  25. package/src/components/FormRows.test.tsx +82 -0
  26. package/src/components/FormRows.tsx +39 -0
  27. package/src/components/SelectField.css +12 -4
  28. package/src/components/SelectField.test.tsx +20 -2
  29. package/src/components/SelectField.tsx +11 -3
  30. package/src/components/Spinner.css +6 -0
  31. package/src/components/Spinner.test.tsx +35 -0
  32. package/src/components/Spinner.tsx +19 -0
  33. package/src/components/TextField.css +2 -2
  34. package/src/host.ts +3 -3
  35. package/src/index.ts +7 -4
  36. package/src/lib/form-problem.test.ts +76 -0
  37. package/src/lib/form-problem.ts +22 -0
package/src/host.ts CHANGED
@@ -8,17 +8,17 @@ export * from './lib/text.js';
8
8
  export * from './lib/ui.js';
9
9
  export * from './lib/panel-state.js';
10
10
  export * from './api/blocker-table.js';
11
+ export * from './host/browser-find.js';
12
+ export * from './host/browser-read.js';
11
13
  export * from './host/cli-error.js';
12
14
  export * from './host/data-root.js';
13
- export * from './host/browser-read.js';
14
- export * from './host/browser-find.js';
15
15
  export * from './host/facts.js';
16
16
  export * from './host/git.js';
17
17
  export * from './host/http.js';
18
18
  export * from './host/long-task.js';
19
19
  export * from './host/opencli-runtime.js';
20
- export * from './host/opencli.js';
21
20
  export * from './host/opencli-window.js';
21
+ export * from './host/opencli.js';
22
22
  export * from './host/plugin-ledger.js';
23
23
  export * from './host/plugin-source.js';
24
24
  export * from './host/provision.js';
package/src/index.ts CHANGED
@@ -7,8 +7,8 @@ export * from './api/facts.js';
7
7
  export * from './api/host-write.js';
8
8
  export * from './api/long-task.js';
9
9
  export * from './api/unit-board.js';
10
- export * from './api/unit.js';
11
10
  export * from './api/unit-declaration.js';
11
+ export * from './api/unit.js';
12
12
  export * from './api/workbench-client.js';
13
13
  export * from './api/workbench-contract.js';
14
14
  export * from './components/Activity.js';
@@ -19,18 +19,20 @@ export * from './components/CardAnchor.js';
19
19
  export * from './components/CardBoundary.js';
20
20
  export * from './components/CardHead.js';
21
21
  export * from './components/ChoiceGroup.js';
22
+ export * from './components/Field.js';
23
+ export * from './components/FormRows.js';
24
+ export * from './components/Link.js';
22
25
  export * from './components/LongTaskRow.js';
23
26
  export * from './components/Note.js';
24
27
  export * from './components/ResourceStates.js';
25
28
  export * from './components/Rows.js';
26
29
  export * from './components/SaveBar.js';
27
- export * from './components/SettingsSection.js';
28
30
  export * from './components/SelectField.js';
31
+ export * from './components/SettingsSection.js';
32
+ export * from './components/Spinner.js';
29
33
  export * from './components/Stack.js';
30
34
  export * from './components/StateBlock.js';
31
35
  export * from './components/TextField.js';
32
- export * from './components/Field.js';
33
- export * from './components/Link.js';
34
36
  export * from './components/board-module.js';
35
37
  export * from './components/icons.js';
36
38
  export * from './config/constants.js';
@@ -41,6 +43,7 @@ export * from './i18n/merge.js';
41
43
  export * from './i18n/zh.js';
42
44
  export * from './lib/blocker-action.js';
43
45
  export * from './lib/busy.js';
46
+ export * from './lib/form-problem.js';
44
47
  export * from './lib/panel-state.js';
45
48
  export * from './lib/react.js';
46
49
  export * from './lib/text.js';
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { firstProblemOf, revealFirstProblem } from './form-problem.js';
3
+
4
+ function rootWith(problem: unknown): { readonly root: ParentNode; readonly queries: string[] } {
5
+ const queries: string[] = [];
6
+ const root = {
7
+ querySelector: (selector: string) => {
8
+ queries.push(selector);
9
+ return selector === '[data-wb-problem]' ? problem : null;
10
+ },
11
+ } as unknown as ParentNode;
12
+ return { root, queries };
13
+ }
14
+
15
+ interface ProblemStub {
16
+ readonly element: Element;
17
+ readonly queries: string[];
18
+ readonly scrollIntoView: ReturnType<typeof vi.fn>;
19
+ readonly focus: ReturnType<typeof vi.fn>;
20
+ }
21
+
22
+ function problemStub(options: { readonly scroll: boolean }): ProblemStub {
23
+ const queries: string[] = [];
24
+ const scrollIntoView = vi.fn();
25
+ const focus = vi.fn();
26
+ const element = {
27
+ scrollIntoView,
28
+ querySelector: (selector: string) => {
29
+ queries.push(selector);
30
+ return { focus };
31
+ },
32
+ } as unknown as Element;
33
+ if (!options.scroll) {
34
+ delete (element as { scrollIntoView?: unknown }).scrollIntoView;
35
+ }
36
+ return { element, queries, scrollIntoView, focus };
37
+ }
38
+
39
+ describe('firstProblemOf / revealFirstProblem', () => {
40
+ it('没有错误行就返回 null / false,什么也不碰', () => {
41
+ const empty = rootWith(null);
42
+ const missing = problemStub({ scroll: true });
43
+
44
+ expect(firstProblemOf(empty.root)).toBeNull();
45
+ expect(revealFirstProblem(rootWith(null).root)).toBe(false);
46
+ expect(missing.focus).not.toHaveBeenCalled();
47
+ expect(missing.scrollIntoView).not.toHaveBeenCalled();
48
+ });
49
+
50
+ it('第一处 [data-wb-problem] 就是它', () => {
51
+ const problem = problemStub({ scroll: true });
52
+ const found = rootWith(problem.element);
53
+
54
+ expect(firstProblemOf(found.root)).toBe(problem.element);
55
+ expect(found.queries).toEqual(['[data-wb-problem]']);
56
+ });
57
+
58
+ it('带进视口 + 聚焦里面第一个可聚焦控件,返回 true', () => {
59
+ const problem = problemStub({ scroll: true });
60
+
61
+ expect(revealFirstProblem(rootWith(problem.element).root)).toBe(true);
62
+ expect(problem.scrollIntoView).toHaveBeenCalledTimes(1);
63
+ expect(problem.scrollIntoView).toHaveBeenCalledWith({ block: 'nearest' });
64
+ expect(problem.queries).toHaveLength(1);
65
+ expect(problem.queries[0]).toContain('input:not([disabled])');
66
+ expect(problem.focus).toHaveBeenCalledTimes(1);
67
+ });
68
+
69
+ it('没有 scrollIntoView 也不炸(jsdom 缺它):照样聚焦、照样返回 true', () => {
70
+ const problem = problemStub({ scroll: false });
71
+
72
+ expect(() => revealFirstProblem(rootWith(problem.element).root)).not.toThrow();
73
+ expect(revealFirstProblem(rootWith(problem.element).root)).toBe(true);
74
+ expect(problem.focus).toHaveBeenCalledTimes(2);
75
+ });
76
+ });
@@ -0,0 +1,22 @@
1
+ const PROBLEM_SELECTOR = '[data-wb-problem]';
2
+
3
+ const FOCUSABLE_SELECTOR = [
4
+ 'input:not([disabled])',
5
+ 'select:not([disabled])',
6
+ 'textarea:not([disabled])',
7
+ 'button:not([disabled])',
8
+ 'a[href]',
9
+ '[tabindex]:not([tabindex="-1"])',
10
+ ].join(', ');
11
+
12
+ export function firstProblemOf(root: ParentNode): Element | null {
13
+ return root.querySelector(PROBLEM_SELECTOR);
14
+ }
15
+
16
+ export function revealFirstProblem(root: ParentNode): boolean {
17
+ const problem = firstProblemOf(root);
18
+ if (problem === null) return false;
19
+ problem.scrollIntoView?.({ block: 'nearest' });
20
+ problem.querySelector<HTMLElement>(FOCUSABLE_SELECTOR)?.focus?.();
21
+ return true;
22
+ }