Repository: opf/openproject
Pull request #17920: [62708] Implementing ARIA live regions to communicate contextual changes
Author: login: bsatarnejad
Labels: name: feature
Created: 2025-02-12T15:09:18Z | Merged: 2025-05-21T11:08:21Z

# Ticket
https://community.openproject.org/wp/62708

<!-- Contributors: Please check our PR guide: https://www.openproject.org/docs/development/code-review-guidelines/#preparing-your-pull-request before opening a PR. -->

<!-- Reviewers: Please check our Review guide: https://www.openproject.org/docs/development/code-review-guidelines/#reviewing -->

# What are you trying to accomplish?
Implement a generic method for ARIA live regions to communicate contextual changes
Add an example of its usage in date picker


- [ ] Added/updated tests
- [ ] Added/updated documentation in Lookbook (patterns, previews, etc)
- [ ] Tested major browsers (Chrome, Firefox, Edge, ...)



Diff:
diff --git a/app/components/concerns/op_turbo/streamable.rb b/app/components/concerns/op_turbo/streamable.rb
index 89180a56ddc2..5da280d7894f 100644
--- a/app/components/concerns/op_turbo/streamable.rb
+++ b/app/components/concerns/op_turbo/streamable.rb
@@ -45,7 +45,7 @@ def wrapper_key
     end
 
     included do
-      def render_as_turbo_stream(view_context:, action: :update, method: nil)
+      def render_as_turbo_stream(view_context:, action: :update, method: nil, **attributes)
         case action
         when :update, *INLINE_ACTIONS
           @inner_html_only = true
@@ -73,7 +73,8 @@ def render_as_turbo_stream(view_context:, action: :update, method: nil)
           action:,
           method:,
           target: wrapper_key,
-          template:
+          template:,
+          **attributes
         ).render_in(view_context)
       end
 
diff --git a/app/components/work_packages/date_picker/date_form_component.rb b/app/components/work_packages/date_picker/date_form_component.rb
index d6a8c53e48f3..8e231a5fdf01 100644
--- a/app/components/work_packages/date_picker/date_form_component.rb
+++ b/app/components/work_packages/date_picker/date_form_component.rb
@@ -211,10 +211,7 @@ def default_field_options(name)
           data[:focus] = "true"
         end
 
-        {
-          data: data,
-          aria: { live: :polite, atomic: true }
-        }
+        { data: data }
       end
 
       def single_date_field_button_link(focused_field)
diff --git a/app/components/work_packages/date_picker/dialog_content_component.html.erb b/app/components/work_packages/date_picker/dialog_content_component.html.erb
index 43bbbfb747d4..70e9c3c2c1d4 100644
--- a/app/components/work_packages/date_picker/dialog_content_component.html.erb
+++ b/app/components/work_packages/date_picker/dialog_content_component.html.erb
@@ -1,5 +1,11 @@
 <%=
   content_tag("turbo-frame", id: "wp-datepicker-dialog--content") do
+%>
+  <live-region></live-region>
+  <% if @live_region_message %>
+    <%= render OpTurbo::StreamComponent.new(action: :liveRegion, message: @live_region_message, politeness: "polite", delay: "500", target: nil) %>
+  <% end %>
+  <%=
     component_wrapper(
       data: { "application-target": "dynamic",
               controller: "work-packages--date-picker--preview",
@@ -99,5 +105,5 @@
         end
       end
     end
-  end
-%>
+  %>
+<% end %>
diff --git a/app/components/work_packages/date_picker/dialog_content_component.rb b/app/components/work_packages/date_picker/dialog_content_component.rb
index b29d21fe5357..77441f3b039e 100644
--- a/app/components/work_packages/date_picker/dialog_content_component.rb
+++ b/app/components/work_packages/date_picker/dialog_content_component.rb
@@ -46,6 +46,7 @@ def initialize(work_package:,
                      focused_field: :start_date,
                      triggering_field: nil,
                      touched_field_map: {},
+                     live_region_message: "",
                      date_mode: nil)
         super
 
@@ -55,6 +56,7 @@ def initialize(work_package:,
         @triggering_field = triggering_field
         @touched_field_map = touched_field_map
         @date_mode = date_mode
+        @live_region_message = live_region_message
       end
 
       private
diff --git a/app/controllers/concerns/op_turbo/component_stream.rb b/app/controllers/concerns/op_turbo/component_stream.rb
index fa64095b3c70..2c606da0ea45 100644
--- a/app/controllers/concerns/op_turbo/component_stream.rb
+++ b/app/controllers/concerns/op_turbo/component_stream.rb
@@ -83,6 +83,12 @@ def render_error_flash_message_via_turbo_stream(**)
       render_flash_message_via_turbo_stream(**, scheme: :danger, icon: :stop)
     end
 
+    def render_live_region_update_message(message:, politeness: "polite", delay: nil)
+      turbo_streams << OpTurbo::StreamComponent
+        .new(action: :liveRegion, message:, politeness:, delay:, target: nil)
+        .render_in(view_context)
+    end
+
     def render_flash_message_via_turbo_stream(message:, component: OpPrimer::FlashComponent, **)
       instance = component.new(**).with_content(message)
       turbo_streams << instance.render_as_turbo_stream(view_context:, action: :flash)
diff --git a/app/controllers/work_packages/date_picker_controller.rb b/app/controllers/work_packages/date_picker_controller.rb
index b3b9b5f689ed..00585fab8a89 100644
--- a/app/controllers/work_packages/date_picker_controller.rb
+++ b/app/controllers/work_packages/date_picker_controller.rb
@@ -157,7 +157,33 @@ def datepicker_modal_component
                                                          focused_field:,
                                                          triggering_field: params[:triggering_field],
                                                          touched_field_map:,
-                                                         date_mode:)
+                                                         date_mode:,
+                                                         live_region_message:)
+  end
+
+  def live_region_message
+    message_parts = [
+      "Scheduling mode: #{scheduling_label}",
+      working_days_label
+    ]
+
+    message_parts << "Start date: #{@work_package.start_date}" if @work_package.start_date.present?
+    message_parts << "Finish date: #{@work_package.due_date}" if @work_package.due_date.present?
+    message_parts << "Duration: #{@work_package.duration} days" if @work_package.duration.present?
+
+    I18n.t(
+      "work_packages.datepicker_modal.update_inputs_aria_live_message",
+      message: message_parts.join(", ")
+    )
+  end
+
+  def working_days_label
+    I18n.t("activerecord.attributes.work_package.include_non_working_days.#{@work_package.ignore_non_working_days}")
+  end
+
+  def scheduling_label
+    mode = @work_package.schedule_manually ? "manual" : "automatic"
+    I18n.t("work_packages.datepicker_modal.mode.#{mode}")
   end
 
   def focused_field
diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb
index 457f19ef3427..6457d03f278e 100644
--- a/app/views/layouts/base.html.erb
+++ b/app/views/layouts/base.html.erb
@@ -59,6 +59,10 @@ See COPYRIGHT and LICENSE files for more details.
 </noscript>
 <opce-toasts-container></opce-toasts-container>
 <opce-modal-overlay></opce-modal-overlay>
+<live-region>
+  <div id="polite" aria-live="polite" aria-atomic="true" class="hidden-for-sighted"></div>
+  <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
+</live-region>
 <opce-spot-drop-modal-portal></opce-spot-drop-modal-portal>
 <% main_menu = render_main_menu(local_assigns.fetch(:menu_name, nil), @project) %>
 <% side_displayed = content_for?(:sidebar) || content_for?(:main_menu) || !main_menu.blank? %>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 78fa871925b5..33141688b866 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -752,6 +752,7 @@ en:
         automatic: "Automatic"
         manual: "Manual"
       show_relations: "Show relations"
+      update_inputs_aria_live_message: "Date picker updated. %{message}"
       tabs:
         aria_label: "Datepicker tabs"
         children: "Children"
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 77aef6f63bb7..053e4bdca198 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -53,6 +53,7 @@
         "@openproject/primer-view-components": "^0.66.2",
         "@openproject/reactivestates": "^3.0.1",
         "@primer/css": "^21.5.0",
+        "@primer/live-region-element": "^0.8.0",
         "@primer/primitives": "^9.1.2",
         "@primer/view-components": "npm:@openproject/primer-view-components@^0.66.2",
         "@types/hotwired__turbo": "^8.0.1",
@@ -4760,6 +4761,15 @@
         "@primer/primitives": "9.x || 10.x"
       }
     },
+    "node_modules/@openproject/primer-view-components/node_modules/@primer/live-region-element": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/@primer/live-region-element/-/live-region-element-0.7.2.tgz",
+      "integrity": "sha512-wdxCHfcJzE1IPPjZNFR4RTwRcSWb7TN0fRdMH5HcxphLEnuZBWy0TAxk3xPA+/6lwiN3uEJ+ZWV4UF/glXh43A==",
+      "license": "MIT",
+      "dependencies": {
+        "@lit-labs/ssr-dom-shim": "^1.2.0"
+      }
+    },
     "node_modules/@openproject/reactivestates": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/@openproject/reactivestates/-/reactivestates-3.0.1.tgz",
@@ -4857,6 +4867,15 @@
         "@primer/primitives": "9.x || 10.x"
       }
     },
+    "node_modules/@primer/view-components/node_modules/@primer/live-region-element": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/@primer/live-region-element/-/live-region-element-0.7.2.tgz",
+      "integrity": "sha512-wdxCHfcJzE1IPPjZNFR4RTwRcSWb7TN0fRdMH5HcxphLEnuZBWy0TAxk3xPA+/6lwiN3uEJ+ZWV4UF/glXh43A==",
+      "license": "MIT",
+      "dependencies": {
+        "@lit-labs/ssr-dom-shim": "^1.2.0"
+      }
+    },
     "node_modules/@probe.gl/env": {
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/@probe.gl/env/-/env-3.6.0.tgz",
@@ -25381,6 +25400,16 @@
         "@oddbird/popover-polyfill": "^0.5.2",
         "@primer/behaviors": "^1.3.4",
         "@primer/live-region-element": "^0.8.0"
+      },
+      "dependencies": {
+        "@primer/live-region-element": {
+          "version": "0.7.2",
+          "resolved": "https://registry.npmjs.org/@primer/live-region-element/-/live-region-element-0.7.2.tgz",
+          "integrity": "sha512-wdxCHfcJzE1IPPjZNFR4RTwRcSWb7TN0fRdMH5HcxphLEnuZBWy0TAxk3xPA+/6lwiN3uEJ+ZWV4UF/glXh43A==",
+          "requires": {
+            "@lit-labs/ssr-dom-shim": "^1.2.0"
+          }
+        }
       }
     },
     "@openproject/reactivestates": {
@@ -25456,6 +25485,16 @@
         "@oddbird/popover-polyfill": "^0.5.2",
         "@primer/behaviors": "^1.3.4",
         "@primer/live-region-element": "^0.8.0"
+      },
+      "dependencies": {
+        "@primer/live-region-element": {
+          "version": "0.7.2",
+          "resolved": "https://registry.npmjs.org/@primer/live-region-element/-/live-region-element-0.7.2.tgz",
+          "integrity": "sha512-wdxCHfcJzE1IPPjZNFR4RTwRcSWb7TN0fRdMH5HcxphLEnuZBWy0TAxk3xPA+/6lwiN3uEJ+ZWV4UF/glXh43A==",
+          "requires": {
+            "@lit-labs/ssr-dom-shim": "^1.2.0"
+          }
+        }
       }
     },
     "@probe.gl/env": {
diff --git a/frontend/package.json b/frontend/package.json
index a0c64ac15cee..c1324388e8f1 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -105,6 +105,7 @@
     "@openproject/primer-view-components": "^0.66.2",
     "@openproject/reactivestates": "^3.0.1",
     "@primer/css": "^21.5.0",
+    "@primer/live-region-element": "^0.8.0",
     "@primer/primitives": "^9.1.2",
     "@primer/view-components": "npm:@openproject/primer-view-components@^0.66.2",
     "@types/hotwired__turbo": "^8.0.1",
diff --git a/frontend/src/stimulus/controllers/dynamic/work-packages/date-picker/preview.controller.ts b/frontend/src/stimulus/controllers/dynamic/work-packages/date-picker/preview.controller.ts
index e7b3262c333a..585c544322e3 100644
--- a/frontend/src/stimulus/controllers/dynamic/work-packages/date-picker/preview.controller.ts
+++ b/frontend/src/stimulus/controllers/dynamic/work-packages/date-picker/preview.controller.ts
@@ -353,7 +353,7 @@ export default class PreviewController extends DialogPreviewController {
     }
   }
 
-  // called from inputs defined in the date_picker/date_form.rb
+  // called from inputs defined in the date_picker/date_form_component.rb
   onHighlightField(e:Event) {
     const fieldToHighlight = e.target as HTMLInputElement;
     if (fieldToHighlight) {
diff --git a/frontend/src/turbo/live-region-stream-action.ts b/frontend/src/turbo/live-region-stream-action.ts
new file mode 100644
index 000000000000..65727bbcb441
--- /dev/null
+++ b/frontend/src/turbo/live-region-stream-action.ts
@@ -0,0 +1,21 @@
+import { StreamActions, StreamElement } from '@hotwired/turbo';
+import { announce } from '@primer/live-region-element';
+
+export function registerLiveRegionStreamAction() {
+  StreamActions.liveRegion = function liveRegionStreamAction(this:StreamElement) {
+    const message = this.getAttribute('message');
+    if (!message) return;
+    const politeness = this.getAttribute('politeness') || 'polite';
+    const delay = parseInt(this.getAttribute('delay') ?? '0', 10);
+    if (politeness === 'assertive') {
+      void announce(message, {
+        politeness: 'assertive',
+      });
+    } else {
+      void announce(message, {
+        politeness: 'polite',
+        delayMs: delay,
+      });
+    }
+  };
+}
diff --git a/frontend/src/turbo/setup.ts b/frontend/src/turbo/setup.ts
index bec41611609b..cfffcd070764 100644
--- a/frontend/src/turbo/setup.ts
+++ b/frontend/src/turbo/setup.ts
@@ -4,6 +4,7 @@ import TurboPower from 'turbo_power';
 import { registerDialogStreamAction } from './dialog-stream-action';
 import { addTurboEventListeners } from './turbo-event-listeners';
 import { registerFlashStreamAction } from './flash-stream-action';
+import { registerLiveRegionStreamAction } from './live-region-stream-action';
 import { registerInputCaptionStreamAction } from './input-caption-stream-action';
 import { addTurboGlobalListeners } from './turbo-global-listeners';
 import { applyTurboNavigationPatch } from './turbo-navigation-patch';
@@ -30,6 +31,7 @@ addTurboEventListeners();
 addTurboGlobalListeners();
 registerDialogStreamAction();
 registerFlashStreamAction();
+registerLiveRegionStreamAction();
 registerInputCaptionStreamAction();
 
 // Apply navigational patch
diff --git a/lookbook/docs/patterns/accessibility/18-aria-live.md.erb b/lookbook/docs/patterns/accessibility/18-aria-live.md.erb
new file mode 100644
index 000000000000..9a685b3d6a92
--- /dev/null
+++ b/lookbook/docs/patterns/accessibility/18-aria-live.md.erb
@@ -0,0 +1,97 @@
+## What are ARIA Live Regions?
+ARIA Live Regions are a crucial accessibility feature that allow assistive technologies to announce dynamic content updates without requiring user focus changes.
+
+### ARIA Live Attributes
+ARIA Live Regions rely on specific attributes to control how updates are announced:
+- `aria-live="polite"`: Announces updates when the user is idle.
+- `aria-live="assertive"`: Announces updates immediately, interrupting the user.
+- `aria-atomic="true"`: Ensures the entire content of the region is read when it changes.
+
+## Implementing ARIA Live Regions in Primer
+The Primer project provides a custom element (Web Component) to ease working with ARIA Live Regions.
+We have decided to use a single global ARIA live region in `base.html` to ensure live regions are present in the DOM before updates occur. This approach is better than having multiple live regions because it ensures that announcements are delivered in a clear and consistent manner. By centralizing updates, we avoid potential conflicts between overlapping live regions and provide a more seamless experience for screen reader users.
+
+### Live Region in `base.html`
+```html
+<live-region>
+  <div id="polite" aria-live="polite" aria-atomic="true" class="hidden-for-sighted"></div>
+  <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
+</live-region>
+```
+
+The `@primer/live-region-element` package provides methods to trigger announcements programmatically:
+
+```ts
+import { announce } from '@primer/live-region-element';
+
+announce('Example polite message', { politeness: 'polite' });
+```
+
+### Using Turbo Streams to Trigger ARIA Live Updates
+To use it in a Rails Turbo response, and send updates to the assistive technology after any action, we created a new Turbo stream component that triggers an liveRegion action:
+
+```ruby
+OpTurbo::StreamComponent.new(action: :liveRegion, message: "Form submission successful!", politeness: "polite", role: "status", target: nil)
+```
+
+- This creates a **Turbo Stream update** with:
+  - `action: :liveRegion` → Calls the `liveRegion` action defined in `live-region-stream-action.ts`
+  - `message: "Form submission successful!"` → The text to be announced to user
+  - `politeness: "polite"` → Ensures the message is read when the user is idle
+  - `target: nil` → Ensures the update isn't inserted into the DOM but is handled as an announcement
+
+### How to Trigger an ARIA Update from the Controller
+You can trigger a assistive technology announcement from your Rails controller by calling the provided `render_live_region_update_message` helper:
+
+```ruby
+render_live_region_update_message(
+  message: "A relation is added",
+  politeness: "polite"
+)
+```
+
+This helper internally renders a Turbo Stream using the liveRegion action to announce the message via ARIA live regions.
+For reference only — you do not need to define this yourself.
+Here is the internal implementation of `render_live_region_update_message`:
+
+```ruby
+    def render_live_region_update_message(message:, politeness:)
+      turbo_streams << OpTurbo::StreamComponent.new(action: :liveRegion, message:, politeness:, target: nil).render_in(view_context)
+    end
+```
+
+### How to Trigger an ARIA Update from a Turbo Frame
+If you're using a Turbo Frame and want to announce dynamic content changes to assistive technologies, you can embed a Turbo Stream using the `liveRegion` action directly inside the frame.
+
+```erb
+<%= content_tag("turbo-frame", id: "frame-id") do %>
+....
+  <%= render OpTurbo::StreamComponent.new(
+    action: :liveRegion,
+    message: "Update!!",
+    politeness: "polite",
+    target: nil
+  ) %>
+<% end %>
+
+```
+
+### How ARIA Live Messages Are Delivered via Turbo Streams
+The `registerLiveRegionStreamAction` method defines a custom Turbo Stream action called `liveRegion`. When triggered, it announces a message (using ARIA live regions) to assistive technologies, with a tone of either `"polite"` or `"assertive"`, depending on the `politeness` attribute in the stream element.
+
+```ts
+<%= render file: "frontend/src/turbo/live-region-stream-action.ts" %>
+```
+
+## Live Regions in Dialogs and Modals
+There is an important exception to our use of the global ARIA Live Region in `base.html`.
+
+When a modal or dialog is open, screen readers often restrict focus and reading context to the content inside the dialog. In this case, live regions outside the dialog (such as those defined in `base.html`) become inaccessible to the screen reader.
+
+To ensure that announcements are still read aloud in this situation, we should include a live-region component within the dialog itself, like what we have implemented for primerized date picker.
+
+```erb
+  <live-region>
+  </live-region>
+  <%= render OpTurbo::StreamComponent.new(action: :liveRegion, message: "Date picker is updated.", politeness: "polite", target: nil) %>
+```
diff --git a/spec/components/work_packages/date_picker/dialog_content_component_spec.rb b/spec/components/work_packages/date_picker/dialog_content_component_spec.rb
index fc068a9d8b2e..e17ffbd27031 100644
--- a/spec/components/work_packages/date_picker/dialog_content_component_spec.rb
+++ b/spec/components/work_packages/date_picker/dialog_content_component_spec.rb
@@ -50,9 +50,9 @@
       let(:schedule_manually) { true }
 
       it "shows the date form" do
-        expect(dialog_content).to have_field(I18n.t("attributes.start_date"), aria: { live: :polite })
-        expect(dialog_content).to have_field(I18n.t("attributes.due_date"), aria: { live: :polite })
-        expect(dialog_content).to have_field(WorkPackage.human_attribute_name("duration"), aria: { live: :polite })
+        expect(dialog_content).to have_field(I18n.t("attributes.start_date"))
+        expect(dialog_content).to have_field(I18n.t("attributes.due_date"))
+        expect(dialog_content).to have_field(WorkPackage.human_attribute_name("duration"))
       end
 
       it "has an enabled save button" do
diff --git a/spec/controllers/work_packages/date_picker_controller_spec.rb b/spec/controllers/work_packages/date_picker_controller_spec.rb
index 9eee2c5541fb..5277f749b35f 100644
--- a/spec/controllers/work_packages/date_picker_controller_spec.rb
+++ b/spec/controllers/work_packages/date_picker_controller_spec.rb
@@ -189,6 +189,31 @@ def date_errors(work_package)
         end
       end
     end
+
+    context "when changing the start date" do
+      let(:params_after_changing_start_date) do
+        params_after_changing_values.deep_merge(
+          "work_package" => {
+            "start_date" => "2025-04-15",
+            "start_date_touched" => "true",
+            "due_date" => "2025-04-21",
+            "due_date_touched" => "true"
+          }
+        )
+      end
+
+      it "includes the live region turbo-stream with the correct message and attributes" do
+        get("new", params: params_after_changing_start_date)
+
+        expect(response.body).to include('<turbo-stream action="liveRegion"')
+        expect(response.body).to include('politeness="polite"')
+        expect(response.body).to include('delay="500"')
+
+        expected_message = "Date picker updated. Scheduling mode: Manual, working days only, " +
+          "Start date: 2025-04-15, Finish date: 2025-04-21, Duration: 5 days"
+        expect(response.body).to include("message=\"#{expected_message}\"")
+      end
+    end
   end
 
   describe "GET /work_packages/:id/progress" do


Reviews:
user: login: HDinger
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T06:50:14Z
user: login: bsatarnejad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T07:27:20Z
user: login: bsatarnejad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T09:15:29Z
user: login: myabc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T11:34:58Z
user: login: myabc
body: Hi @bsatarnejad. I'm really sorry that it's taken so long for me to give feedback on this PR.

I have only tested on macOS with VoiceOver so far - but the good news is that, for the most part, everything works as expected, and that this is a nice enhancement for users of assistive technology.

There are various things I think we should address though:

- **assertiveness**: while the "assertive" behaviour with the date picker seems to work well, the "polite" updates on adding a relation don't work so well, with VoiceOver announcing the change long after the flash message disappears.

<img width="1512" alt="image" src="https://github.com/user-attachments/assets/3c2d9590-fad2-42d7-a054-f2b893c40878" />

https://github.com/user-attachments/assets/1f237bd0-cc0b-41dd-8c60-96a7d291e53a

(this might beg the question as to whether we should look into integrating flash messages + live regions)

- **commit hygiene**: this PR now consists of 54 commits, despite not being huge in terms of changes (`16 files changed, 240 insertions(+), 11 deletions(-)`). I realise that this is because it is a long-running piece of work that has suffered from requirement churn and changes in prioritisation along the way. That said, although OP policy AFAICT is to try to maintain commit history, I think this PR would benefit from some cleanup (namely rebasing + squashing of related commits). I'm happy to help with this.

- **documentation**: this is generally fine, but I think we should prefer the more generic term "asssistive technology" (and "users of A.T.") to "screen readers" where possible.

  - we should try to be consistent with capitalisation: "ARIA Live Regions" vs. "live regions".
  - With Turbo Streams, I think we should encourage the use of the `render_aria_update_message` helper over the component directly.

- **naming**: We should call the custom Turbo Stream Action something more specific than "ARIA". Perhaps `LiveRegionStreamAction`?

- **design**: Should we introduce a specialization of the `OpTurbo::StreamComponent` that enforces correct arguments - rather than passing necessary args for the `aria` action as `**turbo_stream_args`?

- **tests**: there are currently no tests for these changes. We should discuss how best to test (both with Capy features and specs for components/helpers).

If anything is unclear, I'd be happy to explain! - in any case, it's probably worth us setting aside some time to discuss in more detail. Cheers!
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T23:26:09Z
user: login: myabc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T23:48:21Z
user: login: myabc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T23:58:51Z
user: login: bsatarnejad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T15:14:05Z
user: login: bsatarnejad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T12:02:50Z
user: login: myabc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T10:34:45Z
user: login: myabc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T10:43:40Z

Review comments:
diff_hunk: @@ -98,6 +102,8 @@
           end
         end
       end
-    end
-  end
-%>
+      end%>
path: app/components/work_packages/date_picker/dialog_content_component.html.erb
user: login: HDinger
body: Wrong indentation
created_at: 2025-04-30T06:24:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -364,7 +364,6 @@ export default class PreviewController extends DialogPreviewController {
     }
   }
 
-  // called from inputs defined in the date_picker/date_form.rb
path: frontend/src/stimulus/controllers/dynamic/work-packages/date-picker/preview.controller.ts
user: login: HDinger
body: Why did you remove that comment?
created_at: 2025-04-30T06:28:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -101,7 +101,11 @@ def respond_with_relations_tab_update(service_result, **)
       component = WorkPackageRelationsTab::IndexComponent.new(work_package: @work_package, **)
       replace_via_turbo_stream(component:)
       render_success_flash_message_via_turbo_stream(message: I18n.t(:notice_successful_update))
-
+      render_aria_update_message(
+        message: I18n.t("work_package_relations_tab.relations.create_child_aria_live_message"),
+        type: "polite",
+        role: "alert"
path: app/controllers/work_package_children_relations_controller.rb
user: login: HDinger
body: `role: "alert"` is the default, so this line can be ommited.
created_at: 2025-04-30T06:30:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -103,7 +103,11 @@ def respond_with_relations_tab_update(service_result, **)
       component = WorkPackageRelationsTab::IndexComponent.new(work_package: @work_package, **)
       replace_via_turbo_stream(component:)
       render_success_flash_message_via_turbo_stream(message: I18n.t(:notice_successful_update))
-
+      render_aria_update_message(
+        message: I18n.t("work_package_relations_tab.relations.create_relation_aria_live_message"),
+        type: "polite",
+        role: "alert"
path: app/controllers/work_package_relations_controller.rb
user: login: HDinger
body:  Same here, can be omitted
created_at: 2025-04-30T06:30:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,93 @@
+## What are ARIA Live Regions?
+ARIA Live Regions are a crucial accessibility feature that allows screen readers to announce dynamic content updates without requiring user focus changes.
+
+### ARIA Live Attributes
+ARIA Live Regions rely on specific attributes to control how updates are announced:
+- `aria-live="polite"`: Announces updates when the user is idle.
+- `aria-live="assertive"`: Announces updates immediately, interrupting the user.
+- `aria-atomic="true"`: Ensures the entire content of the region is read when it changes.
+
+## Implementing ARIA Live Regions in Primer
+Primer provides a built-in component named live-region to implement ARIA Live Regions easily.
+We have decided to use a single global ARIA live region in base.html to ensure live regions are present in the DOM before updates occur. This approach is better than having multiple live regions because it ensures that announcements are delivered in a clear and consistent manner. By centralizing updates, we avoid potential conflicts between overlapping live regions and provide a more seamless experience for screen reader users.
+There is an exception for modal or dialog, especially if that dialog traps focus. When a dialog is open, screen readers often limit their reading and focus to within the dialog. If aria-live region is in base.html (outside the dialog), it becomes “invisible” to the screen reader while the modal is open — it's still updated in the DOM, but not announced until the dialog is closed, and the screen reader returns to the full DOM scope.
path: lookbook/docs/patterns/18-aria-live.md.erb
user: login: HDinger
body: ```suggestion
There is an exception for modals or dialogs, especially if that dialog traps focus. When a dialog is open, screen readers often limit their reading and focus to the content of the dialog. Aria-live regions that are part of the base.html and thus outside of the dialog become “invisible” to the screen reader as long as the modal is open.
```
created_at: 2025-04-30T06:34:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,93 @@
+## What are ARIA Live Regions?
+ARIA Live Regions are a crucial accessibility feature that allows screen readers to announce dynamic content updates without requiring user focus changes.
+
+### ARIA Live Attributes
+ARIA Live Regions rely on specific attributes to control how updates are announced:
+- `aria-live="polite"`: Announces updates when the user is idle.
+- `aria-live="assertive"`: Announces updates immediately, interrupting the user.
+- `aria-atomic="true"`: Ensures the entire content of the region is read when it changes.
+
+## Implementing ARIA Live Regions in Primer
+Primer provides a built-in component named live-region to implement ARIA Live Regions easily.
+We have decided to use a single global ARIA live region in base.html to ensure live regions are present in the DOM before updates occur. This approach is better than having multiple live regions because it ensures that announcements are delivered in a clear and consistent manner. By centralizing updates, we avoid potential conflicts between overlapping live regions and provide a more seamless experience for screen reader users.
+There is an exception for modal or dialog, especially if that dialog traps focus. When a dialog is open, screen readers often limit their reading and focus to within the dialog. If aria-live region is in base.html (outside the dialog), it becomes “invisible” to the screen reader while the modal is open — it's still updated in the DOM, but not announced until the dialog is closed, and the screen reader returns to the full DOM scope.
+In this case, we should add the live-region component to the dialog itself, like what we have implemented for primerized date picker.
+
+### Live Region in `base.html`
+```html
+<live-region>
+  <div id="polite" aria-live="polite" aria-atomic="true" class="hidden-for-sighted"></div>
+  <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
+</live-region>
+```
+
+The `@primer/live-region-element` package provides methods to trigger announcements programmatically:
+
+```ts
+import { announce } from '@primer/live-region-element';
+
+announce('Example polite message', { politeness: 'polite' });
+```
+
+### Using Turbo Streams to Trigger ARIA Live Updates
+To use it in a Rails Turbo response, and send updates to the screen reader after any action, we create a new Turbo stream component that triggers an ARIA action:
path: lookbook/docs/patterns/18-aria-live.md.erb
user: login: HDinger
body: ```suggestion
To use it in a Rails Turbo response, and send updates to the screen reader after any action, we created a new Turbo stream component that triggers an ARIA action:
```
created_at: 2025-04-30T06:35:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,93 @@
+## What are ARIA Live Regions?
+ARIA Live Regions are a crucial accessibility feature that allows screen readers to announce dynamic content updates without requiring user focus changes.
+
+### ARIA Live Attributes
+ARIA Live Regions rely on specific attributes to control how updates are announced:
+- `aria-live="polite"`: Announces updates when the user is idle.
+- `aria-live="assertive"`: Announces updates immediately, interrupting the user.
+- `aria-atomic="true"`: Ensures the entire content of the region is read when it changes.
+
+## Implementing ARIA Live Regions in Primer
+Primer provides a built-in component named live-region to implement ARIA Live Regions easily.
+We have decided to use a single global ARIA live region in base.html to ensure live regions are present in the DOM before updates occur. This approach is better than having multiple live regions because it ensures that announcements are delivered in a clear and consistent manner. By centralizing updates, we avoid potential conflicts between overlapping live regions and provide a more seamless experience for screen reader users.
+There is an exception for modal or dialog, especially if that dialog traps focus. When a dialog is open, screen readers often limit their reading and focus to within the dialog. If aria-live region is in base.html (outside the dialog), it becomes “invisible” to the screen reader while the modal is open — it's still updated in the DOM, but not announced until the dialog is closed, and the screen reader returns to the full DOM scope.
+In this case, we should add the live-region component to the dialog itself, like what we have implemented for primerized date picker.
+
+### Live Region in `base.html`
+```html
+<live-region>
+  <div id="polite" aria-live="polite" aria-atomic="true" class="hidden-for-sighted"></div>
+  <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
+</live-region>
+```
+
+The `@primer/live-region-element` package provides methods to trigger announcements programmatically:
+
+```ts
+import { announce } from '@primer/live-region-element';
+
+announce('Example polite message', { politeness: 'polite' });
+```
+
+### Using Turbo Streams to Trigger ARIA Live Updates
+To use it in a Rails Turbo response, and send updates to the screen reader after any action, we create a new Turbo stream component that triggers an ARIA action:
+```ruby
+OpTurbo::StreamComponent.new(action: :aria, message: "Form submission successful!", type: "polite", role: "status", target: "nil")
+```
+- This creates a **Turbo Stream update** with:
+  - `action: :aria` → Calls the `aria` action defined in `aria.stream.action.ts`
+  - `message: "Form submission successful!"` → The text announced by the screen reader
+  - `type: "polite"` → Ensures the message is read when the user is idle
+  - `role: "status"` → Helps screen readers recognize this as a status update
+  - `target: "nil"` → Ensures the update isn't inserted into the DOM but is handled as an announcement
+
+#### How to use it?
+In controller:
+```ruby
+    def render_aria_update_message(message:, type:, role: "alert")
+      turbo_streams << OpTurbo::StreamComponent.new(action: :aria, message:, type:, role:, target: "nil").render_in(view_context)
+    end
path: lookbook/docs/patterns/18-aria-live.md.erb
user: login: HDinger
body: I'd split that example into two, as it currently suggests, that people have to add that method themselves. However, they should just use the method we already provided.
created_at: 2025-04-30T06:36:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,93 @@
+## What are ARIA Live Regions?
path: lookbook/docs/patterns/18-aria-live.md.erb
user: login: HDinger
body: Please create a folder called "Accessibility" and move that file in there.
created_at: 2025-04-30T06:37:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,93 @@
+## What are ARIA Live Regions?
+ARIA Live Regions are a crucial accessibility feature that allows screen readers to announce dynamic content updates without requiring user focus changes.
+
+### ARIA Live Attributes
+ARIA Live Regions rely on specific attributes to control how updates are announced:
+- `aria-live="polite"`: Announces updates when the user is idle.
+- `aria-live="assertive"`: Announces updates immediately, interrupting the user.
+- `aria-atomic="true"`: Ensures the entire content of the region is read when it changes.
+
+## Implementing ARIA Live Regions in Primer
+Primer provides a built-in component named live-region to implement ARIA Live Regions easily.
+We have decided to use a single global ARIA live region in base.html to ensure live regions are present in the DOM before updates occur. This approach is better than having multiple live regions because it ensures that announcements are delivered in a clear and consistent manner. By centralizing updates, we avoid potential conflicts between overlapping live regions and provide a more seamless experience for screen reader users.
+There is an exception for modal or dialog, especially if that dialog traps focus. When a dialog is open, screen readers often limit their reading and focus to within the dialog. If aria-live region is in base.html (outside the dialog), it becomes “invisible” to the screen reader while the modal is open — it's still updated in the DOM, but not announced until the dialog is closed, and the screen reader returns to the full DOM scope.
path: lookbook/docs/patterns/18-aria-live.md.erb
user: login: HDinger
body: Please add an empty line between the paragraphs, otherwise it is really hard to read.
created_at: 2025-04-30T06:38:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,93 @@
+## What are ARIA Live Regions?
+ARIA Live Regions are a crucial accessibility feature that allows screen readers to announce dynamic content updates without requiring user focus changes.
+
+### ARIA Live Attributes
+ARIA Live Regions rely on specific attributes to control how updates are announced:
+- `aria-live="polite"`: Announces updates when the user is idle.
+- `aria-live="assertive"`: Announces updates immediately, interrupting the user.
+- `aria-atomic="true"`: Ensures the entire content of the region is read when it changes.
+
+## Implementing ARIA Live Regions in Primer
+Primer provides a built-in component named live-region to implement ARIA Live Regions easily.
+We have decided to use a single global ARIA live region in base.html to ensure live regions are present in the DOM before updates occur. This approach is better than having multiple live regions because it ensures that announcements are delivered in a clear and consistent manner. By centralizing updates, we avoid potential conflicts between overlapping live regions and provide a more seamless experience for screen reader users.
+There is an exception for modal or dialog, especially if that dialog traps focus. When a dialog is open, screen readers often limit their reading and focus to within the dialog. If aria-live region is in base.html (outside the dialog), it becomes “invisible” to the screen reader while the modal is open — it's still updated in the DOM, but not announced until the dialog is closed, and the screen reader returns to the full DOM scope.
+In this case, we should add the live-region component to the dialog itself, like what we have implemented for primerized date picker.
+
+### Live Region in `base.html`
+```html
+<live-region>
+  <div id="polite" aria-live="polite" aria-atomic="true" class="hidden-for-sighted"></div>
+  <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
+</live-region>
+```
+
+The `@primer/live-region-element` package provides methods to trigger announcements programmatically:
+
+```ts
+import { announce } from '@primer/live-region-element';
+
+announce('Example polite message', { politeness: 'polite' });
+```
+
+### Using Turbo Streams to Trigger ARIA Live Updates
+To use it in a Rails Turbo response, and send updates to the screen reader after any action, we create a new Turbo stream component that triggers an ARIA action:
+```ruby
+OpTurbo::StreamComponent.new(action: :aria, message: "Form submission successful!", type: "polite", role: "status", target: "nil")
+```
+- This creates a **Turbo Stream update** with:
+  - `action: :aria` → Calls the `aria` action defined in `aria.stream.action.ts`
+  - `message: "Form submission successful!"` → The text announced by the screen reader
+  - `type: "polite"` → Ensures the message is read when the user is idle
+  - `role: "status"` → Helps screen readers recognize this as a status update
+  - `target: "nil"` → Ensures the update isn't inserted into the DOM but is handled as an announcement
+
+#### How to use it?
+In controller:
+```ruby
+    def render_aria_update_message(message:, type:, role: "alert")
+      turbo_streams << OpTurbo::StreamComponent.new(action: :aria, message:, type:, role:, target: "nil").render_in(view_context)
+    end
+
+    def update
+      render_success_flash_message_via_turbo_stream(message: I18n.t(:notice_successful_update))
+      render_aria_update_message(message: I18n.t("work_package_relations_tab.relations.create_child_aria_live_message"),type: "polite",role: "alert")
+      respond_with_turbo_streams
+    end
+```
+When updating via Turbo Frame:
+```erb
+<%= content_tag("turbo-frame", id: "frame-id") do %>
+....
+  <%= render OpTurbo::StreamComponent.new(
+    action: :aria,
+    message: "Update!!",
+    type: "polite",
+    target: "nil"
+  ) %>
+<% end %>
+
+```
+
+### Turbo Stream Action for ARIA Live
+The `registerAriaStreamAction` method defines a custom Turbo Stream action called `aria`. When triggered, it announces a message (using ARIA live regions) to screen readers, with a tone of either `"polite"` or `"assertive"`, depending on the `type` attribute in the stream element.
+```ts
+import { StreamActions, StreamElement } from '@hotwired/turbo';
+import { announce } from '@primer/live-region-element';
+
+export function registerAriaStreamAction() {
+  StreamActions.aria = function dialogStreamAction(this:StreamElement) {
+      const message = this.getAttribute('message') ?? '';
+      const type = this.getAttribute('type') ?? 'polite';
+      if (type === 'assertive') {
+        void announce(message, {
+          politeness: 'assertive',
+        });
+      } else {
+        void announce(message, {
+          politeness: 'polite',
+          delayMs: 5000,
+        });
+      }
path: lookbook/docs/patterns/18-aria-live.md.erb
user: login: HDinger
body: I really don't understand what this paragraph and the method is about or what it is used for. The headline says "Turbo Stream Action for ARIA Live" but the previous headline is also for Turbo stream actions with headlines..
created_at: 2025-04-30T06:41:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,93 @@
+## What are ARIA Live Regions?
+ARIA Live Regions are a crucial accessibility feature that allows screen readers to announce dynamic content updates without requiring user focus changes.
+
+### ARIA Live Attributes
+ARIA Live Regions rely on specific attributes to control how updates are announced:
+- `aria-live="polite"`: Announces updates when the user is idle.
+- `aria-live="assertive"`: Announces updates immediately, interrupting the user.
+- `aria-atomic="true"`: Ensures the entire content of the region is read when it changes.
+
+## Implementing ARIA Live Regions in Primer
+Primer provides a built-in component named live-region to implement ARIA Live Regions easily.
+We have decided to use a single global ARIA live region in base.html to ensure live regions are present in the DOM before updates occur. This approach is better than having multiple live regions because it ensures that announcements are delivered in a clear and consistent manner. By centralizing updates, we avoid potential conflicts between overlapping live regions and provide a more seamless experience for screen reader users.
+There is an exception for modal or dialog, especially if that dialog traps focus. When a dialog is open, screen readers often limit their reading and focus to within the dialog. If aria-live region is in base.html (outside the dialog), it becomes “invisible” to the screen reader while the modal is open — it's still updated in the DOM, but not announced until the dialog is closed, and the screen reader returns to the full DOM scope.
+In this case, we should add the live-region component to the dialog itself, like what we have implemented for primerized date picker.
+
+### Live Region in `base.html`
+```html
+<live-region>
+  <div id="polite" aria-live="polite" aria-atomic="true" class="hidden-for-sighted"></div>
+  <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
+</live-region>
+```
+
+The `@primer/live-region-element` package provides methods to trigger announcements programmatically:
+
+```ts
+import { announce } from '@primer/live-region-element';
+
+announce('Example polite message', { politeness: 'polite' });
+```
+
+### Using Turbo Streams to Trigger ARIA Live Updates
+To use it in a Rails Turbo response, and send updates to the screen reader after any action, we create a new Turbo stream component that triggers an ARIA action:
+```ruby
+OpTurbo::StreamComponent.new(action: :aria, message: "Form submission successful!", type: "polite", role: "status", target: "nil")
+```
+- This creates a **Turbo Stream update** with:
+  - `action: :aria` → Calls the `aria` action defined in `aria.stream.action.ts`
+  - `message: "Form submission successful!"` → The text announced by the screen reader
+  - `type: "polite"` → Ensures the message is read when the user is idle
+  - `role: "status"` → Helps screen readers recognize this as a status update
+  - `target: "nil"` → Ensures the update isn't inserted into the DOM but is handled as an announcement
+
+#### How to use it?
+In controller:
+```ruby
+    def render_aria_update_message(message:, type:, role: "alert")
+      turbo_streams << OpTurbo::StreamComponent.new(action: :aria, message:, type:, role:, target: "nil").render_in(view_context)
+    end
+
+    def update
+      render_success_flash_message_via_turbo_stream(message: I18n.t(:notice_successful_update))
+      render_aria_update_message(message: I18n.t("work_package_relations_tab.relations.create_child_aria_live_message"),type: "polite",role: "alert")
+      respond_with_turbo_streams
+    end
+```
+When updating via Turbo Frame:
path: lookbook/docs/patterns/18-aria-live.md.erb
user: login: HDinger
body: Missing newline
created_at: 2025-04-30T06:42:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,19 @@
+import { StreamActions, StreamElement } from '@hotwired/turbo';
+import { announce } from '@primer/live-region-element';
+
+export function registerAriaStreamAction() {
+  StreamActions.aria = function dialogStreamAction(this:StreamElement) {
path: frontend/src/turbo/aria-stream-action.ts
user: login: HDinger
body: What is that needed for?
created_at: 2025-04-30T06:42:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -83,6 +83,10 @@ def render_error_flash_message_via_turbo_stream(**)
       render_flash_message_via_turbo_stream(**, scheme: :danger, icon: :stop)
     end
 
+    def render_aria_update_message(message:, type:, role: "alert")
+      turbo_streams << OpTurbo::StreamComponent.new(action: :aria, message:, type:, role:, target: "nil").render_in(view_context)
path: app/controllers/concerns/op_turbo/component_stream.rb
user: login: HDinger
body: The target is weird. Should that really be a string "nil" or rather the object `nil`?
created_at: 2025-04-30T06:43:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,93 @@
+## What are ARIA Live Regions?
+ARIA Live Regions are a crucial accessibility feature that allows screen readers to announce dynamic content updates without requiring user focus changes.
+
+### ARIA Live Attributes
+ARIA Live Regions rely on specific attributes to control how updates are announced:
+- `aria-live="polite"`: Announces updates when the user is idle.
+- `aria-live="assertive"`: Announces updates immediately, interrupting the user.
+- `aria-atomic="true"`: Ensures the entire content of the region is read when it changes.
+
+## Implementing ARIA Live Regions in Primer
+Primer provides a built-in component named live-region to implement ARIA Live Regions easily.
+We have decided to use a single global ARIA live region in base.html to ensure live regions are present in the DOM before updates occur. This approach is better than having multiple live regions because it ensures that announcements are delivered in a clear and consistent manner. By centralizing updates, we avoid potential conflicts between overlapping live regions and provide a more seamless experience for screen reader users.
+There is an exception for modal or dialog, especially if that dialog traps focus. When a dialog is open, screen readers often limit their reading and focus to within the dialog. If aria-live region is in base.html (outside the dialog), it becomes “invisible” to the screen reader while the modal is open — it's still updated in the DOM, but not announced until the dialog is closed, and the screen reader returns to the full DOM scope.
+In this case, we should add the live-region component to the dialog itself, like what we have implemented for primerized date picker.
path: lookbook/docs/patterns/18-aria-live.md.erb
user: login: HDinger
body: Maybe you should make this a separate section with its own heading (e.g. "Live regions in Dialogs") and push it a bit down, maybe even at the end. It feels very confusing and I am missing an example.
created_at: 2025-04-30T06:45:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -98,6 +102,8 @@
           end
         end
       end
-    end
-  end
-%>
+      end%>
+  <% if @live_region_message %>
+    <%= render OpTurbo::StreamComponent.new(action: :aria, message: @live_region_message, type:"assertive", target: "nil") %>
+  <% end %>
path: app/components/work_packages/date_picker/dialog_content_component.html.erb
user: login: HDinger
body: Could that be moved to the top below the actual `live-region`?
created_at: 2025-04-30T06:46:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -364,7 +364,6 @@ export default class PreviewController extends DialogPreviewController {
     }
   }
 
-  // called from inputs defined in the date_picker/date_form.rb
path: frontend/src/stimulus/controllers/dynamic/work-packages/date-picker/preview.controller.ts
user: login: bsatarnejad
body: This file doesn't exist anymore. I will change it to date_picker/date_form_component.rb 👍 
created_at: 2025-04-30T07:27:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,19 @@
+import { StreamActions, StreamElement } from '@hotwired/turbo';
+import { announce } from '@primer/live-region-element';
+
+export function registerAriaStreamAction() {
+  StreamActions.aria = function dialogStreamAction(this:StreamElement) {
path: frontend/src/turbo/aria-stream-action.ts
user: login: bsatarnejad
body: I want to define aria action for turbo, I must explicitly register it by assigning a function to StreamActions.aria. Therefore, when a stream with action="aria" arrives, run this custom function I’m registering as StreamActions.aria.
created_at: 2025-04-30T09:15:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+## What are ARIA Live Regions?
+ARIA Live Regions are a crucial accessibility feature that allows screen readers to announce dynamic content updates without requiring user focus changes.
path: lookbook/docs/patterns/accessibility/18-aria-live.md.erb
user: login: myabc
body: ```suggestion
ARIA Live Regions are a crucial accessibility feature that allow screen readers to announce dynamic content updates without requiring user focus changes.
```
created_at: 2025-05-05T11:34:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,19 @@
+import { StreamActions, StreamElement } from '@hotwired/turbo';
+import { announce } from '@primer/live-region-element';
+
+export function registerAriaStreamAction() {
+  StreamActions.aria = function ariaStreamAction(this:StreamElement) {
+      const message = this.getAttribute('message') ?? '';
+      const type = this.getAttribute('type') ?? 'polite';
path: frontend/src/turbo/aria-stream-action.ts
user: login: myabc
body: Logical OR here is probably better here - so that we also handle blank attribute values, e.g. `type=""`.

```suggestion
      const message = this.getAttribute('message') || '';
      const type = this.getAttribute('type') || 'polite';
```

created_at: 2025-05-06T18:11:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,19 @@
+import { StreamActions, StreamElement } from '@hotwired/turbo';
+import { announce } from '@primer/live-region-element';
+
+export function registerAriaStreamAction() {
+  StreamActions.aria = function ariaStreamAction(this:StreamElement) {
+      const message = this.getAttribute('message') ?? '';
+      const type = this.getAttribute('type') ?? 'polite';
+      if (type === 'assertive') {
+        void announce(message, {
+          politeness: 'assertive',
+        });
+      } else {
+        void announce(message, {
+          politeness: 'polite',
+          delayMs: 5000,
path: frontend/src/turbo/aria-stream-action.ts
user: login: myabc
body: Magic number. Where does this value come from? - it'd be good to replace with a constant, and ideally also document why 5 seconds (as opposed to the [default 150ms](https://github.com/primer/live-region-element/blob/main/packages/live-region-element/src/live-region-element.ts#L49C6-L49C38)) is an appropriate value.

💡 If 5 seconds is an appropriate value for the Date Picker, but not elsewhere, we could also make this configurable - as an attribute on the Turbo Stream element.
created_at: 2025-05-06T18:13:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+## What are ARIA Live Regions?
+ARIA Live Regions are a crucial accessibility feature that allows screen readers to announce dynamic content updates without requiring user focus changes.
+
+### ARIA Live Attributes
+ARIA Live Regions rely on specific attributes to control how updates are announced:
+- `aria-live="polite"`: Announces updates when the user is idle.
+- `aria-live="assertive"`: Announces updates immediately, interrupting the user.
+- `aria-atomic="true"`: Ensures the entire content of the region is read when it changes.
+
+## Implementing ARIA Live Regions in Primer
+Primer provides a built-in component named live-region to implement ARIA Live Regions easily.
path: lookbook/docs/patterns/accessibility/18-aria-live.md.erb
user: login: myabc
body: "built-in" a little ambiguous - it's not a built-in View Component, but rather another library from the Primer project.

```suggestion
The Primer project provides a custom element (Web Component) to ease working with ARIA Live Regions.
```

----

For separate discussion with @camertron, @HDinger and team:

We should differentiate between the "Primer project" (i.e. what's online at https://github.com/primer), PVC upstream and OpenProject's fork of PVC..
created_at: 2025-05-06T18:31:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+## What are ARIA Live Regions?
+ARIA Live Regions are a crucial accessibility feature that allows screen readers to announce dynamic content updates without requiring user focus changes.
+
+### ARIA Live Attributes
+ARIA Live Regions rely on specific attributes to control how updates are announced:
+- `aria-live="polite"`: Announces updates when the user is idle.
+- `aria-live="assertive"`: Announces updates immediately, interrupting the user.
+- `aria-atomic="true"`: Ensures the entire content of the region is read when it changes.
+
+## Implementing ARIA Live Regions in Primer
+Primer provides a built-in component named live-region to implement ARIA Live Regions easily.
+We have decided to use a single global ARIA live region in base.html to ensure live regions are present in the DOM before updates occur. This approach is better than having multiple live regions because it ensures that announcements are delivered in a clear and consistent manner. By centralizing updates, we avoid potential conflicts between overlapping live regions and provide a more seamless experience for screen reader users.
path: lookbook/docs/patterns/accessibility/18-aria-live.md.erb
user: login: myabc
body: ```suggestion
We have decided to use a single global ARIA live region in `base.html` to ensure live regions are present in the DOM before updates occur. This approach is better than having multiple live regions because it ensures that announcements are delivered in a clear and consistent manner. By centralizing updates, we avoid potential conflicts between overlapping live regions and provide a more seamless experience for screen reader users.
```
created_at: 2025-05-06T18:33:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,5 +1,13 @@
 <%=
   content_tag("turbo-frame", id: "wp-datepicker-dialog--content") do
+%>
+  <live-region>
+    <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
+  </live-region>
+  <% if @live_region_message %>
+    <%= render OpTurbo::StreamComponent.new(action: :aria, message: @live_region_message, type: "assertive", target: "nil") %>
path: app/components/work_packages/date_picker/dialog_content_component.html.erb
user: login: myabc
body: Is `target: "nil"` correct here?
created_at: 2025-05-06T18:48:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -157,7 +157,29 @@ def datepicker_modal_component
                                                          focused_field:,
                                                          triggering_field: params[:triggering_field],
                                                          touched_field_map:,
-                                                         date_mode:)
+                                                         date_mode:,
+                                                         live_region_message: live_region_message)
path: app/controllers/work_packages/date_picker_controller.rb
user: login: myabc
body: Nitpick. You can use shorthand Hash syntax here:

```suggestion
                                                         live_region_message:)
```
created_at: 2025-05-06T18:50:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -157,7 +157,29 @@ def datepicker_modal_component
                                                          focused_field:,
                                                          triggering_field: params[:triggering_field],
                                                          touched_field_map:,
-                                                         date_mode:)
+                                                         date_mode:,
+                                                         live_region_message: live_region_message)
+  end
+
+  def live_region_message
+    I18n.t(
+      "work_packages.datepicker_modal.update_inputs_aria_live_message",
+      start_date: @work_package.start_date,
+      due_date: @work_package.due_date,
+      duration: @work_package.duration,
+      working_days_only: working_days_label,
+      scheduling: scheduling_label
path: app/controllers/work_packages/date_picker_controller.rb
user: login: myabc
body: Nitpick (i.e.don't feel obliged to change) - if you name the keys and values the same, you can take advantage of shorthand Hash syntax here too. e.g.

```suggestion
      working_days_label:,
      scheduling_label:
```
created_at: 2025-05-06T18:52:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -157,7 +157,29 @@ def datepicker_modal_component
                                                          focused_field:,
                                                          triggering_field: params[:triggering_field],
                                                          touched_field_map:,
-                                                         date_mode:)
+                                                         date_mode:,
+                                                         live_region_message: live_region_message)
+  end
+
+  def live_region_message
+    I18n.t(
+      "work_packages.datepicker_modal.update_inputs_aria_live_message",
+      start_date: @work_package.start_date,
+      due_date: @work_package.due_date,
+      duration: @work_package.duration,
+      working_days_only: working_days_label,
+      scheduling: scheduling_label
+    )
+  end
+
+  def working_days_label
+    key = @work_package.ignore_non_working_days ? "true" : "false"
+    I18n.t("activerecord.attributes.work_package.include_non_working_days.#{key}")
path: app/controllers/work_packages/date_picker_controller.rb
user: login: myabc
body: You can skip the intermediate variable and just use interpolation:

```suggestion
    I18n.t("activerecord.attributes.work_package.include_non_working_days.#{@work_package.ignore_non_working_days}")
```
created_at: 2025-05-06T18:58:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,6 +31,7 @@ addTurboEventListeners();
 addTurboGlobalListeners();
 registerDialogStreamAction();
 registerFlashStreamAction();
+registerAriaStreamAction();
path: frontend/src/turbo/setup.ts
user: login: myabc
body: I'm not convinced by the naming - ARIA Live Stream? (or perhaps that sounds too much like Twitch 😄 )
created_at: 2025-05-06T19:00:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,19 @@
+import { StreamActions, StreamElement } from '@hotwired/turbo';
+import { announce } from '@primer/live-region-element';
+
+export function registerAriaStreamAction() {
+  StreamActions.aria = function ariaStreamAction(this:StreamElement) {
+      const message = this.getAttribute('message') ?? '';
+      const type = this.getAttribute('type') ?? 'polite';
path: frontend/src/turbo/aria-stream-action.ts
user: login: myabc
body: Perhaps we could also call this "politeness" rather than the much more generic "type"?
created_at: 2025-05-06T19:13:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+## What are ARIA Live Regions?
+ARIA Live Regions are a crucial accessibility feature that allows screen readers to announce dynamic content updates without requiring user focus changes.
+
+### ARIA Live Attributes
+ARIA Live Regions rely on specific attributes to control how updates are announced:
+- `aria-live="polite"`: Announces updates when the user is idle.
+- `aria-live="assertive"`: Announces updates immediately, interrupting the user.
+- `aria-atomic="true"`: Ensures the entire content of the region is read when it changes.
+
+## Implementing ARIA Live Regions in Primer
+Primer provides a built-in component named live-region to implement ARIA Live Regions easily.
+We have decided to use a single global ARIA live region in base.html to ensure live regions are present in the DOM before updates occur. This approach is better than having multiple live regions because it ensures that announcements are delivered in a clear and consistent manner. By centralizing updates, we avoid potential conflicts between overlapping live regions and provide a more seamless experience for screen reader users.
+
+### Live Region in `base.html`
+```html
+<live-region>
+  <div id="polite" aria-live="polite" aria-atomic="true" class="hidden-for-sighted"></div>
+  <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
+</live-region>
+```
+
+The `@primer/live-region-element` package provides methods to trigger announcements programmatically:
+
+```ts
+import { announce } from '@primer/live-region-element';
+
+announce('Example polite message', { politeness: 'polite' });
+```
+
+### Using Turbo Streams to Trigger ARIA Live Updates
+To use it in a Rails Turbo response, and send updates to the screen reader after any action, we created a new Turbo stream component that triggers an ARIA action:
+
+```ruby
+OpTurbo::StreamComponent.new(action: :aria, message: "Form submission successful!", type: "polite", role: "status", target: nil)
+```
+
+- This creates a **Turbo Stream update** with:
+  - `action: :aria` → Calls the `aria` action defined in `aria.stream.action.ts`
+  - `message: "Form submission successful!"` → The text announced by the screen reader
path: lookbook/docs/patterns/accessibility/18-aria-live.md.erb
user: login: myabc
body: ```suggestion
  - `message: "Form submission successful!"` → The text to be announced to user
```
created_at: 2025-05-06T19:21:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+## What are ARIA Live Regions?
+ARIA Live Regions are a crucial accessibility feature that allows screen readers to announce dynamic content updates without requiring user focus changes.
+
+### ARIA Live Attributes
+ARIA Live Regions rely on specific attributes to control how updates are announced:
+- `aria-live="polite"`: Announces updates when the user is idle.
+- `aria-live="assertive"`: Announces updates immediately, interrupting the user.
+- `aria-atomic="true"`: Ensures the entire content of the region is read when it changes.
+
+## Implementing ARIA Live Regions in Primer
+Primer provides a built-in component named live-region to implement ARIA Live Regions easily.
+We have decided to use a single global ARIA live region in base.html to ensure live regions are present in the DOM before updates occur. This approach is better than having multiple live regions because it ensures that announcements are delivered in a clear and consistent manner. By centralizing updates, we avoid potential conflicts between overlapping live regions and provide a more seamless experience for screen reader users.
+
+### Live Region in `base.html`
+```html
+<live-region>
+  <div id="polite" aria-live="polite" aria-atomic="true" class="hidden-for-sighted"></div>
+  <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
+</live-region>
+```
+
+The `@primer/live-region-element` package provides methods to trigger announcements programmatically:
+
+```ts
+import { announce } from '@primer/live-region-element';
+
+announce('Example polite message', { politeness: 'polite' });
+```
+
+### Using Turbo Streams to Trigger ARIA Live Updates
+To use it in a Rails Turbo response, and send updates to the screen reader after any action, we created a new Turbo stream component that triggers an ARIA action:
+
+```ruby
+OpTurbo::StreamComponent.new(action: :aria, message: "Form submission successful!", type: "polite", role: "status", target: nil)
+```
+
+- This creates a **Turbo Stream update** with:
+  - `action: :aria` → Calls the `aria` action defined in `aria.stream.action.ts`
+  - `message: "Form submission successful!"` → The text announced by the screen reader
+  - `type: "polite"` → Ensures the message is read when the user is idle
+  - `role: "status"` → Helps screen readers recognize this as a status update
path: lookbook/docs/patterns/accessibility/18-aria-live.md.erb
user: login: myabc
body: AFAICT we are not actually doing anything with `role`?
created_at: 2025-05-06T19:21:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+## What are ARIA Live Regions?
+ARIA Live Regions are a crucial accessibility feature that allows screen readers to announce dynamic content updates without requiring user focus changes.
+
+### ARIA Live Attributes
+ARIA Live Regions rely on specific attributes to control how updates are announced:
+- `aria-live="polite"`: Announces updates when the user is idle.
+- `aria-live="assertive"`: Announces updates immediately, interrupting the user.
+- `aria-atomic="true"`: Ensures the entire content of the region is read when it changes.
+
+## Implementing ARIA Live Regions in Primer
+Primer provides a built-in component named live-region to implement ARIA Live Regions easily.
+We have decided to use a single global ARIA live region in base.html to ensure live regions are present in the DOM before updates occur. This approach is better than having multiple live regions because it ensures that announcements are delivered in a clear and consistent manner. By centralizing updates, we avoid potential conflicts between overlapping live regions and provide a more seamless experience for screen reader users.
+
+### Live Region in `base.html`
+```html
+<live-region>
+  <div id="polite" aria-live="polite" aria-atomic="true" class="hidden-for-sighted"></div>
+  <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
+</live-region>
+```
+
+The `@primer/live-region-element` package provides methods to trigger announcements programmatically:
+
+```ts
+import { announce } from '@primer/live-region-element';
+
+announce('Example polite message', { politeness: 'polite' });
+```
+
+### Using Turbo Streams to Trigger ARIA Live Updates
+To use it in a Rails Turbo response, and send updates to the screen reader after any action, we created a new Turbo stream component that triggers an ARIA action:
+
+```ruby
+OpTurbo::StreamComponent.new(action: :aria, message: "Form submission successful!", type: "polite", role: "status", target: nil)
+```
+
+- This creates a **Turbo Stream update** with:
+  - `action: :aria` → Calls the `aria` action defined in `aria.stream.action.ts`
+  - `message: "Form submission successful!"` → The text announced by the screen reader
+  - `type: "polite"` → Ensures the message is read when the user is idle
+  - `role: "status"` → Helps screen readers recognize this as a status update
+  - `target: "nil"` → Ensures the update isn't inserted into the DOM but is handled as an announcement
+
+### How to Trigger an ARIA Update from the Controller
+You can trigger a screen reader announcement from your Rails controller by calling the `render_aria_update_message` helper, which is already provided:
+
+```ruby
+render_aria_update_message(
+  message: I18n.t("work_package_relations_tab.relations.create_child_aria_live_message"),
+  type: "polite"
+)
+```
+
+This helper internally renders a Turbo Stream using the aria action to announce the message via ARIA live regions.
+For reference only — you do not need to define this yourself.
+Here is the internal implementation of `render_aria_update_message`:
+
+```ruby
+    def render_aria_update_message(message:, type:, role: "alert")
+      turbo_streams << OpTurbo::StreamComponent.new(action: :aria, message:, type:, role:, target: nil).render_in(view_context)
+    end
+```
+
+### How to Trigger an ARIA Update from a Turbo Frame
+If you're using a Turbo Frame and want to announce dynamic content changes to screen readers, you can embed a Turbo Stream using the `aria` action directly inside the frame.
+
+```erb
+<%= content_tag("turbo-frame", id: "frame-id") do %>
+....
+  <%= render OpTurbo::StreamComponent.new(
+    action: :aria,
+    message: "Update!!",
+    type: "polite",
+    target: nil
+  ) %>
+<% end %>
+
+```
+
+### How ARIA Live Messages Are Delivered via Turbo Streams
+The `registerAriaStreamAction` method defines a custom Turbo Stream action called `aria`. When triggered, it announces a message (using ARIA live regions) to screen readers, with a tone of either `"polite"` or `"assertive"`, depending on the `type` attribute in the stream element.
+
+```ts
+import { StreamActions, StreamElement } from '@hotwired/turbo';
+import { announce } from '@primer/live-region-element';
+
+export function registerAriaStreamAction() {
+  StreamActions.aria = function ariaStreamAction(this:StreamElement) {
+      const message = this.getAttribute('message') ?? '';
+      const type = this.getAttribute('type') ?? 'polite';
+      if (type === 'assertive') {
+        void announce(message, {
+          politeness: 'assertive',
+        });
+      } else {
+        void announce(message, {
+          politeness: 'polite',
+          delayMs: 5000,
+        });
+      }
+  };
+}
+```
+
+## Live Regions in Dialogs and Modals
+There is an important exception to our use of the global ARIA live region in base.html.
path: lookbook/docs/patterns/accessibility/18-aria-live.md.erb
user: login: myabc
body: ```suggestion
There is an important exception to our use of the global ARIA Live Region in `base.html`.
```
created_at: 2025-05-06T19:24:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+## What are ARIA Live Regions?
+ARIA Live Regions are a crucial accessibility feature that allows screen readers to announce dynamic content updates without requiring user focus changes.
+
+### ARIA Live Attributes
+ARIA Live Regions rely on specific attributes to control how updates are announced:
+- `aria-live="polite"`: Announces updates when the user is idle.
+- `aria-live="assertive"`: Announces updates immediately, interrupting the user.
+- `aria-atomic="true"`: Ensures the entire content of the region is read when it changes.
+
+## Implementing ARIA Live Regions in Primer
+Primer provides a built-in component named live-region to implement ARIA Live Regions easily.
+We have decided to use a single global ARIA live region in base.html to ensure live regions are present in the DOM before updates occur. This approach is better than having multiple live regions because it ensures that announcements are delivered in a clear and consistent manner. By centralizing updates, we avoid potential conflicts between overlapping live regions and provide a more seamless experience for screen reader users.
+
+### Live Region in `base.html`
+```html
+<live-region>
+  <div id="polite" aria-live="polite" aria-atomic="true" class="hidden-for-sighted"></div>
+  <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
+</live-region>
+```
+
+The `@primer/live-region-element` package provides methods to trigger announcements programmatically:
+
+```ts
+import { announce } from '@primer/live-region-element';
+
+announce('Example polite message', { politeness: 'polite' });
+```
+
+### Using Turbo Streams to Trigger ARIA Live Updates
+To use it in a Rails Turbo response, and send updates to the screen reader after any action, we created a new Turbo stream component that triggers an ARIA action:
+
+```ruby
+OpTurbo::StreamComponent.new(action: :aria, message: "Form submission successful!", type: "polite", role: "status", target: nil)
+```
+
+- This creates a **Turbo Stream update** with:
+  - `action: :aria` → Calls the `aria` action defined in `aria.stream.action.ts`
+  - `message: "Form submission successful!"` → The text announced by the screen reader
+  - `type: "polite"` → Ensures the message is read when the user is idle
+  - `role: "status"` → Helps screen readers recognize this as a status update
+  - `target: "nil"` → Ensures the update isn't inserted into the DOM but is handled as an announcement
+
+### How to Trigger an ARIA Update from the Controller
+You can trigger a screen reader announcement from your Rails controller by calling the `render_aria_update_message` helper, which is already provided:
+
+```ruby
+render_aria_update_message(
+  message: I18n.t("work_package_relations_tab.relations.create_child_aria_live_message"),
+  type: "polite"
+)
+```
+
+This helper internally renders a Turbo Stream using the aria action to announce the message via ARIA live regions.
+For reference only — you do not need to define this yourself.
+Here is the internal implementation of `render_aria_update_message`:
+
+```ruby
+    def render_aria_update_message(message:, type:, role: "alert")
+      turbo_streams << OpTurbo::StreamComponent.new(action: :aria, message:, type:, role:, target: nil).render_in(view_context)
+    end
+```
+
+### How to Trigger an ARIA Update from a Turbo Frame
+If you're using a Turbo Frame and want to announce dynamic content changes to screen readers, you can embed a Turbo Stream using the `aria` action directly inside the frame.
+
+```erb
+<%= content_tag("turbo-frame", id: "frame-id") do %>
+....
+  <%= render OpTurbo::StreamComponent.new(
+    action: :aria,
+    message: "Update!!",
+    type: "polite",
+    target: nil
+  ) %>
+<% end %>
+
+```
+
+### How ARIA Live Messages Are Delivered via Turbo Streams
+The `registerAriaStreamAction` method defines a custom Turbo Stream action called `aria`. When triggered, it announces a message (using ARIA live regions) to screen readers, with a tone of either `"polite"` or `"assertive"`, depending on the `type` attribute in the stream element.
+
+```ts
+import { StreamActions, StreamElement } from '@hotwired/turbo';
+import { announce } from '@primer/live-region-element';
+
+export function registerAriaStreamAction() {
+  StreamActions.aria = function ariaStreamAction(this:StreamElement) {
+      const message = this.getAttribute('message') ?? '';
+      const type = this.getAttribute('type') ?? 'polite';
+      if (type === 'assertive') {
+        void announce(message, {
+          politeness: 'assertive',
+        });
+      } else {
+        void announce(message, {
+          politeness: 'polite',
+          delayMs: 5000,
+        });
+      }
+  };
+}
+```
+
+## Live Regions in Dialogs and Modals
+There is an important exception to our use of the global ARIA live region in base.html.
+
+When a modal or dialog is open, screen readers often restrict focus and reading context to the content inside the dialog. In this case, ARIA live regions outside the dialog (such as those defined in base.html) become inaccessible to the screen reader.
path: lookbook/docs/patterns/accessibility/18-aria-live.md.erb
user: login: myabc
body: ```suggestion
When a modal or dialog is open, screen readers often restrict focus and reading context to the content inside the dialog. In this case, live regions outside the dialog (such as those defined in `base.html`) become inaccessible to the screen reader.
```
created_at: 2025-05-06T19:25:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+## What are ARIA Live Regions?
+ARIA Live Regions are a crucial accessibility feature that allows screen readers to announce dynamic content updates without requiring user focus changes.
+
+### ARIA Live Attributes
+ARIA Live Regions rely on specific attributes to control how updates are announced:
+- `aria-live="polite"`: Announces updates when the user is idle.
+- `aria-live="assertive"`: Announces updates immediately, interrupting the user.
+- `aria-atomic="true"`: Ensures the entire content of the region is read when it changes.
+
+## Implementing ARIA Live Regions in Primer
+Primer provides a built-in component named live-region to implement ARIA Live Regions easily.
+We have decided to use a single global ARIA live region in base.html to ensure live regions are present in the DOM before updates occur. This approach is better than having multiple live regions because it ensures that announcements are delivered in a clear and consistent manner. By centralizing updates, we avoid potential conflicts between overlapping live regions and provide a more seamless experience for screen reader users.
+
+### Live Region in `base.html`
+```html
+<live-region>
+  <div id="polite" aria-live="polite" aria-atomic="true" class="hidden-for-sighted"></div>
+  <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
+</live-region>
+```
+
+The `@primer/live-region-element` package provides methods to trigger announcements programmatically:
+
+```ts
+import { announce } from '@primer/live-region-element';
+
+announce('Example polite message', { politeness: 'polite' });
+```
+
+### Using Turbo Streams to Trigger ARIA Live Updates
+To use it in a Rails Turbo response, and send updates to the screen reader after any action, we created a new Turbo stream component that triggers an ARIA action:
+
+```ruby
+OpTurbo::StreamComponent.new(action: :aria, message: "Form submission successful!", type: "polite", role: "status", target: nil)
+```
+
+- This creates a **Turbo Stream update** with:
+  - `action: :aria` → Calls the `aria` action defined in `aria.stream.action.ts`
+  - `message: "Form submission successful!"` → The text announced by the screen reader
+  - `type: "polite"` → Ensures the message is read when the user is idle
+  - `role: "status"` → Helps screen readers recognize this as a status update
+  - `target: "nil"` → Ensures the update isn't inserted into the DOM but is handled as an announcement
+
+### How to Trigger an ARIA Update from the Controller
+You can trigger a screen reader announcement from your Rails controller by calling the `render_aria_update_message` helper, which is already provided:
+
+```ruby
+render_aria_update_message(
+  message: I18n.t("work_package_relations_tab.relations.create_child_aria_live_message"),
+  type: "polite"
+)
+```
+
+This helper internally renders a Turbo Stream using the aria action to announce the message via ARIA live regions.
+For reference only — you do not need to define this yourself.
+Here is the internal implementation of `render_aria_update_message`:
+
+```ruby
+    def render_aria_update_message(message:, type:, role: "alert")
+      turbo_streams << OpTurbo::StreamComponent.new(action: :aria, message:, type:, role:, target: nil).render_in(view_context)
+    end
+```
+
+### How to Trigger an ARIA Update from a Turbo Frame
+If you're using a Turbo Frame and want to announce dynamic content changes to screen readers, you can embed a Turbo Stream using the `aria` action directly inside the frame.
+
+```erb
+<%= content_tag("turbo-frame", id: "frame-id") do %>
+....
+  <%= render OpTurbo::StreamComponent.new(
+    action: :aria,
+    message: "Update!!",
+    type: "polite",
+    target: nil
+  ) %>
+<% end %>
+
+```
+
+### How ARIA Live Messages Are Delivered via Turbo Streams
+The `registerAriaStreamAction` method defines a custom Turbo Stream action called `aria`. When triggered, it announces a message (using ARIA live regions) to screen readers, with a tone of either `"polite"` or `"assertive"`, depending on the `type` attribute in the stream element.
+
+```ts
+import { StreamActions, StreamElement } from '@hotwired/turbo';
+import { announce } from '@primer/live-region-element';
+
+export function registerAriaStreamAction() {
+  StreamActions.aria = function ariaStreamAction(this:StreamElement) {
+      const message = this.getAttribute('message') ?? '';
+      const type = this.getAttribute('type') ?? 'polite';
+      if (type === 'assertive') {
+        void announce(message, {
+          politeness: 'assertive',
+        });
+      } else {
+        void announce(message, {
+          politeness: 'polite',
+          delayMs: 5000,
+        });
+      }
+  };
+}
+```
+
+## Live Regions in Dialogs and Modals
+There is an important exception to our use of the global ARIA live region in base.html.
+
+When a modal or dialog is open, screen readers often restrict focus and reading context to the content inside the dialog. In this case, ARIA live regions outside the dialog (such as those defined in base.html) become inaccessible to the screen reader.
+
+To ensure that announcements are still read aloud in this situation, we should include a live-region component within the dialog itself, like what we have implemented for primerized date picker.
+
+```erb
+  <live-region>
+    <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
+  </live-region>
+  <%= render OpTurbo::StreamComponent.new(action: :aria, message: "Date picker is updated.", type: "assertive", target: "nil") %>
path: lookbook/docs/patterns/accessibility/18-aria-live.md.erb
user: login: myabc
body: ~~I don't understand why we suggest rendering the Stream Element inline like this?~~
created_at: 2025-05-06T19:26:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+## What are ARIA Live Regions?
+ARIA Live Regions are a crucial accessibility feature that allows screen readers to announce dynamic content updates without requiring user focus changes.
+
+### ARIA Live Attributes
+ARIA Live Regions rely on specific attributes to control how updates are announced:
+- `aria-live="polite"`: Announces updates when the user is idle.
+- `aria-live="assertive"`: Announces updates immediately, interrupting the user.
+- `aria-atomic="true"`: Ensures the entire content of the region is read when it changes.
+
+## Implementing ARIA Live Regions in Primer
+Primer provides a built-in component named live-region to implement ARIA Live Regions easily.
+We have decided to use a single global ARIA live region in base.html to ensure live regions are present in the DOM before updates occur. This approach is better than having multiple live regions because it ensures that announcements are delivered in a clear and consistent manner. By centralizing updates, we avoid potential conflicts between overlapping live regions and provide a more seamless experience for screen reader users.
+
+### Live Region in `base.html`
+```html
+<live-region>
+  <div id="polite" aria-live="polite" aria-atomic="true" class="hidden-for-sighted"></div>
+  <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
+</live-region>
+```
+
+The `@primer/live-region-element` package provides methods to trigger announcements programmatically:
+
+```ts
+import { announce } from '@primer/live-region-element';
+
+announce('Example polite message', { politeness: 'polite' });
+```
+
+### Using Turbo Streams to Trigger ARIA Live Updates
+To use it in a Rails Turbo response, and send updates to the screen reader after any action, we created a new Turbo stream component that triggers an ARIA action:
+
+```ruby
+OpTurbo::StreamComponent.new(action: :aria, message: "Form submission successful!", type: "polite", role: "status", target: nil)
+```
+
+- This creates a **Turbo Stream update** with:
+  - `action: :aria` → Calls the `aria` action defined in `aria.stream.action.ts`
+  - `message: "Form submission successful!"` → The text announced by the screen reader
+  - `type: "polite"` → Ensures the message is read when the user is idle
+  - `role: "status"` → Helps screen readers recognize this as a status update
+  - `target: "nil"` → Ensures the update isn't inserted into the DOM but is handled as an announcement
path: lookbook/docs/patterns/accessibility/18-aria-live.md.erb
user: login: myabc
body: ```suggestion
  - `target: nil` → Ensures the update isn't inserted into the DOM but is handled as an announcement
```
created_at: 2025-05-06T19:27:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -83,6 +83,10 @@ def render_error_flash_message_via_turbo_stream(**)
       render_flash_message_via_turbo_stream(**, scheme: :danger, icon: :stop)
     end
 
+    def render_aria_update_message(message:, type:, role: "alert")
path: app/controllers/concerns/op_turbo/component_stream.rb
user: login: myabc
body: It would be good to have a sensible default for `type`[^1] here, i.e.

```suggestion
    def render_aria_update_message(message:, type: :polite, role: :alert)
```

Also symbols are slightly more idiomatic Ruby[^2].

[^1]: see other comment about naming - `type` vs `politeness`.
[^2]: see other comment about `role` not being used the custom Turbo Stream Action.
created_at: 2025-05-06T19:33:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+## What are ARIA Live Regions?
+ARIA Live Regions are a crucial accessibility feature that allows screen readers to announce dynamic content updates without requiring user focus changes.
+
+### ARIA Live Attributes
+ARIA Live Regions rely on specific attributes to control how updates are announced:
+- `aria-live="polite"`: Announces updates when the user is idle.
+- `aria-live="assertive"`: Announces updates immediately, interrupting the user.
+- `aria-atomic="true"`: Ensures the entire content of the region is read when it changes.
+
+## Implementing ARIA Live Regions in Primer
+Primer provides a built-in component named live-region to implement ARIA Live Regions easily.
+We have decided to use a single global ARIA live region in base.html to ensure live regions are present in the DOM before updates occur. This approach is better than having multiple live regions because it ensures that announcements are delivered in a clear and consistent manner. By centralizing updates, we avoid potential conflicts between overlapping live regions and provide a more seamless experience for screen reader users.
+
+### Live Region in `base.html`
+```html
+<live-region>
+  <div id="polite" aria-live="polite" aria-atomic="true" class="hidden-for-sighted"></div>
+  <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
+</live-region>
+```
+
+The `@primer/live-region-element` package provides methods to trigger announcements programmatically:
+
+```ts
+import { announce } from '@primer/live-region-element';
+
+announce('Example polite message', { politeness: 'polite' });
+```
+
+### Using Turbo Streams to Trigger ARIA Live Updates
+To use it in a Rails Turbo response, and send updates to the screen reader after any action, we created a new Turbo stream component that triggers an ARIA action:
+
+```ruby
+OpTurbo::StreamComponent.new(action: :aria, message: "Form submission successful!", type: "polite", role: "status", target: nil)
+```
+
+- This creates a **Turbo Stream update** with:
+  - `action: :aria` → Calls the `aria` action defined in `aria.stream.action.ts`
+  - `message: "Form submission successful!"` → The text announced by the screen reader
+  - `type: "polite"` → Ensures the message is read when the user is idle
+  - `role: "status"` → Helps screen readers recognize this as a status update
+  - `target: "nil"` → Ensures the update isn't inserted into the DOM but is handled as an announcement
+
+### How to Trigger an ARIA Update from the Controller
+You can trigger a screen reader announcement from your Rails controller by calling the `render_aria_update_message` helper, which is already provided:
path: lookbook/docs/patterns/accessibility/18-aria-live.md.erb
user: login: myabc
body: ```suggestion
You can trigger a screen reader announcement from your Rails controller by calling the provided `render_aria_update_message` helper:
```
created_at: 2025-05-06T19:38:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -83,6 +83,10 @@ def render_error_flash_message_via_turbo_stream(**)
       render_flash_message_via_turbo_stream(**, scheme: :danger, icon: :stop)
     end
 
+    def render_aria_update_message(message:, type:, role: "alert")
+      turbo_streams << OpTurbo::StreamComponent.new(action: :aria, message:, type:, role:, target: nil).render_in(view_context)
path: app/controllers/concerns/op_turbo/component_stream.rb
user: login: myabc
body: nitpick - formatting

```suggestion
      turbo_streams << OpTurbo::StreamComponent
        .new(action: :aria, message:, type:, role:, target: nil)
        .render_in(view_context)
```

created_at: 2025-05-06T19:51:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+## What are ARIA Live Regions?
+ARIA Live Regions are a crucial accessibility feature that allows screen readers to announce dynamic content updates without requiring user focus changes.
+
+### ARIA Live Attributes
+ARIA Live Regions rely on specific attributes to control how updates are announced:
+- `aria-live="polite"`: Announces updates when the user is idle.
+- `aria-live="assertive"`: Announces updates immediately, interrupting the user.
+- `aria-atomic="true"`: Ensures the entire content of the region is read when it changes.
+
+## Implementing ARIA Live Regions in Primer
+Primer provides a built-in component named live-region to implement ARIA Live Regions easily.
+We have decided to use a single global ARIA live region in base.html to ensure live regions are present in the DOM before updates occur. This approach is better than having multiple live regions because it ensures that announcements are delivered in a clear and consistent manner. By centralizing updates, we avoid potential conflicts between overlapping live regions and provide a more seamless experience for screen reader users.
+
+### Live Region in `base.html`
+```html
+<live-region>
+  <div id="polite" aria-live="polite" aria-atomic="true" class="hidden-for-sighted"></div>
+  <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
+</live-region>
+```
+
+The `@primer/live-region-element` package provides methods to trigger announcements programmatically:
+
+```ts
+import { announce } from '@primer/live-region-element';
+
+announce('Example polite message', { politeness: 'polite' });
+```
+
+### Using Turbo Streams to Trigger ARIA Live Updates
+To use it in a Rails Turbo response, and send updates to the screen reader after any action, we created a new Turbo stream component that triggers an ARIA action:
+
+```ruby
+OpTurbo::StreamComponent.new(action: :aria, message: "Form submission successful!", type: "polite", role: "status", target: nil)
+```
+
+- This creates a **Turbo Stream update** with:
+  - `action: :aria` → Calls the `aria` action defined in `aria.stream.action.ts`
+  - `message: "Form submission successful!"` → The text announced by the screen reader
+  - `type: "polite"` → Ensures the message is read when the user is idle
+  - `role: "status"` → Helps screen readers recognize this as a status update
+  - `target: "nil"` → Ensures the update isn't inserted into the DOM but is handled as an announcement
+
+### How to Trigger an ARIA Update from the Controller
+You can trigger a screen reader announcement from your Rails controller by calling the `render_aria_update_message` helper, which is already provided:
+
+```ruby
+render_aria_update_message(
+  message: I18n.t("work_package_relations_tab.relations.create_child_aria_live_message"),
+  type: "polite"
+)
+```
+
+This helper internally renders a Turbo Stream using the aria action to announce the message via ARIA live regions.
+For reference only — you do not need to define this yourself.
+Here is the internal implementation of `render_aria_update_message`:
+
+```ruby
+    def render_aria_update_message(message:, type:, role: "alert")
+      turbo_streams << OpTurbo::StreamComponent.new(action: :aria, message:, type:, role:, target: nil).render_in(view_context)
+    end
+```
path: lookbook/docs/patterns/accessibility/18-aria-live.md.erb
user: login: myabc
body: I'm not sure you need to go into this level of detail about the implementation.

What is important is that calling controllers also call `respond_to_with_turbo_streams`/`respond_with_turbo_streams` (or the equivalent `respond_to` + block).
created_at: 2025-05-06T19:55:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,5 +1,13 @@
 <%=
   content_tag("turbo-frame", id: "wp-datepicker-dialog--content") do
+%>
+  <live-region>
+    <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
path: app/components/work_packages/date_picker/dialog_content_component.html.erb
user: login: myabc
body: 1. we should use a different `id` here - we are already declaring a `div` with this `id` in `base.html.erb`.
2. Can we start to use Primer's `sr-only` rather than `hidden-for-sighted`? / @HDinger  (se. #18837)
created_at: 2025-05-06T19:58:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+## What are ARIA Live Regions?
+ARIA Live Regions are a crucial accessibility feature that allows screen readers to announce dynamic content updates without requiring user focus changes.
+
+### ARIA Live Attributes
+ARIA Live Regions rely on specific attributes to control how updates are announced:
+- `aria-live="polite"`: Announces updates when the user is idle.
+- `aria-live="assertive"`: Announces updates immediately, interrupting the user.
+- `aria-atomic="true"`: Ensures the entire content of the region is read when it changes.
+
+## Implementing ARIA Live Regions in Primer
+Primer provides a built-in component named live-region to implement ARIA Live Regions easily.
+We have decided to use a single global ARIA live region in base.html to ensure live regions are present in the DOM before updates occur. This approach is better than having multiple live regions because it ensures that announcements are delivered in a clear and consistent manner. By centralizing updates, we avoid potential conflicts between overlapping live regions and provide a more seamless experience for screen reader users.
+
+### Live Region in `base.html`
+```html
+<live-region>
+  <div id="polite" aria-live="polite" aria-atomic="true" class="hidden-for-sighted"></div>
+  <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
+</live-region>
+```
+
+The `@primer/live-region-element` package provides methods to trigger announcements programmatically:
+
+```ts
+import { announce } from '@primer/live-region-element';
+
+announce('Example polite message', { politeness: 'polite' });
+```
+
+### Using Turbo Streams to Trigger ARIA Live Updates
+To use it in a Rails Turbo response, and send updates to the screen reader after any action, we created a new Turbo stream component that triggers an ARIA action:
+
+```ruby
+OpTurbo::StreamComponent.new(action: :aria, message: "Form submission successful!", type: "polite", role: "status", target: nil)
+```
+
+- This creates a **Turbo Stream update** with:
+  - `action: :aria` → Calls the `aria` action defined in `aria.stream.action.ts`
+  - `message: "Form submission successful!"` → The text announced by the screen reader
+  - `type: "polite"` → Ensures the message is read when the user is idle
+  - `role: "status"` → Helps screen readers recognize this as a status update
+  - `target: "nil"` → Ensures the update isn't inserted into the DOM but is handled as an announcement
+
+### How to Trigger an ARIA Update from the Controller
+You can trigger a screen reader announcement from your Rails controller by calling the `render_aria_update_message` helper, which is already provided:
+
+```ruby
+render_aria_update_message(
+  message: I18n.t("work_package_relations_tab.relations.create_child_aria_live_message"),
+  type: "polite"
+)
+```
+
+This helper internally renders a Turbo Stream using the aria action to announce the message via ARIA live regions.
+For reference only — you do not need to define this yourself.
+Here is the internal implementation of `render_aria_update_message`:
+
+```ruby
+    def render_aria_update_message(message:, type:, role: "alert")
+      turbo_streams << OpTurbo::StreamComponent.new(action: :aria, message:, type:, role:, target: nil).render_in(view_context)
+    end
+```
+
+### How to Trigger an ARIA Update from a Turbo Frame
+If you're using a Turbo Frame and want to announce dynamic content changes to screen readers, you can embed a Turbo Stream using the `aria` action directly inside the frame.
+
+```erb
+<%= content_tag("turbo-frame", id: "frame-id") do %>
+....
+  <%= render OpTurbo::StreamComponent.new(
+    action: :aria,
+    message: "Update!!",
+    type: "polite",
+    target: nil
+  ) %>
+<% end %>
+
+```
+
+### How ARIA Live Messages Are Delivered via Turbo Streams
+The `registerAriaStreamAction` method defines a custom Turbo Stream action called `aria`. When triggered, it announces a message (using ARIA live regions) to screen readers, with a tone of either `"polite"` or `"assertive"`, depending on the `type` attribute in the stream element.
+
+```ts
+import { StreamActions, StreamElement } from '@hotwired/turbo';
+import { announce } from '@primer/live-region-element';
+
+export function registerAriaStreamAction() {
+  StreamActions.aria = function ariaStreamAction(this:StreamElement) {
+      const message = this.getAttribute('message') ?? '';
+      const type = this.getAttribute('type') ?? 'polite';
+      if (type === 'assertive') {
+        void announce(message, {
+          politeness: 'assertive',
+        });
+      } else {
+        void announce(message, {
+          politeness: 'polite',
+          delayMs: 5000,
+        });
+      }
+  };
+}
+```
+
+## Live Regions in Dialogs and Modals
+There is an important exception to our use of the global ARIA live region in base.html.
+
+When a modal or dialog is open, screen readers often restrict focus and reading context to the content inside the dialog. In this case, ARIA live regions outside the dialog (such as those defined in base.html) become inaccessible to the screen reader.
+
+To ensure that announcements are still read aloud in this situation, we should include a live-region component within the dialog itself, like what we have implemented for primerized date picker.
+
+```erb
+  <live-region>
+    <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
+  </live-region>
+  <%= render OpTurbo::StreamComponent.new(action: :aria, message: "Date picker is updated.", type: "assertive", target: "nil") %>
path: lookbook/docs/patterns/accessibility/18-aria-live.md.erb
user: login: myabc
body: Ok, I think I understand now - it hadn't occurred to me that you can render `<turbo-stream>` as part of regular HTML response and that those actions would still be processed (this may come in handy in #18716, as we might want to use Turbo Frames with flash messages).
created_at: 2025-05-06T22:18:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -744,6 +744,7 @@ en:
         automatic: "Automatic"
         manual: "Manual"
       show_relations: "Show relations"
+      update_inputs_aria_live_message: "Date picker updated. Scheduling mode: %{scheduling}, %{working_days_only}, Start date: %{start_date}, Finish date: %{due_date}, Duration: %{duration} days."
path: config/locales/en.yml
user: login: myabc
body: This doesn't work well when fields (e.g start date, duration) are empty. We might want to construct this message string in Ruby.

<img width="641" alt="Screenshot 2025-05-06 at 22 59 58" src="https://github.com/user-attachments/assets/bc3a5c89-c50d-4317-bf89-d9a63809152a" />


created_at: 2025-05-06T22:20:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,5 +1,13 @@
 <%=
   content_tag("turbo-frame", id: "wp-datepicker-dialog--content") do
+%>
+  <live-region>
+    <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
+  </live-region>
+  <% if @live_region_message %>
+    <%= render OpTurbo::StreamComponent.new(action: :aria, message: @live_region_message, type: "assertive", target: "nil") %>
+  <% end %>
path: app/components/work_packages/date_picker/dialog_content_component.html.erb
user: login: myabc
body: Is there a good reason (apart from the programmatic delay) that we can't forgo the `live-region` here and just update the live region directly (with Turbo Frames + morphing)?

```suggestion
<div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted">
  <%= @live_region_message %>
</div>
```

It would be one fewer layer of indirection.

(I realise we might have discussed this before, so apologies if I'm forgetting something)
created_at: 2025-05-06T22:27:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+## What are ARIA Live Regions?
+ARIA Live Regions are a crucial accessibility feature that allows screen readers to announce dynamic content updates without requiring user focus changes.
+
+### ARIA Live Attributes
+ARIA Live Regions rely on specific attributes to control how updates are announced:
+- `aria-live="polite"`: Announces updates when the user is idle.
+- `aria-live="assertive"`: Announces updates immediately, interrupting the user.
+- `aria-atomic="true"`: Ensures the entire content of the region is read when it changes.
+
+## Implementing ARIA Live Regions in Primer
+Primer provides a built-in component named live-region to implement ARIA Live Regions easily.
+We have decided to use a single global ARIA live region in base.html to ensure live regions are present in the DOM before updates occur. This approach is better than having multiple live regions because it ensures that announcements are delivered in a clear and consistent manner. By centralizing updates, we avoid potential conflicts between overlapping live regions and provide a more seamless experience for screen reader users.
+
+### Live Region in `base.html`
+```html
+<live-region>
+  <div id="polite" aria-live="polite" aria-atomic="true" class="hidden-for-sighted"></div>
+  <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
+</live-region>
+```
+
+The `@primer/live-region-element` package provides methods to trigger announcements programmatically:
+
+```ts
+import { announce } from '@primer/live-region-element';
+
+announce('Example polite message', { politeness: 'polite' });
+```
+
+### Using Turbo Streams to Trigger ARIA Live Updates
+To use it in a Rails Turbo response, and send updates to the screen reader after any action, we created a new Turbo stream component that triggers an ARIA action:
+
+```ruby
+OpTurbo::StreamComponent.new(action: :aria, message: "Form submission successful!", type: "polite", role: "status", target: nil)
+```
+
+- This creates a **Turbo Stream update** with:
+  - `action: :aria` → Calls the `aria` action defined in `aria.stream.action.ts`
+  - `message: "Form submission successful!"` → The text announced by the screen reader
+  - `type: "polite"` → Ensures the message is read when the user is idle
+  - `role: "status"` → Helps screen readers recognize this as a status update
+  - `target: "nil"` → Ensures the update isn't inserted into the DOM but is handled as an announcement
+
+### How to Trigger an ARIA Update from the Controller
+You can trigger a screen reader announcement from your Rails controller by calling the `render_aria_update_message` helper, which is already provided:
+
+```ruby
+render_aria_update_message(
+  message: I18n.t("work_package_relations_tab.relations.create_child_aria_live_message"),
+  type: "polite"
+)
+```
+
+This helper internally renders a Turbo Stream using the aria action to announce the message via ARIA live regions.
+For reference only — you do not need to define this yourself.
+Here is the internal implementation of `render_aria_update_message`:
+
+```ruby
+    def render_aria_update_message(message:, type:, role: "alert")
+      turbo_streams << OpTurbo::StreamComponent.new(action: :aria, message:, type:, role:, target: nil).render_in(view_context)
+    end
+```
+
+### How to Trigger an ARIA Update from a Turbo Frame
+If you're using a Turbo Frame and want to announce dynamic content changes to screen readers, you can embed a Turbo Stream using the `aria` action directly inside the frame.
+
+```erb
+<%= content_tag("turbo-frame", id: "frame-id") do %>
+....
+  <%= render OpTurbo::StreamComponent.new(
+    action: :aria,
+    message: "Update!!",
+    type: "polite",
+    target: nil
+  ) %>
+<% end %>
+
+```
+
+### How ARIA Live Messages Are Delivered via Turbo Streams
+The `registerAriaStreamAction` method defines a custom Turbo Stream action called `aria`. When triggered, it announces a message (using ARIA live regions) to screen readers, with a tone of either `"polite"` or `"assertive"`, depending on the `type` attribute in the stream element.
+
+```ts
+import { StreamActions, StreamElement } from '@hotwired/turbo';
+import { announce } from '@primer/live-region-element';
+
+export function registerAriaStreamAction() {
+  StreamActions.aria = function ariaStreamAction(this:StreamElement) {
+      const message = this.getAttribute('message') ?? '';
+      const type = this.getAttribute('type') ?? 'polite';
+      if (type === 'assertive') {
+        void announce(message, {
+          politeness: 'assertive',
+        });
+      } else {
+        void announce(message, {
+          politeness: 'polite',
+          delayMs: 5000,
+        });
+      }
+  };
+}
path: lookbook/docs/patterns/accessibility/18-aria-live.md.erb
user: login: myabc
body: I think you should be able to do the following:

```suggestion
<%= render file: "frontend/src/turbo/aria-stream-action.ts" %>
```

created_at: 2025-05-06T23:48:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,5 +1,13 @@
 <%=
   content_tag("turbo-frame", id: "wp-datepicker-dialog--content") do
+%>
+  <live-region>
+    <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
path: app/components/work_packages/date_picker/dialog_content_component.html.erb
user: login: myabc
body: I think `<live-region></live-region>` should suffice. `LiveRegionElement` [defines its own template](https://github.com/primer/live-region-element/blob/main/packages/live-region-element/src/live-region-element.ts#L259-L276).
created_at: 2025-05-06T23:58:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+## What are ARIA Live Regions?
+ARIA Live Regions are a crucial accessibility feature that allows screen readers to announce dynamic content updates without requiring user focus changes.
+
+### ARIA Live Attributes
+ARIA Live Regions rely on specific attributes to control how updates are announced:
+- `aria-live="polite"`: Announces updates when the user is idle.
+- `aria-live="assertive"`: Announces updates immediately, interrupting the user.
+- `aria-atomic="true"`: Ensures the entire content of the region is read when it changes.
+
+## Implementing ARIA Live Regions in Primer
+Primer provides a built-in component named live-region to implement ARIA Live Regions easily.
+We have decided to use a single global ARIA live region in base.html to ensure live regions are present in the DOM before updates occur. This approach is better than having multiple live regions because it ensures that announcements are delivered in a clear and consistent manner. By centralizing updates, we avoid potential conflicts between overlapping live regions and provide a more seamless experience for screen reader users.
+
+### Live Region in `base.html`
+```html
+<live-region>
+  <div id="polite" aria-live="polite" aria-atomic="true" class="hidden-for-sighted"></div>
+  <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
+</live-region>
+```
+
+The `@primer/live-region-element` package provides methods to trigger announcements programmatically:
+
+```ts
+import { announce } from '@primer/live-region-element';
+
+announce('Example polite message', { politeness: 'polite' });
+```
+
+### Using Turbo Streams to Trigger ARIA Live Updates
+To use it in a Rails Turbo response, and send updates to the screen reader after any action, we created a new Turbo stream component that triggers an ARIA action:
+
+```ruby
+OpTurbo::StreamComponent.new(action: :aria, message: "Form submission successful!", type: "polite", role: "status", target: nil)
+```
+
+- This creates a **Turbo Stream update** with:
+  - `action: :aria` → Calls the `aria` action defined in `aria.stream.action.ts`
+  - `message: "Form submission successful!"` → The text announced by the screen reader
+  - `type: "polite"` → Ensures the message is read when the user is idle
+  - `role: "status"` → Helps screen readers recognize this as a status update
path: lookbook/docs/patterns/accessibility/18-aria-live.md.erb
user: login: bsatarnejad
body: Yes, we don't use it, its default value is alert, it can be removed.
created_at: 2025-05-12T15:14:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,5 +1,13 @@
 <%=
   content_tag("turbo-frame", id: "wp-datepicker-dialog--content") do
+%>
+  <live-region>
+    <div id="assertive" aria-live="assertive" aria-atomic="true" class="hidden-for-sighted"></div>
+  </live-region>
+  <% if @live_region_message %>
+    <%= render OpTurbo::StreamComponent.new(action: :aria, message: @live_region_message, type: "assertive", target: "nil") %>
+  <% end %>
path: app/components/work_packages/date_picker/dialog_content_component.html.erb
user: login: bsatarnejad
body: As we discussed, we need it for adding some delay.
created_at: 2025-05-16T12:02:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,21 @@
+import { StreamActions, StreamElement } from '@hotwired/turbo';
+import { announce } from '@primer/live-region-element';
+
+export function registerLiveRegionStreamAction() {
+  StreamActions.liveRegion = function liveRegionStreamAction(this:StreamElement) {
+    const message = this.getAttribute('message') || '';
+    const politeness = this.getAttribute('politeness') || 'polite';
+    const delayAttr = this.getAttribute('delay');
+    const delay = delayAttr !== null && !Number.isNaN(Number(delayAttr)) ? Number(delayAttr) : 0;
path: frontend/src/turbo/live-region-stream-action.ts
user: login: myabc
body: You can simplify this as follows:

```suggestion
    const delay = parseInt(this.getAttribute('delay'), 10) || 0;
```

(`parseInt(null) => NaN`, which is `falsey`)

*updated to specify a radix)
created_at: 2025-05-20T10:34:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,21 @@
+import { StreamActions, StreamElement } from '@hotwired/turbo';
+import { announce } from '@primer/live-region-element';
+
+export function registerLiveRegionStreamAction() {
+  StreamActions.liveRegion = function liveRegionStreamAction(this:StreamElement) {
+    const message = this.getAttribute('message') || '';
path: frontend/src/turbo/live-region-stream-action.ts
user: login: myabc
body: Is there any reason we would want to pass an empty message to `announce`?

We could just return early if `message` is falsey.
created_at: 2025-05-20T10:43:40Z
author_association: CONTRIBUTOR

PR comments:
user: login: myabc
created_at: 2025-05-07T00:00:56Z
body: it would be great if we could look into how this fits in with the [Declarative Shadow DOM](https://github.com/primer/live-region-element?tab=readme-ov-file#declarative-shadow-dom) (my Shadow DOM knowledges is shaky at best - @HDinger and @camertron might be able to shed some light!)
author_association: CONTRIBUTOR
user: login: camertron
created_at: 2025-05-08T18:36:29Z
body: Hey @myabc, thanks for tagging me 😄

> it would be great if we could look into how this fits in with the [Declarative Shadow DOM](https://github.com/primer/live-region-element?tab=readme-ov-file#declarative-shadow-dom) (my Shadow DOM knowledges is shaky at best)

I believe the biggest reason to use a shadow DOM with the `<live-region>` element is to prevent global CSS styles from accidentally making the element's contents visible to sighted users (live regions should typically only be visible to screen readers, which `<live-region>` element achieves with CSS). Other than that, the shadow DOM shouldn't affect any of the actual announcement behavior offered by the element.

I did also want to pop in here and mention that, generally speaking, there should only be one ARIA live region on the page at a time. Having two or more areas [can be flagged as errors or warnings](https://github.com/w3c/aria/issues/1689#issuecomment-1026892454) by automated accessibility checkers, and is [discouraged](https://github.com/w3c/aria/issues/1689#issuecomment-1026892454) by live-region-element itself:

> Having multiple live regions on a page is discouraged so we recommend having a single global live region that is available across every page of your application by embedding this live-region element as part of your page layout.

I think this is mostly due to various screen reader quirks and bugs rather than an inherent limitation of live regions, but it's worth considering to ensure an optimal user experience. In this PR it appears a new `<live-region>` will be constructed for each date picker instance. If I recall correctly, live-region-element's `announce` function (and its friends) will automatically find any `<live-region>` element that exist in the DOM, so adding one to the application's layout should work well.

I would suggest merging this PR first and consolidating live regions in a separate PR 😄 
author_association: COLLABORATOR
user: login: bsatarnejad
created_at: 2025-05-12T14:24:35Z
body: > Hi @bsatarnejad. I'm really sorry that it's taken so long for me to give feedback on this PR.
> 
> I have only tested on macOS with VoiceOver so far - but the good news is that, for the most part, everything works as expected, and that this is a nice enhancement for users of assistive technology.
> 
> There are various things I think we should address though:
> 
> * **assertiveness**: while the "assertive" behaviour with the date picker seems to work well, the "polite" updates on adding a relation don't work so well, with VoiceOver announcing the change long after the flash message disappears.
> 
> <img alt="image" width="1512" src="https://private-user-images.githubusercontent.com/755/441003381-3c2d9590-fad2-42d7-a054-f2b893c40878.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDcwNTk1MTQsIm5iZiI6MTc0NzA1OTIxNCwicGF0aCI6Ii83NTUvNDQxMDAzMzgxLTNjMmQ5NTkwLWZhZDItNDJkNy1hMDU0LWYyYjg5M2M0MDg3OC5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUwNTEyJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MDUxMlQxNDEzMzRaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1hZGE2ZDYzNzAzNDRhMjY0NmViYTZlZDljZGI3MmI1MWQ0MmMwMDFhNWViMzYyNDJmNjUzN2Y1YzM0NWE1ZGQ5JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.X3H8vfnOf8jlHq__P3TaMrwymyhAsZ9HaT6RSPHJtLs">
>  Screen.Recording.2025-05-07.at.01.18.44.mov 
> (this might beg the question as to whether we should look into integrating flash messages + live regions)
> 
> * **commit hygiene**: this PR now consists of 54 commits, despite not being huge in terms of changes (`16 files changed, 240 insertions(+), 11 deletions(-)`). I realise that this is because it is a long-running piece of work that has suffered from requirement churn and changes in prioritisation along the way. That said, although OP policy AFAICT is to try to maintain commit history, I think this PR would benefit from some cleanup (namely rebasing + squashing of related commits). I'm happy to help with this.
> * **documentation**: this is generally fine, but I think we should prefer the more generic term "asssistive technology" (and "users of A.T.") to "screen readers" where possible.
>   
>   * we should try to be consistent with capitalisation: "ARIA Live Regions" vs. "live regions".
>   * With Turbo Streams, I think we should encourage the use of the `render_aria_update_message` helper over the component directly.
> * **naming**: We should call the custom Turbo Stream Action something more specific than "ARIA". Perhaps `LiveRegionStreamAction`?
> * **design**: Should we introduce a specialization of the `OpTurbo::StreamComponent` that enforces correct arguments - rather than passing necessary args for the `aria` action as `**turbo_stream_args`?
> * **tests**: there are currently no tests for these changes. We should discuss how best to test (both with Capy features and specs for components/helpers).
> 
> If anything is unclear, I'd be happy to explain! - in any case, it's probably worth us setting aside some time to discuss in more detail. Cheers!

Hi @myabc , Thanks for the feedback. The issue you mentioned for relation update is right, but it is an example of the aria-live usage. We can remove it, because I think we should give this kind of update in flash messages.
author_association: CONTRIBUTOR
user: login: bsatarnejad
created_at: 2025-05-21T11:08:04Z
body: I'm merging the PR, since the failing test doesn't happen locally.
author_association: CONTRIBUTOR