From 90c04564016c86a7414d7a73ee7023b0ea95df13 Mon Sep 17 00:00:00 2001 From: Brad Simpson Date: Mon, 16 Feb 2026 13:44:02 -0700 Subject: [PATCH 01/13] Move annotations into notify container when necessary --- js/helpers.js | 55 ++++++++++++++++++++++++++++++------------- js/toggle-alt-text.js | 39 ++++++++++++++++++++++++++---- 2 files changed, 72 insertions(+), 22 deletions(-) diff --git a/js/helpers.js b/js/helpers.js index 005ff93..f31805f 100644 --- a/js/helpers.js +++ b/js/helpers.js @@ -65,28 +65,44 @@ function computeAccessibleDescription($element) { return ''; } -function getAnnotationPosition($element, $annotation) { +function getAnnotationPosition($element, $annotation, isInOverlay = false) { const targetBoundingRect = $element[0].getBoundingClientRect(); - const availableWidth = $('html')[0].clientWidth; - const availableHeight = $('html')[0].clientHeight; + let availableWidth = $('html')[0].clientWidth; + let availableHeight = $('html')[0].clientHeight; const tooltipsWidth = $annotation.width(); const tooltipsHeight = $annotation.height(); const elementWidth = $element.width(); const elementHeight = $element.height(); + const isFixedPosition = Boolean($element.parents().add($element).filter((index, el) => $(el).css('position') === 'fixed').length); + const scrollOffsetTop = isFixedPosition ? 0 : $(window).scrollTop(); + const scrollOffsetLeft = isFixedPosition ? 0 : $(window).scrollLeft(); + + // For overlay annotations, calculate position relative to overlay container + let overlayOffsetTop = 0; + let overlayOffsetLeft = 0; + if (isInOverlay) { + const $overlay = $element.closest('.notify, .drawer, dialog'); + if ($overlay.length) { + const overlayRect = $overlay[0].getBoundingClientRect(); + overlayOffsetTop = overlayRect.top; + overlayOffsetLeft = overlayRect.left; + // Use overlay dimensions instead of viewport for boundary checks + availableWidth = overlayRect.right; + availableHeight = overlayRect.bottom; + } + } + const canAlignBottom = targetBoundingRect.bottom + tooltipsHeight < availableHeight; const canAlignRight = targetBoundingRect.right + tooltipsWidth < availableWidth; const canAlignBottomRight = canAlignBottom && canAlignRight; const canBeContained = elementHeight === 0 || (elementHeight * elementWidth >= tooltipsHeight * tooltipsWidth) || $element.is('img'); - const isFixedPosition = Boolean($element.parents().add($element).filter((index, el) => $(el).css('position') === 'fixed').length); - const scrollOffsetTop = isFixedPosition ? 0 : $(window).scrollTop(); - const scrollOffsetLeft = isFixedPosition ? 0 : $(window).scrollLeft(); function getPosition() { if (canBeContained) { return { className: 'is-contained', css: { - left: targetBoundingRect.left + scrollOffsetLeft, - top: targetBoundingRect.top + scrollOffsetTop, + left: targetBoundingRect.left + scrollOffsetLeft - overlayOffsetLeft, + top: targetBoundingRect.top + scrollOffsetTop - overlayOffsetTop, 'max-width': (elementHeight === 0) ? '' : elementWidth } }; @@ -100,8 +116,8 @@ function getAnnotationPosition($element, $annotation) { return { className: 'is-left is-top', css: { - left: targetBoundingRect.left - tooltipsWidth + scrollOffsetLeft, - top: targetBoundingRect.top - tooltipsHeight + scrollOffsetTop, + left: targetBoundingRect.left - tooltipsWidth + scrollOffsetLeft - overlayOffsetLeft, + top: targetBoundingRect.top - tooltipsHeight + scrollOffsetTop - overlayOffsetTop, 'max-width': '' } }; @@ -111,8 +127,8 @@ function getAnnotationPosition($element, $annotation) { return { className: 'is-right is-top', css: { - left: targetBoundingRect.right + scrollOffsetLeft, - top: targetBoundingRect.top - tooltipsHeight + scrollOffsetTop, + left: targetBoundingRect.right + scrollOffsetLeft - overlayOffsetLeft, + top: targetBoundingRect.top - tooltipsHeight + scrollOffsetTop - overlayOffsetTop, 'max-width': '' } }; @@ -122,8 +138,8 @@ function getAnnotationPosition($element, $annotation) { return { className: 'is-left is-bottom', css: { - left: targetBoundingRect.left - tooltipsWidth + scrollOffsetLeft, - top: targetBoundingRect.bottom + scrollOffsetTop, + left: targetBoundingRect.left - tooltipsWidth + scrollOffsetLeft - overlayOffsetLeft, + top: targetBoundingRect.bottom + scrollOffsetTop - overlayOffsetTop, 'max-width': '' } }; @@ -133,14 +149,19 @@ function getAnnotationPosition($element, $annotation) { return { className: 'is-right, is-bottom', css: { - left: targetBoundingRect.right + scrollOffsetLeft, - top: targetBoundingRect.bottom + scrollOffsetTop, + left: targetBoundingRect.right + scrollOffsetLeft - overlayOffsetLeft, + top: targetBoundingRect.bottom + scrollOffsetTop - overlayOffsetTop, 'max-width': '' } }; } const position = getPosition(); - position.css.position = isFixedPosition ? 'fixed' : 'absolute'; + // Use fixed for fixed-position elements, absolute for overlays and normal elements + if (isInOverlay) { + position.css.position = 'absolute'; + } else { + position.css.position = isFixedPosition ? 'fixed' : 'absolute'; + } if (position.css.left < 0) position.css.left = 0; position.css.left += 'px'; position.css.top += 'px'; diff --git a/js/toggle-alt-text.js b/js/toggle-alt-text.js index 6efe00c..c345476 100644 --- a/js/toggle-alt-text.js +++ b/js/toggle-alt-text.js @@ -19,6 +19,7 @@ class Annotation extends Backbone.View { initialize(options) { this.$parent = options.$parent; this.allowText = options.allowText; + this.isInOverlay = options.isInOverlay || false; this.$el.data('annotating', this.$parent); this.$el.data('view', this); } @@ -29,7 +30,7 @@ class Annotation extends Backbone.View { const description = computeAccessibleDescription(this.$parent); this.$el.html(template({ name, description })); if (!name) this.$el.addClass('has-annotation-warning'); - const position = getAnnotationPosition(this.$parent, this.$el); + const position = getAnnotationPosition(this.$parent, this.$el, this.isInOverlay); this.$el.css(position.css); this.$el.removeClass('is-top is-left is-right is-bottom is-contained'); this.$el.addClass(position.className); @@ -89,6 +90,7 @@ class AltText extends Backbone.Controller { }); } this.listenTo(Adapt, { + 'notify:opened drawer:opened drawer:openedCustomView': this.onOverlayOpened, 'popup:closed notify:closed drawer:closed': this.onDomMutation, remove: this.removeAllAnnotations }); @@ -124,6 +126,7 @@ class AltText extends Backbone.Controller { if (this.observer) { this.observer.disconnect(); } + this.stopListening(Adapt, 'notify:opened drawer:opened drawer:openedCustomView', this.onOverlayOpened); this.stopListening(Adapt, 'popup:closed notify:closed drawer:closed', this.onDomMutation); $(window).off('scroll', this.onDomMutation); $(document).off('mouseover', '*', this.onMouseOver); @@ -141,8 +144,24 @@ class AltText extends Backbone.Controller { } addAnnotation($element, allowText) { - const annotation = new Annotation({ $parent: $element, allowText }); - $('.devtools__annotations').append(annotation.$el); + const $overlay = $element.closest('.notify, .drawer, dialog'); + const isInOverlay = $overlay.length > 0; + + const annotation = new Annotation({ + $parent: $element, + allowText, + isInOverlay + }); + + // Check if element is inside an overlay (notify, drawer, dialog) + if (isInOverlay) { + // Append to overlay to be in same stacking context + $overlay.append(annotation.$el); + } else { + // Append to global container for main page content + $('.devtools__annotations').append(annotation.$el); + } + $element.data('annotation', annotation); $element.attr('data-annotated', true); this.updateAnnotation($element, annotation, allowText); @@ -172,7 +191,8 @@ class AltText extends Backbone.Controller { if (!$element) return; const isOutOfDom = ($element.parents('html').length === 0); const isHeadingHeightZero = $element.is('h1,h2,h3,h4,h5,h6,h7,[role=heading]') && $element.height() === 0; - if (!isOutOfDom && ($element.onscreen().onscreen || isHeadingHeightZero)) return; + const isInOverlay = $element.closest('.notify, .drawer, dialog').length > 0; + if (!isOutOfDom && ($element.onscreen().onscreen || isHeadingHeightZero || isInOverlay)) return; this.removeAnnotation($element, annotation); }); } @@ -187,6 +207,14 @@ class AltText extends Backbone.Controller { this.mutated = true; } + onOverlayOpened() { + // Wait for overlay DOM to fully render before annotating + setTimeout(() => { + this.mutated = false; + this.onDomMutation(); + }, 100); + } + render() { if (this.mutated === false) return; this.clearUpAnnotations(); @@ -217,7 +245,8 @@ class AltText extends Backbone.Controller { const allowText = $element.is('.aria-label,h1,h2,h3,h4,h5,h6,h7,[role=heading]'); const isOutOfDom = ($element.parents('html').length === 0); const isHeadingHeightZero = $element.is('h1,h2,h3,h4,h5,h6,h7,[role=heading]') && $element.height() === 0; - if (!isOutOfDom && (isVisible || isHeadingHeightZero) && (isNotAriaHidden || (!isAriaHidden && !isParentAriaHidden) || (isImg && !isParentAriaHidden))) { + const isInOverlay = $element.closest('.notify, .drawer, dialog').length > 0; + if (!isOutOfDom && (isVisible || isHeadingHeightZero || isInOverlay) && (isNotAriaHidden || (!isAriaHidden && !isParentAriaHidden) || (isImg && !isParentAriaHidden))) { if (!annotation) this.addAnnotation($element, allowText); else this.updateAnnotation($element, annotation, allowText); } else if (annotation) { From 67b6550f3b1a6991e7b26d88c7e9897191c8794a Mon Sep 17 00:00:00 2001 From: Brad Simpson Date: Mon, 16 Feb 2026 14:04:49 -0700 Subject: [PATCH 02/13] Refactor --- js/helpers.js | 7 +++++-- js/toggle-alt-text.js | 29 +++++++++++------------------ 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/js/helpers.js b/js/helpers.js index f31805f..8657a3e 100644 --- a/js/helpers.js +++ b/js/helpers.js @@ -1,3 +1,5 @@ +const OVERLAY_SELECTOR = '.notify, .drawer, dialog'; + function findLabel($element) { const id = $element.attr('id'); if (!id) return false; @@ -81,7 +83,7 @@ function getAnnotationPosition($element, $annotation, isInOverlay = false) { let overlayOffsetTop = 0; let overlayOffsetLeft = 0; if (isInOverlay) { - const $overlay = $element.closest('.notify, .drawer, dialog'); + const $overlay = $element.closest(OVERLAY_SELECTOR); if ($overlay.length) { const overlayRect = $overlay[0].getBoundingClientRect(); overlayOffsetTop = overlayRect.top; @@ -147,7 +149,7 @@ function getAnnotationPosition($element, $annotation, isInOverlay = false) { } // Bottom right, default return { - className: 'is-right, is-bottom', + className: 'is-right is-bottom', css: { left: targetBoundingRect.right + scrollOffsetLeft - overlayOffsetLeft, top: targetBoundingRect.bottom + scrollOffsetTop - overlayOffsetTop, @@ -170,6 +172,7 @@ function getAnnotationPosition($element, $annotation, isInOverlay = false) { } export default { + OVERLAY_SELECTOR, computeAccesibleName, computeAccessibleDescription, computeHeadingLevel, diff --git a/js/toggle-alt-text.js b/js/toggle-alt-text.js index c345476..7731eaa 100644 --- a/js/toggle-alt-text.js +++ b/js/toggle-alt-text.js @@ -2,9 +2,7 @@ import Backbone from 'backbone'; import Adapt from 'core/js/adapt'; import helpers from './helpers'; -const computeAccesibleName = helpers.computeAccesibleName; -const computeAccessibleDescription = helpers.computeAccessibleDescription; -const getAnnotationPosition = helpers.getAnnotationPosition; +const { OVERLAY_SELECTOR, computeAccesibleName, computeAccessibleDescription, getAnnotationPosition } = helpers; class Annotation extends Backbone.View { @@ -128,6 +126,7 @@ class AltText extends Backbone.Controller { } this.stopListening(Adapt, 'notify:opened drawer:opened drawer:openedCustomView', this.onOverlayOpened); this.stopListening(Adapt, 'popup:closed notify:closed drawer:closed', this.onDomMutation); + this.stopListening(Adapt, 'remove', this.removeAllAnnotations); $(window).off('scroll', this.onDomMutation); $(document).off('mouseover', '*', this.onMouseOver); } @@ -143,22 +142,16 @@ class AltText extends Backbone.Controller { this.connectObserver(); } - addAnnotation($element, allowText) { - const $overlay = $element.closest('.notify, .drawer, dialog'); - const isInOverlay = $overlay.length > 0; - + addAnnotation($element, allowText, isInOverlay) { const annotation = new Annotation({ $parent: $element, allowText, isInOverlay }); - // Check if element is inside an overlay (notify, drawer, dialog) if (isInOverlay) { - // Append to overlay to be in same stacking context - $overlay.append(annotation.$el); + $element.closest(OVERLAY_SELECTOR).append(annotation.$el); } else { - // Append to global container for main page content $('.devtools__annotations').append(annotation.$el); } @@ -191,8 +184,7 @@ class AltText extends Backbone.Controller { if (!$element) return; const isOutOfDom = ($element.parents('html').length === 0); const isHeadingHeightZero = $element.is('h1,h2,h3,h4,h5,h6,h7,[role=heading]') && $element.height() === 0; - const isInOverlay = $element.closest('.notify, .drawer, dialog').length > 0; - if (!isOutOfDom && ($element.onscreen().onscreen || isHeadingHeightZero || isInOverlay)) return; + if (!isOutOfDom && ($element.onscreen().onscreen || isHeadingHeightZero || annotation.isInOverlay)) return; this.removeAnnotation($element, annotation); }); } @@ -208,11 +200,12 @@ class AltText extends Backbone.Controller { } onOverlayOpened() { - // Wait for overlay DOM to fully render before annotating - setTimeout(() => { + // Wait for next frame to ensure overlay DOM layout is complete + this.mutated = false; + requestAnimationFrame(() => { this.mutated = false; this.onDomMutation(); - }, 100); + }); } render() { @@ -245,9 +238,9 @@ class AltText extends Backbone.Controller { const allowText = $element.is('.aria-label,h1,h2,h3,h4,h5,h6,h7,[role=heading]'); const isOutOfDom = ($element.parents('html').length === 0); const isHeadingHeightZero = $element.is('h1,h2,h3,h4,h5,h6,h7,[role=heading]') && $element.height() === 0; - const isInOverlay = $element.closest('.notify, .drawer, dialog').length > 0; + const isInOverlay = $element.closest(OVERLAY_SELECTOR).length > 0; if (!isOutOfDom && (isVisible || isHeadingHeightZero || isInOverlay) && (isNotAriaHidden || (!isAriaHidden && !isParentAriaHidden) || (isImg && !isParentAriaHidden))) { - if (!annotation) this.addAnnotation($element, allowText); + if (!annotation) this.addAnnotation($element, allowText, isInOverlay); else this.updateAnnotation($element, annotation, allowText); } else if (annotation) { this.removeAnnotation($element, annotation); From 1218ca791617d6bd9730e54910ea835f8ad2b246 Mon Sep 17 00:00:00 2001 From: Brad Simpson Date: Mon, 16 Feb 2026 14:13:54 -0700 Subject: [PATCH 03/13] Icon styles --- less/devtoolsAnnotation.less | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/less/devtoolsAnnotation.less b/less/devtoolsAnnotation.less index f61c1db..8fb1e0e 100644 --- a/less/devtoolsAnnotation.less +++ b/less/devtoolsAnnotation.less @@ -25,7 +25,8 @@ // -------------------------------------------------- // -------------------------------------------------- .devtools__annotation { - font-size: 0.875rem; + font-size: 1rem; + line-height: 1; pointer-events: none; z-index: 100; @@ -40,9 +41,12 @@ } &-inner { + font-size: 0.825rem; background-color: @validation-success; color: @validation-success-inverted; opacity: 0; + padding: 0.25rem; + border-radius: 0.125rem; } &-inner .description { @@ -61,10 +65,14 @@ } .button { - padding: @item-padding / 8; + width: 1.25rem; + height: 1.25rem; + display: flex; + justify-content: center; + align-items: center; background-color: @validation-success; color: @validation-success-inverted; - border-radius: 0.25rem; + border-radius: 0.125rem; pointer-events: all; cursor: pointer; } From 572afa99d1339663de6ef8bfebe00f7c87343bc6 Mon Sep 17 00:00:00 2001 From: Brad Simpson Date: Mon, 16 Feb 2026 14:17:03 -0700 Subject: [PATCH 04/13] Increase padding --- less/devtoolsAnnotation.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/less/devtoolsAnnotation.less b/less/devtoolsAnnotation.less index 8fb1e0e..cd381e7 100644 --- a/less/devtoolsAnnotation.less +++ b/less/devtoolsAnnotation.less @@ -45,7 +45,7 @@ background-color: @validation-success; color: @validation-success-inverted; opacity: 0; - padding: 0.25rem; + padding: 0.375rem; border-radius: 0.125rem; } From 4611d6cc62d76f528a81b71f73f2a68eeda2838c Mon Sep 17 00:00:00 2001 From: Brad Simpson Date: Mon, 16 Feb 2026 14:32:20 -0700 Subject: [PATCH 05/13] Minor fixes --- js/helpers.js | 12 +++++++----- js/toggle-alt-text.js | 13 ++++++------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/js/helpers.js b/js/helpers.js index 8657a3e..8b24e59 100644 --- a/js/helpers.js +++ b/js/helpers.js @@ -1,11 +1,12 @@ const OVERLAY_SELECTOR = '.notify, .drawer, dialog'; +const HEADING_SELECTOR = 'h1,h2,h3,h4,h5,h6,[role=heading]'; function findLabel($element) { const id = $element.attr('id'); if (!id) return false; const $label = $(`[for=${id}]`); if (!$label.length) return false; - return computeAccesibleName($label, true); + return computeAccessibleName($label, true); } function getText(domElement) { @@ -29,10 +30,10 @@ function followId($element, property) { if (!id) return false; const $toElement = $(`#${id}`); if (!$toElement.length) return false; - return computeAccesibleName($toElement, true); + return computeAccessibleName($toElement, true); } -function computeAccesibleName($element, allowText = false) { +function computeAccessibleName($element, allowText = false) { if ($element.is('input:not([type=checkbox], [type=radio]), select, [role=range], textarea') && $element.val()) return $element.val(); const ariaHidden = $element.attr('aria-hidden'); if (ariaHidden === 'true') return 'N/A (hidden from assistive technologies)'; @@ -55,7 +56,7 @@ function computeAccesibleName($element, allowText = false) { } function computeHeadingLevel($element) { - const $heading = $element.parents().add($element).filter('h1, h2, h3, h4, h5, h6, h7, [role=heading]'); + const $heading = $element.parents().add($element).filter(HEADING_SELECTOR); if (!$heading.length) return ''; const headingLevel = parseInt($heading[0].tagName) || $heading.attr('aria-level'); return `h${headingLevel}: `; @@ -172,8 +173,9 @@ function getAnnotationPosition($element, $annotation, isInOverlay = false) { } export default { + HEADING_SELECTOR, OVERLAY_SELECTOR, - computeAccesibleName, + computeAccessibleName, computeAccessibleDescription, computeHeadingLevel, getAnnotationPosition diff --git a/js/toggle-alt-text.js b/js/toggle-alt-text.js index 7731eaa..c76991c 100644 --- a/js/toggle-alt-text.js +++ b/js/toggle-alt-text.js @@ -2,7 +2,7 @@ import Backbone from 'backbone'; import Adapt from 'core/js/adapt'; import helpers from './helpers'; -const { OVERLAY_SELECTOR, computeAccesibleName, computeAccessibleDescription, getAnnotationPosition } = helpers; +const { HEADING_SELECTOR, OVERLAY_SELECTOR, computeAccessibleName, computeAccessibleDescription, getAnnotationPosition } = helpers; class Annotation extends Backbone.View { @@ -24,7 +24,7 @@ class Annotation extends Backbone.View { render() { const template = Handlebars.templates.devtoolsAnnotation; - const name = computeAccesibleName(this.$parent, this.allowText); + const name = computeAccessibleName(this.$parent, this.allowText); const description = computeAccessibleDescription(this.$parent); this.$el.html(template({ name, description })); if (!name) this.$el.addClass('has-annotation-warning'); @@ -56,7 +56,6 @@ class AltText extends Backbone.Controller { onEnabled () { if (!Adapt.devtools.get('_isEnabled')) return; _.bindAll(this, 'onDomMutation', 'render', 'onMouseOver'); - this.mutations = []; this.mutated = false; this.listenTo(Adapt.devtools, 'change:_altTextEnabled', this.toggleAltText); $('body').append($('')); @@ -183,7 +182,7 @@ class AltText extends Backbone.Controller { const annotation = $annotation.data('view'); if (!$element) return; const isOutOfDom = ($element.parents('html').length === 0); - const isHeadingHeightZero = $element.is('h1,h2,h3,h4,h5,h6,h7,[role=heading]') && $element.height() === 0; + const isHeadingHeightZero = $element.is(HEADING_SELECTOR) && $element.height() === 0; if (!isOutOfDom && ($element.onscreen().onscreen || isHeadingHeightZero || annotation.isInOverlay)) return; this.removeAnnotation($element, annotation); }); @@ -211,7 +210,7 @@ class AltText extends Backbone.Controller { render() { if (this.mutated === false) return; this.clearUpAnnotations(); - const $headings = $('h1,h2,h3,h4,h5,h6,h7,[role=heading]'); + const $headings = $(HEADING_SELECTOR); const $labelled = $([ '.aria-label', '[alt]', @@ -235,9 +234,9 @@ class AltText extends Backbone.Controller { const isAriaHidden = Boolean($element.filter('[aria-hidden=true]').length); const isNotAriaHidden = Boolean($element.filter('[aria-hidden=false]').length); const isImg = $element.is('img'); - const allowText = $element.is('.aria-label,h1,h2,h3,h4,h5,h6,h7,[role=heading]'); + const allowText = $element.is(`.aria-label,${HEADING_SELECTOR}`); const isOutOfDom = ($element.parents('html').length === 0); - const isHeadingHeightZero = $element.is('h1,h2,h3,h4,h5,h6,h7,[role=heading]') && $element.height() === 0; + const isHeadingHeightZero = $element.is(HEADING_SELECTOR) && $element.height() === 0; const isInOverlay = $element.closest(OVERLAY_SELECTOR).length > 0; if (!isOutOfDom && (isVisible || isHeadingHeightZero || isInOverlay) && (isNotAriaHidden || (!isAriaHidden && !isParentAriaHidden) || (isImg && !isParentAriaHidden))) { if (!annotation) this.addAnnotation($element, allowText, isInOverlay); From becbff354154c7b501a4adfa23f22d96f2a61d00 Mon Sep 17 00:00:00 2001 From: Brad Simpson Date: Thu, 2 Apr 2026 15:37:27 -0600 Subject: [PATCH 06/13] Add nav button label and tooltip support --- example.json | 12 ++++ js/adapt-devtools.js | 85 +++++++++++++++--------- less/devtools.less | 6 -- properties.schema | 92 +++++++++++++++++++++----- schema/course.schema.json | 80 ++++++++++++++++++++++ templates/devtoolsNavigation.hbs | 3 - templates/devtoolsNavigationButton.jsx | 21 ++++++ 7 files changed, 241 insertions(+), 58 deletions(-) create mode 100644 schema/course.schema.json delete mode 100644 templates/devtoolsNavigation.hbs create mode 100644 templates/devtoolsNavigationButton.jsx diff --git a/example.json b/example.json index e09aecf..9ab56e0 100644 --- a/example.json +++ b/example.json @@ -4,6 +4,18 @@ "_debugFile": "" } +// course.json - _globals._extensions +"_devtools": { + "ariaLabel": "Developer tools", + "_navOrder": 9000, + "_showLabel": false, + "navLabel": "Dev Tools", + "_navTooltip": { + "_isEnabled": true, + "text": "Developer tools" + } +} + /* _debugFile example (e.g. dev.json) diff --git a/js/adapt-devtools.js b/js/adapt-devtools.js index 85e52d8..b026573 100644 --- a/js/adapt-devtools.js +++ b/js/adapt-devtools.js @@ -5,6 +5,10 @@ import wait from 'core/js/wait'; import logging from 'core/js/logging'; import location from 'core/js/location'; import AdaptModel from 'core/js/models/adaptModel'; +import NavigationButtonModel from 'core/js/models/NavigationButtonModel'; +import NavigationButtonView from 'core/js/views/NavigationButtonView'; +import navigation from 'core/js/navigation'; +import tooltips from 'core/js/tooltips'; import DevtoolsModel from './devtools-model'; import PassHalfFail from './pass-half-fail'; import ToggleBanking from './toggle-banking'; @@ -377,44 +381,40 @@ class DevtoolsView extends Backbone.View { } } -class DevtoolsNavigationView extends Backbone.View { +class DevtoolsNavigationButtonView extends NavigationButtonView { - initialize () { - this.render = this.render.bind(this); - const template = Handlebars.templates.devtoolsNavigation; - this.$el = $(template()); - $('html').addClass('devtools-enabled').toggleClass('devtools-extended', Adapt.devtools.get('_extended')); - if (this.$el.is('a') || this.$el.is('button')) this.$el.on('click', this.onDevtoolsClicked.bind(this)); - else this.$el.find('a, button').on('click', this.onDevtoolsClicked.bind(this)); - this.listenTo(Adapt, 'pageView:postRender menuView:postRender', this.onContentRendered); - // ensure render occurs at least once (_isReady will not change to true on menus that exclude content objects) - this.listenToOnce(Adapt, 'pageView:postRender menuView:postRender', this.render); - } - - render () { - $('.nav__inner').append(this.$el); - return this; + events () { + return { + click: 'onDevtoolsClicked' + }; } - remove () { - this.$el.remove(); - this.stopListening(); - return this; + static get template() { + return 'devtoolsNavigationButton.jsx'; } - deferredRender () { - _.defer(this.render); + attributes () { + const attributes = this.model.toJSON(); + return { + name: attributes._id, + role: attributes._role === 'button' ? undefined : attributes._role, + 'data-order': attributes._order, + 'data-tooltip-id': attributes._id, + 'aria-haspopup': 'dialog' + }; } - onContentRendered (view) { - if (view.model.get('_id') === location._currentId) { - this.stopListening(view.model, 'change:_isReady', this.deferredRender); - this.listenToOnce(view.model, 'change:_isReady', this.deferredRender); - } + initialize (options) { + super.initialize(options); + this.listenTo(Adapt, { remove: this.remove }); + tooltips.register({ + _id: this.model.get('_id'), + ...this.model.get('_navTooltip') || {} + }); } onDevtoolsClicked (event) { - if (event && event.preventDefault) event.preventDefault(); + event.preventDefault(); drawer.openCustomView(new DevtoolsView().$el, false); } @@ -426,13 +426,36 @@ Adapt.once('courseModel:dataLoaded', () => { function initNavigationView() { if (!Adapt.devtools.get('_isEnabled')) return; - if (navigationView) navigationView.remove(); - navigationView = new DevtoolsNavigationView(); + if (navigationView) navigation.removeButton(navigationView); + $('html').addClass('devtools-enabled').toggleClass('devtools-extended', Adapt.devtools.get('_extended')); + const { + ariaLabel = 'Developer tools', + _navOrder = 9000, + _showLabel = false, + navLabel = 'Dev Tools', + _navTooltip = {} + } = Adapt.course.get('_globals')?._extensions?._devtools ?? {}; + const model = new NavigationButtonModel({ + _id: 'devtools', + _order: _navOrder, + _showLabel, + _classes: 'nav__devtools-btn devtools__nav-btn', + _iconClasses: 'icon-cog', + _role: 'button', + ariaLabel: ariaLabel || navLabel, + text: navLabel, + _navTooltip + }); + navigationView = new DevtoolsNavigationButtonView({ model }); + navigation.addButton(navigationView); } Adapt.once('adapt:initialize devtools:enable', () => { + Adapt.on({ + 'router:contentObject': initNavigationView, + 'app:languageChanged': initNavigationView + }); initNavigationView(); - Adapt.on('app:languageChanged', initNavigationView); }); data.on('loaded', async () => { diff --git a/less/devtools.less b/less/devtools.less index 5a5cdc4..c826f58 100644 --- a/less/devtools.less +++ b/less/devtools.less @@ -1,10 +1,4 @@ .devtools { - // Nav icons - // -------------------------------------------------- - &__nav-btn .icon { - .icon-cog; - } - // Items // -------------------------------------------------- &__item { diff --git a/properties.schema b/properties.schema index cc30ca6..f38f509 100644 --- a/properties.schema +++ b/properties.schema @@ -1,26 +1,82 @@ { - "type":"object", + "type": "object", "$schema": "http://json-schema.org/draft-04/schema", "id": "http://jsonschema.net", - "required":false, - "properties":{ + "required": false, + "globals": { + "ariaLabel": { + "type": "string", + "required": false, + "default": "Developer tools", + "title": "Navigation button aria label", + "inputType": "Text", + "validators": [], + "translatable": true + }, + "_navOrder": { + "type": "number", + "required": false, + "title": "Navigation bar order", + "help": "Determines the order in which the button is displayed in the navigation bar. Negative numbers (e.g. -100) are left-aligned. Positive numbers (e.g. 100) are right-aligned.", + "default": 9000, + "inputType": "Text" + }, + "_showLabel": { + "type": "boolean", + "required": false, + "default": false, + "title": "Enable navigation bar button label", + "inputType": "Checkbox" + }, + "navLabel": { + "type": "string", + "required": false, + "default": "Dev Tools", + "title": "Navigation bar button label", + "inputType": "Text", + "translatable": true + }, + "_navTooltip": { + "type": "object", + "title": "Navigation tooltip", + "properties": { + "_isEnabled": { + "type": "boolean", + "default": true, + "title": "Enable tooltip for navigation button", + "inputType": "Checkbox", + "validators": [] + }, + "text": { + "type": "string", + "title": "", + "default": "Developer tools", + "help": "The tooltip text to display on hover over this item", + "inputType": "Text", + "validators": [], + "translatable": true + } + } + } + }, + "properties": { "pluginLocations": { - "type":"object", - "required":true, - "properties":{ + "type": "object", + "required": true, + "properties": { "config": { - "type":"object", + "type": "object", "properties": { "_devtools": { - "type":"object", - "required":false, + "type": "object", + "required": false, "legend": "Dev tools", - "properties":{ + "properties": { "_isEnabled": { - "type":"boolean", - "required":false, + "type": "boolean", + "required": false, "title": "Show dev tools in nav bar", - "inputType": { "type": "Boolean", "options": [false, true]}, + "inputType": { "type": "Boolean", "options": [false, true] }, "validators": [], "help": "Set to true to show 'dev tools' in the navigation bar. Remember to disable/uninstall before going live!" } @@ -29,19 +85,19 @@ } }, "course": { - "type":"object" + "type": "object" }, "contentobject": { - "type":"object" + "type": "object" }, "article": { - "type":"object" + "type": "object" }, "block": { - "type":"object" + "type": "object" }, "component": { - "type":"object" + "type": "object" } } } diff --git a/schema/course.schema.json b/schema/course.schema.json new file mode 100644 index 0000000..25a34d4 --- /dev/null +++ b/schema/course.schema.json @@ -0,0 +1,80 @@ +{ + "$anchor": "devtools-course", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "$patch": { + "source": { + "$ref": "course" + }, + "with": { + "properties": { + "_globals": { + "type": "object", + "default": {}, + "properties": { + "_extensions": { + "type": "object", + "default": {}, + "properties": { + "_devtools": { + "type": "object", + "title": "Dev Tools", + "default": {}, + "properties": { + "ariaLabel": { + "type": "string", + "title": "Navigation button aria label", + "default": "Developer tools", + "_adapt": { + "translatable": true + } + }, + "_navOrder": { + "type": "number", + "title": "Navigation bar order", + "description": "Determines the order in which the button is displayed in the navigation bar. Negative numbers (e.g. -100) are left-aligned. Positive numbers (e.g. 100) are right-aligned.", + "default": 9000 + }, + "_showLabel": { + "type": "boolean", + "title": "Enable navigation bar button label", + "default": false + }, + "navLabel": { + "type": "string", + "title": "Navigation bar button label", + "default": "Dev Tools", + "_adapt": { + "translatable": true + } + }, + "_navTooltip": { + "type": "object", + "title": "Navigation tooltip", + "default": {}, + "properties": { + "_isEnabled": { + "type": "boolean", + "title": "Enable tooltip for navigation button", + "default": true + }, + "text": { + "type": "string", + "title": "Tooltip text", + "default": "Developer tools", + "_adapt": { + "translatable": true + } + } + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/templates/devtoolsNavigation.hbs b/templates/devtoolsNavigation.hbs deleted file mode 100644 index b8532eb..0000000 --- a/templates/devtoolsNavigation.hbs +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/templates/devtoolsNavigationButton.jsx b/templates/devtoolsNavigationButton.jsx new file mode 100644 index 0000000..b8e00ba --- /dev/null +++ b/templates/devtoolsNavigationButton.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { classes, compile } from 'core/js/reactHelpers'; + +export default function DevtoolsNavigationButton(props) { + const { + text, + _iconClasses + } = props; + return ( + + + ); +} From 28871d2acaa4f05cace8b7d072b922666cf90eaf Mon Sep 17 00:00:00 2001 From: Oliver Foster Date: Tue, 14 Apr 2026 12:01:51 +0100 Subject: [PATCH 07/13] Reduce complexity --- js/helpers.js | 96 +++++++++++++++++++++++-------------------- js/toggle-alt-text.js | 85 ++++++++++++++++++-------------------- 2 files changed, 91 insertions(+), 90 deletions(-) diff --git a/js/helpers.js b/js/helpers.js index 8b24e59..8ded77f 100644 --- a/js/helpers.js +++ b/js/helpers.js @@ -1,5 +1,4 @@ -const OVERLAY_SELECTOR = '.notify, .drawer, dialog'; -const HEADING_SELECTOR = 'h1,h2,h3,h4,h5,h6,[role=heading]'; +export const HEADING_SELECTOR = 'h1,h2,h3,h4,h5,h6,[role=heading]'; function findLabel($element) { const id = $element.attr('id'); @@ -33,7 +32,7 @@ function followId($element, property) { return computeAccessibleName($toElement, true); } -function computeAccessibleName($element, allowText = false) { +export function computeAccessibleName($element, allowText = false) { if ($element.is('input:not([type=checkbox], [type=radio]), select, [role=range], textarea') && $element.val()) return $element.val(); const ariaHidden = $element.attr('aria-hidden'); if (ariaHidden === 'true') return 'N/A (hidden from assistive technologies)'; @@ -62,38 +61,53 @@ function computeHeadingLevel($element) { return `h${headingLevel}: `; } -function computeAccessibleDescription($element) { +export function computeAccessibleDescription($element) { const describedByText = followId($element, 'aria-describedby'); if (describedByText) return describedByText; return ''; } -function getAnnotationPosition($element, $annotation, isInOverlay = false) { +export function getContainer($element) { + const $fixedParent = $element.parents().add($element).filter((index, el) => $(el).css('position') === 'fixed'); + return $fixedParent.length ? $fixedParent : $('body'); +} + +export function shouldAnnotate($element) { + return isVisible($element) && isReadable($element); +} + +function isVisible($element) { + const isHeadingHeightZero = $element.is(HEADING_SELECTOR) && $element.height() === 0; + const isVisible = !isHeadingHeightZero && isInDom($element) && $element.onscreen().onscreen; + return isVisible; +} + +function isReadable($element) { + const isImg = $element.is('img'); + const isAncestorAriaHidden = Boolean($element.parents().filter('[aria-hidden=true]').length); + const isAriaHidden = Boolean($element.filter('[aria-hidden=true]').length); + const isNotAriaHidden = Boolean($element.filter('[aria-hidden=false]').length); + const isReadable = (isNotAriaHidden || (!isAriaHidden && !isAncestorAriaHidden) || (isImg && !isAncestorAriaHidden)); + return isReadable; +} + +function isInDom($element) { + const isInDom = $element.parents('html').length > 0; + return isInDom; +} + +export function getAnnotationPosition($element, $annotation) { + const $annotationContainer = getContainer($element); + const containerBoundingRect = $annotationContainer[0].getBoundingClientRect(); const targetBoundingRect = $element[0].getBoundingClientRect(); - let availableWidth = $('html')[0].clientWidth; - let availableHeight = $('html')[0].clientHeight; + const availableWidth = $annotationContainer.width(); + const availableHeight = $annotationContainer.height(); const tooltipsWidth = $annotation.width(); const tooltipsHeight = $annotation.height(); const elementWidth = $element.width(); const elementHeight = $element.height(); - const isFixedPosition = Boolean($element.parents().add($element).filter((index, el) => $(el).css('position') === 'fixed').length); - const scrollOffsetTop = isFixedPosition ? 0 : $(window).scrollTop(); - const scrollOffsetLeft = isFixedPosition ? 0 : $(window).scrollLeft(); - - // For overlay annotations, calculate position relative to overlay container - let overlayOffsetTop = 0; - let overlayOffsetLeft = 0; - if (isInOverlay) { - const $overlay = $element.closest(OVERLAY_SELECTOR); - if ($overlay.length) { - const overlayRect = $overlay[0].getBoundingClientRect(); - overlayOffsetTop = overlayRect.top; - overlayOffsetLeft = overlayRect.left; - // Use overlay dimensions instead of viewport for boundary checks - availableWidth = overlayRect.right; - availableHeight = overlayRect.bottom; - } - } + const scrollOffsetTop = -containerBoundingRect.top + $annotationContainer.scrollTop(); + const scrollOffsetLeft = -containerBoundingRect.left + $annotationContainer.scrollLeft(); const canAlignBottom = targetBoundingRect.bottom + tooltipsHeight < availableHeight; const canAlignRight = targetBoundingRect.right + tooltipsWidth < availableWidth; @@ -104,8 +118,8 @@ function getAnnotationPosition($element, $annotation, isInOverlay = false) { return { className: 'is-contained', css: { - left: targetBoundingRect.left + scrollOffsetLeft - overlayOffsetLeft, - top: targetBoundingRect.top + scrollOffsetTop - overlayOffsetTop, + left: targetBoundingRect.left + scrollOffsetLeft, + top: targetBoundingRect.top + scrollOffsetTop, 'max-width': (elementHeight === 0) ? '' : elementWidth } }; @@ -119,8 +133,8 @@ function getAnnotationPosition($element, $annotation, isInOverlay = false) { return { className: 'is-left is-top', css: { - left: targetBoundingRect.left - tooltipsWidth + scrollOffsetLeft - overlayOffsetLeft, - top: targetBoundingRect.top - tooltipsHeight + scrollOffsetTop - overlayOffsetTop, + left: targetBoundingRect.left - tooltipsWidth + scrollOffsetLeft, + top: targetBoundingRect.top - tooltipsHeight + scrollOffsetTop, 'max-width': '' } }; @@ -130,8 +144,8 @@ function getAnnotationPosition($element, $annotation, isInOverlay = false) { return { className: 'is-right is-top', css: { - left: targetBoundingRect.right + scrollOffsetLeft - overlayOffsetLeft, - top: targetBoundingRect.top - tooltipsHeight + scrollOffsetTop - overlayOffsetTop, + left: targetBoundingRect.right + scrollOffsetLeft, + top: targetBoundingRect.top - tooltipsHeight + scrollOffsetTop, 'max-width': '' } }; @@ -141,8 +155,8 @@ function getAnnotationPosition($element, $annotation, isInOverlay = false) { return { className: 'is-left is-bottom', css: { - left: targetBoundingRect.left - tooltipsWidth + scrollOffsetLeft - overlayOffsetLeft, - top: targetBoundingRect.bottom + scrollOffsetTop - overlayOffsetTop, + left: targetBoundingRect.left - tooltipsWidth + scrollOffsetLeft, + top: targetBoundingRect.bottom + scrollOffsetTop, 'max-width': '' } }; @@ -152,19 +166,14 @@ function getAnnotationPosition($element, $annotation, isInOverlay = false) { return { className: 'is-right is-bottom', css: { - left: targetBoundingRect.right + scrollOffsetLeft - overlayOffsetLeft, - top: targetBoundingRect.bottom + scrollOffsetTop - overlayOffsetTop, + left: targetBoundingRect.right + scrollOffsetLeft, + top: targetBoundingRect.bottom + scrollOffsetTop, 'max-width': '' } }; } const position = getPosition(); - // Use fixed for fixed-position elements, absolute for overlays and normal elements - if (isInOverlay) { - position.css.position = 'absolute'; - } else { - position.css.position = isFixedPosition ? 'fixed' : 'absolute'; - } + position.css.position = 'absolute'; if (position.css.left < 0) position.css.left = 0; position.css.left += 'px'; position.css.top += 'px'; @@ -174,9 +183,8 @@ function getAnnotationPosition($element, $annotation, isInOverlay = false) { export default { HEADING_SELECTOR, - OVERLAY_SELECTOR, computeAccessibleName, computeAccessibleDescription, - computeHeadingLevel, - getAnnotationPosition + getAnnotationPosition, + getContainer }; diff --git a/js/toggle-alt-text.js b/js/toggle-alt-text.js index c76991c..27af34c 100644 --- a/js/toggle-alt-text.js +++ b/js/toggle-alt-text.js @@ -1,11 +1,26 @@ import Backbone from 'backbone'; import Adapt from 'core/js/adapt'; -import helpers from './helpers'; - -const { HEADING_SELECTOR, OVERLAY_SELECTOR, computeAccessibleName, computeAccessibleDescription, getAnnotationPosition } = helpers; +import { + HEADING_SELECTOR, + computeAccessibleName, + computeAccessibleDescription, + getAnnotationPosition, + getContainer, + shouldAnnotate +} from './helpers'; class Annotation extends Backbone.View { + events() { + return { + click: 'onClick' + }; + } + + onClick(event) { + console.log('Annotation clicked for', this.$parent[0]); + } + className() { return 'devtools__annotation'; } @@ -16,22 +31,27 @@ class Annotation extends Backbone.View { initialize(options) { this.$parent = options.$parent; + this.$container = getContainer(this.$parent); this.allowText = options.allowText; - this.isInOverlay = options.isInOverlay || false; this.$el.data('annotating', this.$parent); this.$el.data('view', this); } render() { + function hash(name, description, position) { + return name + description + position.className + position.css.top + position.css.left; + } const template = Handlebars.templates.devtoolsAnnotation; const name = computeAccessibleName(this.$parent, this.allowText); const description = computeAccessibleDescription(this.$parent); + const position = getAnnotationPosition(this.$parent, this.$el); + if (this._last === hash(name, description, position)) return; this.$el.html(template({ name, description })); - if (!name) this.$el.addClass('has-annotation-warning'); - const position = getAnnotationPosition(this.$parent, this.$el, this.isInOverlay); + this.$el.toggleClass('has-annotation-warning', !name); this.$el.css(position.css); this.$el.removeClass('is-top is-left is-right is-bottom is-contained'); this.$el.addClass(position.className); + this._last = hash(name, description, position); } showOutline() { @@ -43,13 +63,12 @@ class Annotation extends Backbone.View { this.$parent.removeClass('devtools__annotation-outline'); this.$el.removeClass('has-mouse-over'); } - } class AltText extends Backbone.Controller { initialize() { - this.onDomMutation = this.onDomMutation.bind(this); + this.onDomMutation = this.onDomMutation.bind(this); // _.debounce(this.onDomMutation.bind(this), 250); this.listenToOnce(Adapt, 'adapt:initialize devtools:enable', this.onEnabled); } @@ -59,10 +78,7 @@ class AltText extends Backbone.Controller { this.mutated = false; this.listenTo(Adapt.devtools, 'change:_altTextEnabled', this.toggleAltText); $('body').append($('')); - // if available we can use to avoid unnecessary checks - if (typeof MutationObserver === 'function') { - this.observer = new MutationObserver(this.onDomMutation); - } + this.observer = new MutationObserver(this.onDomMutation); } connectObserver() { @@ -87,11 +103,11 @@ class AltText extends Backbone.Controller { }); } this.listenTo(Adapt, { - 'notify:opened drawer:opened drawer:openedCustomView': this.onOverlayOpened, - 'popup:closed notify:closed drawer:closed': this.onDomMutation, remove: this.removeAllAnnotations }); $(window).on('scroll', this.onDomMutation); + $(document).on('transitionend', this.onDomMutation); + $(document).on('animationend', this.onDomMutation); $(document).on('mouseover', '*', this.onMouseOver); } @@ -123,10 +139,10 @@ class AltText extends Backbone.Controller { if (this.observer) { this.observer.disconnect(); } - this.stopListening(Adapt, 'notify:opened drawer:opened drawer:openedCustomView', this.onOverlayOpened); - this.stopListening(Adapt, 'popup:closed notify:closed drawer:closed', this.onDomMutation); this.stopListening(Adapt, 'remove', this.removeAllAnnotations); $(window).off('scroll', this.onDomMutation); + $(document).off('transitionend', this.onDomMutation); + $(document).off('animationend', this.onDomMutation); $(document).off('mouseover', '*', this.onMouseOver); } @@ -141,18 +157,13 @@ class AltText extends Backbone.Controller { this.connectObserver(); } - addAnnotation($element, allowText, isInOverlay) { + addAnnotation($element, allowText) { const annotation = new Annotation({ $parent: $element, - allowText, - isInOverlay + allowText }); - if (isInOverlay) { - $element.closest(OVERLAY_SELECTOR).append(annotation.$el); - } else { - $('.devtools__annotations').append(annotation.$el); - } + annotation.$container.append(annotation.$el); $element.data('annotation', annotation); $element.attr('data-annotated', true); @@ -181,9 +192,7 @@ class AltText extends Backbone.Controller { const $element = $annotation.data('annotating'); const annotation = $annotation.data('view'); if (!$element) return; - const isOutOfDom = ($element.parents('html').length === 0); - const isHeadingHeightZero = $element.is(HEADING_SELECTOR) && $element.height() === 0; - if (!isOutOfDom && ($element.onscreen().onscreen || isHeadingHeightZero || annotation.isInOverlay)) return; + if (shouldAnnotate($element)) return; this.removeAnnotation($element, annotation); }); } @@ -198,17 +207,9 @@ class AltText extends Backbone.Controller { this.mutated = true; } - onOverlayOpened() { - // Wait for next frame to ensure overlay DOM layout is complete - this.mutated = false; - requestAnimationFrame(() => { - this.mutated = false; - this.onDomMutation(); - }); - } - render() { if (this.mutated === false) return; + console.log('Rendering annotations'); this.clearUpAnnotations(); const $headings = $(HEADING_SELECTOR); const $labelled = $([ @@ -229,17 +230,9 @@ class AltText extends Backbone.Controller { .each((index, element) => { const $element = $(element); const annotation = $element.data('annotation'); - const isVisible = $element.onscreen().onscreen; - const isParentAriaHidden = Boolean($element.parents().filter('[aria-hidden=true]').length); - const isAriaHidden = Boolean($element.filter('[aria-hidden=true]').length); - const isNotAriaHidden = Boolean($element.filter('[aria-hidden=false]').length); - const isImg = $element.is('img'); const allowText = $element.is(`.aria-label,${HEADING_SELECTOR}`); - const isOutOfDom = ($element.parents('html').length === 0); - const isHeadingHeightZero = $element.is(HEADING_SELECTOR) && $element.height() === 0; - const isInOverlay = $element.closest(OVERLAY_SELECTOR).length > 0; - if (!isOutOfDom && (isVisible || isHeadingHeightZero || isInOverlay) && (isNotAriaHidden || (!isAriaHidden && !isParentAriaHidden) || (isImg && !isParentAriaHidden))) { - if (!annotation) this.addAnnotation($element, allowText, isInOverlay); + if (shouldAnnotate($element)) { + if (!annotation) this.addAnnotation($element, allowText); else this.updateAnnotation($element, annotation, allowText); } else if (annotation) { this.removeAnnotation($element, annotation); From 628ca224f21409816129149442871a8af17f8bb1 Mon Sep 17 00:00:00 2001 From: Oliver Foster Date: Tue, 14 Apr 2026 12:08:25 +0100 Subject: [PATCH 08/13] Removed comments and console logs --- js/toggle-alt-text.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/js/toggle-alt-text.js b/js/toggle-alt-text.js index 27af34c..6288761 100644 --- a/js/toggle-alt-text.js +++ b/js/toggle-alt-text.js @@ -68,7 +68,7 @@ class Annotation extends Backbone.View { class AltText extends Backbone.Controller { initialize() { - this.onDomMutation = this.onDomMutation.bind(this); // _.debounce(this.onDomMutation.bind(this), 250); + this.onDomMutation = this.onDomMutation.bind(this); this.listenToOnce(Adapt, 'adapt:initialize devtools:enable', this.onEnabled); } @@ -209,7 +209,6 @@ class AltText extends Backbone.Controller { render() { if (this.mutated === false) return; - console.log('Rendering annotations'); this.clearUpAnnotations(); const $headings = $(HEADING_SELECTOR); const $labelled = $([ From bcdaa83473a4f7159caac0abe04a2a85dbd46d90 Mon Sep 17 00:00:00 2001 From: Oliver Foster Date: Tue, 14 Apr 2026 13:00:34 +0100 Subject: [PATCH 09/13] Added fixes for nav buttons --- js/helpers.js | 16 +++++++++++++--- js/toggle-alt-text.js | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/js/helpers.js b/js/helpers.js index 8ded77f..daa3f47 100644 --- a/js/helpers.js +++ b/js/helpers.js @@ -50,6 +50,8 @@ export function computeAccessibleName($element, allowText = false) { if (valueNow) return valueNow; const alt = $element.attr('alt'); if (alt) return alt; + const childAriaLabel = $element.find('.aria-label').first().text(); + if (childAriaLabel) return childAriaLabel; if (!allowText) return ''; return computeHeadingLevel($element) + getText($element[0]); } @@ -73,7 +75,8 @@ export function getContainer($element) { } export function shouldAnnotate($element) { - return isVisible($element) && isReadable($element); + const shouldAnnotate = isVisible($element) && isReadable($element); + return shouldAnnotate; } function isVisible($element) { @@ -87,7 +90,8 @@ function isReadable($element) { const isAncestorAriaHidden = Boolean($element.parents().filter('[aria-hidden=true]').length); const isAriaHidden = Boolean($element.filter('[aria-hidden=true]').length); const isNotAriaHidden = Boolean($element.filter('[aria-hidden=false]').length); - const isReadable = (isNotAriaHidden || (!isAriaHidden && !isAncestorAriaHidden) || (isImg && !isAncestorAriaHidden)); + const hasAccessibleName = Boolean(computeAccessibleName($element) || computeAccessibleDescription($element)); + const isReadable = !isAncestorAriaHidden && (isNotAriaHidden || !isAriaHidden || isImg || (hasAccessibleName && !isAriaHidden)); return isReadable; } @@ -96,6 +100,11 @@ function isInDom($element) { return isInDom; } +function isFixed($element) { + const isFixed = Boolean($element.parents().add($element).filter((index, el) => $(el).css('position') === 'fixed').length); + return isFixed; +} + export function getAnnotationPosition($element, $annotation) { const $annotationContainer = getContainer($element); const containerBoundingRect = $annotationContainer[0].getBoundingClientRect(); @@ -126,7 +135,8 @@ export function getAnnotationPosition($element, $annotation) { } if (!canAlignBottomRight) { // Find the 'corner' with the most space from the viewport edge - const isTopPreferred = availableHeight - (targetBoundingRect.bottom + tooltipsHeight) < targetBoundingRect.top - tooltipsHeight; + const isHardTop = isFixed($annotationContainer) && (containerBoundingRect.top < tooltipsHeight && targetBoundingRect.top < tooltipsHeight); + const isTopPreferred = !isHardTop && (availableHeight - (targetBoundingRect.bottom + tooltipsHeight) < targetBoundingRect.top - tooltipsHeight); const isLeftPreferred = availableWidth - (targetBoundingRect.right + tooltipsWidth) < targetBoundingRect.left - tooltipsWidth; if (isTopPreferred && isLeftPreferred) { // Top left diff --git a/js/toggle-alt-text.js b/js/toggle-alt-text.js index 6288761..f1e7fb5 100644 --- a/js/toggle-alt-text.js +++ b/js/toggle-alt-text.js @@ -201,7 +201,7 @@ class AltText extends Backbone.Controller { annotation.render(); } - onDomMutation(mutations) { + onDomMutation() { if (this.mutated) return; requestAnimationFrame(this.render); this.mutated = true; From 92828a5e8ec1f33a1820d658c8e7beef66d1d568 Mon Sep 17 00:00:00 2001 From: Oliver Foster Date: Tue, 14 Apr 2026 13:08:32 +0100 Subject: [PATCH 10/13] Update --- js/helpers.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/helpers.js b/js/helpers.js index daa3f47..0f82211 100644 --- a/js/helpers.js +++ b/js/helpers.js @@ -50,7 +50,8 @@ export function computeAccessibleName($element, allowText = false) { if (valueNow) return valueNow; const alt = $element.attr('alt'); if (alt) return alt; - const childAriaLabel = $element.find('.aria-label').first().text(); + const childAriaLabel = !$element.is(HEADING_SELECTOR) && + $element.find('.aria-label').first().text(); if (childAriaLabel) return childAriaLabel; if (!allowText) return ''; return computeHeadingLevel($element) + getText($element[0]); From 2d63b1ba005880839367ffd0e8ae262e44131ec7 Mon Sep 17 00:00:00 2001 From: Brad Simpson Date: Tue, 14 Apr 2026 13:06:09 -0600 Subject: [PATCH 11/13] Nest properties in _navButton --- example.json | 10 ++++--- js/adapt-devtools.js | 9 ++++--- properties.schema | 56 -------------------------------------- schema/course.schema.json | 57 ++++++++++++++++++++++----------------- 4 files changed, 43 insertions(+), 89 deletions(-) diff --git a/example.json b/example.json index 9ab56e0..dbc2d95 100644 --- a/example.json +++ b/example.json @@ -6,10 +6,12 @@ // course.json - _globals._extensions "_devtools": { - "ariaLabel": "Developer tools", - "_navOrder": 9000, - "_showLabel": false, - "navLabel": "Dev Tools", + "_navButton": { + "ariaLabel": "Developer tools", + "_navOrder": 9000, + "_showLabel": true, + "navLabel": "Dev Tools" + }, "_navTooltip": { "_isEnabled": true, "text": "Developer tools" diff --git a/js/adapt-devtools.js b/js/adapt-devtools.js index b026573..00d5f29 100644 --- a/js/adapt-devtools.js +++ b/js/adapt-devtools.js @@ -428,13 +428,14 @@ function initNavigationView() { if (!Adapt.devtools.get('_isEnabled')) return; if (navigationView) navigation.removeButton(navigationView); $('html').addClass('devtools-enabled').toggleClass('devtools-extended', Adapt.devtools.get('_extended')); + const devtoolsGlobals = Adapt.course.get('_globals')?._extensions?._devtools ?? {}; const { ariaLabel = 'Developer tools', _navOrder = 9000, - _showLabel = false, - navLabel = 'Dev Tools', - _navTooltip = {} - } = Adapt.course.get('_globals')?._extensions?._devtools ?? {}; + _showLabel = true, + navLabel = 'Dev Tools' + } = devtoolsGlobals._navButton ?? {}; + const { _navTooltip = {} } = devtoolsGlobals; const model = new NavigationButtonModel({ _id: 'devtools', _order: _navOrder, diff --git a/properties.schema b/properties.schema index f38f509..6aa1046 100644 --- a/properties.schema +++ b/properties.schema @@ -3,62 +3,6 @@ "$schema": "http://json-schema.org/draft-04/schema", "id": "http://jsonschema.net", "required": false, - "globals": { - "ariaLabel": { - "type": "string", - "required": false, - "default": "Developer tools", - "title": "Navigation button aria label", - "inputType": "Text", - "validators": [], - "translatable": true - }, - "_navOrder": { - "type": "number", - "required": false, - "title": "Navigation bar order", - "help": "Determines the order in which the button is displayed in the navigation bar. Negative numbers (e.g. -100) are left-aligned. Positive numbers (e.g. 100) are right-aligned.", - "default": 9000, - "inputType": "Text" - }, - "_showLabel": { - "type": "boolean", - "required": false, - "default": false, - "title": "Enable navigation bar button label", - "inputType": "Checkbox" - }, - "navLabel": { - "type": "string", - "required": false, - "default": "Dev Tools", - "title": "Navigation bar button label", - "inputType": "Text", - "translatable": true - }, - "_navTooltip": { - "type": "object", - "title": "Navigation tooltip", - "properties": { - "_isEnabled": { - "type": "boolean", - "default": true, - "title": "Enable tooltip for navigation button", - "inputType": "Checkbox", - "validators": [] - }, - "text": { - "type": "string", - "title": "", - "default": "Developer tools", - "help": "The tooltip text to display on hover over this item", - "inputType": "Text", - "validators": [], - "translatable": true - } - } - } - }, "properties": { "pluginLocations": { "type": "object", diff --git a/schema/course.schema.json b/schema/course.schema.json index 25a34d4..844e3f1 100644 --- a/schema/course.schema.json +++ b/schema/course.schema.json @@ -21,31 +21,38 @@ "title": "Dev Tools", "default": {}, "properties": { - "ariaLabel": { - "type": "string", - "title": "Navigation button aria label", - "default": "Developer tools", - "_adapt": { - "translatable": true - } - }, - "_navOrder": { - "type": "number", - "title": "Navigation bar order", - "description": "Determines the order in which the button is displayed in the navigation bar. Negative numbers (e.g. -100) are left-aligned. Positive numbers (e.g. 100) are right-aligned.", - "default": 9000 - }, - "_showLabel": { - "type": "boolean", - "title": "Enable navigation bar button label", - "default": false - }, - "navLabel": { - "type": "string", - "title": "Navigation bar button label", - "default": "Dev Tools", - "_adapt": { - "translatable": true + "_navButton": { + "type": "object", + "title": "Navigation button", + "default": {}, + "properties": { + "ariaLabel": { + "type": "string", + "title": "Navigation button aria label", + "default": "Developer tools", + "_adapt": { + "translatable": true + } + }, + "_navOrder": { + "type": "number", + "title": "Navigation bar order", + "description": "Determines the order in which the button is displayed in the navigation bar. Negative numbers (e.g. -100) are left-aligned. Positive numbers (e.g. 100) are right-aligned.", + "default": 9000 + }, + "_showLabel": { + "type": "boolean", + "title": "Enable navigation bar button label", + "default": true + }, + "navLabel": { + "type": "string", + "title": "Navigation bar button label", + "default": "Dev Tools", + "_adapt": { + "translatable": true + } + } } }, "_navTooltip": { From 2256e6b4a58117dc4f3a165e4f395fa4904d024b Mon Sep 17 00:00:00 2001 From: Brad Simpson Date: Fri, 24 Apr 2026 15:48:04 -0600 Subject: [PATCH 12/13] Minor template update to align with Home Button --- templates/devtoolsNavigationButton.jsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/templates/devtoolsNavigationButton.jsx b/templates/devtoolsNavigationButton.jsx index b8e00ba..5a3be6a 100644 --- a/templates/devtoolsNavigationButton.jsx +++ b/templates/devtoolsNavigationButton.jsx @@ -6,8 +6,9 @@ export default function DevtoolsNavigationButton(props) { text, _iconClasses } = props; + return ( - + <> +