@adea-ai/ui 0.136.0 → 0.138.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.
@@ -1,4 +1,4 @@
1
1
  /** Keyboard-accessible tree rows with virtualization-aware focus handoff. */
2
- export { Tree, TreeRow, TreeRowActions, type TreeProps, type TreeRowProps } from './tree';
2
+ export { Tree, TreeRow, TreeRowActions, type TreeDisclosure, type TreeProps, type TreeRowProps, } from './tree';
3
3
  export type { TreeItemDescriptor } from './tree-navigation';
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/composites/tree/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,cAAc,EAAE,KAAK,SAAS,EAAE,KAAK,YAAY,EAAE,MAAM,QAAQ,CAAA;AACzF,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/composites/tree/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,OAAO,EACL,IAAI,EACJ,OAAO,EACP,cAAc,EACd,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,YAAY,GAClB,MAAM,QAAQ,CAAA;AACf,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA"}
@@ -1,5 +1,17 @@
1
1
  import type { ComponentProps, JSX } from 'solid-js';
2
2
  import { type TreeItemDescriptor } from './tree-navigation';
3
+ /**
4
+ * Who owns the pointer on a row's disclosure chevron.
5
+ *
6
+ * - `row` (default): the chevron is decoration; a click anywhere on the row,
7
+ * chevron included, is a row activation. This is the file-explorer
8
+ * convention, where the host's `onActivate` toggles a folder.
9
+ * - `marker`: the chevron of an expandable row is its own pointer target. It
10
+ * calls `onExpand(id, !expanded)` and nothing else; the rest of the row still
11
+ * activates. For trees where expanding and selecting are independent (a
12
+ * source list that selects a source and separately reveals its details).
13
+ */
14
+ export type TreeDisclosure = 'row' | 'marker';
3
15
  type TreeAccessibleName = {
4
16
  'aria-label': string;
5
17
  'aria-labelledby'?: string;
@@ -12,10 +24,19 @@ export type TreeProps = Omit<ComponentProps<'div'>, 'children' | 'role' | 'tabIn
12
24
  visibleItems: readonly TreeItemDescriptor[];
13
25
  /** The host-owned identity of the row that currently owns roving focus. */
14
26
  activeId: string | null;
15
- /** Called when keyboard or pointer navigation changes the active row. */
27
+ /**
28
+ * Called when keyboard or pointer navigation changes the active row, and when
29
+ * the active row leaves `visibleItems` (collapsed away, filtered out) and the
30
+ * first visible row takes over as the roving stop.
31
+ */
16
32
  onActiveIdChange: (id: string) => void;
17
33
  /** Called for Left/Right expand and collapse requests. */
18
34
  onExpand?: (id: string, expanded: boolean) => void;
35
+ /**
36
+ * Opt-in pointer behaviour of the disclosure chevron; see {@link TreeDisclosure}.
37
+ * Keyboard behaviour is identical in both modes: Left/Right expand and collapse.
38
+ */
39
+ disclosure?: TreeDisclosure;
19
40
  /** Called for Enter or pointer activation. Domain action stays host-owned. */
20
41
  onActivate?: (id: string, event: Event) => void;
21
42
  /** Called for Space selection. Selection state stays host-owned. */
@@ -39,6 +60,20 @@ export type TreeProps = Omit<ComponentProps<'div'>, 'children' | 'role' | 'tabIn
39
60
  *
40
61
  * Kobalte 0.13.14 has no Tree primitive, so this component implements the
41
62
  * WAI-ARIA tree row navigation contract and leaves product state to callbacks.
63
+ *
64
+ * By default the disclosure chevron is decoration and a click anywhere on a
65
+ * row activates it, so a file explorer can toggle a folder from `onActivate`.
66
+ * Reach for `disclosure="marker"` when expanding and selecting are separate
67
+ * decisions: the chevron then calls only `onExpand`, and the rest of the row
68
+ * calls only `onActivate`. The chevron stays `aria-hidden` and out of the tab
69
+ * order in both modes; keyboard expansion is Left/Right either way.
70
+ *
71
+ * Collapsing is the one thing that can move focus without a key or a click on
72
+ * the target row: when the active row leaves `visibleItems`, the first visible
73
+ * row becomes the roving stop and `onActiveIdChange` is called with it. If the
74
+ * removed row held focus, or was the last row the tree saw focused, focus moves
75
+ * to that same row; if it had no focus, only the roving stop moves. This is the
76
+ * same in both `disclosure` modes and for every way a host collapses a row.
42
77
  */
43
78
  export declare function Tree(props: TreeProps): JSX.Element;
44
79
  export type TreeRowProps = Omit<ComponentProps<'div'>, 'children' | 'role' | 'tabIndex' | 'onClick' | 'onFocus' | 'onDoubleClick' | 'style' | 'aria-level' | 'aria-expanded' | 'aria-selected' | 'aria-posinset' | 'aria-setsize'> & {
@@ -58,6 +93,15 @@ export type TreeRowProps = Omit<ComponentProps<'div'>, 'children' | 'role' | 'ta
58
93
  * Keeps the donor's disclosure marker, leading file/folder affordance, primary
59
94
  * label and trailing-action hierarchy while replacing the donor's button and
60
95
  * runtime callbacks with a shared treeitem and host-owned slots/actions.
96
+ *
97
+ * With `Tree`'s `disclosure="marker"` the chevron of an expandable row is a
98
+ * separate pointer target (`data-slot="tree-row-disclosure"`) that only calls
99
+ * `onExpand`. It is not focusable and has no role: the row remains the single
100
+ * focus stop, Enter and Space still activate and select, and Left/Right still
101
+ * expand and collapse, so the marker adds a pointer path and nothing else.
102
+ * Pressing it does not move focus or the roving stop to its row; if the toggle
103
+ * hides the focused row, `Tree` recovers focus as it does for any collapse (see
104
+ * the `Tree` comment and docs/tree.md).
61
105
  */
62
106
  export declare function TreeRow(props: TreeRowProps): JSX.Element;
63
107
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../../../src/components/composites/tree/tree.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAYnD,OAAO,EAGL,KAAK,kBAAkB,EAExB,MAAM,mBAAmB,CAAA;AAsB1B,KAAK,kBAAkB,GACnB;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,CAAA;AAExD,MAAM,MAAM,SAAS,GAAG,IAAI,CAC1B,cAAc,CAAC,KAAK,CAAC,EACrB,UAAU,GAAG,MAAM,GAAG,UAAU,GAAG,WAAW,GAAG,YAAY,GAAG,iBAAiB,CAClF,GACC,kBAAkB,GAAG;IACnB,0EAA0E;IAC1E,YAAY,EAAE,SAAS,kBAAkB,EAAE,CAAA;IAC3C,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,yEAAyE;IACzE,gBAAgB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAA;IAClD,8EAA8E;IAC9E,UAAU,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAC/C,oEAAoE;IACpE,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IACjD,4EAA4E;IAC5E,eAAe,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,sFAAsF;IACtF,eAAe,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IACzD,4DAA4D;IAC5D,aAAa,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAA;IACrC,QAAQ,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;CACvB,CAAA;AAEH;;;;;;;;;;;GAWG;AACH,wBAAgB,IAAI,CAAC,KAAK,EAAE,SAAS,eA2KpC;AAED,MAAM,MAAM,YAAY,GAAG,IAAI,CAC7B,cAAc,CAAC,KAAK,CAAC,EACnB,UAAU,GACV,MAAM,GACN,UAAU,GACV,SAAS,GACT,SAAS,GACT,eAAe,GACf,OAAO,GACP,YAAY,GACZ,eAAe,GACf,eAAe,GACf,eAAe,GACf,cAAc,CACjB,GAAG;IACF,IAAI,EAAE,kBAAkB,CAAA;IACxB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;IACrB,QAAQ,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;IACtB,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAA;IACrB,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG;QAAE,aAAa,EAAE,cAAc,CAAA;KAAE,KAAK,IAAI,CAAA;CAC5F,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,YAAY,eAsG1C;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,cAAc,CAAC,MAAM,CAAC,GAAG;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,eAclF;AAsBD,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA"}
1
+ {"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../../../src/components/composites/tree/tree.tsx"],"names":[],"mappings":"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"}
@@ -1,11 +1,11 @@
1
1
  import { cn } from "../../../lib/utils.js";
2
2
  import { indexTreeItems, treeNavigationAction } from "./tree-navigation.js";
3
- import { createComponent, insert, memo, mergeProps, spread, template, use } from "solid-js/web";
3
+ import { addEventListener, className, createComponent, delegateEvents, effect, insert, memo, mergeProps, setAttribute, spread, template, use } from "solid-js/web";
4
4
  import { ChevronRight } from "lucide-solid";
5
5
  import { createContext, createEffect, createMemo, createUniqueId, onCleanup, splitProps, useContext } from "solid-js";
6
6
  //#region src/components/composites/tree/tree.tsx
7
7
  var _tmpl$ = /*#__PURE__*/ template(`<div>`);
8
- var _tmpl$2 = /*#__PURE__*/ template(`<div><span class="flex size-3.5 shrink-0 items-center justify-center text-muted-foreground"></span><span class="min-w-0 flex-1 truncate">`);
8
+ var _tmpl$2 = /*#__PURE__*/ template(`<div><span></span><span class="min-w-0 flex-1 truncate">`);
9
9
  var _tmpl$3 = /*#__PURE__*/ template(`<span class="flex size-4 shrink-0 items-center justify-center [&amp;_svg]:size-4">`);
10
10
  var _tmpl$4 = /*#__PURE__*/ template(`<span class="ms-auto flex shrink-0 items-center gap-1 text-muted-foreground">`);
11
11
  var _tmpl$5 = /*#__PURE__*/ template(`<span data-slot=tree-row-actions>`);
@@ -26,6 +26,20 @@ function useTreeContext() {
26
26
  *
27
27
  * Kobalte 0.13.14 has no Tree primitive, so this component implements the
28
28
  * WAI-ARIA tree row navigation contract and leaves product state to callbacks.
29
+ *
30
+ * By default the disclosure chevron is decoration and a click anywhere on a
31
+ * row activates it, so a file explorer can toggle a folder from `onActivate`.
32
+ * Reach for `disclosure="marker"` when expanding and selecting are separate
33
+ * decisions: the chevron then calls only `onExpand`, and the rest of the row
34
+ * calls only `onActivate`. The chevron stays `aria-hidden` and out of the tab
35
+ * order in both modes; keyboard expansion is Left/Right either way.
36
+ *
37
+ * Collapsing is the one thing that can move focus without a key or a click on
38
+ * the target row: when the active row leaves `visibleItems`, the first visible
39
+ * row becomes the roving stop and `onActiveIdChange` is called with it. If the
40
+ * removed row held focus, or was the last row the tree saw focused, focus moves
41
+ * to that same row; if it had no focus, only the roving stop moves. This is the
42
+ * same in both `disclosure` modes and for every way a host collapses a row.
29
43
  */
30
44
  function Tree(props) {
31
45
  const [local, rest] = splitProps(props, [
@@ -34,6 +48,7 @@ function Tree(props) {
34
48
  "activeId",
35
49
  "onActiveIdChange",
36
50
  "onExpand",
51
+ "disclosure",
37
52
  "onActivate",
38
53
  "onSelectItem",
39
54
  "onRequestReveal",
@@ -117,6 +132,8 @@ function Tree(props) {
117
132
  focusedId = id;
118
133
  },
119
134
  activate: (id, event) => local.onActivate?.(id, event),
135
+ expand: (id, expanded) => local.onExpand?.(id, expanded),
136
+ disclosure: () => local.disclosure ?? "row",
120
137
  onActiveIdChange: local.onActiveIdChange
121
138
  };
122
139
  createEffect(() => {
@@ -171,6 +188,15 @@ function Tree(props) {
171
188
  * Keeps the donor's disclosure marker, leading file/folder affordance, primary
172
189
  * label and trailing-action hierarchy while replacing the donor's button and
173
190
  * runtime callbacks with a shared treeitem and host-owned slots/actions.
191
+ *
192
+ * With `Tree`'s `disclosure="marker"` the chevron of an expandable row is a
193
+ * separate pointer target (`data-slot="tree-row-disclosure"`) that only calls
194
+ * `onExpand`. It is not focusable and has no role: the row remains the single
195
+ * focus stop, Enter and Space still activate and select, and Left/Right still
196
+ * expand and collapse, so the marker adds a pointer path and nothing else.
197
+ * Pressing it does not move focus or the roving stop to its row; if the toggle
198
+ * hides the focused row, `Tree` recovers focus as it does for any collapse (see
199
+ * the `Tree` comment and docs/tree.md).
174
200
  */
175
201
  function TreeRow(props) {
176
202
  const tree = useTreeContext();
@@ -214,6 +240,11 @@ function TreeRow(props) {
214
240
  local.onDoubleClick?.(id(), event);
215
241
  };
216
242
  const position = () => tree.index().positionById.get(id());
243
+ const markerTarget = () => tree.disclosure() === "marker" && local.item.expandable;
244
+ const handleMarkerClick = (event) => {
245
+ event.stopPropagation();
246
+ tree.expand(id(), !local.item.expanded);
247
+ };
217
248
  return (() => {
218
249
  var _el$2 = _tmpl$2(), _el$3 = _el$2.firstChild, _el$4 = _el$3.nextSibling;
219
250
  var _ref$ = composeRefs(setRef, () => local.ref);
@@ -266,6 +297,9 @@ function TreeRow(props) {
266
297
  "onClick": handleClick,
267
298
  "onDblClick": handleDoubleClick
268
299
  }), false, true);
300
+ addEventListener(_el$3, "dblclick", markerTarget() ? handleMarkerDoubleClick : void 0, true);
301
+ addEventListener(_el$3, "click", markerTarget() ? handleMarkerClick : void 0, true);
302
+ addEventListener(_el$3, "mousedown", markerTarget() ? handleMarkerMouseDown : void 0, true);
269
303
  insert(_el$3, (() => {
270
304
  var _c$ = memo(() => !!local.item.expandable);
271
305
  return () => _c$() ? createComponent(ChevronRight, {
@@ -292,6 +326,15 @@ function TreeRow(props) {
292
326
  return _el$6;
293
327
  })() : null;
294
328
  })(), null);
329
+ effect((_p$) => {
330
+ var _v$ = markerTarget() ? "tree-row-disclosure" : void 0, _v$2 = cn("flex", !markerTarget() && "size-3.5", "shrink-0 items-center justify-center text-muted-foreground", markerTarget() && "relative w-3.5 cursor-pointer self-stretch hover:text-foreground after:absolute after:inset-y-0 after:-inset-x-1.5");
331
+ _v$ !== _p$.e && setAttribute(_el$3, "data-slot", _p$.e = _v$);
332
+ _v$2 !== _p$.t && className(_el$3, _p$.t = _v$2);
333
+ return _p$;
334
+ }, {
335
+ e: void 0,
336
+ t: void 0
337
+ });
295
338
  return _el$2;
296
339
  })();
297
340
  }
@@ -319,6 +362,8 @@ function TreeRowActions(props) {
319
362
  return _el$7;
320
363
  })();
321
364
  }
365
+ var handleMarkerMouseDown = (event) => event.preventDefault();
366
+ var handleMarkerDoubleClick = (event) => event.stopPropagation();
322
367
  function isActionTarget(target, currentTarget) {
323
368
  if (!(target instanceof Element) || target === currentTarget) return false;
324
369
  return Boolean(target.closest("a, button, input, textarea, select, [contenteditable=\"true\"], [role=\"button\"], [data-tree-keyboard-stop]"));
@@ -330,6 +375,11 @@ function composeRefs(internal, external) {
330
375
  if (typeof ref === "function") ref(element);
331
376
  };
332
377
  }
378
+ delegateEvents([
379
+ "mousedown",
380
+ "click",
381
+ "dblclick"
382
+ ]);
333
383
  //#endregion
334
384
  export { Tree, TreeRow, TreeRowActions };
335
385
 
@@ -1 +1 @@
1
- {"version":3,"file":"tree.js","names":["ComponentProps","JSX","ChevronRight","createContext","createEffect","createMemo","createUniqueId","onCleanup","splitProps","useContext","cn","indexTreeItems","treeNavigationAction","TreeItemDescriptor","TreeNavigationIndex","TreeContextValue","idPrefix","activeId","index","register","id","element","HTMLDivElement","unregister","focusItem","noteFocus","activate","event","Event","onActiveIdChange","TreeContext","useTreeContext","tree","Error","TreeAccessibleName","TreeProps","Omit","visibleItems","onExpand","expanded","onActivate","onSelectItem","onRequestReveal","onRowSizeChange","blockSize","selectionMode","children","Element","Tree","props","local","rest","effectiveActiveId","candidate","visible","byId","has","ids","rows","Map","rowIdsByElement","WeakMap","pendingFocusId","requestedRevealId","focusedId","disposed","rowSizeObserver","ResizeObserver","undefined","entries","entry","get","target","borderBoxSize","HTMLElement","offsetHeight","Number","isFinite","disconnect","requestReveal","focusWhenMounted","queueMicrotask","row","document","focus","activeElement","context","set","observe","focusWasInside","Boolean","contains","delete","unobserve","fallbackId","handleKeyDown","EventHandler","KeyboardEvent","defaultPrevented","isActionTarget","currentTarget","action","key","preventDefault","type","_$createComponent","Provider","value","_el$","_tmpl$","_$spread","_$mergeProps","aria-multiselectable","class","_$insert","TreeRowProps","item","selected","leading","trailing","onDoubleClick","MouseEvent","TreeRow","registeredId","setRef","nextId","handleFocus","handleClick","handleDoubleClick","position","positionById","_el$2","_tmpl$2","_el$3","firstChild","_el$4","nextSibling","_ref$","composeRefs","ref","_$use","encodeURIComponent","aria-level","level","aria-expanded","_$memo","expandable","aria-selected","aria-posinset","aria-setsize","setSize","tabIndex","data-tree-id","data-active","data-selected","data-expanded","String","style","Math","max","_c$","_c$2","_el$5","_tmpl$3","_c$3","_el$6","_tmpl$4","TreeRowActions","pinned","_el$7","_tmpl$5","data-pinned","EventTarget","closest","internal","external","_$createComponent","_$mergeProps","_$use","_$memo"],"sources":["../../../../src/components/composites/tree/tree.tsx"],"sourcesContent":["import type { ComponentProps, JSX } from 'solid-js'\nimport { ChevronRight } from 'lucide-solid'\nimport {\n createContext,\n createEffect,\n createMemo,\n createUniqueId,\n onCleanup,\n splitProps,\n useContext,\n} from 'solid-js'\nimport { cn } from '#lib/utils'\nimport {\n indexTreeItems,\n treeNavigationAction,\n type TreeItemDescriptor,\n type TreeNavigationIndex,\n} from './tree-navigation'\n\ntype TreeContextValue = {\n idPrefix: string\n activeId: () => string | null\n index: () => TreeNavigationIndex\n register: (id: string, element: HTMLDivElement) => void\n unregister: (id: string, element: HTMLDivElement | undefined) => void\n focusItem: (id: string) => void\n noteFocus: (id: string) => void\n activate: (id: string, event: Event) => void\n onActiveIdChange: (id: string) => void\n}\n\nconst TreeContext = createContext<TreeContextValue>()\n\nfunction useTreeContext() {\n const tree = useContext(TreeContext)\n if (!tree) throw new Error('[adea-ui]: TreeRow must be used inside Tree.')\n return tree\n}\n\ntype TreeAccessibleName =\n | { 'aria-label': string; 'aria-labelledby'?: string }\n | { 'aria-label'?: string; 'aria-labelledby': string }\n\nexport type TreeProps = Omit<\n ComponentProps<'div'>,\n 'children' | 'role' | 'tabIndex' | 'onKeyDown' | 'aria-label' | 'aria-labelledby'\n> &\n TreeAccessibleName & {\n /** Complete, ordered projection of currently visible (expanded) items. */\n visibleItems: readonly TreeItemDescriptor[]\n /** The host-owned identity of the row that currently owns roving focus. */\n activeId: string | null\n /** Called when keyboard or pointer navigation changes the active row. */\n onActiveIdChange: (id: string) => void\n /** Called for Left/Right expand and collapse requests. */\n onExpand?: (id: string, expanded: boolean) => void\n /** Called for Enter or pointer activation. Domain action stays host-owned. */\n onActivate?: (id: string, event: Event) => void\n /** Called for Space selection. Selection state stays host-owned. */\n onSelectItem?: (id: string, event: Event) => void\n /** Ask the host window to mount/reveal an item before focus moves to it. */\n onRequestReveal?: (id: string) => void\n /** Report a mounted row's border-box block size in CSS pixels for host range math. */\n onRowSizeChange?: (id: string, blockSize: number) => void\n /** Declares a multi-select tree to assistive technology. */\n selectionMode?: 'single' | 'multiple'\n children?: JSX.Element\n }\n\n/**\n * Tree.\n *\n * Supplies semantic and keyboard behavior for a tree whose host owns the\n * visible projection, filesystem state, row window and domain actions. The\n * mounted children can be a small virtualized window; `visibleItems` remains\n * the complete ordered projection so keyboard movement and sibling positions\n * do not depend on which rows currently exist in the DOM.\n *\n * Kobalte 0.13.14 has no Tree primitive, so this component implements the\n * WAI-ARIA tree row navigation contract and leaves product state to callbacks.\n */\nexport function Tree(props: TreeProps) {\n const [local, rest] = splitProps(props, [\n 'class',\n 'visibleItems',\n 'activeId',\n 'onActiveIdChange',\n 'onExpand',\n 'onActivate',\n 'onSelectItem',\n 'onRequestReveal',\n 'onRowSizeChange',\n 'selectionMode',\n 'children',\n ])\n\n const index = createMemo(() => indexTreeItems(local.visibleItems))\n const effectiveActiveId = createMemo(() => {\n const candidate = local.activeId\n const visible = index()\n return candidate !== null && visible.byId.has(candidate) ? candidate : (visible.ids[0] ?? null)\n })\n const idPrefix = `tree-${createUniqueId()}`\n const rows = new Map<string, HTMLDivElement>()\n const rowIdsByElement = new WeakMap<HTMLDivElement, string>()\n let pendingFocusId: string | null = null\n let requestedRevealId: string | null = null\n let focusedId: string | null = null\n let disposed = false\n const rowSizeObserver =\n typeof ResizeObserver === 'undefined'\n ? undefined\n : new ResizeObserver((entries) => {\n if (disposed) return\n for (const entry of entries) {\n const id = rowIdsByElement.get(entry.target as HTMLDivElement)\n const blockSize =\n entry.borderBoxSize[0]?.blockSize ?? (entry.target as HTMLElement).offsetHeight\n if (id !== undefined && Number.isFinite(blockSize) && blockSize > 0)\n local.onRowSizeChange?.(id, blockSize)\n }\n })\n\n onCleanup(() => {\n disposed = true\n rowSizeObserver?.disconnect()\n })\n\n const requestReveal = (id: string) => {\n if (requestedRevealId === id) return\n requestedRevealId = id\n local.onRequestReveal?.(id)\n }\n\n const focusWhenMounted = (id: string) => {\n queueMicrotask(() => {\n if (pendingFocusId !== id || effectiveActiveId() !== id) return\n const row = rows.get(id)\n if (!row || typeof document === 'undefined') return\n row.focus()\n if (document.activeElement === row) pendingFocusId = null\n })\n }\n\n const focusItem = (id: string) => {\n pendingFocusId = id\n local.onActiveIdChange(id)\n if (rows.has(id)) requestedRevealId = null\n else requestReveal(id)\n focusWhenMounted(id)\n }\n\n const context: TreeContextValue = {\n idPrefix,\n activeId: effectiveActiveId,\n index,\n register: (id, element) => {\n rows.set(id, element)\n rowIdsByElement.set(element, id)\n rowSizeObserver?.observe(element)\n if (requestedRevealId === id) requestedRevealId = null\n if (pendingFocusId === id) focusWhenMounted(id)\n },\n unregister: (id, element) => {\n if (rows.get(id) !== element) return\n const focusWasInside =\n !disposed &&\n Boolean(\n element && typeof document !== 'undefined' && element.contains(document.activeElement)\n )\n rows.delete(id)\n if (element) {\n rowIdsByElement.delete(element)\n rowSizeObserver?.unobserve(element)\n }\n if (requestedRevealId === id) requestedRevealId = null\n if (focusWasInside) {\n queueMicrotask(() => {\n const fallbackId = effectiveActiveId()\n if (!disposed && fallbackId !== null && pendingFocusId !== fallbackId)\n focusItem(fallbackId)\n })\n }\n },\n focusItem,\n noteFocus: (id) => {\n focusedId = id\n },\n activate: (id, event) => local.onActivate?.(id, event),\n onActiveIdChange: local.onActiveIdChange,\n }\n\n // A filtered or removed active ID falls back to the first visible item. This\n // keeps one treeitem in the sequential tab order while notifying the host.\n createEffect(() => {\n const activeId = effectiveActiveId()\n if (activeId === null) return\n // If a host filter removes the row that had DOM focus, move focus to the\n // same projection fallback that now owns the roving tab stop.\n if (focusedId !== null && !index().byId.has(focusedId) && pendingFocusId !== activeId) {\n focusItem(activeId)\n } else if (local.activeId !== activeId) {\n local.onActiveIdChange(activeId)\n }\n // Preserve the active row in a caller-owned virtual window, including when\n // the pane mounts with its saved active row outside the initial render range.\n if (!rows.has(activeId)) requestReveal(activeId)\n if (pendingFocusId === activeId) focusWhenMounted(activeId)\n })\n\n const handleKeyDown: JSX.EventHandler<HTMLDivElement, KeyboardEvent> = (event) => {\n if (event.defaultPrevented || isActionTarget(event.target, event.currentTarget)) return\n\n const action = treeNavigationAction(index(), effectiveActiveId(), event.key)\n if (!action) return\n event.preventDefault()\n\n switch (action.type) {\n case 'focus':\n focusItem(action.id)\n break\n case 'expand':\n local.onExpand?.(action.id, action.expanded)\n break\n case 'activate':\n local.onActivate?.(action.id, event)\n break\n case 'select':\n local.onSelectItem?.(action.id, event)\n break\n }\n }\n\n return (\n <TreeContext.Provider value={context}>\n <div\n {...rest}\n role=\"tree\"\n aria-multiselectable={local.selectionMode === 'multiple' ? 'true' : undefined}\n data-slot=\"tree\"\n class={cn(\n /* A virtualized host remounts rows as it scrolls; scroll anchoring\n would latch onto the focused row and fight the scrollbar. */\n 'flex min-w-0 flex-col [overflow-anchor:none]',\n local.class\n )}\n onKeyDown={handleKeyDown}\n >\n {local.children}\n </div>\n </TreeContext.Provider>\n )\n}\n\nexport type TreeRowProps = Omit<\n ComponentProps<'div'>,\n | 'children'\n | 'role'\n | 'tabIndex'\n | 'onClick'\n | 'onFocus'\n | 'onDoubleClick'\n | 'style'\n | 'aria-level'\n | 'aria-expanded'\n | 'aria-selected'\n | 'aria-posinset'\n | 'aria-setsize'\n> & {\n item: TreeItemDescriptor\n /** Host-owned selection state; omitted for non-selectable trees. */\n selected?: boolean\n leading?: JSX.Element\n trailing?: JSX.Element\n children: JSX.Element\n onDoubleClick?: (id: string, event: MouseEvent & { currentTarget: HTMLDivElement }) => void\n}\n\n/**\n * TreeRow.\n *\n * Keeps the donor's disclosure marker, leading file/folder affordance, primary\n * label and trailing-action hierarchy while replacing the donor's button and\n * runtime callbacks with a shared treeitem and host-owned slots/actions.\n */\nexport function TreeRow(props: TreeRowProps) {\n const tree = useTreeContext()\n const [local, rest] = splitProps(props, [\n 'class',\n 'item',\n 'selected',\n 'leading',\n 'trailing',\n 'children',\n 'onDoubleClick',\n 'ref',\n ])\n\n let row: HTMLDivElement | undefined\n let registeredId: string | undefined\n const id = () => local.item.id\n\n onCleanup(() => tree.unregister(registeredId ?? id(), row))\n\n const setRef = (element: HTMLDivElement) => {\n row = element\n registeredId = id()\n tree.register(registeredId, element)\n }\n\n createEffect(() => {\n const nextId = id()\n if (!row || registeredId === nextId) return\n if (registeredId !== undefined) tree.unregister(registeredId, row)\n registeredId = nextId\n tree.register(nextId, row)\n })\n\n const handleFocus = () => {\n tree.noteFocus(id())\n if (tree.activeId() !== id()) tree.onActiveIdChange(id())\n }\n\n const handleClick: JSX.EventHandler<HTMLDivElement, MouseEvent> = (event) => {\n if (isActionTarget(event.target, event.currentTarget)) return\n tree.focusItem(id())\n tree.activate(id(), event)\n }\n\n const handleDoubleClick: JSX.EventHandler<HTMLDivElement, MouseEvent> = (event) => {\n if (isActionTarget(event.target, event.currentTarget)) return\n local.onDoubleClick?.(id(), event)\n }\n\n const position = () => tree.index().positionById.get(id())\n\n return (\n <div\n {...rest}\n ref={composeRefs(setRef, () => local.ref)}\n id={rest.id ?? `${tree.idPrefix}-item-${encodeURIComponent(id())}`}\n role=\"treeitem\"\n aria-level={local.item.level}\n aria-expanded={local.item.expandable ? (local.item.expanded ? 'true' : 'false') : undefined}\n aria-selected={local.selected === undefined ? undefined : local.selected ? 'true' : 'false'}\n aria-posinset={position()?.position}\n aria-setsize={position()?.setSize}\n tabIndex={tree.activeId() === id() ? 0 : -1}\n data-tree-id={id()}\n data-active={tree.activeId() === id() ? '' : undefined}\n data-selected={local.selected ? '' : undefined}\n data-expanded={local.item.expandable ? String(local.item.expanded) : undefined}\n class={cn(\n 'group/tree-row flex min-h-row-sm w-full min-w-0 items-center gap-1.5 rounded-md pe-1.5 text-left text-sm outline-none transition-colors',\n 'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',\n {\n 'bg-primary-subtle text-foreground': local.selected,\n 'hover:bg-surface-hover': !local.selected,\n },\n local.class\n )}\n style={{ 'padding-inline-start': `${6 + Math.max(0, local.item.level - 1) * 12}px` }}\n onFocus={handleFocus}\n onClick={handleClick}\n onDblClick={handleDoubleClick}\n >\n <span class=\"flex size-3.5 shrink-0 items-center justify-center text-muted-foreground\">\n {local.item.expandable ? (\n <ChevronRight\n aria-hidden=\"true\"\n class={cn('size-3.5 transition-transform', local.item.expanded && 'rotate-90')}\n />\n ) : null}\n </span>\n {local.leading ? (\n <span class=\"flex size-4 shrink-0 items-center justify-center [&_svg]:size-4\">\n {local.leading}\n </span>\n ) : null}\n <span class=\"min-w-0 flex-1 truncate\">{local.children}</span>\n {local.trailing ? (\n <span class=\"ms-auto flex shrink-0 items-center gap-1 text-muted-foreground\">\n {local.trailing}\n </span>\n ) : null}\n </div>\n )\n}\n\n/**\n * TreeRowActions.\n *\n * Row-level commands (rename, copy, delete) that stay out of the way until the\n * row is hovered or holds focus, so a long tree reads as names rather than a\n * wall of buttons. Put it in `TreeRow`'s `trailing` slot beside any status that\n * must stay visible. `pinned` keeps the actions shown, e.g. while a destructive\n * action waits for its confirming second press.\n */\nexport function TreeRowActions(props: ComponentProps<'span'> & { pinned?: boolean }) {\n const [local, rest] = splitProps(props, ['class', 'pinned'])\n return (\n <span\n data-slot=\"tree-row-actions\"\n data-pinned={local.pinned ? '' : undefined}\n class={cn(\n 'flex shrink-0 items-center gap-1 opacity-0 transition-opacity ease-out',\n 'group-hover/tree-row:opacity-100 group-focus-within/tree-row:opacity-100 data-pinned:opacity-100',\n local.class\n )}\n {...rest}\n />\n )\n}\n\nfunction isActionTarget(target: EventTarget | null, currentTarget: HTMLElement): boolean {\n if (!(target instanceof Element) || target === currentTarget) return false\n return Boolean(\n target.closest(\n 'a, button, input, textarea, select, [contenteditable=\"true\"], [role=\"button\"], [data-tree-keyboard-stop]'\n )\n )\n}\n\nfunction composeRefs(\n internal: (element: HTMLDivElement) => void,\n external: () => ComponentProps<'div'>['ref']\n): (element: HTMLDivElement) => void {\n return (element) => {\n internal(element)\n const ref = external()\n if (typeof ref === 'function') ref(element)\n }\n}\n\nexport type { TreeItemDescriptor } from './tree-navigation'\n"],"mappings":"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"}
1
+ {"version":3,"file":"tree.js","names":["ComponentProps","JSX","ChevronRight","createContext","createEffect","createMemo","createUniqueId","onCleanup","splitProps","useContext","cn","indexTreeItems","treeNavigationAction","TreeItemDescriptor","TreeNavigationIndex","TreeContextValue","idPrefix","activeId","index","register","id","element","HTMLDivElement","unregister","focusItem","noteFocus","activate","event","Event","expand","expanded","disclosure","TreeDisclosure","onActiveIdChange","TreeContext","useTreeContext","tree","Error","TreeAccessibleName","TreeProps","Omit","visibleItems","onExpand","onActivate","onSelectItem","onRequestReveal","onRowSizeChange","blockSize","selectionMode","children","Element","Tree","props","local","rest","effectiveActiveId","candidate","visible","byId","has","ids","rows","Map","rowIdsByElement","WeakMap","pendingFocusId","requestedRevealId","focusedId","disposed","rowSizeObserver","ResizeObserver","undefined","entries","entry","get","target","borderBoxSize","HTMLElement","offsetHeight","Number","isFinite","disconnect","requestReveal","focusWhenMounted","queueMicrotask","row","document","focus","activeElement","context","set","observe","focusWasInside","Boolean","contains","delete","unobserve","fallbackId","handleKeyDown","EventHandler","KeyboardEvent","defaultPrevented","isActionTarget","currentTarget","action","key","preventDefault","type","_$createComponent","Provider","value","_el$","_tmpl$","_$spread","_$mergeProps","aria-multiselectable","class","_$insert","TreeRowProps","item","selected","leading","trailing","onDoubleClick","MouseEvent","TreeRow","registeredId","setRef","nextId","handleFocus","handleClick","handleDoubleClick","position","positionById","markerTarget","expandable","handleMarkerClick","HTMLSpanElement","stopPropagation","_el$2","_tmpl$2","_el$3","firstChild","_el$4","nextSibling","_ref$","composeRefs","ref","_$use","encodeURIComponent","aria-level","level","aria-expanded","_$memo","aria-selected","aria-posinset","aria-setsize","setSize","tabIndex","data-tree-id","data-active","data-selected","data-expanded","String","style","Math","max","_$addEventListener","handleMarkerDoubleClick","handleMarkerMouseDown","_c$","_c$2","_el$5","_tmpl$3","_c$3","_el$6","_tmpl$4","_$effect","_p$","_v$","_v$2","e","_$setAttribute","t","_$className","TreeRowActions","pinned","_el$7","_tmpl$5","data-pinned","EventTarget","closest","internal","external","_$delegateEvents","_$createComponent","_$mergeProps","_$use","_$memo","_$setAttribute","_$className","_$delegateEvents"],"sources":["../../../../src/components/composites/tree/tree.tsx"],"sourcesContent":["import type { ComponentProps, JSX } from 'solid-js'\nimport { ChevronRight } from 'lucide-solid'\nimport {\n createContext,\n createEffect,\n createMemo,\n createUniqueId,\n onCleanup,\n splitProps,\n useContext,\n} from 'solid-js'\nimport { cn } from '#lib/utils'\nimport {\n indexTreeItems,\n treeNavigationAction,\n type TreeItemDescriptor,\n type TreeNavigationIndex,\n} from './tree-navigation'\n\ntype TreeContextValue = {\n idPrefix: string\n activeId: () => string | null\n index: () => TreeNavigationIndex\n register: (id: string, element: HTMLDivElement) => void\n unregister: (id: string, element: HTMLDivElement | undefined) => void\n focusItem: (id: string) => void\n noteFocus: (id: string) => void\n activate: (id: string, event: Event) => void\n expand: (id: string, expanded: boolean) => void\n disclosure: () => TreeDisclosure\n onActiveIdChange: (id: string) => void\n}\n\nconst TreeContext = createContext<TreeContextValue>()\n\nfunction useTreeContext() {\n const tree = useContext(TreeContext)\n if (!tree) throw new Error('[adea-ui]: TreeRow must be used inside Tree.')\n return tree\n}\n\n/**\n * Who owns the pointer on a row's disclosure chevron.\n *\n * - `row` (default): the chevron is decoration; a click anywhere on the row,\n * chevron included, is a row activation. This is the file-explorer\n * convention, where the host's `onActivate` toggles a folder.\n * - `marker`: the chevron of an expandable row is its own pointer target. It\n * calls `onExpand(id, !expanded)` and nothing else; the rest of the row still\n * activates. For trees where expanding and selecting are independent (a\n * source list that selects a source and separately reveals its details).\n */\nexport type TreeDisclosure = 'row' | 'marker'\n\ntype TreeAccessibleName =\n | { 'aria-label': string; 'aria-labelledby'?: string }\n | { 'aria-label'?: string; 'aria-labelledby': string }\n\nexport type TreeProps = Omit<\n ComponentProps<'div'>,\n 'children' | 'role' | 'tabIndex' | 'onKeyDown' | 'aria-label' | 'aria-labelledby'\n> &\n TreeAccessibleName & {\n /** Complete, ordered projection of currently visible (expanded) items. */\n visibleItems: readonly TreeItemDescriptor[]\n /** The host-owned identity of the row that currently owns roving focus. */\n activeId: string | null\n /**\n * Called when keyboard or pointer navigation changes the active row, and when\n * the active row leaves `visibleItems` (collapsed away, filtered out) and the\n * first visible row takes over as the roving stop.\n */\n onActiveIdChange: (id: string) => void\n /** Called for Left/Right expand and collapse requests. */\n onExpand?: (id: string, expanded: boolean) => void\n /**\n * Opt-in pointer behaviour of the disclosure chevron; see {@link TreeDisclosure}.\n * Keyboard behaviour is identical in both modes: Left/Right expand and collapse.\n */\n disclosure?: TreeDisclosure\n /** Called for Enter or pointer activation. Domain action stays host-owned. */\n onActivate?: (id: string, event: Event) => void\n /** Called for Space selection. Selection state stays host-owned. */\n onSelectItem?: (id: string, event: Event) => void\n /** Ask the host window to mount/reveal an item before focus moves to it. */\n onRequestReveal?: (id: string) => void\n /** Report a mounted row's border-box block size in CSS pixels for host range math. */\n onRowSizeChange?: (id: string, blockSize: number) => void\n /** Declares a multi-select tree to assistive technology. */\n selectionMode?: 'single' | 'multiple'\n children?: JSX.Element\n }\n\n/**\n * Tree.\n *\n * Supplies semantic and keyboard behavior for a tree whose host owns the\n * visible projection, filesystem state, row window and domain actions. The\n * mounted children can be a small virtualized window; `visibleItems` remains\n * the complete ordered projection so keyboard movement and sibling positions\n * do not depend on which rows currently exist in the DOM.\n *\n * Kobalte 0.13.14 has no Tree primitive, so this component implements the\n * WAI-ARIA tree row navigation contract and leaves product state to callbacks.\n *\n * By default the disclosure chevron is decoration and a click anywhere on a\n * row activates it, so a file explorer can toggle a folder from `onActivate`.\n * Reach for `disclosure=\"marker\"` when expanding and selecting are separate\n * decisions: the chevron then calls only `onExpand`, and the rest of the row\n * calls only `onActivate`. The chevron stays `aria-hidden` and out of the tab\n * order in both modes; keyboard expansion is Left/Right either way.\n *\n * Collapsing is the one thing that can move focus without a key or a click on\n * the target row: when the active row leaves `visibleItems`, the first visible\n * row becomes the roving stop and `onActiveIdChange` is called with it. If the\n * removed row held focus, or was the last row the tree saw focused, focus moves\n * to that same row; if it had no focus, only the roving stop moves. This is the\n * same in both `disclosure` modes and for every way a host collapses a row.\n */\nexport function Tree(props: TreeProps) {\n const [local, rest] = splitProps(props, [\n 'class',\n 'visibleItems',\n 'activeId',\n 'onActiveIdChange',\n 'onExpand',\n 'disclosure',\n 'onActivate',\n 'onSelectItem',\n 'onRequestReveal',\n 'onRowSizeChange',\n 'selectionMode',\n 'children',\n ])\n\n const index = createMemo(() => indexTreeItems(local.visibleItems))\n const effectiveActiveId = createMemo(() => {\n const candidate = local.activeId\n const visible = index()\n return candidate !== null && visible.byId.has(candidate) ? candidate : (visible.ids[0] ?? null)\n })\n const idPrefix = `tree-${createUniqueId()}`\n const rows = new Map<string, HTMLDivElement>()\n const rowIdsByElement = new WeakMap<HTMLDivElement, string>()\n let pendingFocusId: string | null = null\n let requestedRevealId: string | null = null\n let focusedId: string | null = null\n let disposed = false\n const rowSizeObserver =\n typeof ResizeObserver === 'undefined'\n ? undefined\n : new ResizeObserver((entries) => {\n if (disposed) return\n for (const entry of entries) {\n const id = rowIdsByElement.get(entry.target as HTMLDivElement)\n const blockSize =\n entry.borderBoxSize[0]?.blockSize ?? (entry.target as HTMLElement).offsetHeight\n if (id !== undefined && Number.isFinite(blockSize) && blockSize > 0)\n local.onRowSizeChange?.(id, blockSize)\n }\n })\n\n onCleanup(() => {\n disposed = true\n rowSizeObserver?.disconnect()\n })\n\n const requestReveal = (id: string) => {\n if (requestedRevealId === id) return\n requestedRevealId = id\n local.onRequestReveal?.(id)\n }\n\n const focusWhenMounted = (id: string) => {\n queueMicrotask(() => {\n if (pendingFocusId !== id || effectiveActiveId() !== id) return\n const row = rows.get(id)\n if (!row || typeof document === 'undefined') return\n row.focus()\n if (document.activeElement === row) pendingFocusId = null\n })\n }\n\n const focusItem = (id: string) => {\n pendingFocusId = id\n local.onActiveIdChange(id)\n if (rows.has(id)) requestedRevealId = null\n else requestReveal(id)\n focusWhenMounted(id)\n }\n\n const context: TreeContextValue = {\n idPrefix,\n activeId: effectiveActiveId,\n index,\n register: (id, element) => {\n rows.set(id, element)\n rowIdsByElement.set(element, id)\n rowSizeObserver?.observe(element)\n if (requestedRevealId === id) requestedRevealId = null\n if (pendingFocusId === id) focusWhenMounted(id)\n },\n unregister: (id, element) => {\n if (rows.get(id) !== element) return\n const focusWasInside =\n !disposed &&\n Boolean(\n element && typeof document !== 'undefined' && element.contains(document.activeElement)\n )\n rows.delete(id)\n if (element) {\n rowIdsByElement.delete(element)\n rowSizeObserver?.unobserve(element)\n }\n if (requestedRevealId === id) requestedRevealId = null\n if (focusWasInside) {\n queueMicrotask(() => {\n const fallbackId = effectiveActiveId()\n if (!disposed && fallbackId !== null && pendingFocusId !== fallbackId)\n focusItem(fallbackId)\n })\n }\n },\n focusItem,\n noteFocus: (id) => {\n focusedId = id\n },\n activate: (id, event) => local.onActivate?.(id, event),\n expand: (id, expanded) => local.onExpand?.(id, expanded),\n disclosure: () => local.disclosure ?? 'row',\n onActiveIdChange: local.onActiveIdChange,\n }\n\n // A filtered, collapsed-away or removed active ID falls back to the first\n // visible item. This keeps one treeitem in the sequential tab order while\n // notifying the host.\n createEffect(() => {\n const activeId = effectiveActiveId()\n if (activeId === null) return\n // If the projection removes the last row that took focus (a host filter, or\n // an ancestor collapsing), move focus to the same fallback that now owns the\n // roving tab stop. `focusedId` is not cleared when focus leaves the tree, so\n // this also reclaims focus from a control outside it; that long-standing\n // behaviour is deliberately left as it was.\n if (focusedId !== null && !index().byId.has(focusedId) && pendingFocusId !== activeId) {\n focusItem(activeId)\n } else if (local.activeId !== activeId) {\n local.onActiveIdChange(activeId)\n }\n // Preserve the active row in a caller-owned virtual window, including when\n // the pane mounts with its saved active row outside the initial render range.\n if (!rows.has(activeId)) requestReveal(activeId)\n if (pendingFocusId === activeId) focusWhenMounted(activeId)\n })\n\n const handleKeyDown: JSX.EventHandler<HTMLDivElement, KeyboardEvent> = (event) => {\n if (event.defaultPrevented || isActionTarget(event.target, event.currentTarget)) return\n\n const action = treeNavigationAction(index(), effectiveActiveId(), event.key)\n if (!action) return\n event.preventDefault()\n\n switch (action.type) {\n case 'focus':\n focusItem(action.id)\n break\n case 'expand':\n local.onExpand?.(action.id, action.expanded)\n break\n case 'activate':\n local.onActivate?.(action.id, event)\n break\n case 'select':\n local.onSelectItem?.(action.id, event)\n break\n }\n }\n\n return (\n <TreeContext.Provider value={context}>\n <div\n {...rest}\n role=\"tree\"\n aria-multiselectable={local.selectionMode === 'multiple' ? 'true' : undefined}\n data-slot=\"tree\"\n class={cn(\n /* A virtualized host remounts rows as it scrolls; scroll anchoring\n would latch onto the focused row and fight the scrollbar. */\n 'flex min-w-0 flex-col [overflow-anchor:none]',\n local.class\n )}\n onKeyDown={handleKeyDown}\n >\n {local.children}\n </div>\n </TreeContext.Provider>\n )\n}\n\nexport type TreeRowProps = Omit<\n ComponentProps<'div'>,\n | 'children'\n | 'role'\n | 'tabIndex'\n | 'onClick'\n | 'onFocus'\n | 'onDoubleClick'\n | 'style'\n | 'aria-level'\n | 'aria-expanded'\n | 'aria-selected'\n | 'aria-posinset'\n | 'aria-setsize'\n> & {\n item: TreeItemDescriptor\n /** Host-owned selection state; omitted for non-selectable trees. */\n selected?: boolean\n leading?: JSX.Element\n trailing?: JSX.Element\n children: JSX.Element\n onDoubleClick?: (id: string, event: MouseEvent & { currentTarget: HTMLDivElement }) => void\n}\n\n/**\n * TreeRow.\n *\n * Keeps the donor's disclosure marker, leading file/folder affordance, primary\n * label and trailing-action hierarchy while replacing the donor's button and\n * runtime callbacks with a shared treeitem and host-owned slots/actions.\n *\n * With `Tree`'s `disclosure=\"marker\"` the chevron of an expandable row is a\n * separate pointer target (`data-slot=\"tree-row-disclosure\"`) that only calls\n * `onExpand`. It is not focusable and has no role: the row remains the single\n * focus stop, Enter and Space still activate and select, and Left/Right still\n * expand and collapse, so the marker adds a pointer path and nothing else.\n * Pressing it does not move focus or the roving stop to its row; if the toggle\n * hides the focused row, `Tree` recovers focus as it does for any collapse (see\n * the `Tree` comment and docs/tree.md).\n */\nexport function TreeRow(props: TreeRowProps) {\n const tree = useTreeContext()\n const [local, rest] = splitProps(props, [\n 'class',\n 'item',\n 'selected',\n 'leading',\n 'trailing',\n 'children',\n 'onDoubleClick',\n 'ref',\n ])\n\n let row: HTMLDivElement | undefined\n let registeredId: string | undefined\n const id = () => local.item.id\n\n onCleanup(() => tree.unregister(registeredId ?? id(), row))\n\n const setRef = (element: HTMLDivElement) => {\n row = element\n registeredId = id()\n tree.register(registeredId, element)\n }\n\n createEffect(() => {\n const nextId = id()\n if (!row || registeredId === nextId) return\n if (registeredId !== undefined) tree.unregister(registeredId, row)\n registeredId = nextId\n tree.register(nextId, row)\n })\n\n const handleFocus = () => {\n tree.noteFocus(id())\n if (tree.activeId() !== id()) tree.onActiveIdChange(id())\n }\n\n const handleClick: JSX.EventHandler<HTMLDivElement, MouseEvent> = (event) => {\n if (isActionTarget(event.target, event.currentTarget)) return\n tree.focusItem(id())\n tree.activate(id(), event)\n }\n\n const handleDoubleClick: JSX.EventHandler<HTMLDivElement, MouseEvent> = (event) => {\n if (isActionTarget(event.target, event.currentTarget)) return\n local.onDoubleClick?.(id(), event)\n }\n\n const position = () => tree.index().positionById.get(id())\n // Only an expandable row has a chevron to press; a leaf's marker slot stays\n // part of the row so a click there is an ordinary row click.\n const markerTarget = () => tree.disclosure() === 'marker' && local.item.expandable\n\n const handleMarkerClick: JSX.EventHandler<HTMLSpanElement, MouseEvent> = (event) => {\n // The row's own handler would focus and activate the row.\n event.stopPropagation()\n tree.expand(id(), !local.item.expanded)\n }\n return (\n <div\n {...rest}\n ref={composeRefs(setRef, () => local.ref)}\n id={rest.id ?? `${tree.idPrefix}-item-${encodeURIComponent(id())}`}\n role=\"treeitem\"\n aria-level={local.item.level}\n aria-expanded={local.item.expandable ? (local.item.expanded ? 'true' : 'false') : undefined}\n aria-selected={local.selected === undefined ? undefined : local.selected ? 'true' : 'false'}\n aria-posinset={position()?.position}\n aria-setsize={position()?.setSize}\n tabIndex={tree.activeId() === id() ? 0 : -1}\n data-tree-id={id()}\n data-active={tree.activeId() === id() ? '' : undefined}\n data-selected={local.selected ? '' : undefined}\n data-expanded={local.item.expandable ? String(local.item.expanded) : undefined}\n class={cn(\n 'group/tree-row flex min-h-row-sm w-full min-w-0 items-center gap-1.5 rounded-md pe-1.5 text-left text-sm outline-none transition-colors',\n 'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',\n {\n 'bg-primary-subtle text-foreground': local.selected,\n 'hover:bg-surface-hover': !local.selected,\n },\n local.class\n )}\n style={{ 'padding-inline-start': `${6 + Math.max(0, local.item.level - 1) * 12}px` }}\n onFocus={handleFocus}\n onClick={handleClick}\n onDblClick={handleDoubleClick}\n >\n <span\n data-slot={markerTarget() ? 'tree-row-disclosure' : undefined}\n class={cn(\n 'flex',\n !markerTarget() && 'size-3.5',\n 'shrink-0 items-center justify-center text-muted-foreground',\n /* The glyph stays 14px wide but the target is not: it takes the row's full\n height (a flex child's `self-stretch`, so it follows row density and a\n tall trailing control) and the pseudo-element widens it to 26px, which\n stops at the row's own padding and gap, never over the icon or label. */\n markerTarget() &&\n 'relative w-3.5 cursor-pointer self-stretch hover:text-foreground after:absolute after:inset-y-0 after:-inset-x-1.5'\n )}\n onMouseDown={markerTarget() ? handleMarkerMouseDown : undefined}\n onClick={markerTarget() ? handleMarkerClick : undefined}\n onDblClick={markerTarget() ? handleMarkerDoubleClick : undefined}\n >\n {local.item.expandable ? (\n <ChevronRight\n aria-hidden=\"true\"\n class={cn('size-3.5 transition-transform', local.item.expanded && 'rotate-90')}\n />\n ) : null}\n </span>\n {local.leading ? (\n <span class=\"flex size-4 shrink-0 items-center justify-center [&_svg]:size-4\">\n {local.leading}\n </span>\n ) : null}\n <span class=\"min-w-0 flex-1 truncate\">{local.children}</span>\n {local.trailing ? (\n <span class=\"ms-auto flex shrink-0 items-center gap-1 text-muted-foreground\">\n {local.trailing}\n </span>\n ) : null}\n </div>\n )\n}\n\n/**\n * TreeRowActions.\n *\n * Row-level commands (rename, copy, delete) that stay out of the way until the\n * row is hovered or holds focus, so a long tree reads as names rather than a\n * wall of buttons. Put it in `TreeRow`'s `trailing` slot beside any status that\n * must stay visible. `pinned` keeps the actions shown, e.g. while a destructive\n * action waits for its confirming second press.\n */\nexport function TreeRowActions(props: ComponentProps<'span'> & { pinned?: boolean }) {\n const [local, rest] = splitProps(props, ['class', 'pinned'])\n return (\n <span\n data-slot=\"tree-row-actions\"\n data-pinned={local.pinned ? '' : undefined}\n class={cn(\n 'flex shrink-0 items-center gap-1 opacity-0 transition-opacity ease-out',\n 'group-hover/tree-row:opacity-100 group-focus-within/tree-row:opacity-100 data-pinned:opacity-100',\n local.class\n )}\n {...rest}\n />\n )\n}\n\n// Pressing the chevron must not move focus to its row: a mousedown on a\n// non-focusable child would otherwise focus the row, making it the roving stop\n// (and notifying `onActiveIdChange`) for what is a disclosure toggle, not a\n// selection. The press itself leaves focus where it was, so Left/Right keep\n// acting on the row the keyboard user left off at. It cannot stop the toggle\n// from hiding that row; `Tree` then recovers focus onto the first visible row.\nconst handleMarkerMouseDown: JSX.EventHandler<HTMLSpanElement, MouseEvent> = (event) =>\n event.preventDefault()\n// A double-click is two toggles; it must not also reach `onDoubleClick`\n// (rename, open) as though the row itself had been double-clicked.\nconst handleMarkerDoubleClick: JSX.EventHandler<HTMLSpanElement, MouseEvent> = (event) =>\n event.stopPropagation()\n\nfunction isActionTarget(target: EventTarget | null, currentTarget: HTMLElement): boolean {\n if (!(target instanceof Element) || target === currentTarget) return false\n return Boolean(\n target.closest(\n 'a, button, input, textarea, select, [contenteditable=\"true\"], [role=\"button\"], [data-tree-keyboard-stop]'\n )\n )\n}\n\nfunction composeRefs(\n internal: (element: HTMLDivElement) => void,\n external: () => ComponentProps<'div'>['ref']\n): (element: HTMLDivElement) => void {\n return (element) => {\n internal(element)\n const ref = external()\n if (typeof ref === 'function') ref(element)\n }\n}\n\nexport type { TreeItemDescriptor } from './tree-navigation'\n"],"mappings":"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"}
@@ -3,10 +3,11 @@ import type { ComponentProps } from 'solid-js';
3
3
  /**
4
4
  * AlertDialog.
5
5
  *
6
- * A dialog that cannot be dismissed by accident: Escape and a click on the scrim
7
- * do nothing, and the user must choose one of the actions. That is the whole
8
- * difference from Dialog, and it is why this exists as its own component rather
9
- * than a prop — a caller has to reach for it deliberately.
6
+ * A dialog that cannot be dismissed by accident: a click on the scrim does nothing,
7
+ * and the user must choose one of the actions. Escape is the keyboard's Cancel: it
8
+ * closes the dialog exactly as the cancel action does, so nothing is confirmed. That
9
+ * is the whole difference from Dialog, and it is why this exists as its own
10
+ * component rather than a prop — a caller has to reach for it deliberately.
10
11
  *
11
12
  * The consequence is a rule of use: this is for confirming a destructive or
12
13
  * irreversible step. Wrapping an ordinary form in an AlertDialog traps someone who
@@ -1 +1 @@
1
- {"version":3,"file":"alert-dialog.d.ts","sourceRoot":"","sources":["../../../../src/components/ui/alert-dialog/alert-dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,IAAI,kBAAkB,EACjC,OAAO,IAAI,yBAAyB,EACrC,MAAM,4BAA4B,CAAA;AAEnC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAW9C;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,CAAC,OAAO,kBAAkB,CAAC,kCAE3E;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,CAAC,OAAO,kBAAkB,CAAC,OAAO,CAAC,kCAE1F;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,cAAc,CAAC,OAAO,kBAAkB,CAAC,MAAM,CAAC,kCAExF;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,CAAC,OAAO,kBAAkB,CAAC,OAAO,CAAC,kCAY1F;AA2BD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,CAAC,OAAO,yBAAyB,CAAC,kCAezF;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,cAAc,CAAC,KAAK,CAAC,kCAS7D;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,cAAc,CAAC,KAAK,CAAC,kCAS7D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,CAAC,OAAO,kBAAkB,CAAC,KAAK,CAAC,kCAGtF;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,cAAc,CAAC,OAAO,kBAAkB,CAAC,WAAW,CAAC,kCAS7D;AAsCD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,cAAc,CAAC,OAAO,kBAAkB,CAAC,WAAW,CAAC,kCAE7F;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,cAAc,CAAC,OAAO,kBAAkB,CAAC,WAAW,CAAC,kCAE7F"}
1
+ {"version":3,"file":"alert-dialog.d.ts","sourceRoot":"","sources":["../../../../src/components/ui/alert-dialog/alert-dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,IAAI,kBAAkB,EACjC,OAAO,IAAI,yBAAyB,EACrC,MAAM,4BAA4B,CAAA;AAEnC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAW9C;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,CAAC,OAAO,kBAAkB,CAAC,kCAE3E;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,CAAC,OAAO,kBAAkB,CAAC,OAAO,CAAC,kCAE1F;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,cAAc,CAAC,OAAO,kBAAkB,CAAC,MAAM,CAAC,kCAExF;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,CAAC,OAAO,kBAAkB,CAAC,OAAO,CAAC,kCAY1F;AA+BD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,CAAC,OAAO,yBAAyB,CAAC,kCAezF;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,cAAc,CAAC,KAAK,CAAC,kCAS7D;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,cAAc,CAAC,KAAK,CAAC,kCAS7D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,CAAC,OAAO,kBAAkB,CAAC,KAAK,CAAC,kCAGtF;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,cAAc,CAAC,OAAO,kBAAkB,CAAC,WAAW,CAAC,kCAS7D;AAsCD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,cAAc,CAAC,OAAO,kBAAkB,CAAC,WAAW,CAAC,kCAE7F;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,cAAc,CAAC,OAAO,kBAAkB,CAAC,WAAW,CAAC,kCAE7F"}
@@ -11,10 +11,11 @@ var _tmpl$4 = /*#__PURE__*/ template(`<span class=contents>`);
11
11
  /**
12
12
  * AlertDialog.
13
13
  *
14
- * A dialog that cannot be dismissed by accident: Escape and a click on the scrim
15
- * do nothing, and the user must choose one of the actions. That is the whole
16
- * difference from Dialog, and it is why this exists as its own component rather
17
- * than a prop — a caller has to reach for it deliberately.
14
+ * A dialog that cannot be dismissed by accident: a click on the scrim does nothing,
15
+ * and the user must choose one of the actions. Escape is the keyboard's Cancel: it
16
+ * closes the dialog exactly as the cancel action does, so nothing is confirmed. That
17
+ * is the whole difference from Dialog, and it is why this exists as its own
18
+ * component rather than a prop — a caller has to reach for it deliberately.
18
19
  *
19
20
  * The consequence is a rule of use: this is for confirming a destructive or
20
21
  * irreversible step. Wrapping an ordinary form in an AlertDialog traps someone who
@@ -42,22 +43,26 @@ function AlertDialogOverlay(props) {
42
43
  * The panel.
43
44
  *
44
45
  * Kobalte's `AlertDialog.Content` is `Dialog.Content` with `role="alertdialog"`
45
- * and nothing else — Escape and an outside click dismiss it exactly as they
46
- * dismiss a Dialog. That is a real gap in the primitive, so the blocking is
47
- * implemented here, and it is the whole difference between this component and a
48
- * Dialog:
46
+ * and nothing else — an outside click dismisses it exactly as it dismisses a
47
+ * Dialog. That is a real gap in the primitive, so the blocking is implemented
48
+ * here, and it is the whole difference between this component and a Dialog:
49
49
  *
50
- * - Escape is prevented, because a key that closes a confirmation is a key that
51
- * makes "confirm" meaningless.
52
50
  * - Pointer and focus interactions outside are prevented, including the scrim
53
- * click, for the same reason.
51
+ * click, because a stray click that closes a confirmation is a click that
52
+ * makes "confirm" meaningless.
53
+ *
54
+ * Escape is deliberately left to the primitive. It can only ever cancel, never
55
+ * confirm, so it is not an accident that matters; refusing it traps a keyboard
56
+ * user who has changed their mind, and the WAI-ARIA alertdialog pattern says
57
+ * Escape closes. It reports `onOpenChange(false)` and restores focus to the
58
+ * trigger, which is what the cancel action does, and as the topmost layer it
59
+ * closes only the confirmation, not a dialog underneath it.
54
60
  *
55
- * There is deliberately no prop to turn this off. A surface that can be escaped
61
+ * There is deliberately no prop to turn the scrim blocking off. A surface that can be escaped
56
62
  * accidentally does not need to be an AlertDialog; it is a Dialog, and using this
57
63
  * one would be a claim about the flow that is not true.
58
64
  */
59
65
  var blockDismissal = {
60
- onEscapeKeyDown: (event) => event.preventDefault(),
61
66
  onPointerDownOutside: (event) => event.preventDefault(),
62
67
  onFocusOutside: (event) => event.preventDefault(),
63
68
  onInteractOutside: (event) => event.preventDefault()
@@ -1 +1 @@
1
- {"version":3,"file":"alert-dialog.js","names":["AlertDialog","KobalteAlertDialog","Content","KobalteAlertDialogContent","ComponentProps","createEffect","createSignal","splitProps","dialogSurface","overlayMotion","overlayPositioner","overlayScrim","overlayTitle","cn","props","_$createComponent","AlertDialogTrigger","Trigger","AlertDialogPortal","Portal","AlertDialogOverlay","Overlay","local","rest","_$mergeProps","class","blockDismissal","onEscapeKeyDown","event","KeyboardEvent","preventDefault","onPointerDownOutside","Event","onFocusOutside","onInteractOutside","const","AlertDialogContent","children","_el$","_tmpl$","_$className","_$insert","AlertDialogHeader","_el$2","_tmpl$2","_$spread","AlertDialogFooter","_el$3","_tmpl$3","AlertDialogTitle","Title","AlertDialogDescription","Description","AlertDialogChoice","CloseButton","wrapper","setWrapper","HTMLSpanElement","root","undefined","querySelector","removeAttribute","_el$4","_tmpl$4","_$use","AlertDialogAction","AlertDialogCancel","AlertDialog","_$createComponent","KobalteAlertDialog","_$mergeProps","KobalteAlertDialogContent"],"sources":["../../../../src/components/ui/alert-dialog/alert-dialog.tsx"],"sourcesContent":["import {\n AlertDialog as KobalteAlertDialog,\n Content as KobalteAlertDialogContent,\n} from '@kobalte/core/alert-dialog'\n// Use the alert-specific export rather than DialogRoot.Content, which other imports can replace.\nimport type { ComponentProps } from 'solid-js'\nimport { createEffect, createSignal, splitProps } from 'solid-js'\nimport {\n dialogSurface,\n overlayMotion,\n overlayPositioner,\n overlayScrim,\n overlayTitle,\n} from '#lib/overlay'\nimport { cn } from '#lib/utils'\n\n/**\n * AlertDialog.\n *\n * A dialog that cannot be dismissed by accident: Escape and a click on the scrim\n * do nothing, and the user must choose one of the actions. That is the whole\n * difference from Dialog, and it is why this exists as its own component rather\n * than a prop — a caller has to reach for it deliberately.\n *\n * The consequence is a rule of use: this is for confirming a destructive or\n * irreversible step. Wrapping an ordinary form in an AlertDialog traps someone who\n * wants to back out.\n *\n * There is deliberately no close button in the corner. An X has no label, and the\n * point of this surface is that every exit is a named choice.\n */\nexport function AlertDialog(props: ComponentProps<typeof KobalteAlertDialog>) {\n return <KobalteAlertDialog {...props} />\n}\n\nexport function AlertDialogTrigger(props: ComponentProps<typeof KobalteAlertDialog.Trigger>) {\n return <KobalteAlertDialog.Trigger {...props} />\n}\n\nexport function AlertDialogPortal(props: ComponentProps<typeof KobalteAlertDialog.Portal>) {\n return <KobalteAlertDialog.Portal {...props} />\n}\n\nexport function AlertDialogOverlay(props: ComponentProps<typeof KobalteAlertDialog.Overlay>) {\n const [local, rest] = splitProps(props, ['class'])\n return (\n <KobalteAlertDialog.Overlay\n class={cn(\n overlayScrim,\n 'data-expanded:animate-in data-expanded:fade-in-0 data-closed:animate-out data-closed:fade-out-0',\n local.class\n )}\n {...rest}\n />\n )\n}\n\n/**\n * The panel.\n *\n * Kobalte's `AlertDialog.Content` is `Dialog.Content` with `role=\"alertdialog\"`\n * and nothing else — Escape and an outside click dismiss it exactly as they\n * dismiss a Dialog. That is a real gap in the primitive, so the blocking is\n * implemented here, and it is the whole difference between this component and a\n * Dialog:\n *\n * - Escape is prevented, because a key that closes a confirmation is a key that\n * makes \"confirm\" meaningless.\n * - Pointer and focus interactions outside are prevented, including the scrim\n * click, for the same reason.\n *\n * There is deliberately no prop to turn this off. A surface that can be escaped\n * accidentally does not need to be an AlertDialog; it is a Dialog, and using this\n * one would be a claim about the flow that is not true.\n */\nconst blockDismissal = {\n onEscapeKeyDown: (event: KeyboardEvent) => event.preventDefault(),\n onPointerDownOutside: (event: Event) => event.preventDefault(),\n onFocusOutside: (event: Event) => event.preventDefault(),\n onInteractOutside: (event: Event) => event.preventDefault(),\n} as const\n\nexport function AlertDialogContent(props: ComponentProps<typeof KobalteAlertDialogContent>) {\n const [local, rest] = splitProps(props, ['class'])\n\n return (\n <KobalteAlertDialog.Portal>\n <AlertDialogOverlay />\n <div class={overlayPositioner}>\n <KobalteAlertDialogContent\n {...blockDismissal}\n class={cn(dialogSurface, overlayMotion, 'grid w-full max-w-md gap-4 p-5', local.class)}\n {...rest}\n />\n </div>\n </KobalteAlertDialog.Portal>\n )\n}\n\nexport function AlertDialogHeader(props: ComponentProps<'div'>) {\n const [local, rest] = splitProps(props, ['class'])\n return (\n <div\n data-slot=\"alert-dialog-header\"\n class={cn('flex flex-col gap-1.5 text-start', local.class)}\n {...rest}\n />\n )\n}\n\nexport function AlertDialogFooter(props: ComponentProps<'div'>) {\n const [local, rest] = splitProps(props, ['class'])\n return (\n <div\n data-slot=\"alert-dialog-footer\"\n class={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', local.class)}\n {...rest}\n />\n )\n}\n\nexport function AlertDialogTitle(props: ComponentProps<typeof KobalteAlertDialog.Title>) {\n const [local, rest] = splitProps(props, ['class'])\n return <KobalteAlertDialog.Title class={cn(overlayTitle, local.class)} {...rest} />\n}\n\nexport function AlertDialogDescription(\n props: ComponentProps<typeof KobalteAlertDialog.Description>\n) {\n const [local, rest] = splitProps(props, ['class'])\n return (\n <KobalteAlertDialog.Description\n class={cn('text-muted-foreground text-sm', local.class)}\n {...rest}\n />\n )\n}\n\n/**\n * One of the dialog's two named choices.\n *\n * Kobalte labels its close button \"Dismiss\" by default, because a generic close\n * button is usually an icon with no text of its own. In an AlertDialog it is\n * something else: the named choices. An `aria-label` there *replaces* the visible\n * text, so a screen reader hears \"Dismiss\" twice and the user cannot tell the\n * destructive action from the safe one.\n *\n * The injected label is removed rather than overridden with an empty one. An empty\n * `aria-label` is not reliably ignored — some name computations treat its presence\n * as an explicit empty name, which leaves the button unnamed — so the attribute\n * has to go, and the visible text becomes the name. A caller whose button is an\n * icon passes its own `aria-label`, and that one is left alone.\n *\n * Applied through a layout-neutral wrapper because Kobalte keeps the `ref` for its\n * own dismissable layer and does not forward it. The wrapper is `display: contents`\n * so the button remains the flex item its parent lays out.\n */\nfunction AlertDialogChoice(props: ComponentProps<typeof KobalteAlertDialog.CloseButton>) {\n const [local, rest] = splitProps(props, ['class', 'aria-label'])\n const [wrapper, setWrapper] = createSignal<HTMLSpanElement>()\n\n createEffect(() => {\n const root = wrapper()\n if (!root || local['aria-label'] !== undefined) return\n root.querySelector('button')?.removeAttribute('aria-label')\n })\n\n return (\n <span ref={setWrapper} class=\"contents\">\n <KobalteAlertDialog.CloseButton class={cn(local.class)} {...rest} />\n </span>\n )\n}\n\nexport function AlertDialogAction(props: ComponentProps<typeof KobalteAlertDialog.CloseButton>) {\n return <AlertDialogChoice {...props} />\n}\n\nexport function AlertDialogCancel(props: ComponentProps<typeof KobalteAlertDialog.CloseButton>) {\n return <AlertDialogChoice {...props} />\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"alert-dialog.js","names":["AlertDialog","KobalteAlertDialog","Content","KobalteAlertDialogContent","ComponentProps","createEffect","createSignal","splitProps","dialogSurface","overlayMotion","overlayPositioner","overlayScrim","overlayTitle","cn","props","_$createComponent","AlertDialogTrigger","Trigger","AlertDialogPortal","Portal","AlertDialogOverlay","Overlay","local","rest","_$mergeProps","class","blockDismissal","onPointerDownOutside","event","Event","preventDefault","onFocusOutside","onInteractOutside","const","AlertDialogContent","children","_el$","_tmpl$","_$className","_$insert","AlertDialogHeader","_el$2","_tmpl$2","_$spread","AlertDialogFooter","_el$3","_tmpl$3","AlertDialogTitle","Title","AlertDialogDescription","Description","AlertDialogChoice","CloseButton","wrapper","setWrapper","HTMLSpanElement","root","undefined","querySelector","removeAttribute","_el$4","_tmpl$4","_$use","AlertDialogAction","AlertDialogCancel","AlertDialog","_$createComponent","KobalteAlertDialog","_$mergeProps","KobalteAlertDialogContent"],"sources":["../../../../src/components/ui/alert-dialog/alert-dialog.tsx"],"sourcesContent":["import {\n AlertDialog as KobalteAlertDialog,\n Content as KobalteAlertDialogContent,\n} from '@kobalte/core/alert-dialog'\n// Use the alert-specific export rather than DialogRoot.Content, which other imports can replace.\nimport type { ComponentProps } from 'solid-js'\nimport { createEffect, createSignal, splitProps } from 'solid-js'\nimport {\n dialogSurface,\n overlayMotion,\n overlayPositioner,\n overlayScrim,\n overlayTitle,\n} from '#lib/overlay'\nimport { cn } from '#lib/utils'\n\n/**\n * AlertDialog.\n *\n * A dialog that cannot be dismissed by accident: a click on the scrim does nothing,\n * and the user must choose one of the actions. Escape is the keyboard's Cancel: it\n * closes the dialog exactly as the cancel action does, so nothing is confirmed. That\n * is the whole difference from Dialog, and it is why this exists as its own\n * component rather than a prop — a caller has to reach for it deliberately.\n *\n * The consequence is a rule of use: this is for confirming a destructive or\n * irreversible step. Wrapping an ordinary form in an AlertDialog traps someone who\n * wants to back out.\n *\n * There is deliberately no close button in the corner. An X has no label, and the\n * point of this surface is that every exit is a named choice.\n */\nexport function AlertDialog(props: ComponentProps<typeof KobalteAlertDialog>) {\n return <KobalteAlertDialog {...props} />\n}\n\nexport function AlertDialogTrigger(props: ComponentProps<typeof KobalteAlertDialog.Trigger>) {\n return <KobalteAlertDialog.Trigger {...props} />\n}\n\nexport function AlertDialogPortal(props: ComponentProps<typeof KobalteAlertDialog.Portal>) {\n return <KobalteAlertDialog.Portal {...props} />\n}\n\nexport function AlertDialogOverlay(props: ComponentProps<typeof KobalteAlertDialog.Overlay>) {\n const [local, rest] = splitProps(props, ['class'])\n return (\n <KobalteAlertDialog.Overlay\n class={cn(\n overlayScrim,\n 'data-expanded:animate-in data-expanded:fade-in-0 data-closed:animate-out data-closed:fade-out-0',\n local.class\n )}\n {...rest}\n />\n )\n}\n\n/**\n * The panel.\n *\n * Kobalte's `AlertDialog.Content` is `Dialog.Content` with `role=\"alertdialog\"`\n * and nothing else — an outside click dismisses it exactly as it dismisses a\n * Dialog. That is a real gap in the primitive, so the blocking is implemented\n * here, and it is the whole difference between this component and a Dialog:\n *\n * - Pointer and focus interactions outside are prevented, including the scrim\n * click, because a stray click that closes a confirmation is a click that\n * makes \"confirm\" meaningless.\n *\n * Escape is deliberately left to the primitive. It can only ever cancel, never\n * confirm, so it is not an accident that matters; refusing it traps a keyboard\n * user who has changed their mind, and the WAI-ARIA alertdialog pattern says\n * Escape closes. It reports `onOpenChange(false)` and restores focus to the\n * trigger, which is what the cancel action does, and as the topmost layer it\n * closes only the confirmation, not a dialog underneath it.\n *\n * There is deliberately no prop to turn the scrim blocking off. A surface that can be escaped\n * accidentally does not need to be an AlertDialog; it is a Dialog, and using this\n * one would be a claim about the flow that is not true.\n */\nconst blockDismissal = {\n onPointerDownOutside: (event: Event) => event.preventDefault(),\n onFocusOutside: (event: Event) => event.preventDefault(),\n onInteractOutside: (event: Event) => event.preventDefault(),\n} as const\n\nexport function AlertDialogContent(props: ComponentProps<typeof KobalteAlertDialogContent>) {\n const [local, rest] = splitProps(props, ['class'])\n\n return (\n <KobalteAlertDialog.Portal>\n <AlertDialogOverlay />\n <div class={overlayPositioner}>\n <KobalteAlertDialogContent\n {...blockDismissal}\n class={cn(dialogSurface, overlayMotion, 'grid w-full max-w-md gap-4 p-5', local.class)}\n {...rest}\n />\n </div>\n </KobalteAlertDialog.Portal>\n )\n}\n\nexport function AlertDialogHeader(props: ComponentProps<'div'>) {\n const [local, rest] = splitProps(props, ['class'])\n return (\n <div\n data-slot=\"alert-dialog-header\"\n class={cn('flex flex-col gap-1.5 text-start', local.class)}\n {...rest}\n />\n )\n}\n\nexport function AlertDialogFooter(props: ComponentProps<'div'>) {\n const [local, rest] = splitProps(props, ['class'])\n return (\n <div\n data-slot=\"alert-dialog-footer\"\n class={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', local.class)}\n {...rest}\n />\n )\n}\n\nexport function AlertDialogTitle(props: ComponentProps<typeof KobalteAlertDialog.Title>) {\n const [local, rest] = splitProps(props, ['class'])\n return <KobalteAlertDialog.Title class={cn(overlayTitle, local.class)} {...rest} />\n}\n\nexport function AlertDialogDescription(\n props: ComponentProps<typeof KobalteAlertDialog.Description>\n) {\n const [local, rest] = splitProps(props, ['class'])\n return (\n <KobalteAlertDialog.Description\n class={cn('text-muted-foreground text-sm', local.class)}\n {...rest}\n />\n )\n}\n\n/**\n * One of the dialog's two named choices.\n *\n * Kobalte labels its close button \"Dismiss\" by default, because a generic close\n * button is usually an icon with no text of its own. In an AlertDialog it is\n * something else: the named choices. An `aria-label` there *replaces* the visible\n * text, so a screen reader hears \"Dismiss\" twice and the user cannot tell the\n * destructive action from the safe one.\n *\n * The injected label is removed rather than overridden with an empty one. An empty\n * `aria-label` is not reliably ignored — some name computations treat its presence\n * as an explicit empty name, which leaves the button unnamed — so the attribute\n * has to go, and the visible text becomes the name. A caller whose button is an\n * icon passes its own `aria-label`, and that one is left alone.\n *\n * Applied through a layout-neutral wrapper because Kobalte keeps the `ref` for its\n * own dismissable layer and does not forward it. The wrapper is `display: contents`\n * so the button remains the flex item its parent lays out.\n */\nfunction AlertDialogChoice(props: ComponentProps<typeof KobalteAlertDialog.CloseButton>) {\n const [local, rest] = splitProps(props, ['class', 'aria-label'])\n const [wrapper, setWrapper] = createSignal<HTMLSpanElement>()\n\n createEffect(() => {\n const root = wrapper()\n if (!root || local['aria-label'] !== undefined) return\n root.querySelector('button')?.removeAttribute('aria-label')\n })\n\n return (\n <span ref={setWrapper} class=\"contents\">\n <KobalteAlertDialog.CloseButton class={cn(local.class)} {...rest} />\n </span>\n )\n}\n\nexport function AlertDialogAction(props: ComponentProps<typeof KobalteAlertDialog.CloseButton>) {\n return <AlertDialogChoice {...props} />\n}\n\nexport function AlertDialogCancel(props: ComponentProps<typeof KobalteAlertDialog.CloseButton>) {\n return <AlertDialogChoice {...props} />\n}\n"],"mappings":"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"}
@@ -1,3 +1,10 @@
1
1
  /** Keyboard-accessible tree rows with virtualization-aware focus handoff. */
2
- export { Tree, TreeRow, TreeRowActions, type TreeProps, type TreeRowProps } from './tree'
2
+ export {
3
+ Tree,
4
+ TreeRow,
5
+ TreeRowActions,
6
+ type TreeDisclosure,
7
+ type TreeProps,
8
+ type TreeRowProps,
9
+ } from './tree'
3
10
  export type { TreeItemDescriptor } from './tree-navigation'
@@ -26,6 +26,8 @@ type TreeContextValue = {
26
26
  focusItem: (id: string) => void
27
27
  noteFocus: (id: string) => void
28
28
  activate: (id: string, event: Event) => void
29
+ expand: (id: string, expanded: boolean) => void
30
+ disclosure: () => TreeDisclosure
29
31
  onActiveIdChange: (id: string) => void
30
32
  }
31
33
 
@@ -37,6 +39,19 @@ function useTreeContext() {
37
39
  return tree
38
40
  }
39
41
 
42
+ /**
43
+ * Who owns the pointer on a row's disclosure chevron.
44
+ *
45
+ * - `row` (default): the chevron is decoration; a click anywhere on the row,
46
+ * chevron included, is a row activation. This is the file-explorer
47
+ * convention, where the host's `onActivate` toggles a folder.
48
+ * - `marker`: the chevron of an expandable row is its own pointer target. It
49
+ * calls `onExpand(id, !expanded)` and nothing else; the rest of the row still
50
+ * activates. For trees where expanding and selecting are independent (a
51
+ * source list that selects a source and separately reveals its details).
52
+ */
53
+ export type TreeDisclosure = 'row' | 'marker'
54
+
40
55
  type TreeAccessibleName =
41
56
  | { 'aria-label': string; 'aria-labelledby'?: string }
42
57
  | { 'aria-label'?: string; 'aria-labelledby': string }
@@ -50,10 +65,19 @@ export type TreeProps = Omit<
50
65
  visibleItems: readonly TreeItemDescriptor[]
51
66
  /** The host-owned identity of the row that currently owns roving focus. */
52
67
  activeId: string | null
53
- /** Called when keyboard or pointer navigation changes the active row. */
68
+ /**
69
+ * Called when keyboard or pointer navigation changes the active row, and when
70
+ * the active row leaves `visibleItems` (collapsed away, filtered out) and the
71
+ * first visible row takes over as the roving stop.
72
+ */
54
73
  onActiveIdChange: (id: string) => void
55
74
  /** Called for Left/Right expand and collapse requests. */
56
75
  onExpand?: (id: string, expanded: boolean) => void
76
+ /**
77
+ * Opt-in pointer behaviour of the disclosure chevron; see {@link TreeDisclosure}.
78
+ * Keyboard behaviour is identical in both modes: Left/Right expand and collapse.
79
+ */
80
+ disclosure?: TreeDisclosure
57
81
  /** Called for Enter or pointer activation. Domain action stays host-owned. */
58
82
  onActivate?: (id: string, event: Event) => void
59
83
  /** Called for Space selection. Selection state stays host-owned. */
@@ -78,6 +102,20 @@ export type TreeProps = Omit<
78
102
  *
79
103
  * Kobalte 0.13.14 has no Tree primitive, so this component implements the
80
104
  * WAI-ARIA tree row navigation contract and leaves product state to callbacks.
105
+ *
106
+ * By default the disclosure chevron is decoration and a click anywhere on a
107
+ * row activates it, so a file explorer can toggle a folder from `onActivate`.
108
+ * Reach for `disclosure="marker"` when expanding and selecting are separate
109
+ * decisions: the chevron then calls only `onExpand`, and the rest of the row
110
+ * calls only `onActivate`. The chevron stays `aria-hidden` and out of the tab
111
+ * order in both modes; keyboard expansion is Left/Right either way.
112
+ *
113
+ * Collapsing is the one thing that can move focus without a key or a click on
114
+ * the target row: when the active row leaves `visibleItems`, the first visible
115
+ * row becomes the roving stop and `onActiveIdChange` is called with it. If the
116
+ * removed row held focus, or was the last row the tree saw focused, focus moves
117
+ * to that same row; if it had no focus, only the roving stop moves. This is the
118
+ * same in both `disclosure` modes and for every way a host collapses a row.
81
119
  */
82
120
  export function Tree(props: TreeProps) {
83
121
  const [local, rest] = splitProps(props, [
@@ -86,6 +124,7 @@ export function Tree(props: TreeProps) {
86
124
  'activeId',
87
125
  'onActiveIdChange',
88
126
  'onExpand',
127
+ 'disclosure',
89
128
  'onActivate',
90
129
  'onSelectItem',
91
130
  'onRequestReveal',
@@ -187,16 +226,22 @@ export function Tree(props: TreeProps) {
187
226
  focusedId = id
188
227
  },
189
228
  activate: (id, event) => local.onActivate?.(id, event),
229
+ expand: (id, expanded) => local.onExpand?.(id, expanded),
230
+ disclosure: () => local.disclosure ?? 'row',
190
231
  onActiveIdChange: local.onActiveIdChange,
191
232
  }
192
233
 
193
- // A filtered or removed active ID falls back to the first visible item. This
194
- // keeps one treeitem in the sequential tab order while notifying the host.
234
+ // A filtered, collapsed-away or removed active ID falls back to the first
235
+ // visible item. This keeps one treeitem in the sequential tab order while
236
+ // notifying the host.
195
237
  createEffect(() => {
196
238
  const activeId = effectiveActiveId()
197
239
  if (activeId === null) return
198
- // If a host filter removes the row that had DOM focus, move focus to the
199
- // same projection fallback that now owns the roving tab stop.
240
+ // If the projection removes the last row that took focus (a host filter, or
241
+ // an ancestor collapsing), move focus to the same fallback that now owns the
242
+ // roving tab stop. `focusedId` is not cleared when focus leaves the tree, so
243
+ // this also reclaims focus from a control outside it; that long-standing
244
+ // behaviour is deliberately left as it was.
200
245
  if (focusedId !== null && !index().byId.has(focusedId) && pendingFocusId !== activeId) {
201
246
  focusItem(activeId)
202
247
  } else if (local.activeId !== activeId) {
@@ -282,6 +327,15 @@ export type TreeRowProps = Omit<
282
327
  * Keeps the donor's disclosure marker, leading file/folder affordance, primary
283
328
  * label and trailing-action hierarchy while replacing the donor's button and
284
329
  * runtime callbacks with a shared treeitem and host-owned slots/actions.
330
+ *
331
+ * With `Tree`'s `disclosure="marker"` the chevron of an expandable row is a
332
+ * separate pointer target (`data-slot="tree-row-disclosure"`) that only calls
333
+ * `onExpand`. It is not focusable and has no role: the row remains the single
334
+ * focus stop, Enter and Space still activate and select, and Left/Right still
335
+ * expand and collapse, so the marker adds a pointer path and nothing else.
336
+ * Pressing it does not move focus or the roving stop to its row; if the toggle
337
+ * hides the focused row, `Tree` recovers focus as it does for any collapse (see
338
+ * the `Tree` comment and docs/tree.md).
285
339
  */
286
340
  export function TreeRow(props: TreeRowProps) {
287
341
  const tree = useTreeContext()
@@ -333,7 +387,15 @@ export function TreeRow(props: TreeRowProps) {
333
387
  }
334
388
 
335
389
  const position = () => tree.index().positionById.get(id())
336
-
390
+ // Only an expandable row has a chevron to press; a leaf's marker slot stays
391
+ // part of the row so a click there is an ordinary row click.
392
+ const markerTarget = () => tree.disclosure() === 'marker' && local.item.expandable
393
+
394
+ const handleMarkerClick: JSX.EventHandler<HTMLSpanElement, MouseEvent> = (event) => {
395
+ // The row's own handler would focus and activate the row.
396
+ event.stopPropagation()
397
+ tree.expand(id(), !local.item.expanded)
398
+ }
337
399
  return (
338
400
  <div
339
401
  {...rest}
@@ -364,7 +426,23 @@ export function TreeRow(props: TreeRowProps) {
364
426
  onClick={handleClick}
365
427
  onDblClick={handleDoubleClick}
366
428
  >
367
- <span class="flex size-3.5 shrink-0 items-center justify-center text-muted-foreground">
429
+ <span
430
+ data-slot={markerTarget() ? 'tree-row-disclosure' : undefined}
431
+ class={cn(
432
+ 'flex',
433
+ !markerTarget() && 'size-3.5',
434
+ 'shrink-0 items-center justify-center text-muted-foreground',
435
+ /* The glyph stays 14px wide but the target is not: it takes the row's full
436
+ height (a flex child's `self-stretch`, so it follows row density and a
437
+ tall trailing control) and the pseudo-element widens it to 26px, which
438
+ stops at the row's own padding and gap, never over the icon or label. */
439
+ markerTarget() &&
440
+ 'relative w-3.5 cursor-pointer self-stretch hover:text-foreground after:absolute after:inset-y-0 after:-inset-x-1.5'
441
+ )}
442
+ onMouseDown={markerTarget() ? handleMarkerMouseDown : undefined}
443
+ onClick={markerTarget() ? handleMarkerClick : undefined}
444
+ onDblClick={markerTarget() ? handleMarkerDoubleClick : undefined}
445
+ >
368
446
  {local.item.expandable ? (
369
447
  <ChevronRight
370
448
  aria-hidden="true"
@@ -412,6 +490,19 @@ export function TreeRowActions(props: ComponentProps<'span'> & { pinned?: boolea
412
490
  )
413
491
  }
414
492
 
493
+ // Pressing the chevron must not move focus to its row: a mousedown on a
494
+ // non-focusable child would otherwise focus the row, making it the roving stop
495
+ // (and notifying `onActiveIdChange`) for what is a disclosure toggle, not a
496
+ // selection. The press itself leaves focus where it was, so Left/Right keep
497
+ // acting on the row the keyboard user left off at. It cannot stop the toggle
498
+ // from hiding that row; `Tree` then recovers focus onto the first visible row.
499
+ const handleMarkerMouseDown: JSX.EventHandler<HTMLSpanElement, MouseEvent> = (event) =>
500
+ event.preventDefault()
501
+ // A double-click is two toggles; it must not also reach `onDoubleClick`
502
+ // (rename, open) as though the row itself had been double-clicked.
503
+ const handleMarkerDoubleClick: JSX.EventHandler<HTMLSpanElement, MouseEvent> = (event) =>
504
+ event.stopPropagation()
505
+
415
506
  function isActionTarget(target: EventTarget | null, currentTarget: HTMLElement): boolean {
416
507
  if (!(target instanceof Element) || target === currentTarget) return false
417
508
  return Boolean(
@@ -17,10 +17,11 @@ import { cn } from '../../../lib/utils'
17
17
  /**
18
18
  * AlertDialog.
19
19
  *
20
- * A dialog that cannot be dismissed by accident: Escape and a click on the scrim
21
- * do nothing, and the user must choose one of the actions. That is the whole
22
- * difference from Dialog, and it is why this exists as its own component rather
23
- * than a prop — a caller has to reach for it deliberately.
20
+ * A dialog that cannot be dismissed by accident: a click on the scrim does nothing,
21
+ * and the user must choose one of the actions. Escape is the keyboard's Cancel: it
22
+ * closes the dialog exactly as the cancel action does, so nothing is confirmed. That
23
+ * is the whole difference from Dialog, and it is why this exists as its own
24
+ * component rather than a prop — a caller has to reach for it deliberately.
24
25
  *
25
26
  * The consequence is a rule of use: this is for confirming a destructive or
26
27
  * irreversible step. Wrapping an ordinary form in an AlertDialog traps someone who
@@ -59,22 +60,26 @@ export function AlertDialogOverlay(props: ComponentProps<typeof KobalteAlertDial
59
60
  * The panel.
60
61
  *
61
62
  * Kobalte's `AlertDialog.Content` is `Dialog.Content` with `role="alertdialog"`
62
- * and nothing else — Escape and an outside click dismiss it exactly as they
63
- * dismiss a Dialog. That is a real gap in the primitive, so the blocking is
64
- * implemented here, and it is the whole difference between this component and a
65
- * Dialog:
63
+ * and nothing else — an outside click dismisses it exactly as it dismisses a
64
+ * Dialog. That is a real gap in the primitive, so the blocking is implemented
65
+ * here, and it is the whole difference between this component and a Dialog:
66
66
  *
67
- * - Escape is prevented, because a key that closes a confirmation is a key that
68
- * makes "confirm" meaningless.
69
67
  * - Pointer and focus interactions outside are prevented, including the scrim
70
- * click, for the same reason.
68
+ * click, because a stray click that closes a confirmation is a click that
69
+ * makes "confirm" meaningless.
70
+ *
71
+ * Escape is deliberately left to the primitive. It can only ever cancel, never
72
+ * confirm, so it is not an accident that matters; refusing it traps a keyboard
73
+ * user who has changed their mind, and the WAI-ARIA alertdialog pattern says
74
+ * Escape closes. It reports `onOpenChange(false)` and restores focus to the
75
+ * trigger, which is what the cancel action does, and as the topmost layer it
76
+ * closes only the confirmation, not a dialog underneath it.
71
77
  *
72
- * There is deliberately no prop to turn this off. A surface that can be escaped
78
+ * There is deliberately no prop to turn the scrim blocking off. A surface that can be escaped
73
79
  * accidentally does not need to be an AlertDialog; it is a Dialog, and using this
74
80
  * one would be a claim about the flow that is not true.
75
81
  */
76
82
  const blockDismissal = {
77
- onEscapeKeyDown: (event: KeyboardEvent) => event.preventDefault(),
78
83
  onPointerDownOutside: (event: Event) => event.preventDefault(),
79
84
  onFocusOutside: (event: Event) => event.preventDefault(),
80
85
  onInteractOutside: (event: Event) => event.preventDefault(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adea-ai/ui",
3
- "version": "0.136.0",
3
+ "version": "0.138.0",
4
4
  "description": "The Adea design system: Kobalte-backed Solid components, semantic OKLCH tokens, and the app-shell layout primitives shared by Adea and Cortana.",
5
5
  "keywords": [
6
6
  "adea",
@@ -1,3 +1,10 @@
1
1
  /** Keyboard-accessible tree rows with virtualization-aware focus handoff. */
2
- export { Tree, TreeRow, TreeRowActions, type TreeProps, type TreeRowProps } from './tree'
2
+ export {
3
+ Tree,
4
+ TreeRow,
5
+ TreeRowActions,
6
+ type TreeDisclosure,
7
+ type TreeProps,
8
+ type TreeRowProps,
9
+ } from './tree'
3
10
  export type { TreeItemDescriptor } from './tree-navigation'
@@ -26,6 +26,8 @@ type TreeContextValue = {
26
26
  focusItem: (id: string) => void
27
27
  noteFocus: (id: string) => void
28
28
  activate: (id: string, event: Event) => void
29
+ expand: (id: string, expanded: boolean) => void
30
+ disclosure: () => TreeDisclosure
29
31
  onActiveIdChange: (id: string) => void
30
32
  }
31
33
 
@@ -37,6 +39,19 @@ function useTreeContext() {
37
39
  return tree
38
40
  }
39
41
 
42
+ /**
43
+ * Who owns the pointer on a row's disclosure chevron.
44
+ *
45
+ * - `row` (default): the chevron is decoration; a click anywhere on the row,
46
+ * chevron included, is a row activation. This is the file-explorer
47
+ * convention, where the host's `onActivate` toggles a folder.
48
+ * - `marker`: the chevron of an expandable row is its own pointer target. It
49
+ * calls `onExpand(id, !expanded)` and nothing else; the rest of the row still
50
+ * activates. For trees where expanding and selecting are independent (a
51
+ * source list that selects a source and separately reveals its details).
52
+ */
53
+ export type TreeDisclosure = 'row' | 'marker'
54
+
40
55
  type TreeAccessibleName =
41
56
  | { 'aria-label': string; 'aria-labelledby'?: string }
42
57
  | { 'aria-label'?: string; 'aria-labelledby': string }
@@ -50,10 +65,19 @@ export type TreeProps = Omit<
50
65
  visibleItems: readonly TreeItemDescriptor[]
51
66
  /** The host-owned identity of the row that currently owns roving focus. */
52
67
  activeId: string | null
53
- /** Called when keyboard or pointer navigation changes the active row. */
68
+ /**
69
+ * Called when keyboard or pointer navigation changes the active row, and when
70
+ * the active row leaves `visibleItems` (collapsed away, filtered out) and the
71
+ * first visible row takes over as the roving stop.
72
+ */
54
73
  onActiveIdChange: (id: string) => void
55
74
  /** Called for Left/Right expand and collapse requests. */
56
75
  onExpand?: (id: string, expanded: boolean) => void
76
+ /**
77
+ * Opt-in pointer behaviour of the disclosure chevron; see {@link TreeDisclosure}.
78
+ * Keyboard behaviour is identical in both modes: Left/Right expand and collapse.
79
+ */
80
+ disclosure?: TreeDisclosure
57
81
  /** Called for Enter or pointer activation. Domain action stays host-owned. */
58
82
  onActivate?: (id: string, event: Event) => void
59
83
  /** Called for Space selection. Selection state stays host-owned. */
@@ -78,6 +102,20 @@ export type TreeProps = Omit<
78
102
  *
79
103
  * Kobalte 0.13.14 has no Tree primitive, so this component implements the
80
104
  * WAI-ARIA tree row navigation contract and leaves product state to callbacks.
105
+ *
106
+ * By default the disclosure chevron is decoration and a click anywhere on a
107
+ * row activates it, so a file explorer can toggle a folder from `onActivate`.
108
+ * Reach for `disclosure="marker"` when expanding and selecting are separate
109
+ * decisions: the chevron then calls only `onExpand`, and the rest of the row
110
+ * calls only `onActivate`. The chevron stays `aria-hidden` and out of the tab
111
+ * order in both modes; keyboard expansion is Left/Right either way.
112
+ *
113
+ * Collapsing is the one thing that can move focus without a key or a click on
114
+ * the target row: when the active row leaves `visibleItems`, the first visible
115
+ * row becomes the roving stop and `onActiveIdChange` is called with it. If the
116
+ * removed row held focus, or was the last row the tree saw focused, focus moves
117
+ * to that same row; if it had no focus, only the roving stop moves. This is the
118
+ * same in both `disclosure` modes and for every way a host collapses a row.
81
119
  */
82
120
  export function Tree(props: TreeProps) {
83
121
  const [local, rest] = splitProps(props, [
@@ -86,6 +124,7 @@ export function Tree(props: TreeProps) {
86
124
  'activeId',
87
125
  'onActiveIdChange',
88
126
  'onExpand',
127
+ 'disclosure',
89
128
  'onActivate',
90
129
  'onSelectItem',
91
130
  'onRequestReveal',
@@ -187,16 +226,22 @@ export function Tree(props: TreeProps) {
187
226
  focusedId = id
188
227
  },
189
228
  activate: (id, event) => local.onActivate?.(id, event),
229
+ expand: (id, expanded) => local.onExpand?.(id, expanded),
230
+ disclosure: () => local.disclosure ?? 'row',
190
231
  onActiveIdChange: local.onActiveIdChange,
191
232
  }
192
233
 
193
- // A filtered or removed active ID falls back to the first visible item. This
194
- // keeps one treeitem in the sequential tab order while notifying the host.
234
+ // A filtered, collapsed-away or removed active ID falls back to the first
235
+ // visible item. This keeps one treeitem in the sequential tab order while
236
+ // notifying the host.
195
237
  createEffect(() => {
196
238
  const activeId = effectiveActiveId()
197
239
  if (activeId === null) return
198
- // If a host filter removes the row that had DOM focus, move focus to the
199
- // same projection fallback that now owns the roving tab stop.
240
+ // If the projection removes the last row that took focus (a host filter, or
241
+ // an ancestor collapsing), move focus to the same fallback that now owns the
242
+ // roving tab stop. `focusedId` is not cleared when focus leaves the tree, so
243
+ // this also reclaims focus from a control outside it; that long-standing
244
+ // behaviour is deliberately left as it was.
200
245
  if (focusedId !== null && !index().byId.has(focusedId) && pendingFocusId !== activeId) {
201
246
  focusItem(activeId)
202
247
  } else if (local.activeId !== activeId) {
@@ -282,6 +327,15 @@ export type TreeRowProps = Omit<
282
327
  * Keeps the donor's disclosure marker, leading file/folder affordance, primary
283
328
  * label and trailing-action hierarchy while replacing the donor's button and
284
329
  * runtime callbacks with a shared treeitem and host-owned slots/actions.
330
+ *
331
+ * With `Tree`'s `disclosure="marker"` the chevron of an expandable row is a
332
+ * separate pointer target (`data-slot="tree-row-disclosure"`) that only calls
333
+ * `onExpand`. It is not focusable and has no role: the row remains the single
334
+ * focus stop, Enter and Space still activate and select, and Left/Right still
335
+ * expand and collapse, so the marker adds a pointer path and nothing else.
336
+ * Pressing it does not move focus or the roving stop to its row; if the toggle
337
+ * hides the focused row, `Tree` recovers focus as it does for any collapse (see
338
+ * the `Tree` comment and docs/tree.md).
285
339
  */
286
340
  export function TreeRow(props: TreeRowProps) {
287
341
  const tree = useTreeContext()
@@ -333,7 +387,15 @@ export function TreeRow(props: TreeRowProps) {
333
387
  }
334
388
 
335
389
  const position = () => tree.index().positionById.get(id())
336
-
390
+ // Only an expandable row has a chevron to press; a leaf's marker slot stays
391
+ // part of the row so a click there is an ordinary row click.
392
+ const markerTarget = () => tree.disclosure() === 'marker' && local.item.expandable
393
+
394
+ const handleMarkerClick: JSX.EventHandler<HTMLSpanElement, MouseEvent> = (event) => {
395
+ // The row's own handler would focus and activate the row.
396
+ event.stopPropagation()
397
+ tree.expand(id(), !local.item.expanded)
398
+ }
337
399
  return (
338
400
  <div
339
401
  {...rest}
@@ -364,7 +426,23 @@ export function TreeRow(props: TreeRowProps) {
364
426
  onClick={handleClick}
365
427
  onDblClick={handleDoubleClick}
366
428
  >
367
- <span class="flex size-3.5 shrink-0 items-center justify-center text-muted-foreground">
429
+ <span
430
+ data-slot={markerTarget() ? 'tree-row-disclosure' : undefined}
431
+ class={cn(
432
+ 'flex',
433
+ !markerTarget() && 'size-3.5',
434
+ 'shrink-0 items-center justify-center text-muted-foreground',
435
+ /* The glyph stays 14px wide but the target is not: it takes the row's full
436
+ height (a flex child's `self-stretch`, so it follows row density and a
437
+ tall trailing control) and the pseudo-element widens it to 26px, which
438
+ stops at the row's own padding and gap, never over the icon or label. */
439
+ markerTarget() &&
440
+ 'relative w-3.5 cursor-pointer self-stretch hover:text-foreground after:absolute after:inset-y-0 after:-inset-x-1.5'
441
+ )}
442
+ onMouseDown={markerTarget() ? handleMarkerMouseDown : undefined}
443
+ onClick={markerTarget() ? handleMarkerClick : undefined}
444
+ onDblClick={markerTarget() ? handleMarkerDoubleClick : undefined}
445
+ >
368
446
  {local.item.expandable ? (
369
447
  <ChevronRight
370
448
  aria-hidden="true"
@@ -412,6 +490,19 @@ export function TreeRowActions(props: ComponentProps<'span'> & { pinned?: boolea
412
490
  )
413
491
  }
414
492
 
493
+ // Pressing the chevron must not move focus to its row: a mousedown on a
494
+ // non-focusable child would otherwise focus the row, making it the roving stop
495
+ // (and notifying `onActiveIdChange`) for what is a disclosure toggle, not a
496
+ // selection. The press itself leaves focus where it was, so Left/Right keep
497
+ // acting on the row the keyboard user left off at. It cannot stop the toggle
498
+ // from hiding that row; `Tree` then recovers focus onto the first visible row.
499
+ const handleMarkerMouseDown: JSX.EventHandler<HTMLSpanElement, MouseEvent> = (event) =>
500
+ event.preventDefault()
501
+ // A double-click is two toggles; it must not also reach `onDoubleClick`
502
+ // (rename, open) as though the row itself had been double-clicked.
503
+ const handleMarkerDoubleClick: JSX.EventHandler<HTMLSpanElement, MouseEvent> = (event) =>
504
+ event.stopPropagation()
505
+
415
506
  function isActionTarget(target: EventTarget | null, currentTarget: HTMLElement): boolean {
416
507
  if (!(target instanceof Element) || target === currentTarget) return false
417
508
  return Boolean(
@@ -17,10 +17,11 @@ import { cn } from '#lib/utils'
17
17
  /**
18
18
  * AlertDialog.
19
19
  *
20
- * A dialog that cannot be dismissed by accident: Escape and a click on the scrim
21
- * do nothing, and the user must choose one of the actions. That is the whole
22
- * difference from Dialog, and it is why this exists as its own component rather
23
- * than a prop — a caller has to reach for it deliberately.
20
+ * A dialog that cannot be dismissed by accident: a click on the scrim does nothing,
21
+ * and the user must choose one of the actions. Escape is the keyboard's Cancel: it
22
+ * closes the dialog exactly as the cancel action does, so nothing is confirmed. That
23
+ * is the whole difference from Dialog, and it is why this exists as its own
24
+ * component rather than a prop — a caller has to reach for it deliberately.
24
25
  *
25
26
  * The consequence is a rule of use: this is for confirming a destructive or
26
27
  * irreversible step. Wrapping an ordinary form in an AlertDialog traps someone who
@@ -59,22 +60,26 @@ export function AlertDialogOverlay(props: ComponentProps<typeof KobalteAlertDial
59
60
  * The panel.
60
61
  *
61
62
  * Kobalte's `AlertDialog.Content` is `Dialog.Content` with `role="alertdialog"`
62
- * and nothing else — Escape and an outside click dismiss it exactly as they
63
- * dismiss a Dialog. That is a real gap in the primitive, so the blocking is
64
- * implemented here, and it is the whole difference between this component and a
65
- * Dialog:
63
+ * and nothing else — an outside click dismisses it exactly as it dismisses a
64
+ * Dialog. That is a real gap in the primitive, so the blocking is implemented
65
+ * here, and it is the whole difference between this component and a Dialog:
66
66
  *
67
- * - Escape is prevented, because a key that closes a confirmation is a key that
68
- * makes "confirm" meaningless.
69
67
  * - Pointer and focus interactions outside are prevented, including the scrim
70
- * click, for the same reason.
68
+ * click, because a stray click that closes a confirmation is a click that
69
+ * makes "confirm" meaningless.
70
+ *
71
+ * Escape is deliberately left to the primitive. It can only ever cancel, never
72
+ * confirm, so it is not an accident that matters; refusing it traps a keyboard
73
+ * user who has changed their mind, and the WAI-ARIA alertdialog pattern says
74
+ * Escape closes. It reports `onOpenChange(false)` and restores focus to the
75
+ * trigger, which is what the cancel action does, and as the topmost layer it
76
+ * closes only the confirmation, not a dialog underneath it.
71
77
  *
72
- * There is deliberately no prop to turn this off. A surface that can be escaped
78
+ * There is deliberately no prop to turn the scrim blocking off. A surface that can be escaped
73
79
  * accidentally does not need to be an AlertDialog; it is a Dialog, and using this
74
80
  * one would be a claim about the flow that is not true.
75
81
  */
76
82
  const blockDismissal = {
77
- onEscapeKeyDown: (event: KeyboardEvent) => event.preventDefault(),
78
83
  onPointerDownOutside: (event: Event) => event.preventDefault(),
79
84
  onFocusOutside: (event: Event) => event.preventDefault(),
80
85
  onInteractOutside: (event: Event) => event.preventDefault(),