Content
View differences
Updated by Behrokh Satarnejad about 1 year ago
In order to make the screen readers to announce dynamic content updates without requiring user focus changes, we can use ARIA Live Regions.
We trust on [primer live-region-element](https://github.com/primer/live-region-element) which is A custom element for making announcements with live regions.
As Primer team mentioned, It is **essential** that the `live-region` element exists in the initial HTML payload of your application. 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. Therefore, we added it to the base.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>`
So now when we want to inform a screen reader user about a change, we add a Turbo Stream with `action="aria"` to the response:
A new Turbo Stream Component is defined like this:
`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 can we use it?
When it is called from a controller, we can use the following method:
`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`
like this:
`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")`
`respond_with_turbo_streams`
When you are updating via Turbo Frame, you can add it to the frame:
`<%=`
`content_tag("turbo-frame", id: "frame-id") do %>`
`<%=`
..........
`end%>`
`<= render OpTurbo::StreamComponent.new(action: :aria, message: "Update!!", type:"polite", target: "nil") %>`
`<% end %>`
Then in the front-end part, we define and **register a custom Turbo Stream action** called `aria`. It's designed to send **ARIA live region announcements:**
A new **Turbo Stream action** called `aria` is defined and will be triggered when a Turbo Stream with `action="aria"` is received:
`export function registerAriaStreamAction() {`
`StreamActions.aria = function dialogStreamAction(this: StreamElement) {`
It reads the `message` and `type` attributes from the `<turbo-stream>` element, then it announces the message using ARIA live regions, either:
* **polite**: wait for other announcements to finish.
* **assertive**: interrupt and announce immediately.
`if (type === 'assertive') {`
`announce(message, {`
`politeness: 'assertive',`
`});`
`} else {`
`announce(message, {`
`politeness: 'polite',`
`});`
`}`
If you have a Turbo Stream like this:
`<turbo-stream action="aria" message="Form submitted!" type="polite"></turbo-stream>`
When it's received, what will happen?
* Extract the `message` and `type`
* Trigger a screen reader-friendly ARIA live region announcement
* Without changing the DOM or UI
##### **We have a Challenge to use this method for new Primerized date picker:**
~~When When you enter a value to an input, an input field is focused, and focused input is more prioritized than aria-live, so it won't be called, in this case, we need to add aria-live on each input, so whenever its value changed, it will be announced.~~ announced.
`~~start_date.with_row(classes: `start_date.with_row(classes: "wp-datepicker-dialog-date-form--text-field-container") do~~` do`
`~~render(Primer::Alpha::TextField.new(**text_field_options(name: `render(Primer::Alpha::TextField.new(**text_field_options(name: :start_date, label: start_date_label), aria: { live: :polite }))~~` }))`
`~~end~~` `end`
I investigated more, as the datepickr dialog is opened, whenever I change the value (I'm still searching for a better way of an input, the value of global aria-live is changed. But it is not read out by screen reader until I close the dialog, all the messages will be read out by screen reader.
This method doesn't work, when we have an `aria-live` region **outside a modal/dialog**, especially if that dialog **traps focus**. When we open a dialog, **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. implementation here)
We trust on [primer live-region-element](https://github.com/primer/live-region-element) which is A custom element for making announcements with live regions.
As Primer team mentioned, It is **essential** that the `live-region` element exists in the initial HTML payload of your application. 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. Therefore, we added it to the base.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>`
So now when we want to inform a screen reader user about a change, we add a Turbo Stream with `action="aria"` to the response:
A new Turbo Stream Component is defined like this:
`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 can we use it?
When it is called from a controller, we can use the following method:
`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`
like this:
`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")`
`respond_with_turbo_streams`
When you are updating via Turbo Frame, you can add it to the frame:
`<%=`
`content_tag("turbo-frame", id: "frame-id") do %>`
`<%=`
..........
`end%>`
`<= render OpTurbo::StreamComponent.new(action: :aria, message: "Update!!", type:"polite", target: "nil") %>`
`<% end %>`
Then in the front-end part, we define and **register a custom Turbo Stream action** called `aria`. It's designed to send **ARIA live region announcements:**
A new **Turbo Stream action** called `aria` is defined and will be triggered when a Turbo Stream with `action="aria"` is received:
`export function registerAriaStreamAction() {`
`StreamActions.aria = function dialogStreamAction(this: StreamElement) {`
It reads the `message` and `type` attributes from the `<turbo-stream>` element, then it announces the message using ARIA live regions, either:
* **polite**: wait for other announcements to finish.
* **assertive**: interrupt and announce immediately.
`if (type === 'assertive') {`
`announce(message, {`
`politeness: 'assertive',`
`});`
`} else {`
`announce(message, {`
`politeness: 'polite',`
`});`
`}`
If you have a Turbo Stream like this:
`<turbo-stream action="aria" message="Form submitted!" type="polite"></turbo-stream>`
When it's received, what will happen?
* Extract the `message` and `type`
* Trigger a screen reader-friendly ARIA live region announcement
* Without changing the DOM or UI
##### **We have a Challenge to use this method for new Primerized date picker:**
~~When
`~~start_date.with_row(classes:
`~~render(Primer::Alpha::TextField.new(**text_field_options(name:
`~~end~~`
I investigated more, as the datepickr dialog is opened, whenever I change the value
This method doesn't work, when we have an `aria-live` region **outside a modal/dialog**, especially if that dialog **traps focus**. When we open a dialog, **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.