@woylie/doggo 0.15.1 → 0.16.1
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/README.md +8 -1
- package/assets/js/hooks/carousel.js +19 -13
- package/assets/js/hooks/menu.js +47 -6
- package/assets/js/hooks/tree.js +33 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ The package can be installed by adding `doggo` to your list of dependencies in
|
|
|
21
21
|
```elixir
|
|
22
22
|
def deps do
|
|
23
23
|
[
|
|
24
|
-
{:doggo, "~> 0.
|
|
24
|
+
{:doggo, "~> 0.16.1"}
|
|
25
25
|
]
|
|
26
26
|
end
|
|
27
27
|
```
|
|
@@ -419,3 +419,10 @@ If you encounter any issues with a component, have suggestions for improvements,
|
|
|
419
419
|
or need a component for a specific use case that isn't currently available,
|
|
420
420
|
please don't hesitate to open a
|
|
421
421
|
[Github issue](https://github.com/woylie/doggo/issues).
|
|
422
|
+
|
|
423
|
+
## Related libraries
|
|
424
|
+
|
|
425
|
+
- [ex_icon](https://github.com/woylie/ex_icon) - Generic icon library for
|
|
426
|
+
Phoenix LiveView.
|
|
427
|
+
- [flop_phoenix](https://github.com/woylie/flop_phoenix) - Pagination, sortable
|
|
428
|
+
table and filter components for Flop.
|
|
@@ -112,6 +112,8 @@ export function initCarousel(carousel) {
|
|
|
112
112
|
|
|
113
113
|
// Auto rotation
|
|
114
114
|
let isPaused = false;
|
|
115
|
+
let isHovered = false;
|
|
116
|
+
let isFocused = false;
|
|
115
117
|
|
|
116
118
|
// Stop auto rotation if the last items does not wrap.
|
|
117
119
|
const hasShowEnded = () =>
|
|
@@ -123,8 +125,11 @@ export function initCarousel(carousel) {
|
|
|
123
125
|
syncPauseControl();
|
|
124
126
|
};
|
|
125
127
|
|
|
128
|
+
const canRotate = () =>
|
|
129
|
+
isAutoRotationEnabled() && !isPaused && !isHovered && !isFocused;
|
|
130
|
+
|
|
126
131
|
const startAutoRotation = () => {
|
|
127
|
-
if (!
|
|
132
|
+
if (!canRotate()) return;
|
|
128
133
|
if (autoRotationTimer != null) return;
|
|
129
134
|
|
|
130
135
|
if (hasShowEnded()) {
|
|
@@ -260,12 +265,8 @@ export function initCarousel(carousel) {
|
|
|
260
265
|
// Pause when hovering or focusing, resume when leaving, unless
|
|
261
266
|
// rotation is paused with the pause button.
|
|
262
267
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const resume = () => {
|
|
267
|
-
if (!isHovered && !isFocused) startAutoRotation();
|
|
268
|
-
};
|
|
268
|
+
const isPauseControl = (el) =>
|
|
269
|
+
el != null && el.closest?.(`.${baseClass}-pause`) != null;
|
|
269
270
|
|
|
270
271
|
carousel.addEventListener("pointerenter", () => {
|
|
271
272
|
isHovered = true;
|
|
@@ -274,12 +275,17 @@ export function initCarousel(carousel) {
|
|
|
274
275
|
|
|
275
276
|
carousel.addEventListener("pointerleave", () => {
|
|
276
277
|
isHovered = false;
|
|
277
|
-
|
|
278
|
+
startAutoRotation();
|
|
278
279
|
});
|
|
279
280
|
|
|
280
|
-
carousel.addEventListener("focusin", () => {
|
|
281
|
-
isFocused =
|
|
282
|
-
|
|
281
|
+
carousel.addEventListener("focusin", (e) => {
|
|
282
|
+
isFocused = !isPauseControl(e.target);
|
|
283
|
+
|
|
284
|
+
if (isFocused) {
|
|
285
|
+
stopAutoRotation();
|
|
286
|
+
} else {
|
|
287
|
+
startAutoRotation();
|
|
288
|
+
}
|
|
283
289
|
});
|
|
284
290
|
|
|
285
291
|
carousel.addEventListener("focusout", (e) => {
|
|
@@ -288,7 +294,7 @@ export function initCarousel(carousel) {
|
|
|
288
294
|
if (carousel.contains(e.relatedTarget)) return;
|
|
289
295
|
|
|
290
296
|
isFocused = false;
|
|
291
|
-
|
|
297
|
+
startAutoRotation();
|
|
292
298
|
});
|
|
293
299
|
|
|
294
300
|
// Initialize
|
|
@@ -303,7 +309,7 @@ export function initCarousel(carousel) {
|
|
|
303
309
|
// A patch can add or remove the pause button, and with it the
|
|
304
310
|
// rotation. Starting is a no-op while it runs or is paused.
|
|
305
311
|
if (isAutoRotationEnabled()) {
|
|
306
|
-
|
|
312
|
+
startAutoRotation();
|
|
307
313
|
} else {
|
|
308
314
|
stopAutoRotation();
|
|
309
315
|
}
|
package/assets/js/hooks/menu.js
CHANGED
|
@@ -64,20 +64,52 @@ export function initMenu(menu) {
|
|
|
64
64
|
// through the `aria-controls` pointing at it. Compared rather than put in a
|
|
65
65
|
// selector, because the id is the caller's and may hold characters that
|
|
66
66
|
// would change what the selector matches.
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
)
|
|
67
|
+
const getButton = () =>
|
|
68
|
+
Array.from(document.querySelectorAll("[aria-controls]")).find(
|
|
69
|
+
(el) => el.getAttribute("aria-controls") === menu.id,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const close = ({ focusButton } = { focusButton: true }) => {
|
|
73
|
+
const button = getButton();
|
|
71
74
|
|
|
72
75
|
if (!button) return false;
|
|
73
76
|
|
|
74
77
|
menu.setAttribute("hidden", "");
|
|
75
78
|
button.setAttribute("aria-expanded", "false");
|
|
76
|
-
|
|
79
|
+
|
|
80
|
+
if (focusButton) button.focus();
|
|
77
81
|
|
|
78
82
|
return true;
|
|
79
83
|
};
|
|
80
84
|
|
|
85
|
+
const closeOnClickOutside = (e) => {
|
|
86
|
+
if (menu.hidden) return;
|
|
87
|
+
|
|
88
|
+
const button = getButton();
|
|
89
|
+
|
|
90
|
+
if (!button) return;
|
|
91
|
+
if (menu.contains(e.target) || button.contains(e.target)) return;
|
|
92
|
+
|
|
93
|
+
close({ focusButton: false });
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
document.addEventListener("click", closeOnClickOutside);
|
|
97
|
+
|
|
98
|
+
const closeOnButtonEscape = (e) => {
|
|
99
|
+
if (e.key !== "Escape" || menu.hidden) return;
|
|
100
|
+
|
|
101
|
+
const button = getButton();
|
|
102
|
+
|
|
103
|
+
if (!button || !button.contains(e.target)) return;
|
|
104
|
+
|
|
105
|
+
if (close()) {
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
e.stopPropagation();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
document.addEventListener("keydown", closeOnButtonEscape, true);
|
|
112
|
+
|
|
81
113
|
menu.addEventListener("keydown", (e) => {
|
|
82
114
|
const items = getItems();
|
|
83
115
|
const currentIdx = items.indexOf(e.target.closest(ITEMS));
|
|
@@ -124,7 +156,12 @@ export function initMenu(menu) {
|
|
|
124
156
|
|
|
125
157
|
restoreTabStop();
|
|
126
158
|
|
|
127
|
-
|
|
159
|
+
const destroy = () => {
|
|
160
|
+
document.removeEventListener("click", closeOnClickOutside);
|
|
161
|
+
document.removeEventListener("keydown", closeOnButtonEscape, true);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return { update: restoreTabStop, destroy };
|
|
128
165
|
}
|
|
129
166
|
|
|
130
167
|
export default {
|
|
@@ -135,4 +172,8 @@ export default {
|
|
|
135
172
|
updated() {
|
|
136
173
|
this.instance.update();
|
|
137
174
|
},
|
|
175
|
+
|
|
176
|
+
destroyed() {
|
|
177
|
+
this.instance.destroy();
|
|
178
|
+
},
|
|
138
179
|
};
|
package/assets/js/hooks/tree.js
CHANGED
|
@@ -10,7 +10,26 @@ const labelOf = (item) =>
|
|
|
10
10
|
const parentOf = (item) =>
|
|
11
11
|
item.parentElement.closest('[role="treeitem"]') || null;
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
const labelText = (item) => {
|
|
14
|
+
const label = labelOf(item);
|
|
15
|
+
|
|
16
|
+
if (!label) return "";
|
|
17
|
+
|
|
18
|
+
const copy = label.cloneNode(true);
|
|
19
|
+
|
|
20
|
+
for (const node of copy.querySelectorAll('[aria-hidden="true"], svg')) {
|
|
21
|
+
node.remove();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return copy.textContent;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const domWriter = {
|
|
28
|
+
setAttribute: (el, attr, value) => el.setAttribute(attr, value),
|
|
29
|
+
removeAttribute: (el, attr) => el.removeAttribute(attr),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function initTree(tree, writer = domWriter) {
|
|
14
33
|
// Visible items only. A collapsed branch takes its descendants out of the
|
|
15
34
|
// sequence.
|
|
16
35
|
const getItems = () =>
|
|
@@ -18,22 +37,6 @@ export function initTree(tree) {
|
|
|
18
37
|
(item) => !item.closest('[role="group"][hidden]'),
|
|
19
38
|
);
|
|
20
39
|
|
|
21
|
-
// The hook owns the expanded state. A patch re-renders the server's
|
|
22
|
-
// version, so it is remembered by position and applied again.
|
|
23
|
-
const pathOf = (item) => {
|
|
24
|
-
const path = [];
|
|
25
|
-
|
|
26
|
-
for (let node = item; node; node = parentOf(node)) {
|
|
27
|
-
const siblings = Array.from(
|
|
28
|
-
node.parentElement.querySelectorAll(':scope > [role="treeitem"]'),
|
|
29
|
-
);
|
|
30
|
-
path.unshift(siblings.indexOf(node));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return path.join("-");
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const expandedPaths = new Set();
|
|
37
40
|
let activeIdx = 0;
|
|
38
41
|
let search = "";
|
|
39
42
|
let searchTimeout;
|
|
@@ -46,7 +49,7 @@ export function initTree(tree) {
|
|
|
46
49
|
const active = getItems()[idx];
|
|
47
50
|
|
|
48
51
|
for (const item of tree.querySelectorAll('[role="treeitem"]')) {
|
|
49
|
-
|
|
52
|
+
writer.setAttribute(item, "tabindex", item === active ? "0" : "-1");
|
|
50
53
|
}
|
|
51
54
|
};
|
|
52
55
|
|
|
@@ -56,25 +59,17 @@ export function initTree(tree) {
|
|
|
56
59
|
};
|
|
57
60
|
|
|
58
61
|
const setExpanded = (item, expanded) => {
|
|
59
|
-
|
|
62
|
+
writer.setAttribute(item, "aria-expanded", expanded ? "true" : "false");
|
|
60
63
|
|
|
61
64
|
const group = groupOf(item);
|
|
62
65
|
|
|
63
66
|
if (group) {
|
|
64
67
|
if (expanded) {
|
|
65
|
-
|
|
68
|
+
writer.removeAttribute(group, "hidden");
|
|
66
69
|
} else {
|
|
67
|
-
|
|
70
|
+
writer.setAttribute(group, "hidden", "");
|
|
68
71
|
}
|
|
69
72
|
}
|
|
70
|
-
|
|
71
|
-
const path = pathOf(item);
|
|
72
|
-
|
|
73
|
-
if (expanded) {
|
|
74
|
-
expandedPaths.add(path);
|
|
75
|
-
} else {
|
|
76
|
-
expandedPaths.delete(path);
|
|
77
|
-
}
|
|
78
73
|
};
|
|
79
74
|
|
|
80
75
|
// Only the caret toggles. A click on the label is the caller's, for
|
|
@@ -106,8 +101,7 @@ export function initTree(tree) {
|
|
|
106
101
|
|
|
107
102
|
const repeated = [...search].every((char) => char === search[0]);
|
|
108
103
|
const items = getItems();
|
|
109
|
-
|
|
110
|
-
const labels = items.map((item) => labelOf(item)?.textContent ?? "");
|
|
104
|
+
const labels = items.map(labelText);
|
|
111
105
|
const idx = searchIndex(
|
|
112
106
|
labels,
|
|
113
107
|
repeated ? search[0] : search,
|
|
@@ -131,7 +125,7 @@ export function initTree(tree) {
|
|
|
131
125
|
if (isBranch(item) && !isExpanded(item)) {
|
|
132
126
|
setExpanded(item, true);
|
|
133
127
|
} else if (isBranch(item)) {
|
|
134
|
-
const child = groupOf(item)
|
|
128
|
+
const child = groupOf(item)?.querySelector('[role="treeitem"]');
|
|
135
129
|
|
|
136
130
|
if (child) moveTo(getItems().indexOf(child));
|
|
137
131
|
}
|
|
@@ -155,6 +149,7 @@ export function initTree(tree) {
|
|
|
155
149
|
|
|
156
150
|
const nextIdx = targetIndex(e.key, currentIdx, items.length, {
|
|
157
151
|
orientation: "vertical",
|
|
152
|
+
wrap: false,
|
|
158
153
|
});
|
|
159
154
|
|
|
160
155
|
if (nextIdx !== null) {
|
|
@@ -170,12 +165,6 @@ export function initTree(tree) {
|
|
|
170
165
|
});
|
|
171
166
|
|
|
172
167
|
const restore = () => {
|
|
173
|
-
for (const item of tree.querySelectorAll('[role="treeitem"]')) {
|
|
174
|
-
if (isBranch(item) && expandedPaths.has(pathOf(item))) {
|
|
175
|
-
setExpanded(item, true);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
168
|
const items = getItems();
|
|
180
169
|
|
|
181
170
|
if (items.length > 0) setActive(clamp(activeIdx, items.length));
|
|
@@ -188,7 +177,12 @@ export function initTree(tree) {
|
|
|
188
177
|
|
|
189
178
|
export default {
|
|
190
179
|
mounted() {
|
|
191
|
-
|
|
180
|
+
const js = this.js();
|
|
181
|
+
|
|
182
|
+
this.instance = initTree(this.el, {
|
|
183
|
+
setAttribute: (el, attr, value) => js.setAttribute(el, attr, value),
|
|
184
|
+
removeAttribute: (el, attr) => js.removeAttribute(el, attr),
|
|
185
|
+
});
|
|
192
186
|
},
|
|
193
187
|
|
|
194
188
|
updated() {
|