Converts the Course Wizard to an accessible react component

closes CNVS-17335
closes CNVS-16938
closes CNVS-16941
closes CNVS-16944
closes CNVS-14308
closes CNVS-12791
closes CNVS-12792

Test Plan:
  - Check to make sure the wizard has normal wizard
    behavior (aka regression test)
  - Check all the accessibility concerns including:
     - Close with escape key
     - Focus management throughout
     - Screenreader behaviors

Sorry the test plan isn't super specific, but it would be
extremely long if I specified everything.

Change-Id: I10340246720c0d1db1e5911969a2b9a11499343f
Reviewed-on: https://gerrit.instructure.com/45868
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Dan Minkevitch <dan@instructure.com>
QA-Review: Clare Strong <clare@instructure.com>
Product-Review: Clay Diffrient <cdiffrient@instructure.com>
This commit is contained in:
Clay Diffrient 2014-12-09 14:18:52 -07:00
parent e21ba4e914
commit 0e71c4ff61
17 changed files with 962 additions and 98 deletions

View File

@ -0,0 +1,34 @@
require [
'jquery'
'react'
'compiled/userSettings'
'jsx/course_wizard/CourseWizard'
], ($, React, userSettings, CourseWizard) ->
###
# This essentially handles binding the button events and calling out to the
# CourseWizard React component that is the actual wizard.
###
$wizard_box = $("#wizard_box")
pathname = window.location.pathname
$(".close_wizard_link").click((event) ->
event.preventDefault()
userSettings.set('hide_wizard_' + pathname, true)
$(".wizard_popup_link").slideDown('fast')
$('.wizard_popup_link').focus()
)
$(".wizard_popup_link").click((event) ->
React.renderComponent(CourseWizard({
overlayClassName:'CourseWizard__modalOverlay',
showWizard: true
}), $wizard_box[0])
)
setTimeout( ->
if (!userSettings.get('hide_wizard_' + pathname))
$(".wizard_popup_link.auto_open:first").click()
, 500)

View File

@ -1427,6 +1427,43 @@ class CoursesController < ApplicationController
@course_home_view = (params[:view] == "feed" && 'feed') || @context.default_view || 'feed'
# Course Wizard JS Info
js_env({:COURSE_WIZARD => {
:just_saved => @context_just_saved,
:checklist_states => {
:import_step => @context.attachments.active.first.nil?,
:assignment_step => @context.assignments.active.first.nil?,
:add_student_step => @context.students.first.nil?,
:navigation_step => @context.tab_configuration.empty?,
:home_page_step => true, # The current wizard just always marks this as complete.
:calendar_event_step => @context.calendar_events.active.first.nil?,
:add_ta_step => @context.tas.empty?,
:publish_step => @context.workflow_state === "available"
},
:urls => {
:content_import => context_url(@context, :context_content_migrations_url),
:add_assignments => context_url(@context, :context_assignments_url, :wizard => 1),
:add_students => course_users_path(course_id: @context),
:add_files => context_url(@context, :context_files_url, :wizard => 1),
:select_navigation => context_url(@context, :context_details_url),
:course_calendar => calendar_path(:wizard => 1),
:add_tas => course_users_path(:course_id => @context),
:publish_course => course_path(@context)
},
:permisssions => {
# Sending the permissions just so maybe later we can extract this easier.
:can_manage_content => can_do(@context, @current_user, :manage_content),
:can_manage_students => can_do(@context, @current_user, :manage_students),
:can_manage_assignments => can_do(@context, @current_user, :manage_assignments),
:can_manage_files => can_do(@context, @current_user, :manage_files),
:can_update => can_do(@context, @current_user, :update),
:can_manage_calendar => can_do(@context, @current_user, :manage_calendar),
:can_manage_admin_users => can_do(@context, @current_user, :manage_admin_users),
:can_change_course_state => can_do(@context, @current_user, :change_course_state)
}
}
})
# make sure the wiki front page exists
if @course_home_view == 'wiki'
@context.wiki.check_has_front_page

View File

@ -0,0 +1,51 @@
/** @jsx React.DOM */
define([
'react',
'./ChecklistItem',
'./ListItems'
], function(React, ChecklistItem, ListItems) {
var Checklist = React.createClass({
displayName: 'Checklist',
getInitialState: function () {
return {
selectedItem: this.props.selectedItem || ''
}
},
componentWillReceiveProps: function (newProps) {
this.setState({
selectedItem: newProps.selectedItem
});
},
renderChecklist: function () {
return ListItems.map((item) => {
var isSelected = this.state.selectedItem === item.key;
var id = 'wizard_' + item.key;
return (
<ChecklistItem complete={item.complete}
id={id}
key={item.key}
title={item.title}
onClick={this.props.clickHandler}
isSelected={isSelected}
/>
);
});
},
render: function () {
var checklist = this.renderChecklist();
return (
<div className={this.props.className}>{checklist}</div>
);
}
});
return Checklist;
});

View File

@ -0,0 +1,51 @@
/** @jsx React.DOM */
define([
'react'
], function(React) {
var ChecklistItem = React.createClass({
displayName: 'ChecklistItem',
classNameString: '',
getInitialState: function () {
return {classNameString: ''};
},
componentWillMount: function () {
this.setClassName(this.props);
},
componentWillReceiveProps: function (nextProps) {
this.setClassName(nextProps);
},
handleClick: function (event) {
event.preventDefault();
this.props.onClick(this.props.key)
},
setClassName: function (props) {
this.setState({
classNameString: React.addons.classSet({
"ic-wizard-box__content-trigger": true,
"ic-wizard-box__content-trigger--checked": props.complete,
"ic-wizard-box__content-trigger--active": props.isSelected
})
});
},
render: function () {
return (
<a href="#" id={this.props.id} className={this.state.classNameString} onClick={this.handleClick}>
<span>{this.props.title}</span>
</a>
);
}
});
return ChecklistItem;
});

View File

@ -0,0 +1,102 @@
/** @jsx React.DOM */
define([
'jquery',
'react',
'i18n!course_wizard',
'react-modal',
'./InfoFrame',
'./Checklist',
'compiled/jquery.rails_flash_notifications'
], function($, React, I18n, ReactModal, InfoFrame, Checklist) {
var CourseWizard = React.createClass({
displayName: 'CourseWizard',
propTypes: {
showWizard: React.PropTypes.bool,
overlayClassName: React.PropTypes.string
},
getInitialState: function () {
return {
showWizard: this.props.showWizard,
selectedItem: false
};
},
componentDidMount: function () {
this.refs.closeLink.getDOMNode().focus();
$(this.refs.wizardBox.getDOMNode()).removeClass('ic-wizard-box--is-closed');
$.screenReaderFlashMessageExclusive(I18n.t("Course Setup Wizard is showing."));
},
componentWillReceiveProps: function (nextProps) {
this.setState({
showWizard: nextProps.showWizard
}, () => {
$(this.refs.wizardBox.getDOMNode()).removeClass('ic-wizard-box--is-closed');
if (this.state.showWizard) {
this.refs.closeLink.getDOMNode().focus();
}
});
},
/**
* Handles what should happen when a checklist item is clicked.
*/
checklistClickHandler: function (itemToShowKey) {
this.setState({
selectedItem: itemToShowKey
});
},
closeModal: function (event) {
if (event) {
event.preventDefault()
};
this.setState({
showWizard: false
})
},
render: function () {
return (
<ReactModal
isOpen={this.state.showWizard}
onRequestClose={this.closeModal}
overlayClassName={this.props.overlayClassName}
>
<div ref="wizardBox" className="ic-wizard-box">
<div className="ic-wizard-box__header">
<a href="/" className="ic-wizard-box__logo-link">
<span className="screenreader-only">{I18n.t("My dashboard")}</span>
</a>
<Checklist className="ic-wizard-box__nav"
selectedItem={this.state.selectedItem}
clickHandler={this.checklistClickHandler}
/>
</div>
<div className="ic-wizard-box__main">
<div className="ic-wizard-box__close">
<div className="ic-Expand-link ic-Expand-link--Secondary ic-Expand-link--from-right">
<a ref="closeLink" href="#" className="ic-Expand-link__trigger" onClick={this.closeModal}>
<div className="ic-Expand-link__layout">
<i className="icon-x ic-Expand-link__icon"></i>
<span className="ic-Expand-link__text">{I18n.t("Close and return to Canvas")}</span>
</div>
</a>
</div>
</div>
<InfoFrame className="ic-wizard-box__content" itemToShow={this.state.selectedItem} closeModal={this.closeModal} />
</div>
</div>
</ReactModal>
);
}
});
return CourseWizard;
});

View File

@ -0,0 +1,150 @@
/** @jsx React.DOM */
define([
'jquery',
'underscore',
'react',
'i18n!course_wizard',
'./ListItems'
], function($, _, React, I18n, ListItems) {
var courseNotSetUpItem = {
text: I18n.t("Great, so you've got a course. Now what? Well, before you go publishing it to the world, you may want to check and make sure you've got the basics laid out. Work through the list on the left to ensure that your course is ready to use."),
warning: I18n.t("This course is visible only to teachers until it is published."),
iconClass: 'icon-instructure'
};
var checklistComplete = {
text: I18n.t("Now that your course is set up and available, you probably won't need this checklist anymore. But we'll keep it around in case you realize later you want to try something new, or you just want a little extra help as you make changes to your course content."),
iconClass: 'icon-instructure'
};
var InfoFrame = React.createClass({
displayName: 'InfoFrame',
getInitialState: function () {
return {
itemShown: courseNotSetUpItem,
};
},
componentWillMount: function () {
if (ENV.COURSE_WIZARD.checklist_states.publish_step) {
this.setState({
itemShown: checklistComplete
});
}
},
componentWillReceiveProps: function (newProps) {
this.getWizardItem(newProps.itemToShow);
},
getWizardItem: function (key) {
var item = _.findWhere(ListItems, {key: key});
this.setState({
itemShown: item
}, function () {
$messageBox = $(this.refs.messageBox.getDOMNode());
$messageIcon = $(this.refs.messageIcon.getDOMNode());
// I would use .toggle, but it has too much potential to get all out
// of whack having to be called twice to force the animation.
// Remove the animation classes in case they are there already.
$messageBox.removeClass('ic-wizard-box__message-inner--is-fired');
$messageIcon.removeClass('ic-wizard-box__message-icon--is-fired');
// Add them back
setTimeout(function() {
$messageBox.addClass('ic-wizard-box__message-inner--is-fired');
$messageIcon.addClass('ic-wizard-box__message-icon--is-fired');
}, 100);
// Set the focus to the call to action 'button' if it's there
// otherwise the text.
if (this.refs.callToAction) {
this.refs.callToAction.getDOMNode().focus();
} else {
this.refs.messageBox.getDOMNode().focus();
}
});
},
getHref: function () {
return this.state.itemShown.url || '#';
},
chooseHomePage: function (event) {
event.preventDefault();
this.props.closeModal();
$('.choose_home_page_link').click();
},
renderButton: function () {
if (this.state.itemShown.key === 'home_page') {
return (<a ref="callToAction" onClick={this.chooseHomePage} className="Button Button--primary">
{this.state.itemShown.title}
</a>
);
}
if (this.state.itemShown.key === 'publish_course') {
return (
<form accept-charset="UTF-8" action={ENV.COURSE_WIZARD.publish_course} method="post">
<input name="utf8" type="hidden" value="✓" />
<input name="_method" type="hidden" value="put" />
<input name="authenticity_token" type="hidden" value={$.cookie('_csrf_token')} />
<input type="hidden" name="course[event]" value="offer"/>
<button ref="callToAction" type="submit" className="Button Button--success">{this.state.itemShown.title}</button>
</form>
);
}
if (this.state.itemShown.hasOwnProperty('title')) {
return (
<a ref="callToAction" href={this.getHref()} className="Button Button--primary">
{this.state.itemShown.title}
</a>
);
}
else if (this.state.itemShown.hasOwnProperty('warning')) {
return <b>{this.state.itemShown.warning}</b>
}
else {
return null;
}
},
render: function () {
return (
<div className={this.props.className}>
<h1 className="ic-wizard-box__headline">
{I18n.t("Next Steps")}
</h1>
<div className="ic-wizard-box__message">
<div className="ic-wizard-box__message-layout">
<div ref="messageIcon" className="ic-wizard-box__message-icon ic-wizard-box__message-icon--is-fired">
<i className={this.state.itemShown.iconClass}></i>
</div>
<div ref="messageBox" tabIndex="-1" className="ic-wizard-box__message-inner ic-wizard-box__message-inner--is-fired">
<p className="ic-wizard-box__message-text">
{this.state.itemShown.text}
</p>
<div className="ic-wizard-box__message-button">
{this.renderButton()}
</div>
</div>
</div>
</div>
</div>
);
}
});
return InfoFrame;
});

View File

@ -0,0 +1,82 @@
define([
'i18n!course_wizard'
], function (I18n) {
/**
* Returns an array containing all the possible items for the checklist
*/
return [
{
key :'content_import',
complete: ENV.COURSE_WIZARD.checklist_states.import_step,
title: I18n.t("Import Content"),
text: I18n.t("If you've been using another course management system, you probably have stuff in there that you're going to want moved over to Canvas. We can walk you through the process of easily migrating your content into Canvas."),
url: ENV.COURSE_WIZARD.urls.content_import,
iconClass: 'icon-upload'
},
{
key :'add_assignments',
complete: ENV.COURSE_WIZARD.checklist_states.assignment_step,
title: I18n.t("Add Course Assignments"),
text: I18n.t("Add your assignments. You can just make a long list, or break them up into groups - and even specify weights for each assignment group."),
url: ENV.COURSE_WIZARD.urls.add_assignments,
iconClass: 'icon-assignment'
},
{
key :'add_students',
complete: ENV.COURSE_WIZARD.checklist_states.add_student_step,
title: I18n.t("Add Students to the Course"),
text: I18n.t("You'll definitely want some of these. What's the fun of teaching a course if nobody's even listening?"),
url: ENV.COURSE_WIZARD.urls.add_students,
iconClass: 'icon-group-new'
},
{
key :'add_files',
complete: ENV.COURSE_WIZARD.checklist_states.import_step, /* Super odd in the existing wizard this is set to display: none */
title: I18n.t("Add Files to the Course"),
text: I18n.t("The Files tab is the place to share lecture slides, example documents, study helps -- anything your students will want to download. Uploading and organizing your files is easy with Canvas. We'll show you how."),
url: ENV.COURSE_WIZARD.urls.add_files,
iconClass: 'icon-note-light'
},
{
key :'select_navigation',
complete: ENV.COURSE_WIZARD.checklist_states.navigation_step,
title: I18n.t("Select Navigation Links"),
text: I18n.t("By default all links are enabled for a course. Students won't see links to sections that don't have content. For example, if you haven't created any quizzes, they won't see the quizzes link. You can sort and explicitly disable these links if there are areas of the course you don't want your students accessing."),
url: ENV.COURSE_WIZARD.urls.select_navigation,
iconClass: 'icon-hamburger'
},
{
key :'home_page',
complete: ENV.COURSE_WIZARD.checklist_states.home_page_step,
title: I18n.t("Choose a Course Home Page"),
text: I18n.t("When people visit the course, this is the page they'll see. You can set it to show an activity stream, the list of course modules, a syllabus, or a custom page you write yourself. The default is the course activity stream."),
iconClass: 'icon-home'
},
{
key :'course_calendar',
complete: ENV.COURSE_WIZARD.checklist_states.calendar_event_step,
title: I18n.t("Add Course Calendar Events"),
text: I18n.t("Here's a great chance to get to know the calendar and add any non-assignment events you might have to the course. Don't worry, we'll help you through it."),
url: ENV.COURSE_WIZARD.urls.course_calendar,
iconClass: 'icon-calendar-month'
},
{
key :'add_tas',
complete: ENV.COURSE_WIZARD.checklist_states.add_ta_step,
title: I18n.t("Add TAs to the Course"),
text: I18n.t("You may want to assign some TAs to help you with the course. TAs can grade student submissions, help moderate the discussions and even update due dates and assignment details for you."),
url: ENV.COURSE_WIZARD.urls.add_tas,
iconClass: 'icon-educators'
},
{
key :'publish_course',
complete: ENV.COURSE_WIZARD.checklist_states.publish_step,
title: I18n.t("Publish the Course"),
text: I18n.t("All finished? Time to publish your course! Click the button below to make it official! Publishing will allow the users to begin participating in the course."),
non_registered_text: I18n.t("This course is claimed and ready, but you'll need to finish the registration process before you can publish the course. You should have received an email from Canvas with a link to finish the process. Be sure to check your spam box."),
iconClass: 'icon-publish'
}
]
})

View File

@ -12,4 +12,7 @@
@else if $breakpoint == wide {
@media only screen and (min-width: 1024px) { @content; }
}
@else if $breakpoint == short {
@media only screen and (max-height: 600px) { @content; }
}
}

View File

@ -129,7 +129,6 @@ $ic-Expand-link-size: $can-sp*4;
color: $text-color;
}
&:hover, &:focus {
text-decoration: none;
.ic-Expand-link__layout { background: $bg-color-hover; }
}
}
@ -164,7 +163,11 @@ $ic-Expand-link-size: $can-sp*4;
transform: translateX(0);
}
&:hover, &:focus { @include ic-Expand-link-active-state; }
&:hover, &:focus {
@include ic-Expand-link-active-state;
text-decoration: none;
outline: none;
}
}
@ -213,7 +216,9 @@ $ic-Expand-link-size: $can-sp*4;
transform: translateX(0);
}
&:hover, &:focus { @include ic-Expand-link-active-state; }
&:hover, &:focus {
@include ic-Expand-link-active-state;
}
}
.ic-Expand-link__layout { padding: 0 0 0 $ic-Expand-link-size; }

View File

@ -0,0 +1,288 @@
@import "base/environment";
.ReactModal__Overlay.CourseWizard__modalOverlay {
transition: none;
background: transparent;
}
.ic-wizard-box {
transition: all 1s $can-transition;
width: 100%;
height: 100%;
background: $canvas-secondary;
position: fixed;
bottom: 0; left: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
background: url("/images/wizard-bg.jpg") no-repeat center center;
background-size: cover;
transform: translate3d(0,100%,0);
opacity: 0;
@include breakpoint(desktop) { flex-direction: row; }
.ReactModal__Overlay.ReactModal__Overlay--after-open & {
transform: translate3d(0,0,0);
opacity: 1;
}
*, *:before, *:after { box-sizing: border-box; }
}
.ic-wizard-box__header {
flex: 1.3;
display: flex;
flex-direction: column;
order: 1;
@if $use_high_contrast { background: $canvas-secondary; }
@else { background: rgba($canvas-secondary, 0.9); }
@include breakpoint(mini-tablet) {
flex-direction: row;
}
@include breakpoint(desktop) {
flex: 1;
order: 0;
flex-direction: column;
}
}
.ic-wizard-box__logo-link {
background: url("/images/canvas-logo.svg") no-repeat $can-sp+4 50%;
background-size: 114px 27px;
border-left: 4px solid transparent;
flex: 0 0 36px;
&:hover, &:focus {
border-color: $canvas-primary;
background-color: rgba($canvas-light, 0.1);
}
@include breakpoint(mini-tablet) {
flex: 1;
}
@include breakpoint(tablet) {
background-size: 144px 34px;
}
@include breakpoint(desktop) {
flex: 0 0 $can-sp*9;
@include breakpoint(short) {
flex: 0 0 $can-sp*6;
background-size: 114px 27px;
}
}
}
.ic-wizard-box__nav {
display: flex;
flex: 1;
flex-direction: column;
@include breakpoint(mini-tablet) {
flex: 2;
}
@include breakpoint(desktop) { flex: 1; }
}
.ic-wizard-box__content-trigger {
flex: 1;
display: flex;
align-items: center;
user-select: none;
padding: 0 $can-sp 0 $can-sp*4;
border-left: 4px solid transparent;
background: url("/images/wizard-todo-unchecked.svg") no-repeat $can-sp 50%;
background-size: 18px 18px;
font-weight: 300;
@if $use_high_contrast {
color: $canvas-light;
&:hover, &:focus, &.ic-wizard-box__content-trigger--active {
background-color: $canvas-primary;
color: canvas-light;
}
}
@else {
color: rgba($canvas-light, 0.9);
&:hover, &:focus, &.ic-wizard-box__content-trigger--active {
background-color: rgba($canvas-light, 0.1);
color: $canvas-light;
}
}
@include breakpoint(desktop) { font-size: 15px; }
&:hover, &:focus, &.ic-wizard-box__content-trigger--active {
border-left-color: $canvas-primary;
text-decoration: none;
color: $canvas-light;
}
&.ic-wizard-box__content-trigger--checked {
background-image: url("/images/wizard-todo-checked.svg");
}
}
.ic-wizard-box__main {
flex: 2;
display: flex;
flex-direction: column;
@if $use_high_contrast { background: rgba( $canvas-secondary, 0.9); }
@else { background: linear-gradient(to bottom, rgba($canvas-secondary, 0.75) 0%,rgba(0,0,0,0) 100%); }
}
.ic-wizard-box__content {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.ic-wizard-box__close {
text-align: right;
}
.ic-wizard-box__headline {
line-height: 0.85;
font-size: $can-sp*3;
text-align: center;
color: $canvas-light;
font-weight: 700;
margin: 0;
@include breakpoint(mini-tablet) {
font-size: $can-sp*4;
padding: $can-sp 0;
}
@include breakpoint(tablet) {
font-size: $can-sp*5;
}
@include breakpoint(desktop) {
font-size: $can-sp*6;
padding: $can-sp*2 0;
}
}
.ic-wizard-box__message {
flex: 1;
display: flex;
align-items: center;
}
.ic-wizard-box__message-layout {
position: relative;
perspective: 500px;
@include breakpoint(mini-tablet) {
margin: 0 $can-sp;
}
@include breakpoint(tablet) {
margin: 0 $can-sp*2;
}
@include breakpoint(desktop) {
max-width: 600px;
margin: 0;
}
}
.ic-wizard-box__message-icon {
display: none;
@include breakpoint(mini-tablet) {
transition: all 1s $can-transition;
opacity: 0;
transform: scale(0) translate3d(0,50%,0);
display: block;
background: $canvas-secondary;
width: 72px; height: 72px;
border-radius: 100%;
position: absolute;
left: 50%; top: -36px;
z-index: 1;
margin-left: -36px;
display: flex;
align-items: center;
justify-content: center;
&.ic-wizard-box__message-icon--is-fired {
opacity: 1;
transform: scale(1) translate3d(0,0,0);
}
i[class*=icon-], i[class^=icon-] {
width: auto; height: auto;
color: $canvas-light;
line-height: 1;
&:before {
font-size: $can-sp*3;
top: 0;
}
}
}
@include breakpoint(tablet) {
i[class*=icon-], i[class^=icon-] {
&:before { font-size: $can-sp*3; }
}
}
@include breakpoint(desktop) {
width: 120px; height: 120px;
top: -60px; left: 50%;
margin-left: -60px;
i[class*=icon-], i[class^=icon-] {
&:before { font-size: $can-sp*5; }
}
}
}
.ic-wizard-box__message-inner {
transition: all 1s $can-transition;
opacity: 0;
transform: translate3d(0,50%,0);
background: rgba($canvas-light, 0.9);
padding: $can-sp*4 $can-sp*2 $can-sp*2 $can-sp*2;
box-shadow: 0 1px 4px 1px rgba(black, 0.3);
text-align: center;
&.ic-wizard-box__message-inner--is-fired {
opacity: 1;
transform: translate3d(0,0,0);
}
@if $use_high_contrast { background: $canvas-light; }
@else { background: rgba($canvas-light, 0.9); }
@include breakpoint(mini-tablet) {
border-radius: $baseBorderRadius;
}
@include breakpoint(desktop) { padding: $can-sp*7 $can-sp*2 $can-sp*2 $can-sp*2; }
}
.ic-wizard-box__message-button {
margin-top: $can-sp*2;
}
.ic-wizard-box__message-text {
font-weight: 300;
margin-bottom: 0;
@include breakpoint(desktop) {
font-size: 15px;
line-height: 1.6;
@include breakpoint(short) {
font-size: 14px;
line-height: 1.3;
}
}
}
// Styles for specific React Components
@import "./CourseWizard";

View File

@ -56,7 +56,7 @@
</a>
<% end %>
<% if @can_manage_content %>
<a class="btn button-sidebar-wide element_toggler" aria-controls="edit_course_home_content_form" href="<%= context_url(@context, :context_details_url) %>">
<a class="btn button-sidebar-wide element_toggler choose_home_page_link" aria-controls="edit_course_home_content_form" href="<%= context_url(@context, :context_details_url) %>">
<i class="icon-target"></i>
<%= t('links.choose_home_page', %{Choose Home Page}) %>
</a>
@ -69,15 +69,35 @@
</a>
<% end %>
<% if @can_manage_content %>
<a href="#" class="btn button-sidebar-wide wizard_popup_link <%= 'auto_open' if @context.created? || @context.claimed? %>">
<i class="icon-question"></i> <%= t('links.course_setup', %{Course Setup Checklist}) %>
</a>
<% if @can_manage_content %>
<% js_bundle :course_wizard %>
<% jammit_css :course_wizard %>
<a href="#" class="btn button-sidebar-wide wizard_popup_link <%= 'auto_open' if @context.created? || @context.claimed? %>">
<i class="icon-question"></i> <%= t('links.course_setup', %{Course Setup Checklist}) %>
</a>
<a class="btn button-sidebar-wide" href="<%= context_url(@context, :new_context_discussion_topic_url, :is_announcement => true) %>"><i class="icon-announcement"></i> <%= t('links.new_announcement', %{New Announcement}) %>
</a>
<% end %>
</div>
<% else %>
<% if @can_manage_content || @course_home_sub_navigation_tools.present? %>
<div class="secondary-button-group rs-margin-lr rs-margin-top rs-margin-bottom">
<% @course_home_sub_navigation_tools.each do |tool| %>
<a class="btn button-sidebar-wide course-home-sub-navigation-lti"
href="<%= course_external_tool_path(@context, tool, launch_type: 'course_home_sub_navigation') %>">
<img class="icon" src="<%= tool.course_home_sub_navigation(:icon_url) %>" />
<%= tool.label_for(:course_home_sub_navigation) %>
</a>
<% end %>
<% if @can_manage_content %>
<% js_bundle :course_wizard %>
<% jammit_css :course_wizard %>
<a href="#" class="btn button-sidebar-wide wizard_popup_link <%= 'auto_open' if @context.created? || @context.claimed? %>"><i class="icon-question"></i> <%= t('links.course_setup', %{Course Setup Checklist}) %></a>
<a class="btn button-sidebar-wide" href="<%= context_url(@context, :new_context_discussion_topic_url, :is_announcement => true) %>"><i class="icon-announcement"></i> <%= t('links.new_announcement', %{New Announcement}) %></a>
<% end %>
</div>
<% end %>
<% end %>
<% if @context.available? && @context.self_enrollment_enabled? && @context.open_enrollment && (!@context_enrollment || !@context_enrollment.active?) %>

View File

@ -16,6 +16,8 @@ compress_assets: off
stylesheets:
course_wizard:
- public/stylesheets/compiled/pages/course_wizard/compiler-course_wizard.css
react_files:
- public/stylesheets/compiled/pages/react_files/compiler-react_files.css
instructure_eportfolio:

View File

@ -0,0 +1,20 @@
<svg version="1.1" class="svg-canvas-logo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 115 27" width="115" height="27" enable-background="new 0 0 115 27" xml:space="preserve">
<g class="svg-logo-canvas__logomark">
<path id="Shape" fill="#D64027" d="M3.9,13.5c0-2-1.5-3.6-3.4-3.8C0.2,10.9,0,12.1,0,13.5s0.2,2.6,0.5,3.8C2.4,17.1,3.9,15.4,3.9,13.5L3.9,13.5z"/><circle id="Oval" fill="#D64027" cx="6.2" cy="13.4" r="1.2"/>
<path id="Shape_1_" fill="#D64027" d="M22.8,13.5c0,2,1.5,3.6,3.4,3.8c0.3-1.2,0.5-2.5,0.5-3.8s-0.2-2.6-0.5-3.8C24.3,9.9,22.8,11.5,22.8,13.5L22.8,13.5z"/><circle id="Oval_1_" fill="#D64027" cx="20.2" cy="13.4" r="1.2"/>
<path id="Shape_2_" fill="#D64027" d="M13.3,23c-2,0-3.6,1.5-3.8,3.4c1.2,0.3,2.5,0.5,3.8,0.5c1.3,0,2.6-0.2,3.8-0.5C16.9,24.5,15.3,23,13.3,23L13.3,23z"/><circle id="Oval_2_" fill="#D64027" cx="13.2" cy="20.4" r="1.2"/>
<path id="Shape_3_" fill="#D64027" d="M13.3,4c2,0,3.6-1.5,3.8-3.4c-1.2-0.3-2.5-0.5-3.8-0.5c-1.3,0-2.6,0.2-3.8,0.5C9.7,2.5,11.3,4,13.3,4L13.3,4z"/><circle id="Oval_3_" fill="#D64027" cx="13.2" cy="6.4" r="1.2"/>
<path id="Shape_4_" fill="#D64027" d="M20,20.2c-1.4,1.4-1.5,3.6-0.3,5.1c2.2-1.3,4.1-3.2,5.4-5.4C23.6,18.7,21.4,18.8,20,20.2L20,20.2z"/><circle id="Oval_4_" fill="#D64027" cx="18.2" cy="18.4" r="1.2"/>
<path id="Shape_5_" fill="#D64027" d="M6.6,6.8C8,5.4,8.1,3.2,6.9,1.7C4.7,3,2.8,4.9,1.5,7.1C3,8.3,5.2,8.2,6.6,6.8L6.6,6.8z"/>
<circle id="Oval_5_" fill="#D64027" cx="8.2" cy="8.4" r="1.2"/><path id="Shape_6_" fill="#D64027" d="M20,6.8c1.4,1.4,3.6,1.5,5.1,0.3c-1.3-2.2-3.2-4.1-5.4-5.4C18.5,3.2,18.6,5.4,20,6.8L20,6.8z"/><circle id="Oval_6_" fill="#D64027" cx="18.2" cy="8.4" r="1.2"/>
<path id="Shape_7_" fill="#D64027" d="M6.6,20.2c-1.4-1.4-3.6-1.5-5.1-0.3c1.3,2.2,3.2,4.1,5.4,5.4C8.1,23.7,8,21.6,6.6,20.2L6.6,20.2z"/>
<circle id="Oval_7_" fill="#D64027" cx="8.2" cy="18.4" r="1.2"/>
</g>
<g class="svg-logo-canvas__logotype" transform="translate(37.000000, 8.000000)">
<path id="Shape_8_" fill="#FFFFFF" d="M5.8,12.2c-3,0-5.8-1.8-5.8-6.7s2.9-6.7,5.8-6.7c1.8,0,3.1,0.5,4.3,1.8L8.4,2.4C7.5,1.5,6.9,1.1,5.8,1.1c-1,0-1.9,0.4-2.4,1.2C2.8,3,2.6,4,2.6,5.5S2.8,8,3.4,8.7C4,9.4,4.8,9.9,5.8,9.9c1,0,1.7-0.3,2.5-1.2l1.8,1.7C8.9,11.7,7.7,12.2,5.8,12.2L5.8,12.2z"/>
<path id="Shape_9_" fill="#FFFFFF" d="M20.3,12.1v-1.2c-1,1-1.9,1.3-3.5,1.3c-1.6,0-2.7-0.4-3.5-1.2c-0.7-0.7-1-1.7-1-2.8c0-2.2,1.5-3.8,4.5-3.8h3.5V3.5c0-1.7-0.8-2.5-2.9-2.5c-1.4,0-2.1,0.3-2.9,1.3l-1.7-1.6c1.2-1.4,2.5-1.9,4.7-1.9c3.6,0,5.4,1.5,5.4,4.5v8.8H20.3L20.3,12.1z M20.3,6.3h-3.1c-1.6,0-2.4,0.7-2.4,1.9s0.8,1.9,2.4,1.9c1,0,1.8-0.1,2.5-0.8c0.4-0.4,0.6-1,0.6-1.9V6.3L20.3,6.3z"/>
<path id="Shape_10_" fill="#FFFFFF" d="M35.2,12.1v-8c0-2-1.2-2.9-2.6-2.9c-1.5,0-2.7,0.9-2.7,2.9v8h-2.6V-1.1h2.6v1.3c0.9-1,2.2-1.5,3.5-1.5c1.3,0,2.5,0.4,3.2,1.2c1,1,1.3,2.2,1.3,3.7v8.4L35.2,12.1L35.2,12.1z"/><path id="Shape_11_" fill="#FFFFFF" d="M47.3,12.1h-2.1L40.3-1.1h2.8l3.1,9.2l3.1-9.2h2.8L47.3,12.1L47.3,12.1z"/>
<path id="Shape_12_" fill="#FFFFFF" d="M61.6,12.1v-1.2c-1,1-1.9,1.3-3.5,1.3c-1.6,0-2.7-0.4-3.5-1.2c-0.7-0.7-1-1.7-1-2.8c0-2.2,1.5-3.8,4.5-3.8h3.5V3.5c0-1.7-0.8-2.5-2.9-2.5c-1.4,0-2.1,0.3-2.9,1.3l-1.7-1.6c1.2-1.4,2.5-1.9,4.7-1.9c3.6,0,5.4,1.5,5.4,4.5v8.8H61.6L61.6,12.1z M61.6,6.3h-3.1c-1.6,0-2.4,0.7-2.4,1.9s0.8,1.9,2.4,1.9c1,0,1.8-0.1,2.5-0.8c0.4-0.4,0.6-1,0.6-1.9V6.3L61.6,6.3z"/>
<path id="Shape_13_" fill="#FFFFFF" d="M72.4,12.2c-2.1,0-4-0.4-5.5-1.9l1.7-1.7c1.1,1.1,2.5,1.4,3.8,1.4c1.6,0,2.9-0.6,2.9-1.8c0-0.9-0.5-1.4-1.8-1.6l-2.1-0.2c-2.5-0.2-3.9-1.3-3.9-3.6c0-2.6,2.2-4,4.9-4c2,0,3.6,0.4,4.9,1.5l-1.7,1.7c-0.8-0.7-2-1-3.2-1C70.8,1,70,1.7,70,2.7c0,0.8,0.4,1.4,1.8,1.5l2.1,0.2c2.5,0.2,3.9,1.4,3.9,3.7C77.8,10.8,75.5,12.2,72.4,12.2L72.4,12.2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -21,13 +21,14 @@
// they'll create elements with the same class names we're using to
// find endpoints for updating settings and content. However, since
// only the portfolio's owner can set this content, it seems like
// the worst they can do is override endpoint urls for eportfolio
// settings on their own personal eportfolio, they can't
// the worst they can do is override endpoint urls for eportfolio
// settings on their own personal eportfolio, they can't
// affect anyone else
define([
'i18n!eportfolio',
'jquery' /* $ */,
'compiled/userSettings',
'jquery.ajaxJSON' /* ajaxJSON */,
'jquery.inst_tree' /* instTree */,
'jquery.instructure_forms' /* formSubmit, getFormData, formErrors, errorBox */,
@ -42,7 +43,7 @@ define([
'vendor/jquery.scrollTo' /* /\.scrollTo/ */,
'jqueryui/progressbar' /* /\.progressbar/ */,
'jqueryui/sortable' /* /\.sortable/ */
], function(I18n, $) {
], function(I18n, $, userSettings) {
var ePortfolioValidations = {
object_name: 'eportfolio',
@ -56,7 +57,7 @@ define([
function ePortfolioFormData() {
var data = $("#edit_page_form").getFormData({
object_name: "eportfolio_entry",
object_name: "eportfolio_entry",
values: ['eportfolio_entry[name]', 'eportfolio_entry[allow_comments]', 'eportfolio_entry[show_comments]']
});
var idx = 0;
@ -138,7 +139,7 @@ define([
sectionData.section_content = $.trim(sectionData.section_content);
var section_type = sectionData.section_type;
var edit_type = "edit_" + section_type + "_content";
var $edit = $("#edit_content_templates ." + edit_type).clone(true);
$section.append($edit.show());
if(edit_type == "edit_html_content") {
@ -196,7 +197,7 @@ define([
var section_type = $(this).getTemplateData({textValues: ['section_type']}).section_type;
if(section_type == "rich_text" || section_type == "html") {
var code = $(this).find(".edit_section").val();
if(section_type == "rich_text") {
if(section_type == "rich_text") {
code = $(this).find(".edit_section").editorBox('get_code');
}
$(this).find(".section_content").html($.raw(code));
@ -252,7 +253,7 @@ define([
}
var edit_type = "edit_" + section_type + "_content";
$section.fillTemplateData({
data: {section_type: section_type, section_type_name: section_type_name}
data: {section_type: section_type, section_type_name: section_type_name}
});
var $edit = $("#edit_content_templates ." + edit_type).clone(true);
$section.append($edit.show());
@ -324,11 +325,11 @@ define([
var $section = $(this).parents(".section")
var $message = $("#edit_content_templates").find(".uploading_file").clone();
var $upload = $(this).parents(".section").find(".file_upload");
if(!$upload.val() && $section.find(".file_list .leaf.active").length === 0) {
return;
}
$message.fillTemplateData({
data: {file_name: $upload.val()}
});
@ -483,7 +484,7 @@ define([
});
}).triggerHandler('change');
$.scrollSidebar();
$(".delete_comment_link").click(function(event) {
event.preventDefault();
$(this).parents(".comment").confirmDelete({
@ -521,7 +522,7 @@ define([
$(this).addClass('active');
if($(this).hasClass('file')) {
var id = $(this).getTemplateData({textValues: ['id']}).id;
}
}
});
@ -632,7 +633,7 @@ define([
$("#" + type + "_list .remove_page_link").css('display', '');
} else {
$("#" + type + "_list .remove_page_link").hide();
}
}
}
$(document).ready(function() {
countObjects('page');
@ -773,6 +774,79 @@ define([
});
});
var $wizard_box = $("#wizard_box");
function setWizardSpacerBoxDisplay(action){
$("#wizard_spacer_box").height($wizard_box.height() || 0).showIf(action === 'show');
}
var pathname = window.location.pathname;
$(".close_wizard_link").click(function(event) {
event.preventDefault();
userSettings.set('hide_wizard_' + pathname, true);
$wizard_box.slideUp('fast', function() {
$(".wizard_popup_link").slideDown('fast');
$('.wizard_popup_link').focus();
setWizardSpacerBoxDisplay('hide');
});
});
$(".wizard_popup_link").click(function(event) {
event.preventDefault();
$(".wizard_popup_link").slideUp('fast');
$wizard_box.slideDown('fast', function() {
$wizard_box.triggerHandler('wizard_opened');
$wizard_box.focus();
$([document, window]).triggerHandler('scroll');
});
});
$wizard_box.ifExists(function($wizard_box){
$wizard_box.bind('wizard_opened', function() {
var $wizard_options = $wizard_box.find(".wizard_options"),
height = $wizard_options.height();
$wizard_options.height(height);
$wizard_box.find(".wizard_details").css({
maxHeight: height - 5,
overflow: 'auto'
});
setWizardSpacerBoxDisplay('show');
});
$wizard_box.find(".wizard_options_list .option").click(function(event) {
var $this = $(this);
var $a = $(event.target).closest("a");
if($a.length > 0 && $a.attr('href') != "#") { return; }
event.preventDefault();
$this.parents(".wizard_options_list").find(".option.selected").removeClass('selected');
$this.addClass('selected');
var $details = $wizard_box.find(".wizard_details");
var data = $this.getTemplateData({textValues: ['header']});
data.link = data.header;
$details.fillTemplateData({
data: data
});
$details.find(".details").remove();
$details.find(".header").after($this.find(".details").clone(true).show());
var url = $this.find(".header").attr('href');
if(url != "#") {
$details.find(".link").show().attr('href', url);
} else {
$details.find(".link").hide();
}
$details.hide().fadeIn('fast');
});
setTimeout(function() {
if(!userSettings.get('hide_wizard_' + pathname)) {
$(".wizard_popup_link.auto_open:first").click();
}
}, 500);
});
$(document).ready(function() {
countObjects('section');
$(document).bind('section_deleted', function(event, data) {

View File

@ -845,76 +845,6 @@ define([
}
}
var $wizard_box = $("#wizard_box");
function setWizardSpacerBoxDispay(action){
$("#wizard_spacer_box").height($wizard_box.height() || 0).showIf(action === 'show');
}
var pathname = window.location.pathname;
$(".close_wizard_link").click(function(event) {
event.preventDefault();
userSettings.set('hide_wizard_' + pathname, true);
$wizard_box.slideUp('fast', function() {
$(".wizard_popup_link").slideDown('fast');
$('.wizard_popup_link').focus();
setWizardSpacerBoxDispay('hide');
});
});
$(".wizard_popup_link").click(function(event) {
event.preventDefault();
$(".wizard_popup_link").slideUp('fast');
$wizard_box.slideDown('fast', function() {
$wizard_box.triggerHandler('wizard_opened');
$wizard_box.focus();
$([document, window]).triggerHandler('scroll');
});
});
$wizard_box.ifExists(function($wizard_box){
$wizard_box.bind('wizard_opened', function() {
var $wizard_options = $wizard_box.find(".wizard_options"),
height = $wizard_options.height();
$wizard_options.height(height);
$wizard_box.find(".wizard_details").css({
maxHeight: height - 5,
overflow: 'auto'
});
setWizardSpacerBoxDispay('show');
});
$wizard_box.find(".wizard_options_list .option").click(function(event) {
var $this = $(this);
var $a = $(event.target).closest("a");
if($a.length > 0 && $a.attr('href') != "#") { return; }
event.preventDefault();
$this.parents(".wizard_options_list").find(".option.selected").removeClass('selected');
$this.addClass('selected');
var $details = $wizard_box.find(".wizard_details");
var data = $this.getTemplateData({textValues: ['header']});
data.link = data.header;
$details.fillTemplateData({
data: data
});
$details.find(".details").remove();
$details.find(".header").after($this.find(".details").clone(true).show());
var url = $this.find(".header").attr('href');
if(url != "#") {
$details.find(".link").show().attr('href', url);
} else {
$details.find(".link").hide();
}
$details.hide().fadeIn('fast');
});
setTimeout(function() {
if(!userSettings.get('hide_wizard_' + pathname)) {
$(".wizard_popup_link.auto_open:first").click();
}
}, 500);
});
// this is for things like the to-do, recent items and upcoming, it
// happend a lot so rather than duplicating it everywhere I stuck it here
$("#right-side").delegate(".more_link", "click", function(event) {

View File

@ -81,9 +81,9 @@ describe "courses" do
create_new_course
wizard_box = f("#wizard_box")
wizard_box = f(".ic-wizard-box")
keep_trying_until { expect(wizard_box).to be_displayed }
hover_and_click(".close_wizard_link")
hover_and_click(".ic-wizard-box__close a")
refresh_page
wait_for_ajaximations # we need to give the wizard a chance to pop up
@ -97,7 +97,7 @@ describe "courses" do
it "should open and close wizard after initial close" do
def find_wizard_box
wizard_box = keep_trying_until do
wizard_box = f("#wizard_box")
wizard_box = f(".ic-wizard-box")
expect(wizard_box).to be_displayed
wizard_box
end
@ -109,21 +109,36 @@ describe "courses" do
wait_for_ajaximations
wizard_box = find_wizard_box
hover_and_click(".close_wizard_link")
f(".ic-wizard-box__close a").click
wait_for_ajaximations
expect(wizard_box).not_to be_displayed
wizard_box = f(".ic-wizard-box")
expect(wizard_box).to eq nil
checklist_button = f('.wizard_popup_link')
expect(checklist_button).to be_displayed
checklist_button.click
wait_for_ajaximations
expect(checklist_button).not_to be_displayed
wizard_box = find_wizard_box
hover_and_click(".close_wizard_link")
f(".ic-wizard-box__close a").click
wait_for_ajaximations
expect(wizard_box).not_to be_displayed
wizard_box = f(".ic-wizard-box")
expect(wizard_box).to eq nil
expect(checklist_button).to be_displayed
end
it "should open up the choose home page dialog from the wizard" do
course_with_teacher_logged_in
create_new_course
wizard_box = f(".ic-wizard-box")
keep_trying_until { expect(wizard_box).to be_displayed }
f("#wizard_home_page").click
f(".ic-wizard-box__message-button a").click
wait_for_ajaximations
modal = f("#edit_course_home_content_form")
expect(modal).to be_displayed
end
it "should correctly update the course quota" do
course_with_admin_logged_in