diff --git a/js/TrickleButtonModel.js b/js/TrickleButtonModel.js index af5d21a..15fd7ee 100644 --- a/js/TrickleButtonModel.js +++ b/js/TrickleButtonModel.js @@ -132,18 +132,18 @@ export default class TrickleButtonModel extends ComponentModel { const text = (isDisabled && trickleConfig._button.disabledText) ? trickleConfig._button.disabledText : (isStart && trickleConfig._button.startText) ? - trickleConfig._button.startText : - (isFinal && trickleConfig._button.finalText) ? - trickleConfig._button.finalText : - trickleConfig._button.text; + trickleConfig._button.startText : + (isFinal && trickleConfig._button.finalText) ? + trickleConfig._button.finalText : + trickleConfig._button.text; const ariaLabel = (isDisabled && trickleConfig._button.disabledAriaLabel) ? - trickleConfig._button.disabledAriaLabel : + trickleConfig._button.disabledAriaLabel : (isStart && trickleConfig._button.startAriaLabel) ? - trickleConfig._button.startAriaLabel : - (isFinal && trickleConfig._button.finalAriaLabel) ? - trickleConfig._button.finalAriaLabel : - trickleConfig._button.ariaLabel; + trickleConfig._button.startAriaLabel : + (isFinal && trickleConfig._button.finalAriaLabel) ? + trickleConfig._button.finalAriaLabel : + trickleConfig._button.ariaLabel; this.set({ buttonText: text, diff --git a/js/TrickleButtonView.js b/js/TrickleButtonView.js index 97e39dc..8ce0857 100644 --- a/js/TrickleButtonView.js +++ b/js/TrickleButtonView.js @@ -1,11 +1,13 @@ import Adapt from 'core/js/adapt'; import a11y from 'core/js/a11y'; +import notify from 'core/js/notify'; import ComponentView from 'core/js/views/componentView'; import controller from './controller'; import { getModelConfig, getCompletionAttribute } from './models'; +import wait from 'core/js/wait'; /** @typedef {import('core/js/modelEvent').default} ModelEvent */ @@ -72,7 +74,7 @@ class TrickleButtonView extends ComponentView { this.$el.on('onscreen', this.tryButtonAutoHide); this.listenTo(Adapt, { 'popup:opened': this.onPopupOpened, - 'popup:closed': this.onPopupClosed + 'popup:closing': this.onPopupClosed }); const parentModel = this.model.getParent(); const completionAttribute = getCompletionAttribute(parentModel); @@ -100,6 +102,8 @@ class TrickleButtonView extends ComponentView { this.openPopupCount--; if (this.openPopupCount) return; if (this.isAwaitingPopupClose) { + this._isWaiting = true; + wait.begin(); // Had completed with an open popup, perform final part of finishing return this.finish(); } @@ -187,7 +191,7 @@ class TrickleButtonView extends ComponentView { async finish() { this.stopListening(Adapt, { 'popup:opened': this.onPopupOpened, - 'popup:closed': this.onPopupClosed + 'popup:closing': this.onPopupClosed }); this.updateButtonState(); const isStepLockingCompletionRequired = this.model.isStepLockingCompletionRequired(); @@ -202,10 +206,31 @@ class TrickleButtonView extends ComponentView { */ async continue() { const parent = this.model.getParent(); - await controller.continue(); + // Announce "Loading" concurrent with the load so the message plays during + // the load wait rather than after content is ready (which would race with + // the focus shift to the next component). + const announcePromise = this.announceContentLoaded(); + const childrenAdded = await controller.continue(); + await announcePromise; + if (this._isWaiting) { + this._isWaiting = false; + a11y.setPopupCloseTo(childrenAdded[0]?.$el); + wait.end(); + } await controller.scroll(parent); } + /** + * Announce a message to screenreaders letting them know that additional + * content has been loaded on the page. + */ + async announceContentLoaded() { + const globals = Adapt.course.get('_globals'); + const message = globals?._extensions?._trickle?.additionalContentLoaded; + if (!message) return; + await notify.read(message); + } + tryButtonAutoHide() { if (!this.model.get('_isButtonVisible')) return; const trickleConfig = getModelConfig(this.model.getParent()); diff --git a/js/controller.js b/js/controller.js index 890a270..9a70f65 100644 --- a/js/controller.js +++ b/js/controller.js @@ -115,8 +115,11 @@ class TrickleController extends Backbone.Controller { */ async continue() { applyLocks(); - await Adapt.parentView.addChildren(); + const addedChildren = await Adapt.parentView.addChildren({ + returnNewDescendants: true + }); await Adapt.parentView.whenReady(); + return addedChildren; } /** @@ -160,8 +163,8 @@ class TrickleController extends Backbone.Controller { let scrollToId = getScrollToId(); if (!scrollToId) { - logging.error(`Cannot scroll to the next id as none was found at id: "${fromModel.get('_id')}" with _scrollTo: "${trickleConfig._scrollTo}". Suggestion: Set _showEndOfPage to false.`) - return + logging.error(`Cannot scroll to the next id as none was found at id: "${fromModel.get('_id')}" with _scrollTo: "${trickleConfig._scrollTo}". Suggestion: Set _showEndOfPage to false.`); + return; } const isDescendant = Adapt.parentView.model.getAllDescendantModels().some(model => { diff --git a/migrations/v7.js b/migrations/v7.js index b078f1d..3e5e76a 100644 --- a/migrations/v7.js +++ b/migrations/v7.js @@ -1,4 +1,4 @@ -import { describe, whereContent, whereFromPlugin, mutateContent, checkContent, updatePlugin, testStopWhere, testSuccessWhere, getConfig } from 'adapt-migrations'; +import { describe, whereContent, whereFromPlugin, mutateContent, checkContent, updatePlugin, testStopWhere, testSuccessWhere, getConfig, getCourse } from 'adapt-migrations'; import _ from 'lodash'; describe('Trickle - v7.1.3 to v7.2.0', async () => { @@ -145,3 +145,54 @@ describe('Trickle - v7.5.0 to v7.5.1', async () => { fromPlugins: [{ name: 'adapt-contrib-trickle', version: '7.5.1' }] }); }); + +describe('Trickle - @@CURRENT_VERSION to @@RELEASE_VERSION', async () => { + // https://github.com/adaptlearning/adapt-contrib-trickle/compare/@@CURRENT_VERSION..@@RELEASE_VERSION + + let course, courseTrickleGlobals; + const additionalContentLoaded = 'Loading.'; + + whereFromPlugin('Trickle - from @@CURRENT_VERSION', { name: 'adapt-contrib-trickle', version: '<@@RELEASE_VERSION' }); + + whereContent('Trickle - where course is present', async (content) => { + course = getCourse(); + return course; + }); + + mutateContent('Trickle - add globals if missing', async (content) => { + if (!_.has(course, '_globals._extensions._trickle')) _.set(course, '_globals._extensions._trickle', {}); + courseTrickleGlobals = course._globals._extensions._trickle; + return true; + }); + + mutateContent('Trickle - add global attribute additionalContentLoaded', async (content) => { + if (!_.has(courseTrickleGlobals, 'additionalContentLoaded')) courseTrickleGlobals.additionalContentLoaded = additionalContentLoaded; + return true; + }); + + checkContent('Trickle - check global attribute additionalContentLoaded', async (content) => { + const isValid = _.has(courseTrickleGlobals, 'additionalContentLoaded'); + if (!isValid) throw new Error('Trickle - global attribute additionalContentLoaded'); + return true; + }); + + updatePlugin('Trickle - update to @@RELEASE_VERSION', { name: 'adapt-contrib-trickle', version: '@@RELEASE_VERSION', framework: '>=5.46.4' }); + + testSuccessWhere('trickle with course, no globals', { + fromPlugins: [{ name: 'adapt-contrib-trickle', version: '@@CURRENT_VERSION' }], + content: [ + { _type: 'course' } + ] + }); + + testSuccessWhere('trickle with course, with other globals', { + fromPlugins: [{ name: 'adapt-contrib-trickle', version: '@@CURRENT_VERSION' }], + content: [ + { _type: 'course', _globals: { _extensions: { _trickle: {} } } } + ] + }); + + testStopWhere('trickle incorrect version', { + fromPlugins: [{ name: 'adapt-contrib-trickle', version: '@@RELEASE_VERSION' }] + }); +}); diff --git a/properties.schema b/properties.schema index 2c59209..40de2a1 100644 --- a/properties.schema +++ b/properties.schema @@ -11,6 +11,16 @@ "inputType": "Text", "validators": [], "translatable": true + }, + "additionalContentLoaded": { + "type": "string", + "required": true, + "title": "Additional content loaded.", + "default": "Loading.", + "inputType": "Text", + "help": "Announced to screen readers when additional content is loaded. Recommend keeping this short.", + "validators": [], + "translatable": true } }, "properties": { diff --git a/schema/course.schema.json b/schema/course.schema.json index 77f86ba..a7cd6c8 100644 --- a/schema/course.schema.json +++ b/schema/course.schema.json @@ -28,6 +28,15 @@ "_adapt": { "translatable": true } + }, + "additionalContentLoaded": { + "type": "string", + "title": "Additional content loaded", + "default": "Loading.", + "description": "Announced to screen readers when additional content is loaded. Recommend keeping this short.", + "_adapt": { + "translatable": true + } } } }