diff --git a/app/components/editable_champ/address_component.rb b/app/components/editable_champ/address_component.rb index 0b1db8bff..35ff5aeeb 100644 --- a/app/components/editable_champ/address_component.rb +++ b/app/components/editable_champ/address_component.rb @@ -1,3 +1,2 @@ -class EditableChamp::AddressComponent < EditableChamp::EditableChampBaseComponent - include ApplicationHelper +class EditableChamp::AddressComponent < EditableChamp::ComboSearchComponent end diff --git a/app/components/editable_champ/address_component/address_component.html.haml b/app/components/editable_champ/address_component/address_component.html.haml index da4fa6092..ef480948d 100644 --- a/app/components/editable_champ/address_component/address_component.html.haml +++ b/app/components/editable_champ/address_component/address_component.html.haml @@ -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, +) diff --git a/app/components/editable_champ/combo_search_component.rb b/app/components/editable_champ/combo_search_component.rb new file mode 100644 index 000000000..bbad7a600 --- /dev/null +++ b/app/components/editable_champ/combo_search_component.rb @@ -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 diff --git a/app/components/editable_champ/combo_search_component/combo_search_component.html.haml b/app/components/editable_champ/combo_search_component/combo_search_component.html.haml new file mode 100644 index 000000000..9b2e14a56 --- /dev/null +++ b/app/components/editable_champ/combo_search_component/combo_search_component.html.haml @@ -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) diff --git a/app/javascript/components/ComboSearch.tsx b/app/javascript/components/ComboSearch.tsx index aa1caad09..23cb597cf 100644 --- a/app/javascript/components/ComboSearch.tsx +++ b/app/javascript/components/ComboSearch.tsx @@ -38,6 +38,8 @@ export type ComboSearchProps = { className?: string; placeholder?: string; debounceDelay?: number; + screenReaderInstructions: string; + announceTemplateId: string; }; type QueryKey = readonly [ @@ -57,6 +59,8 @@ function ComboSearch({ transformResults = (_, results) => results as Result[], id, describedby, + screenReaderInstructions, + announceTemplateId, debounceDelay = 0, ...props }: ComboSearchProps) { @@ -134,28 +138,41 @@ function ComboSearch({ }; 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>(); + const announceTemplate = document.querySelector( + `#${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( + '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('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({ )} {!describedby && ( - Quand les résultats de l’autocomplete sont disponibles,{' '} - {a11yInstructions} + {screenReaderInstructions} )}
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 82bd9ca2a..c127bad30 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -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}.*' diff --git a/config/locales/en.yml b/config/locales/en.yml index 6b42db237..b01f24963 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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: results layouts: commencer: no_procedure: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index f333df668..50521590e 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -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: résultats layouts: commencer: no_procedure: