From 28871d2acaa4f05cace8b7d072b922666cf90eaf Mon Sep 17 00:00:00 2001 From: Oliver Foster Date: Tue, 14 Apr 2026 12:01:51 +0100 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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]);