@canonical/utils 0.29.0-experimental.0 → 0.30.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.
- package/dist/esm/lib/navigation/annotateTree.js +1 -1
- package/dist/esm/lib/navigation/annotateTree.js.map +1 -1
- package/dist/esm/lib/navigation/createCrossGroupStateReducer.js +128 -0
- package/dist/esm/lib/navigation/createCrossGroupStateReducer.js.map +1 -0
- package/dist/esm/lib/navigation/getFirstEnabledLeaf.js +42 -0
- package/dist/esm/lib/navigation/getFirstEnabledLeaf.js.map +1 -0
- package/dist/esm/lib/navigation/index.js +2 -0
- package/dist/esm/lib/navigation/index.js.map +1 -1
- package/dist/esm/lib/navigation/prepareIndex.js +4 -2
- package/dist/esm/lib/navigation/prepareIndex.js.map +1 -1
- package/dist/types/lib/navigation/createCrossGroupStateReducer.d.ts +5 -0
- package/dist/types/lib/navigation/createCrossGroupStateReducer.d.ts.map +1 -0
- package/dist/types/lib/navigation/getFirstEnabledLeaf.d.ts +22 -0
- package/dist/types/lib/navigation/getFirstEnabledLeaf.d.ts.map +1 -0
- package/dist/types/lib/navigation/index.d.ts +2 -0
- package/dist/types/lib/navigation/index.d.ts.map +1 -1
- package/dist/types/lib/navigation/prepareIndex.d.ts.map +1 -1
- package/package.json +6 -6
|
@@ -20,7 +20,7 @@ export function annotateTree(root, parentUrl = null, depth = 0) {
|
|
|
20
20
|
};
|
|
21
21
|
if (items) {
|
|
22
22
|
// The tree is homogeneous: every node is the same item type `T`. The base
|
|
23
|
-
// `Item
|
|
23
|
+
// `Item.items` is typed `Item[]`, so narrow children back to `T`
|
|
24
24
|
// (enhanced item types like `NavItem` already declare `items?: NavItem[]`).
|
|
25
25
|
annotated.items = items.map((child) => annotateTree(child, getItemId(root), depth + 1));
|
|
26
26
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"annotateTree.js","sourceRoot":"","sources":["../../../../src/lib/navigation/annotateTree.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAO,EACP,YAA2B,IAAI,EAC/B,KAAK,GAAG,CAAC;IAET,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IAChC,6EAA6E;IAC7E,gFAAgF;IAChF,MAAM,SAAS,GAAG;QAChB,GAAG,IAAI;QACP,SAAS;QACT,KAAK;KACM,CAAC;IACd,IAAI,KAAK,EAAE,CAAC;QACV,0EAA0E;QAC1E,
|
|
1
|
+
{"version":3,"file":"annotateTree.js","sourceRoot":"","sources":["../../../../src/lib/navigation/annotateTree.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAO,EACP,YAA2B,IAAI,EAC/B,KAAK,GAAG,CAAC;IAET,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IAChC,6EAA6E;IAC7E,gFAAgF;IAChF,MAAM,SAAS,GAAG;QAChB,GAAG,IAAI;QACP,SAAS;QACT,KAAK;KACM,CAAC;IACd,IAAI,KAAK,EAAE,CAAC;QACV,0EAA0E;QAC1E,iEAAiE;QACjE,4EAA4E;QAC5E,SAAS,CAAC,KAAK,GAAI,KAAa,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAC7C,YAAY,CAAI,KAAK,EAAE,SAAS,CAAC,IAAI,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CACnD,CAAC;IACJ,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import getFirstEnabledChild from "./getFirstEnabledChild.js";
|
|
2
|
+
import getLastEnabledChild from "./getLastEnabledChild.js";
|
|
3
|
+
import { NavigationActionType, } from "./navigationTypes.js";
|
|
4
|
+
/**
|
|
5
|
+
* Scan the groups adjacent to `group` (skipping disabled or empty ones) and
|
|
6
|
+
* return the group to land in, in the given direction.
|
|
7
|
+
* @param groups The sibling groups (children of the menu root).
|
|
8
|
+
* @param group The current group being left.
|
|
9
|
+
* @param direction 1 for the next group, -1 for the previous.
|
|
10
|
+
* @returns [adjacentGroup, landingItem] or undefined at the tree's edge.
|
|
11
|
+
*/
|
|
12
|
+
const findAdjacentLanding = (groups, group, direction) => {
|
|
13
|
+
let cursor = groups.indexOf(group) + direction;
|
|
14
|
+
while (cursor >= 0 && cursor < groups.length) {
|
|
15
|
+
const candidate = groups.at(cursor);
|
|
16
|
+
if (candidate && !candidate.disabled) {
|
|
17
|
+
// Enter a forward group at its first item, a backward group at its last.
|
|
18
|
+
const landing = direction === 1
|
|
19
|
+
? getFirstEnabledChild(candidate)
|
|
20
|
+
: getLastEnabledChild(candidate);
|
|
21
|
+
if (landing)
|
|
22
|
+
return [candidate, landing];
|
|
23
|
+
}
|
|
24
|
+
cursor += direction;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Create a `stateReducer` for `useNavigationTree` that lets vertical arrow keys
|
|
30
|
+
* cross group boundaries. Within a group, {@link createNavigationReducer}
|
|
31
|
+
* already moves between items; at a group edge its `getSibling` finds no sibling
|
|
32
|
+
* and leaves the highlight unchanged. This reducer detects that edge structurally
|
|
33
|
+
* (the highlighted item is the first/last enabled child of its group) and moves
|
|
34
|
+
* the highlight to the adjacent group's boundary item.
|
|
35
|
+
*
|
|
36
|
+
* It operates purely on `state.highlightedItems` — the ancestor path
|
|
37
|
+
* `[root, group, item]` — so it uses the same annotated item instances the
|
|
38
|
+
* navigation tree holds internally. It needs no external index, which avoids the
|
|
39
|
+
* instance-mismatch that would arise from re-annotating the tree separately.
|
|
40
|
+
*
|
|
41
|
+
* The group boundary logic lives entirely here, so the base navigation reducer
|
|
42
|
+
* stays group-agnostic. It suits any grouped, vertically-navigated widget
|
|
43
|
+
* (contextual menus, sectioned side navigation) built on a
|
|
44
|
+
* root -> group -> item tree.
|
|
45
|
+
*
|
|
46
|
+
* @returns A `(state, action) => state` reducer to pass as `stateReducer`.
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* Type-ahead across group boundaries. The base reducer only searches the
|
|
50
|
+
* current item's siblings (one group), so typing a letter that matches an item
|
|
51
|
+
* in ANOTHER group finds nothing. This searches every group's items — from just
|
|
52
|
+
* after the current item, wrapping — for the first enabled item whose label
|
|
53
|
+
* matches `keysSoFar`, and moves the highlight there.
|
|
54
|
+
* @returns The updated state, or the input state if there is no match.
|
|
55
|
+
*/
|
|
56
|
+
const handleCrossGroupTypeAhead = (state) => {
|
|
57
|
+
const path = state.highlightedItems;
|
|
58
|
+
const current = path.at(-1);
|
|
59
|
+
const group = path.at(-2);
|
|
60
|
+
const root = path.at(-3);
|
|
61
|
+
if (!current || !group || !root?.items)
|
|
62
|
+
return state;
|
|
63
|
+
const search = state.keysSoFar.toLowerCase();
|
|
64
|
+
if (!search)
|
|
65
|
+
return state;
|
|
66
|
+
// If the current item already matches, the base reducer handled it in-group.
|
|
67
|
+
if (current.label?.toLowerCase().startsWith(search))
|
|
68
|
+
return state;
|
|
69
|
+
// Flatten every group's enabled items into one ordered list, remembering each
|
|
70
|
+
// item's group so the highlight path can be rebuilt from the tree's instances.
|
|
71
|
+
const entries = [];
|
|
72
|
+
for (const g of root.items) {
|
|
73
|
+
if (g.disabled)
|
|
74
|
+
continue;
|
|
75
|
+
for (const it of g.items ?? []) {
|
|
76
|
+
if (!it.disabled)
|
|
77
|
+
entries.push({ group: g, item: it });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const currentIndex = entries.findIndex((e) => e.item === current);
|
|
81
|
+
const start = currentIndex + 1;
|
|
82
|
+
for (let i = 0; i < entries.length; i++) {
|
|
83
|
+
const entry = entries[(start + i) % entries.length];
|
|
84
|
+
if (entry.item.label?.toLowerCase().startsWith(search)) {
|
|
85
|
+
return {
|
|
86
|
+
...state,
|
|
87
|
+
highlightedItems: [...path.slice(0, -2), entry.group, entry.item],
|
|
88
|
+
currentDepth: entry.item.depth,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return state;
|
|
93
|
+
};
|
|
94
|
+
const createCrossGroupStateReducer = () => (state, action) => {
|
|
95
|
+
if (action.type === NavigationActionType.TYPE_AHEAD) {
|
|
96
|
+
return handleCrossGroupTypeAhead(state);
|
|
97
|
+
}
|
|
98
|
+
const isDown = action.type === NavigationActionType.ARROW_DOWN;
|
|
99
|
+
const isUp = action.type === NavigationActionType.ARROW_UP;
|
|
100
|
+
if (!isDown && !isUp)
|
|
101
|
+
return state;
|
|
102
|
+
// The highlighted path is [root, group, item] for a grouped menu.
|
|
103
|
+
const path = state.highlightedItems;
|
|
104
|
+
const current = path.at(-1);
|
|
105
|
+
const group = path.at(-2);
|
|
106
|
+
const root = path.at(-3);
|
|
107
|
+
// Only items nested inside a group (root -> group -> item) can cross groups.
|
|
108
|
+
if (!current || !group || !root?.items)
|
|
109
|
+
return state;
|
|
110
|
+
const boundaryChild = isDown
|
|
111
|
+
? getLastEnabledChild(group)
|
|
112
|
+
: getFirstEnabledChild(group);
|
|
113
|
+
// Not at the group edge — the base reducer already moved within the group.
|
|
114
|
+
if (boundaryChild !== current)
|
|
115
|
+
return state;
|
|
116
|
+
const landing = findAdjacentLanding(root.items, group, isDown ? 1 : -1);
|
|
117
|
+
if (!landing)
|
|
118
|
+
return state;
|
|
119
|
+
const [adjacentGroup, target] = landing;
|
|
120
|
+
return {
|
|
121
|
+
...state,
|
|
122
|
+
// Build the path from the known chain — same instances the tree holds.
|
|
123
|
+
highlightedItems: [...path.slice(0, -2), adjacentGroup, target],
|
|
124
|
+
currentDepth: target.depth,
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
export default createCrossGroupStateReducer;
|
|
128
|
+
//# sourceMappingURL=createCrossGroupStateReducer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createCrossGroupStateReducer.js","sourceRoot":"","sources":["../../../../src/lib/navigation/createCrossGroupStateReducer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Descend to the first enabled LEAF of a subtree — the first enabled node that
|
|
3
|
+
* has no enabled children of its own.
|
|
4
|
+
*
|
|
5
|
+
* The point of difference from {@link getFirstEnabledChild} is the structural
|
|
6
|
+
* group layer of a contextual menu. A menu is a `root -> group -> item` tree:
|
|
7
|
+
* the root's direct children are GROUPS (rendered `role="group"`, never
|
|
8
|
+
* focusable), and the real `role="menuitem"` nodes live one level deeper.
|
|
9
|
+
* Highlighting the first *child* of the root lands on a group, so no item
|
|
10
|
+
* becomes the roving tab stop and arrow keys have no current item to move from.
|
|
11
|
+
* Descending to the first leaf lands on the first real menuitem instead.
|
|
12
|
+
*
|
|
13
|
+
* For a flat `root -> item` tree (e.g. a simple menu with no groups) the first
|
|
14
|
+
* child is already a leaf, so this returns exactly what
|
|
15
|
+
* {@link getFirstEnabledChild} would — no behaviour change.
|
|
16
|
+
*
|
|
17
|
+
* @param item The subtree root to descend from (typically the menu root).
|
|
18
|
+
* @returns The first enabled leaf, or `undefined` if the subtree has none.
|
|
19
|
+
*/
|
|
20
|
+
export default function getFirstEnabledLeaf(item) {
|
|
21
|
+
// Walk the enabled children in order; recurse into any that themselves have
|
|
22
|
+
// children (a group), and backtrack to the next sibling if a branch dead-ends
|
|
23
|
+
// (e.g. a group whose items are all disabled). An enabled child with no
|
|
24
|
+
// children of its own IS the leaf we want.
|
|
25
|
+
for (const child of item.items ?? []) {
|
|
26
|
+
if (child.disabled)
|
|
27
|
+
continue;
|
|
28
|
+
if (child.items?.length) {
|
|
29
|
+
// A container (group / submenu parent): descend, but keep scanning
|
|
30
|
+
// siblings if this branch has no enabled leaf (e.g. all items disabled).
|
|
31
|
+
const leaf = getFirstEnabledLeaf(child);
|
|
32
|
+
if (leaf)
|
|
33
|
+
return leaf;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
// An enabled node with no children of its own — the first real leaf.
|
|
37
|
+
return child;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=getFirstEnabledLeaf.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getFirstEnabledLeaf.js","sourceRoot":"","sources":["../../../../src/lib/navigation/getFirstEnabledLeaf.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,OAAO,UAAU,mBAAmB,CACzC,IAAc;IAEd,4EAA4E;IAC5E,8EAA8E;IAC9E,wEAAwE;IACxE,2CAA2C;IAC3C,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;QACrC,IAAI,KAAK,CAAC,QAAQ;YAAE,SAAS;QAC7B,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;YACxB,mEAAmE;YACnE,yEAAyE;YACzE,MAAM,IAAI,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;YACxC,IAAI,IAAI;gBAAE,OAAO,IAAI,CAAC;QACxB,CAAC;aAAM,CAAC;YACN,qEAAqE;YACrE,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { annotateTree } from "./annotateTree.js";
|
|
2
|
+
export { default as createCrossGroupStateReducer } from "./createCrossGroupStateReducer.js";
|
|
2
3
|
export { default as createNavigationReducer } from "./createNavigationReducer.js";
|
|
3
4
|
export { default as findAncestorPath } from "./findAncestorPath.js";
|
|
4
5
|
export { default as getFirstEnabledChild } from "./getFirstEnabledChild.js";
|
|
6
|
+
export { default as getFirstEnabledLeaf } from "./getFirstEnabledLeaf.js";
|
|
5
7
|
export { getItemId } from "./getItemId.js";
|
|
6
8
|
export { default as getLastEnabledChild } from "./getLastEnabledChild.js";
|
|
7
9
|
export { default as getParentItem } from "./getParentItem.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/lib/navigation/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/lib/navigation/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AAE5F,OAAO,EAAE,OAAO,IAAI,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAClF,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAAE,OAAO,IAAI,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAC5E,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC1E,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAQ9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,yBAAyB,CAAC"}
|
|
@@ -8,8 +8,10 @@ import { getItemId } from "./getItemId.js";
|
|
|
8
8
|
export function prepareIndex(root) {
|
|
9
9
|
const index = {};
|
|
10
10
|
const stack = [root];
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// Drain the stack by popping into the loop condition: `item` narrows to a
|
|
12
|
+
// defined `_Item<T>` inside the loop with no non-null assertion, and the
|
|
13
|
+
// only exit is the empty-stack `undefined` — no unreachable guard branch.
|
|
14
|
+
for (let item = stack.pop(); item !== undefined; item = stack.pop()) {
|
|
13
15
|
const id = getItemId(item);
|
|
14
16
|
index[id] = item;
|
|
15
17
|
if (item.items) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prepareIndex.js","sourceRoot":"","sources":["../../../../src/lib/navigation/prepareIndex.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAwB,IAAc;IAChE,MAAM,KAAK,GAAc,EAAE,CAAC;IAC5B,MAAM,KAAK,GAAe,CAAC,IAAI,CAAC,CAAC;IACjC,
|
|
1
|
+
{"version":3,"file":"prepareIndex.js","sourceRoot":"","sources":["../../../../src/lib/navigation/prepareIndex.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAwB,IAAc;IAChE,MAAM,KAAK,GAAc,EAAE,CAAC;IAC5B,MAAM,KAAK,GAAe,CAAC,IAAI,CAAC,CAAC;IACjC,0EAA0E;IAC1E,yEAAyE;IACzE,0EAA0E;IAC1E,KAAK,IAAI,IAAI,GAAG,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,KAAK,SAAS,EAAE,IAAI,GAAG,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC;QACpE,MAAM,EAAE,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QAC3B,KAAK,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC;QACjB,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;gBAChD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YAC5B,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Item } from "@canonical/ds-types";
|
|
2
|
+
import { type NavigationAction, type NavigationState } from "./navigationTypes.js";
|
|
3
|
+
declare const createCrossGroupStateReducer: <T extends Item = Item>() => (state: NavigationState<T>, action: NavigationAction<T>) => NavigationState<T>;
|
|
4
|
+
export default createCrossGroupStateReducer;
|
|
5
|
+
//# sourceMappingURL=createCrossGroupStateReducer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createCrossGroupStateReducer.d.ts","sourceRoot":"","sources":["../../../../src/lib/navigation/createCrossGroupStateReducer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAGvD,OAAO,EACL,KAAK,gBAAgB,EAErB,KAAK,eAAe,EACrB,MAAM,sBAAsB,CAAC;AAmG9B,QAAA,MAAM,4BAA4B,GAC/B,CAAC,SAAS,IAAI,GAAG,IAAI,QAEpB,OAAO,eAAe,CAAC,CAAC,CAAC,EACzB,QAAQ,gBAAgB,CAAC,CAAC,CAAC,KAC1B,eAAe,CAAC,CAAC,CAiCnB,CAAC;AAEJ,eAAe,4BAA4B,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { _Item, Item } from "@canonical/ds-types";
|
|
2
|
+
/**
|
|
3
|
+
* Descend to the first enabled LEAF of a subtree — the first enabled node that
|
|
4
|
+
* has no enabled children of its own.
|
|
5
|
+
*
|
|
6
|
+
* The point of difference from {@link getFirstEnabledChild} is the structural
|
|
7
|
+
* group layer of a contextual menu. A menu is a `root -> group -> item` tree:
|
|
8
|
+
* the root's direct children are GROUPS (rendered `role="group"`, never
|
|
9
|
+
* focusable), and the real `role="menuitem"` nodes live one level deeper.
|
|
10
|
+
* Highlighting the first *child* of the root lands on a group, so no item
|
|
11
|
+
* becomes the roving tab stop and arrow keys have no current item to move from.
|
|
12
|
+
* Descending to the first leaf lands on the first real menuitem instead.
|
|
13
|
+
*
|
|
14
|
+
* For a flat `root -> item` tree (e.g. a simple menu with no groups) the first
|
|
15
|
+
* child is already a leaf, so this returns exactly what
|
|
16
|
+
* {@link getFirstEnabledChild} would — no behaviour change.
|
|
17
|
+
*
|
|
18
|
+
* @param item The subtree root to descend from (typically the menu root).
|
|
19
|
+
* @returns The first enabled leaf, or `undefined` if the subtree has none.
|
|
20
|
+
*/
|
|
21
|
+
export default function getFirstEnabledLeaf<T extends Item = Item>(item: _Item<T>): _Item<T> | undefined;
|
|
22
|
+
//# sourceMappingURL=getFirstEnabledLeaf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getFirstEnabledLeaf.d.ts","sourceRoot":"","sources":["../../../../src/lib/navigation/getFirstEnabledLeaf.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAEvD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,OAAO,UAAU,mBAAmB,CAAC,CAAC,SAAS,IAAI,GAAG,IAAI,EAC/D,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,GACb,KAAK,CAAC,CAAC,CAAC,GAAG,SAAS,CAkBtB"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export { annotateTree } from "./annotateTree.js";
|
|
2
|
+
export { default as createCrossGroupStateReducer } from "./createCrossGroupStateReducer.js";
|
|
2
3
|
export type { NavigationReducerOptions } from "./createNavigationReducer.js";
|
|
3
4
|
export { default as createNavigationReducer } from "./createNavigationReducer.js";
|
|
4
5
|
export { default as findAncestorPath } from "./findAncestorPath.js";
|
|
5
6
|
export { default as getFirstEnabledChild } from "./getFirstEnabledChild.js";
|
|
7
|
+
export { default as getFirstEnabledLeaf } from "./getFirstEnabledLeaf.js";
|
|
6
8
|
export { getItemId } from "./getItemId.js";
|
|
7
9
|
export { default as getLastEnabledChild } from "./getLastEnabledChild.js";
|
|
8
10
|
export { default as getParentItem } from "./getParentItem.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/navigation/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AAC7E,OAAO,EAAE,OAAO,IAAI,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAClF,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAAE,OAAO,IAAI,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAC5E,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC1E,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAC9D,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,UAAU,EACV,WAAW,EACX,iBAAiB,GAClB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,yBAAyB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/navigation/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AAC5F,YAAY,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AAC7E,OAAO,EAAE,OAAO,IAAI,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAClF,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAAE,OAAO,IAAI,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAC5E,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC1E,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAC9D,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,UAAU,EACV,WAAW,EACX,iBAAiB,GAClB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,yBAAyB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prepareIndex.d.ts","sourceRoot":"","sources":["../../../../src/lib/navigation/prepareIndex.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAG/D;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,IAAI,GAAG,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"prepareIndex.d.ts","sourceRoot":"","sources":["../../../../src/lib/navigation/prepareIndex.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAG/D;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,IAAI,GAAG,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAgB7E"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonical/utils",
|
|
3
3
|
"description": "Standard utility functions for Canonical's Web Engineering team",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.30.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
7
7
|
"types": "dist/types/index.d.ts",
|
|
@@ -45,18 +45,18 @@
|
|
|
45
45
|
"test:vitest:watch": "vitest"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@canonical/ds-types": "^0.
|
|
48
|
+
"@canonical/ds-types": "^0.30.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@biomejs/biome": "2.4.9",
|
|
52
|
-
"@canonical/biome-config": "^0.
|
|
53
|
-
"@canonical/typescript-config": "^0.
|
|
54
|
-
"@canonical/webarchitect": "^0.
|
|
52
|
+
"@canonical/biome-config": "^0.30.0",
|
|
53
|
+
"@canonical/typescript-config": "^0.30.0",
|
|
54
|
+
"@canonical/webarchitect": "^0.30.0",
|
|
55
55
|
"@types/node": "^24.12.0",
|
|
56
56
|
"globals": "^17.4.0",
|
|
57
57
|
"typescript": "^5.9.3",
|
|
58
58
|
"vite": "^8.0.1",
|
|
59
59
|
"vitest": "^4.0.18"
|
|
60
60
|
},
|
|
61
|
-
"gitHead": "
|
|
61
|
+
"gitHead": "40fe86ac955473536ac14f074812a9821f5b65c0"
|
|
62
62
|
}
|