Repository: flutter
Issue: [web] `RichText`/`Text.rich` semantics not emitted correctly for children that emit `SemanticsNode`s.
Labels: framework,a: accessibility,platform-web,has reproducible steps,P2,found in release: 3.10,found in release: 3.12,team-web,triaged-web

### Is there an existing issue for this?

- [X] I have searched the [existing issues](https://github.com/flutter/flutter/issues)
- [X] I have read the [guide to filing a bug](https://flutter.dev/docs/resources/bug-reports)

### Steps to reproduce

Related: https://github.com/flutter/flutter/issues/111931

The fundamental issue is not web-specific, but my use-case is on Flutter-web, and I'll mention the DOM structure below.

**tl;dr:**
* **Problem:** `RichText` attempts to emit a single `SemanticNode` with a single accumulated label string. But the label skips interleaved standalone widgets, which are separately emitted as child nodes. Screen reader users hear incomplete content when navigating to the rich text field, and then navigating further, encounter the missing content out of order from the text.
* **Ideal world:** If the goal of `RichText` is to support embedded widgets (say, even text fields for inline fill-in-the-blanks), it shouldn't be trying to emit a single label. Instead, it should emit each contained span as a child in the semantic tree, which would naturally result in text and widgets being interleaved in the right order. **See "Code sample" section** for a concrete example of improvements.

**Real-world scenario:**
This is causing an accessibility issue in the following scenario:

1. I have a rich-text containing text interleaved with rendered math (within `WidgetSpan`) and when the math exceeds the width of the rich-text container, then we make the math horizontally scrollable.
2. To make the scroll region keyboard accessible (e.g. for users with motor impairments unable to use mouse/touch), we need to make the scroll region keyboard focusable so that the user can scroll using left/right. The consequence of the scroll region plus focus support is that the `WidgetSpan` emits a separate `SemanticNode`.
3. Consequently, the rich-text emits
     a. a single root semantic node with a label concatenating everything _except_ for the scrollable math
     b. emits a child semantic node (well, the one emitted by the scrollable math region)

When manifested on web, the user gets the following accessibility DOM:
```html
<flt-semantics role="group" aria-label="[everything with the scrollable math content skipped]">
  <flt-semantics-container>
    <flt-semantics tabindex="0">[whatever the scrollable math content generates]</flt-semantics>
  </flt-semantics-container>
</flt-semantics>
```
This is very confusing when navigated with the screen reader since the context within which the math is interleaved is lost.

See the "Code sample" below for a simple case demonstrating the issue and suggesting an alternative semantic output.

### Expected and Actual results

See "Code sample" below.

### Code sample

<details><summary>Code sample</summary>

```dart
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() {
  runApp(const RichTextSemantics());
  SemanticsBinding.instance.ensureSemantics();
}

class RichTextSemantics extends StatelessWidget {
  const RichTextSemantics({super.key});

  @override
  Widget build(BuildContext context) {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      debugDumpSemanticsTree();
    });
    return MaterialApp(
      title: 'Rich-text semantics',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: Scaffold(
        appBar: AppBar(
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          title: const Text('Rich-text semantics'),
        ),
        body: Text.rich(
          // Issue: Rich text emits a role="group" just because a child emits
          // a semantics node.
          TextSpan(
            text: 'Hello ',
            style: DefaultTextStyle.of(context).style,
            children: [
              // Minor: Formatting no longer surfaced to screen reader.
              //        Screen readers _can_ vocalize them with different pitch,
              //        but they don't in the current state of the world.
              const TextSpan(text: 'bold', style: TextStyle(fontWeight: FontWeight.bold)),
              // Pretend this is a self-contained widget, like scrollable math
              // or a `TextField`.
              // Issue: Child semantic nodes omitted from label and appear
              //        "out of order" to a screen reader.
              WidgetSpan(
                child: Semantics(
                  container: true,
                  child: const Text('within widget'),
                ),
              ),
              const TextSpan(text: ' world!'),
            ],
          ),
        ),
      ),
    );
  }
}
```

This generates the following semantic tree:
```
...
│
└─SemanticsNode#4
  │ Rect.fromLTRB(0.0, 56.0, 191.0, 76.0)
  │ label:
  │   "Hello bold
  │    world!"
  │ textDirection: ltr
  │
  └─SemanticsNode#5
      Rect.fromLTRB(62.8, 0.0, 148.8, 20.0)
      tags: PlaceholderSpanIndexSemanticsTag(0)
      label: "within widget"
      textDirection: ltr
```

and accessibility DOM:

```html
<flt-semantics id="flt-semantic-node-4" aria-label="Hello bold
 world!" role="group" style="[omitted]">
    <flt-semantics-container style="[omitted]">
        <flt-semantics id="flt-semantic-node-5" aria-label="within widget" role="text" style="[omitted]">
        </flt-semantics>
    </flt-semantics-container>
</flt-semantics>
```

**Issue:** When traversed with a screen reader, "within widget" is no longer interleaved within the text and is navigated to separately without the context of where it originally was.

IMO, **what it should roughly generate for correct accessibility:**
```
...
│
├─SemanticsNode
|   label: "Hello "
|  
├─SemanticsNode
|   label: "bold"
|
├─SemanticsNode
|   Rect.fromLTRB(62.8, 0.0, 148.8, 20.0)
|   tags: PlaceholderSpanIndexSemanticsTag(0)
|   label: "within widget"
|   textDirection: ltr
|
└─SemanticsNode
    label: " world!"
```

```html
<!-- _If_ possible to emit spans. -->
<flt-semantics id="flt-semantic-node-4" style="[omitted]">
    <span>Hello </span>
    <b>bold</b>
    <flt-semantics-container style="[omitted]">
        <flt-semantics id="flt-semantic-node-5" aria-label="within widget" role="text" style="[omitted]">
        </flt-semantics>
    </flt-semantics-container>
    <span> world!</span>
</flt-semantics>
```

or
```html
<flt-semantics id="flt-semantic-node-4" style="[omitted]">
    <div role="text" aria-label="Hello "></div>
    <div role="text" aria-label="bold"></div>
    <flt-semantics-container style="[omitted]">
        <flt-semantics id="flt-semantic-node-5" aria-label="within widget" role="text" style="[omitted]">
        </flt-semantics>
    </flt-semantics-container>
    <div role="text" aria-label=" world!"></div>
</flt-semantics>
```

**Backward-compatibility hazard:** End users may have written semantics tests that implicitly relied on the current text merging behavior for passing tests. Such tests may fail.
</details>