Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically discover a11y text #1526

Closed
samreid opened this issue Jan 21, 2023 · 23 comments
Closed

Automatically discover a11y text #1526

samreid opened this issue Jan 21, 2023 · 23 comments

Comments

@samreid
Copy link
Member

samreid commented Jan 21, 2023

In discussion with @jessegreenberg, we saw that visual textual information was not exposed to the a11y tree by default. We had an idea to pluck button/checkbox labels and move them to the PDOM automatically as a default.

@samreid
Copy link
Member Author

samreid commented Feb 17, 2023

It seems like a small amount of effort could dramatically improve our screen reader coverage. But this issue seems like it is complex enough that it should be moved to an iteration + subteam. So I'll propose it on the upcoming priorities board, and unassign myself.

@samreid samreid removed their assignment Feb 17, 2023
@jessegreenberg jessegreenberg removed their assignment Feb 22, 2023
@terracoda
Copy link

terracoda commented Mar 9, 2023

This is a great idea @samreid and could lead to knowledge sharing around creating names for objects that work well in both the visual space and the described space. Totally, agree that default on-screen text is better than no text and a step in the right direction.

@samreid
Copy link
Member Author

samreid commented Feb 14, 2024

@terracoda indicated difficulty in designing the keyboard traversal order and separating play area vs control area (see phetsims/projectile-data-lab#107) in Projectile Data Lab because the a11y text is all blank:

image

Here is a patch that takes information from the PhET-iO IDs to populate the a11y view:

Subject: [PATCH] Pass launchButtonEnabledProperty directly to the launchButton.enabledProperty, see https://github.com/phetsims/projectile-data-lab/issues/141
---
Index: js/accessibility/pdom/ParallelDOM.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/accessibility/pdom/ParallelDOM.ts b/js/accessibility/pdom/ParallelDOM.ts
--- a/js/accessibility/pdom/ParallelDOM.ts	(revision 42fefc31fc2910edfd75a72c69e33e78b088c84a)
+++ b/js/accessibility/pdom/ParallelDOM.ts	(date 1707927532191)
@@ -141,6 +141,7 @@
 import TProperty from '../../../../axon/js/TProperty.js';
 import isSettingPhetioStateProperty from '../../../../tandem/js/isSettingPhetioStateProperty.js';
 import Bounds2 from '../../../../dot/js/Bounds2.js';
+import { animationFrameTimer } from '../../../../axon/js/imports.js';
 
 const INPUT_TAG = PDOMUtils.TAGS.INPUT;
 const P_TAG = PDOMUtils.TAGS.P;
@@ -632,6 +633,16 @@
     this.focusHighlightChangedEmitter = new TinyEmitter();
     this.pdomDisplaysEmitter = new TinyEmitter();
     this.pdomBoundInputEnabledListener = this.pdomInputEnabledListener.bind( this );
+
+    animationFrameTimer.setTimeout( () => {
+      if ( this.accessibleName === null && this.tandem && this.tandem.supplied ) {
+        let text = this.phetioID.substring( this.phetioID.lastIndexOf( '.' ) + 1 );
+        if ( text.endsWith( 'RadioButton' ) ) {
+          text = text.substring( 0, text.length - 'RadioButton'.length );
+        }
+        this.accessibleName = text;
+      }
+    }, 1000 );
   }
 
   /***********************************************************************************************************/

With this patch, the a11y view looks like this:

image

Would be good to get thoughts from @terracoda about whether this is helpful enough or from @jessegreenberg about how we might bring an idea like this to production (even if only for team/development purposes).

@terracoda
Copy link

Thanks @samreid, I'll look again at the A11y View.

@samreid
Copy link
Member Author

samreid commented Feb 14, 2024

The proposal above is not committed, it is a proposal to get the ball rolling toward a committable solution. Does it seem useful?

@terracoda
Copy link

terracoda commented Feb 14, 2024

I think it could be useful. I see some useful overlap with the description design tool. It pre-populates the accessible name with something unique and human readable.

@terracoda
Copy link

Upgrading to very useful.

@jessegreenberg
Copy link
Contributor

jessegreenberg commented Feb 14, 2024

Yes, cool idea! This seems great for internal use, I could see adding a query parameter that does this.

Thinking about the best place to put it, maybe somewhere in joist/SimDisplay? Like maybe we could override updateDisplay and add a walk down the AccessibleInstance tree to set these names?

I will add this to the alt-input Monday board and there we can determine a priority for adding this (unless it gets added organically sooner though I don't think I will have time for a while).

@samreid
Copy link
Member Author

samreid commented Feb 15, 2024

Adding to the Monday board and scheduling for a future subteam sounds good. Some questions for then:

  • Is there a way we can make this the default behavior, not behind a query parameter? For instance, if we automatically change the camel casing to spaced, like "Mystery Launcher 1" radio button instead of mysteryLauncher1 radio button, could it be good enough to use without a query parameter? What else would need to be done before we make it available to screen reader users? Is it "something is better than nothing" or "we shouldn't add anything if we can't meet a certain quality/quantity threshold"?
  • I was unclear about the ParallelDOM lifecycle. I thought tapping in to the constructor or mutate would be the correct time to know that accessibleName is null but the tandem is specified. But I couldn't get that to work. But likely something along those lines could be helpful.
  • There are ideas at the top of the issue about using the translatable visually presented strings here. We could design a scaffolded system where: If the accessibleName is provided, it takes precedence. If not, check if the human visible string is accessible, such as a label on a button or next to a checkbox. If not, check if the tandem was supplied, and convert it from camelcase to readable.

@samreid samreid removed their assignment Feb 15, 2024
@jessegreenberg jessegreenberg removed their assignment Feb 22, 2024
@zepumph
Copy link
Member

zepumph commented Feb 23, 2024

The alt-input management team discussed this today and we would like to loop this into the greater description work:
phetsims/joist#941

@marlitas
Copy link
Contributor

Meeting 3/11/24

  • We need to explore how screen readers will treat camelCase names.
  • @terracoda mentioned that a name is better than no name.
  • It would most definitely be helpful at least in the a11y view. We can do this via a query parameter.
  • Further discussion on wether we want to publish these names with sims is needed.

@AgustinVallejo
Copy link
Contributor

@samreid @zepumph and I worked on this as part of phetsims/buoyancy#109 and they wrote a patch for doing this automatically in some sun components. Leaving it here in case it's useful for future work

Subject: [PATCH] Pass launchButtonEnabledProperty directly to the launchButton.enabledProperty, see https://github.com/phetsims/projectile-data-lab/issues/141
---
Index: sun/js/Checkbox.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/sun/js/Checkbox.ts b/sun/js/Checkbox.ts
--- a/sun/js/Checkbox.ts	(revision 3cd5c702159e13688afd1b0b9da468f129ccee71)
+++ b/sun/js/Checkbox.ts	(date 1715911723171)
@@ -121,6 +121,7 @@
       tagName: 'input',
       inputType: 'checkbox',
       appendDescription: true,
+      accessibleName: Tandem.toAccessibleName( providedOptions, 'Checkbox' ),
 
       // voicing
       voicingCheckedObjectResponse: null,
Index: sun/js/ComboBoxListItemNode.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/sun/js/ComboBoxListItemNode.ts b/sun/js/ComboBoxListItemNode.ts
--- a/sun/js/ComboBoxListItemNode.ts	(revision 3cd5c702159e13688afd1b0b9da468f129ccee71)
+++ b/sun/js/ComboBoxListItemNode.ts	(date 1715912676366)
@@ -69,6 +69,7 @@
       tagName: 'li',
       focusable: true,
       ariaRole: 'option',
+      accessibleName: Tandem.toAccessibleName( providedOptions, 'Item' ),
 
       // the `li` with ariaRole `option` does not get click events on iOS VoiceOver, so position
       // elements so they receive pointer events
Index: sun/js/ComboBoxButton.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/sun/js/ComboBoxButton.ts b/sun/js/ComboBoxButton.ts
--- a/sun/js/ComboBoxButton.ts	(revision 3cd5c702159e13688afd1b0b9da468f129ccee71)
+++ b/sun/js/ComboBoxButton.ts	(date 1715912953495)
@@ -105,7 +105,8 @@
       // pdom
       containerTagName: 'div',
       labelTagName: 'p', // NOTE: A `span` causes duplicate name-speaking with VO+safari in https://github.com/phetsims/ratio-and-proportion/issues/532
-      accessibleNameBehavior: ACCESSIBLE_NAME_BEHAVIOR
+      accessibleNameBehavior: ACCESSIBLE_NAME_BEHAVIOR,
+      // accessibleName: Tandem.toAccessibleName( providedOptions, 'ComboBoxButton' )
     }, providedOptions );
 
     assert && assert( _.includes( ALIGN_VALUES, options.align ),
Index: sun/js/AccordionBox.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/sun/js/AccordionBox.ts b/sun/js/AccordionBox.ts
--- a/sun/js/AccordionBox.ts	(revision 3cd5c702159e13688afd1b0b9da468f129ccee71)
+++ b/sun/js/AccordionBox.ts	(date 1715912565653)
@@ -198,6 +198,7 @@
       tagName: 'div',
       headingTagName: 'h3', // specify the heading that this AccordionBox will be, TODO: use this.headingLevel when no longer experimental https://github.com/phetsims/scenery/issues/855
       accessibleNameBehavior: AccordionBox.ACCORDION_BOX_ACCESSIBLE_NAME_BEHAVIOR,
+      accessibleName: Tandem.toAccessibleName( providedOptions, 'AccordionBox' ),
 
       // voicing
       voicingNameResponse: null,
Index: sun/js/ComboBox.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/sun/js/ComboBox.ts b/sun/js/ComboBox.ts
--- a/sun/js/ComboBox.ts	(revision 3cd5c702159e13688afd1b0b9da468f129ccee71)
+++ b/sun/js/ComboBox.ts	(date 1715913290569)
@@ -244,6 +244,7 @@
       buttonLabelTagName: 'p',
       accessibleNameBehavior: ACCESSIBLE_NAME_BEHAVIOR,
       helpTextBehavior: HELP_TEXT_BEHAVIOR,
+      accessibleName: Tandem.toAccessibleName( providedOptions, 'ComboBox' ),
 
       comboBoxVoicingNameResponsePattern: SunConstants.VALUE_NAMED_PLACEHOLDER,
       comboBoxVoicingContextResponse: null,
@@ -565,6 +566,9 @@
       else if ( typeof item.a11yName === 'string' ) {
         property = new TinyProperty( item.a11yName );
       }
+      else if ( item.tandemName ) {
+        property = new TinyProperty( Tandem.tandemNameToAccessibleName( item.tandemName, 'Item' ) );
+      }
       else {
         property = new TinyProperty( null );
       }
Index: sun/js/AquaRadioButton.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/sun/js/AquaRadioButton.ts b/sun/js/AquaRadioButton.ts
--- a/sun/js/AquaRadioButton.ts	(revision 3cd5c702159e13688afd1b0b9da468f129ccee71)
+++ b/sun/js/AquaRadioButton.ts	(date 1715912110736)
@@ -119,7 +119,8 @@
       containerTagName: 'li',
       labelTagName: 'label',
       appendLabel: true,
-      appendDescription: true
+      appendDescription: true,
+      accessibleName: Tandem.toAccessibleName( providedOptions, 'RadioButton' )
 
     }, providedOptions );
 
Index: density-buoyancy-common/js/common/view/BuoyancyDisplayOptionsPanel.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/density-buoyancy-common/js/common/view/BuoyancyDisplayOptionsPanel.ts b/density-buoyancy-common/js/common/view/BuoyancyDisplayOptionsPanel.ts
--- a/density-buoyancy-common/js/common/view/BuoyancyDisplayOptionsPanel.ts	(revision f4549040101bae01a024fc86025259d7e038e20d)
+++ b/density-buoyancy-common/js/common/view/BuoyancyDisplayOptionsPanel.ts	(date 1715912187354)
@@ -136,7 +136,7 @@
                   tandem: options.tandem.createTandem( 'massesCheckbox' )
                 }, checkboxOptions ) ),
                 new Checkbox( model.showForceValuesProperty, new Text( DensityBuoyancyCommonStrings.forceValuesStringProperty, labelOptions ), combineOptions<CheckboxOptions>( {
-                  tandem: options.tandem.createTandem( 'forcesCheckbox' )
+                  tandem: options.tandem.createTandem( 'forceValuesCheckbox' )
                 }, checkboxOptions ) ),
                 ...( model.supportsDepthLines ?
                   [ new Checkbox( model.showDepthLinesProperty, new Text( DensityBuoyancyCommonStrings.depthLinesStringProperty, labelOptions ), combineOptions<CheckboxOptions>( {
Index: tandem/js/Tandem.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/tandem/js/Tandem.ts b/tandem/js/Tandem.ts
--- a/tandem/js/Tandem.ts	(revision 27c3e2c9daa7165780d04465016c2e898a96c192)
+++ b/tandem/js/Tandem.ts	(date 1715913246619)
@@ -11,9 +11,10 @@
 import arrayRemove from '../../phet-core/js/arrayRemove.js';
 import merge from '../../phet-core/js/merge.js';
 import optionize from '../../phet-core/js/optionize.js';
-import PhetioObject from './PhetioObject.js';
+import PhetioObject, { PhetioObjectOptions } from './PhetioObject.js';
 import TandemConstants, { PhetioID } from './TandemConstants.js';
 import tandemNamespace from './tandemNamespace.js';
+import PickOptional from '../../phet-core/js/types/PickOptional.js';
 
 // constants
 // Tandem can't depend on joist, so cannot use packageJSON module
@@ -597,6 +598,29 @@
    * Use this as the parent tandem for Properties that are related to sim-specific preferences.
    */
   public static readonly PREFERENCES = Tandem.GLOBAL_MODEL.createTandem( 'preferences' );
+
+  /**
+   * Tandem names can be used to create accessible names for screen readers. This method will convert a tandem name to
+   * a human-readable name. For example, 'resetAllButton' would become 'Reset All'.
+   */
+  public static toAccessibleName( providedOptions: PickOptional<PhetioObjectOptions, 'tandem'> | undefined, suffix: string ): string | null {
+    if ( providedOptions && providedOptions.tandem ) {
+      return Tandem.tandemNameToAccessibleName( providedOptions.tandem.name, suffix );
+    }
+    return null;
+  }
+
+  public static tandemNameToAccessibleName( tandemName: string, suffix: string ): string | null {
+    assert && assert( tandemName.toLowerCase().endsWith( suffix.toLowerCase() ), `suffix should be at the end of the tandem name: ${tandemName}` );
+
+    // trim the suffix
+    const withoutSuffix = tandemName.slice( 0, -suffix.length );
+
+    const whitespaceName = withoutSuffix.replace( /([A-Z])/g, ' $1' ).trim();
+
+    // capitalize the first letter of each word, no matter how many words
+    return whitespaceName.replace( /\b\w/g, c => c.toUpperCase() );
+  }
 }
 
 Tandem.addLaunchListener( () => {

@zepumph
Copy link
Member

zepumph commented Jun 27, 2024

In addition to the above, it does seem that Tandem.toAccessibleName() is getting used in RectangularRadioButton, but that is all. I'd like to take a closer look.

  1. It seems awesome to try to add this eagerly
  2. The components we add this to need to get audited to make sure they aren't also sometimes getting their accessibleName from some other strategy in certain cases.

@zepumph zepumph self-assigned this Jun 27, 2024
@zepumph
Copy link
Member

zepumph commented Jul 1, 2024

I really would like to apply some form of this patch, but there are too many cases where common code is using the older style of labelContent/innerContent for the accessible name. We can't just add accessibleName as the default, because it doesn't get overwritten in a standard way. My guess is that I could get to a commit point in 2 hours, but it would involve making pretty error prone changes to get more consistent to having common code set default accessible names.

Subject: [PATCH] Pass launchButtonEnabledProperty directly to the launchButton.enabledProperty, see https://github.com/phetsims/projectile-data-lab/issues/141
---
Index: area-model-common/js/common/view/AreaScreenView.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/area-model-common/js/common/view/AreaScreenView.js b/area-model-common/js/common/view/AreaScreenView.js
--- a/area-model-common/js/common/view/AreaScreenView.js	(revision 1241c8db48406403bb2f81ad204a2d47355911b1)
+++ b/area-model-common/js/common/view/AreaScreenView.js	(date 1719857314680)
@@ -137,8 +137,7 @@
         contentYSpacing: model.allowExponents ? 5 : 8,
 
         // pdom
-        labelTagName: 'h3',
-        labelContent: AreaModelCommonStrings.a11y.factorsBoxStringProperty,
+        accessibleName: AreaModelCommonStrings.a11y.factorsBoxStringProperty,
         titleBarOptions: {
           descriptionContent: AreaModelCommonStrings.a11y.factorsBoxDescriptionStringProperty
         }
Index: masses-and-springs/js/common/view/SpringScreenView.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/masses-and-springs/js/common/view/SpringScreenView.js b/masses-and-springs/js/common/view/SpringScreenView.js
--- a/masses-and-springs/js/common/view/SpringScreenView.js	(revision 927626152eaf1c63178fe4006b8ecf636164adf5)
+++ b/masses-and-springs/js/common/view/SpringScreenView.js	(date 1719855829019)
@@ -273,7 +273,7 @@
    */
   createStopperButton( spring, tandem ) {
     return new StopperButtonNode(
-      tandem.createTandem( 'secondSpringStopperButtonNode' ), {
+      tandem.createTandem( 'secondSpringStopperButton' ), {
         listener: () => {
           spring.stopSpring();
         },
Index: sun/js/AquaRadioButton.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/sun/js/AquaRadioButton.ts b/sun/js/AquaRadioButton.ts
--- a/sun/js/AquaRadioButton.ts	(revision eec0449cc1afd5b7885682250a167ae4d472e700)
+++ b/sun/js/AquaRadioButton.ts	(date 1719857020681)
@@ -119,10 +119,14 @@
       containerTagName: 'li',
       labelTagName: 'label',
       appendLabel: true,
-      appendDescription: true
+      appendDescription: true,
+      labelContent: Tandem.toAccessibleName( providedOptions, 'RadioButton' )
 
     }, providedOptions );
 
+    if ( options.hasOwnProperty( 'accessibleName' ) || options.hasOwnProperty( 'innerContent' ) ) {
+      throw new Error( 'inner content for aquaRadioButton' );
+    }
     super();
 
     this.value = value;
Index: scenery-phet/js/buttons/ResetAllButton.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/scenery-phet/js/buttons/ResetAllButton.ts b/scenery-phet/js/buttons/ResetAllButton.ts
--- a/scenery-phet/js/buttons/ResetAllButton.ts	(revision e1d063709def87011597a2edcaf12a6f465efc9c)
+++ b/scenery-phet/js/buttons/ResetAllButton.ts	(date 1719856960491)
@@ -61,7 +61,7 @@
       soundPlayer: resetAllSoundPlayer,
 
       // pdom
-      innerContent: SceneryPhetStrings.a11y.resetAll.labelStringProperty,
+      accessibleName: SceneryPhetStrings.a11y.resetAll.labelStringProperty,
 
       // voicing
       voicingNameResponse: SceneryPhetStrings.a11y.resetAll.labelStringProperty,
Index: tandem/js/Tandem.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/tandem/js/Tandem.ts b/tandem/js/Tandem.ts
--- a/tandem/js/Tandem.ts	(revision eb8d2f1587c2bb63481ca4cec19c6d27bfc1464b)
+++ b/tandem/js/Tandem.ts	(date 1719856150660)
@@ -614,10 +614,12 @@
    * a human-readable name. For example, 'resetAllButton' would become 'Reset All'.
    */
   public static tandemNameToAccessibleName( tandemName: string, suffix: string ): string | null {
-    assert && assert( tandemName.toLowerCase().endsWith( suffix.toLowerCase() ), `suffix should be at the end of the tandem name: ${tandemName}` );
 
     // trim the suffix
-    const withoutSuffix = tandemName.slice( 0, -suffix.length );
+    let withoutSuffix = tandemName;
+    if ( tandemName.toLowerCase().endsWith( suffix.toLowerCase() ) ) {
+      withoutSuffix = tandemName.slice( 0, -suffix.length );
+    }
 
     const whitespaceName = withoutSuffix.replace( /([A-Z])/g, ' $1' ).trim();
 
Index: sun/js/buttons/RectangularRadioButton.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/sun/js/buttons/RectangularRadioButton.ts b/sun/js/buttons/RectangularRadioButton.ts
--- a/sun/js/buttons/RectangularRadioButton.ts	(revision eec0449cc1afd5b7885682250a167ae4d472e700)
+++ b/sun/js/buttons/RectangularRadioButton.ts	(date 1719857140461)
@@ -94,7 +94,7 @@
       containerTagName: 'li',
       appendDescription: true,
       appendLabel: true,
-      accessibleName: Tandem.toAccessibleName( providedOptions, 'RadioButton' ),
+      labelContent: Tandem.toAccessibleName( providedOptions, 'RadioButton' ),
 
       // phet-io
       tandem: Tandem.REQUIRED,
Index: sun/js/ComboBox.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/sun/js/ComboBox.ts b/sun/js/ComboBox.ts
--- a/sun/js/ComboBox.ts	(revision eec0449cc1afd5b7885682250a167ae4d472e700)
+++ b/sun/js/ComboBox.ts	(date 1719856936006)
@@ -244,6 +244,7 @@
       buttonLabelTagName: 'p',
       accessibleNameBehavior: ACCESSIBLE_NAME_BEHAVIOR,
       helpTextBehavior: HELP_TEXT_BEHAVIOR,
+      accessibleName: Tandem.toAccessibleName( providedOptions, 'ComboBox' ),
 
       comboBoxVoicingNameResponsePattern: SunConstants.VALUE_NAMED_PLACEHOLDER,
       comboBoxVoicingContextResponse: null,
@@ -259,6 +260,9 @@
       phetioEnabledPropertyInstrumented: true // opt into default PhET-iO instrumented enabledProperty
     }, providedOptions );
 
+    if ( options.hasOwnProperty( 'labelContent' ) || options.hasOwnProperty( 'innerContent' ) ) {
+      throw new Error( 'inner content for combo' );
+    }
     const nodes = getGroupItemNodes( items, options.tandem.createTandem( 'items' ) );
 
     assert && nodes.forEach( node => {
@@ -561,6 +565,9 @@
       else if ( typeof item.a11yName === 'string' ) {
         property = new TinyProperty( item.a11yName );
       }
+      else if ( item.tandemName ) {
+        property = new TinyProperty( Tandem.tandemNameToAccessibleName( item.tandemName, 'Item' ) );
+      }
       else {
         property = new TinyProperty( null );
       }
Index: sun/js/buttons/TextPushButton.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/sun/js/buttons/TextPushButton.ts b/sun/js/buttons/TextPushButton.ts
--- a/sun/js/buttons/TextPushButton.ts	(revision eec0449cc1afd5b7885682250a167ae4d472e700)
+++ b/sun/js/buttons/TextPushButton.ts	(date 1719857070232)
@@ -38,7 +38,7 @@
 
       // RectangularPushButtonOptions
       tandem: Tandem.REQUIRED,
-      innerContent: string
+      accessibleName: string
     }, providedOptions );
 
     const text = new Text( string, combineOptions<TextOptions>( {
Index: sun/js/buttons/ButtonNode.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/sun/js/buttons/ButtonNode.ts b/sun/js/buttons/ButtonNode.ts
--- a/sun/js/buttons/ButtonNode.ts	(revision eec0449cc1afd5b7885682250a167ae4d472e700)
+++ b/sun/js/buttons/ButtonNode.ts	(date 1719857268827)
@@ -23,6 +23,8 @@
 import TButtonAppearanceStrategy, { TButtonAppearanceStrategyOptions } from './TButtonAppearanceStrategy.js';
 import TContentAppearanceStrategy, { TContentAppearanceStrategyOptions } from './TContentAppearanceStrategy.js';
 import TinyProperty from '../../../axon/js/TinyProperty.js';
+import Tandem from '../../../tandem/js/Tandem.js';
+import inheritance from '../../../phet-core/js/inheritance.js';
 
 // constants
 const CONTRAST_FILTER = new Contrast( 0.7 );
@@ -123,6 +125,8 @@
       buttonAppearanceStrategyOptions: {},
       contentAppearanceStrategy: null,
       contentAppearanceStrategyOptions: {},
+      accessibleName: Tandem.toAccessibleName( providedOptions, 'Button' ),
+
       enabledAppearanceStrategy: ( enabled, button, background, content ) => {
         background.filters = enabled ? [] : [ CONTRAST_FILTER, BRIGHTNESS_FILTER ];
 
@@ -151,6 +155,9 @@
     options.enabledProperty = buttonModel.enabledProperty;
 
     super();
+    if ( ( inheritance( this ).map( x => x.name ).includes( 'RectangularRadioButton' ) && options.hasOwnProperty( 'labelContent' ) ) || options.hasOwnProperty( 'innerContent' ) ) {
+      throw new Error( 'inner content for button' );
+    }
 
     this.content = options.content;
     this.buttonModel = buttonModel;
Index: sun/js/ComboBoxListItemNode.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/sun/js/ComboBoxListItemNode.ts b/sun/js/ComboBoxListItemNode.ts
--- a/sun/js/ComboBoxListItemNode.ts	(revision eec0449cc1afd5b7885682250a167ae4d472e700)
+++ b/sun/js/ComboBoxListItemNode.ts	(date 1719856936011)
@@ -69,6 +69,7 @@
       tagName: 'li',
       focusable: true,
       ariaRole: 'option',
+      accessibleName: Tandem.toAccessibleName( providedOptions, 'Item' ),
 
       // the `li` with ariaRole `option` does not get click events on iOS VoiceOver, so position
       // elements so they receive pointer events
@@ -89,6 +90,9 @@
       visiblePropertyOptions: { phetioFeatured: true }
     }, providedOptions );
 
+    if ( options.hasOwnProperty( 'labelContent' ) || options.hasOwnProperty( 'innerContent' ) ) {
+      throw new Error( 'inner content for combobox button' );
+    }
     // @ts-expect-error convert Property into string
     options.comboBoxVoicingNameResponsePattern = options.comboBoxVoicingNameResponsePattern.get ?
       // @ts-expect-error convert Property into string
Index: sun/js/Checkbox.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/sun/js/Checkbox.ts b/sun/js/Checkbox.ts
--- a/sun/js/Checkbox.ts	(revision eec0449cc1afd5b7885682250a167ae4d472e700)
+++ b/sun/js/Checkbox.ts	(date 1719856936021)
@@ -121,6 +121,7 @@
       tagName: 'input',
       inputType: 'checkbox',
       appendDescription: true,
+      accessibleName: Tandem.toAccessibleName( providedOptions, 'Checkbox' ),
 
       // voicing
       voicingCheckedObjectResponse: null,
@@ -133,6 +134,9 @@
       voiceNameResponseOnSelection: true
     }, providedOptions );
 
+    if ( options.hasOwnProperty( 'labelContent' ) || options.hasOwnProperty( 'innerContent' ) ) {
+      throw new Error( 'inner content for checkbox' );
+    }
     super();
 
     // sends out notifications when the checkbox is toggled.
Index: sun/js/AccordionBox.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/sun/js/AccordionBox.ts b/sun/js/AccordionBox.ts
--- a/sun/js/AccordionBox.ts	(revision eec0449cc1afd5b7885682250a167ae4d472e700)
+++ b/sun/js/AccordionBox.ts	(date 1719856935998)
@@ -198,6 +198,7 @@
       tagName: 'div',
       headingTagName: 'h3', // specify the heading that this AccordionBox will be, TODO: use this.headingLevel when no longer experimental https://github.com/phetsims/scenery/issues/855
       accessibleNameBehavior: AccordionBox.ACCORDION_BOX_ACCESSIBLE_NAME_BEHAVIOR,
+      accessibleName: Tandem.toAccessibleName( providedOptions, 'AccordionBox' ),
 
       // voicing
       voicingNameResponse: null,
@@ -218,6 +219,9 @@
       }
     }, providedOptions );
 
+    if ( options.hasOwnProperty( 'labelContent' ) || options.hasOwnProperty( 'innerContent' ) ) {
+      throw new Error( 'inner content for accordionbox' );
+    }
     // expandCollapseButtonOptions defaults
     options.expandCollapseButtonOptions = combineOptions<ExpandCollapseButtonOptions>( {
       sideLength: 16, // button is a square, this is the length of one side

@zepumph zepumph removed their assignment Jul 1, 2024
@pixelzoom
Copy link
Contributor

pixelzoom commented Oct 21, 2024

Concerns about "discovering" accessible names from tandem names, and specific problems caused in RectangularRadioButtonGroup are noted in phetsims/sun#904.

@pixelzoom pixelzoom removed their assignment Oct 21, 2024
@samreid
Copy link
Member Author

samreid commented Oct 23, 2024

Here is an untested idea for discovering the translatable text for a checkbox:

Subject: [PATCH] Pass repo to check, see https://github.com/phetsims/perennial/issues/364
---
Index: js/Checkbox.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/Checkbox.ts b/js/Checkbox.ts
--- a/js/Checkbox.ts	(revision 1e2dd5869880fd41672adeeba92aa300f40b410e)
+++ b/js/Checkbox.ts	(date 1729696723066)
@@ -14,7 +14,7 @@
 import InstanceRegistry from '../../phet-core/js/documentation/InstanceRegistry.js';
 import optionize from '../../phet-core/js/optionize.js';
 import StrictOmit from '../../phet-core/js/types/StrictOmit.js';
-import { assertNoAdditionalChildren, FireListener, LayoutConstraint, Node, NodeOptions, Path, Rectangle, SceneryConstants, TPaint, Voicing, VoicingOptions, WidthSizable, WidthSizableOptions } from '../../scenery/js/imports.js';
+import { assertNoAdditionalChildren, FireListener, LayoutConstraint, Node, NodeOptions, Path, Rectangle, RichText, Text, SceneryConstants, TPaint, Voicing, VoicingOptions, WidthSizable, WidthSizableOptions } from '../../scenery/js/imports.js';
 import checkEmptySolidShape from '../../sherpa/js/fontawesome-4/checkEmptySolidShape.js';
 import checkSquareOSolidShape from '../../sherpa/js/fontawesome-4/checkSquareOSolidShape.js';
 import sharedSoundPlayers from '../../tambo/js/sharedSoundPlayers.js';
@@ -25,6 +25,7 @@
 import Tandem from '../../tandem/js/Tandem.js';
 import Utterance, { TAlertable } from '../../utterance-queue/js/Utterance.js';
 import sun from './sun.js';
+import TReadOnlyProperty from '../../axon/js/TReadOnlyProperty.js';
 
 // constants
 const BOOLEAN_VALIDATOR = { valueType: 'boolean' };
@@ -86,6 +87,26 @@
 
   public constructor( property: PhetioProperty<boolean>, content: Node, providedOptions?: CheckboxOptions ) {
 
+    // recursive search over content.children to find any Text
+    // if found, set the accessibleName of the Checkbox to the text of the first Text found
+    const findText = ( node: Node ): string | null | TReadOnlyProperty<string> => {
+      if ( node instanceof Text ) {
+        return node.stringProperty;
+      }
+      else if ( node instanceof RichText ) {
+        return node.stringProperty;
+      }
+      else if ( node.children ) {
+        for ( const child of node.children ) {
+          const text = findText( child );
+          if ( text ) {
+            return text;
+          }
+        }
+      }
+      return null;
+    };
+
     const options = optionize<CheckboxOptions, SelfOptions, ParentOptions>()( {
 
       // CheckboxOptions
@@ -129,7 +150,9 @@
       // the voicingContextResponse
       checkedContextResponse: null,
       uncheckedContextResponse: null,
-      voiceNameResponseOnSelection: true
+      voiceNameResponseOnSelection: true,
+
+      accessibleName: findText( content )
     }, providedOptions );
 
     super();

@samreid
Copy link
Member Author

samreid commented Oct 23, 2024

In today's pose meeting, we discussed the importance of having accessibleName and other a11y features happen as automatically as possible, with an ability to override them where possible.

@samreid samreid assigned pixelzoom and unassigned terracoda Oct 24, 2024
@samreid
Copy link
Member Author

samreid commented Oct 24, 2024

This issue is kind of blocked with on #1526 (comment). But we like the idea to automatically discover the a11y text labels where possible, as long as it can be overridden.

Therefore, we would like to remove the tandem support and proceed with automatically discovered text like #1526 (comment) when it is time.

@pixelzoom
Copy link
Contributor

My input is that we should develop reusable strategies for discovering acessibleName, and use them in the appropriate UI components. For example, "look for string Property in content Node" seems like a strategy (and implementation) that would be identical for many UI components.

@pixelzoom pixelzoom removed their assignment Oct 24, 2024
@marlitas
Copy link
Contributor

marlitas commented Nov 1, 2024

Meeting 11/1/24

  • A general solution should be workshopped, but each component should be thought of individually to make sure the general pattern makes sense for that implementation.
  • Any accessibleName that is "discovered" by the code should be the default, but needs to have the ability to easily be overwritten.
  • Assigning to @jessegreenberg for next steps.

@jessegreenberg
Copy link
Contributor

Thanks @samreid , #1526 (comment) is a good idea!

I just committed an implementation in 8b34da4.

@pixelzoom would you mind reviewing this? It is being used in Checkbox and Dialog now.

@pixelzoom
Copy link
Contributor

8b34da4 looks good to me. PDOMUtils seems like the right place for this, and findStringProperty seems like a good name.

The usage in Checkbox phetsims/sun@2cf8d3d also looks nice.

Back to @jessegreenberg for next steps.

@jessegreenberg
Copy link
Contributor

Great, thank you for reviewing. I created phetsims/sun#911, where we will track rolling it out by using it in PhET's common code components. Closing this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants