Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Slider accessibility #320

Merged
merged 7 commits into from
Feb 17, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 134 additions & 12 deletions js/slider.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
indicators: true,
height: 400,
duration: 500,
interval: 6000
interval: 6000,
pauseOnFocus: true,
pauseOnHover: true,
indicatorLabelFunc: null // Function which will generate a label for the indicators (ARIA)
};

/**
Expand All @@ -31,9 +34,18 @@
* @prop {Number} [height=400] - height of slider
* @prop {Number} [duration=500] - Length in ms of slide transition
* @prop {Number} [interval=6000] - Length in ms of slide interval
* @prop {Boolean} [pauseOnFocus=true] - Pauses transition when slider receives keyboard focus
* @prop {Boolean} [pauseOnHover=true] - Pauses transition while mouse hovers the slider
mauromascarenhas marked this conversation as resolved.
Show resolved Hide resolved
*/
this.options = $.extend({}, Slider.defaults, options);

// init props
this.interval = null;
this.eventPause = false;
this._hovered = false;
this._focused = false;
this._focusCurrent = false;

// setup
this.$slider = this.$el.find('.slides');
this.$slides = this.$slider.children('li');
Expand All @@ -49,6 +61,13 @@

this._setSliderHeight();

// Sets element id if it does not have one
if (this.$slider[0].hasAttribute("id")) this._sliderId = this.$slider[0].getAttribute("id");
else {
this._sliderId = "slider-" + M.guid();
this.$slider[0].setAttribute("id", this._sliderId);
}
mauromascarenhas marked this conversation as resolved.
Show resolved Hide resolved

// Set initial positions of captions
this.$slides.find('.caption').each((el) => {
this._animateCaptionIn(el, 0);
Expand All @@ -63,12 +82,19 @@
$(el).attr('src', placeholderBase64);
}
});
this.$slides.each((el) => {
// Sets slide as focusable by code
if (!el.hasAttribute("tabindex")) el.setAttribute("tabindex", -1);
mauromascarenhas marked this conversation as resolved.
Show resolved Hide resolved
// Removes initial visibility from "inactive" slides
el.style.visibility = 'hidden';
});

this._setupIndicators();

// Show active slide
if (this.$active) {
this.$active.css('display', 'block');
this.$active[0].style.visibility = 'visible';
mauromascarenhas marked this conversation as resolved.
Show resolved Hide resolved
} else {
this.$slides.first().addClass('active');
anim({
Expand All @@ -77,13 +103,14 @@
duration: this.options.duration,
easing: 'easeOutQuad'
});
this.$slides.first()[0].style.visibility = 'visible';
mauromascarenhas marked this conversation as resolved.
Show resolved Hide resolved

this.activeIndex = 0;
this.$active = this.$slides.eq(this.activeIndex);

// Update indicators
if (this.options.indicators) {
this.$indicators.eq(this.activeIndex).addClass('active');
this.$indicators.eq(this.activeIndex).children().first().addClass('active');
}
}

Expand Down Expand Up @@ -137,10 +164,23 @@
_setupEventHandlers() {
this._handleIntervalBound = this._handleInterval.bind(this);
this._handleIndicatorClickBound = this._handleIndicatorClick.bind(this);
this._handleAutoPauseFocusBound = this._handleAutoPauseFocus.bind(this);
this._handleAutoStartFocusBound = this._handleAutoStartFocus.bind(this);
this._handleAutoPauseHoverBound = this._handleAutoPauseHover.bind(this);
this._handleAutoStartHoverBound = this._handleAutoStartHover.bind(this);

if (this.options.pauseOnFocus){
mauromascarenhas marked this conversation as resolved.
Show resolved Hide resolved
this.el.addEventListener('focusin', this._handleAutoPauseFocusBound);
this.el.addEventListener('focusout', this._handleAutoStartFocusBound);
}
if (this.options.pauseOnHover){
mauromascarenhas marked this conversation as resolved.
Show resolved Hide resolved
this.el.addEventListener('mouseenter', this._handleAutoPauseHoverBound);
this.el.addEventListener('mouseleave', this._handleAutoStartHoverBound);
}

if (this.options.indicators) {
this.$indicators.each((el) => {
el.addEventListener('click', this._handleIndicatorClickBound);
el.children[0].addEventListener('click', this._handleIndicatorClickBound);
});
mauromascarenhas marked this conversation as resolved.
Show resolved Hide resolved
}
}
Expand All @@ -149,9 +189,17 @@
* Remove Event Handlers
*/
_removeEventHandlers() {
if (this.options.pauseOnFocus){
mauromascarenhas marked this conversation as resolved.
Show resolved Hide resolved
this.el.removeEventListener('focusin', this._handleAutoPauseFocusBound);
this.el.removeEventListener('focusout', this._handleAutoStartFocusBound);
}
if (this.options.pauseOnHover){
mauromascarenhas marked this conversation as resolved.
Show resolved Hide resolved
this.el.removeEventListener('mouseenter', this._handleAutoPauseHoverBound);
this.el.removeEventListener('mouseleave', this._handleAutoStartHoverBound);
}
if (this.options.indicators) {
this.$indicators.each((el) => {
el.removeEventListener('click', this._handleIndicatorClickBound);
el.children[0].removeEventListener('click', this._handleIndicatorClickBound);
});
mauromascarenhas marked this conversation as resolved.
Show resolved Hide resolved
}
}
Expand All @@ -161,10 +209,51 @@
* @param {Event} e
*/
_handleIndicatorClick(e) {
let currIndex = $(e.target).index();
let currIndex = $(e.target).parent().index();
this._focusCurrent = true;
this.set(currIndex);
}

/**
* Mouse enter event handler
*/
_handleAutoPauseHover() {
this._hovered = true;
if (this.interval != null){
mauromascarenhas marked this conversation as resolved.
Show resolved Hide resolved
this._pause(true);
}
}

/**
* Focus in event handler
*/
_handleAutoPauseFocus() {
this._focused = true;
if (this.interval != null){
mauromascarenhas marked this conversation as resolved.
Show resolved Hide resolved
this._pause(true);
}
}

/**
* Mouse enter event handler
*/
_handleAutoStartHover() {
this._hovered = false;
if (!(this.options.pauseOnFocus && this._focused) && this.eventPause){
mauromascarenhas marked this conversation as resolved.
Show resolved Hide resolved
this.start();
}
}

/**
* Focus out leave event handler
*/
_handleAutoStartFocus() {
this._focused = false;
if (!(this.options.pauseOnHover && this._hovered) && this.eventPause){
mauromascarenhas marked this conversation as resolved.
Show resolved Hide resolved
this.start();
}
}

/**
* Handle Interval
*/
Expand Down Expand Up @@ -223,8 +312,11 @@
_setupIndicators() {
if (this.options.indicators) {
this.$indicators = $('<ul class="indicators"></ul>');
this.$slides.each((el, index) => {
let $indicator = $('<li class="indicator-item"></li>');
this.$slides.each((el, i) => {
let label = this.options.indicatorLabelFunc ? this.options.indicatorLabelFunc.call(this, i + 1, i === 0) : `${i + 1}`;
mauromascarenhas marked this conversation as resolved.
Show resolved Hide resolved
let $indicator = $(`<li class="indicator-item">
<button type="button" class="indicator-item-btn" aria-label="${label}" aria-controls="${this._sliderId}"></button>
</li>`);
this.$indicators.append($indicator[0]);
});
this.$el.append(this.$indicators[0]);
Expand Down Expand Up @@ -253,6 +345,10 @@
this.$active = this.$slides.eq(this.activeIndex);
let $caption = this.$active.find('.caption');
this.$active.removeClass('active');
// Enables every slide
this.$slides.each((el) => {
el.style.visibility = 'visible';
});
mauromascarenhas marked this conversation as resolved.
Show resolved Hide resolved

anim({
targets: this.$active[0],
Expand All @@ -269,6 +365,8 @@
duration: 0,
easing: 'easeOutQuad'
});
// Disables invisible slides (for assistive technologies)
el.style.visibility = 'hidden';
});
}
});
Expand All @@ -277,8 +375,14 @@

// Update indicators
if (this.options.indicators) {
this.$indicators.eq(this.activeIndex).removeClass('active');
this.$indicators.eq(index).addClass('active');
let oActive = this.$indicators.eq(this.activeIndex).children().first()[0];
let nActive = this.$indicators.eq(index).children().first()[0];
oActive.classList.remove('active');
nActive.classList.add('active');
if (typeof this.options.indicatorLabelFunc === "function"){
oActive.setAttribute("aria-label", this.options.indicatorLabelFunc.call(this, this.$indicators.eq(this.activeIndex).index(), false));
nActive.setAttribute("aria-label", this.options.indicatorLabelFunc.call(this, this.$indicators.eq(index).index(), true));
}
mauromascarenhas marked this conversation as resolved.
Show resolved Hide resolved
}

anim({
Expand All @@ -299,18 +403,35 @@
});

this.$slides.eq(index).addClass('active');
if (this._focusCurrent){
mauromascarenhas marked this conversation as resolved.
Show resolved Hide resolved
this.$slides.eq(index)[0].focus();
this._focusCurrent = false;
}
this.activeIndex = index;

// Reset interval
this.start();
// Reset interval, if allowed. This check prevents autostart
// when slider is paused, since it can be changed though indicators.
if (this.interval != null){
mauromascarenhas marked this conversation as resolved.
Show resolved Hide resolved
this.start();
}
}
}

/**
* "Protected" function which pauses current interval
* @param {boolean} fromEvent Specifies if request came from event
*/
_pause(fromEvent) {
clearInterval(this.interval);
this.eventPause = fromEvent;
this.interval = null;
}

/**
* Pause slider interval
*/
pause() {
clearInterval(this.interval);
this._pause(false);
}

/**
Expand All @@ -322,6 +443,7 @@
this._handleIntervalBound,
this.options.duration + this.options.interval
);
this.eventPause = false;
}

/**
Expand Down
Loading