From f237faf8fdfc0b66b7679f6a53a6938cad6a3e25 Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Thu, 27 Nov 2025 17:29:13 +0800 Subject: [PATCH 1/3] MDL-85858 mod_scorm: Set page and iframe titles for the SCORM player --- mod/scorm/module.js | 1 + mod/scorm/player.php | 24 +++++++++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/mod/scorm/module.js b/mod/scorm/module.js index ef0fff59658..48afdea15ff 100644 --- a/mod/scorm/module.js +++ b/mod/scorm/module.js @@ -192,6 +192,7 @@ M.mod_scorm.init = function(Y, nav_display, navposition_left, navposition_top, h obj.setAttribute('allowfullscreen', 'allowfullscreen'); obj.setAttribute('webkitallowfullscreen', 'webkitallowfullscreen'); obj.setAttribute('mozallowfullscreen', 'mozallowfullscreen'); + obj.setAttribute('title', toc_title); if (!window_name && node.title != null) { obj.setAttribute('src', url_prefix + node.title); } diff --git a/mod/scorm/player.php b/mod/scorm/player.php index 5962d310807..c5ae795d00c 100644 --- a/mod/scorm/player.php +++ b/mod/scorm/player.php @@ -106,12 +106,19 @@ $strexit = get_string('exitactivity', 'scorm'); $coursecontext = context_course::instance($course->id); +$scormname = format_string($scorm->name); +if ($mode === 'browse') { + $scormname = get_string('browsemodewithname', 'scorm', $scormname); +} +$titleparts = [ + $scormname, + format_string($course->shortname, true, ['context' => $coursecontext]), +]; +$PAGE->set_title(implode(moodle_page::TITLE_SEPARATOR, $titleparts)); + if ($displaymode == 'popup') { $PAGE->set_pagelayout('embedded'); } else { - $shortname = format_string($course->shortname, true, array('context' => $coursecontext)); - $pagetitle = strip_tags("$shortname: ".format_string($scorm->name)); - $PAGE->set_title($pagetitle); $PAGE->set_heading($course->fullname); } if (!$cm->visible and !has_capability('moodle/course:viewhiddenactivities', context_module::instance($cm->id))) { @@ -258,8 +265,15 @@ if ($result->prerequisites) { js_writer::function_call('scorm_openpopup', Array($url->out(false), $name, $scorm->options, $scorm->width, $scorm->height))); - echo html_writer::tag('noscript', html_writer::tag('iframe', '', array('id' => 'main', - 'class' => 'scoframe', 'name' => 'main', 'src' => 'loadSCO.php?id='.$cm->id.$scoidstr.$modestr))); + $frametitle = $result->toctitle ?? get_string('contents', 'scorm'); + $iframe = html_writer::tag('iframe', '', [ + 'id' => 'main', + 'class' => 'scoframe', + 'name' => 'main', + 'src' => 'loadSCO.php?id=' . $cm->id . $scoidstr . $modestr, + 'title' => $frametitle, + ]); + echo html_writer::tag('noscript', $iframe); } } else { echo $OUTPUT->box(get_string('noprerequisites', 'scorm')); From a485e9e12523e82367e0b77da2eb84cf22b80fa4 Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Thu, 27 Nov 2025 17:30:23 +0800 Subject: [PATCH 2/3] MDL-85858 mod_scorm: Use semantic heading for the Preview mode --- mod/scorm/lang/en/scorm.php | 1 + mod/scorm/player.php | 3 ++- mod/scorm/styles.css | 5 ----- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/mod/scorm/lang/en/scorm.php b/mod/scorm/lang/en/scorm.php index 6625753cc5c..84e475d3ab0 100644 --- a/mod/scorm/lang/en/scorm.php +++ b/mod/scorm/lang/en/scorm.php @@ -71,6 +71,7 @@ $string['badarchive'] = 'You must provide a valid zip file'; $string['browse'] = 'Preview'; $string['browsed'] = 'Browsed'; $string['browsemode'] = 'Preview mode'; +$string['browsemodewithname'] = 'Preview mode: {$a}'; $string['browserepository'] = 'Browse repository'; $string['cachedef_elements'] = 'Element cache'; $string['calculatedweight'] = 'Calculated weight'; diff --git a/mod/scorm/player.php b/mod/scorm/player.php index c5ae795d00c..28b5fe66056 100644 --- a/mod/scorm/player.php +++ b/mod/scorm/player.php @@ -228,7 +228,8 @@ echo html_writer::div(html_writer::tag('script', '', array('id' => 'external-sco if ($scorm->hidetoc == SCORM_TOC_POPUP or $mode == 'browse' or $mode == 'review') { echo html_writer::start_div('mb-3', array('id' => 'scormtop')); if ($mode == 'browse' || $mode == 'review') { - echo html_writer::div(get_string("{$mode}mode", 'scorm'), 'scorm-left h3', ['id' => 'scormmode']); + $headinglevel = $PAGE->activityheader->get_heading_level(); + echo $OUTPUT->heading(get_string("{$mode}mode", 'scorm'), $headinglevel, 'scorm-left', 'scormmode'); } if ($scorm->hidetoc == SCORM_TOC_POPUP) { echo html_writer::div($result->tocmenu, 'scorm-right', array('id' => 'scormnav')); diff --git a/mod/scorm/styles.css b/mod/scorm/styles.css index 7246048ff75..0a6950f7c6d 100644 --- a/mod/scorm/styles.css +++ b/mod/scorm/styles.css @@ -164,11 +164,6 @@ padding: 0; } -#page-mod-scorm-player #scormmode { - float: left; - border: 0; -} - #page-mod-scorm-player.pagelayout-popup #page-content .region-content { padding: 0; } From c6e762b2435042c996aeb5b7fee342d981198911 Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Thu, 27 Nov 2025 19:24:03 +0800 Subject: [PATCH 3/3] MDL-85858 mod_scorm: Fix Table of Contents toggle button's accessibility * Set proper label via `aria-label`. * Instead of changing the button's label to 'Show'/'Hide', set the button's `aria-expanded` attribute accordingly. * Remove unnecessary event handling for the toggle button when pressing Enter or Space key. This is already natively supported and triggers the click event. * Adjust Behat tests accordingly. --- mod/scorm/lang/en/scorm.php | 1 + mod/scorm/locallib.php | 11 ++++++++++- mod/scorm/module.js | 13 ++++++------- .../tests/behat/scorm_display_options.feature | 16 ++++++++-------- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/mod/scorm/lang/en/scorm.php b/mod/scorm/lang/en/scorm.php index 84e475d3ab0..b571eaeb2a5 100644 --- a/mod/scorm/lang/en/scorm.php +++ b/mod/scorm/lang/en/scorm.php @@ -422,6 +422,7 @@ $string['syntax'] = 'Syntax error'; $string['tag_error'] = 'Unknown tag ({$a->tag}) with this content: {$a->value}'; $string['time'] = 'Time'; $string['title'] = 'Title'; +$string['toggletableofcontents'] = 'Toggle table of contents'; $string['toolbar'] = 'Show the toolbar'; $string['too_many_attributes'] = 'Tag {$a->tag} has too many attributes'; $string['too_many_children'] = 'Tag {$a->tag} has too many children'; diff --git a/mod/scorm/locallib.php b/mod/scorm/locallib.php index 109e667a899..c27f232a4f7 100644 --- a/mod/scorm/locallib.php +++ b/mod/scorm/locallib.php @@ -2018,7 +2018,16 @@ function scorm_get_toc($user, $scorm, $cmid, $toclink=TOCJSLINK, $currentorg='', if ($tocheader) { $result->toc .= html_writer::end_div().html_writer::end_div(); $result->toc .= html_writer::start_div('loading', array('id' => 'scorm_toc_toggle')); - $result->toc .= html_writer::tag('button', '', array('id' => 'scorm_toc_toggle_btn')).html_writer::end_div(); + $toggletoclabel = get_string('toggletableofcontents', 'scorm'); + $toggletocbutton = html_writer::tag('button', '', [ + 'id' => 'scorm_toc_toggle_btn', + 'type' => 'button', + 'class' => 'btn btn-secondary btn-sm mb-1', + 'title' => $toggletoclabel, + 'aria-label' => $toggletoclabel, + 'aria-controls' => 'scorm_toc', + ]); + $result->toc .= $toggletocbutton . html_writer::end_div(); $result->toc .= html_writer::start_div('', array('id' => 'scorm_content')); $result->toc .= html_writer::div('', '', array('id' => 'scorm_navpanel')); $result->toc .= html_writer::end_div().html_writer::end_div(); diff --git a/mod/scorm/module.js b/mod/scorm/module.js index 48afdea15ff..52ab424ac40 100644 --- a/mod/scorm/module.js +++ b/mod/scorm/module.js @@ -293,14 +293,14 @@ M.mod_scorm.init = function(Y, nav_display, navposition_left, navposition_top, h toc.addClass(cssclasses.disabled) .setAttribute('disabled-by', 'screen-size'); scorm_toc_toggle_btn.setHTML('>') - .set('title', M.util.get_string('show', 'moodle')); + .set('aria-expanded', false); scorm_content.removeClass(cssclasses.scorm_grid_content_toc_visible) .addClass(cssclasses.scorm_grid_content_toc_hidden); } else if (body.get('winWidth') > collapsetocwinsize) { toc.removeClass(cssclasses.disabled) .removeAttribute('disabled-by'); scorm_toc_toggle_btn.setHTML('<') - .set('title', M.util.get_string('hide', 'moodle')); + .set('aria-expanded', true); scorm_content.removeClass(cssclasses.scorm_grid_content_toc_hidden) .addClass(cssclasses.scorm_grid_content_toc_visible); } @@ -310,14 +310,14 @@ M.mod_scorm.init = function(Y, nav_display, navposition_left, navposition_top, h toc.removeClass(cssclasses.disabled) .removeAttribute('disabled-by'); scorm_toc_toggle_btn.setHTML('<') - .set('title', M.util.get_string('hide', 'moodle')); + .set('aria-expanded', true); scorm_content.removeClass(cssclasses.scorm_grid_content_toc_hidden) .addClass(cssclasses.scorm_grid_content_toc_visible); } else { toc.addClass(cssclasses.disabled) .setAttribute('disabled-by', 'user'); scorm_toc_toggle_btn.setHTML('>') - .set('title', M.util.get_string('show', 'moodle')); + .set('aria-expanded', false); scorm_content.removeClass(cssclasses.scorm_grid_content_toc_visible) .addClass(cssclasses.scorm_grid_content_toc_hidden); } @@ -634,7 +634,7 @@ M.mod_scorm.init = function(Y, nav_display, navposition_left, navposition_top, h Y.one('#scorm_toc_toggle').addClass(cssclasses.scorm_grid_toggle); Y.one('#scorm_toc_toggle_btn') .setHTML('<') - .setAttribute('title', M.util.get_string('hide', 'moodle')); + .setAttribute('aria-expanded', true); Y.one('#scorm_content').addClass(cssclasses.scorm_grid_content_toc_visible); scorm_toggle_toc(true); } @@ -645,7 +645,7 @@ M.mod_scorm.init = function(Y, nav_display, navposition_left, navposition_top, h Y.one('#scorm_toc').addClass(cssclasses.disabled); Y.one('#scorm_toc_toggle_btn') .setHTML('>') - .setAttribute('title', M.util.get_string('show', 'moodle')); + .setAttribute('aria-expanded', false); Y.one('#scorm_content') .removeClass(cssclasses.scorm_grid_content_toc_visible) .addClass(cssclasses.scorm_grid_content_toc_hidden); @@ -861,7 +861,6 @@ M.mod_scorm.init = function(Y, nav_display, navposition_left, navposition_top, h // Collapse/expand TOC. Y.one('#scorm_toc_toggle').on('click', scorm_toggle_toc); - Y.one('#scorm_toc_toggle').on('key', scorm_toggle_toc, 'down:enter,32'); // fix layout if window resized Y.on("windowresize", function() { scorm_resize_layout(); diff --git a/mod/scorm/tests/behat/scorm_display_options.feature b/mod/scorm/tests/behat/scorm_display_options.feature index 7bd454ad0d7..eb7a01d5b14 100644 --- a/mod/scorm/tests/behat/scorm_display_options.feature +++ b/mod/scorm/tests/behat/scorm_display_options.feature @@ -29,20 +29,20 @@ Feature: Scorm display options When I press "Preview" # Confirm TOC display # Used css_element to check > and < button display in TOC since similar buttons also exist in navigation - Then I see "Golf Explained - Minimum Run-time Calls" - And "[title='Show']" "css_element" exist - And "[title='Hide']" "css_element" exist + Then I see "Golf Explained - Minimum Run-time Calls" in the "#scorm_toc" "css_element" + And "Toggle table of contents" "button" be visible # Confirm TOC dropdown display And "scoid" "select" exist # Confirm the navigation display And "[id='scorm_nav']" "css_element" exist Examples: - | toc | shownavigation | tocdisplay | showdisplay | hidedisplay | tocdropdown | navbar | - | 1 | | should not | should | should not | should not | should not | - | 2 | 1 | should | should | should not | should | should not | - | 0 | 0 | should | should not | should | should not | should not | - | 0 | 1 | should | should not | should | should not | should | + | toc | shownavigation | tocdisplay | toggletoc | tocdropdown | navbar | + | 1 | | should not | should | should not | should not | + | 2 | 1 | should not | should | should | should not | + | 0 | 0 | should | should | should not | should not | + | 0 | 1 | should | should | should not | should | + | 3 | 0 | should not | should not | should not | should not | Scenario: Student can exit Scorm activity in single activity course format Given the following "activities" exist: