Commit 2df00412 by jmclaus

Merge pull request #933 from edx/jmclaus/bugfix_tabbing_captions

Tabbing through captions
parents c0b710b8 7659c5b1
...@@ -525,12 +525,19 @@ div.video { ...@@ -525,12 +525,19 @@ div.video {
margin-bottom: 8px; margin-bottom: 8px;
padding: 0; padding: 0;
line-height: lh(); line-height: lh();
outline-width: 0px;
outline-style: none;
&.current { &.current {
color: #333; color: #333;
font-weight: 700; font-weight: 700;
} }
&.focused {
outline-width: 1px;
outline-style: dotted;
}
&:hover { &:hover {
color: $blue; color: $blue;
} }
......
...@@ -93,6 +93,7 @@ ...@@ -93,6 +93,7 @@
$('.subtitles li[data-index]').each(function(index, link) { $('.subtitles li[data-index]').each(function(index, link) {
expect($(link)).toHaveData('index', index); expect($(link)).toHaveData('index', index);
expect($(link)).toHaveData('start', captionsData.start[index]); expect($(link)).toHaveData('start', captionsData.start[index]);
expect($(link)).toHaveAttr('tabindex', 0);
expect($(link)).toHaveText(captionsData.text[index]); expect($(link)).toHaveText(captionsData.text[index]);
}); });
}); });
...@@ -104,7 +105,13 @@ ...@@ -104,7 +105,13 @@
it('bind all the caption link', function() { it('bind all the caption link', function() {
$('.subtitles li[data-index]').each(function(index, link) { $('.subtitles li[data-index]').each(function(index, link) {
expect($(link)).toHandleWith('click', videoCaption.seekPlayer); expect($(link)).toHandleWith('mouseover', videoCaption.captionMouseOverOut);
expect($(link)).toHandleWith('mouseout', videoCaption.captionMouseOverOut);
expect($(link)).toHandleWith('mousedown', videoCaption.captionMouseDown);
expect($(link)).toHandleWith('click', videoCaption.captionClick);
expect($(link)).toHandleWith('focus', videoCaption.captionFocus);
expect($(link)).toHandleWith('blur', videoCaption.captionBlur);
expect($(link)).toHandleWith('keydown', videoCaption.captionKeyDown);
}); });
}); });
...@@ -278,6 +285,7 @@ ...@@ -278,6 +285,7 @@
$('.subtitles li[data-index]').each(function(index, link) { $('.subtitles li[data-index]').each(function(index, link) {
expect($(link)).toHaveData('index', index); expect($(link)).toHaveData('index', index);
expect($(link)).toHaveData('start', captionsData.start[index]); expect($(link)).toHaveData('start', captionsData.start[index]);
expect($(link)).toHaveAttr('tabindex', 0);
expect($(link)).toHaveText(captionsData.text[index]); expect($(link)).toHaveText(captionsData.text[index]);
}); });
}); });
...@@ -289,7 +297,13 @@ ...@@ -289,7 +297,13 @@
it('bind all the caption link', function() { it('bind all the caption link', function() {
$('.subtitles li[data-index]').each(function(index, link) { $('.subtitles li[data-index]').each(function(index, link) {
expect($(link)).toHandleWith('click', videoCaption.seekPlayer); expect($(link)).toHandleWith('mouseover', videoCaption.captionMouseOverOut);
expect($(link)).toHandleWith('mouseout', videoCaption.captionMouseOverOut);
expect($(link)).toHandleWith('mousedown', videoCaption.captionMouseDown);
expect($(link)).toHandleWith('click', videoCaption.captionClick);
expect($(link)).toHandleWith('focus', videoCaption.captionFocus);
expect($(link)).toHandleWith('blur', videoCaption.captionBlur);
expect($(link)).toHandleWith('keydown', videoCaption.captionKeyDown);
}); });
}); });
...@@ -558,6 +572,99 @@ ...@@ -558,6 +572,99 @@
}); });
}); });
}); });
describe('caption accessibility', function() {
beforeEach(function() {
initialize();
});
describe('when getting focus through TAB key', function() {
beforeEach(function() {
videoCaption.isMouseFocus = false;
$('.subtitles li[data-index=0]').trigger(jQuery.Event('focus'));
});
it('shows an outline around the caption', function() {
expect($('.subtitles li[data-index=0]')).toHaveClass('focused');
});
it('has automatic scrolling disabled', function() {
expect(videoCaption.autoScrolling).toBe(false);
});
});
describe('when loosing focus through TAB key', function() {
beforeEach(function() {
$('.subtitles li[data-index=0]').trigger(jQuery.Event('blur'));
});
it('does not show an outline around the caption', function() {
expect($('.subtitles li[data-index=0]')).not.toHaveClass('focused');
});
it('has automatic scrolling enabled', function() {
expect(videoCaption.autoScrolling).toBe(true);
});
});
describe('when same caption gets the focus through mouse after having focus through TAB key', function() {
beforeEach(function() {
videoCaption.isMouseFocus = false;
$('.subtitles li[data-index=0]').trigger(jQuery.Event('focus'));
$('.subtitles li[data-index=0]').trigger(jQuery.Event('mousedown'));
});
it('does not show an outline around it', function() {
expect($('.subtitles li[data-index=0]')).not.toHaveClass('focused');
});
it('has automatic scrolling enabled', function() {
expect(videoCaption.autoScrolling).toBe(true);
});
});
describe('when a second caption gets focus through mouse after first had focus through TAB key', function() {
beforeEach(function() {
videoCaption.isMouseFocus = false;
$('.subtitles li[data-index=0]').trigger(jQuery.Event('focus'));
$('.subtitles li[data-index=0]').trigger(jQuery.Event('blur'));
videoCaption.isMouseFocus = true;
$('.subtitles li[data-index=1]').trigger(jQuery.Event('mousedown'));
});
it('does not show an outline around the first', function() {
expect($('.subtitles li[data-index=0]')).not.toHaveClass('focused');
});
it('does not show an outline around the second', function() {
expect($('.subtitles li[data-index=1]')).not.toHaveClass('focused');
});
it('has automatic scrolling enabled', function() {
expect(videoCaption.autoScrolling).toBe(true);
});
});
describe('when enter key is pressed on a caption', function() {
beforeEach(function() {
var e;
spyOn(videoCaption, 'seekPlayer').andCallThrough();
videoCaption.isMouseFocus = false;
$('.subtitles li[data-index=0]').trigger(jQuery.Event('focus'));
e = jQuery.Event('keydown');
e.which = 13; // ENTER key
$('.subtitles li[data-index=0]').trigger(e);
});
it('shows an outline around it', function() {
expect($('.subtitles li[data-index=0]')).toHaveClass('focused');
});
it('calls seekPlayer', function() {
expect(videoCaption.seekPlayer).toHaveBeenCalled();
});
});
});
}); });
}).call(this); }).call(this);
...@@ -37,31 +37,37 @@ function () { ...@@ -37,31 +37,37 @@ function () {
// Functions which will be accessible via 'state' object. When called, these functions will // Functions which will be accessible via 'state' object. When called, these functions will
// get the 'state' object as a context. // get the 'state' object as a context.
function _makeFunctionsPublic(state) { function _makeFunctionsPublic(state) {
state.videoCaption.autoShowCaptions = _.bind(autoShowCaptions, state); state.videoCaption.autoShowCaptions = _.bind(autoShowCaptions, state);
state.videoCaption.autoHideCaptions = _.bind(autoHideCaptions, state); state.videoCaption.autoHideCaptions = _.bind(autoHideCaptions, state);
state.videoCaption.resize = _.bind(resize, state); state.videoCaption.resize = _.bind(resize, state);
state.videoCaption.toggle = _.bind(toggle, state); state.videoCaption.toggle = _.bind(toggle, state);
state.videoCaption.onMouseEnter = _.bind(onMouseEnter, state); state.videoCaption.onMouseEnter = _.bind(onMouseEnter, state);
state.videoCaption.onMouseLeave = _.bind(onMouseLeave, state); state.videoCaption.onMouseLeave = _.bind(onMouseLeave, state);
state.videoCaption.onMovement = _.bind(onMovement, state); state.videoCaption.onMovement = _.bind(onMovement, state);
state.videoCaption.renderCaption = _.bind(renderCaption, state); state.videoCaption.renderCaption = _.bind(renderCaption, state);
state.videoCaption.captionHeight = _.bind(captionHeight, state); state.videoCaption.captionHeight = _.bind(captionHeight, state);
state.videoCaption.topSpacingHeight = _.bind(topSpacingHeight, state); state.videoCaption.topSpacingHeight = _.bind(topSpacingHeight, state);
state.videoCaption.bottomSpacingHeight = _.bind(bottomSpacingHeight, state); state.videoCaption.bottomSpacingHeight = _.bind(bottomSpacingHeight, state);
state.videoCaption.scrollCaption = _.bind(scrollCaption, state); state.videoCaption.scrollCaption = _.bind(scrollCaption, state);
state.videoCaption.search = _.bind(search, state); state.videoCaption.search = _.bind(search, state);
state.videoCaption.play = _.bind(play, state); state.videoCaption.play = _.bind(play, state);
state.videoCaption.pause = _.bind(pause, state); state.videoCaption.pause = _.bind(pause, state);
state.videoCaption.seekPlayer = _.bind(seekPlayer, state); state.videoCaption.seekPlayer = _.bind(seekPlayer, state);
state.videoCaption.hideCaptions = _.bind(hideCaptions, state); state.videoCaption.hideCaptions = _.bind(hideCaptions, state);
state.videoCaption.calculateOffset = _.bind(calculateOffset, state); state.videoCaption.calculateOffset = _.bind(calculateOffset, state);
state.videoCaption.updatePlayTime = _.bind(updatePlayTime, state); state.videoCaption.updatePlayTime = _.bind(updatePlayTime, state);
state.videoCaption.setSubtitlesHeight = _.bind(setSubtitlesHeight, state); state.videoCaption.setSubtitlesHeight = _.bind(setSubtitlesHeight, state);
state.videoCaption.renderElements = _.bind(renderElements, state); state.videoCaption.renderElements = _.bind(renderElements, state);
state.videoCaption.bindHandlers = _.bind(bindHandlers, state); state.videoCaption.bindHandlers = _.bind(bindHandlers, state);
state.videoCaption.fetchCaption = _.bind(fetchCaption, state); state.videoCaption.fetchCaption = _.bind(fetchCaption, state);
state.videoCaption.captionURL = _.bind(captionURL, state); state.videoCaption.captionURL = _.bind(captionURL, state);
state.videoCaption.captionMouseOverOut = _.bind(captionMouseOverOut, state);
state.videoCaption.captionMouseDown = _.bind(captionMouseDown, state);
state.videoCaption.captionClick = _.bind(captionClick, state);
state.videoCaption.captionFocus = _.bind(captionFocus, state);
state.videoCaption.captionBlur = _.bind(captionBlur, state);
state.videoCaption.captionKeyDown = _.bind(captionKeyDown, state);
} }
// *************************************************************** // ***************************************************************
...@@ -309,7 +315,8 @@ function () { ...@@ -309,7 +315,8 @@ function () {
liEl.attr({ liEl.attr({
'data-index': index, 'data-index': index,
'data-start': _this.videoCaption.start[index] 'data-start': _this.videoCaption.start[index],
'tabindex': 0
}); });
container.append(liEl); container.append(liEl);
...@@ -317,7 +324,33 @@ function () { ...@@ -317,7 +324,33 @@ function () {
this.videoCaption.subtitlesEl.html(container.html()); this.videoCaption.subtitlesEl.html(container.html());
this.videoCaption.subtitlesEl.find('li[data-index]').on('click', this.videoCaption.seekPlayer); this.videoCaption.subtitlesEl.find('li[data-index]').on({
mouseover: this.videoCaption.captionMouseOverOut,
mouseout: this.videoCaption.captionMouseOverOut,
mousedown: this.videoCaption.captionMouseDown,
click: this.videoCaption.captionClick,
focus: this.videoCaption.captionFocus,
blur: this.videoCaption.captionBlur,
keydown: this.videoCaption.captionKeyDown
});
// Enables or disables automatic scrolling of the captions when the
// video is playing. This feature has to be disabled when tabbing
// through them as it interferes with that action. Initially, have this
// flag enabled as we assume mouse use. Then, if the first caption
// (through forward tabbing) or the last caption (through backwards
// tabbing) gets the focus, disable that feature. Renable it if tabbing
// then cycles out of the the captions.
this.videoCaption.autoScrolling = true;
// Keeps track of where the focus is situated in the array of captions.
// Used to implement the automatic scrolling behavior and decide if the
// outline around a caption has to be hidden or shown on a mouseenter or
// mouseleave.
this.videoCaption.currentCaptionIndex = 0;
// Used to track if the focus is coming from a click or tabbing. This
// has to be known to decide if, when a caption gets the focus, an
// outline has to be drawn (tabbing) or not (mouse click).
this.videoCaption.isMouseFocus = false;
this.videoCaption.subtitlesEl.prepend($('<li class="spacing">').height(this.videoCaption.topSpacingHeight())); this.videoCaption.subtitlesEl.prepend($('<li class="spacing">').height(this.videoCaption.topSpacingHeight()));
this.videoCaption.subtitlesEl.append($('<li class="spacing">').height(this.videoCaption.bottomSpacingHeight())); this.videoCaption.subtitlesEl.append($('<li class="spacing">').height(this.videoCaption.bottomSpacingHeight()));
...@@ -325,10 +358,85 @@ function () { ...@@ -325,10 +358,85 @@ function () {
this.videoCaption.rendered = true; this.videoCaption.rendered = true;
} }
// On mouseOver, hide the outline of a caption that has been tabbed to.
// On mouseOut, show the outline of a caption that has been tabbed to.
function captionMouseOverOut(event) {
var caption = $(event.target),
captionIndex = parseInt(caption.attr('data-index'), 10);
if (captionIndex === this.videoCaption.currentCaptionIndex) {
if (event.type === 'mouseover') {
caption.removeClass('focused');
}
else { // mouseout
caption.addClass('focused');
}
}
}
function captionMouseDown(event) {
var caption = $(event.target);
this.videoCaption.isMouseFocus = true;
this.videoCaption.autoScrolling = true;
caption.removeClass('focused');
this.videoCaption.currentCaptionIndex = -1;
}
function captionClick(event) {
this.videoCaption.seekPlayer(event);
}
function captionFocus(event) {
var caption = $(event.target),
captionIndex = parseInt(caption.attr('data-index'), 10);
// If the focus comes from a mouse click, hide the outline, turn on
// automatic scrolling and set currentCaptionIndex to point outside of
// caption list (ie -1) to disable mouseenter, mouseleave behavior.
if (this.videoCaption.isMouseFocus) {
this.videoCaption.autoScrolling = true;
caption.removeClass('focused');
this.videoCaption.currentCaptionIndex = -1;
}
// If the focus comes from tabbing, show the outline and turn off
// automatic scrolling.
else {
this.videoCaption.currentCaptionIndex = captionIndex;
caption.addClass('focused');
// The second and second to last elements turn automatic scrolling
// off again as it may have been enabled in captionBlur.
if (captionIndex <= 1 || captionIndex >= this.videoCaption.captions.length-2) {
this.videoCaption.autoScrolling = false;
}
}
}
function captionBlur(event) {
var caption = $(event.target),
captionIndex = parseInt(caption.attr('data-index'), 10);
caption.removeClass('focused');
// If we are on first or last index, we have to turn automatic scroll on
// again when losing focus. There is no way to know in what direction we
// are tabbing. So we could be on the first element and tabbing back out
// of the captions or on the last element and tabbing forward out of the
// captions.
if (captionIndex === 0 ||
captionIndex === this.videoCaption.captions.length-1) {
this.videoCaption.autoScrolling = true;
}
}
function captionKeyDown(event) {
this.videoCaption.isMouseFocus = false;
if (event.which === 13) { //Enter key
this.videoCaption.seekPlayer(event);
}
}
function scrollCaption() { function scrollCaption() {
var el = this.videoCaption.subtitlesEl.find('.current:first'); var el = this.videoCaption.subtitlesEl.find('.current:first');
if (!this.videoCaption.frozen && el.length) { // Automatic scrolling gets disabled if one of the captions has received
// focus through tabbing.
if (!this.videoCaption.frozen && el.length && this.videoCaption.autoScrolling) {
this.videoCaption.subtitlesEl.scrollTo( this.videoCaption.subtitlesEl.scrollTo(
el, el,
{ {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment