@@ -218,12 +218,11 @@ export class BlockSvg
218218 // The page-wide unique ID of this Block used for focusing.
219219 svgPath . id = idGenerator . getNextUniqueId ( ) ;
220220
221- aria . setState ( svgPath , aria . State . ROLEDESCRIPTION , 'block' ) ;
222- aria . setRole ( svgPath , aria . Role . TREEITEM ) ;
223221 svgPath . tabIndex = - 1 ;
224222 this . currentConnectionCandidate = null ;
225223
226224 this . doInit_ ( ) ;
225+ this . computeAriaRole ( ) ;
227226 }
228227
229228 private recomputeAriaLabel ( ) {
@@ -235,29 +234,67 @@ export class BlockSvg
235234 }
236235
237236 private computeAriaLabel ( ) : string {
238- // Guess the block's aria label based on its field labels.
239- if ( this . isShadow ( ) || this . isSimpleReporter ( ) ) {
240- // TODO: Shadows may have more than one field.
241- // Shadow blocks are best represented directly by their field since they
242- // effectively operate like a field does for keyboard navigation purposes.
243- const field = Array . from ( this . getFields ( ) ) [ 0 ] ;
244- try {
245- return (
246- aria . getState ( field . getFocusableElement ( ) , aria . State . LABEL ) ??
247- 'Unknown?'
248- ) ;
249- } catch {
250- return 'Unknown?' ;
237+ const { blockSummary, inputCount} = buildBlockSummary ( this ) ;
238+ const inputSummary = inputCount
239+ ? ` ${ inputCount } ${ inputCount > 1 ? 'inputs' : 'input' } `
240+ : '' ;
241+
242+ let currentBlock : Block | null = null ;
243+ let nestedStatementBlockCount = 0 ;
244+ // This won't work well for if/else blocks.
245+ this . inputList . forEach ( ( input ) => {
246+ if (
247+ input . connection &&
248+ input . connection . type === ConnectionType . NEXT_STATEMENT
249+ ) {
250+ currentBlock = input . connection . targetBlock ( ) ;
251+ }
252+ } ) ;
253+ // The type is poorly inferred here.
254+ while ( currentBlock as Block | null ) {
255+ nestedStatementBlockCount ++ ;
256+ // The type is poorly inferred here.
257+ // If currentBlock is null, we can't enter this while loop...
258+ currentBlock = currentBlock ! . getNextBlock ( ) ;
259+ }
260+
261+ let blockTypeText = 'block' ;
262+ if ( this . isShadow ( ) ) {
263+ blockTypeText = 'input block' ;
264+ } else if ( this . outputConnection ) {
265+ blockTypeText = 'replacable block' ;
266+ } else if ( this . statementInputCount ) {
267+ blockTypeText = 'C-shaped block' ;
268+ }
269+
270+ let additionalInfo = blockTypeText ;
271+ if ( inputSummary && ! nestedStatementBlockCount ) {
272+ additionalInfo = `${ additionalInfo } with ${ inputSummary } ` ;
273+ } else if ( nestedStatementBlockCount ) {
274+ const childBlockSummary = `${ nestedStatementBlockCount } child ${ nestedStatementBlockCount > 1 ? 'blocks' : 'block' } ` ;
275+ if ( inputSummary ) {
276+ additionalInfo = `${ additionalInfo } with ${ inputSummary } and ${ childBlockSummary } ` ;
277+ } else {
278+ additionalInfo = `${ additionalInfo } with ${ childBlockSummary } ` ;
251279 }
252280 }
253281
254- const fieldLabels = [ ] ;
255- for ( const field of this . getFields ( ) ) {
256- if ( field instanceof FieldLabel ) {
257- fieldLabels . push ( field . getText ( ) ) ;
258- }
282+ return blockSummary + ', ' + additionalInfo ;
283+ }
284+
285+ private computeAriaRole ( ) {
286+ if ( this . isSimpleReporter ( ) ) {
287+ aria . setRole ( this . pathObject . svgPath , aria . Role . BUTTON ) ;
288+ } else {
289+ // This isn't read out by VoiceOver and it will read in the wrong place
290+ // as a duplicate in ChromeVox due to the other changes in this branch.
291+ // aria.setState(
292+ // this.pathObject.svgPath,
293+ // aria.State.ROLEDESCRIPTION,
294+ // 'block',
295+ // );
296+ aria . setRole ( this . pathObject . svgPath , aria . Role . TREEITEM ) ;
259297 }
260- return fieldLabels . join ( ' ' ) ;
261298 }
262299
263300 collectSiblingBlocks ( surroundParent : BlockSvg | null ) : BlockSvg [ ] {
@@ -1724,6 +1761,8 @@ export class BlockSvg
17241761 * settings.
17251762 */
17261763 render ( ) {
1764+ this . recomputeAriaLabel ( ) ;
1765+
17271766 this . queueRender ( ) ;
17281767 renderManagement . triggerQueuedRenders ( ) ;
17291768 }
@@ -1735,6 +1774,8 @@ export class BlockSvg
17351774 * @internal
17361775 */
17371776 renderEfficiently ( ) {
1777+ this . recomputeAriaLabel ( ) ;
1778+
17381779 dom . startTextWidthCache ( ) ;
17391780
17401781 if ( this . isCollapsed ( ) ) {
@@ -1991,3 +2032,51 @@ export class BlockSvg
19912032 }
19922033 }
19932034}
2035+
2036+ interface BlockSummary {
2037+ blockSummary : string ;
2038+ inputCount : number ;
2039+ }
2040+
2041+ function buildBlockSummary ( block : BlockSvg ) : BlockSummary {
2042+ let inputCount = 0 ;
2043+ function recursiveInputSummary (
2044+ block : BlockSvg ,
2045+ isNestedInput : boolean = false ,
2046+ ) : string {
2047+ return block . inputList
2048+ . flatMap ( ( input ) => {
2049+ const fields = input . fieldRow . map ( ( field ) => {
2050+ // If the block is a full block field, we only want to know if it's an
2051+ // editable field if we're not directly on it.
2052+ if ( field . EDITABLE && ! field . isFullBlockField ( ) && ! isNestedInput ) {
2053+ inputCount ++ ;
2054+ }
2055+ return [ field . getText ( ) ?? field . getValue ( ) ] ;
2056+ } ) ;
2057+ if (
2058+ input . connection &&
2059+ input . connection . type === ConnectionType . INPUT_VALUE
2060+ ) {
2061+ if ( ! isNestedInput ) {
2062+ inputCount ++ ;
2063+ }
2064+ const targetBlock = input . connection . targetBlock ( ) ;
2065+ if ( targetBlock ) {
2066+ return [
2067+ ...fields ,
2068+ recursiveInputSummary ( targetBlock as BlockSvg , true ) ,
2069+ ] ;
2070+ }
2071+ }
2072+ return fields ;
2073+ } )
2074+ . join ( ' ' ) ;
2075+ }
2076+
2077+ const blockSummary = recursiveInputSummary ( block ) ;
2078+ return {
2079+ blockSummary,
2080+ inputCount,
2081+ } ;
2082+ }
0 commit comments