feat(a11y/combosearch): translatations for screen reader
This commit is contained in:
parent
acffd45a22
commit
71d43e9591
8 changed files with 74 additions and 19 deletions
|
@ -1,3 +1,2 @@
|
|||
class EditableChamp::AddressComponent < EditableChamp::EditableChampBaseComponent
|
||||
include ApplicationHelper
|
||||
class EditableChamp::AddressComponent < EditableChamp::ComboSearchComponent
|
||||
end
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
17
app/components/editable_champ/combo_search_component.rb
Normal file
17
app/components/editable_champ/combo_search_component.rb
Normal 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
|
|
@ -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)
|
|
@ -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 l’autocomplete sont disponibles,{' '}
|
||||
{a11yInstructions}
|
||||
{screenReaderInstructions}
|
||||
</span>
|
||||
)}
|
||||
<div aria-live="assertive" className="sr-only">
|
||||
|
|
|
@ -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}.*'
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -58,6 +58,12 @@ fr:
|
|||
skiplinks:
|
||||
quick: Accès rapide
|
||||
content: Contenu
|
||||
combo_search_component:
|
||||
screen_reader_instructions: "Quand les résultats de l’autocomplete 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:
|
||||
|
|
Loading…
Add table
Reference in a new issue