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 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.
`start_date.with_row(classes: "wp-datepicker-dialog-date-form--text-field-container") do`
`render(Primer::Alpha::TextField.new(**text_field_options(name: :start_date, label: start_date_label), aria: { live: :polite }))`
`end`
(I'm still searching for a better way of 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 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.
`start_date.with_row(classes: "wp-datepicker-dialog-date-form--text-field-container") do`
`render(Primer::Alpha::TextField.new(**text_field_options(name: :start_date, label: start_date_label), aria: { live: :polite }))`
`end`
(I'm still searching for a better way of implementation here)