Skip to content

Commit

Permalink
added first support for multiple top level HTMLElements for a single …
Browse files Browse the repository at this point in the history
…peer, support #715
  • Loading branch information
zepumph committed Jun 16, 2018
1 parent 5fb9122 commit 79ffa41
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 64 deletions.
42 changes: 42 additions & 0 deletions js/accessibility/AccessibilityUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,48 @@ define( function( require ) {
return tagNameSupportsContent( tagName );
},

/**
* Helper function to remove multiple HTMLElements from another HTMLElement
* @param {HTMLElement} element
* @param {Array.<HTMLElement>} childrenToRemove
*/
removeElements: function( element, childrenToRemove ) {

for ( var i = 0; i < childrenToRemove.length; i++ ) {
var childToRemove = element[ i ];

// TODO: very inefficient error checking, n^2 time, uh oh
if ( assert ) {

var hasChild = false;
for ( var j = 0; j < element.childNodes; j++ ) {
var child = element[ j ];
if ( child === childToRemove ) {
hasChild = true;
}
}
assert( hasChild, 'element does not contain child to be removed: ', child );
}

element.removeChild( childToRemove );
}

},

/**
* Helper function to add multiple elements as children to a parent
* @param {HTMLElement} element - to add children to
* @param {Array.<HTMLElement>} childrenToAdd
* @param {HTMLElement} [beforeThisElement] - if not supplied, the insertBefore call will just use 'null'
*/
insertElements: function( element, childrenToAdd, beforeThisElement ) {

for ( var i = 0; i < childrenToAdd.length; i++ ) {
var childToAdd = childrenToAdd[ i ];
element.insertBefore( childToAdd, beforeThisElement || null );
}
},

TAGS: {
INPUT: INPUT_TAG,
LABEL: LABEL_TAG,
Expand Down
57 changes: 34 additions & 23 deletions js/accessibility/AccessibleInstance.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
define( function( require ) {
'use strict';

var AccessibilityUtil = require( 'SCENERY/accessibility/AccessibilityUtil' );
var AccessiblePeer = require( 'SCENERY/accessibility/AccessiblePeer' );
var cleanArray = require( 'PHET_CORE/cleanArray' );
var Events = require( 'AXON/Events' );
Expand Down Expand Up @@ -209,7 +210,7 @@ define( function( require ) {
for ( var i = 0; i < accessibleInstances.length; i++ ) {
// Append the container parent to the end (so that, when provided in order, we don't have to resort below
// when initializing).
this.peer.primarySibling.insertBefore( accessibleInstances[ i ].peer.getContainerParent(), null );
AccessibilityUtil.insertElements( this.peer.primarySibling, accessibleInstances[ i ].peer.topLevelElements );
}

if ( hadChildren ) {
Expand Down Expand Up @@ -347,13 +348,12 @@ define( function( require ) {
* @private
*/
updateVisibility: function() {
var parentElement = this.peer.getContainerParent();
parentElement.hidden = this.invisibleCount > 0;
this.peer.setVisible( this.invisibleCount <= 0 );

// if we hid a parent element, blur focus if active element was an ancestor
if ( parentElement.hidden ) {
if ( parentElement.contains( document.activeElement ) ) {
scenery.Display.focus = null;
if ( !this.peer.isVisible() ) {
if ( this.peer.primarySibling.contains( document.activeElement ) ) { // still true if activeElement is this primary sibling
scenery.Display.focus = null; // TODO is this the best way to blur a focus? shouldn't we `document.activeElement.blur()` or something?
}
}

Expand All @@ -374,8 +374,8 @@ define( function( require ) {
* @returns {boolean}
*/
isGloballyVisible: function() {
if ( this.peer.getContainerParent().hidden ) {
return false;
if ( this.peer.isVisible() ) {
return true;
}
if ( this.parent ) {
return this.parent.isGloballyVisible();
Expand All @@ -402,20 +402,20 @@ define( function( require ) {
var potentialInstances = node.accessibleInstances;

instanceLoop:
for ( i = 0; i < potentialInstances.length; i++ ) {
var potentialInstance = potentialInstances[ i ];
if ( potentialInstance.parent !== this ) {
continue instanceLoop;
}

for ( var j = 0; j < trail.length; j++ ) {
if ( trail.nodes[ j ] !== potentialInstance.trail.nodes[ j + potentialInstance.trail.length - trail.length ] ) {
for ( i = 0; i < potentialInstances.length; i++ ) {
var potentialInstance = potentialInstances[ i ];
if ( potentialInstance.parent !== this ) {
continue instanceLoop;
}
}

instances.push( potentialInstance );
}
for ( var j = 0; j < trail.length; j++ ) {
if ( trail.nodes[ j ] !== potentialInstance.trail.nodes[ j + potentialInstance.trail.length - trail.length ] ) {
continue instanceLoop;
}
}

instances.push( potentialInstance );
}

assert && assert( instances.length <= 1, 'If we select more than one this way, we have problems' );
}
Expand Down Expand Up @@ -448,12 +448,21 @@ define( function( require ) {

// Reorder DOM elements in a way that doesn't do any work if they are already in a sorted order.
var primarySibling = this.peer.primarySibling;

// Since there isn't a 1x1 correlation between a peer and an HTMLElement to be added to the parent, keep track of how many children you've added to the parent
var totalElementChildren = 0;
for ( var n = this.children.length - 1; n >= 0; n-- ) {
var peerDOMElement = this.children[ n ].peer.getContainerParent();
if ( peerDOMElement === primarySibling.childNodes[ n ] ) {
var peerDOMElements = this.children[ n ].peer.topLevelElements;


// TODO: we can't make this assumption anymore, because there isn't a 1X1 correlation to peer's and top level HTMLElements for the peer
if ( peerDOMElements[ 0 ] === primarySibling.childNodes[ totalElementChildren + 1 ] ) {
totalElementChildren += peerDOMElements.length;
continue;
}
primarySibling.insertBefore( peerDOMElement, primarySibling.childNodes[ n + 1 ] );
totalElementChildren += peerDOMElements.length;
AccessibilityUtil.insertElements( primarySibling, peerDOMElements, primarySibling.childNodes[ totalElementChildren + 1 ] );
// primarySibling.insertBefore( peerDOMElements, primarySibling.childNodes[ n + 1 ]);
}
},

Expand All @@ -469,9 +478,10 @@ define( function( require ) {

// Disconnect DOM and remove listeners
if ( !this.isRootInstance ) {

// remove this peer's primary sibling DOM Element (or its container parent) from the parent peer's
// primary sibling (or its child container)
this.parent.peer.primarySibling.removeChild( this.peer.getContainerParent() );
AccessibilityUtil.removeElements( this.parent.peer.primarySibling, this.peer.topLevelElements );

for ( var i = 0; i < this.relativeNodes.length; i++ ) {
this.relativeNodes[ i ].offStatic( 'accessibleDisplays', this.relativeListeners[ i ] );
Expand Down Expand Up @@ -621,6 +631,7 @@ define( function( require ) {
}
return fakeInstances;
}

return {
node: null,
children: createFakeTree( rootNode )
Expand Down
103 changes: 89 additions & 14 deletions js/accessibility/AccessiblePeer.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,23 @@ define( function( require ) {
// the label and description content.
this.containerParent = options.containerParent;

// @public {Array.<HTMLElement>} Rather than guarantee that a peer is a tree with a root DOMElement,
// allow multiple HTMLElements at the top level of the peer. This is used for sorting the instance
this.topLevelElements = [];

if ( this.containerParent ) {
// The first child of the container parent element should be the peer dom element
// if undefined, the insertBefore method will insert the primarySiblingDOMElement as the first child
var primarySiblingDOMElement = this.primarySibling;
var firstChild = this.containerParent.children[ 0 ] || null;
this.containerParent.insertBefore( primarySiblingDOMElement, firstChild );
this.containerParent.insertBefore( primarySiblingDOMElement, firstChild );

this.topLevelElements = [ this.containerParent ];
}
else {

// Wean out any null siblings
this.topLevelElements = [ this.labelSibling, this.descriptionSibling, this.primarySibling ].filter( function( i ) { return i; } );
}

// @private {boolean} - Whether we are currently in a "disposed" (in the pool) state, or are available to be
Expand Down Expand Up @@ -133,19 +144,6 @@ define( function( require ) {
}
},

/**
* Get the container parent or the peer's dom element direclty. Used for sorting.
* @public (scenery-internal)
*
* @returns {HTMLElement}
*/
getContainerParent: function() {
return this.containerParent || this.primarySibling;
},

getTopLevelElements: function(){
},

/**
* Get an element on this node, looked up by the association flag passed in.
* @public (scenery-internal)
Expand All @@ -171,6 +169,83 @@ define( function( require ) {

return htmlElement;
},
/**
* Called by invalidateAccessibleContent. The contentElement will either be a
* label or description element. The contentElement will be sorted relative to the primarySibling. Its placement
* will also depend on whether or not this node wants to append this element,
* see setAppendLabel() and setAppendDescription(). By default, the "content" element will be placed before the
* primarySibling.
*
*
* @param {HTMLElement} contentElement
* @param {boolean} appendElement
*/
arrangeContentElement: function( contentElement, appendElement ) {

// if there is a containerParent
if ( this.topLevelElements[ 0 ] === this.containerParent ) {
assert && assert( this.topLevelElements.length === 1 );

if ( appendElement ) {
this.containerParent.appendChild( contentElement );
}
else {
this.containerParent.insertBefore( contentElement, this.primarySibling );
}
}

// If there are multiple top level nodes
else {
assert && assert( this.topLevelElements.indexOf( contentElement ) >= 0, 'element is not part of this peer, thus cannot be arranged' );

// keep this.topLevelElements in sync
this.topLevelElements = this.topLevelElements.splice( this.topLevelElements.indexOf( contentElement ), 1 );

var indexOffset = appendElement ? 1 : -1;
indexOffset = indexOffset < 0 ? 0 : indexOffset; //support primarySibling in the first position
this.topLevelElements = this.topLevelElements.splice( this.topLevelElements.indexOf( this.primarySibling ) + indexOffset, contentElement );


// TODO tell the parent that things changed. DO WE NEED TO DO THIS? BECAUSE I THINK THAT THIS IS ALWAYS CALLED BEFORE THE PEER IS ADDED TO THE INSTANCE AND SORTED

}
},


/**
* Is this peer hidden in the PDOM
* @returns {boolean}
*/
isVisible: function() {

// TODO: assert if some visible and some are not

var visibleElements = 0;
this.topLevelElements.forEach( function( element ) {
if ( !element.hidden ) {
visibleElements += 1;
}
} );

return visibleElements === this.topLevelElements.length;
},

/**
*
* @param {boolean} visible
*/
setVisible: function( visible ) {

this.topLevelElements.forEach( function( element ) {

if ( visible ) {
element.removeAttribute( 'hidden' );
}
else {
element.setAttribute( 'hidden', '' );
}
} );
},

/**
* Removes external references from this peer, and places it in the pool.
Expand Down
29 changes: 2 additions & 27 deletions js/accessibility/invalidateAccessibleContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,8 @@ define( function( require ) {
}

// insert the label and description elements in the correct location if they exist
labelSibling && insertContentElement( accessiblePeer, labelSibling, self._appendLabel );
descriptionSibling && insertContentElement( accessiblePeer, descriptionSibling, self._appendDescription );
labelSibling && accessiblePeer.arrangeContentElement( labelSibling, self._appendLabel );
descriptionSibling && accessiblePeer.arrangeContentElement( descriptionSibling, self._appendDescription );

// Default the focus highlight in this special case to be invisible until selected.
if ( self._focusHighlightLayerable ) {
Expand Down Expand Up @@ -247,31 +247,6 @@ define( function( require ) {
return domElement;
}

/**
* Called by invalidateAccessibleContent. "this" will be bound by call. The contentElement will either be a
* label or description element. The contentElement will be sorted relative to the primary sibling in its
* containerParent. Its placement will also depend on whether or not this node wants to append this element,
* see setAppendLabel() and setAppendDescription(). By default, the "content" element will be placed before the
* primary sibling.
*
*
* @param {AccessiblePeer} accessiblePeer
* @param {HTMLElement} contentElement
* @param {boolean} appendElement
*/
function insertContentElement( accessiblePeer, contentElement, appendElement ) {
assert && assert( accessiblePeer.containerParent, 'Cannot add sibling if there is no container element' );
if ( appendElement ) {
accessiblePeer.containerParent.appendChild( contentElement );
}
else if ( accessiblePeer.containerParent === accessiblePeer.primarySibling.parentNode ) {
accessiblePeer.containerParent.insertBefore( contentElement, accessiblePeer.primarySibling );
}
else {
assert && assert( false, 'no append flag for DOM Element, and container parent did not match primary sibling' );
}
}

scenery.register( 'invalidateAccessibleContent', invalidateAccessibleContent );


Expand Down

0 comments on commit 79ffa41

Please sign in to comment.