diff --git a/js/accessibility/A11yBehaviorFunctionDef.js b/js/accessibility/A11yBehaviorFunctionDef.js index 02be77492..2adf119c9 100644 --- a/js/accessibility/A11yBehaviorFunctionDef.js +++ b/js/accessibility/A11yBehaviorFunctionDef.js @@ -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' ); diff --git a/js/accessibility/Accessibility.js b/js/accessibility/Accessibility.js index da8f950ac..4b47b06e4 100644 --- a/js/accessibility/Accessibility.js +++ b/js/accessibility/Accessibility.js @@ -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 ) { @@ -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 ) { @@ -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(); }, /** @@ -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 ) { diff --git a/js/accessibility/AccessibilityTests.js b/js/accessibility/AccessibilityTests.js index 50deb7d15..54fa02be0 100644 --- a/js/accessibility/AccessibilityTests.js +++ b/js/accessibility/AccessibilityTests.js @@ -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' ); } ); @@ -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' ); } ); diff --git a/js/accessibility/AccessiblePeer.js b/js/accessibility/AccessiblePeer.js index d6ab167f2..1018598fe 100644 --- a/js/accessibility/AccessiblePeer.js +++ b/js/accessibility/AccessiblePeer.js @@ -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 ); @@ -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 ) { @@ -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 ); } },