Skip to content

Commit

Permalink
mobilie a11y support behind a query parameter, see #852
Browse files Browse the repository at this point in the history
  • Loading branch information
jessegreenberg committed Aug 28, 2018
1 parent 30be78a commit f888972
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 63 deletions.
3 changes: 3 additions & 0 deletions js/accessibility/AccessibilityTree.js
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,9 @@ define( function( require ) {
* @private (scenery-internal)
*/
updateCSSTransformsForSubTree: function( rootInstance ) {

// for now, this should only ever be called experimentally, see https://github.com/phetsims/scenery/issues/852
assert && assert( window.phet && window.phet.chipper.queryParameters.mobileA11yTest, 'this should be hidden behind mobileA11yTest query parameter' );

// no longer dirty for next time
rootInstance.peer.transformDirty = false;
Expand Down
87 changes: 54 additions & 33 deletions js/accessibility/AccessibilityUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ define( function( require ) {
// modules
var Random = require( 'DOT/Random' );
var scenery = require( 'SCENERY/scenery' );
var Matrix3 = require( 'DOT/Matrix3' );

// constants
var NEXT = 'NEXT';
Expand Down Expand Up @@ -59,9 +60,12 @@ define( function( require ) {
// valid types of DOM events that can be added to a node
var DOM_EVENTS = [ 'input', 'change', 'click', 'keydown', 'keyup', 'focus', 'blur' ];

// a matrix in its CSS form that "hides" an HTMLElement by shifting it off screen and making it very small
var HIDING_MATRIX_CSS = Matrix3.translation( -1000, 0 ).timesMatrix( Matrix3.scaling( 0.1, 0.1 ) ).getCSSTransform();

// these elements require a minimum width to be visible in Safari, see https://github.com/phetsims/john-travoltage/issues/204
// NOTE: if transforming PDOM over display, this is not needed
// var ELEMENTS_REQUIRE_WIDTH = [ INPUT_TAG, A_TAG ];
var ELEMENTS_REQUIRE_WIDTH = [ INPUT_TAG, A_TAG ];

var ARIA_LABELLEDBY = 'aria-labelledby';
var ARIA_DESCRIBEDBY = 'aria-describedby';
Expand Down Expand Up @@ -458,42 +462,59 @@ define( function( require ) {

domElement.tabIndex = focusable ? 0 : -1;

// // Safari requires that certain input elements have dimension, otherwise it will not be keyboard accessible
// var upperCaseTagName = tagName.toUpperCase();
// NOTE: We don't want this for mobile a11y, but we will still need something like this if mobile a11y/PDOM
// transforms are not enabled
// if ( _.includes( ELEMENTS_REQUIRE_WIDTH, upperCaseTagName ) ) {
// domElement.style.width = '1px';
// domElement.style.height = '1px';
// }

// positioned absolutely, the bounds are defined relative to the top left at 0, 0 so our transformations are
// correct from DOM to local bounds
domElement.style.position = 'absolute';
domElement.style.top = '0';
domElement.style.left = '0';
domElement.style.padding = '0';
domElement.style.transformOrigin = 'left top';

// so that client width/height are exact
domElement.style.borderWidth = '0';

// so that text content can be positioned exactly without any margins
domElement.style.margin = '0';

// doesn't really impact behavior but looks a little nicer?
domElement.style.whiteSpace = 'nowrap';

// so that elements can never be seen visually, can comment this out to "see" transformed elements in the PDOM
// text and backgrounds of elements are made transparent where possible, and opacity takes care of the rest
// for things like radio buttons, check boxes, and others where color doesn't change element visuals
// domElement.style.color = 'Transparent';
// domElement.style.backgroundColor = 'Transparent';
// domElement.style.opacity = '0.0001';
// if transforming the PDOM elements for mobile a11y support, add style attributes to support the transform
// attribute
if ( window.phet && window.phet.chipper.queryParameters.mobileA11yTest ) {

// positioned absolutely, the bounds are defined relative to the top left at 0, 0 so our transformations are
// correct from DOM to local bounds
domElement.style.position = 'absolute';
domElement.style.top = '0';
domElement.style.left = '0';
domElement.style.padding = '0';
domElement.style.transformOrigin = 'left top';

// so that client width/height are exact
domElement.style.borderWidth = '0';

// so that text content can be positioned exactly without any margins
domElement.style.margin = '0';

// doesn't really impact behavior but looks a little nicer?
domElement.style.whiteSpace = 'nowrap';

// so that elements can never be seen visually, can comment this out to "see" transformed elements in the PDOM
// text and backgrounds of elements are made transparent where possible, and opacity takes care of the rest
// for things like radio buttons, check boxes, and others where color doesn't change element visuals
// domElement.style.color = 'Transparent';
// domElement.style.backgroundColor = 'Transparent';
// domElement.style.opacity = '0.0001';
}
else {

// Safari requires that certain input elements have dimension, otherwise it will not be keyboard accessible
var upperCaseTagName = tagName.toUpperCase();
if ( _.includes( ELEMENTS_REQUIRE_WIDTH, upperCaseTagName ) ) {
domElement.style.width = '1px';
domElement.style.height = '1px';
}
}


return domElement;
},

/**
* Make an HTML element "invisible" by shifting it way off screen and making it really small. By giving it non-zero
* bounds, it is still readable by assistive devices, particularly those that use CSS to determine if it should
* have content in the Accessibility tree.
*
* @param {HTMLElement}
*/
hideElement: function( element ) {
element.style.transform = HIDING_MATRIX_CSS;
},

TAGS: {
INPUT: INPUT_TAG,
LABEL: LABEL_TAG,
Expand Down
4 changes: 3 additions & 1 deletion js/accessibility/AccessibleInstance.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ define( function( require ) {
// prevent pointer events on PDOM elements, they should be captured by the sim
// But this prevents scanning with elements with finger when using a mobile device.
// TODO: We need to figure out a different way to do this.
// accessibilityContainer.style.pointerEvents = 'none';
if ( window.phet && !window.phet.chipper.queryParameters.mobileA11yTest ) {
accessibilityContainer.style.pointerEvents = 'none';
}

var self = this;
this.globalKeyListener = function( event ) {
Expand Down
48 changes: 26 additions & 22 deletions js/accessibility/AccessiblePeer.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,16 @@ define( function( require ) {
// the focus. It also will contain any children.
this._primarySibling = options.primarySibling;

// root is relatively styled so that descendants can be positioned absolutely
this._primarySibling.style.position = 'relative';
// if testing mobile a11y, give the primary sibling style attributes to support transforming the HTML'
if ( window.phet && phet.chipper.queryParameters.mobileA11yTest ) {

// root is relatively styled so that descendants can be positioned absolutely
this._primarySibling.style.position = 'relative';

// TODO: another way to make sure that DOM is on top of display? Is this critical?
this._primarySibling.style.zIndex = '10000';
}

// TODO: another way to make sure that DOM is on top of display? Is this critical?
this._primarySibling.style.zIndex = '10000';
return this;
}

Expand All @@ -168,8 +173,17 @@ define( function( require ) {
// @private {TransformTracker} - update CSS bounds when transform of this node changes
this.transformTracker = new TransformTracker( this.trail );

// @private - must be removed on disposal
// attach a MutationObserver that will update the transformation of the element when content or children change
// only create new one if not from pool
var self = this;
this._primaryObserver = this._primaryObserver || new MutationObserver( function( mutations ) {

// there is no need to iterate over the entire list of mutations because a single mutation is all that is
// required to mark dirty for the next Display.updateDisplay
self.invalidateCSSTransforms();
} );

// @private - must be removed on disposal
this.transformListener = this.transformListener || function() {
self.invalidateCSSTransforms();
};
Expand Down Expand Up @@ -211,15 +225,6 @@ define( function( require ) {
} );
primarySibling.id = uniqueId;

// attach a MutationObserver that will update the transformation of the element when content or children change
var self = this;
var primaryObserver = new MutationObserver( function( mutations ) {

// there is no need to iterate over the entire list of mutations because a single mutation is all that is
// required to mark dirty for the next Display.updateDisplay
self.invalidateCSSTransforms();
} );

// create the container parent for the dom siblings
var containerParent = null;
if ( options.containerTagName ) {
Expand All @@ -239,7 +244,7 @@ define( function( require ) {
labelSibling.id = 'label-' + uniqueId;

// labels are just pushed off screen, only inputs are required to be transformed
labelSibling.style.transform = Matrix3.translation( -1000, 0 ).timesMatrix( Matrix3.scaling( 0.1, 0.1 ) ).getCSSTransform();
AccessibilityUtil.hideElement( labelSibling );
}

// create the description DOM element representing this instance
Expand All @@ -250,7 +255,7 @@ define( function( require ) {

// descriptions are just pushed off screen, only inputs are required to be transformed
// TODO: factor this out
descriptionSibling.style.transform = Matrix3.translation( -1000, 0 ).timesMatrix( Matrix3.scaling( 0.1, 0.1 ) ).getCSSTransform();
AccessibilityUtil.hideElement( descriptionSibling );
}

// assign elements
Expand All @@ -259,11 +264,10 @@ define( function( require ) {
this._descriptionSibling = descriptionSibling;
this._containerParent = containerParent;

// assign listeners (to be removed during disposal)
this._primaryObserver = primaryObserver;

this.orderElements( options );

// assign listeners (to be removed or detached during disposal)

// @private {function} - Referenced for disposal
this.focusEventListener = this.focusEventListener || this.onFocus.bind( this );
this.blurEventListener = this.blurEventListener || this.onBlur.bind( this );
Expand Down Expand Up @@ -833,6 +837,7 @@ define( function( require ) {
*/
updateCSSTransforms: function() {
assert && assert( this.primarySibling, 'a primary sibling should be defined to receive a transform' );
assert && assert( window.phet && phet.chipper.queryParameters.mobileA11yTest, 'should only be run when testing' );

var localBounds = this.node.localBounds;
var clientWidth = this.primarySibling.clientWidth;
Expand Down Expand Up @@ -873,9 +878,9 @@ define( function( require ) {
else {

// If the node has no bounds, this sibling is purely fro DOM content or scene graph structure and
// doesn't have visual representation. Just hide this by shrinking the content nearly nothing
// doesn't have visual representation. Just hide this element.
if ( clientHeight !== 0 && clientWidth !== 0 ) {
this.primarySibling.style.transform = Matrix3.translation( -1000, 0 ).timesMatrix( Matrix3.scaling( 0.1, 0.1 ) ).getCSSTransform();
AccessibilityUtil.hideElement( this.primarySibling );
}

// if the node has no bounds, this sibling is purely for DOM content or scene graph structure and doesn't
Expand Down Expand Up @@ -915,7 +920,6 @@ define( function( require ) {
this._labelSibling = null;
this._descriptionSibling = null;
this._containerParent = null;
this._primaryObserver = null; // TODO: Actually, can this be set up once and not recreated?

// for now
this.freeToPool();
Expand Down
17 changes: 12 additions & 5 deletions js/display/Display.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ define( function( require ) {
var Features = require( 'SCENERY/util/Features' );
var Node = require( 'SCENERY/nodes/Node' );
var scenery = require( 'SCENERY/scenery' );
var SceneryStyle = require( 'SCENERY/util/SceneryStyle' );
require( 'SCENERY/display/BackboneDrawable' );
require( 'SCENERY/display/CanvasBlock' );
require( 'SCENERY/display/CanvasSelfDrawable' );
Expand Down Expand Up @@ -271,11 +272,16 @@ define( function( require ) {
this._domElement.setAttribute( 'aria-role', 'application' );
}

// whether or not we are testing mobile a11y support by transforming elements in the PDOM
this._mobileA11yTest = ( window.phet && phet.chipper.queryParameters.mobileA11yTest );

// make the PDOM invisible in the browser - it has some width and is shifted off screen so that AT can read the
// formatting tags, see https://github.com/phetsims/scenery/issues/730
// We do not want to hide the PDOM in this way for mobile a11y. Commented out for that work, but we will
// probably need something like this when PDOM transformations are not enabled
// SceneryStyle.addRule( '.accessibility * { position: relative; left: -1000px; top: 0; width: 250px; height: 0; clip: rect(0,0,0,0); pointerEvents: none }' );
// We do not want to hide the PDOM in this way for mobile a11y, once that is integrated full, this can be removed
// see
if ( !this._mobileA11yTest ) {
SceneryStyle.addRule( '.accessibility * { position: relative; left: -1000px; top: 0; width: 250px; height: 0; clip: rect(0,0,0,0); pointerEvents: none }' );
}

this._focusRootNode = new Node();
this._focusOverlay = new FocusOverlay( this, this._focusRootNode );
Expand Down Expand Up @@ -460,9 +466,10 @@ define( function( require ) {
}
}

if ( this._accessible ) {
if ( this._accessible && this._mobileA11yTest ) {

// make sure that accessible DOM elements are correctly positioned with CSS transforms
// Update any out of date CSS transforms for AccessiblePeer content - for now only doing this while exploring
// a solution for a11y on mobile devices
AccessibilityTree.updateDirtyCSSTransforms( this._rootAccessibleInstance );
}

Expand Down
5 changes: 3 additions & 2 deletions js/input/Input.js
Original file line number Diff line number Diff line change
Expand Up @@ -1123,8 +1123,9 @@ define( function( require ) {

// TODO: This makes the focus go away when interacting with an HTML element with a pointer, which
// is undesirable for mobile a11y. We need a better way to manage this.
// scenery.Display.focus = null;

if ( !( window.phet && phet.chipper.queryParameters.mobileA11yTest ) ) {
scenery.Display.focus = null;
}
}
}

Expand Down

0 comments on commit f888972

Please sign in to comment.