CQS: accessible summary stats table
Closes CNVS-15310 TEST PLAN ---- ---- - go to statistics cqs page - turn on screen-reader - locate the table and verify that it reads in a sensible manner - verify you hear a short description of what the table is when you get to it - verify that no column header is abbreviated anymore, so "Std. Deviation" now reads "Standard Deviation" and "Avg. Time" now reads "Average Time" - verify the last column for the Average Time reads a human-understandable duration, like 10 seconds or 5 minutes and 30 seconds instead of zero-zero-colon-one-zero (00:10) - sighted users should still see the duration in timestamp-form, e.g, 00:10 and 05:30 Change-Id: Ia15877f654a4538e939758fcf89ad45f36e52df2 Reviewed-on: https://gerrit.instructure.com/40686 Tested-by: Jenkins <jenkins@instructure.com> QA-Review: Trevor deHaan <tdehaan@instructure.com> Reviewed-by: Josh Simpson <jsimpson@instructure.com> Product-Review: Derek DeVries <ddevries@instructure.com>
This commit is contained in:
parent
ece4d0067a
commit
f3d3c5ec02
|
@ -0,0 +1,45 @@
|
|||
/** @jsx React.DOM */
|
||||
define(function(require) {
|
||||
var React = require('react');
|
||||
|
||||
/**
|
||||
* @class Components.SightedUserContent
|
||||
*
|
||||
* A component that *tries* to hide itself from screen-readers, absolutely
|
||||
* expecting that you're providing a more accessible version of the resource
|
||||
* using something like a ScreenReaderContent component.
|
||||
*
|
||||
* Be warned that this does not totally prevent all screen-readers from
|
||||
* seeing this content in all modes. For example, VoiceOver in OS X will
|
||||
* still see this element when running in the "Say-All" mode and read it
|
||||
* along with the accessible version you're providing.
|
||||
*
|
||||
* > **Warning**
|
||||
* >
|
||||
* > Use of this component is discouraged unless there's no alternative!!!
|
||||
* >
|
||||
* > The only one case that justifies its use is when design provides a
|
||||
* > totally inaccessible version of a resource, and you're trying to
|
||||
* > accommodate the design (for sighted users,) and provide a genuine layer
|
||||
* > of accessibility (for others.)
|
||||
*/
|
||||
var SightedUserContent = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
tagName: 'span'
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var tagFactory = React.DOM[this.props.tagName];
|
||||
|
||||
return this.transferPropsTo(tagFactory({
|
||||
'aria-hidden': true,
|
||||
'role': 'presentation',
|
||||
'aria-role': 'presentation'
|
||||
}, this.props.children));
|
||||
}
|
||||
});
|
||||
|
||||
return SightedUserContent;
|
||||
});
|
|
@ -1,4 +1,5 @@
|
|||
define(function() {
|
||||
define(function(require) {
|
||||
var I18n = require('i18n!quiz_statistics');
|
||||
var floor = Math.floor;
|
||||
|
||||
var pad = function(duration) {
|
||||
|
@ -35,5 +36,82 @@ define(function() {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Instead of rendering a timestamp as the main method does, this method will
|
||||
* render a given number of seconds into a human readable sentence. This is
|
||||
* the prefered alternative to present to screen-readers if you're using
|
||||
* the method above to format a duration.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* - 1 => `1 second`
|
||||
* - 32 => `32 seconds`
|
||||
* - 84 => `1 minute, and 24 seconds`
|
||||
* - 3684 => `1 hour, and 1 minute`
|
||||
*
|
||||
* Note that the seconds are discarded when the duration is longer than an
|
||||
* hour.
|
||||
*
|
||||
* @param {Number} seconds
|
||||
* Duration in seconds.
|
||||
*
|
||||
* @return {String}
|
||||
* A human-readable string representation of the duration.
|
||||
*/
|
||||
secondsToTime.toReadableString = function(seconds) {
|
||||
var hours, minutes, strHours, strMinutes, strSeconds;
|
||||
|
||||
if (seconds < 60) {
|
||||
return I18n.t('duration_in_seconds', {
|
||||
one: '1 second',
|
||||
other: '%{count} seconds'
|
||||
}, { count: floor(seconds) });
|
||||
}
|
||||
else if (seconds < 3600) {
|
||||
minutes = floor(seconds / 60);
|
||||
seconds = floor(seconds % 60);
|
||||
|
||||
strMinutes = I18n.t('duration_in_minutes', {
|
||||
one: '1 minute',
|
||||
other: '%{count} minutes'
|
||||
}, { count: minutes });
|
||||
|
||||
strSeconds = I18n.t('duration_in_seconds', {
|
||||
one: '1 second',
|
||||
other: '%{count} seconds'
|
||||
}, {
|
||||
count: seconds
|
||||
});
|
||||
|
||||
return I18n.t('duration_in_minutes_and_seconds', '%{minutes} and %{seconds}', {
|
||||
minutes: strMinutes,
|
||||
seconds: strSeconds
|
||||
});
|
||||
}
|
||||
else {
|
||||
hours = floor(seconds / 3600);
|
||||
minutes = floor((seconds - hours*3600) / 60);
|
||||
|
||||
strMinutes = I18n.t('duration_in_minutes', {
|
||||
one: '1 minute',
|
||||
other: '%{count} minutes'
|
||||
}, {
|
||||
count: minutes
|
||||
});
|
||||
|
||||
strHours = I18n.t('duration_in_hours', {
|
||||
one: '1 hour',
|
||||
other: '%{count} hours'
|
||||
}, {
|
||||
count: hours
|
||||
});
|
||||
|
||||
return I18n.t('duration_in_hours_and_minutes', '%{hours} and %{minutes}', {
|
||||
minutes: strMinutes,
|
||||
hours: strHours
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return secondsToTime;
|
||||
});
|
|
@ -7,6 +7,20 @@ define(function(require) {
|
|||
var secondsToTime = require('../util/seconds_to_time');
|
||||
var round = require('../util/round');
|
||||
var formatNumber = require('../util/format_number');
|
||||
var SightedUserContent = require('jsx!../components/sighted_user_content');
|
||||
var ScreenReaderContent = require('jsx!../components/screen_reader_content');
|
||||
|
||||
var Column = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<th scope="col">
|
||||
<SightedUserContent tagName="i" className={this.props.icon} />
|
||||
{' '}
|
||||
{this.props.label}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var Summary = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
|
@ -47,28 +61,29 @@ define(function(require) {
|
|||
</header>
|
||||
|
||||
<table className="text-left">
|
||||
<ScreenReaderContent tagName="caption">
|
||||
{I18n.t('table_description',
|
||||
'Summary statistics for all turned in submissions')
|
||||
}
|
||||
</ScreenReaderContent>
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<i className="icon-quiz-stats-avg"></i>{' '}
|
||||
{I18n.t('stats_mean', 'Avg Score')}
|
||||
</th>
|
||||
<th>
|
||||
<i className="icon-quiz-stats-high"></i>{' '}
|
||||
{I18n.t('stats_high', 'High Score')}
|
||||
</th>
|
||||
<th>
|
||||
<i className="icon-quiz-stats-low"></i>{' '}
|
||||
{I18n.t('stats_low', 'Low Score')}
|
||||
</th>
|
||||
<th>
|
||||
<i className="icon-quiz-stats-deviation"></i>{' '}
|
||||
{I18n.t('stats_stdev', 'Std. Deviation')}
|
||||
</th>
|
||||
<th>
|
||||
<i className="icon-quiz-stats-time"></i>{' '}
|
||||
{I18n.t('stats_avg_time', 'Avg Time')}
|
||||
</th>
|
||||
<Column
|
||||
icon="icon-quiz-stats-avg"
|
||||
label={I18n.t('mean', 'Average Score')} />
|
||||
<Column
|
||||
icon="icon-quiz-stats-high"
|
||||
label={I18n.t('high_score', 'High Score')} />
|
||||
<Column
|
||||
icon="icon-quiz-stats-low"
|
||||
label={I18n.t('low_score', 'Low Score')} />
|
||||
<Column
|
||||
icon="icon-quiz-stats-deviation"
|
||||
label={I18n.t('stdev', 'Standard Deviation')} />
|
||||
<Column
|
||||
icon="icon-quiz-stats-time"
|
||||
label={I18n.t('avg_time', 'Average Time')} />
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
|
@ -80,14 +95,24 @@ define(function(require) {
|
|||
<td>{this.ratioFor(this.props.scoreHigh)}%</td>
|
||||
<td>{this.ratioFor(this.props.scoreLow)}%</td>
|
||||
<td>{formatNumber(round(this.props.scoreStdev, 2), 2)}</td>
|
||||
<td>{secondsToTime(this.props.durationAverage)}</td>
|
||||
<td>
|
||||
<ScreenReaderContent>
|
||||
{secondsToTime.toReadableString(this.props.durationAverage)}
|
||||
</ScreenReaderContent>
|
||||
{/*
|
||||
try to hide the [HH:]MM:SS timestamp from SR users because
|
||||
it's not really useful, however this doesn't work in all
|
||||
modes such as the Speak-All mode (at least on VoiceOver)
|
||||
*/}
|
||||
<SightedUserContent>
|
||||
{secondsToTime(this.props.durationAverage)}
|
||||
</SightedUserContent>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<ScorePercentileChart
|
||||
key="chart"
|
||||
scores={this.props.scores} />
|
||||
<ScorePercentileChart key="chart" scores={this.props.scores} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
define(function(require) {
|
||||
var secondsToTime = require('util/seconds_to_time');
|
||||
|
||||
describe('Util.secondsToTime', function() {
|
||||
describe('#toReadableString', function() {
|
||||
var subject = secondsToTime.toReadableString;
|
||||
|
||||
it('24 => 24 seconds', function() {
|
||||
expect(subject(24)).toEqual('24 seconds');
|
||||
});
|
||||
|
||||
it('84 => one minute and 24 seconds', function() {
|
||||
expect(subject(84)).toEqual('1 minute and 24 seconds');
|
||||
});
|
||||
|
||||
it('144 => 2 minutes and 24 seconds', function() {
|
||||
expect(subject(144)).toEqual('2 minutes and 24 seconds');
|
||||
});
|
||||
|
||||
it('3684 => one hour and one minute', function() {
|
||||
expect(subject(3684)).toEqual('1 hour and 1 minute');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue