@woylie/doggo 0.15.0 → 0.16.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/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.15.0"}
24
+ {:doggo, "~> 0.16.0"}
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.
@@ -83,11 +83,17 @@ export function initCarousel(carousel) {
83
83
 
84
84
  const syncActiveState = () => setActiveIdx(getActiveIdx());
85
85
 
86
+ const SCROLL_SETTLE_MS = 100;
87
+
88
+ let pendingIdx = null;
89
+ let settleTimer = null;
90
+
86
91
  const scrollToIdx = (idx) => {
87
92
  const item = getItems()[idx];
88
93
 
89
94
  if (!item) return;
90
95
 
96
+ pendingIdx = idx;
91
97
  setActiveIdx(idx);
92
98
 
93
99
  const itemsContainer = getItemsContainer();
@@ -106,6 +112,8 @@ export function initCarousel(carousel) {
106
112
 
107
113
  // Auto rotation
108
114
  let isPaused = false;
115
+ let isHovered = false;
116
+ let isFocused = false;
109
117
 
110
118
  // Stop auto rotation if the last items does not wrap.
111
119
  const hasShowEnded = () =>
@@ -117,8 +125,11 @@ export function initCarousel(carousel) {
117
125
  syncPauseControl();
118
126
  };
119
127
 
128
+ const canRotate = () =>
129
+ isAutoRotationEnabled() && !isPaused && !isHovered && !isFocused;
130
+
120
131
  const startAutoRotation = () => {
121
- if (!isAutoRotationEnabled() || isPaused) return;
132
+ if (!canRotate()) return;
122
133
  if (autoRotationTimer != null) return;
123
134
 
124
135
  if (hasShowEnded()) {
@@ -190,7 +201,15 @@ export function initCarousel(carousel) {
190
201
  carousel.addEventListener(
191
202
  "scroll",
192
203
  (e) => {
193
- if (e.target === getItemsContainer()) syncActiveState();
204
+ if (e.target !== getItemsContainer()) return;
205
+
206
+ clearTimeout(settleTimer);
207
+ settleTimer = setTimeout(() => {
208
+ pendingIdx = null;
209
+ syncActiveState();
210
+ }, SCROLL_SETTLE_MS);
211
+
212
+ if (pendingIdx === null) syncActiveState();
194
213
  },
195
214
  true,
196
215
  );
@@ -246,12 +265,8 @@ export function initCarousel(carousel) {
246
265
  // Pause when hovering or focusing, resume when leaving, unless
247
266
  // rotation is paused with the pause button.
248
267
 
249
- let isHovered = false;
250
- let isFocused = false;
251
-
252
- const resume = () => {
253
- if (!isHovered && !isFocused) startAutoRotation();
254
- };
268
+ const isPauseControl = (el) =>
269
+ el != null && el.closest?.(`.${baseClass}-pause`) != null;
255
270
 
256
271
  carousel.addEventListener("pointerenter", () => {
257
272
  isHovered = true;
@@ -260,12 +275,17 @@ export function initCarousel(carousel) {
260
275
 
261
276
  carousel.addEventListener("pointerleave", () => {
262
277
  isHovered = false;
263
- resume();
278
+ startAutoRotation();
264
279
  });
265
280
 
266
- carousel.addEventListener("focusin", () => {
267
- isFocused = true;
268
- stopAutoRotation();
281
+ carousel.addEventListener("focusin", (e) => {
282
+ isFocused = !isPauseControl(e.target);
283
+
284
+ if (isFocused) {
285
+ stopAutoRotation();
286
+ } else {
287
+ startAutoRotation();
288
+ }
269
289
  });
270
290
 
271
291
  carousel.addEventListener("focusout", (e) => {
@@ -274,7 +294,7 @@ export function initCarousel(carousel) {
274
294
  if (carousel.contains(e.relatedTarget)) return;
275
295
 
276
296
  isFocused = false;
277
- resume();
297
+ startAutoRotation();
278
298
  });
279
299
 
280
300
  // Initialize
@@ -289,13 +309,18 @@ export function initCarousel(carousel) {
289
309
  // A patch can add or remove the pause button, and with it the
290
310
  // rotation. Starting is a no-op while it runs or is paused.
291
311
  if (isAutoRotationEnabled()) {
292
- resume();
312
+ startAutoRotation();
293
313
  } else {
294
314
  stopAutoRotation();
295
315
  }
296
316
  };
297
317
 
298
- return { update: syncAfterUpdate, destroy: stopAutoRotation };
318
+ const destroy = () => {
319
+ stopAutoRotation();
320
+ clearTimeout(settleTimer);
321
+ };
322
+
323
+ return { update: syncAfterUpdate, destroy };
299
324
  }
300
325
 
301
326
  export default {
@@ -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 close = () => {
68
- const button = Array.from(
69
- document.querySelectorAll("[aria-controls]"),
70
- ).find((el) => el.getAttribute("aria-controls") === menu.id);
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
- button.focus();
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
- return { update: restoreTabStop };
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@woylie/doggo",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "JavaScript hooks for the Doggo component library",
5
5
  "keywords": [
6
6
  "phoenix",