Skip to content

Commit 47da3df

Browse files
authored
Fix: Allow "Show alt text" to work in Notify and Drawer (fix #119)
1 parent fa78f67 commit 47da3df

3 files changed

Lines changed: 118 additions & 52 deletions

File tree

js/helpers.js

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
export const HEADING_SELECTOR = 'h1,h2,h3,h4,h5,h6,[role=heading]';
2+
13
function findLabel($element) {
24
const id = $element.attr('id');
35
if (!id) return false;
46
const $label = $(`[for=${id}]`);
57
if (!$label.length) return false;
6-
return computeAccesibleName($label, true);
8+
return computeAccessibleName($label, true);
79
}
810

911
function getText(domElement) {
@@ -27,10 +29,10 @@ function followId($element, property) {
2729
if (!id) return false;
2830
const $toElement = $(`#${id}`);
2931
if (!$toElement.length) return false;
30-
return computeAccesibleName($toElement, true);
32+
return computeAccessibleName($toElement, true);
3133
}
3234

33-
function computeAccesibleName($element, allowText = false) {
35+
export function computeAccessibleName($element, allowText = false) {
3436
if ($element.is('input:not([type=checkbox], [type=radio]), select, [role=range], textarea') && $element.val()) return $element.val();
3537
const ariaHidden = $element.attr('aria-hidden');
3638
if (ariaHidden === 'true') return '<span class="u-nobr">N/A (hidden from assistive technologies)</span>';
@@ -48,38 +50,79 @@ function computeAccesibleName($element, allowText = false) {
4850
if (valueNow) return valueNow;
4951
const alt = $element.attr('alt');
5052
if (alt) return alt;
53+
const childAriaLabel = !$element.is(HEADING_SELECTOR) &&
54+
$element.find('.aria-label').first().text();
55+
if (childAriaLabel) return childAriaLabel;
5156
if (!allowText) return '';
5257
return computeHeadingLevel($element) + getText($element[0]);
5358
}
5459

5560
function computeHeadingLevel($element) {
56-
const $heading = $element.parents().add($element).filter('h1, h2, h3, h4, h5, h6, h7, [role=heading]');
61+
const $heading = $element.parents().add($element).filter(HEADING_SELECTOR);
5762
if (!$heading.length) return '';
5863
const headingLevel = parseInt($heading[0].tagName) || $heading.attr('aria-level');
5964
return `h${headingLevel}: `;
6065
}
6166

62-
function computeAccessibleDescription($element) {
67+
export function computeAccessibleDescription($element) {
6368
const describedByText = followId($element, 'aria-describedby');
6469
if (describedByText) return describedByText;
6570
return '';
6671
}
6772

68-
function getAnnotationPosition($element, $annotation) {
73+
export function getContainer($element) {
74+
const $fixedParent = $element.parents().add($element).filter((index, el) => $(el).css('position') === 'fixed');
75+
return $fixedParent.length ? $fixedParent : $('body');
76+
}
77+
78+
export function shouldAnnotate($element) {
79+
const shouldAnnotate = isVisible($element) && isReadable($element);
80+
return shouldAnnotate;
81+
}
82+
83+
function isVisible($element) {
84+
const isHeadingHeightZero = $element.is(HEADING_SELECTOR) && $element.height() === 0;
85+
const isVisible = !isHeadingHeightZero && isInDom($element) && $element.onscreen().onscreen;
86+
return isVisible;
87+
}
88+
89+
function isReadable($element) {
90+
const isImg = $element.is('img');
91+
const isAncestorAriaHidden = Boolean($element.parents().filter('[aria-hidden=true]').length);
92+
const isAriaHidden = Boolean($element.filter('[aria-hidden=true]').length);
93+
const isNotAriaHidden = Boolean($element.filter('[aria-hidden=false]').length);
94+
const hasAccessibleName = Boolean(computeAccessibleName($element) || computeAccessibleDescription($element));
95+
const isReadable = !isAncestorAriaHidden && (isNotAriaHidden || !isAriaHidden || isImg || (hasAccessibleName && !isAriaHidden));
96+
return isReadable;
97+
}
98+
99+
function isInDom($element) {
100+
const isInDom = $element.parents('html').length > 0;
101+
return isInDom;
102+
}
103+
104+
function isFixed($element) {
105+
const isFixed = Boolean($element.parents().add($element).filter((index, el) => $(el).css('position') === 'fixed').length);
106+
return isFixed;
107+
}
108+
109+
export function getAnnotationPosition($element, $annotation) {
110+
const $annotationContainer = getContainer($element);
111+
const containerBoundingRect = $annotationContainer[0].getBoundingClientRect();
69112
const targetBoundingRect = $element[0].getBoundingClientRect();
70-
const availableWidth = $('html')[0].clientWidth;
71-
const availableHeight = $('html')[0].clientHeight;
113+
const availableWidth = $annotationContainer.width();
114+
const availableHeight = $annotationContainer.height();
72115
const tooltipsWidth = $annotation.width();
73116
const tooltipsHeight = $annotation.height();
74117
const elementWidth = $element.width();
75118
const elementHeight = $element.height();
119+
const scrollOffsetTop = -containerBoundingRect.top + $annotationContainer.scrollTop();
120+
const scrollOffsetLeft = -containerBoundingRect.left + $annotationContainer.scrollLeft();
121+
76122
const canAlignBottom = targetBoundingRect.bottom + tooltipsHeight < availableHeight;
77123
const canAlignRight = targetBoundingRect.right + tooltipsWidth < availableWidth;
78124
const canAlignBottomRight = canAlignBottom && canAlignRight;
79125
const canBeContained = elementHeight === 0 || (elementHeight * elementWidth >= tooltipsHeight * tooltipsWidth) || $element.is('img');
80-
const isFixedPosition = Boolean($element.parents().add($element).filter((index, el) => $(el).css('position') === 'fixed').length);
81-
const scrollOffsetTop = isFixedPosition ? 0 : $(window).scrollTop();
82-
const scrollOffsetLeft = isFixedPosition ? 0 : $(window).scrollLeft();
83126
function getPosition() {
84127
if (canBeContained) {
85128
return {
@@ -93,7 +136,8 @@ function getAnnotationPosition($element, $annotation) {
93136
}
94137
if (!canAlignBottomRight) {
95138
// Find the 'corner' with the most space from the viewport edge
96-
const isTopPreferred = availableHeight - (targetBoundingRect.bottom + tooltipsHeight) < targetBoundingRect.top - tooltipsHeight;
139+
const isHardTop = isFixed($annotationContainer) && (containerBoundingRect.top < tooltipsHeight && targetBoundingRect.top < tooltipsHeight);
140+
const isTopPreferred = !isHardTop && (availableHeight - (targetBoundingRect.bottom + tooltipsHeight) < targetBoundingRect.top - tooltipsHeight);
97141
const isLeftPreferred = availableWidth - (targetBoundingRect.right + tooltipsWidth) < targetBoundingRect.left - tooltipsWidth;
98142
if (isTopPreferred && isLeftPreferred) {
99143
// Top left
@@ -131,7 +175,7 @@ function getAnnotationPosition($element, $annotation) {
131175
}
132176
// Bottom right, default
133177
return {
134-
className: 'is-right, is-bottom',
178+
className: 'is-right is-bottom',
135179
css: {
136180
left: targetBoundingRect.right + scrollOffsetLeft,
137181
top: targetBoundingRect.bottom + scrollOffsetTop,
@@ -140,7 +184,7 @@ function getAnnotationPosition($element, $annotation) {
140184
};
141185
}
142186
const position = getPosition();
143-
position.css.position = isFixedPosition ? 'fixed' : 'absolute';
187+
position.css.position = 'absolute';
144188
if (position.css.left < 0) position.css.left = 0;
145189
position.css.left += 'px';
146190
position.css.top += 'px';
@@ -149,8 +193,9 @@ function getAnnotationPosition($element, $annotation) {
149193
}
150194

151195
export default {
152-
computeAccesibleName,
196+
HEADING_SELECTOR,
197+
computeAccessibleName,
153198
computeAccessibleDescription,
154-
computeHeadingLevel,
155-
getAnnotationPosition
199+
getAnnotationPosition,
200+
getContainer
156201
};

js/toggle-alt-text.js

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
11
import Backbone from 'backbone';
22
import Adapt from 'core/js/adapt';
3-
import helpers from './helpers';
4-
5-
const computeAccesibleName = helpers.computeAccesibleName;
6-
const computeAccessibleDescription = helpers.computeAccessibleDescription;
7-
const getAnnotationPosition = helpers.getAnnotationPosition;
3+
import {
4+
HEADING_SELECTOR,
5+
computeAccessibleName,
6+
computeAccessibleDescription,
7+
getAnnotationPosition,
8+
getContainer,
9+
shouldAnnotate
10+
} from './helpers';
811

912
class Annotation extends Backbone.View {
1013

14+
events() {
15+
return {
16+
click: 'onClick'
17+
};
18+
}
19+
20+
onClick(event) {
21+
console.log('Annotation clicked for', this.$parent[0]);
22+
}
23+
1124
className() {
1225
return 'devtools__annotation';
1326
}
@@ -18,21 +31,27 @@ class Annotation extends Backbone.View {
1831

1932
initialize(options) {
2033
this.$parent = options.$parent;
34+
this.$container = getContainer(this.$parent);
2135
this.allowText = options.allowText;
2236
this.$el.data('annotating', this.$parent);
2337
this.$el.data('view', this);
2438
}
2539

2640
render() {
41+
function hash(name, description, position) {
42+
return name + description + position.className + position.css.top + position.css.left;
43+
}
2744
const template = Handlebars.templates.devtoolsAnnotation;
28-
const name = computeAccesibleName(this.$parent, this.allowText);
45+
const name = computeAccessibleName(this.$parent, this.allowText);
2946
const description = computeAccessibleDescription(this.$parent);
30-
this.$el.html(template({ name, description }));
31-
if (!name) this.$el.addClass('has-annotation-warning');
3247
const position = getAnnotationPosition(this.$parent, this.$el);
48+
if (this._last === hash(name, description, position)) return;
49+
this.$el.html(template({ name, description }));
50+
this.$el.toggleClass('has-annotation-warning', !name);
3351
this.$el.css(position.css);
3452
this.$el.removeClass('is-top is-left is-right is-bottom is-contained');
3553
this.$el.addClass(position.className);
54+
this._last = hash(name, description, position);
3655
}
3756

3857
showOutline() {
@@ -44,7 +63,6 @@ class Annotation extends Backbone.View {
4463
this.$parent.removeClass('devtools__annotation-outline');
4564
this.$el.removeClass('has-mouse-over');
4665
}
47-
4866
}
4967

5068
class AltText extends Backbone.Controller {
@@ -57,14 +75,10 @@ class AltText extends Backbone.Controller {
5775
onEnabled () {
5876
if (!Adapt.devtools.get('_isEnabled')) return;
5977
_.bindAll(this, 'onDomMutation', 'render', 'onMouseOver');
60-
this.mutations = [];
6178
this.mutated = false;
6279
this.listenTo(Adapt.devtools, 'change:_altTextEnabled', this.toggleAltText);
6380
$('body').append($('<div class="devtools__annotations" aria-hidden="true"></div>'));
64-
// if available we can use to avoid unnecessary checks
65-
if (typeof MutationObserver === 'function') {
66-
this.observer = new MutationObserver(this.onDomMutation);
67-
}
81+
this.observer = new MutationObserver(this.onDomMutation);
6882
}
6983

7084
connectObserver() {
@@ -89,10 +103,11 @@ class AltText extends Backbone.Controller {
89103
});
90104
}
91105
this.listenTo(Adapt, {
92-
'popup:closed notify:closed drawer:closed': this.onDomMutation,
93106
remove: this.removeAllAnnotations
94107
});
95108
$(window).on('scroll', this.onDomMutation);
109+
$(document).on('transitionend', this.onDomMutation);
110+
$(document).on('animationend', this.onDomMutation);
96111
$(document).on('mouseover', '*', this.onMouseOver);
97112
}
98113

@@ -124,8 +139,10 @@ class AltText extends Backbone.Controller {
124139
if (this.observer) {
125140
this.observer.disconnect();
126141
}
127-
this.stopListening(Adapt, 'popup:closed notify:closed drawer:closed', this.onDomMutation);
142+
this.stopListening(Adapt, 'remove', this.removeAllAnnotations);
128143
$(window).off('scroll', this.onDomMutation);
144+
$(document).off('transitionend', this.onDomMutation);
145+
$(document).off('animationend', this.onDomMutation);
129146
$(document).off('mouseover', '*', this.onMouseOver);
130147
}
131148

@@ -141,8 +158,13 @@ class AltText extends Backbone.Controller {
141158
}
142159

143160
addAnnotation($element, allowText) {
144-
const annotation = new Annotation({ $parent: $element, allowText });
145-
$('.devtools__annotations').append(annotation.$el);
161+
const annotation = new Annotation({
162+
$parent: $element,
163+
allowText
164+
});
165+
166+
annotation.$container.append(annotation.$el);
167+
146168
$element.data('annotation', annotation);
147169
$element.attr('data-annotated', true);
148170
this.updateAnnotation($element, annotation, allowText);
@@ -170,9 +192,7 @@ class AltText extends Backbone.Controller {
170192
const $element = $annotation.data('annotating');
171193
const annotation = $annotation.data('view');
172194
if (!$element) return;
173-
const isOutOfDom = ($element.parents('html').length === 0);
174-
const isHeadingHeightZero = $element.is('h1,h2,h3,h4,h5,h6,h7,[role=heading]') && $element.height() === 0;
175-
if (!isOutOfDom && ($element.onscreen().onscreen || isHeadingHeightZero)) return;
195+
if (shouldAnnotate($element)) return;
176196
this.removeAnnotation($element, annotation);
177197
});
178198
}
@@ -181,7 +201,7 @@ class AltText extends Backbone.Controller {
181201
annotation.render();
182202
}
183203

184-
onDomMutation(mutations) {
204+
onDomMutation() {
185205
if (this.mutated) return;
186206
requestAnimationFrame(this.render);
187207
this.mutated = true;
@@ -190,7 +210,7 @@ class AltText extends Backbone.Controller {
190210
render() {
191211
if (this.mutated === false) return;
192212
this.clearUpAnnotations();
193-
const $headings = $('h1,h2,h3,h4,h5,h6,h7,[role=heading]');
213+
const $headings = $(HEADING_SELECTOR);
194214
const $labelled = $([
195215
'.aria-label',
196216
'[alt]',
@@ -209,15 +229,8 @@ class AltText extends Backbone.Controller {
209229
.each((index, element) => {
210230
const $element = $(element);
211231
const annotation = $element.data('annotation');
212-
const isVisible = $element.onscreen().onscreen;
213-
const isParentAriaHidden = Boolean($element.parents().filter('[aria-hidden=true]').length);
214-
const isAriaHidden = Boolean($element.filter('[aria-hidden=true]').length);
215-
const isNotAriaHidden = Boolean($element.filter('[aria-hidden=false]').length);
216-
const isImg = $element.is('img');
217-
const allowText = $element.is('.aria-label,h1,h2,h3,h4,h5,h6,h7,[role=heading]');
218-
const isOutOfDom = ($element.parents('html').length === 0);
219-
const isHeadingHeightZero = $element.is('h1,h2,h3,h4,h5,h6,h7,[role=heading]') && $element.height() === 0;
220-
if (!isOutOfDom && (isVisible || isHeadingHeightZero) && (isNotAriaHidden || (!isAriaHidden && !isParentAriaHidden) || (isImg && !isParentAriaHidden))) {
232+
const allowText = $element.is(`.aria-label,${HEADING_SELECTOR}`);
233+
if (shouldAnnotate($element)) {
221234
if (!annotation) this.addAnnotation($element, allowText);
222235
else this.updateAnnotation($element, annotation, allowText);
223236
} else if (annotation) {

less/devtoolsAnnotation.less

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
// --------------------------------------------------
2626
// --------------------------------------------------
2727
.devtools__annotation {
28-
font-size: 0.875rem;
28+
font-size: 1rem;
29+
line-height: 1;
2930
pointer-events: none;
3031
z-index: 100;
3132

@@ -40,9 +41,12 @@
4041
}
4142

4243
&-inner {
44+
font-size: 0.825rem;
4345
background-color: @validation-success;
4446
color: @validation-success-inverted;
4547
opacity: 0;
48+
padding: 0.375rem;
49+
border-radius: 0.125rem;
4650
}
4751

4852
&-inner .description {
@@ -61,10 +65,14 @@
6165
}
6266

6367
.button {
64-
padding: @item-padding / 8;
68+
width: 1.25rem;
69+
height: 1.25rem;
70+
display: flex;
71+
justify-content: center;
72+
align-items: center;
6573
background-color: @validation-success;
6674
color: @validation-success-inverted;
67-
border-radius: 0.25rem;
75+
border-radius: 0.125rem;
6876
pointer-events: all;
6977
cursor: pointer;
7078
}

0 commit comments

Comments
 (0)