diff --git a/js/accessibility/AccessibilityTree.js b/js/accessibility/AccessibilityTree.js index b779a2ee3..b1f1ae5e5 100644 --- a/js/accessibility/AccessibilityTree.js +++ b/js/accessibility/AccessibilityTree.js @@ -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; diff --git a/js/accessibility/AccessibilityUtil.js b/js/accessibility/AccessibilityUtil.js index a7b928b38..d29232ff0 100644 --- a/js/accessibility/AccessibilityUtil.js +++ b/js/accessibility/AccessibilityUtil.js @@ -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'; @@ -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'; @@ -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, diff --git a/js/accessibility/AccessibleInstance.js b/js/accessibility/AccessibleInstance.js index 37ede52c9..530fda647 100644 --- a/js/accessibility/AccessibleInstance.js +++ b/js/accessibility/AccessibleInstance.js @@ -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 ) { diff --git a/js/accessibility/AccessiblePeer.js b/js/accessibility/AccessiblePeer.js index 82df5ee47..5baa9aae9 100644 --- a/js/accessibility/AccessiblePeer.js +++ b/js/accessibility/AccessiblePeer.js @@ -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; } @@ -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(); }; @@ -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 ) { @@ -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 @@ -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 @@ -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 ); @@ -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; @@ -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 @@ -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(); diff --git a/js/display/Display.js b/js/display/Display.js index a67f4ed8d..0af5aeab6 100644 --- a/js/display/Display.js +++ b/js/display/Display.js @@ -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' ); @@ -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 ); @@ -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 ); } diff --git a/js/input/Input.js b/js/input/Input.js index 3c30fc617..43a2912cc 100644 --- a/js/input/Input.js +++ b/js/input/Input.js @@ -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; + } } }