Merge branch 'MDL-72569-master' of https://github.com/ferranrecio/moodle

This commit is contained in:
Andrew Nicols
2021-11-03 14:47:51 +08:00
24 changed files with 385 additions and 52 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+34
View File
@@ -607,6 +607,32 @@ define(
}
};
/**
* Get the focused element path in an activity if any.
*
* This method is used to restore focus when the activity HTML is refreshed.
* Only the main course editor elements can be refocused as they are always present
* even if the activity content changes.
*
* @param {String} id the element id the activity element
* @return {String|undefined} the inner path of the focused element or undefined
*/
const getActivityFocusedElement = function(id) {
const element = document.getElementById(id);
if (!element || !element.contains(document.activeElement)) {
return undefined;
}
// Check if the actions menu toggler is focused.
if (element.querySelector(SELECTOR.ACTIONAREA).contains(document.activeElement)) {
return `${SELECTOR.ACTIONAREA} [tabindex="0"]`;
}
// Return the current element id if any.
if (document.activeElement.id) {
return `#${document.activeElement.id}`;
}
return undefined;
};
/**
* Replaces the course module with the new html (used to update module after it was edited or its visibility was changed).
*
@@ -616,10 +642,18 @@ define(
$('<div>' + activityHTML + '</div>').find(SELECTOR.ACTIVITYLI).each(function() {
// Extract id from the new activity html.
var id = $(this).attr('id');
// Check if the current focused element is inside the activity.
let focusedPath = getActivityFocusedElement(id);
// Find the existing element with the same id and replace its contents with new html.
$(SELECTOR.ACTIVITYLI + '#' + id).replaceWith(activityHTML);
// Initialise action menu.
initActionMenu(id);
// Re-focus the previous elements.
if (focusedPath) {
const newItem = document.getElementById(id);
newItem.querySelector(focusedPath)?.focus();
}
});
};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
define ("core_courseformat/local/courseeditor/contenttree",["exports","jquery","core/tree","core/normalise"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=e(b);c=e(c);function e(a){return a&&a.__esModule?a:{default:a}}function f(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){f=function(a){return typeof a}}else{f=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return f(a)}function g(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function h(a,b){for(var c=0,d;c<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function i(a,b,c){if(b)h(a.prototype,b);if(c)h(a,c);return a}function j(a,b){if("function"!=typeof b&&null!==b){throw new TypeError("Super expression must either be null or a function")}a.prototype=Object.create(b&&b.prototype,{constructor:{value:a,writable:!0,configurable:!0}});if(b)k(a,b)}function k(a,b){k=Object.setPrototypeOf||function(a,b){a.__proto__=b;return a};return k(a,b)}function l(a){return function(){var b=p(a),c;if(o()){var d=p(this).constructor;c=Reflect.construct(b,arguments,d)}else{c=b.apply(this,arguments)}return m(this,c)}}function m(a,b){if(b&&("object"===f(b)||"function"==typeof b)){return b}return n(a)}function n(a){if(void 0===a){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return a}function o(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return!0}catch(a){return!1}}function p(a){p=Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||Object.getPrototypeOf(a)};return p(a)}var q=function(a){j(c,a);var e=l(c);function c(a,b,d){var f,h;g(this,c);h=e.call(this,a);h.selectors={SECTION:b.SECTION,TOGGLER:b.TOGGLER,COLLAPSE:b.COLLAPSE,ENTER:null!==(f=b.ENTER)&&void 0!==f?f:b.TOGGLER};if(d){h._getVisibleItems=h.getVisibleItems;h.getVisibleItems=function(){h.refreshVisibleItemsCache();return h._getVisibleItems()}}h.treeRoot.on("hidden.bs.collapse shown.bs.collapse",function(){h.refreshVisibleItemsCache()});h.registerEnterCallback(h.enterCallback.bind(n(h)));return h}i(c,[{key:"getActiveItem",value:function getActiveItem(){var a=this.treeRoot.data("activeItem");if(a){return(0,d.getList)(a)[0]}}},{key:"enterCallback",value:function enterCallback(a){var b=(0,d.getList)(a)[0];if(this.isGroupItem(a)){var c=b.querySelector(this.selectors.ENTER);if("#"!==c.getAttribute("href")){window.location.href=c.getAttribute("href")}c.click()}else{var e=b.querySelector("a");if("#"!==e.getAttribute("href")){window.location.href=e.getAttribute("href")}else{e.click()}}}},{key:"isGroupCollapsed",value:function isGroupCollapsed(a){var b=(0,d.getList)(a)[0],c=b.querySelector("[aria-expanded]");return"false"===c.getAttribute("aria-expanded")}},{key:"toggleGroup",value:function toggleGroup(a){var c,d=a.find(this.selectors.COLLAPSE),e=null!==(c=d.data("target"))&&void 0!==c?c:d.attr("href");if(!e){return}e=e.replace("#","");var f=(0,b.default)("#".concat(e));if(f.length){(0,b.default)("#".concat(e)).collapse("toggle")}}},{key:"expandGroup",value:function expandGroup(a){if(this.isGroupCollapsed(a)){this.toggleGroup(a)}}},{key:"collapseGroup",value:function collapseGroup(a){if(!this.isGroupCollapsed(a)){this.toggleGroup(a)}}},{key:"expandAllGroups",value:function expandAllGroups(){var a=this,c=(0,d.getList)(this.treeRoot)[0].querySelectorAll(this.selectors.SECTION);c.forEach(function(c){a.expandGroup((0,b.default)(c))})}}]);return c}(c.default);a.default=q;return a.default});
//# sourceMappingURL=contenttree.min.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
define ("core_courseformat/local/courseindex/keyboardnav",["exports","jquery","core/tree"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}function e(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){e=function(a){return typeof a}}else{e=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return e(a)}function f(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function g(a,b){for(var c=0,d;c<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function h(a,b,c){if(b)g(a.prototype,b);if(c)g(a,c);return a}function i(a,b){if("function"!=typeof b&&null!==b){throw new TypeError("Super expression must either be null or a function")}a.prototype=Object.create(b&&b.prototype,{constructor:{value:a,writable:!0,configurable:!0}});if(b)j(a,b)}function j(a,b){j=Object.setPrototypeOf||function(a,b){a.__proto__=b;return a};return j(a,b)}function k(a){return function(){var b=p(a),c;if(n()){var d=p(this).constructor;c=Reflect.construct(b,arguments,d)}else{c=b.apply(this,arguments)}return l(this,c)}}function l(a,b){if(b&&("object"===e(b)||"function"==typeof b)){return b}return m(a)}function m(a){if(void 0===a){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return a}function n(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return!0}catch(a){return!1}}function p(a){p=Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||Object.getPrototypeOf(a)};return p(a)}var q=function(a){i(c,a);var d=k(c);function c(a){var b;f(this,c);b=d.call(this,a.element);b.selectors=a.selectors;if(a.reactive.isEditing){b._getVisibleItems=b.getVisibleItems;b.getVisibleItems=function(){b.refreshVisibleItemsCache();return b._getVisibleItems()}}b.treeRoot.on("hidden.bs.collapse shown.bs.collapse",function(){b.refreshVisibleItemsCache()});b.registerEnterCallback(b.enterCallback.bind(m(b)));return b}h(c,[{key:"getActiveItem",value:function getActiveItem(){var a=this.treeRoot.data("activeItem");if(a){return a.get(0)}}},{key:"enterCallback",value:function enterCallback(a){if(this.isGroupItem(a)){window.location.href=a.find(this.selectors.TOGGLER).first().attr("href");a.find(this.selectors.TOGGLER).get(0).click()}else{window.location.href=a.find("a").first().attr("href")}}},{key:"isGroupCollapsed",value:function isGroupCollapsed(a){var b=a.find("[aria-expanded]");return"false"===b.attr("aria-expanded")}},{key:"toggleGroup",value:function toggleGroup(a){var c,d=a.find(this.selectors.COLLAPSE),e=null!==(c=d.data("target"))&&void 0!==c?c:d.attr("href");if(!e){return}e=e.replace("#","");(0,b.default)("#".concat(e)).collapse("toggle")}},{key:"expandGroup",value:function expandGroup(a){if(this.isGroupCollapsed(a)){this.toggleGroup(a)}}},{key:"collapseGroup",value:function collapseGroup(a){if(!this.isGroupCollapsed(a)){this.toggleGroup(a)}}},{key:"expandAllGroups",value:function expandAllGroups(){var a=this,c=this.treeRoot.find(this.selectors.SECTION);c.each(function(c,d){a.expandGroup((0,b.default)(d))})}}]);return c}(c.default);a.default=q;return a.default});
//# sourceMappingURL=keyboardnav.min.js.map
File diff suppressed because one or more lines are too long
+84 -5
View File
@@ -33,6 +33,10 @@ import {prefetchStrings} from 'core/prefetch';
import {get_string as getString} from 'core/str';
import {getList} from 'core/normalise';
import * as CourseEvents from 'core_course/events';
import Pending from 'core/pending';
import ContentTree from 'core_courseformat/local/courseeditor/contenttree';
// The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-79179 is integrated.
import jQuery from 'jquery';
// Load global strings.
prefetchStrings('core', ['movecoursesection', 'movecoursemodule', 'confirm', 'delete']);
@@ -48,11 +52,15 @@ export default class extends BaseComponent {
// Default query selectors.
this.selectors = {
ACTIONLINK: `[data-action]`,
// Move modal selectors.
SECTIONLINK: `[data-for='section']`,
CMLINK: `[data-for='cm']`,
SECTIONNODE: `[data-for='sectionnode']`,
TOGGLER: `[data-toggle='collapse']`,
MODALTOGGLER: `[data-toggle='collapse']`,
ADDSECTION: `[data-action='addSection']`,
CONTENTTREE: `#destination-selector`,
ACTIONMENU: `.action-menu`,
ACTIONMENUTOGGLER: `[data-toggle="dropdown"]`,
};
// Component css classes.
this.classes = {
@@ -145,6 +153,9 @@ export default class extends BaseComponent {
event.preventDefault();
// The section edit menu to refocus on end.
const editTools = this._getClosestActionMenuToogler(target);
// Collect section information from the state.
const exporter = this.reactive.getExporter();
const data = exporter.course(this.reactive.state);
@@ -170,6 +181,17 @@ export default class extends BaseComponent {
const generalSection = modalBody.querySelector(`${this.selectors.SECTIONLINK}[data-number='0']`);
this._disableLink(generalSection);
// Setup keyboard navigation.
new ContentTree(
modalBody.querySelector(this.selectors.CONTENTTREE),
{
SECTION: this.selectors.SECTIONNODE,
TOGGLER: this.selectors.MODALTOGGLER,
COLLAPSE: this.selectors.MODALTOGGLER,
},
true
);
// Capture click.
modalBody.addEventListener('click', (event) => {
const target = event.target;
@@ -181,7 +203,7 @@ export default class extends BaseComponent {
}
event.preventDefault();
this.reactive.dispatch('sectionMove', [sectionId], target.dataset.id);
modal.destroy();
this._destroyModal(modal, editTools);
});
}
@@ -201,6 +223,9 @@ export default class extends BaseComponent {
event.preventDefault();
// The section edit menu to refocus on end.
const editTools = this._getClosestActionMenuToogler(target);
// Collect section information from the state.
const exporter = this.reactive.getExporter();
const data = exporter.course(this.reactive.state);
@@ -224,8 +249,27 @@ export default class extends BaseComponent {
let currentElement = modalBody.querySelector(`${this.selectors.CMLINK}[data-id='${cmId}']`);
this._disableLink(currentElement);
// Open the cm section node if possible.
currentElement.closest(this.selectors.SECTIONNODE)?.querySelector(this.selectors.TOGGLER)?.click();
// Setup keyboard navigation.
new ContentTree(
modalBody.querySelector(this.selectors.CONTENTTREE),
{
SECTION: this.selectors.SECTIONNODE,
TOGGLER: this.selectors.MODALTOGGLER,
COLLAPSE: this.selectors.MODALTOGGLER,
ENTER: this.selectors.SECTIONLINK,
}
);
// Open the cm section node if possible (Bootstrap 4 uses jQuery to interact with collapsibles).
// All jQuery int this code can be replaced when MDL-79179 is integrated.
const sectionnode = currentElement.closest(this.selectors.SECTIONNODE);
const toggler = jQuery(sectionnode).find(this.selectors.MODALTOGGLER);
let collapsibleId = toggler.data('target') ?? toggler.attr('href');
if (collapsibleId) {
// We cannot be sure we have # in the id element name.
collapsibleId = collapsibleId.replace('#', '');
jQuery(`#${collapsibleId}`).collapse('toggle');
}
// Capture click.
modalBody.addEventListener('click', (event) => {
@@ -252,7 +296,7 @@ export default class extends BaseComponent {
}
this.reactive.dispatch('cmMove', [cmId], targetSectionId, targetCmId);
modal.destroy();
this._destroyModal(modal, editTools);
});
}
@@ -349,6 +393,7 @@ export default class extends BaseComponent {
_modalBodyRenderedPromise(modalParams) {
return new Promise((resolve, reject) => {
ModalFactory.create(modalParams).then((modal) => {
modal.setRemoveOnClose(true);
// Handle body loading event.
modal.getRoot().on(ModalEvents.bodyRendered, () => {
resolve(modal);
@@ -364,4 +409,38 @@ export default class extends BaseComponent {
});
});
}
/**
* Hide and later destroy a modal.
*
* Behat will fail if we remove the modal while some boostrap collapse is executing.
*
* @param {Modal} modal
* @param {HTMLElement} element the dom element to focus on.
*/
_destroyModal(modal, element) {
modal.hide();
const pendingDestroy = new Pending(`courseformat/actions:destroyModal`);
if (element) {
element.focus();
}
setTimeout(() =>{
modal.destroy();
pendingDestroy.resolve();
}, 500);
}
/**
* Get the closest actions menu toggler to an action element.
*
* @param {HTMLElement} element the action link element
* @returns {HTMLElement|undefined}
*/
_getClosestActionMenuToogler(element) {
const actionMenu = element.closest(this.selectors.ACTIONMENU);
if (!actionMenu) {
return undefined;
}
return actionMenu.querySelector(this.selectors.ACTIONMENUTOGGLER);
}
}
@@ -27,33 +27,41 @@
*/
// The core/tree uses jQuery to expand all nodes.
import $ from 'jquery';
import jQuery from 'jquery';
import Tree from 'core/tree';
import {getList} from 'core/normalise';
export default class extends Tree {
/**
* Setup the core/tree keyboard navigation.
*
* @param {CourseIndex} parent the parent component
* @param {Element|undefined} mainElement an alternative main element in case it is not from the parent component
* @param {Object|undefined} selectors alternative selectors
* @param {boolean} preventcache if the elements cache must be disabled.
*/
constructor(parent) {
constructor(mainElement, selectors, preventcache) {
// Init this value with the parent DOM element.
super(parent.element);
super(mainElement);
// Get selectors from parent.
this.selectors = parent.selectors;
this.selectors = {
SECTION: selectors.SECTION,
TOGGLER: selectors.TOGGLER,
COLLAPSE: selectors.COLLAPSE,
ENTER: selectors.ENTER ?? selectors.TOGGLER,
};
// The core/tree library saves the visible elements cache inside the main tree node.
// However, in edit mode content can change suddenly so we need to refresh caches when needed.
if (parent.reactive.isEditing) {
if (preventcache) {
this._getVisibleItems = this.getVisibleItems;
this.getVisibleItems = () => {
this.refreshVisibleItemsCache();
return this._getVisibleItems();
};
}
// Add jQuery events to detect boostrap collapse and uncollapse.
// All jQuery events can be replaced when MDL-79179 is integrated.
this.treeRoot.on('hidden.bs.collapse shown.bs.collapse', () => {
this.refreshVisibleItemsCache();
});
@@ -69,7 +77,7 @@ export default class extends Tree {
getActiveItem() {
const activeItem = this.treeRoot.data('activeItem');
if (activeItem) {
return activeItem.get(0);
return getList(activeItem)[0];
}
return undefined;
}
@@ -77,16 +85,25 @@ export default class extends Tree {
/**
* Handle enter key on a collpasible node.
*
* @param {JQuery} item the jQuery object
* @param {JQuery} jQueryItem the jQuery object
*/
enterCallback(item) {
if (this.isGroupItem(item)) {
enterCallback(jQueryItem) {
const item = getList(jQueryItem)[0];
if (this.isGroupItem(jQueryItem)) {
// Group elements is like clicking a topic but without loosing the focus.
window.location.href = item.find(this.selectors.TOGGLER).first().attr('href');
item.find(this.selectors.TOGGLER).get(0).click();
const enter = item.querySelector(this.selectors.ENTER);
if (enter.getAttribute('href') !== '#') {
window.location.href = enter.getAttribute('href');
}
enter.click();
} else {
// Activity links just follow the link href.
window.location.href = item.find('a').first().attr('href');
const link = item.querySelector('a');
if (link.getAttribute('href') !== '#') {
window.location.href = link.getAttribute('href');
} else {
link.click();
}
return;
}
}
@@ -94,12 +111,13 @@ export default class extends Tree {
/**
* Check if a gorup item is collapsed.
*
* @param {JQuery} item the jQuery object
* @param {JQuery} jQueryItem the jQuery object
* @returns {boolean} if the element is collapsed
*/
isGroupCollapsed(item) {
const toggler = item.find(`[aria-expanded]`);
return toggler.attr('aria-expanded') === 'false';
isGroupCollapsed(jQueryItem) {
const item = getList(jQueryItem)[0];
const toggler = item.querySelector(`[aria-expanded]`);
return toggler.getAttribute('aria-expanded') === 'false';
}
/**
@@ -108,6 +126,7 @@ export default class extends Tree {
* @param {JQuery} item the jQuery object
*/
toggleGroup(item) {
// All jQuery in this segment of code can be replaced when MDL-79179 is integrated.
const toggler = item.find(this.selectors.COLLAPSE);
let collapsibleId = toggler.data('target') ?? toggler.attr('href');
if (!collapsibleId) {
@@ -116,7 +135,10 @@ export default class extends Tree {
collapsibleId = collapsibleId.replace('#', '');
// Bootstrap 4 uses jQuery to interact with collapsibles.
$(`#${collapsibleId}`).collapse('toggle');
const collapsible = jQuery(`#${collapsibleId}`);
if (collapsible.length) {
jQuery(`#${collapsibleId}`).collapse('toggle');
}
}
/**
@@ -145,9 +167,9 @@ export default class extends Tree {
* Expand all groups.
*/
expandAllGroups() {
const togglers = this.treeRoot.find(this.selectors.SECTION);
togglers.each((index, item) => {
this.expandGroup($(item));
const togglers = getList(this.treeRoot)[0].querySelectorAll(this.selectors.SECTION);
togglers.forEach(item => {
this.expandGroup(jQuery(item));
});
}
}
@@ -25,7 +25,7 @@
import {BaseComponent} from 'core/reactive';
import {getCurrentCourseEditor} from 'core_courseformat/courseeditor';
import jQuery from 'jquery';
import TreeNav from 'core_courseformat/local/courseindex/keyboardnav';
import ContentTree from 'core_courseformat/local/courseeditor/contenttree';
export default class Component extends BaseComponent {
@@ -90,7 +90,7 @@ export default class Component extends BaseComponent {
});
// Configure Aria Tree.
this.treeNav = new TreeNav(this);
this.contentTree = new ContentTree(this.element, this.selectors, this.reactive.isEditing);
}
getWatchers() {
@@ -187,7 +187,7 @@ export default class Component extends BaseComponent {
// Course index is based on Bootstrap 4 collapsibles. To collapse them we need jQuery to
// interact with collapsibles methods. Hopefully, this will change in Bootstrap 5 because
// it does not require jQuery anymore.
// it does not require jQuery anymore (when MDL-79179 is integrated).
const togglerValue = (forceValue) ? 'show' : 'hide';
jQuery(collapsible).collapse(togglerValue);
}
@@ -77,9 +77,11 @@
}}
<p data-for="sectionname">{{#str}} movefull, moodle, {{cmname}} {{/str}}:</p>
<nav class="collapse-list" id="destination-selector">
<nav class="collapse-list" id="destination-selector" role="tree">
{{#sections}}
<div data-for="sectionnode">
<div data-for="sectionnode"
role="treeitem"
>
<div class="collapse-list-item d-flex"
id="movemodalsection{{number}}"
data-for="section_item"
@@ -101,7 +103,7 @@
<span class="sr-only">{{#str}} collapse, core {{/str}}</span>
</span>
</a>
<a href="{{{sectionurl}}}"
<a href="#"
class="collapse-list-link text-truncate"
data-for="section"
data-id="{{id}}"
@@ -113,12 +115,14 @@
</div>
<div id="movemodalcollapse{{number}}"
class="collapse-list-item-content collapse"
aria-labelledby="movemodalsection{{number}}">
aria-labelledby="movemodalsection{{number}}"
role="group"
>
<ul class="unlist" data-for="cmlist" data-id="{{id}}">
{{#cms}}
<li class="collapse-list-item d-flex">
<li class="collapse-list-item d-flex" role="treeitem">
<a class="collapse-list-link text-truncate"
href="{{{url}}}"
href="#"
data-for="cm"
data-id="{{id}}"
>
@@ -43,12 +43,13 @@
}}
<p data-for="sectionname">{{#str}} movefull, moodle, {{sectiontitle}} {{/str}}:</p>
<nav class="collapse-list" id="destination-selector">
<nav class="collapse-list" id="destination-selector" role="tree">
{{#sections}}
<div
class="collapse-list-item"
role="treeitem"
>
<a href="{{{sectionurl}}}"
<a href="#"
class="collapse-list-link text-truncate"
data-for="section"
data-id="{{id}}"
@@ -0,0 +1,121 @@
@core @core_course @core_courseformat
Feature: Move activity using keyboard
In order to move activities without a mouse
As a user
I need to select the activity destination with the keyboard.
Background:
Given the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| category | 0 |
| enablecompletion | 1 |
| numsections | 4 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 |
| book | Activity sample 2 | Test book description | C1 | sample2 | 2 |
| choice | Activity sample 3 | Test choice description | C1 | sample3 | 3 |
| choice | Other sample 3 | Test choice description | C1 | sample31 | 3 |
And I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
@javascript
Scenario: Move activity to another section selecting the section name
Given I open "Activity sample 3" actions menu
And I click on "Move" "link" in the "Activity sample 3" activity
# Focus on the modal content tree.
When I press the tab key
And I press the tab key
# Select the section 2.
And I press the down key
And I press the down key
And I press enter
Then I should see "Activity sample 3" in the "Topic 2" "section"
@javascript
Scenario: Move activity to another section selecting an inner activity
Given I open "Activity sample 3" actions menu
And I click on "Move" "link" in the "Activity sample 3" activity
# Focus on the modal content tree.
When I press the tab key
And I press the tab key
# Open section 2.
And I press the down key
And I press the down key
And I press the right key
# Select first activity.
And I press the down key
And I press enter
Then I should see "Activity sample 3" in the "Topic 2" "section"
@javascript
Scenario: Close a section in the move modal
Given I open "Activity sample 3" actions menu
And I click on "Move" "link" in the "Activity sample 3" activity
And I should see "Activity sample 3" in the ".modal-body" "css_element"
# Focus on the modal content tree.
When I press the tab key
And I press the tab key
# Close section 3.
And I press the down key
And I press the down key
And I press the down key
And I press the left key
# Move to section 4.
And I press the down key
And I press enter
Then I should see "Activity sample 3" in the "Topic 4" "section"
@javascript
Scenario: Move activity using open all sections
Given I open "Activity sample 3" actions menu
And I click on "Move" "link" in the "Activity sample 3" activity
And I should see "Activity sample 3" in the ".modal-body" "css_element"
# Focus on the modal content tree.
When I press the tab key
And I press the tab key
# Open all sections.
And I press the multiply key
# Move down to section 4
And I press the down key
And I press the down key
And I press the down key
And I press the down key
And I press the down key
And I press the down key
And I press the down key
And I press the down key
And I press enter
Then I should see "Activity sample 3" in the "Topic 4" "section"
@javascript
Scenario: Move activity using go to the last element
Given I open "Activity sample 3" actions menu
And I click on "Move" "link" in the "Activity sample 3" activity
# Focus on the modal content tree.
When I press the tab key
And I press the tab key
# Go to the last section.
And I press the end key
# Move down to section 4
And I press enter
Then I should see "Activity sample 3" in the "Topic 4" "section"
@javascript
Scenario: Move activity using go to the first element
Given I open "Activity sample 3" actions menu
And I click on "Move" "link" in the "Activity sample 3" activity
And I should see "Activity sample 3" in the ".modal-body" "css_element"
# Focus on the modal content tree.
When I press the tab key
And I press the tab key
# Move some sections down.
And I press the down key
And I press the down key
And I press the down key
# Go to the first section.
And I press the home key
# Move down to general section
And I press enter
Then I should see "Activity sample 3" in the "General" "section"
@@ -0,0 +1,65 @@
@core @core_course @core_courseformat
Feature: Move a section using keyboard
In order to move sections without a mouse
As a user
I need to select the section destination with the keyboard.
Background:
Given the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| category | 0 |
| enablecompletion | 1 |
| numsections | 4 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 |
| book | Activity sample 2 | Test book description | C1 | sample2 | 2 |
| choice | Activity sample 3 | Test choice description | C1 | sample3 | 3 |
| choice | Other sample 3 | Test choice description | C1 | sample31 | 3 |
And I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
@javascript
Scenario: Move section above another section
Given I open section "3" edit menu
And I click on "Move" "link" in the "Topic 3" "section"
# Focus on the modal content tree.
When I press the tab key
And I press the tab key
# Select the section 2.
And I press the down key
And I press the down key
And I press enter
Then I should see "Activity sample 3" in the "Topic 2" "section"
@javascript
Scenario: Move section using go to the last element
Given I open section "2" edit menu
And I click on "Move" "link" in the "Topic 2" "section"
# Focus on the modal content tree.
When I press the tab key
And I press the tab key
# Go to the last section.
And I press the end key
# Move down to section 4
And I press enter
Then I should see "Activity sample 2" in the "Topic 4" "section"
@javascript
Scenario: Move section using go to the first element
Given I open section "3" edit menu
And I click on "Move" "link" in the "Topic 3" "section"
# Focus on the modal content tree.
When I press the tab key
And I press the tab key
# Move some sections down.
And I press the down key
And I press the down key
And I press the down key
# Go to the first section.
And I press the home key
# Move down to Topic 1
And I press the down key
And I press enter
Then I should see "Activity sample 3" in the "Topic 1" "section"
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7
View File
@@ -288,7 +288,14 @@ export default class {
}
const listener = (event) => {
// Prevent any watcher from losing the page focus.
const currentFocus = document.activeElement;
// Execute watcher.
handler.apply(component, [event.detail]);
// Restore focus in case it is lost.
if (document.activeElement === document.body && document.body.contains(currentFocus)) {
currentFocus.focus();
}
};
// Save the listener information in case the component must be unregistered later.
+1 -1
View File
@@ -355,7 +355,7 @@ define(['jquery'], function($) {
// eslint-disable-next-line complexity
Tree.prototype.handleKeyDown = function(e) {
var item = $(e.target);
var currentIndex = this.getVisibleItems().index(item);
var currentIndex = this.getVisibleItems()?.index(item);
if ((e.altKey || e.ctrlKey || e.metaKey) || (e.shiftKey && e.keyCode != this.keys.tab)) {
// Do nothing.