@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/README.md +17 -16
  3. package/dist/android/dimens.xml +2 -0
  4. package/dist/classes.json +1774 -0
  5. package/dist/css/juno-tokens.css +10 -0
  6. package/dist/css/juno.css +1057 -56
  7. package/dist/flutter/juno_tokens.dart +10 -0
  8. package/dist/ios/JunoTokens.swift +10 -0
  9. package/dist/js/tokens.js +16 -0
  10. package/dist/json/tokens.json +51 -0
  11. package/dist/rust/juno_tokens.rs +383 -0
  12. package/dist/scss/_juno-tokens.scss +10 -0
  13. package/docs/accessibility.md +6 -0
  14. package/docs/browser-support.md +3 -0
  15. package/docs/components/button.md +11 -2
  16. package/docs/components/canvas-ink.md +71 -0
  17. package/docs/components/dock.md +34 -0
  18. package/docs/components/fold-slot.md +26 -1
  19. package/docs/components/gizmo.md +114 -0
  20. package/docs/components/swatch.md +95 -0
  21. package/docs/components/tree.md +112 -0
  22. package/docs/conformance-kit.md +255 -0
  23. package/docs/getting-started.md +14 -0
  24. package/docs/integration.md +52 -6
  25. package/docs/ios-conformance.md +200 -3
  26. package/docs/ios-pwa.md +273 -0
  27. package/docs/native.md +38 -1
  28. package/docs/tokens-reference.md +15 -0
  29. package/package.json +8 -2
  30. package/src/css/base.css +35 -43
  31. package/src/css/components/button.css +42 -2
  32. package/src/css/components/canvas-ink.css +97 -0
  33. package/src/css/components/dock.css +65 -6
  34. package/src/css/components/fold-slot.css +49 -3
  35. package/src/css/components/gizmo.css +238 -0
  36. package/src/css/components/segmented.css +15 -2
  37. package/src/css/components/swatch.css +187 -0
  38. package/src/css/components/tree.css +259 -0
  39. package/src/css/touch-surfaces.mjs +95 -0
  40. package/tools/gizmo.mjs +144 -0
  41. package/tools/testing.mjs +177 -0
  42. 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
+ }