Skip to content

Commit

Permalink
added accessibleName setter, #795
Browse files Browse the repository at this point in the history
  • Loading branch information
zepumph committed Jul 25, 2018
1 parent b9c4c0a commit e8c7165
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 71 deletions.
2 changes: 1 addition & 1 deletion js/accessibility/A11yBehaviorFunctionDef.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ define( function( require ) {
* @param {function} behaviorFunction
*/
validateA11yBehaviorFunctionDef: function( behaviorFunction ) {

assert && assert( typeof behaviorFunction === 'function' );
assert && assert( behaviorFunction.length === 3, 'behavior function should take three args' );
var options = behaviorFunction( new phet.scenery.Node(), {}, '' );
assert && assert( typeof options === 'object', 'behavior function should return an object' );
Expand Down
104 changes: 60 additions & 44 deletions js/accessibility/Accessibility.js
Original file line number Diff line number Diff line change
Expand Up @@ -394,10 +394,25 @@ define( function( require ) {
// {string|null} - sets the "Accessible Name" of the Node, as defined by the Browser's Accessibility Tree
this._accessibleName = null;

// {A11yBehaviorFunctionDef} - function that returns the options needed to set the appropriate accessible name for the Node
this._accessibleNameBehavior = function( node, options, accessibleName ) {
if ( node.tagName === 'input' ) {
options.labelTagName = 'label';
options.labelContent = accessibleName;
}
else if ( AccessibilityUtil.tagNameSupportsContent( node.tagName ) ) {
options.innerContent = accessibleName;
}
else {
options.ariaLabel = accessibleName;
}
return options;
};

// {string|null} - sets the help text of the Node, this most often corresponds to description text.
this._helpText = null;

// {function} - sets the help text of the Node, this most often corresponds to description text.
// {A11yBehaviorFunctionDef} - sets the help text of the Node, this most often corresponds to description text.
// TODO: for efficiency maybe don't set this on all nodes always, https://github.com/phetsims/scenery/issues/795
this._helpTextBehavior = function( node, options, helpText ) {

Expand Down Expand Up @@ -599,20 +614,6 @@ define( function( require ) {
* Set the Node's accessible content in a way that will define the Accessible Name for the browser. Different
* HTML components and code situations require different methods of setting the Accessible Name.
*
* This method does the best it can to create a general method to set the Accessible Name for a variety of
* different Node types and configurations, but if a Node is more complicated, then this method will not
* properly set the Accessible Name for the Node's HTML content. In this situation this setter needs to be
* overridden by the subtype to meet its specific constraints. When doing this make sure that the Accessible
* Name is properly being set and conveyed to AT.
*
* NOTE: By Accessible Name (capitalized), we mean the proper title of the HTML element that will be set in
* the browser Accessibility Tree and then interpreted by AT. This is necessily different from scenery internal
* names of HTML elements like "label sibling" (even though, in certain circumstances, an Accessible Name could
* be set by using the "label sibling" with tag name "label" and a "for" attribute).
*
* For more information about setting an Accessible Name on HTML see the scenery docs for accessibility,
* and see https://developer.paciellogroup.com/blog/2017/04/what-is-an-accessible-name/
*
* @param {string|null} accessibleName
*/
setAccessibleName: function( accessibleName ) {
Expand All @@ -621,51 +622,67 @@ define( function( require ) {
if ( this._accessibleName !== accessibleName ) {
this._accessibleName = accessibleName;

this.setAccessibleNameImplementation( accessibleName );

this.onAccessibleContentChange();
}
},
set accessibleName( accessibleName ) { this.setAccessibleName( accessibleName ); },

/**
* This function is to manage the public accessiblName setter and invalidateAccessibleContent wanting to do the
* same accessibleName setting work, but setAccessibleName wants to do a few more Client side error checks first
* that causes an infinite loop if called from invalidateAccessibleContent.
* @public (scenery-internal) - should only be called from setAccessibleName and invalidateAccessibleContent
* @param {string} accessibleName
* Get the tag name of the DOM element representing this node for accessibility.
* @public
*
* @returns {string|null}
*/
setAccessibleNameImplementation: function( accessibleName ) {
assert && assert( accessibleName === null || typeof accessibleName === 'string' );
getAccessibleName: function() {
return this._accessibleName;
},
get accessibleName() { return this.getAccessibleName(); },

if ( this._tagName ) {

// input tag with a label tag that has a "for" attribute
if ( this._tagName === 'input' ) {
this.labelTagName = 'label';
this.labelContent = accessibleName;
}
/**
* accessibleNameBehavior is a function that will set the appropriate options on this node to get the desired
* "Accessible Name"
*
* This accessibleNameBehavior's default does the best it can to create a general method to set the Accessible
* Name for a variety of different Node types and configurations, but if a Node is more complicated, then this
* method will not properly set the Accessible Name for the Node's HTML content. In this situation this function
* needs to be overridden by the subtype to meet its specific constraints. When doing this make it is up to the
* usage site to make sure that the Accessible Name is properly being set and conveyed to AT, as it is very hard
* to validate this function.
*
* NOTE: By Accessible Name (capitalized), we mean the proper title of the HTML element that will be set in
* the browser Accessibility Tree and then interpreted by AT. This is necessily different from scenery internal
* names of HTML elements like "label sibling" (even though, in certain circumstances, an Accessible Name could
* be set by using the "label sibling" with tag name "label" and a "for" attribute).
*
* For more information about setting an Accessible Name on HTML see the scenery docs for accessibility,
* and see https://developer.paciellogroup.com/blog/2017/04/what-is-an-accessible-name/
*
*
* @param {A11yBehaviorFunctionDef|function} accessibleNameBehavior
*/
setAccessibleNameBehavior: function( accessibleNameBehavior ) {
assert && A11yBehaviorFunctionDef.validateA11yBehaviorFunctionDef( accessibleNameBehavior );

// if you can put inner content on the element, then do so
else if ( AccessibilityUtil.tagNameSupportsContent( this._tagName ) ) {
this.innerContent = accessibleName;
if ( this._accessibleNameBehavior !== accessibleNameBehavior ) {

}
else {
this.ariaLabel = accessibleName;
}
this._accessibleNameBehavior = accessibleNameBehavior;

this.onAccessibleContentChange();
}
},
set accessibleNameBehavior( accessibleNameBehavior ) { this.setAccessibleNameBehavior( accessibleNameBehavior ); },

/**
* Get the tag name of the DOM element representing this node for accessibility.
* Get the help text of the interactive element.
* @public
*
* @returns {string|null}
* @returns {function}
*/
getAccessibleName: function() {
return this._accessibleName;
getAccessibleNameBehavior: function() {
return this._accessibleNameBehavior;
},
get accessibleName() { return this.getAccessibleName(); },
get accessibleNameBehavior() { return this.getAccessibleNameBehavior(); },


/**
Expand Down Expand Up @@ -701,10 +718,9 @@ define( function( require ) {
* helpTextBehavior is a function that will set the appropriate options on this node to get the desired
* "Help Text"
*
* @param {A11yBehaviorFunctionDef|function} helpTextBehavior - a function that takes
* @param {A11yBehaviorFunctionDef|function} helpTextBehavior
*/
setHelpTextBehavior: function( helpTextBehavior ) {
assert && assert( typeof helpTextBehavior === 'function' );
assert && A11yBehaviorFunctionDef.validateA11yBehaviorFunctionDef( helpTextBehavior );

if ( this._helpTextBehavior !== helpTextBehavior ) {
Expand Down
68 changes: 48 additions & 20 deletions js/accessibility/AccessibilityTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -1530,28 +1530,56 @@ define( function( require ) {
// TODO: this should be passing,see https://github.com/phetsims/scenery/issues/811

// test the behavior of focusable function
// var rootNode = new Node( { tagName: 'div' } );
// var display = new Display( rootNode ); // eslint-disable-line
// document.body.appendChild( display.domElement );
//
// var a = new Node( { tagName: 'div', accessibleName: TEST_LABEL } );
// rootNode.addChild( a );
//
// assert.ok( a.accessibleName === TEST_LABEL, 'accessibleName getter' );
//
// var aElement = getPrimarySiblingElementByNode( a );
// assert.ok( aElement.textContent === TEST_LABEL, 'accessibleName setter on div' );
var rootNode = new Node( { tagName: 'div' } );
var display = new Display( rootNode ); // eslint-disable-line
document.body.appendChild( display.domElement );

var a = new Node( { tagName: 'div', accessibleName: TEST_LABEL } );
rootNode.addChild( a );

assert.ok( a.accessibleName === TEST_LABEL, 'accessibleName getter' );

var aElement = getPrimarySiblingElementByNode( a );
assert.ok( aElement.textContent === TEST_LABEL, 'accessibleName setter on div' );

// TODO: this should be passing,see https://github.com/phetsims/scenery/issues/811

// var b = new Node( { tagName: 'input', accessibleName: TEST_LABEL } );
// a.addChild( b );
// var bElement = getPrimarySiblingElementByNode( b );
// var bParent = getPrimarySiblingElementByNode( b ).parentElement;
// var bLabelSibling = bParent.children[ DEFAULT_LABEL_SIBLING_INDEX ];
// assert.ok( bLabelSibling.textContent === TEST_LABEL, 'accessibleName sets label sibling' );
// assert.ok( bLabelSibling.getAttribute( 'for' ).indexOf( bElement.id ) >= 0, 'accessibleName sets label\'s "for" attribute' );
var b = new Node( { tagName: 'input', accessibleName: TEST_LABEL } );
a.addChild( b );
var bElement = getPrimarySiblingElementByNode( b );
var bParent = getPrimarySiblingElementByNode( b ).parentElement;
var bLabelSibling = bParent.children[ DEFAULT_LABEL_SIBLING_INDEX ];
assert.ok( bLabelSibling.textContent === TEST_LABEL, 'accessibleName sets label sibling' );
assert.ok( bLabelSibling.getAttribute( 'for' ).indexOf( bElement.id ) >= 0, 'accessibleName sets label\'s "for" attribute' );


var c = new Node( { containerTagName: 'div', tagName: 'div', ariaLabel: 'overrideThis' } );
rootNode.addChild( c );
var accessibleNameBehavior = function( node, options, accessibleName ) {

options.ariaLabel = accessibleName;
return options;
};
c.accessibleNameBehavior = accessibleNameBehavior;

assert.ok( c.accessibleNameBehavior === accessibleNameBehavior, 'getter works' );

var cLabelElement = getPrimarySiblingElementByNode( c ).parentElement.children[ DEFAULT_LABEL_SIBLING_INDEX ];
assert.ok( cLabelElement.getAttribute( 'aria-label' ) === 'overrideThis', 'accessibleNameBehavior should not work until there is accessible name' );
c.accessibleName = 'accessible name description';
cLabelElement = getPrimarySiblingElementByNode( c ).parentElement.children[ DEFAULT_LABEL_SIBLING_INDEX ];
assert.ok( cLabelElement.getAttribute( 'aria-label' ) === 'accessible name description', 'accessible name setter' );

c.accessibleName = '';

cLabelElement = getPrimarySiblingElementByNode( c ).parentElement.children[ DEFAULT_LABEL_SIBLING_INDEX ];
debugger;
assert.ok( cLabelElement.getAttribute( 'aria-label' ) === '', 'accessibleNameBehavior should work for empty string' );


c.accessibleName = null;
cLabelElement = getPrimarySiblingElementByNode( c ).parentElement.children[ DEFAULT_LABEL_SIBLING_INDEX ];
assert.ok( cLabelElement.getAttribute( 'aria-label' ) === 'overrideThis', 'accessibleNameBehavior should not work until there is accessible name' );

} );

Expand Down Expand Up @@ -1604,10 +1632,10 @@ define( function( require ) {
b.helpText = '';

bDescriptionElement = getPrimarySiblingElementByNode( b ).parentElement.children[ DEFAULT_DESCRIPTION_SIBLING_INDEX ];
assert.ok( bDescriptionElement.textContent === '', 'helpTextBehavior should not work for empty string' );
assert.ok( bDescriptionElement.textContent === '', 'helpTextBehavior should work for empty string' );


b.helpText = null
b.helpText = null;
bDescriptionElement = getPrimarySiblingElementByNode( b ).parentElement.children[ DEFAULT_DESCRIPTION_SIBLING_INDEX ];
assert.ok( bDescriptionElement.textContent === 'overrideThis', 'helpTextBehavior should not work until there is help text' );
} );
Expand Down
21 changes: 15 additions & 6 deletions js/accessibility/AccessiblePeer.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,9 @@ define( function( require ) {

var options = this.node.getBaseOptions();

// if( this.node.accessibleName){
// options = this.node.accessibleNameBehavior( this.node, options, this.accessibleName );
// }
if ( this.node.accessibleName !== null ) {
options = this.node.accessibleNameBehavior( this.node, options, this.node.accessibleName );
}

if ( this.node.helpText !== null ) {
options = this.node.helpTextBehavior( this.node, options, this.node.helpText );
Expand Down Expand Up @@ -225,7 +225,7 @@ define( function( require ) {
}

// update all attributes for the peer, should cover aria-label, role, input value and others
this.onAttributeChange();
this.onAttributeChange( options );

// Default the focus highlight in this special case to be invisible until selected.
if ( this.node.focusHighlightLayerable ) {
Expand Down Expand Up @@ -348,12 +348,21 @@ define( function( require ) {

/**
* Set all accessible attributes onto the peer elements from the model's stored data objects
* @param {Object} [a11yOptions] - these can override the values of the node, see this.update()
*/
onAttributeChange: function() {
onAttributeChange: function( a11yOptions ) {

for ( var i = 0; i < this.node.accessibleAttributes.length; i++ ) {
var dataObject = this.node.accessibleAttributes[ i ];
this.setAttributeToElement( dataObject.attribute, dataObject.value, dataObject.options );
var attribute = dataObject.attribute;
var value = dataObject.value;

// allow overriding of aria-label for accessibleName setter
// TODO: this is a specific workaround, it would be nice to sort out a general case for this, #795
if ( attribute === 'aria-label' && a11yOptions && typeof a11yOptions.ariaLabel === 'string' && dataObject.options.elementName === PRIMARY_SIBLING ) {
value = a11yOptions.ariaLabel;
}
this.setAttributeToElement( attribute, value, dataObject.options );
}
},

Expand Down

0 comments on commit e8c7165

Please sign in to comment.