@evermade/overflow-slider 1.0.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/.editorconfig +22 -0
- package/.github/workflows/npm-publish.yml +33 -0
- package/.nvmrc +1 -0
- package/LICENSE +21 -0
- package/README.md +104 -0
- package/changelog.md +5 -0
- package/dist/index.esm.js +694 -0
- package/dist/index.esm.min.js +2 -0
- package/dist/index.js +709 -0
- package/dist/index.min.js +2 -0
- package/dist/overflow-slider.css +1 -0
- package/docs/assets/demo.css +513 -0
- package/docs/assets/demo.js +113 -0
- package/docs/dist/overflow-slider.css +1 -0
- package/docs/dist/overflow-slider.esm.js +694 -0
- package/docs/index.html +230 -0
- package/package.json +55 -0
- package/rollup.config.js +45 -0
- package/src/core/details.ts +43 -0
- package/src/core/slider.ts +234 -0
- package/src/core/types.ts +41 -0
- package/src/core/utils.ts +24 -0
- package/src/index.ts +18 -0
- package/src/overflow-slider.scss +213 -0
- package/src/overflow-slider.ts +40 -0
- package/src/plugins/arrows.ts +107 -0
- package/src/plugins/dots.ts +129 -0
- package/src/plugins/drag-scrolling.ts +78 -0
- package/src/plugins/scroll-indicator.ts +152 -0
- package/src/plugins/skip-links.ts +61 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Slider } from '../core/types';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_CLASS_NAMES = {
|
|
4
|
+
scrollIndicator: 'overflow-slider__scroll-indicator',
|
|
5
|
+
scrollIndicatorBar: 'overflow-slider__scroll-indicator-bar',
|
|
6
|
+
scrollIndicatorButton: 'overflow-slider__scroll-indicator-button',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type ScrollIndicatorOptions = {
|
|
10
|
+
classNames: {
|
|
11
|
+
scrollIndicator: string;
|
|
12
|
+
scrollIndicatorBar: string;
|
|
13
|
+
scrollIndicatorButton: string;
|
|
14
|
+
},
|
|
15
|
+
container: HTMLElement | null,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default function ScrollIndicatorPlugin( args: { [key: string]: any } ) {
|
|
19
|
+
return ( slider: Slider ) => {
|
|
20
|
+
|
|
21
|
+
const options = <ScrollIndicatorOptions>{
|
|
22
|
+
classNames: {
|
|
23
|
+
...DEFAULT_CLASS_NAMES,
|
|
24
|
+
...args?.classNames || []
|
|
25
|
+
},
|
|
26
|
+
container: args?.container ?? null,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
const scrollbarContainer = document.createElement( 'div' );
|
|
31
|
+
scrollbarContainer.setAttribute( 'class', options.classNames.scrollIndicator );
|
|
32
|
+
scrollbarContainer.setAttribute( 'tabindex', '0' );
|
|
33
|
+
scrollbarContainer.setAttribute( 'role', 'scrollbar' );
|
|
34
|
+
scrollbarContainer.setAttribute( 'aria-controls', slider.container.getAttribute( 'id' ) ?? '' );
|
|
35
|
+
scrollbarContainer.setAttribute( 'aria-orientation', 'horizontal' );
|
|
36
|
+
scrollbarContainer.setAttribute( 'aria-valuemax', '100' );
|
|
37
|
+
scrollbarContainer.setAttribute( 'aria-valuemin', '0' );
|
|
38
|
+
scrollbarContainer.setAttribute( 'aria-valuenow', '0' );
|
|
39
|
+
|
|
40
|
+
const scrollbar = document.createElement( 'div' );
|
|
41
|
+
scrollbar.setAttribute( 'class', options.classNames.scrollIndicatorBar );
|
|
42
|
+
|
|
43
|
+
const scrollbarButton = document.createElement( 'div' );
|
|
44
|
+
scrollbarButton.setAttribute( 'class', options.classNames.scrollIndicatorButton );
|
|
45
|
+
scrollbarButton.setAttribute( 'data-is-grabbed', 'false' );
|
|
46
|
+
|
|
47
|
+
scrollbar.appendChild( scrollbarButton );
|
|
48
|
+
scrollbarContainer.appendChild( scrollbar );
|
|
49
|
+
|
|
50
|
+
const setDataAttributes = () => {
|
|
51
|
+
scrollbarContainer.setAttribute( 'data-has-overflow', slider.details.hasOverflow.toString() );
|
|
52
|
+
}
|
|
53
|
+
setDataAttributes();
|
|
54
|
+
|
|
55
|
+
const getScrollbarButtonLeftOffset = () => {
|
|
56
|
+
const scrollbarRatio = slider.container.offsetWidth / slider.container.scrollWidth;
|
|
57
|
+
return slider.container.scrollLeft * scrollbarRatio;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// scrollbarbutton width and position is calculated based on the scroll position and available width
|
|
61
|
+
let requestId = 0;
|
|
62
|
+
const update = () => {
|
|
63
|
+
if ( requestId ) {
|
|
64
|
+
window.cancelAnimationFrame( requestId );
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
requestId = window.requestAnimationFrame(() => {
|
|
68
|
+
const scrollbarButtonWidth = (slider.container.offsetWidth / slider.container.scrollWidth) * 100;
|
|
69
|
+
const scrollLeftInPortion = getScrollbarButtonLeftOffset();
|
|
70
|
+
scrollbarButton.style.width = `${scrollbarButtonWidth}%`;
|
|
71
|
+
scrollbarButton.style.transform = `translateX(${scrollLeftInPortion}px)`;
|
|
72
|
+
|
|
73
|
+
// aria-valuenow
|
|
74
|
+
const scrollLeft = slider.container.scrollLeft;
|
|
75
|
+
const scrollWidth = slider.container.scrollWidth;
|
|
76
|
+
const containerWidth = slider.container.offsetWidth;
|
|
77
|
+
const scrollPercentage = (scrollLeft / (scrollWidth - containerWidth)) * 100;
|
|
78
|
+
scrollbarContainer.setAttribute( 'aria-valuenow', Math.round(Number.isNaN(scrollPercentage) ? 0 : scrollPercentage).toString() );
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// insert to DOM
|
|
83
|
+
if ( options.container ) {
|
|
84
|
+
options.container.appendChild( scrollbarContainer );
|
|
85
|
+
} else {
|
|
86
|
+
slider.container.parentNode?.insertBefore( scrollbarContainer, slider.container.nextSibling );
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// update the scrollbar when the slider is scrolled
|
|
90
|
+
update();
|
|
91
|
+
slider.on( 'scroll', update );
|
|
92
|
+
slider.on( 'contentsChanged', update );
|
|
93
|
+
slider.on( 'containerSizeChanged', update );
|
|
94
|
+
slider.on( 'detailsChanged', setDataAttributes );
|
|
95
|
+
|
|
96
|
+
// handle arrow keys while focused
|
|
97
|
+
scrollbarContainer.addEventListener( 'keydown', (e) => {
|
|
98
|
+
if ( e.key === 'ArrowLeft' ) {
|
|
99
|
+
slider.moveToDirection( 'prev' );
|
|
100
|
+
} else if ( e.key === 'ArrowRight' ) {
|
|
101
|
+
slider.moveToDirection( 'next' );
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// handle click to before or after the scrollbar button
|
|
106
|
+
scrollbarContainer.addEventListener( 'click', (e) => {
|
|
107
|
+
const scrollbarButtonWidth = scrollbarButton.offsetWidth;
|
|
108
|
+
const scrollbarButtonLeft = getScrollbarButtonLeftOffset();
|
|
109
|
+
const scrollbarButtonRight = scrollbarButtonLeft + scrollbarButtonWidth;
|
|
110
|
+
const clickX = e.pageX - scrollbarContainer.offsetLeft;
|
|
111
|
+
if ( clickX < scrollbarButtonLeft ) {
|
|
112
|
+
slider.moveToDirection( 'prev' );
|
|
113
|
+
} else if ( clickX > scrollbarButtonRight ) {
|
|
114
|
+
slider.moveToDirection( 'next' );
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// make scrollbar button draggable via mouse/touch and update the scroll position
|
|
119
|
+
let isMouseDown = false;
|
|
120
|
+
let startX = 0;
|
|
121
|
+
let scrollLeft = 0;
|
|
122
|
+
|
|
123
|
+
scrollbarButton.addEventListener('mousedown', (e) => {
|
|
124
|
+
isMouseDown = true;
|
|
125
|
+
startX = e.pageX - scrollbarContainer.offsetLeft;
|
|
126
|
+
scrollLeft = slider.container.scrollLeft;
|
|
127
|
+
// change cursor to grabbing
|
|
128
|
+
scrollbarButton.style.cursor = 'grabbing';
|
|
129
|
+
slider.container.style.scrollSnapType = 'none';
|
|
130
|
+
scrollbarButton.setAttribute( 'data-is-grabbed', 'true' );
|
|
131
|
+
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
e.stopPropagation();
|
|
134
|
+
});
|
|
135
|
+
window.addEventListener('mouseup', () => {
|
|
136
|
+
isMouseDown = false;
|
|
137
|
+
scrollbarButton.style.cursor = '';
|
|
138
|
+
slider.container.style.scrollSnapType = '';
|
|
139
|
+
scrollbarButton.setAttribute( 'data-is-grabbed', 'false' );
|
|
140
|
+
});
|
|
141
|
+
window.addEventListener('mousemove', (e) => {
|
|
142
|
+
if (!isMouseDown) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
const x = e.pageX - scrollbarContainer.offsetLeft;
|
|
147
|
+
const scrollingFactor = slider.container.scrollWidth / scrollbarContainer.offsetWidth;
|
|
148
|
+
const walk = (x - startX) * scrollingFactor;
|
|
149
|
+
slider.container.scrollLeft = scrollLeft + walk;
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Slider } from '../core/types';
|
|
2
|
+
import { generateId } from '../core/utils';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_TEXTS = {
|
|
5
|
+
skipList: 'Skip list'
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const DEFAULT_CLASS_NAMES = {
|
|
9
|
+
skipLink: 'screen-reader-text',
|
|
10
|
+
skipLinkTarget: 'overflow-slider__skip-link-target',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type SkipLinkOptions = {
|
|
14
|
+
texts: {
|
|
15
|
+
skipList: string;
|
|
16
|
+
},
|
|
17
|
+
classNames: {
|
|
18
|
+
skipLink: string;
|
|
19
|
+
skipLinkTarget: string;
|
|
20
|
+
},
|
|
21
|
+
containerBefore: HTMLElement | null,
|
|
22
|
+
containerAfter: HTMLElement | null,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default function SkipLinksPlugin( args: { [key: string]: any } ) {
|
|
26
|
+
return ( slider: Slider ) => {
|
|
27
|
+
const options = <SkipLinkOptions>{
|
|
28
|
+
texts: {
|
|
29
|
+
...DEFAULT_TEXTS,
|
|
30
|
+
...args?.texts || []
|
|
31
|
+
},
|
|
32
|
+
classNames: {
|
|
33
|
+
...DEFAULT_CLASS_NAMES,
|
|
34
|
+
...args?.classNames || []
|
|
35
|
+
},
|
|
36
|
+
containerBefore: args?.containerAfter ?? null,
|
|
37
|
+
containerAfter: args?.containerAfter ?? null,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const skipId = generateId( 'overflow-slider-skip' );
|
|
41
|
+
const skipLinkEl = document.createElement( 'a' );
|
|
42
|
+
skipLinkEl.setAttribute( 'href', `#${skipId}` );
|
|
43
|
+
skipLinkEl.textContent = options.texts.skipList;
|
|
44
|
+
skipLinkEl.classList.add( options.classNames.skipLink );
|
|
45
|
+
|
|
46
|
+
const skipTargetEl = document.createElement( 'div' );
|
|
47
|
+
skipTargetEl.setAttribute( 'id', skipId );
|
|
48
|
+
skipTargetEl.setAttribute( 'tabindex', '-1' );
|
|
49
|
+
|
|
50
|
+
if ( options.containerBefore ) {
|
|
51
|
+
options.containerBefore.parentNode?.insertBefore( skipLinkEl, options.containerBefore );
|
|
52
|
+
} else {
|
|
53
|
+
slider.container.parentNode?.insertBefore( skipLinkEl, slider.container );
|
|
54
|
+
}
|
|
55
|
+
if ( options.containerAfter ) {
|
|
56
|
+
options.containerAfter.parentNode?.insertBefore( skipTargetEl, options.containerAfter.nextSibling );
|
|
57
|
+
} else {
|
|
58
|
+
slider.container.parentNode?.insertBefore( skipTargetEl, slider.container.nextSibling );
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
};
|