[a11y] Adds context to module level buttons

Makes it so that screenreaders are given specific module context
i.e., the module name, whenever they navigation to a module
level button

fixes CNVS-22866

Test Plan:
  - Go to the modules page
  - Using a screenreader go to the publish cloud at the module
    level.
  - It should indicate the name of the module, for example:
    "Published. Click to unpublish My Cool Module."
  - Similar things should occur for the Add content button and
    the manage module dropdown.
  - The publish button and cog buttons for items within modules
    should similarly give context.

Change-Id: I1ba3f5e5c09e8186667af38eb19852295d5f01e2
Reviewed-on: https://gerrit.instructure.com/63049
Product-Review: Aaron Cannon <acannon@instructure.com>
Reviewed-by: Sterling Cobb <sterling@instructure.com>
Tested-by: Jenkins
QA-Review: Clare Strong <clare@instructure.com>
This commit is contained in:
Clay Diffrient 2015-09-10 17:04:00 -06:00
parent f53907c0dd
commit 60d3834e9d
10 changed files with 85 additions and 21 deletions

View File

@ -16,6 +16,7 @@ define [
published: true published: true
publishable: true publishable: true
unpublishable: true unpublishable: true
module_item_name: null
branch: (key) -> branch: (key) ->
(@[key][@get('module_type')] or @[key].generic).call(this) (@[key][@get('module_type')] or @[key].generic).call(this)
@ -45,10 +46,24 @@ define [
module: -> module: @attributes module: -> module: @attributes
disabledMessages: disabledMessages:
generic: -> I18n.t('disabled', 'Publishing is disabled for this item') generic: -> if @get('module_item_name')
assignment: -> I18n.t('disabled_assignment', "Can't unpublish if there are student submissions") I18n.t('Publishing %{item_name} is disabled', {item_name: @get('module_item_name')})
quiz: -> I18n.t('disabled_quiz', "Can't unpublish if there are student submissions") else
discussion_topic: -> I18n.t('disabled_discussion_topic', "Can't unpublish if there are student submissions") I18n.t('Publishing is disabled for this item')
assignment: -> if @get('module_item_name')
I18n.t("Can't unpublish %{item_name} if there are student submissions", {item_name: @get('module_item_name')})
else
I18n.t("Can't unpublish if there are student submissions")
quiz: -> if @get('module_item_name')
I18n.t("Can't unpublish %{item_name} if there are student submissions", {item_name: @get('module_item_name')})
else
I18n.t("Can't unpublish if there are student submissions")
discussion_topic: -> if @get('module_item_name')
I18n.t("Can't unpublish %{item_name} if there are student submissions", {item_name: @get('module_item_name')})
else
I18n.t("Can't unpublish if there are student submissions")
publish: -> publish: ->
@save 'published', yes @save 'published', yes

View File

@ -16,6 +16,7 @@ define [
togglePublishClassOn: React.PropTypes.object togglePublishClassOn: React.PropTypes.object
model: customPropTypes.filesystemObject model: customPropTypes.filesystemObject
userCanManageFilesForContext: React.PropTypes.bool.isRequired userCanManageFilesForContext: React.PropTypes.bool.isRequired
fileName: React.PropTypes.string
# == React Functions == # # == React Functions == #
getInitialState: -> @extractStateFromModel( @props.model ) getInitialState: -> @extractStateFromModel( @props.model )

View File

@ -13,6 +13,10 @@ define [
publishedClass: 'btn-published' publishedClass: 'btn-published'
unpublishClass: 'btn-unpublish' unpublishClass: 'btn-unpublish'
# These values allow the default text to be overridden if necessary
@optionProperty 'publishText'
@optionProperty 'unpublishText'
tagName: 'button' tagName: 'button'
className: 'btn' className: 'btn'
@ -55,7 +59,7 @@ define [
addAriaLabel: (label) -> addAriaLabel: (label) ->
$label = @$el.find('span.screenreader-only.accessible_label') $label = @$el.find('span.screenreader-only.accessible_label')
$('<span class="screenreader-only accessible_label"></span>').appendTo(@$el) unless $label.length $label = $('<span class="screenreader-only accessible_label"></span>').appendTo(@$el) unless $label.length
$label.text label $label.text label
@$el.attr 'aria-label', label @$el.attr 'aria-label', label
@ -134,14 +138,14 @@ define [
renderPublish: -> renderPublish: ->
@renderState @renderState
text: I18n.t 'buttons.publish', 'Publish' text: I18n.t 'buttons.publish', 'Publish'
label: I18n.t 'buttons.publish_desc', 'Unpublished. Click to publish.' label: @publishText || I18n.t 'Unpublished. Click to publish.'
buttonClass: @publishClass buttonClass: @publishClass
iconClass: 'icon-unpublish' iconClass: 'icon-unpublish'
renderPublished: -> renderPublished: ->
@renderState @renderState
text: I18n.t 'buttons.published', 'Published' text: I18n.t 'buttons.published', 'Published'
label: I18n.t 'buttons.published_desc', 'Published. Click to unpublish.' label: @unpublishText || I18n.t 'Published. Click to unpublish.'
buttonClass: @publishedClass buttonClass: @publishedClass
iconClass: 'icon-publish' iconClass: 'icon-publish'

View File

@ -11,7 +11,12 @@ define [
tagName: 'span' tagName: 'span'
className: 'publish-icon' className: 'publish-icon'
# These values allow the default text to be overridden if necessary
@optionProperty 'publishText'
@optionProperty 'unpublishText'
initialize: -> initialize: ->
super
@events = _.extend({}, PublishButtonView.prototype.events, @events) @events = _.extend({}, PublishButtonView.prototype.events, @events)
events: {'keyclick' : 'click'} events: {'keyclick' : 'click'}

View File

@ -30,6 +30,7 @@ define([
}; };
PublishCloud.render = function () { PublishCloud.render = function () {
var fileName = this.props.fileName || I18n.t('This file');
if (this.props.userCanManageFilesForContext) { if (this.props.userCanManageFilesForContext) {
if (this.state.published && this.state.restricted) { if (this.state.published && this.state.restricted) {
return ( return (
@ -40,7 +41,7 @@ define([
ref='publishCloud' ref='publishCloud'
className='btn-link published-status restricted' className='btn-link published-status restricted'
title={this.getRestrictedText()} title={this.getRestrictedText()}
aria-label={this.getRestrictedText() + ' - ' + I18n.t('Click to modify')} aria-label={`${fileName} is ${this.getRestrictedText()} - ${I18n.t('Click to modify')}`}
> >
<i className='icon-cloud-lock' /> <i className='icon-cloud-lock' />
</button> </button>
@ -54,7 +55,7 @@ define([
ref='publishCloud' ref='publishCloud'
className='btn-link published-status hiddenState' className='btn-link published-status hiddenState'
title={I18n.t('Hidden. Available with a link')} title={I18n.t('Hidden. Available with a link')}
aria-label={I18n.t('Hidden. Available with a link - Click to modify')} aria-label={`${fileName} is ${I18n.t('Hidden. Available with a link - Click to modify')}`}
> >
<i className='icon-cloud-lock' /> <i className='icon-cloud-lock' />
</button> </button>
@ -68,7 +69,7 @@ define([
ref='publishCloud' ref='publishCloud'
className='btn-link published-status published' className='btn-link published-status published'
title={I18n.t('Published')} title={I18n.t('Published')}
aria-label={I18n.t('Published - Click to modify')} aria-label={`${fileName} is ${I18n.t('Published - Click to modify')}`}
> >
<i className='icon-publish' /> <i className='icon-publish' />
</button> </button>
@ -82,7 +83,7 @@ define([
ref='publishCloud' ref='publishCloud'
className='btn-link published-status unpublished' className='btn-link published-status unpublished'
title={I18n.t('Unpublished')} title={I18n.t('Unpublished')}
aria-label={I18n.t('Unpublished - Click to modify')} aria-label={`${fileName} is ${I18n.t('Unpublished - Click to modify')}`}
> >
<i className='icon-unpublish' /> <i className='icon-unpublish' />
</button> </button>
@ -97,7 +98,7 @@ define([
ref='publishCloud' ref='publishCloud'
className='published-status restricted' className='published-status restricted'
title={this.getRestrictedText()} title={this.getRestrictedText()}
aria-label={this.getRestrictedText()} aria-label={`${fileName} is ${this.getRestrictedText()}`}
> >
<i className='icon-calendar-day' /> <i className='icon-calendar-day' />
</div> </div>

View File

@ -115,6 +115,8 @@
data-course-id="<%= context_module && context_module.context_id %>" data-course-id="<%= context_module && context_module.context_id %>"
data-published="<%= published_status == 'published' %>" data-published="<%= published_status == 'published' %>"
data-publishable="<%= true %>" data-publishable="<%= true %>"
data-publish-message="<%= t('Unpublished. Click to publish %{module_name}.', {module_name: context_module ? context_module.name : 'module'}) %>"
data-unpublish-message="<%= t('Published. Click to unpublish %{module_name}.', {module_name: context_module ? context_module.name : 'module'}) %>"
title="" title=""
data-tooltip data-tooltip
class="publish-icon module <%= published_status %>" class="publish-icon module <%= published_status %>"
@ -123,10 +125,10 @@
</span> </span>
<button <button
aria-label="<%= t('aria_labels.add_item', %{Add Content}) %>" aria-label="<%= t('Add Content to %{module_name}', {module_name: context_module ? context_module.name : 'module'}) %>"
rel="<%= context_url(@context, :context_url) %>/modules/<%= context_module ? context_module.id : "{{ id }}" %>/items" rel="<%= context_url(@context, :context_url) %>/modules/<%= context_module ? context_module.id : "{{ id }}" %>/items"
class="add_module_item_link btn"><i class="icon-plus"></i><span class="screenreader-only"><%= t('links.add_item', %{Add Content}) %></span></button> class="add_module_item_link btn"><i class="icon-plus"></i><span class="screenreader-only"><%= t('Add Content to %{module_name}', {module_name: context_module ? context_module.name : 'module'}) %></span></button>
<button class="btn al-trigger" aria-label="<%= t("manager_module", "Manage module") %>"> <button class="btn al-trigger" aria-label="<%= t('Manage %{module_name}', {module_name: context_module ? context_module.name : 'module'}) %>">
<i class="icon-settings"></i><i class="icon-mini-arrow-down"></i> <i class="icon-settings"></i><i class="icon-mini-arrow-down"></i>
</button> </button>
<ul class="al-options"> <ul class="al-options">

View File

@ -115,6 +115,7 @@
<% if editable || module_item.nil? %> <% if editable || module_item.nil? %>
<div class="ig-admin"> <div class="ig-admin">
<span <span
data-module-item-name="<%= tag && tag.title %>"
data-module-type="<%= module_item && module_item.content_type_class %>" data-module-type="<%= module_item && module_item.content_type_class %>"
data-content-id="<%= module_item && module_item.content_id %>" data-content-id="<%= module_item && module_item.content_id %>"
data-id="<%= module_item_publishable_id(module_item) %>" data-id="<%= module_item_publishable_id(module_item) %>"
@ -124,6 +125,8 @@
data-published="<%= module_item && published_status == 'published' %>" data-published="<%= module_item && published_status == 'published' %>"
data-publishable="<%= module_item_publishable?(module_item) %>" data-publishable="<%= module_item_publishable?(module_item) %>"
data-unpublishable="<%= module_item_unpublishable?(module_item) %>" data-unpublishable="<%= module_item_unpublishable?(module_item) %>"
data-publish-message="<%= t('Unpublished. Click to publish %{item_name}.', {item_name: tag && tag.title ? tag.title : ''}) %>"
data-unpublish-message="<%= t('Published. Click to unpublish %{item_name}.', {item_name: tag && tag.title ? tag.title : ''}) %>"
title="" title=""
data-tooltip data-tooltip
class="publish-icon <%= published_status %>" class="publish-icon <%= published_status %>"
@ -134,7 +137,7 @@
<div class="inline-block cog-menu-container"> <div class="inline-block cog-menu-container">
<a class="al-trigger al-trigger-gray" role="button" tabindex="0" href="#"> <a class="al-trigger al-trigger-gray" role="button" tabindex="0" href="#">
<i class="icon-settings"></i><i class="icon-mini-arrow-down"></i> <i class="icon-settings"></i><i class="icon-mini-arrow-down"></i>
<span class="screenreader-only"><%= t('settings', 'Settings') %></span> <span class="screenreader-only"><%= t('Manage %{item_name}', {item_name: tag && tag.title ? tag.title : 'item'}) %></span>
</a> </a>
<ul class="al-options"> <ul class="al-options">

View File

@ -1323,6 +1323,7 @@ define([
var publishData = { var publishData = {
moduleType: data.type, moduleType: data.type,
id: data.publishable_id, id: data.publishable_id,
moduleItemName: data.moduleItemName,
moduleId: data.context_module_id, moduleId: data.context_module_id,
courseId: data.context_id, courseId: data.context_id,
published: data.published, published: data.published,
@ -1376,7 +1377,8 @@ define([
model: file, model: file,
togglePublishClassOn: $el.parents('.ig-row')[0], togglePublishClassOn: $el.parents('.ig-row')[0],
userCanManageFilesForContext: ENV.MODULE_FILE_PERMISSIONS.manage_files, userCanManageFilesForContext: ENV.MODULE_FILE_PERMISSIONS.manage_files,
usageRightsRequiredForContext: ENV.MODULE_FILE_PERMISSIONS.usage_rights_required usageRightsRequiredForContext: ENV.MODULE_FILE_PERMISSIONS.usage_rights_required,
fileName: file.displayName()
} }
React.render(PublishCloud(props), $el[0]); React.render(PublishCloud(props), $el[0]);
@ -1389,13 +1391,26 @@ define([
id: data.id, id: data.id,
module_id: data.moduleId, module_id: data.moduleId,
module_item_id: data.moduleItemId, module_item_id: data.moduleItemId,
module_item_name: data.moduleItemName,
course_id: data.courseId, course_id: data.courseId,
published: data.published, published: data.published,
publishable: data.publishable, publishable: data.publishable,
unpublishable: data.unpublishable unpublishable: data.unpublishable
}); });
var view = new PublishIconView({model: model, el: $el[0]}); var viewOptions = {
model: model,
el: $el[0]
};
if (data.publishMessage) {
viewOptions.publishText = data.publishMessage;
}
if (data.unpublishMessage) {
viewOptions.unpublishText = data.unpublishMessage;
}
var view = new PublishIconView(viewOptions);
var row = $el.closest('.ig-row'); var row = $el.closest('.ig-row');
if (data.published) { row.addClass('ig-published'); } if (data.published) { row.addClass('ig-published'); }

View File

@ -50,6 +50,24 @@ define [
equal btnView.$text.html().match(/Published/).length, 1 equal btnView.$text.html().match(/Published/).length, 1
equal btnView.$el.attr('aria-label').match(/can't unpublish/).length, 1 equal btnView.$el.attr('aria-label').match(/can't unpublish/).length, 1
test 'should render the provided publish text when given', ->
testText = 'Test Publish Text'
btnView = new PublishButtonView({
model: @publish,
publishText: testText
}).render()
equal btnView.$('.screenreader-only.accessible_label').text(), testText
test 'should render the provided unpublish text when given', ->
testText = 'Test Unpublish Text'
btnView = new PublishButtonView({
model: @published,
unpublishText: testText
}).render()
equal btnView.$('.screenreader-only.accessible_label').text(), testText
# state # state
test 'disable should add disabled state', -> test 'disable should add disabled state', ->
btnView = new PublishButtonView(model: @publish).render() btnView = new PublishButtonView(model: @publish).render()

View File

@ -41,7 +41,7 @@ describe 'editing a quiz' do
type_in_tiny('#quiz_description', 'changed description') type_in_tiny('#quiz_description', 'changed description')
click_save_settings_button click_save_settings_button
wait_for_ajax_requests wait_for_ajax_requests
expect(f('#quiz-publish-link').text.strip!).to eq 'Published' expect(f('#quiz-publish-link .publish-text').text.strip!).to eq 'Published'
end end
it 'deletes the quiz', priority: "1", test_id: 351921 do it 'deletes the quiz', priority: "1", test_id: 351921 do
@ -54,7 +54,7 @@ describe 'editing a quiz' do
end end
f('#quiz-publish-link').click f('#quiz-publish-link').click
wait_for_ajax_requests wait_for_ajax_requests
expect(f('#quiz-publish-link').text.strip!).to eq 'Publish' expect(f('#quiz-publish-link .publish-text').text.strip!).to eq 'Publish'
end end
end end
@ -90,7 +90,7 @@ describe 'editing a quiz' do
end end
f('#quiz-publish-link').click f('#quiz-publish-link').click
wait_for_ajax_requests wait_for_ajax_requests
expect(f('#quiz-publish-link').text.strip!).to eq 'Unpublish' expect(f('#quiz-publish-link .publish-text').text.strip!).to eq 'Unpublish'
end end
end end