@wanjapflueger/a11y-slider
Version:
A fully accessible slider
810 lines (605 loc) • 22.3 kB
Markdown
# a11y-slider
A slider carousel that is fully accessible. It has no design or theme applied. That way you can be certain the slider is usable and accessible and you can apply your own styles.
[](https://gitlab.com/wanjapflueger/a11y-slider/-/pipelines)



_*Yes I know that the slider in the screenshot above has some [example styles](#example) applied..._
---
## Table of contents
<!-- TOC -->
- [a11y-slider](#a11y-slider)
- [Table of contents](#table-of-contents)
- [Requirements](#requirements)
- [Browser support](#browser-support)
- [Changelog](#changelog)
- [Install](#install)
- [Example](#example)
- [Usage](#usage)
- [Methods](#methods)
- [Method Create](#method-create)
- [Parameters](#parameters)
- [slides](#slides)
- [arrows](#arrows)
- [autoplay](#autoplay)
- [loop](#loop)
- [caption](#caption)
- [lang](#lang)
- [pagination](#pagination)
- [slideBy](#slideby)
- [syncWith](#syncwith)
- [onMoveStart](#onmovestart)
- [onMoveEnd](#onmoveend)
- [onCreated](#oncreated)
- [onDestroyed](#ondestroyed)
- [onUpdate](#onupdate)
- [Method Destroy](#method-destroy)
- [Breakpoints](#breakpoints)
- [Method Prev](#method-prev)
- [Method Next](#method-next)
- [Method GoTo](#method-goto)
- [GET](#get)
- [Get elements](#get-elements)
- [Get config](#get-config)
- [Styles](#styles)
- [Import](#import)
- [Space between slides](#space-between-slides)
- [Hide every nth bullet](#hide-every-nth-bullet)
- [Slides per view](#slides-per-view)
- [Tips and Tricks](#tips-and-tricks)
- [Keyboard support](#keyboard-support)
- [Dataset attributes](#dataset-attributes)
- [Issues](#issues)
- [Event Listeners do not work](#event-listeners-do-not-work)
- [Accessibility Compliance Report](#accessibility-compliance-report)
<!-- /TOC -->
---
## Requirements
- You need to be able to <a target="_blank" href="https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Statements/import">import</a> or <a target="_blank" href="https://nodejs.org/en/knowledge/getting-started/what-is-require/">require</a> _JavaScript_ files
- You need to be able to <a target="_blank" href="https://sass-lang.com/documentation/at-rules/import">import</a> _SCSS_ with [SASS](https://github.com/sass/dart-sass) (since [v2.0.0](https://gitlab.com/wanjapflueger/a11y-slider/-/compare/v1.7.0...v2.0.0)).
### Browser support
- Chrome 84
- Firefox 63
- Edge 84
- Opera 73
- Safari 14
## Changelog
See [changelog.md](https://gitlab.com/wanjapflueger/a11y-slider/-/blob/master/changelog.md).
## Install
```
npm i @wanjapflueger/a11y-slider
```
## Example
[example.wanjapflueger.de/a11y-slider/](https://example.wanjapflueger.de/a11y-slider/)
## Usage
Import class `Slider`:
```js
import { Slider } from "@wanjapflueger/a11y-slider";
// var a11ySlider = require("@wanjapflueger/a11y-slider")
```
Create a new instance:
```js
const mySlider = new Slider();
// const mySlider = new a11ySlider.Slider();
```
## Methods
### Method Create
1. Create any reference element containing child elements. The contents of the each child element will be a single slide.
```html
<div id="my-list">
<div>1</div>
<div>2</div>
...
</div>
```
1. Assign that reference element with _JavaScript_ to a variable, initiate a new instance of `Slider` and call _method_ `Create`.
```js
import { Slider } from "@wanjapflueger/a11y-slider";
const mySlider = new Slider();
mySlider.Create({
slides: document.getElementById('my-list')
});
```
#### Parameters
See _interface_ `SliderParameters` in [src/index.ts](https://gitlab.com/wanjapflueger/a11y-slider/-/blob/master/src/index.ts).
##### slides
| Parameter | Required | Type | Default value |
| --------- | -------- | ------------- | ------------- |
| `slides` | ✅ Yes | `HTMLElement` | – |
Any `HTMLElement` containing child elements (`HTMLUListElement` in the example below). Of each child element (`HTMLLIElement` in the example below) the `innerHTML` will be placed inside a slide. This means the `HTMLUListElement` and `HTMLLIElement[]` will not be transferred to the slider, only the `HTMLDivElement[]` will, because they are inside the children of the `HTMLUListElement`.
Example:
```html
<ul>
<li><div class="item">A</div></li>
<li><div class="item">B</div></li>
<li><div class="item">C</div></li>
</ul>
```
```js
const slider = new Slider()
slider.Create({
slides: document.querySelector('ul')
})
```
##### arrows
| Parameter | Required | Type | Default value |
| --------- | -------- | --------------------- | ------------- |
| `arrows` | ❌ No | `boolean` or `Arrows` | `false` |
Arrows configuration. You may use a custom `Element` for the previous and/or next button. Do not use a `HTMLButtonElement`. You may change the text on the arrows.
```html
<span class="next">Next slide</span>
<span class="prev">Previous slide</span>
```
```js
const slider = new Slider()
slider.Create({
// ...
arrows: true | false | {
label: 'Slider controls' | undefined,
next: {
icon: document.querySelector('.next') | '→' | undefined,
label: document.querySelector('.next').innerText
},
prev: {
icon: document.querySelector('.prev') | '←' | undefined,
label: document.querySelector('.prev').innerText
}
}
})
```
See also:
- _interface_ [`Arrows`](https://gitlab.com/wanjapflueger/a11y-slider/-/blob/master/src/index.ts)
- _interface_ [`Arrow`](https://gitlab.com/wanjapflueger/a11y-slider/-/blob/master/src/index.ts)
##### autoplay
| Parameter | Required | Type | Default value |
| ---------- | -------- | -------- | ------------- |
| `autoplay` | ❌ No | `number` | `undefined` |
Autoplay speed in milliseconds.
Example:
```js
const slider = new Slider()
slider.Create({
// ...
autoplay: 4000 | undefined
})
```
##### loop
| Parameter | Required | Type | Default value |
| --------- | -------- | --------- | ------------- |
| `loop` | ❌ No | `boolean` | `false` |
Go from the first to the last slide and the other way around.
Example:
```js
const slider = new Slider()
slider.Create({
// ...
loop: true | false
})
```
##### caption
| Parameter | Required | Type | Default value |
| --------- | -------- | -------- | ------------- |
| `caption` | ❌ No | `string` | `'Slider'` |
Slider caption for screen readers and bots. If empty will use:
- the `[aria-label]` attribute text on [slides](#slides) or
- the `innerText` of any `Element` that is referenced with `[aria-labelledby]` or
- the `[title]` attribute text on [slides](#slides).
Example:
```js
const slider = new Slider()
slider.Create({
// ...
caption: 'My Slider' | undefined
})
```
##### lang
| Parameter | Required | Type | Default value |
| --------- | -------- | -------- | -------------------------------------------------------- |
| `lang` | ❌ No | `string` | `document.documentElement.lang` (the documents language) |
Language Code <a href="https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes" target="_blanK">ISO 639-1</a>.
Example:
```js
const slider = new Slider()
slider.Create({
// ...
lang: 'de' | 'es' | 'en' | undefined
})
```
##### pagination
| Parameter | Required | Type | Default value |
| ------------ | -------- | ------------------------- | ------------- |
| `pagination` | ❌ No | `boolean` or `Pagination` | `false` |
Pagination configuration. You can change the label for the pagination element. You can change the `innerHTML` of the `HTMLButtonElement[]` elements that are the paginations bullets. Each bullet displays a number by default (starting with `'1'`). Each Bullet will always have the current index as text for screen readers and bots.
```html
<span class="bullet">⦿</span>
```
```js
const slider = new Slider()
slider.Create({
// ...
pagination: true | false | {
icon: document.querySelector('.bullet') | '⦿' | undefined,
label: 'Slider pagination' | undefined
}
})
```
See also:
- _interface_ [`Pagination`](https://gitlab.com/wanjapflueger/a11y-slider/-/blob/master/src/index.ts)
##### slideBy
| Parameter | Required | Type | Default value |
| --------- | -------- | -------- | ------------- |
| `slideBy` | ❌ No | `number` | `1` |
Number of slides to scroll when using prev/next arrows, arrow keys or autoplay.
Example:
```js
const slider = new Slider()
slider.Create({
// ...
slideBy: 2 | undefined
})
```
##### syncWith
| Parameter | Required | Type | Default value |
| ---------- | -------- | -------- | ------------- |
| `syncWith` | ❌ No | `Slider` | `undefined` |
Sync a slider with other sliders.
Example:
```js
const sliderA = new Slider()
const sliderB = new Slider()
const sliderC = new Slider()
sliderA.Create({
// ...
syncWith: [sliderB, sliderC]
})
sliderB.Create({
// ...
syncWith: [sliderA, sliderC]
})
sliderC.Create({
// ...
syncWith: [sliderA, sliderB]
})
```
See also:
- _interface_ [`Slider`](https://gitlab.com/wanjapflueger/a11y-slider/-/blob/master/src/index.ts)
##### onMoveStart
| Parameter | Required | Type | Default value |
| ------------- | -------- | ------------------------ | ------------- |
| `onMoveStart` | ❌ No | `(status: Status) => {}` | `undefined` |
Callback function once the sliders movement starts.
Example:
```js
const slider = new Slider()
slider.Create({
// ...
onMoveStart: (status) => {
console.log('Slider has started moving', status)
} | undefined
})
```
See also:
- _interface_ [`Status`](https://gitlab.com/wanjapflueger/a11y-slider/-/blob/master/src/index.ts)
##### onMoveEnd
| Parameter | Required | Type | Default value |
| ----------- | -------- | ------------------------ | ------------- |
| `onMoveEnd` | ❌ No | `(status: Status) => {}` | `undefined` |
Callback function once the sliders movement stops.
Example:
```js
const slider = new Slider()
slider.Create({
// ...
onMoveEnd: (status) => {
console.log('Slider has stopped moving', status)
} | undefined
})
```
See also:
- _interface_ [`Status`](https://gitlab.com/wanjapflueger/a11y-slider/-/blob/master/src/index.ts)
##### onCreated
| Parameter | Required | Type | Default value |
| ----------- | -------- | ---------- | ------------- |
| `onCreated` | ❌ No | `() => {}` | `undefined` |
Callback function after the slider was created. Function is called once, when the slider is successfully [created](#method-create). Use this to apply event listeners to elements in any slide ([read more](#event-listener-do-not-work)).
Example:
```js
const slider = new Slider()
slider.Create({
// ...
onCreated: () => {
console.log('Slider was created')
} | undefined
})
```
##### onDestroyed
| Parameter | Required | Type | Default value |
| ------------- | -------- | ---------- | ------------- |
| `onDestroyed` | ❌ No | `() => {}` | `undefined` |
Callback function after the slider was destroyed. Function is called once, when the slider is successfully [destroyed](#method-destroy).
Example:
```js
const slider = new Slider()
slider.Create({
// ...
onDestroyed: () => {
console.log('Slider was destroyed')
} | undefined
})
```
##### onUpdate
| Parameter | Required | Type | Default value |
| ---------- | -------- | ------------------------ | ------------- |
| `onUpdate` | ❌ No | `(status: Status) => {}` | `undefined` |
Callback function on update. Update is called, whenever the slider changes or moves.
Example:
```js
const slider = new Slider()
slider.Create({
// ...
onUpdate: (status) => {
console.log('Slider is moving or updating', status)
} | undefined
})
```
See also:
- _interface_ [`Status`](https://gitlab.com/wanjapflueger/a11y-slider/-/blob/master/src/index.ts)
#### Method Destroy
Destroy the slider.
```js
mySlider.Destroy()
```
Once destroyed you can call [Method Create](#method-create) again at any time, for example on a window resize event.
##### Breakpoints
To destroy the slider for a specific breakpoint, whilst maintaining a solid performance, one could use the following code.
```js
myEfficientFn(window, 'resize', () => {
if(window.innerWidth >= 1024) {
mySlider.Destroy()
} else {
mySlider.Create()
}
}, 100, true);
```
_myEfficientFn.js_
```js
/**
* Very efficient function with debounce and throttle events
* @param {HTMLElement} target Target element for eventListener
* @param {EventListener} eventListener EventListener
* @param {function} func Callback function
* @param {number} time Wait for miliseconds after each call
* @param {boolean} callOnLoad Call func when this function is called
* @returns {void}
* @example
* myEfficientFn(window, 'scroll', () => { return console.log('hello'); }, 100, true);
*/
function myEfficientFn(target, eventListener, func, time, callOnLoad) {
/**
* Throttle function
* @see {@link https://codeburst.io/throttling-and-debouncing-in-javascript-b01cad5c8edf}
* @param {function} f Callback function
* @param {number} w Wait for miliseconds until next execution
* @returns {function} Calls callback function
* @example
* window.addEventListener('scroll', throttle(() => { return console.log('hello') }, 100));
*/
function throttle(f, w) {
/** @type {boolean} Is throtteling */
let t;
return function () {
/** @type {*} Arguments */
const a = arguments;
/** @type {*} Context */
const c = this;
if (!t) {
f.apply(c, a);
t = true;
setTimeout(() => (t = false), w);
}
};
}
/**
* Returns a function, that, as long as it continues to be invoked, will not be triggered. The function will be called after it stops being called for N milliseconds. If `immediate` is passed, trigger the function on the leading edge, instead of the trailing.
* @see {@link https://davidwalsh.name/javascript-debounce-function}
* @param {function} f Callback function
* @param {number} w Wait for miliseconds until next execution
* @param {boolean} i Do not wait
* @returns {function} func
* @example
* window.addEventListener('scroll', debounce(() => { console.log('hello'); }, 100));
*/
function debounce(f, w, i) {
/** @type {Number|null} Timeout */
let t;
return () => {
/** @type {*} Arguments */
const a = arguments;
/** @type {*} Context */
const c = this;
const later = () => {
t = null;
if (!i) f.apply(c, a);
};
/** @type {Boolean} Call now */
const n = i && !t;
clearTimeout(t);
t = setTimeout(later, w);
if (n) f.apply(c, a);
};
}
// Call on load
if (callOnLoad) {
func.call();
}
// Throttle
target.addEventListener(
eventListener,
throttle(() => {
return func.call();
}, time)
);
// Debounce
target.addEventListener(
eventListener,
debounce(() => {
func.call();
}, time)
);
}
```
#### Method Prev
Go to previous slide.
```js
mySlider.Prev();
```
#### Method Next
Go to next slide.
```js
mySlider.Next();
```
#### Method GoTo
Go to a specific slide.
```js
mySlider.GoTo(3); // go to slide 3
```
### GET
#### Get elements
Get all the sliders HTML-elements.
```js
mySlider.elements.arrowNext // The HTMLButtonElement of the next arrow
mySlider.elements.arrowPrev // The HTMLButtonElement of the previous arrow
mySlider.elements.arrows // The element containing the arrows
mySlider.elements.caption // The element containing the caption
mySlider.elements.items // All slides as list items
mySlider.elements.list // The unordered list element containing all slides
mySlider.elements.pagination // The element containing the pagination
mySlider.elements.paginationBullets // All pagination bullets as HTMLButtonElement
```
#### Get config
Get slider configuration.
```js
mySlider.config.arrows // `true` if the slider has arrows
mySlider.config.autoplay // `true` if autoplay is currently active
mySlider.config.autoplaySpeed // Autoplay speed
mySlider.config.caption // The slider caption
mySlider.config.slides // The number of slides
mySlider.config.pagination // `true` if the slider has a pagination
```
## Styles
The slider does not come with any design or theme applied. It has only required functional styles. You may however apply some [example styles](#example-styles).
### Import
Some functional CSS is required to make the slider work. Make sure to import the _SCSS_ from the node module.
```scss
@import "path-to/node_modules/@wanjapflueger/a11y-slider/lib/index";
```
### Space between slides
Overwrite the default value for `$space-between-slides` from [src/partials/slide/_index.scss](https://gitlab.com/wanjapflueger/a11y-slider/-/blob/master/src/partials/slide/_index.scss) globally __before you import the SCSS__.
```scss
$space-between-slides: 80px;
@import "path-to/node_modules/@wanjapflueger/a11y-slider/lib/index";
```
If you do not want to set this value globally but per slider, continue reading [Slides per view](#slides-per-view).
### Hide every nth bullet
A single bullet exists for each slide. However if you have lots of slides this will look silly. You can hide some of those bullets like shown in the example below.
```scss
[data-a11y-slider] [data-a11y-slider-pagination] > nav > ul > li {
// Show only every fourth bullet (1, 5, 9 etc.) for tablet
&:not(:nth-child(4n + 1)) {
@media (max-width: 767px) {
display: none;
}
}
// Show only every second bullet (1, 3, 5, etc.) for tablet
&:not(:nth-child(2n + 1)) {
@media (min-width: 768px) and (max-width: 1024px) {
display: none;
}
}
}
```
### Slides per view
Use `@mixin slide-sizing` from [src/partials/slide/_index.scss](https://gitlab.com/wanjapflueger/a11y-slider/-/blob/master/src/partials/slide/_index.scss) to define how many slides should be visible for each breakpoint.
```scss
[data-a11y-slider] {
@include slide-sizing(1); // Mobile: show 1 slide, space between slides is $space-between-slides
@media (min-width: 1280px) {
@include slide-sizing(2.5, 80); // Desktop: show 2 and a half slides, space between slides is 80px
}
}
```
__Pro Tip:__ Even if you display only one slide with `slide-sizing(1)` you can still provide a grid gap.
## Tips and Tricks
- Add a `margin` when using a `box-shadow`:
```scss
[data-a11y-slider-slide] {
margin: 2em 0; // set margin according to the shadows dimensions
}
```
## Keyboard support
In [src/partials/keys.ts](https://gitlab.com/wanjapflueger/a11y-slider/-/blob/master/src/partials/keys.ts) several keys have been assigned a function. The keyboard support starts when a slider is active. A slider is active when:
- the user hovers the slider
- the focus is on the slider (`[data-a11y-slider]`)
- any child element inside the slider has the focus (`[data-a11y-slider]`)
The slider will have `[aria-current="true"]` while active.
| Key | Description |
| ------------ | -------------- |
| `ArrowRight` | Next slide |
| `ArrowLeft` | Previous slide |
| `Digit1` | Slide 1 |
| `Digit2` | Slide 2 |
| `Digit3` | Slide 3 |
| `Digit4` | Slide 4 |
| `Digit5` | Slide 5 |
| `Digit6` | Slide 6 |
| `Digit7` | Slide 7 |
| `Digit8` | Slide 8 |
| `Digit9` | Slide 9 |
## Dataset attributes
You may overwrite some _JavaScript_ [parameters](#parameters) with the following HTML attributes. Set these attributes on your [reference element](#usage).
- [autoplay](#autoplay)
```html
<div id="my-list" data-a11y-slider-autoplay="4000">
...
</div>
```
- [lang](#lang)
```html
<div id="my-list" data-a11y-slider-lang="de">
...
</div>
```
- [slideBy](#slideby)
```html
<div id="my-list" data-a11y-slider-slide-by="2">
...
</div>
```
## Issues
### Event Listeners do not work
Issue: [https://gitlab.com/wanjapflueger/a11y-slider/-/issues/1](https://gitlab.com/wanjapflueger/a11y-slider/-/issues/1)
As stated in [How to copy a DOM node with event listeners?](https://stackoverflow.com/a/15411683), event listeners applied to elements within the slider cannot be copied when creating a new slider instance.
To workaround this issue, apply any event listeners in a callback function, that can be passed with the [parameter `onCreated`](#parameters-on-created).
Example:
```js
slider.Create({
// ...
onCreated: () => {
const buttons = document.querySelectorAll('button');
buttons.forEach((button) => {
button.addEventListener('click', () => {
console.log('clicked on a button inside a slide');
})
});
}
// ...
});
```
## Accessibility Compliance Report
[WCAG](https://www.w3.org/WAI/WCAG21/quickref/) Level: **AAA** [^1]
| Browser | Platform | Screen reader | Passed |
| -------------------- | ------------- | -------------------------------------------------------------------------- | ------ |
| Chrome 90.0.4430.212 | MacOS 10.15.7 | [VoiceOver](https://www.apple.com/de/accessibility/vision/) | ✅ |
| Chrome 90.0.4430.210 | Android 10 | [Talkback](https://support.google.com/accessibility/android#topic=6007234) | ✅ |
---
[^1]: This information refers only to the technical aspects of the component, not to the design or the editorial handling of any content.