feat(a11y/combosearch): translatations for screen reader

This commit is contained in:
Colin Darie 2023-02-22 18:32:42 +01:00
parent acffd45a22
commit 71d43e9591
No known key found for this signature in database
GPG key ID: 4FB865FDBCA4BCC4
8 changed files with 74 additions and 19 deletions

View file

@ -1,3 +1,2 @@
class EditableChamp::AddressComponent < EditableChamp::EditableChampBaseComponent
include ApplicationHelper
class EditableChamp::AddressComponent < EditableChamp::ComboSearchComponent
end

View file

@ -1,6 +1,11 @@
- render_parent
= @form.hidden_field :value
= @form.hidden_field :external_id
= react_component("ComboAdresseSearch",
required: @champ.required?,
id: @champ.input_id,
describedby: @champ.describedby_id)
describedby: @champ.describedby_id,
**react_combo_props,
)

View file

@ -0,0 +1,17 @@
class EditableChamp::ComboSearchComponent < EditableChamp::EditableChampBaseComponent
include ApplicationHelper
def announce_template_id
@announce_template_id ||= dom_id(@champ, "aria-announce-template")
end
# NOTE: because this template is called by `render_parent` from a child template,
# as of ViewComponent 2.x translations virtual paths are not properly propagated
# and we can't use the usual component namespacing. Instead we use global translations.
def react_combo_props
{
screenReaderInstructions: t("combo_search_component.screen_reader_instructions"),
announceTemplateId: announce_template_id
}
end
end

View file

@ -0,0 +1,4 @@
%template{ id: announce_template_id }
%slot{ "name": "0" }= t("combo_search_component.result_slot_html", count: 0)
%slot{ "name": "1" }= t("combo_search_component.result_slot_html", count: 1)
%slot{ "name": "many" }= t("combo_search_component.result_slot_html", count: 2)

View file

@ -38,6 +38,8 @@ export type ComboSearchProps<Result> = {
className?: string;
placeholder?: string;
debounceDelay?: number;
screenReaderInstructions: string;
announceTemplateId: string;
};
type QueryKey = readonly [
@ -57,6 +59,8 @@ function ComboSearch<Result>({
transformResults = (_, results) => results as Result[],
id,
describedby,
screenReaderInstructions,
announceTemplateId,
debounceDelay = 0,
...props
}: ComboSearchProps<Result>) {
@ -134,28 +138,41 @@ function ComboSearch<Result>({
};
const [announceLive, setAnnounceLive] = useState('');
const a11yInstructions =
'utilisez les flèches haut et bas pour naviguer et la touche Entrée pour choisir.\
Sur un appareil tactile, explorez par toucher ou avec des gestes.';
const announceTimeout = useRef<ReturnType<typeof setTimeout>>();
const announceTemplate = document.querySelector<HTMLTemplateElement>(
`#${announceTemplateId}`
);
invariant(announceTemplate, `Missing #${announceTemplateId}`);
const announceFragment = useRef(
announceTemplate.content.cloneNode(true) as DocumentFragment
).current;
useEffect(() => {
if (isSuccess && results.length > 0) {
setAnnounceLive(
`${results.length} résultats disponibles, ${a11yInstructions}`
if (isSuccess) {
const slot = announceFragment.querySelector<HTMLSlotElement>(
'slot[name="' + (results.length <= 1 ? results.length : 'many') + '"]'
);
announceTimeout.current = setTimeout(() => {
setAnnounceLive('');
}, 4000);
} else {
setAnnounceLive('Aucun résultat trouvé.');
if (!slot) {
return;
}
const countSlot =
slot.querySelector<HTMLSlotElement>('slot[name="count"]');
if (countSlot) {
countSlot.replaceWith(String(results.length));
}
setAnnounceLive(slot.textContent ?? '');
}
announceTimeout.current = setTimeout(() => {
setAnnounceLive('');
}, 3000);
return () => clearTimeout(announceTimeout.current);
}, [results.length, isSuccess]);
}, [announceFragment, results.length, isSuccess]);
const initInstrId = useId();
const resultsId = useId();
@ -192,8 +209,7 @@ function ComboSearch<Result>({
)}
{!describedby && (
<span id={initInstrId} className="hidden">
Quand les résultats de lautocomplete sont disponibles,{' '}
{a11yInstructions}
{screenReaderInstructions}
</span>
)}
<div aria-live="assertive" className="sr-only">

View file

@ -110,6 +110,8 @@ ignore_unused:
- 'instructeurs.dossiers.filterable_state.*'
- 'views.prefill_descriptions.edit.possible_values.*'
- 'helpers.page_entries_info.*'
- 'combo_search_component.result_slot_html.*'
- 'combo_search_component.screen_reader_instructions'
# - '{devise,kaminari,will_paginate}.*'
# - 'simple_form.{yes,no}'
# - 'simple_form.{placeholders,hints,labels}.*'

View file

@ -67,6 +67,12 @@ en:
skiplinks:
quick: Quick access
content: Content
combo_search_component:
screen_reader_instructions: "When autocomplete results are available, use the up and down arrows to navigate through the list of results. Press Enter to select a result. Press Escape to close the results list."
result_slot_html:
zero: No result
one: 1 result
other: <slot name="count"></slot> results
layouts:
commencer:
no_procedure:

View file

@ -58,6 +58,12 @@ fr:
skiplinks:
quick: Accès rapide
content: Contenu
combo_search_component:
screen_reader_instructions: "Quand les résultats de lautocomplete sont disponibles, utilisez les flèches haut et bas pour naviguer dans la liste des résultats. Appuyez sur Entrée pour sélectionner un résultat. Appuyez sur Echap pour fermer la liste des résultats."
result_slot_html:
zero: Aucun résultat
one: 1 résultat
other: <slot name="count"></slot> résultats
layouts:
commencer:
no_procedure: