@@ -243,7 +243,7 @@ export class BlockSvg
243243 }
244244
245245 private computeAriaLabel ( ) : string {
246- const { blockSummary , inputCount} = buildBlockSummary ( this ) ;
246+ const { commaSeparatedSummary , inputCount} = buildBlockSummary ( this ) ;
247247 const inputSummary = inputCount
248248 ? ` ${ inputCount } ${ inputCount > 1 ? 'inputs' : 'input' } `
249249 : '' ;
@@ -291,7 +291,7 @@ export class BlockSvg
291291 additionalInfo = `${ additionalInfo } with ${ inputSummary } ` ;
292292 }
293293
294- return prefix + blockSummary + ', ' + additionalInfo ;
294+ return prefix + commaSeparatedSummary + ', ' + additionalInfo ;
295295 }
296296
297297 private computeAriaRole ( ) {
@@ -2022,57 +2022,111 @@ export class BlockSvg
20222022
20232023interface BlockSummary {
20242024 blockSummary : string ;
2025+ commaSeparatedSummary : string ;
20252026 inputCount : number ;
20262027}
20272028
20282029function buildBlockSummary ( block : BlockSvg ) : BlockSummary {
20292030 let inputCount = 0 ;
2031+
2032+ // Produce structured segments
2033+ // For example, the block:
2034+ // "create list with item foo repeated 5 times"
2035+ // becomes:
2036+ // LABEL("create list with item"),
2037+ // INPUT("foo"),
2038+ // LABEL("repeated")
2039+ // INPUT("5"),
2040+ // LABEL("times")
2041+ type SummarySegment =
2042+ | { kind : 'label' ; text : string }
2043+ | { kind : 'input' ; text : string } ;
2044+
20302045 function recursiveInputSummary (
20312046 block : BlockSvg ,
20322047 isNestedInput : boolean = false ,
2033- ) : string {
2034- return block . inputList
2035- . flatMap ( ( input ) => {
2036- const fields = input . fieldRow
2037- . filter ( ( field ) => {
2038- if ( ! field . isVisible ( ) ) return false ;
2039- if ( field instanceof FieldImage && field . isClickable ( ) ) {
2040- return false ;
2041- }
2042- return true ;
2043- } )
2044- . map ( ( field ) => {
2045- // If the block is a full block field, we only want to know if it's an
2046- // editable field if we're not directly on it.
2047- if ( field . EDITABLE && ! field . isFullBlockField ( ) && ! isNestedInput ) {
2048- inputCount ++ ;
2049- }
2050- return [ field . getText ( ) ?? field . getValue ( ) ] ;
2051- } ) ;
2052- if (
2053- input . isVisible ( ) &&
2054- input . connection &&
2055- input . connection . type === ConnectionType . INPUT_VALUE
2056- ) {
2057- if ( ! isNestedInput ) {
2048+ ) : SummarySegment [ ] {
2049+ return block . inputList . flatMap ( ( input ) => {
2050+ const fields : SummarySegment [ ] = input . fieldRow
2051+ . filter ( ( field ) => {
2052+ if ( ! field . isVisible ( ) ) return false ;
2053+ if ( field instanceof FieldImage && field . isClickable ( ) ) {
2054+ return false ;
2055+ }
2056+ return true ;
2057+ } )
2058+ . map ( ( field ) => {
2059+ const text = field . getText ( ) ?? field . getValue ( ) ;
2060+ // If the block is a full block field, we only want to know if it's an
2061+ // editable field if we're not directly on it.
2062+ if ( field . EDITABLE && ! field . isFullBlockField ( ) && ! isNestedInput ) {
20582063 inputCount ++ ;
2064+ return { kind : 'input' , text} ;
20592065 }
2060- const targetBlock = input . connection . targetBlock ( ) ;
2061- if ( targetBlock ) {
2062- return [
2063- ...fields ,
2064- recursiveInputSummary ( targetBlock as BlockSvg , true ) ,
2065- ] ;
2066+
2067+ return { kind : 'label' , text} ;
2068+ } ) ;
2069+
2070+ if (
2071+ input . isVisible ( ) &&
2072+ input . connection &&
2073+ input . connection . type === ConnectionType . INPUT_VALUE
2074+ ) {
2075+ if ( ! isNestedInput ) {
2076+ inputCount ++ ;
2077+ }
2078+
2079+ const targetBlock = input . connection . targetBlock ( ) ;
2080+ if ( targetBlock ) {
2081+ const nestedSegments = recursiveInputSummary (
2082+ targetBlock as BlockSvg ,
2083+ true ,
2084+ ) ;
2085+
2086+ if ( ! isNestedInput ) {
2087+ // treat the whole nested summary as a single input segment
2088+ const nestedText = nestedSegments . map ( ( s ) => s . text ) . join ( ' ' ) ;
2089+ return [ ...fields , { kind : 'input' , text : nestedText } ] ;
20662090 }
2091+
2092+ return [ ...fields , ...nestedSegments ] ;
20672093 }
2068- return fields ;
2069- } )
2070- . join ( ' ' ) ;
2094+ }
2095+
2096+ return fields ;
2097+ } ) ;
2098+ }
2099+
2100+ const segments = recursiveInputSummary ( block ) ;
2101+
2102+ const blockSummary = segments . map ( ( s ) => s . text ) . join ( ' ' ) ;
2103+
2104+ const spokenParts : string [ ] = [ ] ;
2105+ let labelRun : string [ ] = [ ] ;
2106+
2107+ // create runs of labels, flush when hitting an input
2108+ const flushLabels = ( ) => {
2109+ if ( ! labelRun . length ) return ;
2110+ spokenParts . push ( labelRun . join ( ' ' ) ) ;
2111+ labelRun = [ ] ;
2112+ } ;
2113+
2114+ for ( const seg of segments ) {
2115+ if ( seg . kind === 'label' ) {
2116+ labelRun . push ( seg . text ) ;
2117+ } else {
2118+ flushLabels ( ) ;
2119+ spokenParts . push ( seg . text ) ;
2120+ }
20712121 }
2122+ flushLabels ( ) ;
2123+
2124+ // comma-separate label runs and inputs
2125+ const commaSeparatedSummary = spokenParts . join ( ', ' ) ;
20722126
2073- const blockSummary = recursiveInputSummary ( block ) ;
20742127 return {
20752128 blockSummary,
2129+ commaSeparatedSummary,
20762130 inputCount,
20772131 } ;
20782132}
0 commit comments