@djangocfg/ui-tools 2.1.465 → 2.1.468

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.
@@ -219,6 +219,7 @@ function TreeRootShell<T>({
219
219
  // nav, type-ahead, programmatic). Centralised so every focus source gets
220
220
  // consistent scrolling — previously only type-ahead scrolled.
221
221
  const focusedId = ctx.focused;
222
+ const isEmpty = ctx.flatRows.length === 0;
222
223
  useEffect(() => {
223
224
  if (!focusedId) return;
224
225
  const el = containerRef.current?.querySelector<HTMLElement>(
@@ -249,11 +250,15 @@ function TreeRootShell<T>({
249
250
  const treeBody = (
250
251
  <div
251
252
  ref={setContainerRef}
252
- tabIndex={0}
253
- role="tree"
254
- aria-label={ctx.labels.ariaLabel}
255
- aria-multiselectable={ctx.selectionMode === 'multiple' || undefined}
256
- aria-activedescendant={focusedId ? treeRowDomId(focusedId) : undefined}
253
+ tabIndex={isEmpty ? undefined : 0}
254
+ role={isEmpty ? undefined : 'tree'}
255
+ aria-label={isEmpty ? undefined : ctx.labels.ariaLabel}
256
+ aria-multiselectable={
257
+ !isEmpty && ctx.selectionMode === 'multiple' ? true : undefined
258
+ }
259
+ aria-activedescendant={
260
+ !isEmpty && focusedId ? treeRowDomId(focusedId) : undefined
261
+ }
257
262
  className={cn(
258
263
  'group/tree flex h-full w-full flex-col gap-2 rounded-sm outline-none',
259
264
  'focus-visible:ring-1 focus-visible:ring-ring/50',
@@ -264,7 +269,9 @@ function TreeRootShell<T>({
264
269
  >
265
270
  {enableSearch ? <TreeSearchInput className="mx-2 mt-2" /> : null}
266
271
  <div className="flex min-h-0 flex-1 flex-col overflow-auto px-1">
267
- <TreeContent<T> role="group">{renderRow}</TreeContent>
272
+ <TreeContent<T> role={isEmpty ? "presentation" : "group"}>
273
+ {renderRow}
274
+ </TreeContent>
268
275
  {/* Empty-area: catches right-clicks on whitespace below the
269
276
  last row + acts as the root drop target for DnD. Always
270
277
  rendered — it self-disables when there's nothing to do. */}
@@ -0,0 +1,36 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { act } from 'react';
4
+ import { createRoot } from 'react-dom/client';
5
+ import { afterEach, describe, expect, it } from 'vitest';
6
+
7
+ import { TreeRoot } from '../TreeRoot';
8
+
9
+ (globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
10
+
11
+ describe('TreeRoot empty semantics', () => {
12
+ const mounted: Array<ReturnType<typeof createRoot>> = [];
13
+
14
+ afterEach(() => {
15
+ act(() => mounted.splice(0).forEach((root) => root.unmount()));
16
+ });
17
+
18
+ it('announces the empty state without exposing an invalid empty tree', () => {
19
+ const container = document.createElement('div');
20
+ const root = createRoot(container);
21
+ mounted.push(root);
22
+
23
+ act(() => {
24
+ root.render(
25
+ <TreeRoot<{ name: string }>
26
+ data={[]}
27
+ getItemName={(node) => node.data.name}
28
+ />,
29
+ );
30
+ });
31
+
32
+ expect(container.querySelector('[role="tree"]')).toBeNull();
33
+ expect(container.querySelector('[data-tree-root]')?.hasAttribute('tabindex')).toBe(false);
34
+ expect(container.querySelector('[role="status"]')?.textContent).toBe('Nothing to show');
35
+ });
36
+ });
@@ -10,6 +10,7 @@ export interface TreeEmptyProps {
10
10
  export function TreeEmpty({ children, className }: TreeEmptyProps) {
11
11
  return (
12
12
  <div
13
+ role="status"
13
14
  className={cn(
14
15
  'flex h-full min-h-32 items-center justify-center px-4 py-6 text-sm text-muted-foreground',
15
16
  className,