Skip to content

Commit

Permalink
added active descendant support, see phetsims/sun#314 #873
Browse files Browse the repository at this point in the history
  • Loading branch information
zepumph committed Oct 27, 2018
1 parent f0f1fb7 commit 4a1bc1e
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 26 deletions.
1 change: 1 addition & 0 deletions js/accessibility/A11yBehaviorFunctionDef.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
* REVIEW: inputType (setter doesn't call onAccessibleContentChange, can introduce buggy behavior)
* REVIEW: ariaLabelledbyAssociations (ok, probably won't override with behaviors?)
* REVIEW: ariaDescribedbyAssociations (ok, probably won't override with behaviors?)
* REVIEW: activeDescendantAssociations (ok, probably won't override with behaviors?)
* REVIEW: accessibleAttributes (we WANT to override these with behavior, but can't right now) from onAttributeChange
* REVIEW: inputValue (doesn't look like behaviors can override)
* REVIEW: node.focusHighlight (looks like updates to this are completely broken)
Expand Down
166 changes: 164 additions & 2 deletions js/accessibility/Accessibility.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ define( function( require ) {
'focusable', // Sets whether or not the node can receive keyboard focus, see setFocusable()
'accessibleOrder', // Modifies the order of accessible navigation, see setAccessibleOrder()
'ariaLabelledbyAssociations', // sets the list of aria-labelledby associations between from this node to others (including itself), see setAriaLabelledbyAssociations
'ariaDescribedbyAssociations' // sets the list of aria-describedby associations between from this node to others (including itself), see setAriaDescribedbyAssociations
'ariaDescribedbyAssociations', // sets the list of aria-describedby associations between from this node to others (including itself), see setAriaDescribedbyAssociations
'activeDescendantAssociations' // sets the list of aria-activedescendant associations between from this node to others (including itself), see setActiveDescendantAssociations
];

var Accessibility = {
Expand Down Expand Up @@ -353,7 +354,7 @@ define( function( require ) {
// {Array.<Node>}
this._nodesThatAreAriaLabelledbyThisNode = [];

// @private {Array.<Object>} - Keep track of what this Node is aria-descripbedby via "associationObjects"
// @private {Array.<Object>} - Keep track of what this Node is aria-describedby via "associationObjects"
// see addAriaDescribedbyAssociation for why we support more than one association.
this._ariaDescribedbyAssociations = [];

Expand All @@ -364,6 +365,17 @@ define( function( require ) {
// {Array.<Node>}
this._nodesThatAreAriaDescribedbyThisNode = [];

// @private {Array.<Object>} - Keep track of what this Node is aria-activedescendant via "associationObjects"
// see addActiveDescendantAssociation for why we support more than one association.
this._activeDescendantAssociations = [];

// Keep a reference to all nodes that are aria-activedescendant this node, i.e. that have store one of this Node's
// peer HTMLElement's id in their peer HTMLElement's aria-activedescendant attribute. This way we can tell other
// nodes to update their aria-activedescendant associations when this Node rebuilds its accessible content.
// @private
// {Array.<Node>}
this._nodesThatAreActiveDescendantToThisNode = [];

// @private {boolean|null} - whether or not this node's DOM element has been explicitly set to receive focus from
// tab navigation. Sets the tabIndex attribute on the node's DOM element. Setting to false will not remove the
// node's DOM from the document, but will ensure that it cannot receive focus by pressing 'tab'. Several
Expand Down Expand Up @@ -567,6 +579,7 @@ define( function( require ) {
// Clear out aria association attributes, which hold references to other nodes.
this.setAriaLabelledbyAssociations( [] );
this.setAriaDescribedbyAssociations( [] );
this.setActiveDescendantAssociations( [] );

this.removeAllAccessibleInputListeners();
},
Expand Down Expand Up @@ -1871,6 +1884,154 @@ define( function( require ) {
},
get nodesThatAreAriaDescribedbyThisNode() { return this.getNodesThatAreAriaDescribedbyThisNode(); },



/**
* @public
* @param {Array.<Object>} activeDescendantAssociations - list of associationObjects, see this._activeDescendantAssociations.
*/
setActiveDescendantAssociations: function( activeDescendantAssociations ) {
var associationObject;
if ( assert ) {
assert( Array.isArray( activeDescendantAssociations ) );
for ( var j = 0; j < activeDescendantAssociations.length; j++ ) {
associationObject = activeDescendantAssociations[ j ];
assert && AccessibilityUtil.validateAssociationObject( associationObject );
}
}

// if the list isn't the same, TODO: make order in the list not matter
if ( !_.isEqual( activeDescendantAssociations, this._activeDescendantAssociations ) ) {

var beforeOnly = []; // Will hold all nodes that will be removed.
var afterOnly = []; // Will hold all nodes that will be "new" children (added)
var inBoth = []; // Child nodes that "stay". Will be ordered for the "after" case.
var i;

// get a difference of the desired new list, and the old
arrayDifference( activeDescendantAssociations, this._activeDescendantAssociations, afterOnly, beforeOnly, inBoth );

// remove each current associationObject that isn't in the new list
for ( i = 0; i < beforeOnly.length; i++ ) {
associationObject = beforeOnly[ i ];
this.removeActiveDescendantAssociation( associationObject );
}

assert && assert( this._activeDescendantAssociations.length === inBoth.length,
'Removing associations should not have triggered other association changes' );

// add each association from the new list that hasn't been added yet
for ( i = 0; i < afterOnly.length; i++ ) {
var activeDescendantAssociation = activeDescendantAssociations[ i ];
this.addActiveDescendantAssociation( activeDescendantAssociation );
}

// TODO maybe reorder them, but right now order doesn't seem to matter
}
},
set activeDescendantAssociations( activeDescendantAssociations ) { return this.setActiveDescendantAssociations( activeDescendantAssociations ); },

/**
* @public
* @returns {Array.<Object>} - the list of current association objects
*/
getActiveDescendantAssociations: function() {
return this._activeDescendantAssociations;
},
get activeDescendantAssociations() { return this.getActiveDescendantAssociations(); },

/**
* Add an aria-activeDescendant association to this node. The data in the associationObject will be implemented like
* "a peer's HTMLElement of this Node (specified with the string constant stored in `thisElementName`) will have an
* aria-activeDescendant attribute with a value that includes the `otherNode`'s peer HTMLElement's id (specified with
* `otherElementName`)."
*
* @param {Object} associationObject - with key value pairs like
* { otherNode: {Node}, otherElementName: {string}, thisElementName: {string } }
* see AccessiblePeer for valid element names.
*/
addActiveDescendantAssociation: function( associationObject ) {
assert && AccessibilityUtil.validateAssociationObject( associationObject );

// TODO: assert if this associationObject is already in the association objects list! https://github.com/phetsims/scenery/issues/832
this._activeDescendantAssociations.push( associationObject ); // Keep track of this association.

// Flag that this node is is being described by the other node, so that if the other node changes it can tell
// this node to restore the association appropriately.
associationObject.otherNode._nodesThatAreActiveDescendantToThisNode.push( this );

// update the accessiblePeers with this aria-activeDescendant association
this.updateActiveDescendantAssociationsInPeers();
},

/**
* Remove an aria-activeDescendant association object, see addActiveDescendantAssociation for more details
* @public
*/
removeActiveDescendantAssociation: function( associationObject ) {
assert && assert( _.includes( this._activeDescendantAssociations, associationObject ) );

// remove the
var removedObject = this._activeDescendantAssociations.splice( _.indexOf( this._activeDescendantAssociations, associationObject ), 1 );

// remove the reference from the other node back to this node because we don't need it anymore
removedObject[ 0 ].otherNode.removeNodeThatIsActiveDescendantThisNode( this );

this.updateActiveDescendantAssociationsInPeers();
},

/**
* Remove the reference to the node that is using this Node's ID as an aria-activeDescendant value
* @param {Node} node
* @public (scenery-internal)
*/
removeNodeThatIsActiveDescendantThisNode: function( node ) {
assert && assert( node instanceof scenery.Node );
var indexOfNode = _.indexOf( this._nodesThatAreActiveDescendantToThisNode, node );
assert && assert( indexOfNode >= 0 );
this._nodesThatAreActiveDescendantToThisNode.splice( indexOfNode, 1 );

},

/**
* Trigger the view update for each AccessiblePeer
* @public
*/
updateActiveDescendantAssociationsInPeers: function() {
for ( var i = 0; i < this.accessibleInstances.length; i++ ) {
var peer = this.accessibleInstances[ i ].peer;
peer.onActiveDescendantAssociationChange();
}
},

/**
* Update the associations for aria-activeDescendant
* @public (scenery-internal)
*/
updateOtherNodesActiveDescendant: function() {

// if any other nodes are aria-activeDescendant this Node, update those associations too. Since this node's
// accessible content needs to be recreated, they need to update their aria-activeDescendant associations accordingly.
// TODO: only use unique elements of the array (_.unique)
for ( var i = 0; i < this._nodesThatAreActiveDescendantToThisNode.length; i++ ) {
var otherNode = this._nodesThatAreActiveDescendantToThisNode[ i ];
otherNode.updateActiveDescendantAssociationsInPeers();
}
},

/**
* The list of Nodes that are aria-activeDescendant this node (other node's peer element will have this Node's Peer element's
* id in the aria-activeDescendant attribute
* @public
* @returns {Array.<Node>}
*/
getNodesThatAreActiveDescendantToThisNode: function() {
return this._nodesThatAreActiveDescendantToThisNode;
},
get nodesThatAreActiveDescendantToThisNode() { return this.getNodesThatAreActiveDescendantToThisNode(); },



/**
* Sets the accessible focus order for this node. This includes not only focused items, but elements that can be
* placed in the parallel DOM. If provided, it will override the focus order between children (and
Expand Down Expand Up @@ -2508,6 +2669,7 @@ define( function( require ) {
// to this Node (they are pointing to this Node's IDs). https://github.com/phetsims/scenery/issues/816
node.updateOtherNodesAriaLabelledby();
node.updateOtherNodesAriaDescribedby();
node.updateOtherNodesActiveDescendant();

sceneryLog && sceneryLog.Accessibility && sceneryLog.pop();
},
Expand Down
56 changes: 34 additions & 22 deletions js/accessibility/AccessibilityTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -455,14 +455,16 @@ define( function( require ) {
} );

// tests for aria-labelledby and aria-describedby should be the same, since both support the same feature set
function testAriaLabelledOrDescribedBy( assert, attribute ) { // eslint-disable-line
function testAssociationAttribute( assert, attribute ) { // eslint-disable-line

// use a different setter depending on if testing labelledby or describedby
var addAssociationFunction = attribute === 'aria-labelledby' ? 'addAriaLabelledbyAssociation' :
attribute === 'aria-describedby' ? 'addAriaDescribedbyAssociation' : null;
attribute === 'aria-describedby' ? 'addAriaDescribedbyAssociation' :
attribute === 'aria-activedescendant' ? 'addActiveDescendantAssociation' :
null;

if ( !addAssociationFunction ) {
throw new Error( 'incorrect attribute name while in testAriaLabelledOrDescribedBy' );
throw new Error( 'incorrect attribute name while in testAssociationAttribute' );
}


Expand Down Expand Up @@ -631,7 +633,7 @@ define( function( require ) {
otherElementName: AccessiblePeer.LABEL_SIBLING
} );

var checkOnYourOwnAriaLabelledByAssociations = function( node ) {
var checkOnYourOwnAssociations = function( node ) {

var instance = node._accessibleInstances[ 0 ];
var nodePrimaryElement = instance.peer.primarySibling;
Expand Down Expand Up @@ -662,44 +664,44 @@ define( function( require ) {


// Check basic associations within single node
checkOnYourOwnAriaLabelledByAssociations( j );
checkOnYourOwnAssociations( j );
testK();

// Moving this node around the scene graph should not change it's aria labelled by associations.
rootNode.addChild( new Node( { children: [ j ] } ) );
checkOnYourOwnAriaLabelledByAssociations( j );
checkOnYourOwnAssociations( j );
testK();

// check remove child
rootNode.removeChild( j );
checkOnYourOwnAriaLabelledByAssociations( j );
checkOnYourOwnAssociations( j );
testK();

// check dispose
var jParent = new Node( { children: [ j ] } );
rootNode.children = [];
rootNode.addChild( jParent );
checkOnYourOwnAriaLabelledByAssociations( j );
checkOnYourOwnAssociations( j );
rootNode.addChild( j );
checkOnYourOwnAriaLabelledByAssociations( j );
checkOnYourOwnAssociations( j );
rootNode.addChild( k );
checkOnYourOwnAriaLabelledByAssociations( j );
checkOnYourOwnAssociations( j );
testK();
jParent.dispose();
checkOnYourOwnAriaLabelledByAssociations( j );
checkOnYourOwnAssociations( j );
testK();

// check removeChild with dag
var jParent2 = new Node( { children: [ j ] } );
rootNode.insertChild( 0, jParent2 );
checkOnYourOwnAriaLabelledByAssociations( j );
checkOnYourOwnAssociations( j );
testK();
rootNode.removeChild( jParent2 );
checkOnYourOwnAriaLabelledByAssociations( j );
checkOnYourOwnAssociations( j );
testK();
}

function testAriaLabelledOrDescribedBySetters( assert, attribute ) { // eslint-disable-line
function testAssociationAttributeBySetters( assert, attribute ) { // eslint-disable-line


var rootNode = new Node();
Expand All @@ -709,11 +711,15 @@ define( function( require ) {

// use a different setter depending on if testing labelledby or describedby
var associationsArrayName = attribute === 'aria-labelledby' ? 'ariaLabelledbyAssociations' :
attribute === 'aria-describedby' ? 'ariaDescribedbyAssociations' : null;
attribute === 'aria-describedby' ? 'ariaDescribedbyAssociations' :
attribute === 'aria-activedescendant' ? 'activeDescendantAssociations' :
null;

// use a different setter depending on if testing labelledby or describedby
var associationRemovalFunction = attribute === 'aria-labelledby' ? 'removeAriaLabelledbyAssociation' :
attribute === 'aria-describedby' ? 'removeAriaDescribedbyAssociation' : null;
attribute === 'aria-describedby' ? 'removeAriaDescribedbyAssociation' :
attribute === 'aria-activedescendant' ? 'removeActiveDescendantAssociation' :
null;


var options = {
Expand Down Expand Up @@ -775,17 +781,23 @@ define( function( require ) {
assert.ok( m[ associationsArrayName ].length === 0, 'cleared when disposed' );
}

// TODO: comment these back in once aria-labelledby is working again
QUnit.test( 'aria-labelledby', function( assert ) {

testAriaLabelledOrDescribedBy( assert, 'aria-labelledby' );
testAriaLabelledOrDescribedBySetters( assert, 'aria-labelledby' );
testAssociationAttribute( assert, 'aria-labelledby' );
testAssociationAttributeBySetters( assert, 'aria-labelledby' );

} );
QUnit.test( 'aria-describedby', function( assert ) {

testAriaLabelledOrDescribedBy( assert, 'aria-describedby' );
testAriaLabelledOrDescribedBySetters( assert, 'aria-describedby' );
testAssociationAttribute( assert, 'aria-describedby' );
testAssociationAttributeBySetters( assert, 'aria-describedby' );

} );

QUnit.test( 'aria-activedescendant', function( assert ) {

testAssociationAttribute( assert, 'aria-activedescendant' );
testAssociationAttributeBySetters( assert, 'aria-activedescendant' );

} );

Expand Down Expand Up @@ -879,7 +891,7 @@ define( function( require ) {
a1Element = getPrimarySiblingElementByNode( a1 );
assert.ok( a1Element.hidden, true, 'hidden set as Property' );
assert.ok( a1Element.getAttribute( 'hidden' ) === '', 'hidden should not be set as attribute' );

} );

QUnit.test( 'Accessibility input listeners', function( assert ) {
Expand Down
3 changes: 2 additions & 1 deletion js/accessibility/AccessibilityUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,10 @@ define( function( require ) {

var ARIA_LABELLEDBY = 'aria-labelledby';
var ARIA_DESCRIBEDBY = 'aria-describedby';
var ARIA_ACTIVE_DESCENDANT = 'aria-activedescendant';

// {Array.<String>} attributes that put an ID of another attribute as the value, see https://github.com/phetsims/scenery/issues/819
var ASSOCIATION_ATTRIBUTES = [ ARIA_LABELLEDBY, ARIA_DESCRIBEDBY ];
var ASSOCIATION_ATTRIBUTES = [ ARIA_LABELLEDBY, ARIA_DESCRIBEDBY, ARIA_ACTIVE_DESCENDANT ];

/**
* Get all 'element' nodes off the parent element, placing them in an array for easy traversal. Note that this
Expand Down
Loading

0 comments on commit 4a1bc1e

Please sign in to comment.