@junoput01/junoui 0.6.0 → 0.8.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/CHANGELOG.md +209 -0
- package/README.md +17 -16
- package/dist/android/dimens.xml +2 -0
- package/dist/classes.json +1774 -0
- package/dist/css/juno-tokens.css +10 -0
- package/dist/css/juno.css +1057 -56
- package/dist/flutter/juno_tokens.dart +10 -0
- package/dist/ios/JunoTokens.swift +10 -0
- package/dist/js/tokens.js +16 -0
- package/dist/json/tokens.json +51 -0
- package/dist/rust/juno_tokens.rs +383 -0
- package/dist/scss/_juno-tokens.scss +10 -0
- package/docs/accessibility.md +6 -0
- package/docs/browser-support.md +3 -0
- package/docs/components/button.md +11 -2
- package/docs/components/canvas-ink.md +71 -0
- package/docs/components/dock.md +34 -0
- package/docs/components/fold-slot.md +26 -1
- package/docs/components/gizmo.md +114 -0
- package/docs/components/swatch.md +95 -0
- package/docs/components/tree.md +112 -0
- package/docs/conformance-kit.md +255 -0
- package/docs/getting-started.md +14 -0
- package/docs/integration.md +52 -6
- package/docs/ios-conformance.md +200 -3
- package/docs/ios-pwa.md +273 -0
- package/docs/native.md +38 -1
- package/docs/tokens-reference.md +15 -0
- package/package.json +8 -2
- package/src/css/base.css +35 -43
- package/src/css/components/button.css +42 -2
- package/src/css/components/canvas-ink.css +97 -0
- package/src/css/components/dock.css +65 -6
- package/src/css/components/fold-slot.css +49 -3
- package/src/css/components/gizmo.css +238 -0
- package/src/css/components/segmented.css +15 -2
- package/src/css/components/swatch.css +187 -0
- package/src/css/components/tree.css +259 -0
- package/src/css/touch-surfaces.mjs +95 -0
- package/tools/gizmo.mjs +144 -0
- package/tools/testing.mjs +177 -0
- package/tools/tree.mjs +178 -0
package/tools/tree.mjs
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// junoui/tree — keyboard traversal for .juno-tree
|
|
3
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
4
|
+
// A tree without arrow-key traversal and a roving tabindex is a list of
|
|
5
|
+
// buttons wearing tree roles. That part is behaviour, so no stylesheet can
|
|
6
|
+
// ship it — and it is also the part apps get wrong, so junoui ships it
|
|
7
|
+
// rather than only describing it.
|
|
8
|
+
//
|
|
9
|
+
// import { enhanceTree } from 'junoui/tree';
|
|
10
|
+
// const stop = enhanceTree(document.querySelector('.juno-tree'));
|
|
11
|
+
//
|
|
12
|
+
// STATELESS, which is what keeps it inside junoui's "no stateful widgets"
|
|
13
|
+
// line: it stores nothing. Expansion lives in `aria-expanded` and selection
|
|
14
|
+
// in `aria-selected`, both on the DOM, both owned by your app — this reads
|
|
15
|
+
// them, moves focus, and asks you to change them by dispatching events.
|
|
16
|
+
// Unmount and remount the tree and nothing here needs to know.
|
|
17
|
+
//
|
|
18
|
+
// WHAT IT DOES NOT DO: it does not expand, collapse, select or reorder. It
|
|
19
|
+
// dispatches `juno-tree-toggle` and `juno-tree-select` (bubbling, cancelable,
|
|
20
|
+
// `detail.item`) and leaves the decision to you, because in a real outliner
|
|
21
|
+
// expanding a node may need to load it.
|
|
22
|
+
//
|
|
23
|
+
// WAI-ARIA Authoring Practices, Tree View pattern. See
|
|
24
|
+
// docs/components/tree.md for the full contract and the markup.
|
|
25
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
26
|
+
|
|
27
|
+
const ITEM = '[role="treeitem"]';
|
|
28
|
+
|
|
29
|
+
/** Is this item's subtree currently shown? Absent aria-expanded = a leaf. */
|
|
30
|
+
const isExpanded = (el) => el.getAttribute('aria-expanded') === 'true';
|
|
31
|
+
const isBranch = (el) => el.hasAttribute('aria-expanded');
|
|
32
|
+
|
|
33
|
+
/** Every treeitem the user can currently reach, in visual order.
|
|
34
|
+
*
|
|
35
|
+
* Derived from the DOM each time rather than cached: a tree's shape changes
|
|
36
|
+
* under it (lazy children, filtering, a reorder), and a cached list is a
|
|
37
|
+
* stale list the moment it does. Cheap — this only runs on a keypress. */
|
|
38
|
+
function visibleItems(root) {
|
|
39
|
+
return [...root.querySelectorAll(ITEM)].filter((el) => {
|
|
40
|
+
for (let p = el.parentElement?.closest(ITEM); p; p = p.parentElement?.closest(ITEM)) {
|
|
41
|
+
if (!isExpanded(p)) return false;
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The row inside an item — the thing that actually takes focus. */
|
|
48
|
+
const rowOf = (item) => item.querySelector(':scope > .juno-tree__row') ?? item;
|
|
49
|
+
|
|
50
|
+
/** Move the roving tabindex, and focus. Exactly one row is tabbable at a
|
|
51
|
+
* time; that is the difference between one Tab stop and one per node.
|
|
52
|
+
*
|
|
53
|
+
* The reset here and the one in `onFocusIn` are REDUNDANT ON PURPOSE, and
|
|
54
|
+
* mutation testing is what turned that into a decision: removing either alone
|
|
55
|
+
* leaves the invariant holding, because `row.focus()` below fires focusin.
|
|
56
|
+
* They cover different entrances — this one the keyboard, that one a pointer
|
|
57
|
+
* click, which never passes through here — so neither is dead, and a single
|
|
58
|
+
* edit to either cannot leave two rows tabbable. */
|
|
59
|
+
function focusItem(root, item) {
|
|
60
|
+
for (const other of root.querySelectorAll(ITEM)) rowOf(other).tabIndex = -1;
|
|
61
|
+
const row = rowOf(item);
|
|
62
|
+
row.tabIndex = 0;
|
|
63
|
+
row.focus();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const ask = (item, type) =>
|
|
67
|
+
item.dispatchEvent(new CustomEvent(type, { bubbles: true, cancelable: true, detail: { item } }));
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Wire keyboard traversal on a `.juno-tree`. Returns a teardown function.
|
|
71
|
+
*
|
|
72
|
+
* Idempotent: enhancing the same root twice replaces the first listener
|
|
73
|
+
* rather than stacking two, so a framework that re-runs an effect does not
|
|
74
|
+
* double-fire every keypress.
|
|
75
|
+
*/
|
|
76
|
+
export function enhanceTree(root) {
|
|
77
|
+
if (!root) throw new Error('enhanceTree: no root element');
|
|
78
|
+
root._junoTreeTeardown?.();
|
|
79
|
+
|
|
80
|
+
// Seed the roving tabindex: the selected item if there is one, else the
|
|
81
|
+
// first. Without this every row is tabbable (0) or none is (-1), and both
|
|
82
|
+
// are wrong.
|
|
83
|
+
const items = visibleItems(root);
|
|
84
|
+
if (items.length) {
|
|
85
|
+
for (const el of root.querySelectorAll(ITEM)) rowOf(el).tabIndex = -1;
|
|
86
|
+
const seed = items.find((el) => el.getAttribute('aria-selected') === 'true') ?? items[0];
|
|
87
|
+
rowOf(seed).tabIndex = 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const onKeyDown = (event) => {
|
|
91
|
+
const item = event.target.closest(ITEM);
|
|
92
|
+
if (!item || !root.contains(item)) return;
|
|
93
|
+
|
|
94
|
+
const list = visibleItems(root);
|
|
95
|
+
const at = list.indexOf(item);
|
|
96
|
+
const go = (next) => {
|
|
97
|
+
if (!next) return;
|
|
98
|
+
event.preventDefault();
|
|
99
|
+
focusItem(root, next);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
switch (event.key) {
|
|
103
|
+
case 'ArrowDown':
|
|
104
|
+
return go(list[at + 1]);
|
|
105
|
+
case 'ArrowUp':
|
|
106
|
+
return go(list[at - 1]);
|
|
107
|
+
|
|
108
|
+
case 'ArrowRight':
|
|
109
|
+
// Closed branch → open it. Open branch → into its first child. Leaf →
|
|
110
|
+
// nothing, which is the pattern's answer and not an oversight.
|
|
111
|
+
if (isBranch(item) && !isExpanded(item)) {
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
ask(item, 'juno-tree-toggle');
|
|
114
|
+
} else if (isExpanded(item)) {
|
|
115
|
+
go(list[at + 1]);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
|
|
119
|
+
case 'ArrowLeft': {
|
|
120
|
+
// Open branch → close it. Anything else → out to the parent. This is
|
|
121
|
+
// what makes a deep tree navigable without a mouse.
|
|
122
|
+
if (isBranch(item) && isExpanded(item)) {
|
|
123
|
+
event.preventDefault();
|
|
124
|
+
ask(item, 'juno-tree-toggle');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
return go(item.parentElement?.closest(ITEM));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
case 'Home':
|
|
131
|
+
return go(list[0]);
|
|
132
|
+
case 'End':
|
|
133
|
+
return go(list[list.length - 1]);
|
|
134
|
+
|
|
135
|
+
case 'Enter':
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
return void ask(item, 'juno-tree-select');
|
|
138
|
+
|
|
139
|
+
case ' ':
|
|
140
|
+
// Space selects; it must not scroll the page out from under the tree.
|
|
141
|
+
event.preventDefault();
|
|
142
|
+
return void ask(item, 'juno-tree-select');
|
|
143
|
+
|
|
144
|
+
case '*':
|
|
145
|
+
// Expand every sibling at this level — the pattern's one bulk action.
|
|
146
|
+
event.preventDefault();
|
|
147
|
+
for (const sib of item.parentElement?.children ?? []) {
|
|
148
|
+
if (sib.matches?.(ITEM) && isBranch(sib) && !isExpanded(sib)) {
|
|
149
|
+
ask(sib, 'juno-tree-toggle');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
|
|
154
|
+
default:
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Clicking a row moves the roving tabindex there too, or the next arrow key
|
|
160
|
+
// jumps back to wherever focus notionally was.
|
|
161
|
+
const onFocusIn = (event) => {
|
|
162
|
+
const item = event.target.closest(ITEM);
|
|
163
|
+
if (item && root.contains(item)) {
|
|
164
|
+
for (const other of root.querySelectorAll(ITEM)) rowOf(other).tabIndex = -1;
|
|
165
|
+
rowOf(item).tabIndex = 0;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
root.addEventListener('keydown', onKeyDown);
|
|
170
|
+
root.addEventListener('focusin', onFocusIn);
|
|
171
|
+
const teardown = () => {
|
|
172
|
+
root.removeEventListener('keydown', onKeyDown);
|
|
173
|
+
root.removeEventListener('focusin', onFocusIn);
|
|
174
|
+
delete root._junoTreeTeardown;
|
|
175
|
+
};
|
|
176
|
+
root._junoTreeTeardown = teardown;
|
|
177
|
+
return teardown;
|
|
178
|
+
}
|