Improve accessibility of forum vote buttons

This change involves substantial refactoring of the relevant code to
reduce duplication. It also makes the templates for the vote buttons
more consistent and cleaner.

JIRA: FOR-64
This commit is contained in:
Greg Price
2014-01-06 16:10:48 -05:00
parent d4f7cd68d8
commit b7d7751dc2
14 changed files with 331 additions and 150 deletions

View File

@@ -1,13 +1,12 @@
describe "DiscussionContentView", ->
beforeEach ->
setFixtures
(
setFixtures(
"""
<div class="discussion-post">
<header>
<a data-tooltip="vote" data-role="discussion-vote" class="vote-btn discussion-vote discussion-vote-up" href="#">
<span class="plus-icon">+</span> <span class="votes-count-number">0</span></a>
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false">
<span class="plus-icon"/><span class='votes-count-number'>0</span> <span class="sr">votes (click to vote)</span></a>
<h1>Post Title</h1>
<p class="posted-details">
<a class="username" href="/courses/MITx/999/Robot_Super_Course/discussion/forum/users/1">robot</a>
@@ -23,16 +22,21 @@ describe "DiscussionContentView", ->
"""
)
@thread = new Thread {
id: '01234567',
user_id: '567',
course_id: 'mitX/999/test',
body: 'this is a thread',
created_at: '2013-04-03T20:08:39Z',
abuse_flaggers: ['123']
roles: []
@threadData = {
id: '01234567',
user_id: '567',
course_id: 'mitX/999/test',
body: 'this is a thread',
created_at: '2013-04-03T20:08:39Z',
abuse_flaggers: ['123'],
votes: {up_count: '42'},
type: "thread",
roles: []
}
@thread = new Thread(@threadData)
@view = new DiscussionContentView({ model: @thread })
@view.setElement($('.discussion-post'))
window.user = new DiscussionUser({id: '567', upvoted_ids: []})
it 'defines the tag', ->
expect($('#jasmine-fixtures')).toExist
@@ -56,3 +60,15 @@ describe "DiscussionContentView", ->
@thread.set("abuse_flaggers",temp_array)
@thread.unflagAbuse()
expect(@thread.get 'abuse_flaggers').toEqual []
it 'renders the vote button properly', ->
DiscussionViewSpecHelper.checkRenderVote(@view, @thread)
it 'votes correctly', ->
DiscussionViewSpecHelper.checkVote(@view, @thread, @threadData, false)
it 'unvotes correctly', ->
DiscussionViewSpecHelper.checkUnvote(@view, @thread, @threadData, false)
it 'toggles the vote correctly', ->
DiscussionViewSpecHelper.checkToggleVote(@view, @thread)

View File

@@ -0,0 +1,40 @@
describe "DiscussionThreadProfileView", ->
beforeEach ->
setFixtures(
"""
<div class="discussion-post">
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false">
<span class="plus-icon"/><span class="votes-count-number">0</span> <span class="sr">votes (click to vote)</span>
</a>
</div>
"""
)
@threadData = {
id: "dummy",
user_id: "567",
course_id: "TestOrg/TestCourse/TestRun",
body: "this is a thread",
created_at: "2013-04-03T20:08:39Z",
abuse_flaggers: [],
votes: {up_count: "42"}
}
@thread = new Thread(@threadData)
@view = new DiscussionThreadProfileView({ model: @thread })
@view.setElement($(".discussion-post"))
window.user = new DiscussionUser({id: "567", upvoted_ids: []})
it "renders the vote correctly", ->
DiscussionViewSpecHelper.checkRenderVote(@view, @thread)
it "votes correctly", ->
DiscussionViewSpecHelper.checkVote(@view, @thread, @threadData, true)
it "unvotes correctly", ->
DiscussionViewSpecHelper.checkUnvote(@view, @thread, @threadData, true)
it "toggles the vote correctly", ->
DiscussionViewSpecHelper.checkToggleVote(@view, @thread)
it "vote button activates on appropriate events", ->
DiscussionViewSpecHelper.checkVoteButtonEvents(@view)

View File

@@ -0,0 +1,40 @@
describe "DiscussionThreadShowView", ->
beforeEach ->
setFixtures(
"""
<div class="discussion-post">
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false">
<span class="plus-icon"/><span class="votes-count-number">0</span> <span class="sr">votes (click to vote)</span>
</a>
</div>
"""
)
@threadData = {
id: "dummy",
user_id: "567",
course_id: "TestOrg/TestCourse/TestRun",
body: "this is a thread",
created_at: "2013-04-03T20:08:39Z",
abuse_flaggers: [],
votes: {up_count: "42"}
}
@thread = new Thread(@threadData)
@view = new DiscussionThreadShowView({ model: @thread })
@view.setElement($(".discussion-post"))
window.user = new DiscussionUser({id: "567", upvoted_ids: []})
it "renders the vote correctly", ->
DiscussionViewSpecHelper.checkRenderVote(@view, @thread)
it "votes correctly", ->
DiscussionViewSpecHelper.checkVote(@view, @thread, @threadData, true)
it "unvotes correctly", ->
DiscussionViewSpecHelper.checkUnvote(@view, @thread, @threadData, true)
it 'toggles the vote correctly', ->
DiscussionViewSpecHelper.checkToggleVote(@view, @thread)
it "vote button activates on appropriate events", ->
DiscussionViewSpecHelper.checkVoteButtonEvents(@view)

View File

@@ -0,0 +1,113 @@
class @DiscussionViewSpecHelper
@expectVoteRendered = (view, voted) ->
button = view.$el.find(".vote-btn")
if voted
expect(button.hasClass("is-cast")).toBe(true)
expect(button.attr("aria-pressed")).toEqual("true")
expect(button.attr("data-tooltip")).toEqual("remove vote")
expect(button.find(".votes-count-number").html()).toEqual("43")
expect(button.find(".sr").html()).toEqual("votes (click to remove your vote)")
else
expect(button.hasClass("is-cast")).toBe(false)
expect(button.attr("aria-pressed")).toEqual("false")
expect(button.attr("data-tooltip")).toEqual("vote")
expect(button.find(".votes-count-number").html()).toEqual("42")
expect(button.find(".sr").html()).toEqual("votes (click to vote)")
@checkRenderVote = (view, model) ->
view.renderVote()
DiscussionViewSpecHelper.expectVoteRendered(view, false)
window.user.vote(model)
view.renderVote()
DiscussionViewSpecHelper.expectVoteRendered(view, true)
window.user.unvote(model)
view.renderVote()
DiscussionViewSpecHelper.expectVoteRendered(view, false)
@checkVote = (view, model, modelData, checkRendering) ->
view.renderVote()
if checkRendering
DiscussionViewSpecHelper.expectVoteRendered(view, false)
spyOn($, "ajax").andCallFake((params) =>
newModelData = {}
$.extend(newModelData, modelData, {votes: {up_count: "43"}})
params.success(newModelData, "success")
# Caller invokes always function on return value but it doesn't matter here
{always: ->}
)
view.vote()
expect(window.user.voted(model)).toBe(true)
if checkRendering
DiscussionViewSpecHelper.expectVoteRendered(view, true)
expect($.ajax).toHaveBeenCalled()
$.ajax.reset()
# Check idempotence
view.vote()
expect(window.user.voted(model)).toBe(true)
if checkRendering
DiscussionViewSpecHelper.expectVoteRendered(view, true)
expect($.ajax).toHaveBeenCalled()
@checkUnvote = (view, model, modelData, checkRendering) ->
window.user.vote(model)
expect(window.user.voted(model)).toBe(true)
if checkRendering
DiscussionViewSpecHelper.expectVoteRendered(view, true)
spyOn($, "ajax").andCallFake((params) =>
newModelData = {}
$.extend(newModelData, modelData, {votes: {up_count: "42"}})
params.success(newModelData, "success")
# Caller invokes always function on return value but it doesn't matter here
{always: ->}
)
view.unvote()
expect(window.user.voted(model)).toBe(false)
if checkRendering
DiscussionViewSpecHelper.expectVoteRendered(view, false)
expect($.ajax).toHaveBeenCalled()
$.ajax.reset()
# Check idempotence
view.unvote()
expect(window.user.voted(model)).toBe(false)
if checkRendering
DiscussionViewSpecHelper.expectVoteRendered(view, false)
expect($.ajax).toHaveBeenCalled()
@checkToggleVote = (view, model) ->
event = {preventDefault: ->}
spyOn(event, "preventDefault")
spyOn(view, "vote").andCallFake(() -> window.user.vote(model))
spyOn(view, "unvote").andCallFake(() -> window.user.unvote(model))
expect(window.user.voted(model)).toBe(false)
view.toggleVote(event)
expect(view.vote).toHaveBeenCalled()
expect(view.unvote).not.toHaveBeenCalled()
expect(event.preventDefault.callCount).toEqual(1)
view.vote.reset()
view.unvote.reset()
expect(window.user.voted(model)).toBe(true)
view.toggleVote(event)
expect(view.vote).not.toHaveBeenCalled()
expect(view.unvote).toHaveBeenCalled()
expect(event.preventDefault.callCount).toEqual(2)
@checkVoteButtonEvents = (view) ->
spyOn(view, "toggleVote")
button = view.$el.find(".vote-btn")
button.click()
expect(view.toggleVote).toHaveBeenCalled()
view.toggleVote.reset()
button.trigger($.Event("keydown", {which: 13}))
expect(view.toggleVote).toHaveBeenCalled()
view.toggleVote.reset()
button.trigger($.Event("keydown", {which: 32}))
expect(view.toggleVote).not.toHaveBeenCalled()

View File

@@ -0,0 +1,40 @@
describe "ThreadResponseShowView", ->
beforeEach ->
setFixtures(
"""
<div class="discussion-post">
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false">
<span class="plus-icon"/><span class="votes-count-number">0</span> <span class="sr">votes (click to vote)</span>
</a>
</div>
"""
)
@commentData = {
id: "dummy",
user_id: "567",
course_id: "TestOrg/TestCourse/TestRun",
body: "this is a comment",
created_at: "2013-04-03T20:08:39Z",
abuse_flaggers: [],
votes: {up_count: "42"}
}
@comment = new Comment(@commentData)
@view = new ThreadResponseShowView({ model: @comment })
@view.setElement($(".discussion-post"))
window.user = new DiscussionUser({id: "567", upvoted_ids: []})
it "renders the vote correctly", ->
DiscussionViewSpecHelper.checkRenderVote(@view, @comment)
it "votes correctly", ->
DiscussionViewSpecHelper.checkVote(@view, @comment, @commentData, true)
it "unvotes correctly", ->
DiscussionViewSpecHelper.checkUnvote(@view, @comment, @commentData, true)
it 'toggles the vote correctly', ->
DiscussionViewSpecHelper.checkToggleVote(@view, @comment)
it "vote button activates on appropriate events", ->
DiscussionViewSpecHelper.checkVoteButtonEvents(@view)

View File

@@ -99,6 +99,13 @@ if Backbone?
@get("abuse_flaggers").pop(window.user.get('id'))
@trigger "change", @
vote: ->
@get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) + 1
@trigger "change", @
unvote: ->
@get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) - 1
@trigger "change", @
class @Thread extends @Content
urlMappers:
@@ -130,14 +137,6 @@ if Backbone?
unfollow: ->
@set('subscribed', false)
vote: ->
@get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) + 1
@trigger "change", @
unvote: ->
@get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) - 1
@trigger "change", @
display_body: ->
if @has("highlighted_body")
String(@get("highlighted_body")).replace(/<highlight>/g, '<mark>').replace(/<\/highlight>/g, '</mark>')

View File

@@ -91,7 +91,7 @@ class @DiscussionUtil
@activateOnEnter: (event, func) ->
if event.which == 13
e.preventDefault()
event.preventDefault()
func(event)
@makeFocusTrap: (elem) ->

View File

@@ -159,3 +159,42 @@ if Backbone?
temp_array = []
@model.set('abuse_flaggers', temp_array)
renderVote: =>
button = @$el.find(".vote-btn")
voted = window.user.voted(@model)
voteNum = @model.get("votes")["up_count"]
button.toggleClass("is-cast", voted)
button.attr("aria-pressed", voted)
button.attr("data-tooltip", if voted then "remove vote" else "vote")
button.find(".votes-count-number").html(voteNum)
button.find(".sr").html(if voted then "votes (click to remove your vote)" else "votes (click to vote)")
toggleVote: (event) =>
event.preventDefault()
if window.user.voted(@model)
@unvote()
else
@vote()
vote: =>
window.user.vote(@model)
url = @model.urlFor("upvote")
DiscussionUtil.safeAjax
$elem: @$el.find(".vote-btn")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
unvote: =>
window.user.unvote(@model)
url = @model.urlFor("unvote")
DiscussionUtil.safeAjax
$elem: @$el.find(".vote-btn")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)

View File

@@ -2,7 +2,10 @@ if Backbone?
class @DiscussionThreadProfileView extends DiscussionContentView
expanded = false
events:
"click .discussion-vote": "toggleVote"
"click .vote-btn":
(event) -> @toggleVote(event)
"keydown .vote-btn":
(event) -> DiscussionUtil.activateOnEnter(event, @toggleVote)
"click .action-follow": "toggleFollowing"
"keypress .action-follow":
(event) -> DiscussionUtil.activateOnEnter(event, toggleFollowing)
@@ -27,7 +30,7 @@ if Backbone?
@$el.html(Mustache.render(@template, params))
@initLocal()
@delegateEvents()
@renderVoted()
@renderVote()
@renderAttrs()
@$("span.timeago").timeago()
@convertMath()
@@ -35,15 +38,8 @@ if Backbone?
@renderResponses()
@
renderVoted: =>
if window.user.voted(@model)
@$("[data-role=discussion-vote]").addClass("is-cast")
else
@$("[data-role=discussion-vote]").removeClass("is-cast")
updateModelDetails: =>
@renderVoted()
@$("[data-role=discussion-vote] .votes-count-number").html(@model.get("votes")["up_count"])
@renderVote()
convertMath: ->
element = @$(".post-body")
@@ -71,35 +67,6 @@ if Backbone?
addComment: =>
@model.comment()
toggleVote: (event) ->
event.preventDefault()
if window.user.voted(@model)
@unvote()
else
@vote()
vote: ->
window.user.vote(@model)
url = @model.urlFor("upvote")
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
unvote: ->
window.user.unvote(@model)
url = @model.urlFor("unvote")
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
edit: ->
abbreviateBody: ->

View File

@@ -2,7 +2,10 @@ if Backbone?
class @DiscussionThreadShowView extends DiscussionContentView
events:
"click .discussion-vote": "toggleVote"
"click .vote-btn":
(event) -> @toggleVote(event)
"keydown .vote-btn":
(event) -> DiscussionUtil.activateOnEnter(event, @toggleVote)
"click .discussion-flag-abuse": "toggleFlagAbuse"
"keypress .discussion-flag-abuse":
(event) -> DiscussionUtil.activateOnEnter(event, toggleFlagAbuse)
@@ -28,7 +31,7 @@ if Backbone?
render: ->
@$el.html(@renderTemplate())
@delegateEvents()
@renderVoted()
@renderVote()
@renderFlagged()
@renderPinned()
@renderAttrs()
@@ -38,14 +41,6 @@ if Backbone?
@highlight @$("h1,h3")
@
renderVoted: =>
if window.user.voted(@model)
@$("[data-role=discussion-vote]").addClass("is-cast")
@$("[data-role=discussion-vote] span.sr").html("votes (click to remove your vote)")
else
@$("[data-role=discussion-vote]").removeClass("is-cast")
@$("[data-role=discussion-vote] span.sr").html("votes (click to vote)")
renderFlagged: =>
if window.user.id in @model.get("abuse_flaggers") or (DiscussionUtil.isFlagModerator and @model.get("abuse_flaggers").length > 0)
@$("[data-role=thread-flag]").addClass("flagged")
@@ -70,52 +65,15 @@ if Backbone?
updateModelDetails: =>
@renderVoted()
@renderVote()
@renderFlagged()
@renderPinned()
@$("[data-role=discussion-vote] .votes-count-number").html(@model.get("votes")["up_count"] + '<span class ="sr"></span>')
if window.user.voted(@model)
@$("[data-role=discussion-vote] .votes-count-number span.sr").html("votes (click to remove your vote)")
else
@$("[data-role=discussion-vote] .votes-count-number span.sr").html("votes (click to vote)")
convertMath: ->
element = @$(".post-body")
element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text()
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
toggleVote: (event) ->
event.preventDefault()
if window.user.voted(@model)
@unvote()
else
@vote()
vote: ->
window.user.vote(@model)
url = @model.urlFor("upvote")
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response, {silent: true})
unvote: ->
window.user.unvote(@model)
url = @model.urlFor("unvote")
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response, {silent: true})
edit: (event) ->
@trigger "thread:edit", event

View File

@@ -1,7 +1,10 @@
if Backbone?
class @ThreadResponseShowView extends DiscussionContentView
events:
"click .vote-btn": "toggleVote"
"click .vote-btn":
(event) -> @toggleVote(event)
"keydown .vote-btn":
(event) -> DiscussionUtil.activateOnEnter(event, @toggleVote)
"click .action-endorse": "toggleEndorse"
"click .action-delete": "_delete"
"click .action-edit": "edit"
@@ -23,9 +26,7 @@ if Backbone?
render: ->
@$el.html(@renderTemplate())
@delegateEvents()
if window.user.voted(@model)
@$(".vote-btn").addClass("is-cast")
@$(".vote-btn span.sr").html("votes (click to remove your vote)")
@renderVote()
@renderAttrs()
@renderFlagged()
@$el.find(".posted-details").timeago()
@@ -46,39 +47,6 @@ if Backbone?
@$el.addClass("community-ta")
@$el.prepend('<div class="community-ta-banner">Community TA</div>')
toggleVote: (event) ->
event.preventDefault()
@$(".vote-btn").toggleClass("is-cast")
if @$(".vote-btn").hasClass("is-cast")
@vote()
@$(".vote-btn span.sr").html("votes (click to remove your vote)")
else
@unvote()
@$(".vote-btn span.sr").html("votes (click to vote)")
vote: ->
url = @model.urlFor("upvote")
@$(".votes-count-number").html((parseInt(@$(".votes-count-number").html()) + 1) + '<span class="sr"></span>')
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
unvote: ->
url = @model.urlFor("unvote")
@$(".votes-count-number").html((parseInt(@$(".votes-count-number").html()) - 1)+'<span class="sr"></span>')
DiscussionUtil.safeAjax
$elem: @$(".discussion-vote")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
edit: (event) ->
@trigger "response:edit", event
@@ -115,4 +83,5 @@ if Backbone?
@$(".discussion-flag-abuse .flag-label").html("Report Misuse")
updateModelDetails: =>
@renderVote()
@renderFlagged()

View File

@@ -31,8 +31,8 @@
<div class="group-visibility-label">${"<%- obj.group_string%>"}</div>
${"<% } %>"}
<a href="#" class="vote-btn discussion-vote discussion-vote-up" data-role="discussion-vote" data-tooltip="vote">
<span class="plus-icon">+</span> <span class='votes-count-number'>${'<%- votes["up_count"] %>'}<span class="sr">votes (click to vote)</span></span></a>
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false">
<span class="plus-icon"/><span class='votes-count-number'>${'<%- votes["up_count"] %>'}</span> <span class="sr">votes (click to vote)</span></a>
<h1>${'<%- title %>'}</h1>
<p class="posted-details">
${"<% if (obj.username) { %>"}
@@ -123,7 +123,7 @@
<script type="text/template" id="thread-response-show-template">
<header class="response-local">
<a href="javascript:void(0)" class="vote-btn" data-tooltip="vote"><span class="plus-icon"></span><span class="votes-count-number">${"<%- votes['up_count'] %>"}<span class="sr">votes (click to vote)</span></span></a>
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false"><span class="plus-icon"/><span class="votes-count-number">${"<%- votes['up_count'] %>"}</span> <span class="sr">votes (click to vote)</span></a>
<a href="javascript:void(0)" class="endorse-btn${'<% if (endorsed) { %> is-endorsed<% } %>'} action-endorse" style="cursor: default; display: none;" data-tooltip="endorse"><span class="check-icon" style="pointer-events: none; "></span></a>
${"<% if (obj.username) { %>"}
<a href="${'<%- user_url %>'}" class="posted-by">${'<%- username %>'}</a>

View File

@@ -1,7 +1,7 @@
<div class="discussion-post local">
<div><a href="javascript:void(0)" class="dogear action-follow" data-tooltip="follow"></a></div>
<header>
<a href="#" class="vote-btn discussion-vote discussion-vote-up" data-role="discussion-vote" data-tooltip="vote"><span class="plus-icon">+</span> <span class='votes-count-number'>{{votes.up_count}}</span><span class="sr">votes (click to vote)</span></a>
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false"><span class="plus-icon"/><span class='votes-count-number'>{{votes.up_count}}</span> <span class="sr">votes (click to vote)</span></a>
<h3>{{title}}</h3>
<div class="discussion-flag-abuse notflagged" data-role="thread-flag" data-tooltip="Report Misuse">
<i class="icon icon-flag"></i><span class="flag-label">Flagged</span></div>

View File

@@ -2,7 +2,7 @@
<div class="local"><a href="javascript:void(0)" class="dogear action-follow"></a></div>
<div class="discussion-post local">
<header>
<a href="#" class="vote-btn discussion-vote discussion-vote-up" data-role="discussion-vote"><span class="plus-icon">+</span> <span class='votes-count-number'>{{votes.up_count}}</span><span class="sr">votes (click to vote)</span></a>
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false"><span class="plus-icon"/><span class='votes-count-number'>{{votes.up_count}}</span> <span class="sr">votes (click to vote)</span></a>
<h3>{{title}}</h3>
<p class="posted-details">
{{#user}}