@svadmin/ui 0.88.1 → 0.91.0

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 (171) hide show
  1. package/dist/app.css +4 -0
  2. package/dist/app.theme.css +4 -0
  3. package/dist/component-registry.svelte.d.ts +2 -2
  4. package/dist/components/AboutSettings.svelte +23 -4
  5. package/dist/components/AdminApp.svelte +6 -4
  6. package/dist/components/AdminApp.svelte.d.ts +3 -3
  7. package/dist/components/ApiSettings.svelte +43 -27
  8. package/dist/components/ApprovalActionCard.svelte +8 -9
  9. package/dist/components/ApprovalCenter.svelte +24 -5
  10. package/dist/components/AuditLogViewer.svelte +2 -2
  11. package/dist/components/AutoComplete.svelte +610 -0
  12. package/dist/components/AutoComplete.svelte.d.ts +25 -0
  13. package/dist/components/AutoForm.svelte +263 -18
  14. package/dist/components/AutoForm.svelte.d.ts +19 -2
  15. package/dist/components/AutoTable.svelte +135 -35
  16. package/dist/components/AutoTable.svelte.d.ts +8 -0
  17. package/dist/components/BatchActionBar.svelte +42 -14
  18. package/dist/components/BatchActionBar.svelte.d.ts +5 -0
  19. package/dist/components/BoundRecordDetailDrawer.svelte +5 -3
  20. package/dist/components/CancelTaskButton.svelte +14 -1
  21. package/dist/components/Cascader.svelte +412 -53
  22. package/dist/components/Cascader.svelte.d.ts +7 -1
  23. package/dist/components/ColorPicker.svelte +1 -1
  24. package/dist/components/ColorPicker.svelte.d.ts +1 -1
  25. package/dist/components/ComboboxField.svelte +270 -34
  26. package/dist/components/ComboboxField.svelte.d.ts +4 -0
  27. package/dist/components/CommandPalette.svelte +4 -4
  28. package/dist/components/DashboardView.svelte +73 -7
  29. package/dist/components/DashboardView.svelte.d.ts +3 -1
  30. package/dist/components/DateRangeInput.svelte +8 -1
  31. package/dist/components/DetailWorkspaceAsyncSection.svelte +107 -0
  32. package/dist/components/DetailWorkspaceAsyncSection.svelte.d.ts +36 -0
  33. package/dist/components/DetailWorkspaceSections.svelte +233 -0
  34. package/dist/components/DetailWorkspaceSections.svelte.d.ts +9 -0
  35. package/dist/components/DrawerForm.svelte +78 -9
  36. package/dist/components/DrawerForm.svelte.d.ts +3 -3
  37. package/dist/components/ErrorBoundary.svelte +1 -1
  38. package/dist/components/ErrorComponent.svelte +19 -6
  39. package/dist/components/ErrorComponent.svelte.d.ts +1 -0
  40. package/dist/components/ErrorSummary.svelte +9 -4
  41. package/dist/components/FieldRenderer.svelte +203 -90
  42. package/dist/components/FieldRenderer.svelte.d.ts +16 -1
  43. package/dist/components/FileUpload.svelte +116 -51
  44. package/dist/components/FileUpload.svelte.d.ts +7 -27
  45. package/dist/components/FileUploadField.svelte +170 -0
  46. package/dist/components/FileUploadField.svelte.d.ts +21 -0
  47. package/dist/components/FilterBuilder.svelte +13 -7
  48. package/dist/components/FilterCollectionInput.svelte +9 -7
  49. package/dist/components/FilterCollectionInput.svelte.d.ts +3 -0
  50. package/dist/components/ForgotPasswordPage.svelte +1 -1
  51. package/dist/components/Header.svelte +1 -1
  52. package/dist/components/ImportWizard.svelte +197 -28
  53. package/dist/components/ImportWizard.svelte.d.ts +5 -1
  54. package/dist/components/InferencerPanel.svelte +4 -2
  55. package/dist/components/InfiniteList.svelte +52 -9
  56. package/dist/components/JsonSchemaForm.svelte +237 -42
  57. package/dist/components/JsonSchemaForm.svelte.d.ts +10 -3
  58. package/dist/components/KanbanBoard.svelte +43 -7
  59. package/dist/components/KanbanBoard.svelte.d.ts +2 -2
  60. package/dist/components/Layout.svelte +1 -1
  61. package/dist/components/LoginPage.svelte +1 -1
  62. package/dist/components/MetricStrip.svelte +8 -3
  63. package/dist/components/MetricStrip.svelte.d.ts +1 -0
  64. package/dist/components/ModalForm.svelte +85 -9
  65. package/dist/components/ModalForm.svelte.d.ts +3 -3
  66. package/dist/components/MoneyInput.svelte +5 -3
  67. package/dist/components/MoneyInput.svelte.d.ts +4 -2
  68. package/dist/components/MultiTabKeepAlive.svelte +502 -43
  69. package/dist/components/MultiTabKeepAlive.svelte.d.ts +40 -6
  70. package/dist/components/NotificationsSettings.svelte +10 -10
  71. package/dist/components/NumberInput.svelte +5 -3
  72. package/dist/components/NumberInput.svelte.d.ts +4 -2
  73. package/dist/components/NumericInput.svelte +277 -25
  74. package/dist/components/NumericInput.svelte.d.ts +3 -1
  75. package/dist/components/PercentInput.svelte +5 -3
  76. package/dist/components/PercentInput.svelte.d.ts +4 -2
  77. package/dist/components/PermissionMatrix.svelte +44 -8
  78. package/dist/components/PermissionMatrix.svelte.d.ts +1 -0
  79. package/dist/components/ProfilePage.svelte +38 -13
  80. package/dist/components/RadioGroup.svelte +203 -4
  81. package/dist/components/RadioGroup.svelte.d.ts +1 -1
  82. package/dist/components/Rate.svelte +3 -3
  83. package/dist/components/Rate.svelte.d.ts +2 -2
  84. package/dist/components/RecordDetailDrawer.svelte +1 -1
  85. package/dist/components/RecordDetailDrawer.svelte.d.ts +1 -1
  86. package/dist/components/RegisterPage.svelte +1 -1
  87. package/dist/components/RelationPicker.svelte +39 -11
  88. package/dist/components/ResourceAccessGuard.svelte +7 -2
  89. package/dist/components/RetryTaskButton.svelte +14 -1
  90. package/dist/components/RolesSettings.svelte +74 -28
  91. package/dist/components/SecuritySettings.svelte +66 -51
  92. package/dist/components/SegmentedControl.svelte +4 -2
  93. package/dist/components/SegmentedControl.svelte.d.ts +1 -1
  94. package/dist/components/Sidebar.svelte +1 -1
  95. package/dist/components/StepsForm.svelte +60 -10
  96. package/dist/components/StepsForm.svelte.d.ts +4 -2
  97. package/dist/components/SvadminLogo.svelte +1 -0
  98. package/dist/components/SvarDataGrid.svelte +42 -24
  99. package/dist/components/SvarDataGrid.svelte.d.ts +10 -10
  100. package/dist/components/SvarResourceRead.svelte +2 -0
  101. package/dist/components/SvarResourceTableView.svelte +4 -2
  102. package/dist/components/TagsInput.svelte +3 -3
  103. package/dist/components/TagsInput.svelte.d.ts +2 -2
  104. package/dist/components/TaskDetails.svelte +22 -16
  105. package/dist/components/TaskDetails.svelte.d.ts +1 -1
  106. package/dist/components/TaskList.svelte +57 -15
  107. package/dist/components/TaskQueueDrawer.svelte +54 -34
  108. package/dist/components/Transfer.svelte +67 -5
  109. package/dist/components/Transfer.svelte.d.ts +4 -0
  110. package/dist/components/TreeSelect.svelte +337 -77
  111. package/dist/components/TreeSelect.svelte.d.ts +4 -0
  112. package/dist/components/TreeTable.svelte +6 -1
  113. package/dist/components/TwoFactorAuthPage.svelte +8 -4
  114. package/dist/components/UpdatePasswordPage.svelte +1 -1
  115. package/dist/components/VersionDiffViewer.svelte +9 -7
  116. package/dist/components/account/CompanyProfilePage.svelte +13 -12
  117. package/dist/components/account/MemberDirectory.svelte +14 -10
  118. package/dist/components/account/SettingsEnterprisePage.svelte +18 -18
  119. package/dist/components/content/AuditTimeline.svelte +8 -6
  120. package/dist/components/content/DataState.svelte +1 -1
  121. package/dist/components/content/FileList.svelte +44 -13
  122. package/dist/components/content/FileList.svelte.d.ts +4 -2
  123. package/dist/components/content/FileListRow.svelte +87 -0
  124. package/dist/components/content/FileListRow.svelte.d.ts +11 -0
  125. package/dist/components/content/ImportDropzone.svelte +109 -12
  126. package/dist/components/content/ImportDropzone.svelte.d.ts +1 -0
  127. package/dist/components/content/IntegrationCard.svelte +3 -3
  128. package/dist/components/content/IntegrationCard.svelte.d.ts +2 -0
  129. package/dist/components/content/MediaThumbnail.svelte +6 -2
  130. package/dist/components/detail-workspace.d.ts +60 -0
  131. package/dist/components/detail-workspace.js +3 -0
  132. package/dist/components/enterprise/json-schema-form.d.ts +1 -1
  133. package/dist/components/enterprise/recursive-schema-form.d.ts +15 -1
  134. package/dist/components/enterprise/recursive-schema-form.js +288 -14
  135. package/dist/components/enterprise/schema-form-references.d.ts +14 -0
  136. package/dist/components/enterprise/schema-form-references.js +316 -0
  137. package/dist/components/fields/ArrayField.svelte +178 -14
  138. package/dist/components/fields/ArrayField.svelte.d.ts +17 -3
  139. package/dist/components/fields/array-field-metadata.d.ts +4 -0
  140. package/dist/components/fields/array-field-metadata.js +36 -0
  141. package/dist/components/file-upload-reference.d.ts +11 -0
  142. package/dist/components/file-upload-reference.js +50 -0
  143. package/dist/components/file-upload-types.d.ts +32 -0
  144. package/dist/components/file-upload-types.js +1 -0
  145. package/dist/components/form-contract.upload-type-fixture.svelte +108 -0
  146. package/dist/components/form-contract.upload-type-fixture.svelte.d.ts +18 -0
  147. package/dist/components/json-schema-form-state.d.ts +19 -0
  148. package/dist/components/list-deletion.svelte.js +6 -1
  149. package/dist/components/list-state.svelte.d.ts +12 -1
  150. package/dist/components/list-state.svelte.js +56 -16
  151. package/dist/components/numeric-format.d.ts +44 -0
  152. package/dist/components/numeric-format.js +198 -0
  153. package/dist/components/record-detail.svelte.js +2 -0
  154. package/dist/components/saved-list-views.d.ts +9 -2
  155. package/dist/components/saved-list-views.js +42 -11
  156. package/dist/components/saved-list-views.svelte.d.ts +3 -0
  157. package/dist/components/saved-list-views.svelte.js +56 -7
  158. package/dist/components/task-action.svelte.d.ts +1 -0
  159. package/dist/components/task-action.svelte.js +55 -3
  160. package/dist/components/task-utils.d.ts +1 -0
  161. package/dist/components/task-utils.js +18 -0
  162. package/dist/components/ui/input/input.svelte +1 -1
  163. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  164. package/dist/components/ui/sheet/sheet.svelte +4 -1
  165. package/dist/components/version-history.d.ts +14 -0
  166. package/dist/components/version-history.js +32 -0
  167. package/dist/components/workspace-tab-state.d.ts +78 -0
  168. package/dist/components/workspace-tab-state.js +587 -0
  169. package/dist/index.d.ts +13 -1
  170. package/dist/index.js +4 -0
  171. package/package.json +5 -5
@@ -22,6 +22,10 @@
22
22
  const i18n = useTranslation();
23
23
 
24
24
  interface Props {
25
+ id?: string;
26
+ ariaLabel?: string;
27
+ describedby?: string;
28
+ invalid?: boolean;
25
29
  options?: TreeSelectOption[];
26
30
  value?: string | number | (string | number)[];
27
31
  multiple?: boolean;
@@ -39,6 +43,10 @@
39
43
  }
40
44
 
41
45
  let {
46
+ id,
47
+ ariaLabel,
48
+ describedby,
49
+ invalid = false,
42
50
  options = $bindable([]),
43
51
  value = $bindable(undefined),
44
52
  multiple = false,
@@ -55,6 +63,10 @@
55
63
  onchange,
56
64
  }: Props = $props();
57
65
 
66
+ const instanceId = $props.id();
67
+ const triggerId = $derived(id ?? `${instanceId}-trigger`);
68
+ const panelId = $derived(`${triggerId}-panel`);
69
+ const treeId = `${instanceId}-tree`;
58
70
  let open = $state(false);
59
71
  let searchQuery = $state('');
60
72
  let loadingValues = new SvelteSet<string | number>();
@@ -62,11 +74,21 @@
62
74
  node: TreeSelectOption;
63
75
  loader: NonNullable<Props['loadChildren']>;
64
76
  treeEpoch: number;
77
+ selection: SelectionAttempt | undefined;
78
+ promise: Promise<boolean>;
79
+ retrying: boolean;
65
80
  }
66
81
  let loadErrors = new SvelteMap<string | number, ChildLoad>();
67
- let expandedNodes = new SvelteSet<string | number>();
82
+ let expandedOverrides = new SvelteMap<string | number, boolean>();
68
83
  let mounted = true;
69
84
  let treeRoot: HTMLElement | undefined = $state();
85
+ let panelElement: HTMLDivElement | undefined = $state();
86
+ let triggerElement: HTMLButtonElement | undefined = $state();
87
+ let controlElement: HTMLDivElement | undefined = $state();
88
+ let searchElement: HTMLInputElement | null = $state(null);
89
+ let closedByOutsideInteraction = false;
90
+ let outsideFocusTarget: HTMLElement | undefined;
91
+ let activeTreeValue = $state<string | number>();
70
92
  let treeScrollTop = $state(0);
71
93
  let treeRevision = $state(0);
72
94
  const loads = new Map<string | number, ChildLoad>();
@@ -80,6 +102,7 @@
80
102
  }
81
103
  let selectionAttempt = $state.raw<SelectionAttempt>();
82
104
  const selectionErrors = new SvelteSet<string | number>();
105
+ const capabilityErrors = new SvelteSet<string | number>();
83
106
  let treeEpoch = 0;
84
107
  onDestroy(() => { mounted = false; loads.clear(); });
85
108
 
@@ -122,12 +145,15 @@
122
145
 
123
146
  $effect.pre(() => {
124
147
  // 外部替换根数组开启新数据版本;内部补充子节点保留当前版本。
148
+ void disabledValues;
125
149
  if (options !== observedOptions) {
126
150
  observedOptions = options;
127
151
  treeEpoch += 1;
128
152
  treeRevision += 1;
153
+ expandedOverrides.clear();
129
154
  selectionAttempt = undefined;
130
155
  selectionErrors.clear();
156
+ capabilityErrors.clear();
131
157
  }
132
158
  const loader = loadChildren;
133
159
  const unavailable = disabled;
@@ -135,36 +161,61 @@
135
161
  observedLoader = loader;
136
162
  selectionAttempt = undefined;
137
163
  selectionErrors.clear();
164
+ capabilityErrors.clear();
138
165
  }
139
- if (!multiple || onlyLeafSelectable || selectionAttempt?.value !== value) selectionAttempt = undefined;
166
+ if (!multiple || onlyLeafSelectable || selectionAttempt?.value !== value
167
+ || selectionAttempt && disabledValues.has(selectionAttempt.key)) selectionAttempt = undefined;
140
168
  for (const [key, request] of loads) {
141
- if (unavailable || request.treeEpoch !== treeEpoch || loader !== request.loader) {
169
+ if (unavailable || disabledValues.has(key)
170
+ || request.treeEpoch !== treeEpoch || loader !== request.loader) {
171
+ if (request.selection === selectionAttempt) selectionAttempt = undefined;
142
172
  loads.delete(key);
143
173
  loadingValues.delete(key);
144
174
  loadErrors.delete(key);
145
175
  }
146
176
  }
147
177
  for (const [key, request] of loadErrors) {
148
- if (unavailable || request.treeEpoch !== treeEpoch || loader !== request.loader) loadErrors.delete(key);
178
+ if (unavailable || disabledValues.has(key)
179
+ || request.treeEpoch !== treeEpoch || loader !== request.loader) loadErrors.delete(key);
180
+ }
181
+ for (const key of [...capabilityErrors]) {
182
+ const current = optionMap.get(key);
183
+ if (unavailable || disabledValues.has(key) || !current
184
+ || !subtreeLacksLoadCapability(current)) {
185
+ capabilityErrors.delete(key);
186
+ }
149
187
  }
150
188
  });
151
189
 
152
190
  function toggleExpand(nodeValue: string | number, event?: MouseEvent) {
153
191
  event?.stopPropagation();
154
- if (expandedNodes.has(nodeValue)) {
155
- expandedNodes.delete(nodeValue);
156
- } else {
157
- expandedNodes.add(nodeValue);
158
- }
192
+ const node = optionMap.get(nodeValue);
193
+ if (!mounted || !node || disabled || disabledValues.has(nodeValue) || searchQuery.trim()) return;
194
+ expandedOverrides.set(nodeValue, !isNodeExpanded(node));
159
195
  }
160
196
 
161
197
  function hasChildren(node: TreeSelectOption): boolean {
162
198
  return Boolean(node.hasChildren ?? (node.children && node.children.length > 0));
163
199
  }
164
200
 
201
+ function lacksLoadCapability(node: TreeSelectOption): boolean {
202
+ const current = optionMap.get(node.value) ?? node;
203
+ return hasChildren(current) && !current.children && !loadChildren;
204
+ }
205
+
206
+ // 祖先反馈只有在整个可交互子树仍存在缺失加载能力的分支时保留。
207
+ function subtreeLacksLoadCapability(node: TreeSelectOption, depth = 0): boolean {
208
+ const current = optionMap.get(node.value) ?? node;
209
+ if (current.disabled || depth > 64) return false;
210
+ if (hasChildren(current) && !current.children) return !loadChildren;
211
+ return (current.children ?? []).some(child => subtreeLacksLoadCapability(child, depth + 1));
212
+ }
213
+
165
214
  function isNodeExpanded(node: TreeSelectOption): boolean {
166
215
  if (searchQuery.trim()) return true; // Auto expand all on search
167
- return expandedNodes.has(node.value) || !!node.expanded;
216
+ return expandedOverrides.has(node.value)
217
+ ? expandedOverrides.get(node.value) === true
218
+ : !!node.expanded;
168
219
  }
169
220
 
170
221
  function selectableLeaves(node: TreeSelectOption): TreeSelectOption[] {
@@ -215,46 +266,80 @@
215
266
  return changed ? next : nodes;
216
267
  }
217
268
 
218
- async function ensureChildren(node: TreeSelectOption): Promise<boolean> {
219
- if (!mounted || disabled || disabledValues.has(node.value) || loadingValues.has(node.value)) return false;
269
+ async function ensureChildren(node: TreeSelectOption, selection?: SelectionAttempt): Promise<boolean> {
270
+ if (!mounted || disabled || disabledValues.has(node.value)) return false;
271
+ const existing = loads.get(node.value);
272
+ if (existing) {
273
+ if (selection) existing.selection = selection;
274
+ return existing.promise;
275
+ }
276
+ if (loadingValues.has(node.value)) return false;
220
277
  if (!node.hasChildren || node.children) return true;
221
278
  const currentNode = optionMap.get(node.value);
222
279
  if (!loadChildren || !currentNode) return false;
223
- const request = { node: currentNode, loader: loadChildren, treeEpoch };
280
+ const request: ChildLoad = {
281
+ node: currentNode,
282
+ loader: loadChildren,
283
+ treeEpoch,
284
+ selection,
285
+ promise: Promise.resolve(false),
286
+ retrying: false,
287
+ };
224
288
  loads.set(node.value, request);
225
289
  const current = () => mounted && !disabled && loads.get(node.value) === request
226
290
  && options === observedOptions && treeEpoch === request.treeEpoch
227
291
  && optionMap.has(node.value) && !disabledValues.has(node.value) && loadChildren === request.loader;
228
292
  loadingValues.add(node.value);
229
293
  loadErrors.delete(node.value);
230
- try {
231
- const children = await request.loader(request.node);
232
- if (!current()) return false;
233
- options = updateOptionsChildren(options, node.value, children);
234
- observedOptions = options;
235
- return true;
236
- } catch {
237
- if (current()) loadErrors.set(node.value, request);
238
- return false;
239
- } finally {
240
- if (mounted && loads.get(node.value) === request) {
241
- loads.delete(node.value);
242
- loadingValues.delete(node.value);
294
+ // 展开和级联选择共享包含归属校验、写入与清理的完整请求结果。
295
+ request.promise = (async () => {
296
+ try {
297
+ const children = await request.loader(request.node);
298
+ if (!current()) return false;
299
+ options = updateOptionsChildren(options, node.value, children);
300
+ observedOptions = options;
301
+ return true;
302
+ } catch {
303
+ if (current()) loadErrors.set(node.value, request);
304
+ return false;
305
+ } finally {
306
+ if (mounted && loads.get(node.value) === request) {
307
+ loads.delete(node.value);
308
+ loadingValues.delete(node.value);
309
+ }
243
310
  }
244
- }
311
+ })();
312
+ return request.promise;
245
313
  }
246
314
 
247
315
  async function toggleExpandAsync(node: TreeSelectOption, event?: MouseEvent): Promise<void> {
248
316
  event?.stopPropagation();
317
+ if (!mounted || disabled || disabledValues.has(node.value)) return;
318
+ if (loadingValues.has(node.value)) return;
319
+ if (lacksLoadCapability(node)) {
320
+ capabilityErrors.add(node.value);
321
+ return;
322
+ }
323
+ capabilityErrors.delete(node.value);
249
324
  if (!await ensureChildren(node)) return;
250
325
  if (!mounted || disabled) return;
251
326
  toggleExpand(node.value);
252
327
  }
253
328
 
254
- function retryChildren(node: TreeSelectOption, event: MouseEvent): void {
329
+ function ownerTreeItem(event: Event): HTMLElement | null {
330
+ return event.currentTarget instanceof HTMLElement
331
+ ? event.currentTarget.closest<HTMLElement>('[role="treeitem"]')
332
+ : null;
333
+ }
334
+
335
+ function retryChildren(node: TreeSelectOption, event: Event): void {
255
336
  event.stopPropagation();
337
+ const owner = ownerTreeItem(event);
338
+ owner?.focus();
256
339
  if (selectionErrors.has(node.value)) void handleSelect(node);
257
340
  else void toggleExpandAsync(node);
341
+ const request = loads.get(node.value);
342
+ if (request) request.retrying = true;
258
343
  }
259
344
 
260
345
  function setMultipleSelection(node: TreeSelectOption): void {
@@ -272,6 +357,7 @@
272
357
  function isCurrentSelection(attempt: SelectionAttempt): boolean {
273
358
  return mounted && !disabled && multiple && !onlyLeafSelectable
274
359
  && selectionAttempt === attempt && attempt.epoch === treeEpoch
360
+ && !disabledValues.has(attempt.key)
275
361
  && options === observedOptions && attempt.loader === loadChildren && attempt.value === value;
276
362
  }
277
363
 
@@ -280,8 +366,10 @@
280
366
  const attempt: SelectionAttempt = { key: node.value, epoch: treeEpoch, loader: loadChildren, value };
281
367
  selectionAttempt = attempt;
282
368
  selectionErrors.delete(node.value);
369
+ capabilityErrors.delete(node.value);
283
370
  let visited = 0;
284
371
  let requests = 0;
372
+ let missingCapability = false;
285
373
  // 级联加载有界且只在全部成功后写入选择;搜索裁剪不改变选择范围。
286
374
  async function loadSubtree(key: string | number, depth: number): Promise<boolean> {
287
375
  if (!isCurrentSelection(attempt)) return false;
@@ -289,7 +377,11 @@
289
377
  if (!currentNode || ++visited > 10000 || depth > 64) return false;
290
378
  if (currentNode.disabled) return true;
291
379
  if (currentNode.hasChildren && !currentNode.children) {
292
- if (++requests > 100 || !await ensureChildren(currentNode)) return false;
380
+ if (lacksLoadCapability(currentNode)) {
381
+ missingCapability = true;
382
+ return false;
383
+ }
384
+ if (++requests > 100 || !await ensureChildren(currentNode, attempt)) return false;
293
385
  }
294
386
  if (!isCurrentSelection(attempt)) return false;
295
387
  const loaded = optionMap.get(key);
@@ -301,7 +393,10 @@
301
393
  }
302
394
  try {
303
395
  if (!await loadSubtree(node.value, 0)) {
304
- if (isCurrentSelection(attempt)) selectionErrors.add(node.value);
396
+ if (isCurrentSelection(attempt)) {
397
+ if (missingCapability) capabilityErrors.add(node.value);
398
+ else selectionErrors.add(node.value);
399
+ }
305
400
  return;
306
401
  }
307
402
  if (isCurrentSelection(attempt)) {
@@ -361,6 +456,10 @@
361
456
  return `${typeof value}:${String(value)}`;
362
457
  }
363
458
 
459
+ function nodeId(value: string | number): string {
460
+ return `${instanceId}-node-${treeKey(value).split('').map(unit => unit.charCodeAt(0).toString(16)).join('-')}`;
461
+ }
462
+
364
463
  async function focusTreeNode(valueToFocus: string | number): Promise<void> {
365
464
  const index = flatVisibleNodes.findIndex(entry => entry.node.value === valueToFocus);
366
465
  if (index < 0) return;
@@ -372,7 +471,7 @@
372
471
  await tick();
373
472
  }
374
473
  }
375
- if (!mounted) return;
474
+ if (!mounted || !open) return;
376
475
  const element = treeRoot?.querySelector<HTMLElement>(`[data-tree-key="${CSS.escape(treeKey(valueToFocus))}"]`);
377
476
  element?.focus();
378
477
  }
@@ -383,12 +482,73 @@
383
482
  if (target) void focusTreeNode(target.node.value);
384
483
  }
385
484
 
485
+ async function handleOpenAutoFocus(event: Event): Promise<void> {
486
+ event.preventDefault();
487
+ closedByOutsideInteraction = false;
488
+ outsideFocusTarget = undefined;
489
+ await tick();
490
+ const panel = panelElement;
491
+ if (!mounted || !open || !panel || !panel.isConnected || panel.contains(document.activeElement)) return;
492
+ if (searchable && searchElement) {
493
+ searchElement.focus();
494
+ return;
495
+ }
496
+ const first = flatVisibleNodes[0];
497
+ if (first) await focusTreeNode(first.node.value);
498
+ if (!mounted || !open || panelElement !== panel || !panel.isConnected) return;
499
+ if (!panel.contains(document.activeElement)) panel.focus();
500
+ }
501
+
502
+ async function handleCloseAutoFocus(event: Event): Promise<void> {
503
+ event.preventDefault();
504
+ const preserveOutsideFocus = closedByOutsideInteraction;
505
+ const focusTarget = outsideFocusTarget;
506
+ await tick();
507
+ if (!mounted || open) return;
508
+ if (preserveOutsideFocus) {
509
+ const active = document.activeElement;
510
+ if (focusTarget?.isConnected
511
+ && (active === document.body || controlElement?.contains(active) || panelElement?.contains(active))) {
512
+ focusTarget.focus();
513
+ }
514
+ return;
515
+ }
516
+ triggerElement?.focus();
517
+ }
518
+
519
+ function handleInteractOutside(event: Event): void {
520
+ // 控制区内的触发/清空仍回焦本实例,真正的外部操作保留新焦点。
521
+ if (event.target instanceof Node && !controlElement?.contains(event.target)) {
522
+ closedByOutsideInteraction = true;
523
+ outsideFocusTarget = event.target instanceof HTMLElement ? event.target : undefined;
524
+ }
525
+ }
526
+
527
+ function handleWindowFocusIn(event: FocusEvent): void {
528
+ if (!open || !(event.target instanceof HTMLElement)) return;
529
+ if (panelElement?.contains(event.target) || controlElement?.contains(event.target)) return;
530
+ closedByOutsideInteraction = true;
531
+ outsideFocusTarget = event.target;
532
+ open = false;
533
+ }
534
+
535
+ function handleSearchKeydown(event: KeyboardEvent): void {
536
+ if (event.key !== 'ArrowDown' || event.isComposing || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
537
+ const first = flatVisibleNodes[0];
538
+ if (!first) return;
539
+ event.preventDefault();
540
+ void focusTreeNode(first.node.value);
541
+ }
542
+
386
543
  function handleTreeKeydown(event: KeyboardEvent, node: TreeSelectOption, expanded: boolean): void {
387
- if (event.target !== event.currentTarget) return;
388
- const nodeHasChildren = hasChildren(node);
544
+ if (event.target !== event.currentTarget || event.isComposing || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
545
+ const sourceNode = optionMap.get(node.value) ?? node;
546
+ const nodeHasChildren = hasChildren(sourceNode);
389
547
  if (event.key === 'Enter' || event.key === ' ') {
390
548
  event.preventDefault();
391
- void handleSelect(node);
549
+ if (loads.get(node.value)?.retrying) return;
550
+ if (loadErrors.has(node.value) || selectionErrors.has(node.value)) retryChildren(sourceNode, event);
551
+ else void handleSelect(node);
392
552
  } else if (event.key === 'ArrowDown') {
393
553
  event.preventDefault();
394
554
  focusSibling(node.value, 1);
@@ -405,7 +565,12 @@
405
565
  if (last) void focusTreeNode(last.node.value);
406
566
  } else if (event.key === 'ArrowRight' && nodeHasChildren) {
407
567
  event.preventDefault();
408
- if (!expanded) void toggleExpandAsync(node);
568
+ if (disabled || disabledValues.has(sourceNode.value)) return;
569
+ if (searchQuery.trim() && !sourceNode.children) {
570
+ if (lacksLoadCapability(sourceNode)) capabilityErrors.add(sourceNode.value);
571
+ else void ensureChildren(sourceNode);
572
+ }
573
+ else if (!expanded) void toggleExpandAsync(sourceNode);
409
574
  else {
410
575
  const index = flatVisibleNodes.findIndex(entry => entry.node.value === node.value);
411
576
  const current = flatVisibleNodes[index];
@@ -426,20 +591,24 @@
426
591
  event?.stopPropagation();
427
592
  if (disabled) return;
428
593
  selectionAttempt = undefined;
594
+ open = false;
429
595
  const next = multiple ? [] : undefined;
430
596
  value = next;
431
597
  onchange?.(next);
598
+ triggerElement?.focus();
432
599
  }
433
600
 
434
601
  function removeSingle(val: string | number, event?: MouseEvent | KeyboardEvent) {
435
602
  event?.stopPropagation();
436
603
  if (disabled) return;
437
604
  selectionAttempt = undefined;
605
+ open = false;
438
606
  if (multiple) {
439
607
  const next = selectedValues.filter((v) => v !== val);
440
608
  value = next;
441
609
  onchange?.(next);
442
610
  }
611
+ triggerElement?.focus();
443
612
  }
444
613
 
445
614
  function filterTree(nodes: TreeSelectOption[], query: string): TreeSelectOption[] {
@@ -488,6 +657,13 @@
488
657
  const clampedScrollTop = $derived(Math.max(0, Math.min(treeScrollTop, totalHeight - windowHeight)));
489
658
  const virtualStart = $derived(Math.max(0, Math.floor(clampedScrollTop / rowHeight) - 5));
490
659
  const virtualEnd = $derived(Math.min(flatVisibleNodes.length, Math.ceil((clampedScrollTop + windowHeight) / rowHeight) + 5));
660
+ const renderedNodes = $derived(shouldVirtualize
661
+ ? flatVisibleNodes.slice(virtualStart, virtualEnd)
662
+ : flatVisibleNodes);
663
+ // 当前节点被过滤、折叠或滚出渲染窗口时,仍保留一个有效的 Tab 入口。
664
+ const tabStopValue = $derived(renderedNodes.some(entry => entry.node.value === activeTreeValue)
665
+ ? activeTreeValue
666
+ : renderedNodes[0]?.node.value);
491
667
 
492
668
  $effect(() => {
493
669
  // 搜索和外部数据替换从顶部开始;折叠或数据缩短只钳制窗口。
@@ -511,11 +687,17 @@
511
687
  {@const partial = isPartiallySelected(node)}
512
688
  {@const busy = selectionAttempt?.key === node.value || loadingValues.has(node.value)}
513
689
  {@const unavailable = disabled || disabledValues.has(node.value)}
690
+ {@const currentNodeId = nodeId(node.value)}
691
+ {@const hasRenderedGroup = recursive && nodeHasChildren && expanded && !!node.children?.length}
514
692
 
515
693
  <div class="svadmin-u-60fbb7713999 svadmin-u-8dddea0773ed">
516
694
  <div
517
695
  role="treeitem"
518
- tabindex="0"
696
+ id={currentNodeId}
697
+ tabindex={tabStopValue === node.value ? 0 : -1}
698
+ aria-label={node.label}
699
+ aria-owns={hasRenderedGroup ? `${currentNodeId}-group` : undefined}
700
+ aria-describedby={loadErrors.has(node.value) || selectionErrors.has(node.value) || capabilityErrors.has(node.value) ? `${currentNodeId}-error` : undefined}
519
701
  aria-selected={selected}
520
702
  aria-checked={multiple ? (partial ? 'mixed' : selected) : undefined}
521
703
  aria-disabled={unavailable ? 'true' : undefined}
@@ -533,17 +715,22 @@
533
715
  )}
534
716
  style={`padding-left:${level * 16 + 8}px;${recursive ? '' : `height:${rowHeight}px;min-height:${rowHeight}px;box-sizing:border-box;overflow:hidden;`}`}
535
717
  onclick={() => void handleSelect(node)}
718
+ onfocus={() => { activeTreeValue = node.value; }}
536
719
  onkeydown={(e) => handleTreeKeydown(e, node, expanded)}
537
720
  >
538
721
  <div class="svadmin-u-60fbb7713999 svadmin-u-7e0b7cdf1a94 svadmin-u-3960ffc248d9 svadmin-u-58284b4ea568">
539
722
  {#if nodeHasChildren}
540
723
  <button
541
724
  type="button"
725
+ tabindex="-1"
542
726
  disabled={unavailable}
543
727
  aria-label={`${node.label}: ${i18n.t('tree.toggleChildren')}`}
544
728
  aria-busy={busy}
545
729
  class="svadmin-u-60fbb7713999 svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3 svadmin-u-012fbd121f37 svadmin-u-3960ffc248d9 svadmin-u-86843cf1e227 svadmin-u-07389a777c1f svadmin-u-bfa603190748 svadmin-u-8e551981c8d7"
546
- onclick={(e) => void toggleExpandAsync(node, e)}
730
+ onclick={(e) => {
731
+ ownerTreeItem(e)?.focus();
732
+ void toggleExpandAsync(node, e);
733
+ }}
547
734
  >
548
735
  {#if busy}
549
736
  <Loader2 class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c svadmin-u-afbdd13a380e" />
@@ -560,13 +747,16 @@
560
747
  <span class="svadmin-u-f283ea9bea0e">{node.label}</span>
561
748
  </div>
562
749
 
563
- {#if loadErrors.has(node.value) || selectionErrors.has(node.value)}
564
- <span role="alert" class="svadmin-u-bfa603190748">{i18n.t(selectionErrors.has(node.value) ? 'tree.selectionFailed' : 'tree.loadFailed')}</span>
565
- <button type="button" aria-label={`${node.label}: ${i18n.t('common.retry')}`}
566
- disabled={unavailable}
567
- title={i18n.t('common.retry')} onclick={(e) => retryChildren(node, e)}>
568
- <RotateCw class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
569
- </button>
750
+ {#if loadErrors.has(node.value) || selectionErrors.has(node.value) || capabilityErrors.has(node.value)}
751
+ <span id={`${currentNodeId}-error`} role="alert" class="svadmin-u-bfa603190748">{i18n.t(capabilityErrors.has(node.value) ? 'tree.childrenUnavailable' : selectionErrors.has(node.value) ? 'tree.selectionFailed' : 'tree.loadFailed')}</span>
752
+ {#if !capabilityErrors.has(node.value)}
753
+ <button type="button" aria-label={`${node.label}: ${i18n.t('common.retry')}`}
754
+ tabindex="-1"
755
+ disabled={unavailable}
756
+ title={i18n.t('common.retry')} onclick={(e) => retryChildren(node, e)}>
757
+ <RotateCw class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
758
+ </button>
759
+ {/if}
570
760
  {/if}
571
761
 
572
762
  {#if partial}
@@ -577,7 +767,7 @@
577
767
  </div>
578
768
 
579
769
  {#if recursive && nodeHasChildren && expanded && node.children}
580
- <div role="group" class="svadmin-u-60fbb7713999 svadmin-u-8dddea0773ed">
770
+ <div id={`${currentNodeId}-group`} role="group" class="svadmin-u-60fbb7713999 svadmin-u-8dddea0773ed">
581
771
  {#each node.children as child, index (treeKey(child.value))}
582
772
  {@render treeNode(child, level + 1, index + 1, node.children.length)}
583
773
  {/each}
@@ -586,20 +776,39 @@
586
776
  </div>
587
777
  {/snippet}
588
778
 
779
+ <svelte:window onfocusin={handleWindowFocusIn} />
780
+
589
781
  <div class={cn('svadmin-u-d89972fe17d6 svadmin-u-6da6a3c3f741', className)} data-testid="tree-select">
590
782
  <Popover.Root bind:open>
591
- <Popover.Trigger class="svadmin-u-6da6a3c3f741">
783
+ <Popover.Trigger id={triggerId} {disabled} class="svadmin-u-6da6a3c3f741">
592
784
  {#snippet child({ props })}
593
- <button
594
- type="button"
595
- {...props}
596
- {disabled}
785
+ <div bind:this={controlElement}
786
+ data-disabled={disabled || undefined}
787
+ data-invalid={invalid || undefined}
597
788
  class={cn(
789
+ 'tree-select-control',
598
790
  'svadmin-u-60fbb7713999 svadmin-u-968a1e649bad svadmin-u-6da6a3c3f741 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22 svadmin-u-e6f9e383a762 svadmin-u-0e17f2bd9074 svadmin-u-ec0091ee009b svadmin-u-359090c2d529 svadmin-u-582e6ef4b245 svadmin-u-9c24ab70af61 svadmin-u-674ec2b09890 svadmin-u-608dd26cd5ba svadmin-u-80b9d0ae125f svadmin-u-6b22a22a9752 svadmin-u-5f533b3a7de7 svadmin-u-b29d8adbad2e svadmin-u-2eba0d65d059',
599
791
  open && 'svadmin-u-16b1efa5875e svadmin-u-3e1868fc53e2 svadmin-u-0c15f6cff5a0'
600
792
  )}
601
793
  >
602
- <div class="svadmin-u-60fbb7713999 svadmin-u-1eb5c6df38c1 svadmin-u-3960ffc248d9 svadmin-u-44ee8ba0a421 svadmin-u-7e0b7cdf1a94 svadmin-u-36e579c0b41c">
794
+ <button
795
+ {...props}
796
+ type="button"
797
+ bind:this={triggerElement}
798
+ aria-label={ariaLabel?.trim() ? ariaLabel : undefined}
799
+ aria-controls={open ? panelId : undefined}
800
+ aria-describedby={describedby}
801
+ data-invalid={invalid || undefined}
802
+ {disabled}
803
+ class="tree-select-trigger"
804
+ >
805
+ <span class="svadmin-u-2daa8e5e2f2e">
806
+ {selectedLabels.length ? selectedLabels.join(', ') : placeholder ?? i18n.t('field.selectPlaceholder')}
807
+ </span>
808
+ </button>
809
+ <div
810
+ class="tree-select-display svadmin-u-60fbb7713999 svadmin-u-1eb5c6df38c1 svadmin-u-3960ffc248d9 svadmin-u-44ee8ba0a421 svadmin-u-7e0b7cdf1a94 svadmin-u-36e579c0b41c"
811
+ >
603
812
  {#if selectedValues.length === 0}
604
813
  <span class="svadmin-u-bfa603190748">
605
814
  {placeholder ?? (i18n.t('field.selectPlaceholder', undefined) ?? '请选择...')}
@@ -610,21 +819,14 @@
610
819
  <Badge variant="secondary" class="svadmin-u-cd0d9c512cdc svadmin-u-45d828117213 svadmin-u-68ecb30dbec6 svadmin-u-d058ca6de60f svadmin-u-44ee8ba0a421 svadmin-u-012fbd121f37">
611
820
  <span>{opt?.label ?? val}</span>
612
821
  {#if !disabled}
613
- <span
614
- role="button"
615
- tabindex="0"
822
+ <button type="button" tabindex="0"
616
823
  aria-label={i18n.t('tree.removeSelection', { label: opt?.label ?? String(val) })}
617
- class="svadmin-u-51e95020d6f2 svadmin-u-34516836730d"
824
+ title={i18n.t('tree.removeSelection', { label: opt?.label ?? String(val) })}
825
+ class="tree-select-action svadmin-u-51e95020d6f2 svadmin-u-34516836730d"
618
826
  onclick={(e) => removeSingle(val, e)}
619
- onkeydown={(e) => {
620
- if (e.key === 'Enter' || e.key === ' ') {
621
- e.preventDefault();
622
- removeSingle(val, e);
623
- }
624
- }}
625
827
  >
626
828
  <X class="svadmin-u-6a60c09e6aaa svadmin-u-9cea05671a29" />
627
- </span>
829
+ </button>
628
830
  {/if}
629
831
  </Badge>
630
832
  {/each}
@@ -635,37 +837,42 @@
635
837
  {/if}
636
838
  </div>
637
839
 
638
- <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-44ee8ba0a421 svadmin-u-012fbd121f37 svadmin-u-f58b02572ab2 svadmin-u-bfa603190748">
840
+ <div
841
+ class="tree-select-display svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-44ee8ba0a421 svadmin-u-012fbd121f37 svadmin-u-f58b02572ab2 svadmin-u-bfa603190748"
842
+ >
639
843
  {#if allowClear && selectedValues.length > 0 && !disabled}
640
- <span
641
- role="button"
642
- tabindex="0"
844
+ <button type="button" tabindex="0"
643
845
  aria-label={i18n.t('common.clearSelection')}
644
- class="svadmin-u-ea7b2e9e070e svadmin-u-34516836730d svadmin-u-de8350a3bbad"
846
+ title={i18n.t('common.clearSelection')}
847
+ class="tree-select-action svadmin-u-ea7b2e9e070e svadmin-u-34516836730d svadmin-u-de8350a3bbad"
645
848
  onclick={clearAll}
646
- onkeydown={(e) => {
647
- if (e.key === 'Enter' || e.key === ' ') {
648
- e.preventDefault();
649
- clearAll(e);
650
- }
651
- }}
652
849
  >
653
850
  <X class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
654
- </span>
851
+ </button>
655
852
  {/if}
656
853
  <ChevronsUpDown class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c svadmin-u-0b8c506a0596" />
657
854
  </div>
658
- </button>
855
+ </div>
659
856
  {/snippet}
660
857
  </Popover.Trigger>
661
858
 
662
- <Popover.Content class="svadmin-u-4827635fb99f svadmin-u-cbc20887874e svadmin-u-7b17becb34a1 svadmin-u-7660b450905a" align="start">
859
+ <Popover.Content id={panelId} role="dialog" aria-labelledby={triggerId}
860
+ onOpenAutoFocus={handleOpenAutoFocus} onCloseAutoFocus={handleCloseAutoFocus}
861
+ onInteractOutside={handleInteractOutside}
862
+ class="svadmin-u-4827635fb99f svadmin-u-cbc20887874e svadmin-u-7b17becb34a1 svadmin-u-7660b450905a" align="start">
863
+ {#snippet child({ props, wrapperProps })}
864
+ <div {...wrapperProps}>
865
+ <div {...props} id={panelId} bind:this={panelElement}>
663
866
  {#if searchable}
664
867
  <div class="svadmin-u-d89972fe17d6 svadmin-u-a77ed4d908c0">
665
868
  <Search class="svadmin-u-da4dbfbc4fdc svadmin-u-ecfeb742a3f3 svadmin-u-7a470f4c9b28 svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c svadmin-u-bfa603190748" />
666
869
  <Input
667
870
  type="text"
871
+ bind:ref={searchElement}
668
872
  bind:value={searchQuery}
873
+ aria-label={i18n.t('common.search')}
874
+ aria-controls={treeId}
875
+ onkeydown={handleSearchKeydown}
669
876
  placeholder={i18n.t('common.search', undefined) ?? '搜索...'}
670
877
  class="svadmin-u-ed8a5df7b2fb svadmin-u-e4af885410fe svadmin-u-aa2c13a5e1b4 svadmin-u-359090c2d529"
671
878
  />
@@ -674,7 +881,10 @@
674
881
 
675
882
  <div
676
883
  bind:this={treeRoot}
884
+ id={treeId}
677
885
  role="tree"
886
+ tabindex="-1"
887
+ aria-label={ariaLabel ?? placeholder ?? i18n.t('field.selectPlaceholder')}
678
888
  aria-multiselectable={multiple ? 'true' : undefined}
679
889
  class="svadmin-u-67d7e383dca6 svadmin-u-92bf82f493b1 svadmin-u-e2eedc5718f0 svadmin-u-eda955402ba6"
680
890
  style={shouldVirtualize ? `height:${windowHeight}px;max-height:${windowHeight}px;overflow-y:auto;padding:0;` : undefined}
@@ -683,7 +893,7 @@
683
893
  {#if shouldVirtualize}
684
894
  <div style={`height:${totalHeight}px;position:relative;`}>
685
895
  <div style={`position:absolute;top:${virtualStart * rowHeight}px;left:0;right:0;`}>
686
- {#each flatVisibleNodes.slice(virtualStart, virtualEnd) as entry (treeKey(entry.node.value))}
896
+ {#each renderedNodes as entry (treeKey(entry.node.value))}
687
897
  {@render treeNode(entry.node, entry.level, entry.position, entry.size, false)}
688
898
  {/each}
689
899
  </div>
@@ -698,6 +908,56 @@
698
908
  </div>
699
909
  {/if}
700
910
  </div>
911
+ </div>
912
+ </div>
913
+ {/snippet}
701
914
  </Popover.Content>
702
915
  </Popover.Root>
703
916
  </div>
917
+
918
+ <style>
919
+ .tree-select-control {
920
+ position: relative;
921
+ isolation: isolate;
922
+ min-inline-size: 0;
923
+ }
924
+ .tree-select-trigger {
925
+ position: absolute;
926
+ inset: 0;
927
+ z-index: 0;
928
+ width: 100%;
929
+ height: 100%;
930
+ border: 0;
931
+ border-radius: inherit;
932
+ background: transparent;
933
+ cursor: pointer;
934
+ }
935
+ .tree-select-trigger:focus-visible,
936
+ .tree-select-action:focus-visible {
937
+ outline: 2px solid var(--ring);
938
+ outline-offset: 2px;
939
+ }
940
+ .tree-select-control[data-disabled] {
941
+ opacity: 0.5;
942
+ }
943
+ .tree-select-trigger:disabled {
944
+ cursor: not-allowed;
945
+ }
946
+ .tree-select-display {
947
+ position: relative;
948
+ z-index: 1;
949
+ pointer-events: none;
950
+ min-inline-size: 0;
951
+ }
952
+ .tree-select-action {
953
+ pointer-events: auto;
954
+ display: inline-flex;
955
+ align-items: center;
956
+ justify-content: center;
957
+ padding: 0;
958
+ border: 0;
959
+ background: transparent;
960
+ color: inherit;
961
+ cursor: pointer;
962
+ }
963
+ </style>