@evermade/overflow-slider 4.2.1 → 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 -107
- package/RELEASE.md +44 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +107 -15
- 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/overflow-slider.css +1 -1
- package/dist/plugins/arrows/index.d.ts +1 -1
- package/dist/plugins/autoplay/index.d.ts +1 -1
- package/dist/plugins/classnames/index.d.ts +1 -1
- package/dist/plugins/core/index.d.ts +10 -63
- package/dist/plugins/core/index.d2.ts +64 -10
- package/dist/plugins/dots/index.d.ts +1 -1
- package/dist/plugins/drag-scrolling/index.d.ts +1 -1
- package/dist/plugins/fade/index.d.ts +1 -1
- package/dist/plugins/full-width/index.d.ts +1 -1
- package/dist/plugins/scroll-indicator/index.d.ts +1 -1
- package/dist/plugins/skip-links/index.d.ts +1 -1
- package/dist/plugins/thumbnails/index.d.ts +3 -3
- 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 +107 -15
- 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/overflow-slider.css +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/overflow-slider.scss +1 -0
- package/src/plugins/thumbnails/index.ts +4 -4
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import { SliderOptionArgs, SliderPlugin, SliderCallback, SliderOptions, SliderDetails } from './index.d2.ts';
|
|
2
|
+
|
|
3
|
+
declare function OverflowSlider(container: HTMLElement, options?: SliderOptionArgs, plugins?: SliderPlugin[]): {
|
|
2
4
|
container: HTMLElement;
|
|
3
5
|
slides: HTMLElement[];
|
|
4
|
-
emit: (name:
|
|
5
|
-
moveToDirection: (direction:
|
|
6
|
-
moveToSlideInDirection: (direction:
|
|
7
|
-
snapToClosestSlide: (direction:
|
|
6
|
+
emit: (name: string) => void;
|
|
7
|
+
moveToDirection: (direction: "prev" | "next") => void;
|
|
8
|
+
moveToSlideInDirection: (direction: "prev" | "next") => void;
|
|
9
|
+
snapToClosestSlide: (direction: "prev" | "next") => void;
|
|
8
10
|
moveToSlide: (index: number) => void;
|
|
9
11
|
canMoveToSlide: (index: number) => boolean;
|
|
10
12
|
getInclusiveScrollWidth: () => number;
|
|
@@ -14,65 +16,10 @@ type Slider<O = {}, C = {}, H extends string = string> = {
|
|
|
14
16
|
getScrollLeft: () => number;
|
|
15
17
|
setScrollLeft: (value: number) => void;
|
|
16
18
|
setActiveSlideIdx: () => void;
|
|
17
|
-
on: (name:
|
|
19
|
+
on: (name: string, cb: SliderCallback) => void;
|
|
18
20
|
options: SliderOptions;
|
|
19
21
|
details: SliderDetails;
|
|
20
22
|
activeSlideIdx: number;
|
|
21
|
-
}
|
|
22
|
-
type SliderCallback<O = {}, C = {}, H extends string = string> = (props: Slider<O, C, H>) => void;
|
|
23
|
-
/**
|
|
24
|
-
* Recursively makes all properties of T optional.
|
|
25
|
-
* @see https://www.typescriptlang.org/docs/handbook/utility-types.html#mapped-types
|
|
26
|
-
*/
|
|
27
|
-
type DeepPartial<T> = {
|
|
28
|
-
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
|
29
|
-
};
|
|
30
|
-
type SliderOptions = {
|
|
31
|
-
scrollBehavior: string;
|
|
32
|
-
scrollStrategy: string;
|
|
33
|
-
slidesSelector: string;
|
|
34
|
-
emulateScrollSnap: boolean;
|
|
35
|
-
emulateScrollSnapMaxThreshold?: number;
|
|
36
|
-
cssVariableContainer: HTMLElement;
|
|
37
|
-
rtl: boolean;
|
|
38
|
-
targetWidth?: (slider: Slider) => number;
|
|
39
|
-
[key: string]: unknown;
|
|
40
|
-
};
|
|
41
|
-
type SliderOptionArgs = {
|
|
42
|
-
scrollBehavior?: 'smooth' | 'auto';
|
|
43
|
-
scrollStrategy?: 'fullSlide' | 'partialSlide';
|
|
44
|
-
slidesSelector?: string;
|
|
45
|
-
emulateScrollSnap?: boolean;
|
|
46
|
-
emulateScrollSnapMaxThreshold?: number;
|
|
47
|
-
cssVariableContainer?: HTMLElement;
|
|
48
|
-
rtl?: boolean;
|
|
49
|
-
targetWidth?: (slider: Slider) => number;
|
|
50
|
-
[key: string]: unknown;
|
|
51
|
-
};
|
|
52
|
-
type SliderDetails = {
|
|
53
|
-
hasOverflow: boolean;
|
|
54
|
-
slideCount: number;
|
|
55
|
-
containerWidth: number;
|
|
56
|
-
containerHeight: number;
|
|
57
|
-
scrollableAreaWidth: number;
|
|
58
|
-
amountOfPages: number;
|
|
59
|
-
currentPage: number;
|
|
60
|
-
};
|
|
61
|
-
type SliderHooks = HOOK_CREATED | HOOK_CONTENTS_CHANGED | HOOK_DETAILS_CHANGED | HOOK_CONTAINER_SIZE_CHANGED | HOOK_ACTIVE_SLIDE_CHANGED | HOOK_SCROLL_START | HOOK_SCROLL | HOOK_SCROLL_END | HOOK_NATIVE_SCROLL_START | HOOK_NATIVE_SCROLL | HOOK_NATIVE_SCROLL_END | HOOK_PROGRAMMATIC_SCROLL_START | HOOK_PROGRAMMATIC_SCROLL | HOOK_PROGRAMMATIC_SCROLL_END;
|
|
62
|
-
type HOOK_CREATED = 'created';
|
|
63
|
-
type HOOK_DETAILS_CHANGED = 'detailsChanged';
|
|
64
|
-
type HOOK_CONTENTS_CHANGED = 'contentsChanged';
|
|
65
|
-
type HOOK_CONTAINER_SIZE_CHANGED = 'containerSizeChanged';
|
|
66
|
-
type HOOK_ACTIVE_SLIDE_CHANGED = 'activeSlideChanged';
|
|
67
|
-
type HOOK_SCROLL_START = 'scrollStart';
|
|
68
|
-
type HOOK_SCROLL = 'scroll';
|
|
69
|
-
type HOOK_SCROLL_END = 'scrollEnd';
|
|
70
|
-
type HOOK_NATIVE_SCROLL_START = 'nativeScrollStart';
|
|
71
|
-
type HOOK_NATIVE_SCROLL = 'nativeScroll';
|
|
72
|
-
type HOOK_NATIVE_SCROLL_END = 'nativeScrollEnd';
|
|
73
|
-
type HOOK_PROGRAMMATIC_SCROLL_START = 'programmaticScrollStart';
|
|
74
|
-
type HOOK_PROGRAMMATIC_SCROLL = 'programmaticScroll';
|
|
75
|
-
type HOOK_PROGRAMMATIC_SCROLL_END = 'programmaticScrollEnd';
|
|
76
|
-
type SliderPlugin = (slider: Slider) => void;
|
|
23
|
+
} | undefined;
|
|
77
24
|
|
|
78
|
-
export
|
|
25
|
+
export { OverflowSlider as default };
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
declare function OverflowSlider(container: HTMLElement, options?: SliderOptionArgs, plugins?: SliderPlugin[]): {
|
|
1
|
+
type Slider<O = {}, C = {}, H extends string = string> = {
|
|
4
2
|
container: HTMLElement;
|
|
5
3
|
slides: HTMLElement[];
|
|
6
|
-
emit: (name:
|
|
7
|
-
moveToDirection: (direction:
|
|
8
|
-
moveToSlideInDirection: (direction:
|
|
9
|
-
snapToClosestSlide: (direction:
|
|
4
|
+
emit: (name: H | SliderHooks) => void;
|
|
5
|
+
moveToDirection: (direction: 'prev' | 'next') => void;
|
|
6
|
+
moveToSlideInDirection: (direction: 'prev' | 'next') => void;
|
|
7
|
+
snapToClosestSlide: (direction: 'prev' | 'next') => void;
|
|
10
8
|
moveToSlide: (index: number) => void;
|
|
11
9
|
canMoveToSlide: (index: number) => boolean;
|
|
12
10
|
getInclusiveScrollWidth: () => number;
|
|
@@ -16,10 +14,66 @@ declare function OverflowSlider(container: HTMLElement, options?: SliderOptionAr
|
|
|
16
14
|
getScrollLeft: () => number;
|
|
17
15
|
setScrollLeft: (value: number) => void;
|
|
18
16
|
setActiveSlideIdx: () => void;
|
|
19
|
-
on: (name:
|
|
17
|
+
on: (name: H | SliderHooks, cb: SliderCallback) => void;
|
|
20
18
|
options: SliderOptions;
|
|
21
19
|
details: SliderDetails;
|
|
22
20
|
activeSlideIdx: number;
|
|
23
|
-
}
|
|
21
|
+
} & C;
|
|
22
|
+
type SliderCallback<O = {}, C = {}, H extends string = string> = (props: Slider<O, C, H>) => void;
|
|
23
|
+
/**
|
|
24
|
+
* Recursively makes all properties of T optional.
|
|
25
|
+
* @see https://www.typescriptlang.org/docs/handbook/utility-types.html#mapped-types
|
|
26
|
+
*/
|
|
27
|
+
type DeepPartial<T> = {
|
|
28
|
+
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
|
29
|
+
};
|
|
30
|
+
type SliderOptions = {
|
|
31
|
+
scrollBehavior: string;
|
|
32
|
+
scrollStrategy: string;
|
|
33
|
+
slidesSelector: string;
|
|
34
|
+
emulateScrollSnap: boolean;
|
|
35
|
+
emulateScrollSnapMaxThreshold?: number;
|
|
36
|
+
cssVariableContainer: HTMLElement;
|
|
37
|
+
rtl: boolean;
|
|
38
|
+
targetWidth?: (slider: Slider) => number;
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
};
|
|
41
|
+
type SliderOptionArgs = {
|
|
42
|
+
scrollBehavior?: 'smooth' | 'auto';
|
|
43
|
+
scrollStrategy?: 'fullSlide' | 'partialSlide';
|
|
44
|
+
slidesSelector?: string;
|
|
45
|
+
emulateScrollSnap?: boolean;
|
|
46
|
+
emulateScrollSnapMaxThreshold?: number;
|
|
47
|
+
cssVariableContainer?: HTMLElement;
|
|
48
|
+
rtl?: boolean;
|
|
49
|
+
targetWidth?: (slider: Slider) => number;
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
};
|
|
52
|
+
type SliderDetails = {
|
|
53
|
+
hasOverflow: boolean;
|
|
54
|
+
slideCount: number;
|
|
55
|
+
containerWidth: number;
|
|
56
|
+
containerHeight: number;
|
|
57
|
+
scrollableAreaWidth: number;
|
|
58
|
+
amountOfPages: number;
|
|
59
|
+
currentPage: number;
|
|
60
|
+
};
|
|
61
|
+
type SliderHooks = HOOK_CREATED | HOOK_CONTENTS_CHANGED | HOOK_DETAILS_CHANGED | HOOK_CONTAINER_SIZE_CHANGED | HOOK_ACTIVE_SLIDE_CHANGED | HOOK_SCROLL_START | HOOK_SCROLL | HOOK_SCROLL_END | HOOK_NATIVE_SCROLL_START | HOOK_NATIVE_SCROLL | HOOK_NATIVE_SCROLL_END | HOOK_PROGRAMMATIC_SCROLL_START | HOOK_PROGRAMMATIC_SCROLL | HOOK_PROGRAMMATIC_SCROLL_END | HOOK_FOCUS_SCROLL;
|
|
62
|
+
type HOOK_CREATED = 'created';
|
|
63
|
+
type HOOK_DETAILS_CHANGED = 'detailsChanged';
|
|
64
|
+
type HOOK_CONTENTS_CHANGED = 'contentsChanged';
|
|
65
|
+
type HOOK_CONTAINER_SIZE_CHANGED = 'containerSizeChanged';
|
|
66
|
+
type HOOK_ACTIVE_SLIDE_CHANGED = 'activeSlideChanged';
|
|
67
|
+
type HOOK_SCROLL_START = 'scrollStart';
|
|
68
|
+
type HOOK_SCROLL = 'scroll';
|
|
69
|
+
type HOOK_SCROLL_END = 'scrollEnd';
|
|
70
|
+
type HOOK_NATIVE_SCROLL_START = 'nativeScrollStart';
|
|
71
|
+
type HOOK_NATIVE_SCROLL = 'nativeScroll';
|
|
72
|
+
type HOOK_NATIVE_SCROLL_END = 'nativeScrollEnd';
|
|
73
|
+
type HOOK_PROGRAMMATIC_SCROLL_START = 'programmaticScrollStart';
|
|
74
|
+
type HOOK_PROGRAMMATIC_SCROLL = 'programmaticScroll';
|
|
75
|
+
type HOOK_PROGRAMMATIC_SCROLL_END = 'programmaticScrollEnd';
|
|
76
|
+
type HOOK_FOCUS_SCROLL = 'focusScroll';
|
|
77
|
+
type SliderPlugin = (slider: Slider) => void;
|
|
24
78
|
|
|
25
|
-
export {
|
|
79
|
+
export type { DeepPartial, HOOK_ACTIVE_SLIDE_CHANGED, HOOK_CONTAINER_SIZE_CHANGED, HOOK_CONTENTS_CHANGED, HOOK_CREATED, HOOK_DETAILS_CHANGED, HOOK_FOCUS_SCROLL, HOOK_NATIVE_SCROLL, HOOK_NATIVE_SCROLL_END, HOOK_NATIVE_SCROLL_START, HOOK_PROGRAMMATIC_SCROLL, HOOK_PROGRAMMATIC_SCROLL_END, HOOK_PROGRAMMATIC_SCROLL_START, HOOK_SCROLL, HOOK_SCROLL_END, HOOK_SCROLL_START, Slider, SliderCallback, SliderDetails, SliderHooks, SliderOptionArgs, SliderOptions, SliderPlugin };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { DeepPartial, Slider } from '../core/index.
|
|
1
|
+
import { DeepPartial, Slider } from '../core/index.d2.ts';
|
|
2
2
|
|
|
3
3
|
type ThumbnailsOptions = {
|
|
4
4
|
mainSlider: Slider;
|
|
5
5
|
};
|
|
6
|
-
declare function
|
|
6
|
+
declare function ThumbnailPlugin(args: DeepPartial<ThumbnailsOptions>): (slider: Slider) => void;
|
|
7
7
|
|
|
8
|
-
export {
|
|
8
|
+
export { ThumbnailPlugin as default };
|
|
9
9
|
export type { ThumbnailsOptions };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function
|
|
1
|
+
function ThumbnailPlugin(args) {
|
|
2
2
|
return (slider) => {
|
|
3
3
|
const options = {
|
|
4
4
|
mainSlider: args.mainSlider,
|
|
@@ -31,15 +31,15 @@ function FullWidthPlugin(args) {
|
|
|
31
31
|
setTimeout(() => {
|
|
32
32
|
const mainActiveSlideIdx = mainSlider.activeSlideIdx;
|
|
33
33
|
const thumbActiveSlideIdx = slider.activeSlideIdx;
|
|
34
|
+
const activeThumbnail = slider.slides[mainActiveSlideIdx];
|
|
35
|
+
setActiveThumbnail(activeThumbnail);
|
|
34
36
|
if (thumbActiveSlideIdx === mainActiveSlideIdx) {
|
|
35
37
|
return;
|
|
36
38
|
}
|
|
37
|
-
const activeThumbnail = slider.slides[mainActiveSlideIdx];
|
|
38
|
-
setActiveThumbnail(activeThumbnail);
|
|
39
39
|
slider.moveToSlide(mainActiveSlideIdx);
|
|
40
40
|
}, 50);
|
|
41
41
|
});
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export {
|
|
45
|
+
export { ThumbnailPlugin as default };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
function e(e){return i=>{const
|
|
1
|
+
function e(e){return i=>{const l={mainSlider:e.mainSlider}.mainSlider,t=(e=null)=>{null===e&&i.slides.length>0&&(e=i.slides[0]),null!==e&&(i.slides.forEach(e=>{e.setAttribute("aria-current","false")}),e.setAttribute("aria-current","true"))};t(),i.slides.forEach((e,i)=>{e.addEventListener("click",()=>{l.moveToSlide(i),t(e)})}),l.on("scrollEnd",()=>{setTimeout(()=>{const e=l.activeSlideIdx,r=i.activeSlideIdx,s=i.slides[e];t(s),r!==e&&i.moveToSlide(e)},50)})}}export{e as default};
|
package/docs/index.html
CHANGED
|
@@ -288,6 +288,35 @@
|
|
|
288
288
|
</div>
|
|
289
289
|
</div>
|
|
290
290
|
|
|
291
|
+
<div class="entry__item">
|
|
292
|
+
<h3>Keyboard focus</h3>
|
|
293
|
+
<p>Focused slide or slide's sub focusable element is moved into view when focused. This is core feature.</p>
|
|
294
|
+
<div class="example-container-1-focus-wrapper">
|
|
295
|
+
<div class="overflow-slider example-container example-container-1-focus">
|
|
296
|
+
<a href="#1" class="example-item example-item--1" style="width: 120px;">1</a>
|
|
297
|
+
<div class="example-item example-item--2" style="width: 500px;">
|
|
298
|
+
<a href="#2a" style="color: #fff;">2a</a>
|
|
299
|
+
<a href="#2b" style="color: #fff; margin-left: 1rem;">2b</a>
|
|
300
|
+
</div>
|
|
301
|
+
<a href="#3" class="example-item example-item--3" style="width: 80px;">3</a>
|
|
302
|
+
<div class="example-item example-item--4" style="width: 450px;">
|
|
303
|
+
<button type="button" style="padding: 0.5rem 1rem;">Button 4</button>
|
|
304
|
+
</div>
|
|
305
|
+
<a href="#5" class="example-item example-item--5" style="width: 150px;">5</a>
|
|
306
|
+
<div class="example-item example-item--6" style="width: 600px;">
|
|
307
|
+
<a href="#6a" style="color: #fff;">6a</a>
|
|
308
|
+
<input type="text" placeholder="Type here" style="margin-left: auto; padding: 0.25rem 0.5rem; width: 120px;" />
|
|
309
|
+
</div>
|
|
310
|
+
<a href="#7" class="example-item example-item--7" style="width: 100px;">7</a>
|
|
311
|
+
<a href="#8" class="example-item example-item--8">8</a>
|
|
312
|
+
<div class="example-item example-item--9" style="width: 400px;">
|
|
313
|
+
<a href="#9" style="color: #fff;">Link 9</a>
|
|
314
|
+
</div>
|
|
315
|
+
<a href="#10" class="example-item example-item--10" style="width: 140px;">10</a>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
291
320
|
</section>
|
|
292
321
|
|
|
293
322
|
<section class="entry__section">
|
package/package.json
CHANGED
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;
|
package/src/overflow-slider.scss
CHANGED
|
@@ -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
|
+
|