@evermade/overflow-slider 4.2.2 → 4.2.3

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 (39) hide show
  1. package/.github/workflows/npm-publish.yml +40 -22
  2. package/.github/workflows/publish.yml +35 -0
  3. package/CHANGELOG.md +116 -0
  4. package/README.md +55 -111
  5. package/RELEASE.md +44 -0
  6. package/dist/index.esm.js +106 -13
  7. package/dist/index.esm.js.map +1 -1
  8. package/dist/index.min.js +1 -1
  9. package/dist/index.min.js.map +1 -1
  10. package/dist/plugins/core/index.d2.ts +3 -2
  11. package/dist/plugins/thumbnails/index.d.ts +2 -2
  12. package/dist/plugins/thumbnails/index.esm.js +4 -4
  13. package/dist/plugins/thumbnails/index.min.js +1 -1
  14. package/docs/assets/demo.css +5 -0
  15. package/docs/assets/demo.js +11 -0
  16. package/docs/dist/index.d.ts +1 -1
  17. package/docs/dist/index.esm.js +106 -13
  18. package/docs/dist/index.esm.js.map +1 -1
  19. package/docs/dist/index.min.js +1 -1
  20. package/docs/dist/index.min.js.map +1 -1
  21. package/docs/dist/plugins/arrows/index.d.ts +1 -1
  22. package/docs/dist/plugins/autoplay/index.d.ts +1 -1
  23. package/docs/dist/plugins/classnames/index.d.ts +1 -1
  24. package/docs/dist/plugins/core/index.d.ts +10 -63
  25. package/docs/dist/plugins/core/index.d2.ts +64 -10
  26. package/docs/dist/plugins/dots/index.d.ts +1 -1
  27. package/docs/dist/plugins/drag-scrolling/index.d.ts +1 -1
  28. package/docs/dist/plugins/fade/index.d.ts +1 -1
  29. package/docs/dist/plugins/full-width/index.d.ts +1 -1
  30. package/docs/dist/plugins/scroll-indicator/index.d.ts +1 -1
  31. package/docs/dist/plugins/skip-links/index.d.ts +1 -1
  32. package/docs/dist/plugins/thumbnails/index.d.ts +3 -3
  33. package/docs/dist/plugins/thumbnails/index.esm.js +4 -4
  34. package/docs/dist/plugins/thumbnails/index.min.js +1 -1
  35. package/docs/index.html +29 -0
  36. package/package.json +1 -1
  37. package/src/core/slider.ts +111 -12
  38. package/src/core/types.ts +4 -1
  39. package/src/plugins/thumbnails/index.ts +4 -4
@@ -202,20 +202,42 @@ export default function Slider( container: HTMLElement, options : SliderOptionAr
202
202
  wasInteractedWith = true;
203
203
  }, { passive: true });
204
204
  slider.container.addEventListener('focusin', (e) => {
205
- // move target parents as long as they are not the container
206
- // but only if focus didn't start from mouse or touch
207
- if (!wasInteractedWith) {
208
- let target = e.target as HTMLElement;
209
- while (target.parentElement !== slider.container) {
210
- if (target.parentElement) {
211
- target = target.parentElement;
212
- } else {
213
- break;
214
- }
215
- }
216
- ensureSlideIsInView(target, 'auto');
205
+ // Only handle keyboard-initiated focus (not mouse or touch)
206
+ if (wasInteractedWith) {
207
+ wasInteractedWith = false;
208
+ return;
217
209
  }
218
210
  wasInteractedWith = false;
211
+
212
+ // No scrolling needed if there is no overflow
213
+ if ( !slider.details.hasOverflow ) {
214
+ return;
215
+ }
216
+
217
+ const focusedElement = e.target as HTMLElement;
218
+
219
+ // Walk up from the focused element to find the direct child (slide) of the container
220
+ let slide = focusedElement;
221
+ while (slide.parentElement !== slider.container) {
222
+ if (slide.parentElement) {
223
+ slide = slide.parentElement;
224
+ } else {
225
+ // Focused element is not inside the slider container
226
+ return;
227
+ }
228
+ }
229
+
230
+ // Emit programmaticScrollStart immediately so the browser's native focus
231
+ // scroll events are classified as programmatic (not native). This prevents
232
+ // nativeScrollStart from restoring scrollSnapType and fighting our correction.
233
+ slider.emit('programmaticScrollStart');
234
+
235
+ // Use setTimeout to let the browser's native focus scroll complete,
236
+ // then override with our WCAG-compliant scroll positioning
237
+ setTimeout(() => {
238
+ scrollFocusedSlideIntoView(slide, focusedElement);
239
+ slider.emit('focusScroll');
240
+ }, 50);
219
241
  });
220
242
 
221
243
 
@@ -265,6 +287,83 @@ export default function Slider( container: HTMLElement, options : SliderOptionAr
265
287
  }
266
288
  };
267
289
 
290
+ /**
291
+ * Scrolls a focused slide (or child element) into view for WCAG AA compliance.
292
+ * Priority:
293
+ * 1. Show the full slide if it fits in the container
294
+ * 2. If the slide is wider than the container, show the focused element
295
+ * 3. If neither fits, align the leading edge (left for LTR, right for RTL)
296
+ */
297
+ function scrollFocusedSlideIntoView( slide: HTMLElement, focusedElement: HTMLElement ) {
298
+ const isRtl = slider.options.rtl;
299
+ const containerRect = slider.container.getBoundingClientRect();
300
+ const containerWidth = slider.container.offsetWidth;
301
+ const slideRect = slide.getBoundingClientRect();
302
+ const scrollLeft = slider.container.scrollLeft;
303
+
304
+ // Calculate visual offsets relative to the container viewport
305
+ const slideLeftOffset = slideRect.left - containerRect.left;
306
+ const slideRightOffset = slideRect.right - containerRect.right;
307
+
308
+ // Check if slide is already fully visible (1px tolerance for sub-pixel rounding)
309
+ if ( slideLeftOffset >= -1 && slideRightOffset <= 1 ) {
310
+ slider.container.style.scrollSnapType = '';
311
+ slider.emit('programmaticScrollEnd');
312
+ return;
313
+ }
314
+
315
+ let scrollTarget: number;
316
+
317
+ if ( slideRect.width <= containerWidth ) {
318
+ // Slide fits in container — align its leading edge to show it fully
319
+ if ( isRtl ) {
320
+ // RTL: align slide's right edge with container's right edge
321
+ scrollTarget = scrollLeft + slideRightOffset;
322
+ } else {
323
+ // LTR: align slide's left edge with container's left edge
324
+ scrollTarget = scrollLeft + slideLeftOffset;
325
+ }
326
+ } else if ( focusedElement !== slide ) {
327
+ // Slide is wider than container — try to show the focused child element
328
+ const focusRect = focusedElement.getBoundingClientRect();
329
+ const focusLeftOffset = focusRect.left - containerRect.left;
330
+ const focusRightOffset = focusRect.right - containerRect.right;
331
+
332
+ // Check if focused element is already fully visible
333
+ if ( focusLeftOffset >= -1 && focusRightOffset <= 1 ) {
334
+ slider.container.style.scrollSnapType = '';
335
+ slider.emit('programmaticScrollEnd');
336
+ return;
337
+ }
338
+
339
+ if ( focusRect.width <= containerWidth ) {
340
+ // Focused element fits in container — align its leading edge
341
+ if ( isRtl ) {
342
+ scrollTarget = scrollLeft + focusRightOffset;
343
+ } else {
344
+ scrollTarget = scrollLeft + focusLeftOffset;
345
+ }
346
+ } else {
347
+ // Focused element is also wider than container — align leading edge
348
+ if ( isRtl ) {
349
+ scrollTarget = scrollLeft + focusRightOffset;
350
+ } else {
351
+ scrollTarget = scrollLeft + focusLeftOffset;
352
+ }
353
+ }
354
+ } else {
355
+ // Slide is the focused element and wider than container — align leading edge
356
+ if ( isRtl ) {
357
+ scrollTarget = scrollLeft + slideRightOffset;
358
+ } else {
359
+ scrollTarget = scrollLeft + slideLeftOffset;
360
+ }
361
+ }
362
+
363
+ slider.emit('programmaticScrollStart');
364
+ slider.container.scrollTo({ left: scrollTarget!, behavior: 'auto' });
365
+ };
366
+
268
367
  function setActiveSlideIdx() {
269
368
  const sliderRect = slider.container.getBoundingClientRect();
270
369
  const scrollLeft = slider.getScrollLeft();
package/src/core/types.ts CHANGED
@@ -95,7 +95,8 @@ export type SliderHooks =
95
95
  | HOOK_NATIVE_SCROLL_END
96
96
  | HOOK_PROGRAMMATIC_SCROLL_START
97
97
  | HOOK_PROGRAMMATIC_SCROLL
98
- | HOOK_PROGRAMMATIC_SCROLL_END;
98
+ | HOOK_PROGRAMMATIC_SCROLL_END
99
+ | HOOK_FOCUS_SCROLL;
99
100
 
100
101
  export type HOOK_CREATED = 'created';
101
102
  export type HOOK_DETAILS_CHANGED = 'detailsChanged';
@@ -118,5 +119,7 @@ export type HOOK_PROGRAMMATIC_SCROLL_START = 'programmaticScrollStart';
118
119
  export type HOOK_PROGRAMMATIC_SCROLL = 'programmaticScroll';
119
120
  export type HOOK_PROGRAMMATIC_SCROLL_END = 'programmaticScrollEnd';
120
121
 
122
+ // keyboard focus triggered scroll
123
+ export type HOOK_FOCUS_SCROLL = 'focusScroll';
121
124
 
122
125
  export type SliderPlugin = (slider: Slider) => void;
@@ -4,7 +4,7 @@ export type ThumbnailsOptions = {
4
4
  mainSlider: Slider,
5
5
  };
6
6
 
7
- export default function FullWidthPlugin( args: DeepPartial<ThumbnailsOptions> ) {
7
+ export default function ThumbnailPlugin( args: DeepPartial<ThumbnailsOptions> ) {
8
8
  return ( slider: Slider ) => {
9
9
 
10
10
  const options = <ThumbnailsOptions>{
@@ -43,14 +43,14 @@ export default function FullWidthPlugin( args: DeepPartial<ThumbnailsOptions> )
43
43
  setTimeout(() => {
44
44
  const mainActiveSlideIdx = mainSlider.activeSlideIdx;
45
45
  const thumbActiveSlideIdx = slider.activeSlideIdx;
46
+ const activeThumbnail = slider.slides[mainActiveSlideIdx] as HTMLElement;
47
+ setActiveThumbnail(activeThumbnail);
46
48
  if ( thumbActiveSlideIdx === mainActiveSlideIdx ) {
47
49
  return;
48
50
  }
49
- const activeThumbnail = slider.slides[mainActiveSlideIdx] as HTMLElement;
50
- setActiveThumbnail(activeThumbnail);
51
51
  slider.moveToSlide(mainActiveSlideIdx);
52
52
  }, 50);
53
53
  });
54
-
55
54
  };
56
55
  }
56
+