diff --git a/js/accessibility/AccessibilityTree.js b/js/accessibility/AccessibilityTree.js index ea31de694..81a0dd584 100644 --- a/js/accessibility/AccessibilityTree.js +++ b/js/accessibility/AccessibilityTree.js @@ -394,6 +394,49 @@ define( function( require ) { } }, + /** + * Debugging - after the all AccessibleInstances have had CSS transforms updated, make sure that none of the + * instances are still marked dirty + * + * @param {AccessibleInstance} rootInstance + */ + ensureNoDirtyInstances: function( rootInstance ) { + assert && assert( !rootInstance.hasDirtyTransform, 'dirty transform for node ' + rootInstance.node ); + + for ( var i = 0; i < rootInstance.children.length; i++ ) { + var instance = rootInstance.children[ i ]; + AccessibilityTree.ensureNoDirtyInstances( instance ); + } + }, + + /** + * Update CSS transforms on DOM elements in the PDOM. Does a depth first search for all descendants on the rootNode + * marked with descendantHasDirtyTransform until it finds the descendant marked with hasDirtyTransform. + * Once it finds this, it will update the CSS transforms on that AccessibleInstance and all descendants (in that + * order because parents need to be updated before children do to the computation of the matrix that transforms + * DOM to the local node coordinate frame). + * + * @private (scenery-internal) + */ + updateCSSTransforms: function( rootAccessibleInstance ) { + + // now that we have completed the search, we don't have to search down this subtree next time + rootAccessibleInstance.descendantHasDirtyTransform = false; + + if ( rootAccessibleInstance.hasDirtyTransform ) { + console.log( rootAccessibleInstance.node ); + AccessibleInstance.updateCSSTransformsForSubTree( rootAccessibleInstance ); + } + else { + for ( var i = 0; i < rootAccessibleInstance.children.length; i++ ) { + var child = rootAccessibleInstance.children[ i ]; + if ( child.hasDirtyTransform || child.descendantHasDirtyTransform ) { + AccessibilityTree.updateCSSTransforms( rootAccessibleInstance.children[ i ] ); + } + } + } + }, + /** * Prepares for an a11y-tree-changing operation (saving some state). * @private diff --git a/js/accessibility/AccessibleInstance.js b/js/accessibility/AccessibleInstance.js index 9a94be6d7..16abfc6e1 100644 --- a/js/accessibility/AccessibleInstance.js +++ b/js/accessibility/AccessibleInstance.js @@ -123,6 +123,14 @@ define( function( require ) { // node, identity until siblings have client bounds this.domToLocalMatrix = Matrix3.IDENTITY; + // @private - flag that indicates that this AccessibleInstance has a peer with accessible content + // that is out of date and needs to be updated in the next animation frame + this.hasDirtyTransform = false; + + // @private - flag that indicates this AccessibleInstance has a some descendant with a dirty transform + // that needs to be updated + this.descendantHasDirtyTransform = false; + // PURELY FOR DEBUGGING display.rootNode.setOpacity( 0.2 ); @@ -207,7 +215,8 @@ define( function( require ) { this.primaryObserver = new MutationObserver( function( mutations ) { for ( var i = 0; i < mutations.length; i++ ) { - self.updateCSSTransforms( self.peer.primarySibling ); + self.invalidateCSSTransforms(); + // self.updateCSSTransforms( self.peer.primarySibling ); } } ); this.primaryObserver.observe( this.peer.primarySibling, config ); @@ -216,7 +225,7 @@ define( function( require ) { if ( this.peer.descriptionSibling ) { this.descriptionObserver = new MutationObserver( function( mutations ) { for ( var i = 0; i < mutations.length; i++ ) { - self.updateCSSTransforms( self.peer.descriptionSibling ); + self.invalidateCSSTransforms(); } } ); this.descriptionObserver.observe( this.peer.descriptionSibling, config ); @@ -225,20 +234,18 @@ define( function( require ) { if ( this.peer.labelSibling ) { this.labelObserver = new MutationObserver( function( mutations ) { for ( var i = 0; i < mutations.length; i++ ) { - self.updateCSSTransforms( self.peer.labelSibling ); + self.invalidateCSSTransforms(); } } ); this.labelObserver.observe( this.peer.labelSibling, config ); } // @private {TransformTracker} - also update all css bounds when transform of this node changes - this.transformTracker = new TransformTracker( this.trail ); + this.transformTracker = new TransformTracker( this.trail, { isStatic: true } ); // @private {function} - the listener that updates css transforms, to be disposed this.transformListener = function() { - self.updateCSSTransforms( self.peer.primarySibling ); - self.peer.descriptionSibling && self.updateCSSTransforms( self.peer.descriptionSibling ); - self.peer.labelSibling && self.updateCSSTransforms( self.peer.labelSibling ); + self.invalidateCSSTransforms(); } this.transformTracker.addListener( this.transformListener ); } @@ -679,6 +686,21 @@ define( function( require ) { audit( AccessibleInstance.createFakeAccessibleTree( rootNode ), this ); }, + invalidateCSSTransforms: function() { + if ( !this.hasDirtyTransform ) { + + // mark that this instance needs to be updated + this.hasDirtyTransform = true; + + // mark all ancestors to indicate that this instance will require redrawing of CSS transforms + var parent = this.parent; + while ( parent ) { + parent.descendantHasDirtyTransform = true; + parent = parent.parent; + } + } + }, + /** * Update the CSS transform of the elements of the AccessiblePeer. Correct CSS transformation is required by * assistive technology to handle DOM events such as click and pointer down. @@ -694,7 +716,6 @@ define( function( require ) { var nodeBoundsFinite = localBounds.isFinite(); // if leaf is empty and there are still no dimensions, we will just use the identity matrix - // var localNodeTranslation = Matrix3.translation( localBounds.minX, localBounds.minY ); if ( clientWidth > 0 && clientHeight > 0 ) { // inefficient version, can potentially combine later to not create extra matrices that aren't used @@ -731,6 +752,34 @@ define( function( require ) { } } }, { + + /** + * TODO: Perhaps handle invalidation of individual Sibling elements, rather than all of them. Or maybe this is OK + * because we can mark dirty on a sibling by sibling basis and if node transform changes, then all of them are + * dirty anyway. + * + * @param {AccessibleInstance} rootInstance - root of the subtree whose transforms we are going to update + * @private (scenery-internal) + */ + updateCSSTransformsForSubTree: function( rootInstance ) { + + // no longer dirty for next time + rootInstance.hasDirtyTransform = false; + + // we are going to update the entire subtree, no instance down this sub tree need to be marked as having + // dirty descendants + rootInstance.descendantHasDirtyTransform = false; + + rootInstance.updateCSSTransforms( rootInstance.peer.primarySibling ); + rootInstance.peer.labelSibling && rootInstance.updateCSSTransforms( rootInstance.peer.labelSibling ); + rootInstance.peer.descriptionSibling && rootInstance.updateCSSTransforms( rootInstance.peer.descriptionSibling ); + + // now apply to all instance in the subtree (depth-first) + for ( var i = 0; i < rootInstance.children.length; i++ ) { + AccessibleInstance.updateCSSTransformsForSubTree( rootInstance.children[ i ] ); + } + }, + /** * Creates a fake AccessibleInstance-like tree structure (with the equivalent nodes and children structure). * For debugging. diff --git a/js/display/Display.js b/js/display/Display.js index 2c6543356..8fffd72ae 100644 --- a/js/display/Display.js +++ b/js/display/Display.js @@ -459,6 +459,13 @@ define( function( require ) { } } + if ( this._accessible ) { + + // make sure that DOM elements are correctly positioned with CSS transforms + AccessibilityTree.updateCSSTransforms( this._rootAccessibleInstance ); + AccessibilityTree.ensureNoDirtyInstances( this._rootAccessibleInstance ); + } + this._frameId++; if ( sceneryLog && scenery.isLoggingPerformance() ) {