@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.
- package/.github/workflows/npm-publish.yml +40 -22
- package/.github/workflows/publish.yml +35 -0
- package/CHANGELOG.md +116 -0
- package/README.md +55 -111
- package/RELEASE.md +44 -0
- package/dist/index.esm.js +106 -13
- package/dist/index.esm.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/plugins/core/index.d2.ts +3 -2
- package/dist/plugins/thumbnails/index.d.ts +2 -2
- package/dist/plugins/thumbnails/index.esm.js +4 -4
- package/dist/plugins/thumbnails/index.min.js +1 -1
- package/docs/assets/demo.css +5 -0
- package/docs/assets/demo.js +11 -0
- package/docs/dist/index.d.ts +1 -1
- package/docs/dist/index.esm.js +106 -13
- package/docs/dist/index.esm.js.map +1 -1
- package/docs/dist/index.min.js +1 -1
- package/docs/dist/index.min.js.map +1 -1
- package/docs/dist/plugins/arrows/index.d.ts +1 -1
- package/docs/dist/plugins/autoplay/index.d.ts +1 -1
- package/docs/dist/plugins/classnames/index.d.ts +1 -1
- package/docs/dist/plugins/core/index.d.ts +10 -63
- package/docs/dist/plugins/core/index.d2.ts +64 -10
- package/docs/dist/plugins/dots/index.d.ts +1 -1
- package/docs/dist/plugins/drag-scrolling/index.d.ts +1 -1
- package/docs/dist/plugins/fade/index.d.ts +1 -1
- package/docs/dist/plugins/full-width/index.d.ts +1 -1
- package/docs/dist/plugins/scroll-indicator/index.d.ts +1 -1
- package/docs/dist/plugins/skip-links/index.d.ts +1 -1
- package/docs/dist/plugins/thumbnails/index.d.ts +3 -3
- package/docs/dist/plugins/thumbnails/index.esm.js +4 -4
- package/docs/dist/plugins/thumbnails/index.min.js +1 -1
- package/docs/index.html +29 -0
- package/package.json +1 -1
- package/src/core/slider.ts +111 -12
- package/src/core/types.ts +4 -1
- package/src/plugins/thumbnails/index.ts +4 -4
package/src/core/slider.ts
CHANGED
|
@@ -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
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
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
|
+
|