Skip to content

Commit 27372a6

Browse files
authored
feat: Add comma-separation to ARIA labels (#9505)
* feat: Add comma-separation to ARIA labels * fix: remove unneeded complexity of returned comma-separated string
1 parent 0f7cdde commit 27372a6

1 file changed

Lines changed: 91 additions & 37 deletions

File tree

core/block_svg.ts

Lines changed: 91 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -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

20232023
interface BlockSummary {
20242024
blockSummary: string;
2025+
commaSeparatedSummary: string;
20252026
inputCount: number;
20262027
}
20272028

20282029
function 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

Comments
 (0)