Skip to content

Conversation

@flutter-zl
Copy link
Contributor

@flutter-zl flutter-zl commented Jun 23, 2025

Description

This PR implements comprehensive aria-labelledby support for Flutter Web, enabling complex accessible labels composed of multiple semantic parts. This significantly improves web accessibility by allowing developers to create structured, screen reader-friendly labels that follow WAI-ARIA best practices.

Problem Solved

#162094

Before: Flutter Web only supported single aria-label attributes, making it difficult to create complex form labels with multiple semantic components (e.g., field name + format instructions + requirement indicators).

After: Developers can now create rich, multi-part labels that screen readers can properly interpret and announce to users.

Demo app: https://labelledby-0618.web.app/

Test 1: Customer Name + Required Field (2 parts)

<input aria-labelledby="textfield-label-9-0 textfield-label-9-1" type="text">
<span id="textfield-label-9-0" aria-hidden="true" style="display: none;">Customer Name</span>
<span id="textfield-label-9-1" aria-hidden="true" style="display: none;">Required Field</span>

Test 2: Email + Format Instructions + Required (3 parts)

<input aria-labelledby="textfield-label-11-0 textfield-label-11-1 textfield-label-11-2" type="text">
<span id="textfield-label-11-0" aria-hidden="true" style="display: none;">Email Address</span>
<span id="textfield-label-11-1" aria-hidden="true" style="display: none;">Format: [email protected]</span>
<span id="textfield-label-11-2" aria-hidden="true" style="display: none;">Required</span>

Test 3: Submit Button + Keyboard Shortcut (Group semantics)

<flt-semantics role="group" aria-labelledby="label-13-0 label-13-1">
  <span id="label-13-0" aria-hidden="true" style="display: none;">Submit Form</span>
  <span id="label-13-1" aria-hidden="true" style="display: none;">Ctrl+Enter shortcut available</span>
  <flt-semantics role="button" tabindex="0">Submit</flt-semantics>
</flt-semantics>

Test 4: Complex Password Instructions (4 parts)

<input aria-labelledby="textfield-label-16-0 textfield-label-16-1 textfield-label-16-2 textfield-label-16-3" type="password">
<span id="textfield-label-16-0" aria-hidden="true" style="display: none;">Password Field</span>
<span id="textfield-label-16-1" aria-hidden="true" style="display: none;">Minimum 8 characters</span>
<span id="textfield-label-16-2" aria-hidden="true" style="display: none;">Must include uppercase, lowercase, and numbers</span>
<span id="textfield-label-16-3" aria-hidden="true" style="display: none;">Required for account security</span>

Test 5: Single Label Comparison (Falls back to aria-label)

<input aria-label="Regular single label field (should use aria-label)" type="text">

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@github-actions github-actions bot added a: text input Entering text in a text field or keyboard related problems framework flutter/packages/flutter repository. See also f: labels. engine flutter/engine related. See also e: labels. a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) platform-web Web applications specifically labels Jun 23, 2025
@flutter-zl flutter-zl marked this pull request as ready for review June 24, 2025 16:52
@flutter-zl flutter-zl requested review from chunhtai and mdebbar June 24, 2025 16:52
Copy link
Contributor

@mdebbar mdebbar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me with a few minor suggestions.

I would like to make sure this also looks good to @chunhtai, especially the framework parts.

Comment on lines +644 to +663
// If labelParts are provided, use them instead of the regular label
if (labelParts != null && labelParts.isNotEmpty) {
final String labelPartsValue = labelParts
.where((String part) => part.trim().isNotEmpty)
.join(' ');
if (labelPartsValue.isNotEmpty) {
// Combine labelParts with value if both exist
final String combinedValue = <String?>[
labelPartsValue,
value,
].whereType<String>().where((String element) => element.trim().isNotEmpty).join(' ');
return combinedValue.isNotEmpty ? combinedValue : null;
}
}

// Fallback to regular label + value logic
final String combinedValue = <String?>[
label,
value,
].whereType<String>().where((String element) => element.trim().isNotEmpty).join(' ');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To simplify this a little bit:

Suggested change
// If labelParts are provided, use them instead of the regular label
if (labelParts != null && labelParts.isNotEmpty) {
final String labelPartsValue = labelParts
.where((String part) => part.trim().isNotEmpty)
.join(' ');
if (labelPartsValue.isNotEmpty) {
// Combine labelParts with value if both exist
final String combinedValue = <String?>[
labelPartsValue,
value,
].whereType<String>().where((String element) => element.trim().isNotEmpty).join(' ');
return combinedValue.isNotEmpty ? combinedValue : null;
}
}
// Fallback to regular label + value logic
final String combinedValue = <String?>[
label,
value,
].whereType<String>().where((String element) => element.trim().isNotEmpty).join(' ');
List<String?>? allParts;
// If labelParts are provided, use them instead of the regular label
if (labelParts != null && labelParts.isNotEmpty) {
final Iterable<String> validLabelParts = labelParts.where((String part) => part.trim().isNotEmpty);
if (validLabelParts.isNotEmpty) {
allParts = <String?>[...validLabelParts, value];
}
}
// Fallback to regular label + value
allParts ??= <String?>[label, value];
final String combinedValue = allParts
.whereType<String>()
.where((String element) => element.trim().isNotEmpty)
.join(' ');

final String labelId = '$idPrefix-${semanticsObject.id}-$i';
labelIds.add(labelId);

DomElement? labelElement = containerElement.querySelector('#$labelId');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of relying on querySelector, I suggest keeping a list of these span elements, _labelPartElements or something like that.

Comment on lines +752 to +753
final String oldLabelId = '$idPrefix-${semanticsObject.id}-$i';
containerElement.querySelector('#$oldLabelId')?.remove();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would become:

_labelPartElements[i].remove();

and after the for loop:

_labelPartElements.length = labelParts.length;

Comment on lines +768 to +771
for (int i = 0; i < _previousLabelParts!.length; i++) {
final String labelId = '$idPrefix-${semanticsObject.id}-$i';
containerElement.querySelector('#$labelId')?.remove();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would become:

_labelPartElements.forEach((DomElement element) => element.remove());
_labelPartElements.clear();

@flutter-zl
Copy link
Contributor Author

@chunhtai and I had the following conversation.

@chunhtai : for this pr, what is the difference between between using aria-labelledby with spans of strings vs concatenate them and assign to aria-label?

@flutter-zl : the major advantage i can think of is aria-labelledby programmatically concatenate strings. developers need to manually aria-label concatenate strings which may raise error.
For example developers may forget to add spacing between $firstName $lastName.
labelParts: [’$firstName, $lastName’]
aria-label=’$firstName $lastName’

@chunhtai : if so, sounds like we can just provide a utility in dart side so they can use before setting the Semantics.label?
if we have to provide an API like this, we can also just concatenate in the dart side and the web engine doesn't need to change

Hi @yjbanov, what is your opinion since you already have the context of #162094?

@yjbanov
Copy link
Contributor

yjbanov commented Jun 27, 2025

Yeah, I was thinking about some method of concatenation outside the Semantics widget itself (see #162094 (comment)). An alternative method could be for label to accept String | List<String>, but unfortunately Dart doesn't support union types, so this is not expressible. labelParts is kind of an emulation of union types, but it kind of complicates the API a little. We already have label + attributedLabel. Now we'd have label + attributedLabel + labelParts. Later we may have to also add attributedLabelParts? :)

Maybe what we need is a LabelBuilder class that incapsulates label and attributes, supporting concatenation of multiple labels. Strawman interface for such a class could be:

final class SemanticsLabelBuilder {
  void addPart(String label);
  void addAttributedPart(AttributedString label);
  SemanticsLabel build();
}

final class SemanticsLabel {
  /// The concatenation of labels made of parts supplied using `addPart` and `addAttributedPart` invocations.
  String get label;

  /// The attributed concatenation of labels made of parts supplied using `addPart` and `addAttributedPart` invocations.
  ///
  /// Returns null if no attributed parts were supplied in `SemanticsLabelBuilder`.
  AttributedString? get attributedLabel;
}

Sample usage:

// Plain label concatenation
var builder = SemanticsLabelBuilder()
  ..addPart('hello')
  ..addPart('world');
var label = builder build;
label.label; // returns 'hello world'
label.attributedLabel; // returns null

// Plain label concatenation
var builder = SemanticsLabelBuilder()
  ..addPart('hello')
  ..addAttributedPart(AttributedString('world', attributes = [LocaleStringAttribute(...)]));
var label = builder build;
label.label; // returns 'hello world' (loses attributes, but otherwise correct)
label.attributedLabel; // returns AttributedString with concatenated label and
                       // string attributes with adjusted text ranges, because text ranges
                       // can shift due to concatenation

@chunhtai
Copy link
Contributor

chunhtai commented Jun 27, 2025

I also prefer having concatenation outside of semantics system, adding a property is costly to all embeddings.

@flutter-zl
Copy link
Contributor Author

I created a new PR(#171683) based on the feedback in #171040 (comment) and #171040 (comment).

github-merge-queue bot pushed a commit that referenced this pull request Jul 11, 2025
## Description

Please check comment:
#171040 (comment).

This PR adds `SemanticsLabelBuilder`, a new utility class for creating
accessible concatenated labels with proper text direction handling and
spacing. Currently, developers manually concatenate semantic labels
using string interpolation, which is error-prone and leads to
accessibility issues like missing spaces, incorrect text direction for
RTL languages. The new builder provides automatic spacing, Unicode
bidirectional text embedding for mixed LTR/RTL content.

### Before (error-prone manual concatenation):
```dart
//  Missing space
String label = "$firstName$lastName";  // "JohnDoe"

String englishText = "Welcome";  
String arabicText = "مرحبا";     // Arabic "Hello"

// Manual Concatenation (WITHOUT Unicode embedding):
aria-label="Welcome 欢迎 مرحبا स्वागत है to our global app"
// Problem: Arabic does not have proper text direction handling
```

### After (automatic and accessible):

Demo app after the change: https://label-0702.web.app/

```dart
// Automatic spacing and text direction handling
final label = (SemanticsLabelBuilder()
  ..addPart(firstName)
  ..addPart(lastName)).build();
// Result: "John Doe" with proper aria-label generation

// SemanticsLabelBuilder (WITH Unicode embedding):
aria-label="Welcome 欢迎 [U+202B]مرحبا[U+202C] स्वागत है to our global app"
//  Result: Arabic has proper text direction handling
```

## Issues Fixed

This fixes #162094. 

This PR addresses the general accessibility problem of error-prone
manual label concatenation that affects screen reader users. While not
fixing a specific filed issue, it provides a robust solution for the
common pattern of building complex semantic labels that are critical for
accessibility compliance, particularly for multilingual applications and
complex UI components like contact cards, dashboards, and e-commerce
listings.

## Breaking Changes

No breaking changes were made. This is a purely additive API that
doesn't modify existing behavior or require any migration.

## Key Features Added

- **SemanticsLabelBuilder**: Main builder class for concatenating text
parts
- **Automatic spacing**: Configurable separators with intelligent empty
part handling
- **Text direction support**: Unicode bidirectional embedding for
RTL/LTR mixed content

## Example Usage

```dart
// Basic usage
final label = (SemanticsLabelBuilder()
  ..addPart('Contact')
  ..addPart('John Doe')
  ..addPart('Phone: +1-555-0123')).build();
// Result: "Contact John Doe Phone: +1-555-0123"

// Custom separator
final label = (SemanticsLabelBuilder(separator: ', ')
  ..addPart('Name: Alice')
  ..addPart('Status: Online')).build();
// Result: "Name: Alice, Status: Online"

// Multilingual support
final label = (SemanticsLabelBuilder(textDirection: TextDirection.ltr)
  ..addPart('Welcome', textDirection: TextDirection.ltr)
  ..addPart('مرحبا', textDirection: TextDirection.rtl)
  ..addPart('to our app')).build();
// Result: "Welcome ‫مرحبا‬ to our app" (with proper RTL embedding)
```
```

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is [test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported.
- [x] All existing and new tests are passing.

---------

Co-authored-by: Mouad Debbar <[email protected]>
justinmc pushed a commit to justinmc/flutter that referenced this pull request Jul 11, 2025
…#171683)

## Description

Please check comment:
flutter#171040 (comment).

This PR adds `SemanticsLabelBuilder`, a new utility class for creating
accessible concatenated labels with proper text direction handling and
spacing. Currently, developers manually concatenate semantic labels
using string interpolation, which is error-prone and leads to
accessibility issues like missing spaces, incorrect text direction for
RTL languages. The new builder provides automatic spacing, Unicode
bidirectional text embedding for mixed LTR/RTL content.

### Before (error-prone manual concatenation):
```dart
//  Missing space
String label = "$firstName$lastName";  // "JohnDoe"

String englishText = "Welcome";  
String arabicText = "مرحبا";     // Arabic "Hello"

// Manual Concatenation (WITHOUT Unicode embedding):
aria-label="Welcome 欢迎 مرحبا स्वागत है to our global app"
// Problem: Arabic does not have proper text direction handling
```

### After (automatic and accessible):

Demo app after the change: https://label-0702.web.app/

```dart
// Automatic spacing and text direction handling
final label = (SemanticsLabelBuilder()
  ..addPart(firstName)
  ..addPart(lastName)).build();
// Result: "John Doe" with proper aria-label generation

// SemanticsLabelBuilder (WITH Unicode embedding):
aria-label="Welcome 欢迎 [U+202B]مرحبا[U+202C] स्वागत है to our global app"
//  Result: Arabic has proper text direction handling
```

## Issues Fixed

This fixes flutter#162094. 

This PR addresses the general accessibility problem of error-prone
manual label concatenation that affects screen reader users. While not
fixing a specific filed issue, it provides a robust solution for the
common pattern of building complex semantic labels that are critical for
accessibility compliance, particularly for multilingual applications and
complex UI components like contact cards, dashboards, and e-commerce
listings.

## Breaking Changes

No breaking changes were made. This is a purely additive API that
doesn't modify existing behavior or require any migration.

## Key Features Added

- **SemanticsLabelBuilder**: Main builder class for concatenating text
parts
- **Automatic spacing**: Configurable separators with intelligent empty
part handling
- **Text direction support**: Unicode bidirectional embedding for
RTL/LTR mixed content

## Example Usage

```dart
// Basic usage
final label = (SemanticsLabelBuilder()
  ..addPart('Contact')
  ..addPart('John Doe')
  ..addPart('Phone: +1-555-0123')).build();
// Result: "Contact John Doe Phone: +1-555-0123"

// Custom separator
final label = (SemanticsLabelBuilder(separator: ', ')
  ..addPart('Name: Alice')
  ..addPart('Status: Online')).build();
// Result: "Name: Alice, Status: Online"

// Multilingual support
final label = (SemanticsLabelBuilder(textDirection: TextDirection.ltr)
  ..addPart('Welcome', textDirection: TextDirection.ltr)
  ..addPart('مرحبا', textDirection: TextDirection.rtl)
  ..addPart('to our app')).build();
// Result: "Welcome ‫مرحبا‬ to our app" (with proper RTL embedding)
```
```

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is [test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported.
- [x] All existing and new tests are passing.

---------

Co-authored-by: Mouad Debbar <[email protected]>
azatech pushed a commit to azatech/flutter that referenced this pull request Jul 28, 2025
…#171683)

## Description

Please check comment:
flutter#171040 (comment).

This PR adds `SemanticsLabelBuilder`, a new utility class for creating
accessible concatenated labels with proper text direction handling and
spacing. Currently, developers manually concatenate semantic labels
using string interpolation, which is error-prone and leads to
accessibility issues like missing spaces, incorrect text direction for
RTL languages. The new builder provides automatic spacing, Unicode
bidirectional text embedding for mixed LTR/RTL content.

### Before (error-prone manual concatenation):
```dart
//  Missing space
String label = "$firstName$lastName";  // "JohnDoe"

String englishText = "Welcome";  
String arabicText = "مرحبا";     // Arabic "Hello"

// Manual Concatenation (WITHOUT Unicode embedding):
aria-label="Welcome 欢迎 مرحبا स्वागत है to our global app"
// Problem: Arabic does not have proper text direction handling
```

### After (automatic and accessible):

Demo app after the change: https://label-0702.web.app/

```dart
// Automatic spacing and text direction handling
final label = (SemanticsLabelBuilder()
  ..addPart(firstName)
  ..addPart(lastName)).build();
// Result: "John Doe" with proper aria-label generation

// SemanticsLabelBuilder (WITH Unicode embedding):
aria-label="Welcome 欢迎 [U+202B]مرحبا[U+202C] स्वागत है to our global app"
//  Result: Arabic has proper text direction handling
```

## Issues Fixed

This fixes flutter#162094. 

This PR addresses the general accessibility problem of error-prone
manual label concatenation that affects screen reader users. While not
fixing a specific filed issue, it provides a robust solution for the
common pattern of building complex semantic labels that are critical for
accessibility compliance, particularly for multilingual applications and
complex UI components like contact cards, dashboards, and e-commerce
listings.

## Breaking Changes

No breaking changes were made. This is a purely additive API that
doesn't modify existing behavior or require any migration.

## Key Features Added

- **SemanticsLabelBuilder**: Main builder class for concatenating text
parts
- **Automatic spacing**: Configurable separators with intelligent empty
part handling
- **Text direction support**: Unicode bidirectional embedding for
RTL/LTR mixed content

## Example Usage

```dart
// Basic usage
final label = (SemanticsLabelBuilder()
  ..addPart('Contact')
  ..addPart('John Doe')
  ..addPart('Phone: +1-555-0123')).build();
// Result: "Contact John Doe Phone: +1-555-0123"

// Custom separator
final label = (SemanticsLabelBuilder(separator: ', ')
  ..addPart('Name: Alice')
  ..addPart('Status: Online')).build();
// Result: "Name: Alice, Status: Online"

// Multilingual support
final label = (SemanticsLabelBuilder(textDirection: TextDirection.ltr)
  ..addPart('Welcome', textDirection: TextDirection.ltr)
  ..addPart('مرحبا', textDirection: TextDirection.rtl)
  ..addPart('to our app')).build();
// Result: "Welcome ‫مرحبا‬ to our app" (with proper RTL embedding)
```
```

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is [test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported.
- [x] All existing and new tests are passing.

---------

Co-authored-by: Mouad Debbar <[email protected]>
ksokolovskyi pushed a commit to ksokolovskyi/flutter that referenced this pull request Aug 19, 2025
…#171683)

## Description

Please check comment:
flutter#171040 (comment).

This PR adds `SemanticsLabelBuilder`, a new utility class for creating
accessible concatenated labels with proper text direction handling and
spacing. Currently, developers manually concatenate semantic labels
using string interpolation, which is error-prone and leads to
accessibility issues like missing spaces, incorrect text direction for
RTL languages. The new builder provides automatic spacing, Unicode
bidirectional text embedding for mixed LTR/RTL content.

### Before (error-prone manual concatenation):
```dart
//  Missing space
String label = "$firstName$lastName";  // "JohnDoe"

String englishText = "Welcome";  
String arabicText = "مرحبا";     // Arabic "Hello"

// Manual Concatenation (WITHOUT Unicode embedding):
aria-label="Welcome 欢迎 مرحبا स्वागत है to our global app"
// Problem: Arabic does not have proper text direction handling
```

### After (automatic and accessible):

Demo app after the change: https://label-0702.web.app/

```dart
// Automatic spacing and text direction handling
final label = (SemanticsLabelBuilder()
  ..addPart(firstName)
  ..addPart(lastName)).build();
// Result: "John Doe" with proper aria-label generation

// SemanticsLabelBuilder (WITH Unicode embedding):
aria-label="Welcome 欢迎 [U+202B]مرحبا[U+202C] स्वागत है to our global app"
//  Result: Arabic has proper text direction handling
```

## Issues Fixed

This fixes flutter#162094. 

This PR addresses the general accessibility problem of error-prone
manual label concatenation that affects screen reader users. While not
fixing a specific filed issue, it provides a robust solution for the
common pattern of building complex semantic labels that are critical for
accessibility compliance, particularly for multilingual applications and
complex UI components like contact cards, dashboards, and e-commerce
listings.

## Breaking Changes

No breaking changes were made. This is a purely additive API that
doesn't modify existing behavior or require any migration.

## Key Features Added

- **SemanticsLabelBuilder**: Main builder class for concatenating text
parts
- **Automatic spacing**: Configurable separators with intelligent empty
part handling
- **Text direction support**: Unicode bidirectional embedding for
RTL/LTR mixed content

## Example Usage

```dart
// Basic usage
final label = (SemanticsLabelBuilder()
  ..addPart('Contact')
  ..addPart('John Doe')
  ..addPart('Phone: +1-555-0123')).build();
// Result: "Contact John Doe Phone: +1-555-0123"

// Custom separator
final label = (SemanticsLabelBuilder(separator: ', ')
  ..addPart('Name: Alice')
  ..addPart('Status: Online')).build();
// Result: "Name: Alice, Status: Online"

// Multilingual support
final label = (SemanticsLabelBuilder(textDirection: TextDirection.ltr)
  ..addPart('Welcome', textDirection: TextDirection.ltr)
  ..addPart('مرحبا', textDirection: TextDirection.rtl)
  ..addPart('to our app')).build();
// Result: "Welcome ‫مرحبا‬ to our app" (with proper RTL embedding)
```
```

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is [test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported.
- [x] All existing and new tests are passing.

---------

Co-authored-by: Mouad Debbar <[email protected]>
mboetger pushed a commit to mboetger/flutter that referenced this pull request Sep 18, 2025
…#171683)

## Description

Please check comment:
flutter#171040 (comment).

This PR adds `SemanticsLabelBuilder`, a new utility class for creating
accessible concatenated labels with proper text direction handling and
spacing. Currently, developers manually concatenate semantic labels
using string interpolation, which is error-prone and leads to
accessibility issues like missing spaces, incorrect text direction for
RTL languages. The new builder provides automatic spacing, Unicode
bidirectional text embedding for mixed LTR/RTL content.

### Before (error-prone manual concatenation):
```dart
//  Missing space
String label = "$firstName$lastName";  // "JohnDoe"

String englishText = "Welcome";  
String arabicText = "مرحبا";     // Arabic "Hello"

// Manual Concatenation (WITHOUT Unicode embedding):
aria-label="Welcome 欢迎 مرحبا स्वागत है to our global app"
// Problem: Arabic does not have proper text direction handling
```

### After (automatic and accessible):

Demo app after the change: https://label-0702.web.app/

```dart
// Automatic spacing and text direction handling
final label = (SemanticsLabelBuilder()
  ..addPart(firstName)
  ..addPart(lastName)).build();
// Result: "John Doe" with proper aria-label generation

// SemanticsLabelBuilder (WITH Unicode embedding):
aria-label="Welcome 欢迎 [U+202B]مرحبا[U+202C] स्वागत है to our global app"
//  Result: Arabic has proper text direction handling
```

## Issues Fixed

This fixes flutter#162094. 

This PR addresses the general accessibility problem of error-prone
manual label concatenation that affects screen reader users. While not
fixing a specific filed issue, it provides a robust solution for the
common pattern of building complex semantic labels that are critical for
accessibility compliance, particularly for multilingual applications and
complex UI components like contact cards, dashboards, and e-commerce
listings.

## Breaking Changes

No breaking changes were made. This is a purely additive API that
doesn't modify existing behavior or require any migration.

## Key Features Added

- **SemanticsLabelBuilder**: Main builder class for concatenating text
parts
- **Automatic spacing**: Configurable separators with intelligent empty
part handling
- **Text direction support**: Unicode bidirectional embedding for
RTL/LTR mixed content

## Example Usage

```dart
// Basic usage
final label = (SemanticsLabelBuilder()
  ..addPart('Contact')
  ..addPart('John Doe')
  ..addPart('Phone: +1-555-0123')).build();
// Result: "Contact John Doe Phone: +1-555-0123"

// Custom separator
final label = (SemanticsLabelBuilder(separator: ', ')
  ..addPart('Name: Alice')
  ..addPart('Status: Online')).build();
// Result: "Name: Alice, Status: Online"

// Multilingual support
final label = (SemanticsLabelBuilder(textDirection: TextDirection.ltr)
  ..addPart('Welcome', textDirection: TextDirection.ltr)
  ..addPart('مرحبا', textDirection: TextDirection.rtl)
  ..addPart('to our app')).build();
// Result: "Welcome ‫مرحبا‬ to our app" (with proper RTL embedding)
```
```

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is [test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported.
- [x] All existing and new tests are passing.

---------

Co-authored-by: Mouad Debbar <[email protected]>
lucaantonelli pushed a commit to lucaantonelli/flutter that referenced this pull request Nov 21, 2025
…#171683)

## Description

Please check comment:
flutter#171040 (comment).

This PR adds `SemanticsLabelBuilder`, a new utility class for creating
accessible concatenated labels with proper text direction handling and
spacing. Currently, developers manually concatenate semantic labels
using string interpolation, which is error-prone and leads to
accessibility issues like missing spaces, incorrect text direction for
RTL languages. The new builder provides automatic spacing, Unicode
bidirectional text embedding for mixed LTR/RTL content.

### Before (error-prone manual concatenation):
```dart
//  Missing space
String label = "$firstName$lastName";  // "JohnDoe"

String englishText = "Welcome";  
String arabicText = "مرحبا";     // Arabic "Hello"

// Manual Concatenation (WITHOUT Unicode embedding):
aria-label="Welcome 欢迎 مرحبا स्वागत है to our global app"
// Problem: Arabic does not have proper text direction handling
```

### After (automatic and accessible):

Demo app after the change: https://label-0702.web.app/

```dart
// Automatic spacing and text direction handling
final label = (SemanticsLabelBuilder()
  ..addPart(firstName)
  ..addPart(lastName)).build();
// Result: "John Doe" with proper aria-label generation

// SemanticsLabelBuilder (WITH Unicode embedding):
aria-label="Welcome 欢迎 [U+202B]مرحبا[U+202C] स्वागत है to our global app"
//  Result: Arabic has proper text direction handling
```

## Issues Fixed

This fixes flutter#162094. 

This PR addresses the general accessibility problem of error-prone
manual label concatenation that affects screen reader users. While not
fixing a specific filed issue, it provides a robust solution for the
common pattern of building complex semantic labels that are critical for
accessibility compliance, particularly for multilingual applications and
complex UI components like contact cards, dashboards, and e-commerce
listings.

## Breaking Changes

No breaking changes were made. This is a purely additive API that
doesn't modify existing behavior or require any migration.

## Key Features Added

- **SemanticsLabelBuilder**: Main builder class for concatenating text
parts
- **Automatic spacing**: Configurable separators with intelligent empty
part handling
- **Text direction support**: Unicode bidirectional embedding for
RTL/LTR mixed content

## Example Usage

```dart
// Basic usage
final label = (SemanticsLabelBuilder()
  ..addPart('Contact')
  ..addPart('John Doe')
  ..addPart('Phone: +1-555-0123')).build();
// Result: "Contact John Doe Phone: +1-555-0123"

// Custom separator
final label = (SemanticsLabelBuilder(separator: ', ')
  ..addPart('Name: Alice')
  ..addPart('Status: Online')).build();
// Result: "Name: Alice, Status: Online"

// Multilingual support
final label = (SemanticsLabelBuilder(textDirection: TextDirection.ltr)
  ..addPart('Welcome', textDirection: TextDirection.ltr)
  ..addPart('مرحبا', textDirection: TextDirection.rtl)
  ..addPart('to our app')).build();
// Result: "Welcome ‫مرحبا‬ to our app" (with proper RTL embedding)
```
```

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is [test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported.
- [x] All existing and new tests are passing.

---------

Co-authored-by: Mouad Debbar <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) a: text input Entering text in a text field or keyboard related problems engine flutter/engine related. See also e: labels. framework flutter/packages/flutter repository. See also f: labels. platform-web Web applications specifically

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants