From 0e71c4ff616c37b2836905bc8f46c6a4ed450a53 Mon Sep 17 00:00:00 2001 From: Clay Diffrient Date: Tue, 9 Dec 2014 14:18:52 -0700 Subject: [PATCH] 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 Reviewed-by: Dan Minkevitch QA-Review: Clare Strong Product-Review: Clay Diffrient --- .../bundles/course_wizard.coffee | 34 +++ app/controllers/courses_controller.rb | 37 +++ app/jsx/course_wizard/Checklist.jsx | 51 ++++ app/jsx/course_wizard/ChecklistItem.jsx | 51 ++++ app/jsx/course_wizard/CourseWizard.jsx | 102 +++++++ app/jsx/course_wizard/InfoFrame.jsx | 150 +++++++++ app/jsx/course_wizard/ListItems.jsx | 82 +++++ app/stylesheets/base/mixins/_breakpoints.scss | 3 + .../components/_ic-expand-link.scss | 11 +- .../pages/course_wizard/_CourseWizard.scss | 0 .../course_wizard/compiler-course_wizard.scss | 288 ++++++++++++++++++ app/views/courses/show.html.erb | 30 +- config/assets_real.yml | 2 + public/images/canvas-logo.svg | 20 ++ public/javascripts/eportfolio.js | 98 +++++- public/javascripts/instructure.js | 70 ----- spec/selenium/courses_spec.rb | 31 +- 17 files changed, 962 insertions(+), 98 deletions(-) create mode 100644 app/coffeescripts/bundles/course_wizard.coffee create mode 100644 app/jsx/course_wizard/Checklist.jsx create mode 100644 app/jsx/course_wizard/ChecklistItem.jsx create mode 100644 app/jsx/course_wizard/CourseWizard.jsx create mode 100644 app/jsx/course_wizard/InfoFrame.jsx create mode 100644 app/jsx/course_wizard/ListItems.jsx create mode 100644 app/stylesheets/pages/course_wizard/_CourseWizard.scss create mode 100644 app/stylesheets/pages/course_wizard/compiler-course_wizard.scss create mode 100644 public/images/canvas-logo.svg diff --git a/app/coffeescripts/bundles/course_wizard.coffee b/app/coffeescripts/bundles/course_wizard.coffee new file mode 100644 index 00000000000..ad3ac7fcc97 --- /dev/null +++ b/app/coffeescripts/bundles/course_wizard.coffee @@ -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) \ No newline at end of file diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index d6a8ab45eaa..1235a8241dd 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -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 diff --git a/app/jsx/course_wizard/Checklist.jsx b/app/jsx/course_wizard/Checklist.jsx new file mode 100644 index 00000000000..27233f828d3 --- /dev/null +++ b/app/jsx/course_wizard/Checklist.jsx @@ -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 ( + + ); + }); + }, + + render: function () { + var checklist = this.renderChecklist(); + return ( +
{checklist}
+ ); + } + + }); + + return Checklist; + +}); \ No newline at end of file diff --git a/app/jsx/course_wizard/ChecklistItem.jsx b/app/jsx/course_wizard/ChecklistItem.jsx new file mode 100644 index 00000000000..28053679805 --- /dev/null +++ b/app/jsx/course_wizard/ChecklistItem.jsx @@ -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 ( + + {this.props.title} + + ); + } + + }); + + return ChecklistItem; + +}); \ No newline at end of file diff --git a/app/jsx/course_wizard/CourseWizard.jsx b/app/jsx/course_wizard/CourseWizard.jsx new file mode 100644 index 00000000000..c9946a05daa --- /dev/null +++ b/app/jsx/course_wizard/CourseWizard.jsx @@ -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 ( + + + + ); + } + }); + + return CourseWizard; + +}); \ No newline at end of file diff --git a/app/jsx/course_wizard/InfoFrame.jsx b/app/jsx/course_wizard/InfoFrame.jsx new file mode 100644 index 00000000000..51aa8c2c53a --- /dev/null +++ b/app/jsx/course_wizard/InfoFrame.jsx @@ -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 ( + {this.state.itemShown.title} + + ); + } + if (this.state.itemShown.key === 'publish_course') { + return ( +
+ + + + + +
+ ); + } + if (this.state.itemShown.hasOwnProperty('title')) { + return ( + + {this.state.itemShown.title} + + ); + } + else if (this.state.itemShown.hasOwnProperty('warning')) { + return {this.state.itemShown.warning} + } + else { + return null; + } + }, + + render: function () { + return ( +
+

+ {I18n.t("Next Steps")} +

+
+
+
+ +
+
+

+ {this.state.itemShown.text} +

+
+ {this.renderButton()} +
+
+
+
+
+ ); + } + }); + + return InfoFrame; + +}); \ No newline at end of file diff --git a/app/jsx/course_wizard/ListItems.jsx b/app/jsx/course_wizard/ListItems.jsx new file mode 100644 index 00000000000..eee00d262cd --- /dev/null +++ b/app/jsx/course_wizard/ListItems.jsx @@ -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' + } + ] +}) + + diff --git a/app/stylesheets/base/mixins/_breakpoints.scss b/app/stylesheets/base/mixins/_breakpoints.scss index 29275c132cf..c500b5f0acf 100644 --- a/app/stylesheets/base/mixins/_breakpoints.scss +++ b/app/stylesheets/base/mixins/_breakpoints.scss @@ -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; } + } } \ No newline at end of file diff --git a/app/stylesheets/components/_ic-expand-link.scss b/app/stylesheets/components/_ic-expand-link.scss index 3fbe5da43d8..4e24dc81325 100644 --- a/app/stylesheets/components/_ic-expand-link.scss +++ b/app/stylesheets/components/_ic-expand-link.scss @@ -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; } diff --git a/app/stylesheets/pages/course_wizard/_CourseWizard.scss b/app/stylesheets/pages/course_wizard/_CourseWizard.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/stylesheets/pages/course_wizard/compiler-course_wizard.scss b/app/stylesheets/pages/course_wizard/compiler-course_wizard.scss new file mode 100644 index 00000000000..e7b72b2368f --- /dev/null +++ b/app/stylesheets/pages/course_wizard/compiler-course_wizard.scss @@ -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"; + diff --git a/app/views/courses/show.html.erb b/app/views/courses/show.html.erb index 0358271b171..72d6c9030b5 100644 --- a/app/views/courses/show.html.erb +++ b/app/views/courses/show.html.erb @@ -56,7 +56,7 @@ <% end %> <% if @can_manage_content %> - + <%= t('links.choose_home_page', %{Choose Home Page}) %> @@ -69,15 +69,35 @@ <% end %> - <% if @can_manage_content %> - - <%= t('links.course_setup', %{Course Setup Checklist}) %> - + <% if @can_manage_content %> + <% js_bundle :course_wizard %> + <% jammit_css :course_wizard %> + + <%= t('links.course_setup', %{Course Setup Checklist}) %> + <%= t('links.new_announcement', %{New Announcement}) %> <% end %> + <% else %> + <% if @can_manage_content || @course_home_sub_navigation_tools.present? %> +
+ <% @course_home_sub_navigation_tools.each do |tool| %> + + + <%= tool.label_for(:course_home_sub_navigation) %> + + <% end %> + <% if @can_manage_content %> + <% js_bundle :course_wizard %> + <% jammit_css :course_wizard %> + <%= t('links.course_setup', %{Course Setup Checklist}) %> + <%= t('links.new_announcement', %{New Announcement}) %> + <% end %> +
+ <% end %> <% end %> <% if @context.available? && @context.self_enrollment_enabled? && @context.open_enrollment && (!@context_enrollment || !@context_enrollment.active?) %> diff --git a/config/assets_real.yml b/config/assets_real.yml index 97381de3993..bc6538de83a 100644 --- a/config/assets_real.yml +++ b/config/assets_real.yml @@ -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: diff --git a/public/images/canvas-logo.svg b/public/images/canvas-logo.svg new file mode 100644 index 00000000000..f01ceaffe0a --- /dev/null +++ b/public/images/canvas-logo.svg @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/public/javascripts/eportfolio.js b/public/javascripts/eportfolio.js index b8782fe7773..8c715dc4ded 100644 --- a/public/javascripts/eportfolio.js +++ b/public/javascripts/eportfolio.js @@ -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) { diff --git a/public/javascripts/instructure.js b/public/javascripts/instructure.js index 8b0cff92d20..3b9c6cdc164 100644 --- a/public/javascripts/instructure.js +++ b/public/javascripts/instructure.js @@ -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) { diff --git a/spec/selenium/courses_spec.rb b/spec/selenium/courses_spec.rb index a22c5b47a29..6316c0b80fe 100644 --- a/spec/selenium/courses_spec.rb +++ b/spec/selenium/courses_spec.rb @@ -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