Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 49 additions & 23 deletions js/helpers.js
Original file line number Diff line number Diff line change
@@ -1,9 +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) {
Expand All @@ -27,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 '<span class="u-nobr">N/A (hidden from assistive technologies)</span>';
Expand All @@ -53,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}: `;
Expand All @@ -65,28 +68,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(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 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
}
};
Expand All @@ -100,8 +119,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': ''
}
};
Expand All @@ -111,8 +130,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': ''
}
};
Expand All @@ -122,25 +141,30 @@ 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': ''
}
};
}
}
// Bottom right, default
return {
className: 'is-right, is-bottom',
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';
Expand All @@ -149,7 +173,9 @@ function getAnnotationPosition($element, $annotation) {
}

export default {
computeAccesibleName,
HEADING_SELECTOR,
OVERLAY_SELECTOR,
computeAccessibleName,
computeAccessibleDescription,
computeHeadingLevel,
getAnnotationPosition
Expand Down
53 changes: 37 additions & 16 deletions js/toggle-alt-text.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 { HEADING_SELECTOR, OVERLAY_SELECTOR, computeAccessibleName, computeAccessibleDescription, getAnnotationPosition } = helpers;

class Annotation extends Backbone.View {

Expand All @@ -19,17 +17,18 @@ 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);
}

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');
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);
Expand Down Expand Up @@ -57,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($('<div class="devtools__annotations" aria-hidden="true"></div>'));
Expand Down Expand Up @@ -89,6 +87,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
});
Expand Down Expand Up @@ -124,7 +123,9 @@ 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('mouseover', '*', this.onMouseOver);
}
Expand All @@ -140,9 +141,19 @@ class AltText extends Backbone.Controller {
this.connectObserver();
}

addAnnotation($element, allowText) {
const annotation = new Annotation({ $parent: $element, allowText });
$('.devtools__annotations').append(annotation.$el);
addAnnotation($element, allowText, isInOverlay) {
const annotation = new Annotation({
$parent: $element,
allowText,
isInOverlay
});

if (isInOverlay) {
$element.closest(OVERLAY_SELECTOR).append(annotation.$el);
} else {
$('.devtools__annotations').append(annotation.$el);
}

$element.data('annotation', annotation);
$element.attr('data-annotated', true);
this.updateAnnotation($element, annotation, allowText);
Expand Down Expand Up @@ -171,8 +182,8 @@ 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;
if (!isOutOfDom && ($element.onscreen().onscreen || isHeadingHeightZero)) return;
const isHeadingHeightZero = $element.is(HEADING_SELECTOR) && $element.height() === 0;
if (!isOutOfDom && ($element.onscreen().onscreen || isHeadingHeightZero || annotation.isInOverlay)) return;
this.removeAnnotation($element, annotation);
});
}
Expand All @@ -187,10 +198,19 @@ 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;
this.clearUpAnnotations();
const $headings = $('h1,h2,h3,h4,h5,h6,h7,[role=heading]');
const $headings = $(HEADING_SELECTOR);
const $labelled = $([
'.aria-label',
'[alt]',
Expand All @@ -214,11 +234,12 @@ 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;
if (!isOutOfDom && (isVisible || isHeadingHeightZero) && (isNotAriaHidden || (!isAriaHidden && !isParentAriaHidden) || (isImg && !isParentAriaHidden))) {
if (!annotation) this.addAnnotation($element, allowText);
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);
else this.updateAnnotation($element, annotation, allowText);
} else if (annotation) {
this.removeAnnotation($element, annotation);
Expand Down
14 changes: 11 additions & 3 deletions less/devtoolsAnnotation.less
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
// --------------------------------------------------
// --------------------------------------------------
.devtools__annotation {
font-size: 0.875rem;
font-size: 1rem;
line-height: 1;
pointer-events: none;
z-index: 100;

Expand All @@ -40,9 +41,12 @@
}

&-inner {
font-size: 0.825rem;
background-color: @validation-success;
color: @validation-success-inverted;
opacity: 0;
padding: 0.375rem;
border-radius: 0.125rem;
}

&-inner .description {
Expand All @@ -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;
}
Expand Down