From fc058f721d4f0d4b3aa6f29c16f3248b835159f0 Mon Sep 17 00:00:00 2001
From: Paul Chavard
Date: Wed, 5 Jan 2022 11:32:05 +0100
Subject: [PATCH 1/6] a11y(champs): expose ids for UI on champ
---
app/helpers/application_helper.rb | 2 +-
app/helpers/champ_helper.rb | 6 ------
app/models/champ.rb | 20 ++++++++++++++++++++
3 files changed, 21 insertions(+), 7 deletions(-)
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 194fd3513..98ec37d23 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -50,7 +50,7 @@ module ApplicationHelper
end
def render_champ(champ)
- champ_selector = ".editable-champ[data-champ-id=\"#{champ.id}\"]"
+ champ_selector = "##{champ.input_group_id}"
form_html = render 'shared/dossiers/edit', dossier: champ.dossier, apercu: false
champ_html = Nokogiri::HTML.fragment(form_html).at_css(champ_selector).to_s
# rubocop:disable Rails/OutputSafety
diff --git a/app/helpers/champ_helper.rb b/app/helpers/champ_helper.rb
index 41881adf7..0381e9c32 100644
--- a/app/helpers/champ_helper.rb
+++ b/app/helpers/champ_helper.rb
@@ -20,12 +20,6 @@ module ChampHelper
simple_format(auto_linked_text, {}, sanitize: false)
end
- def describedby_id(champ)
- if champ.description.present?
- "desc-#{champ.type_de_champ.id}-#{champ.row}"
- end
- end
-
def auto_attach_url(form, object)
if object.is_a?(Champ) && object.persisted? && object.public?
champs_piece_justificative_url(object.id)
diff --git a/app/models/champ.rb b/app/models/champ.rb
index 552d0676d..aa093ccc5 100644
--- a/app/models/champ.rb
+++ b/app/models/champ.rb
@@ -139,6 +139,22 @@ class Champ < ApplicationRecord
true
end
+ def input_group_id
+ "champ-#{html_id}"
+ end
+
+ def input_id
+ "#{html_id}-input"
+ end
+
+ def labelledby_id
+ "#{html_id}-label"
+ end
+
+ def describedby_id
+ "#{html_id}-description" if description.present?
+ end
+
def stable_id
type_de_champ.stable_id
end
@@ -159,6 +175,10 @@ class Champ < ApplicationRecord
private
+ def html_id
+ "#{stable_id}-#{id}"
+ end
+
def needs_dossier_id?
!dossier_id && parent_id
end
From d6b6bb0f2a435dfbf19b03359dd5cf104d319a0d Mon Sep 17 00:00:00 2001
From: Paul Chavard
Date: Wed, 5 Jan 2022 11:34:43 +0100
Subject: [PATCH 2/6] a11y(combobox): add support for describedby and
labelledby and improuve external fields handling
---
app/assets/stylesheets/forms.scss | 21 +-
.../stylesheets/personnes_impliquees.scss | 6 +-
app/assets/stylesheets/procedure_show.scss | 11 +-
.../components/ComboAdresseSearch.jsx | 19 +-
.../ComboAnnuaireEducationSearch.jsx | 5 +-
.../components/ComboCommunesSearch.jsx | 71 ++--
.../components/ComboDepartementsSearch.jsx | 9 +-
app/javascript/components/ComboMultiple.jsx | 314 ++++++++++++++++++
.../components/ComboMultipleDropdownList.jsx | 299 +----------------
app/javascript/components/ComboPaysSearch.jsx | 5 +-
.../components/ComboRegionsSearch.jsx | 5 +-
app/javascript/components/ComboSearch.jsx | 85 ++---
app/javascript/components/shared/hooks.js | 35 +-
app/javascript/packs/application.js | 1 +
package.json | 2 +
yarn.lock | 21 ++
16 files changed, 458 insertions(+), 451 deletions(-)
create mode 100644 app/javascript/components/ComboMultiple.jsx
diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss
index 922b89d97..031f96c37 100644
--- a/app/assets/stylesheets/forms.scss
+++ b/app/assets/stylesheets/forms.scss
@@ -317,7 +317,7 @@
list-style: none;
}
- [data-reach-combobox-token] {
+ [data-reach-combobox-token] button {
border: solid 1px $border-grey;
color: $black;
border-radius: 4px;
@@ -328,14 +328,9 @@
align-items: center;
}
- [data-reach-combobox-token]:focus {
+ [data-reach-combobox-token] button:focus {
background-color: $black;
color: $white;
-
- [data-combobox-remove-token] {
- background-color: $black;
- color: $white;
- }
}
.editable-champ {
@@ -493,13 +488,13 @@
}
}
-[data-react-class]:not([data-react-class="ComboMultipleDropdownList"]) {
+[data-react-class]:not([data-react-class^="ComboMultiple"]) {
[data-reach-combobox-input]:not(.no-margin) {
margin-bottom: $default-fields-spacer;
}
}
-[data-react-class="ComboMultipleDropdownList"] {
+[data-react-class^="ComboMultiple"] {
margin-bottom: $default-fields-spacer;
[data-reach-combobox-input] {
@@ -516,7 +511,7 @@
}
}
-[data-combobox-token-label] {
+[data-reach-combobox-token-label] {
border: 1px solid #CCCCCC;
border-radius: 4px;
display: flex;
@@ -533,14 +528,14 @@
color: $white;
}
-[data-combobox-separator] {
+[data-reach-combobox-separator] {
font-size: 16px;
color: $dark-grey;
background: $light-grey;
padding: $default-spacer;
}
-[data-combobox-remove-token] {
+[data-reach-combobox-token] button {
cursor: pointer;
background-color: transparent;
background-image: none;
@@ -552,7 +547,7 @@
align-items: center !important;
}
-[data-reach-combobox-input]:focus {
+[data-reach-combobox-input] button:focus {
outline-color: $light-blue;
}
diff --git a/app/assets/stylesheets/personnes_impliquees.scss b/app/assets/stylesheets/personnes_impliquees.scss
index 381886823..7be4fb89e 100644
--- a/app/assets/stylesheets/personnes_impliquees.scss
+++ b/app/assets/stylesheets/personnes_impliquees.scss
@@ -9,7 +9,7 @@
margin-left: 16px;
}
- [data-react-class="ComboMultipleDropdownList"] {
+ [data-react-class^="ComboMultiple"] {
margin-bottom: $default-fields-spacer;
[data-reach-combobox-token-list] {
@@ -17,7 +17,7 @@
display: flex;
}
- [data-reach-combobox-token] {
+ [data-reach-combobox-token] button {
border: solid 1px $border-grey;
color: $black;
margin-top: 0.5 * $default-padding;
@@ -29,7 +29,7 @@
list-style: none;
}
- [data-reach-combobox-token]:focus {
+ [data-reach-combobox-token] button:focus {
background-color: $black;
color: $white;
}
diff --git a/app/assets/stylesheets/procedure_show.scss b/app/assets/stylesheets/procedure_show.scss
index 5b4f6a9ba..182be6324 100644
--- a/app/assets/stylesheets/procedure_show.scss
+++ b/app/assets/stylesheets/procedure_show.scss
@@ -62,7 +62,7 @@
text-align: center;
}
- [data-react-class="ComboMultipleDropdownList"] {
+ [data-react-class^="ComboMultiple"] {
margin-bottom: $default-fields-spacer;
[data-reach-combobox-token-list] {
@@ -71,7 +71,7 @@
width: 100%;
}
- [data-reach-combobox-token] {
+ [data-reach-combobox-token] button {
border: solid 1px $border-grey;
color: $black;
margin: 0.25 * $default-padding;
@@ -83,14 +83,9 @@
align-items: center;
}
- [data-reach-combobox-token]:focus {
+ [data-reach-combobox-token] button:focus {
background-color: $black;
color: $white;
-
- [data-combobox-remove-token] {
- background-color: $black;
- color: $white;
- }
}
diff --git a/app/javascript/components/ComboAdresseSearch.jsx b/app/javascript/components/ComboAdresseSearch.jsx
index 78177dbe8..5691d76e0 100644
--- a/app/javascript/components/ComboAdresseSearch.jsx
+++ b/app/javascript/components/ComboAdresseSearch.jsx
@@ -6,42 +6,29 @@ import ComboSearch from './ComboSearch';
import { queryClient } from './shared/queryClient';
function ComboAdresseSearch({
- mandatory,
- placeholder,
- hiddenFieldId,
- onChange,
transformResult = ({ properties: { label } }) => [label, label],
allowInputValues = true,
- className
+ ...props
}) {
const transformResults = useCallback((_, { features }) => features);
return (
);
}
ComboAdresseSearch.propTypes = {
- className: PropTypes.string,
- placeholder: PropTypes.string,
- mandatory: PropTypes.bool,
- hiddenFieldId: PropTypes.string,
transformResult: PropTypes.func,
- allowInputValues: PropTypes.bool,
- onChange: PropTypes.func
+ allowInputValues: PropTypes.bool
};
export default ComboAdresseSearch;
diff --git a/app/javascript/components/ComboAnnuaireEducationSearch.jsx b/app/javascript/components/ComboAnnuaireEducationSearch.jsx
index bbc81414a..a0bc4c969 100644
--- a/app/javascript/components/ComboAnnuaireEducationSearch.jsx
+++ b/app/javascript/components/ComboAnnuaireEducationSearch.jsx
@@ -4,12 +4,10 @@ import { QueryClientProvider } from 'react-query';
import ComboSearch from './ComboSearch';
import { queryClient } from './shared/queryClient';
-function ComboAnnuaireEducationSearch(params) {
+function ComboAnnuaireEducationSearch(props) {
return (
records}
@@ -20,6 +18,7 @@ function ComboAnnuaireEducationSearch(params) {
nom_commune
}
}) => [id, `${nom_etablissement}, ${nom_commune} (${id})`]}
+ {...props}
/>
);
diff --git a/app/javascript/components/ComboCommunesSearch.jsx b/app/javascript/components/ComboCommunesSearch.jsx
index c9dac0a21..85d1b54c3 100644
--- a/app/javascript/components/ComboCommunesSearch.jsx
+++ b/app/javascript/components/ComboCommunesSearch.jsx
@@ -1,10 +1,12 @@
-import React, { useState, useMemo } from 'react';
+import React from 'react';
import { QueryClientProvider } from 'react-query';
import { matchSorter } from 'match-sorter';
+import PropTypes from 'prop-types';
import ComboSearch from './ComboSearch';
import { queryClient } from './shared/queryClient';
import { ComboDepartementsSearch } from './ComboDepartementsSearch';
+import { useHiddenField, groupId } from './shared/hooks';
// Avoid hiding similar matches for precise queries (like "Sainte Marie")
function searchResultsLimit(term) {
@@ -48,34 +50,18 @@ const [placeholderDepartement, placeholderCommune] =
Math.floor(Math.random() * (placeholderDepartements.length - 1))
];
-function ComboCommunesSearch(params) {
- const hiddenDepartementFieldId = `${params.hiddenFieldId}:departement`;
- const hiddenDepartementField = useMemo(
- () =>
- document.querySelector(`input[data-attr="${hiddenDepartementFieldId}"]`),
- [params.hiddenFieldId]
+function ComboCommunesSearch({ id, ...props }) {
+ const group = groupId(id);
+ const [departementValue, setDepartementValue] = useHiddenField(
+ group,
+ 'departement'
);
- const hiddenCodeDepartementField = useMemo(
- () =>
- document.querySelector(
- `input[data-attr="${params.hiddenFieldId}:code_departement"]`
- ),
- [params.hiddenFieldId]
+ const [codeDepartement, setCodeDepartement] = useHiddenField(
+ group,
+ 'code_departement'
);
- const inputId = useMemo(
- () =>
- document.querySelector(`input[data-uuid="${params.hiddenFieldId}"]`)?.id,
- [params.hiddenFieldId]
- );
- const [departementCode, setDepartementCode] = useState(
- () => hiddenCodeDepartementField?.value
- );
- const departementValue = useMemo(
- () => hiddenDepartementField?.value,
- [hiddenDepartementField]
- );
- const departementDescribedBy = `${inputId}_departement_notice`;
- const communeDescribedBy = `${inputId}_commune_notice`;
+ const departementDescribedBy = `${id}_departement_notice`;
+ const communeDescribedBy = `${id}_commune_notice`;
return (
@@ -87,22 +73,19 @@ function ComboCommunesSearch(params) {
{
- setDepartementCode(result?.code);
- if (hiddenDepartementField && hiddenCodeDepartementField) {
- hiddenDepartementField.setAttribute('value', result?.nom);
- hiddenCodeDepartementField.setAttribute('value', result?.code);
- }
+ setDepartementValue(result?.nom);
+ setCodeDepartement(result?.code);
}}
/>
- {departementCode ? (
+ {codeDepartement ? (
@@ -111,14 +94,12 @@ function ComboCommunesSearch(params) {
[
code,
@@ -132,4 +113,8 @@ function ComboCommunesSearch(params) {
);
}
+ComboCommunesSearch.propTypes = {
+ id: PropTypes.string
+};
+
export default ComboCommunesSearch;
diff --git a/app/javascript/components/ComboDepartementsSearch.jsx b/app/javascript/components/ComboDepartementsSearch.jsx
index 3f4c3350a..dab35ebb4 100644
--- a/app/javascript/components/ComboDepartementsSearch.jsx
+++ b/app/javascript/components/ComboDepartementsSearch.jsx
@@ -19,11 +19,11 @@ function expandResultsWithForeignDepartement(term, results) {
export function ComboDepartementsSearch({
addForeignDepartement = true,
- ...params
+ ...props
}) {
return (
[code, `${code} - ${nom}`]}
@@ -37,10 +37,7 @@ export function ComboDepartementsSearch({
function ComboDepartementsSearchDefault(params) {
return (
-
+
);
}
diff --git a/app/javascript/components/ComboMultiple.jsx b/app/javascript/components/ComboMultiple.jsx
new file mode 100644
index 000000000..9b7d961ed
--- /dev/null
+++ b/app/javascript/components/ComboMultiple.jsx
@@ -0,0 +1,314 @@
+import React, {
+ useMemo,
+ useState,
+ useRef,
+ useContext,
+ createContext,
+ useEffect,
+ useLayoutEffect
+} from 'react';
+import PropTypes from 'prop-types';
+import {
+ Combobox,
+ ComboboxInput,
+ ComboboxList,
+ ComboboxOption,
+ ComboboxPopover
+} from '@reach/combobox';
+import { useId } from '@reach/auto-id';
+import '@reach/combobox/styles.css';
+import { matchSorter } from 'match-sorter';
+import { XIcon } from '@heroicons/react/outline';
+import isHotkey from 'is-hotkey';
+import invariant from 'tiny-invariant';
+
+import { useDeferredSubmit, useHiddenField } from './shared/hooks';
+
+const Context = createContext();
+
+function ComboMultiple({
+ options,
+ id,
+ labelledby,
+ describedby,
+ label,
+ group,
+ name = 'value',
+ selected,
+ acceptNewValues = false
+}) {
+ invariant(id || label, 'ComboMultiple: `id` or a `label` are required');
+ invariant(group, 'ComboMultiple: `group` is required');
+
+ if (!Array.isArray(options[0])) {
+ options = options.filter((o) => o).map((o) => [o, o]);
+ }
+ const inputRef = useRef();
+ const [term, setTerm] = useState('');
+ const [selections, setSelections] = useState(selected);
+ const [newValues, setNewValues] = useState([]);
+ const inputId = useId(id);
+ const removedLabelledby = `${inputId}-remove`;
+ const selectedLabelledby = `${inputId}-selected`;
+
+ const optionValueByLabel = (label) => {
+ const maybeOption = newValues.includes(label)
+ ? [label, label]
+ : options.find(([optionLabel]) => optionLabel == label);
+ return maybeOption ? maybeOption[1] : undefined;
+ };
+ const optionLabelByValue = (value) => {
+ const maybeOption = newValues.includes(value)
+ ? [value, value]
+ : options.find(([, optionValue]) => optionValue == value);
+ return maybeOption ? maybeOption[0] : undefined;
+ };
+
+ const extraOptions = useMemo(
+ () =>
+ acceptNewValues && term && term.length > 2 && !optionLabelByValue(term)
+ ? [[term, term]]
+ : [],
+ [acceptNewValues, term, newValues.join(',')]
+ );
+ const results = useMemo(
+ () =>
+ [
+ ...extraOptions,
+ ...(term
+ ? matchSorter(
+ options.filter(([label]) => !label.startsWith('--')),
+ term
+ )
+ : options)
+ ].filter(([, value]) => !selections.includes(value)),
+ [term, selections.join(','), newValues.join(',')]
+ );
+ const [, setHiddenFieldValue, hiddenField] = useHiddenField(group, name);
+ const awaitFormSubmit = useDeferredSubmit(hiddenField);
+
+ const handleChange = (event) => {
+ setTerm(event.target.value);
+ };
+
+ const saveSelection = (fn) => {
+ setSelections((selections) => {
+ selections = fn(selections);
+ setHiddenFieldValue(JSON.stringify(selections));
+ return selections;
+ });
+ };
+
+ const onSelect = (value) => {
+ const maybeValue = [...extraOptions, ...options].find(
+ ([val]) => val == value
+ );
+ const selectedValue = maybeValue && maybeValue[1];
+ if (selectedValue) {
+ if (
+ acceptNewValues &&
+ extraOptions[0] &&
+ extraOptions[0][0] == selectedValue
+ ) {
+ setNewValues((newValues) => [...newValues, selectedValue]);
+ }
+ saveSelection((selections) => [...selections, selectedValue]);
+ }
+ setTerm('');
+ awaitFormSubmit.done();
+ };
+
+ const onRemove = (label) => {
+ const optionValue = optionValueByLabel(label);
+ if (optionValue) {
+ saveSelection((selections) =>
+ selections.filter((value) => value != optionValue)
+ );
+ setNewValues((newValues) =>
+ newValues.filter((value) => value != optionValue)
+ );
+ }
+ inputRef.current.focus();
+ };
+
+ const onKeyDown = (event) => {
+ if (
+ isHotkey('enter', event) ||
+ isHotkey(' ', event) ||
+ isHotkey(',', event) ||
+ isHotkey(';', event)
+ ) {
+ if (
+ term &&
+ [...extraOptions, ...options].map(([label]) => label).includes(term)
+ ) {
+ event.preventDefault();
+ onSelect(term);
+ }
+ }
+ };
+
+ const onBlur = () => {
+ if (
+ term &&
+ [...extraOptions, ...options].map(([label]) => label).includes(term)
+ ) {
+ awaitFormSubmit(() => {
+ onSelect(term);
+ });
+ }
+ };
+
+ return (
+
+
+
+ désélectionner
+
+
+ {selections.map((selection) => (
+
+ ))}
+
+
+
+ {results && (results.length > 0 || !acceptNewValues) && (
+
+ {results.length === 0 && (
+
+ Aucun résultat{' '}
+
+
+ )}
+
+ {results.map(([label, value], index) => {
+ if (label.startsWith('--')) {
+ return ;
+ }
+ return (
+
+ );
+ })}
+
+
+ )}
+
+ );
+}
+
+function ComboboxTokenLabel({ onRemove, ...props }) {
+ const selectionsRef = useRef([]);
+
+ useLayoutEffect(() => {
+ selectionsRef.current = [];
+ return () => (selectionsRef.current = []);
+ });
+
+ const context = {
+ onRemove,
+ selectionsRef
+ };
+
+ return (
+
+
+
+ );
+}
+
+ComboboxTokenLabel.propTypes = {
+ onRemove: PropTypes.func
+};
+
+function ComboboxSeparator({ value }) {
+ return (
+
+ {value.slice(2, -2)}
+
+ );
+}
+
+ComboboxSeparator.propTypes = {
+ value: PropTypes.string
+};
+
+function ComboboxToken({ value, describedby, ...props }) {
+ const { selectionsRef, onRemove } = useContext(Context);
+ useEffect(() => {
+ selectionsRef.current.push(value);
+ });
+
+ return (
+
+
+
+ );
+}
+
+ComboboxToken.propTypes = {
+ value: PropTypes.string,
+ describedby: PropTypes.string
+};
+
+ComboMultiple.propTypes = {
+ options: PropTypes.oneOfType([
+ PropTypes.arrayOf(PropTypes.string),
+ PropTypes.arrayOf(
+ PropTypes.arrayOf(
+ PropTypes.oneOfType([PropTypes.string, PropTypes.number])
+ )
+ )
+ ]),
+ selected: PropTypes.arrayOf(PropTypes.string),
+ arraySelected: PropTypes.arrayOf(PropTypes.array),
+ acceptNewValues: PropTypes.bool,
+ mandatory: PropTypes.bool,
+ id: PropTypes.string,
+ group: PropTypes.string,
+ name: PropTypes.string,
+ labelledby: PropTypes.string,
+ describedby: PropTypes.string,
+ label: PropTypes.string
+};
+
+export default ComboMultiple;
diff --git a/app/javascript/components/ComboMultipleDropdownList.jsx b/app/javascript/components/ComboMultipleDropdownList.jsx
index 93f5d4d22..b918a5603 100644
--- a/app/javascript/components/ComboMultipleDropdownList.jsx
+++ b/app/javascript/components/ComboMultipleDropdownList.jsx
@@ -1,302 +1,15 @@
-import React, {
- useMemo,
- useState,
- useRef,
- useContext,
- createContext,
- useEffect,
- useLayoutEffect
-} from 'react';
+import React from 'react';
import PropTypes from 'prop-types';
-import {
- Combobox,
- ComboboxInput,
- ComboboxList,
- ComboboxOption,
- ComboboxPopover
-} from '@reach/combobox';
-import '@reach/combobox/styles.css';
-import { matchSorter } from 'match-sorter';
-import { fire } from '@utils';
-import { XIcon } from '@heroicons/react/outline';
-import isHotkey from 'is-hotkey';
-import { useDeferredSubmit } from './shared/hooks';
+import { groupId } from './shared/hooks';
+import ComboMultiple from './ComboMultiple';
-const Context = createContext();
-
-function ComboMultipleDropdownList({
- options,
- hiddenFieldId,
- selected,
- label,
- acceptNewValues = false
-}) {
- if (label == undefined) {
- label = 'Choisir une option';
- }
- if (!Array.isArray(options[0])) {
- options = options.filter((o) => o).map((o) => [o, o]);
- }
- const inputRef = useRef();
- const [term, setTerm] = useState('');
- const [selections, setSelections] = useState(selected);
- const [newValues, setNewValues] = useState([]);
-
- const optionValueByLabel = (label) => {
- const maybeOption = newValues.includes(label)
- ? [label, label]
- : options.find(([optionLabel]) => optionLabel == label);
- return maybeOption ? maybeOption[1] : undefined;
- };
- const optionLabelByValue = (value) => {
- const maybeOption = newValues.includes(value)
- ? [value, value]
- : options.find(([, optionValue]) => optionValue == value);
- return maybeOption ? maybeOption[0] : undefined;
- };
-
- const extraOptions = useMemo(
- () =>
- acceptNewValues && term && term.length > 2 && !optionLabelByValue(term)
- ? [[term, term]]
- : [],
- [acceptNewValues, term, newValues.join(',')]
- );
- const results = useMemo(
- () =>
- [
- ...extraOptions,
- ...(term
- ? matchSorter(
- options.filter(([label]) => !label.startsWith('--')),
- term
- )
- : options)
- ].filter(([, value]) => !selections.includes(value)),
- [term, selections.join(','), newValues.join(',')]
- );
- const hiddenField = useMemo(
- () => document.querySelector(`input[data-uuid="${hiddenFieldId}"]`),
- [hiddenFieldId]
- );
- const awaitFormSubmit = useDeferredSubmit(hiddenField);
-
- const handleChange = (event) => {
- setTerm(event.target.value);
- };
-
- const saveSelection = (fn) => {
- setSelections((selections) => {
- selections = fn(selections);
- if (hiddenField) {
- hiddenField.setAttribute('value', JSON.stringify(selections));
- fire(hiddenField, 'autosave:trigger');
- }
- return selections;
- });
- };
-
- const onSelect = (value) => {
- const maybeValue = [...extraOptions, ...options].find(
- ([val]) => val == value
- );
- const selectedValue = maybeValue && maybeValue[1];
- if (selectedValue) {
- if (
- acceptNewValues &&
- extraOptions[0] &&
- extraOptions[0][0] == selectedValue
- ) {
- setNewValues((newValues) => [...newValues, selectedValue]);
- }
- saveSelection((selections) => [...selections, selectedValue]);
- }
- setTerm('');
- awaitFormSubmit.done();
- };
-
- const onRemove = (label) => {
- const optionValue = optionValueByLabel(label);
- if (optionValue) {
- saveSelection((selections) =>
- selections.filter((value) => value != optionValue)
- );
- setNewValues((newValues) =>
- newValues.filter((value) => value != optionValue)
- );
- }
- inputRef.current.focus();
- };
-
- const onKeyDown = (event) => {
- if (
- isHotkey('enter', event) ||
- isHotkey(' ', event) ||
- isHotkey(',', event) ||
- isHotkey(';', event)
- ) {
- if (
- term &&
- [...extraOptions, ...options].map(([label]) => label).includes(term)
- ) {
- event.preventDefault();
- onSelect(term);
- }
- }
- };
-
- const onBlur = () => {
- if (
- term &&
- [...extraOptions, ...options].map(([label]) => label).includes(term)
- ) {
- awaitFormSubmit(() => {
- onSelect(term);
- });
- }
- };
-
- return (
-
-
-
- {selections.map((selection) => (
-
- ))}
-
-
-
- {results && (results.length > 0 || !acceptNewValues) && (
-
- {results.length === 0 && (
-
- Aucun résultat{' '}
-
-
- )}
-
- {results.map(([label, value], index) => {
- if (label.startsWith('--')) {
- return ;
- }
- return (
-
- );
- })}
-
-
- )}
-
- );
+function ComboMultipleDropdownList({ id, ...props }) {
+ return ;
}
-function ComboboxTokenLabel({ onRemove, ...props }) {
- const selectionsRef = useRef([]);
-
- useLayoutEffect(() => {
- selectionsRef.current = [];
- return () => (selectionsRef.current = []);
- });
-
- const context = {
- onRemove,
- selectionsRef
- };
-
- return (
-
-
-
- );
-}
-
-ComboboxTokenLabel.propTypes = {
- onRemove: PropTypes.func
-};
-
-function ComboboxSeparator({ value }) {
- return (
-
- {value.slice(2, -2)}
-
- );
-}
-
-ComboboxSeparator.propTypes = {
- value: PropTypes.string
-};
-
-function ComboboxToken({ value, ...props }) {
- const { selectionsRef, onRemove } = useContext(Context);
- useEffect(() => {
- selectionsRef.current.push(value);
- });
-
- return (
- {
- if (event.key === 'Backspace') {
- onRemove(value);
- }
- }}
- {...props}
- >
-
- {value}
-
- );
-}
-
-ComboboxToken.propTypes = {
- value: PropTypes.string,
- label: PropTypes.string
-};
-
ComboMultipleDropdownList.propTypes = {
- options: PropTypes.oneOfType([
- PropTypes.arrayOf(PropTypes.string),
- PropTypes.arrayOf(
- PropTypes.arrayOf(
- PropTypes.oneOfType([PropTypes.string, PropTypes.number])
- )
- )
- ]),
- hiddenFieldId: PropTypes.string,
- selected: PropTypes.arrayOf(PropTypes.string),
- arraySelected: PropTypes.arrayOf(PropTypes.array),
- label: PropTypes.string,
- acceptNewValues: PropTypes.bool
+ id: PropTypes.string
};
export default ComboMultipleDropdownList;
diff --git a/app/javascript/components/ComboPaysSearch.jsx b/app/javascript/components/ComboPaysSearch.jsx
index 22b1cdcd6..d44fde47c 100644
--- a/app/javascript/components/ComboPaysSearch.jsx
+++ b/app/javascript/components/ComboPaysSearch.jsx
@@ -4,15 +4,14 @@ import { QueryClientProvider } from 'react-query';
import ComboSearch from './ComboSearch';
import { queryClient } from './shared/queryClient';
-function ComboPaysSearch(params) {
+function ComboPaysSearch(props) {
return (
[code, value, label]}
+ {...props}
/>
);
diff --git a/app/javascript/components/ComboRegionsSearch.jsx b/app/javascript/components/ComboRegionsSearch.jsx
index 0a1a5c723..e031cc5dd 100644
--- a/app/javascript/components/ComboRegionsSearch.jsx
+++ b/app/javascript/components/ComboRegionsSearch.jsx
@@ -4,15 +4,14 @@ import { QueryClientProvider } from 'react-query';
import ComboSearch from './ComboSearch';
import { queryClient } from './shared/queryClient';
-function ComboRegionsSearch(params) {
+function ComboRegionsSearch(props) {
return (
[code, nom]}
+ {...props}
/>
);
diff --git a/app/javascript/components/ComboSearch.jsx b/app/javascript/components/ComboSearch.jsx
index 8677f2b5d..bc209c6d0 100644
--- a/app/javascript/components/ComboSearch.jsx
+++ b/app/javascript/components/ComboSearch.jsx
@@ -1,10 +1,4 @@
-import React, {
- useState,
- useMemo,
- useCallback,
- useRef,
- useEffect
-} from 'react';
+import React, { useState, useCallback, useRef } from 'react';
import { useDebounce } from 'use-debounce';
import { useQuery } from 'react-query';
import PropTypes from 'prop-types';
@@ -16,42 +10,33 @@ import {
ComboboxOption
} from '@reach/combobox';
import '@reach/combobox/styles.css';
-import { fire } from '@utils';
+import invariant from 'tiny-invariant';
-import { useDeferredSubmit } from './shared/hooks';
+import { useDeferredSubmit, useHiddenField, groupId } from './shared/hooks';
function defaultTransformResults(_, results) {
return results;
}
function ComboSearch({
- hiddenFieldId,
onChange,
+ value: controlledValue,
scope,
- inputId,
scopeExtra,
minimumInputLength,
transformResult,
allowInputValues = false,
transformResults = defaultTransformResults,
+ id,
+ describedby,
...props
}) {
- const hiddenValueField = useMemo(
- () => document.querySelector(`input[data-uuid="${hiddenFieldId}"]`),
- [hiddenFieldId]
- );
- const comboInputId = useMemo(
- () => hiddenValueField?.id || inputId,
- [inputId, hiddenValueField]
- );
- const hiddenIdField = useMemo(
- () =>
- document.querySelector(
- `input[data-uuid="${hiddenFieldId}"] + input[data-reference]`
- ),
- [hiddenFieldId]
- );
- const initialValue = hiddenValueField ? hiddenValueField.value : props.value;
+ invariant(id || onChange, 'ComboSearch: `id` or `onChange` are required');
+
+ const group = !onChange ? groupId(id) : null;
+ const [externalValue, setExternalValue, hiddenField] = useHiddenField(group);
+ const [, setExternalId] = useHiddenField(group, 'external_id');
+ const initialValue = externalValue ? externalValue : controlledValue;
const [searchTerm, setSearchTerm] = useState('');
const [debouncedSearchTerm] = useDebounce(searchTerm, 300);
const [value, setValue] = useState(initialValue);
@@ -60,41 +45,26 @@ function ComboSearch({
const [, value, label] = transformResult(result);
return label ?? value;
};
- const setExternalValue = useCallback(
- (value) => {
- if (hiddenValueField) {
- hiddenValueField.setAttribute('value', value);
- fire(hiddenValueField, 'autosave:trigger');
- }
- },
- [hiddenValueField]
- );
- const setExternalId = useCallback(
- (key) => {
- if (hiddenIdField) {
- hiddenIdField.setAttribute('value', key);
- }
- },
- [hiddenIdField]
- );
const setExternalValueAndId = useCallback((label) => {
const { key, value, result } = resultsMap.current[label];
- setExternalId(key);
- setExternalValue(value);
if (onChange) {
onChange(value, result);
+ } else {
+ setExternalId(key);
+ setExternalValue(value);
}
}, []);
- const awaitFormSubmit = useDeferredSubmit(hiddenValueField);
+ const awaitFormSubmit = useDeferredSubmit(hiddenField);
const handleOnChange = useCallback(
({ target: { value } }) => {
setValue(value);
if (!value) {
- setExternalId('');
- setExternalValue('');
if (onChange) {
onChange(null);
+ } else {
+ setExternalId('');
+ setExternalValue('');
}
} else if (value.length >= minimumInputLength) {
setSearchTerm(value.trim());
@@ -133,20 +103,16 @@ function ComboSearch({
}
}, [data]);
- useEffect(() => {
- document
- .querySelector(`#${comboInputId}[type="hidden"]`)
- ?.removeAttribute('id');
- }, [comboInputId]);
-
return (
{isSuccess && (
@@ -178,15 +144,16 @@ function ComboSearch({
ComboSearch.propTypes = {
value: PropTypes.string,
- hiddenFieldId: PropTypes.string,
scope: PropTypes.string,
minimumInputLength: PropTypes.number,
transformResult: PropTypes.func,
transformResults: PropTypes.func,
allowInputValues: PropTypes.bool,
onChange: PropTypes.func,
- inputId: PropTypes.string,
- scopeExtra: PropTypes.string
+ scopeExtra: PropTypes.string,
+ mandatory: PropTypes.bool,
+ id: PropTypes.string,
+ describedby: PropTypes.string
};
export default ComboSearch;
diff --git a/app/javascript/components/shared/hooks.js b/app/javascript/components/shared/hooks.js
index a64aca7c5..a07191d75 100644
--- a/app/javascript/components/shared/hooks.js
+++ b/app/javascript/components/shared/hooks.js
@@ -1,4 +1,5 @@
-import { useRef, useCallback } from 'react';
+import { useRef, useCallback, useMemo, useState } from 'react';
+import { fire } from '@utils';
export function useDeferredSubmit(input) {
const calledRef = useRef(false);
@@ -31,3 +32,35 @@ export function useDeferredSubmit(input) {
};
return awaitFormSubmit;
}
+
+export function groupId(id) {
+ return `#champ-${id.replace(/-input$/, '')}`;
+}
+
+export function useHiddenField(group, name = 'value') {
+ const hiddenField = useMemo(
+ () => selectInputInGroup(group, name),
+ [group, name]
+ );
+ const [value, setValue] = useState(() => hiddenField?.value);
+
+ return [
+ value,
+ (value) => {
+ if (hiddenField) {
+ hiddenField.setAttribute('value', value);
+ setValue(value);
+ fire(hiddenField, 'autosave:trigger');
+ }
+ },
+ hiddenField
+ ];
+}
+
+function selectInputInGroup(group, name) {
+ if (group) {
+ return document.querySelector(
+ `${group} input[type="hidden"][name$="[${name}]"], ${group} input[type="hidden"][name="${name}"]`
+ );
+ }
+}
diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js
index f0cc6ec15..898eea8f6 100644
--- a/app/javascript/packs/application.js
+++ b/app/javascript/packs/application.js
@@ -69,6 +69,7 @@ registerReactComponents({
ComboMultipleDropdownList: Loadable(() =>
import('../components/ComboMultipleDropdownList')
),
+ ComboMultiple: Loadable(() => import('../components/ComboMultiple')),
ComboPaysSearch: Loadable(() => import('../components/ComboPaysSearch')),
ComboRegionsSearch: Loadable(() =>
import('../components/ComboRegionsSearch')
diff --git a/package.json b/package.json
index 42bc33e43..7ff9df2bf 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"@rails/activestorage": "^6.1.4-1",
"@rails/ujs": "^6.1.4-1",
"@rails/webpacker": "5.4.3",
+ "@reach/auto-id": "^0.16.0",
"@reach/combobox": "^0.13.0",
"@reach/slider": "^0.15.0",
"@reach/visually-hidden": "^0.15.2",
@@ -36,6 +37,7 @@
"react-popper": "^2.2.5",
"react-query": "^3.9.7",
"react-sortable-hoc": "^1.11.0",
+ "tiny-invariant": "^1.2.0",
"trix": "^1.2.3",
"use-debounce": "^5.2.0",
"webpack": "^4.46.0",
diff --git a/yarn.lock b/yarn.lock
index 14f977e5c..bf48941f5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1838,6 +1838,14 @@
"@reach/utils" "0.15.3"
tslib "^2.3.0"
+"@reach/auto-id@^0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.16.0.tgz#dfabc3227844e8c04f8e6e45203a8e14a8edbaed"
+ integrity sha512-5ssbeP5bCkM39uVsfQCwBBL+KT8YColdnMN5/Eto6Rj7929ql95R3HZUOkKIvj7mgPtEb60BLQxd1P3o6cjbmg==
+ dependencies:
+ "@reach/utils" "0.16.0"
+ tslib "^2.3.0"
+
"@reach/combobox@^0.13.0":
version "0.13.2"
resolved "https://registry.yarnpkg.com/@reach/combobox/-/combobox-0.13.2.tgz#f0a38c685a2704f6a189709e63cd7e25809da1e4"
@@ -1922,6 +1930,14 @@
tiny-warning "^1.0.3"
tslib "^2.3.0"
+"@reach/utils@0.16.0":
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.16.0.tgz#5b0777cf16a7cab1ddd4728d5d02762df0ba84ce"
+ integrity sha512-PCggBet3qaQmwFNcmQ/GqHSefadAFyNCUekq9RrWoaU9hh/S4iaFgf2MBMdM47eQj5i/Bk0Mm07cP/XPFlkN+Q==
+ dependencies:
+ tiny-warning "^1.0.3"
+ tslib "^2.3.0"
+
"@reach/visually-hidden@^0.15.2":
version "0.15.2"
resolved "https://registry.yarnpkg.com/@reach/visually-hidden/-/visually-hidden-0.15.2.tgz#07794cb53f4bd23a9452d53a0ad7778711ee323f"
@@ -12445,6 +12461,11 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
+tiny-invariant@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9"
+ integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==
+
tiny-warning@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
From 3513182e7c3c512e5807a80c2bb707e19a6bc821 Mon Sep 17 00:00:00 2001
From: Paul Chavard
Date: Wed, 5 Jan 2022 11:42:33 +0100
Subject: [PATCH 3/6] a11y(select): use new ComboMultiple component
---
.../experts_procedures/index.html.haml | 13 ++++++------
.../_instructeurs.html.haml | 10 ++++-----
app/views/experts/shared/avis/_form.html.haml | 13 ++++++------
.../dossiers/_envoyer_dossier_block.html.haml | 10 ++++++---
.../instructeurs/procedures/show.html.haml | 11 +++++++---
.../instructeurs/shared/avis/_form.html.haml | 21 ++++++++++---------
6 files changed, 44 insertions(+), 34 deletions(-)
diff --git a/app/views/administrateurs/experts_procedures/index.html.haml b/app/views/administrateurs/experts_procedures/index.html.haml
index 9c3264b72..357010386 100644
--- a/app/views/administrateurs/experts_procedures/index.html.haml
+++ b/app/views/administrateurs/experts_procedures/index.html.haml
@@ -44,14 +44,15 @@
.instructeur-wrapper
%p.notice Pendant l'instruction d’un dossier, les instructeurs peuvent demander leur avis à un ou plusieurs experts.
- %p.notice Entrez les adresses email des experts que vous souhaitez affecter à cette démarche
- - hidden_field_id = SecureRandom.uuid
- = hidden_field_tag :emails, nil, data: { uuid: hidden_field_id }
- = react_component("ComboMultipleDropdownList",
+ %p#experts-emails.notice Entrez les adresses email des experts que vous souhaitez affecter à cette démarche
+ = hidden_field_tag :emails, nil
+ = react_component("ComboMultiple",
options: [],
selected: [], disabled: [],
- hiddenFieldId: hidden_field_id,
- label: 'email expert',
+ group: '.instructeur-wrapper',
+ name: 'emails',
+ label: 'Emails',
+ describedby: 'experts-emails',
acceptNewValues: true)
= f.submit 'Affecter à la démarche', class: 'button primary send'
diff --git a/app/views/administrateurs/groupe_instructeurs/_instructeurs.html.haml b/app/views/administrateurs/groupe_instructeurs/_instructeurs.html.haml
index ea2baa00e..657fb465b 100644
--- a/app/views/administrateurs/groupe_instructeurs/_instructeurs.html.haml
+++ b/app/views/administrateurs/groupe_instructeurs/_instructeurs.html.haml
@@ -5,12 +5,12 @@
.instructeur-wrapper
- if !procedure.routee?
%p.notice Entrez les adresses email des instructeurs que vous souhaitez affecter à cette démarche
- - hidden_field_id = SecureRandom.uuid
- = hidden_field_tag :emails, nil, data: { uuid: hidden_field_id }
- = react_component("ComboMultipleDropdownList",
+ = hidden_field_tag :emails, nil
+ = react_component("ComboMultiple",
options: available_instructeur_emails, selected: [], disabled: [],
- hiddenFieldId: hidden_field_id,
- label: 'email instructeur',
+ group: '.instructeur-wrapper',
+ name: 'emails',
+ label: 'Emails',
acceptNewValues: true)
= f.submit 'Affecter', class: 'button primary send'
diff --git a/app/views/experts/shared/avis/_form.html.haml b/app/views/experts/shared/avis/_form.html.haml
index f4fc339f4..84aa541ad 100644
--- a/app/views/experts/shared/avis/_form.html.haml
+++ b/app/views/experts/shared/avis/_form.html.haml
@@ -4,13 +4,12 @@
%p.avis-notice Les invités pourront consulter le dossier, donner un avis et contribuer au fil de messagerie. Ils ne pourront pas modifier le dossier.
= form_for avis, url: url, html: { class: 'form', data: { persisted_content_id: "expert-ask-avis-for-dossier-#{@avis.dossier.id}" } } do |f|
- - hidden_field_id = SecureRandom.uuid
- = hidden_field_tag 'avis[emails]', nil, data: { uuid: hidden_field_id }
- = react_component("ComboMultipleDropdownList",
- options: [],
- selected: [], disabled: [],
- hiddenFieldId: hidden_field_id,
- label: 'avis_emails',
+ = hidden_field_tag 'avis[emails]', nil
+ = react_component("ComboMultiple",
+ options: [], selected: [], disabled: [],
+ group: '.ask-avis',
+ name: 'emails',
+ label: 'Emails',
acceptNewValues: true)
= f.text_area :introduction, rows: 3, value: avis.introduction || 'Bonjour, merci de me donner votre avis sur ce dossier.', required: true, class: 'persisted-input'
%p.tab-title Ajouter une pièce jointe
diff --git a/app/views/instructeurs/dossiers/_envoyer_dossier_block.html.haml b/app/views/instructeurs/dossiers/_envoyer_dossier_block.html.haml
index 8405c620d..1ab77bf97 100644
--- a/app/views/instructeurs/dossiers/_envoyer_dossier_block.html.haml
+++ b/app/views/instructeurs/dossiers/_envoyer_dossier_block.html.haml
@@ -8,8 +8,12 @@
Le destinataire suivra automatiquement le dossier
= form_for dossier, url: send_to_instructeurs_instructeur_dossier_path(dossier.procedure, dossier), method: :post, html: { class: 'form recipients-form' } do |f|
.flex.justify-start.align-start
- - hidden_field_id = SecureRandom.uuid
- = hidden_field_tag :recipients, nil, data: { uuid: hidden_field_id }
- = react_component("ComboMultipleDropdownList", options: potential_recipients.map{|r| [r.email, r.id]}, selected: [], disabled: [], hiddenFieldId: hidden_field_id, label: "email instructeur")
+ = hidden_field_tag :recipients, nil
+ = react_component("ComboMultiple",
+ options: potential_recipients.map{|r| [r.email, r.id]},
+ selected: [], disabled: [],
+ group: '.recipients-form',
+ name: 'recipients',
+ label: 'Emails')
= f.submit "Envoyer", class: "button large send gap-left"
diff --git a/app/views/instructeurs/procedures/show.html.haml b/app/views/instructeurs/procedures/show.html.haml
index 9984426ea..4738e7f6e 100644
--- a/app/views/instructeurs/procedures/show.html.haml
+++ b/app/views/instructeurs/procedures/show.html.haml
@@ -96,9 +96,14 @@
Personnaliser
#custom-menu.dropdown-content.fade-in-down
= form_tag update_displayed_fields_instructeur_procedure_path(@procedure), method: :patch, class: 'dropdown-form large columns-form' do
- - hidden_field_id = SecureRandom.uuid
- = hidden_field_tag :values, nil, data: { uuid: hidden_field_id }
- = react_component("ComboMultipleDropdownList", options: @displayed_fields_options, selected: @displayed_fields_selected, disabled: [], hiddenFieldId: hidden_field_id, label: 'colonne')
+ = hidden_field_tag :values, nil
+ = react_component("ComboMultiple",
+ options: @displayed_fields_options,
+ selected: @displayed_fields_selected,
+ disabled: [],
+ label: 'Colonne à afficher',
+ group: '.columns-form',
+ name: 'values')
= submit_tag "Enregistrer", class: 'button'
diff --git a/app/views/instructeurs/shared/avis/_form.html.haml b/app/views/instructeurs/shared/avis/_form.html.haml
index 9764a7cba..b82396296 100644
--- a/app/views/instructeurs/shared/avis/_form.html.haml
+++ b/app/views/instructeurs/shared/avis/_form.html.haml
@@ -2,20 +2,21 @@
%h1.tab-title Inviter des personnes à donner leur avis
%p.avis-notice Les invités pourront consulter le dossier, donner un avis et contribuer au fil de messagerie. Ils ne pourront pas modifier le dossier.
- if @dossier.procedure.experts_require_administrateur_invitation
- %p.avis-notice Choisissez des experts à qui vous souhaitez demander un avis parmi la liste prédéfinie par les administrateurs de la démarche
+ %p#avis-emails-description.avis-notice
+ Choisissez des experts à qui vous souhaitez demander un avis parmi la liste prédéfinie par les administrateurs de la démarche
- else
- %p.avis-notice Entrez les adresses email des experts à qui vous souhaitez demander un avis
+ %p#avis-emails-description.avis-notice
+ Entrez les adresses email des experts à qui vous souhaitez demander un avis
= form_for avis, url: url, html: { class: 'form', data: { persisted_content_id: "instructeur-ask-avis-for-dossier-#{@dossier.id}" } } do |f|
- - hidden_field_id = SecureRandom.uuid
- = hidden_field_tag 'avis[emails]', nil, data: { uuid: hidden_field_id }
- = react_component("ComboMultipleDropdownList",
+ = hidden_field_tag 'avis[emails]', nil
+ = react_component("ComboMultiple",
options: @dossier.procedure.experts_require_administrateur_invitation ? @experts_emails : [],
- selected: [],
- disabled: [],
- hiddenFieldId: hidden_field_id,
- label: 'avis_emails',
- id: 'avis_emails',
+ selected: [], disabled: [],
+ label: 'Emails',
+ group: '.ask-avis',
+ name: 'emails',
+ describedby: 'avis-emails-description',
acceptNewValues: !@dossier.procedure.experts_require_administrateur_invitation)
= f.text_area :introduction, rows: 3, value: avis.introduction || 'Bonjour, merci de me donner votre avis sur ce dossier.', required: true, class: "persisted-input"
%p.tab-title Ajouter une pièce jointe
From 28c176370161039666287529b42309e7fbc1e6e9 Mon Sep 17 00:00:00 2001
From: Paul Chavard
Date: Wed, 5 Jan 2022 11:44:07 +0100
Subject: [PATCH 4/6] a11y(champs): generalize describedby and update to use
new Combo props
---
app/views/shared/attachment/_edit.html.haml | 3 +++
.../editable_champs/_address.html.haml | 10 +++++----
.../_annuaire_education.html.haml | 10 +++++----
.../editable_champs/_champ_label.html.haml | 4 ++--
.../editable_champs/_checkbox.html.haml | 2 +-
.../dossiers/editable_champs/_cnaf.html.haml | 4 ++--
.../editable_champs/_communes.html.haml | 14 +++++++-----
.../dossiers/editable_champs/_date.html.haml | 2 ++
.../editable_champs/_datetime.html.haml | 2 +-
.../editable_champs/_decimal_number.html.haml | 2 ++
.../editable_champs/_departements.html.haml | 10 +++++----
.../dossiers/editable_champs/_dgfip.html.haml | 4 ++--
.../editable_champs/_dossier_link.html.haml | 2 ++
.../editable_champs/_drop_down_list.html.haml | 2 +-
.../editable_champs/_editable_champ.html.haml | 2 +-
.../dossiers/editable_champs/_email.html.haml | 2 ++
.../editable_champs/_engagement.html.haml | 2 +-
.../dossiers/editable_champs/_iban.html.haml | 3 ++-
.../editable_champs/_integer_number.html.haml | 2 ++
.../_linked_drop_down_list.html.haml | 22 +++++++++----------
.../dossiers/editable_champs/_mesri.html.haml | 2 +-
.../_multiple_drop_down_list.html.haml | 13 +++++++----
.../editable_champs/_number.html.haml | 2 ++
.../dossiers/editable_champs/_pays.html.haml | 10 +++++----
.../dossiers/editable_champs/_phone.html.haml | 2 ++
.../editable_champs/_pole_emploi.html.haml | 2 +-
.../editable_champs/_regions.html.haml | 10 +++++----
.../dossiers/editable_champs/_siret.html.haml | 2 ++
.../dossiers/editable_champs/_text.html.haml | 3 ++-
.../editable_champs/_textarea.html.haml | 2 ++
.../piece_justificative_controller_spec.rb | 2 +-
31 files changed, 97 insertions(+), 57 deletions(-)
diff --git a/app/views/shared/attachment/_edit.html.haml b/app/views/shared/attachment/_edit.html.haml
index 54ec64236..d61f555b9 100644
--- a/app/views/shared/attachment/_edit.html.haml
+++ b/app/views/shared/attachment/_edit.html.haml
@@ -6,6 +6,7 @@
- accept = defined?(accept) ? accept : nil
- user_can_destroy = defined?(user_can_destroy) ? user_can_destroy : false
- direct_upload = direct_upload != nil ? false : true
+- champ = form.object.is_a?(Champ) ? form.object : nil
.attachment
- if defined?(template) && template.attached?
@@ -36,4 +37,6 @@
class: "attachment-input attachment-input-#{attachment_id} #{'hidden' if persisted}",
accept: accept,
direct_upload: direct_upload,
+ id: champ&.input_id,
+ aria: { describedby: champ&.describedby_id },
data: { 'auto-attach-url': auto_attach_url(form, form.object) }
diff --git a/app/views/shared/dossiers/editable_champs/_address.html.haml b/app/views/shared/dossiers/editable_champs/_address.html.haml
index 4d3be0a26..10cb637b3 100644
--- a/app/views/shared/dossiers/editable_champs/_address.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_address.html.haml
@@ -1,4 +1,6 @@
-- hidden_field_id = SecureRandom.uuid
-= form.hidden_field :value, { data: { uuid: hidden_field_id } }
-= form.hidden_field :external_id, { data: { reference: true } }
-= react_component("ComboAdresseSearch", mandatory: champ.mandatory?, hiddenFieldId: hidden_field_id)
+= form.hidden_field :value
+= form.hidden_field :external_id
+= react_component("ComboAdresseSearch",
+ required: champ.mandatory?,
+ id: champ.input_id,
+ describedby: champ.describedby_id)
diff --git a/app/views/shared/dossiers/editable_champs/_annuaire_education.html.haml b/app/views/shared/dossiers/editable_champs/_annuaire_education.html.haml
index d550752b5..4c59e2ffe 100644
--- a/app/views/shared/dossiers/editable_champs/_annuaire_education.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_annuaire_education.html.haml
@@ -1,4 +1,6 @@
-- hidden_field_id = SecureRandom.uuid
-= form.hidden_field :value, { data: { uuid: hidden_field_id } }
-= form.hidden_field :external_id, { data: { reference: true } }
-= react_component("ComboAnnuaireEducationSearch", mandatory: champ.mandatory?, hiddenFieldId: hidden_field_id )
+= form.hidden_field :value
+= form.hidden_field :external_id
+= react_component("ComboAnnuaireEducationSearch",
+ required: champ.mandatory?,
+ id: champ.input_id,
+ describedby: champ.describedby_id)
diff --git a/app/views/shared/dossiers/editable_champs/_champ_label.html.haml b/app/views/shared/dossiers/editable_champs/_champ_label.html.haml
index fc341a5d2..6258a7bcf 100644
--- a/app/views/shared/dossiers/editable_champs/_champ_label.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_champ_label.html.haml
@@ -1,10 +1,10 @@
= # we do this trick because some html elements should use 'label' and some should be plain paragraphs
- if champ.html_label?
- = form.label champ.main_value_name do
+ = form.label champ.main_value_name, id: champ.labelledby_id, for: champ.input_id do
= render partial: 'shared/dossiers/editable_champs/champ_label_content', locals: { champ: champ, seen_at: seen_at }
- else
.form-label.mb-4
= render partial: 'shared/dossiers/editable_champs/champ_label_content', locals: { champ: champ, seen_at: seen_at }
- if champ.description.present?
- .notice{ id: describedby_id(champ) }= string_to_html(champ.description)
+ .notice{ id: champ.describedby_id }= string_to_html(champ.description)
diff --git a/app/views/shared/dossiers/editable_champs/_checkbox.html.haml b/app/views/shared/dossiers/editable_champs/_checkbox.html.haml
index 707b2adcd..ca8743947 100644
--- a/app/views/shared/dossiers/editable_champs/_checkbox.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_checkbox.html.haml
@@ -1,4 +1,4 @@
= form.check_box :value,
- { required: champ.mandatory? },
+ { required: champ.mandatory?, id: champ.input_id, aria: { describedby: champ.describedby_id } },
'on',
'off'
diff --git a/app/views/shared/dossiers/editable_champs/_cnaf.html.haml b/app/views/shared/dossiers/editable_champs/_cnaf.html.haml
index 1d8d90610..455a7e516 100644
--- a/app/views/shared/dossiers/editable_champs/_cnaf.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_cnaf.html.haml
@@ -5,7 +5,7 @@
= form.text_field :numero_allocataire,
required: champ.mandatory?,
size: 7,
- aria: { describedby: describedby_id(champ) }
+ aria: { describedby: champ.describedby_id }
%div
= form.label :code_postal, t('.code_postal_label')
@@ -13,4 +13,4 @@
= form.text_field :code_postal,
size: 5,
required: champ.mandatory?,
- aria: { describedby: describedby_id(champ) }
+ aria: { describedby: champ.describedby_id }
diff --git a/app/views/shared/dossiers/editable_champs/_communes.html.haml b/app/views/shared/dossiers/editable_champs/_communes.html.haml
index 0e43f37b6..ca33da594 100644
--- a/app/views/shared/dossiers/editable_champs/_communes.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_communes.html.haml
@@ -1,6 +1,8 @@
-- hidden_field_id = SecureRandom.uuid
-= form.hidden_field :value, { data: { uuid: hidden_field_id } }
-= form.hidden_field :external_id, { data: { reference: true } }
-= form.hidden_field :departement, { data: { attr: "#{hidden_field_id}:departement" } }
-= form.hidden_field :code_departement, { data: { attr: "#{hidden_field_id}:code_departement" } }
-= react_component("ComboCommunesSearch", mandatory: champ.mandatory?, hiddenFieldId: hidden_field_id)
+= form.hidden_field :value
+= form.hidden_field :external_id
+= form.hidden_field :departement
+= form.hidden_field :code_departement
+= react_component("ComboCommunesSearch",
+ required: champ.mandatory?,
+ id: champ.input_id,
+ describedby: champ.describedby_id)
diff --git a/app/views/shared/dossiers/editable_champs/_date.html.haml b/app/views/shared/dossiers/editable_champs/_date.html.haml
index 1f0c344d2..7ca761d22 100644
--- a/app/views/shared/dossiers/editable_champs/_date.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_date.html.haml
@@ -1,4 +1,6 @@
= form.date_field :value,
+ id: champ.input_id,
+ aria: { describedby: champ.describedby_id },
value: champ.value,
required: champ.mandatory?,
placeholder: 'aaaa-mm-jj'
diff --git a/app/views/shared/dossiers/editable_champs/_datetime.html.haml b/app/views/shared/dossiers/editable_champs/_datetime.html.haml
index 3283633f9..3c1756dae 100644
--- a/app/views/shared/dossiers/editable_champs/_datetime.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_datetime.html.haml
@@ -1,4 +1,4 @@
- parsed_value = champ.value.present? ? Time.zone.parse(champ.value) : nil
.datetime
- = form.datetime_select(:value, selected: parsed_value, start_year: datetime_start_year(parsed_value), end_year: Date.today.year + 50, minute_step: 5, include_blank: true)
+ = form.datetime_select(:value, id: champ.input_id, aria: { describedby: champ.describedby_id }, selected: parsed_value, start_year: datetime_start_year(parsed_value), end_year: Date.today.year + 50, minute_step: 5, include_blank: true)
diff --git a/app/views/shared/dossiers/editable_champs/_decimal_number.html.haml b/app/views/shared/dossiers/editable_champs/_decimal_number.html.haml
index 6e7aa3c8a..34a27270f 100644
--- a/app/views/shared/dossiers/editable_champs/_decimal_number.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_decimal_number.html.haml
@@ -1,4 +1,6 @@
= form.number_field :value,
+ id: champ.input_id,
+ aria: { describedby: champ.describedby_id },
step: :any,
placeholder: champ.libelle,
required: champ.mandatory?
diff --git a/app/views/shared/dossiers/editable_champs/_departements.html.haml b/app/views/shared/dossiers/editable_champs/_departements.html.haml
index a062f3671..f91cd543b 100644
--- a/app/views/shared/dossiers/editable_champs/_departements.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_departements.html.haml
@@ -1,4 +1,6 @@
-- hidden_field_id = SecureRandom.uuid
-= form.hidden_field :value, { data: { uuid: hidden_field_id } }
-= form.hidden_field :external_id, { data: { reference: true } }
-= react_component("ComboDepartementsSearch", mandatory: champ.mandatory?, hiddenFieldId: hidden_field_id)
+= form.hidden_field :value
+= form.hidden_field :external_id
+= react_component("ComboDepartementsSearch",
+ required: champ.mandatory?,
+ id: champ.input_id,
+ describedby: champ.describedby_id)
diff --git a/app/views/shared/dossiers/editable_champs/_dgfip.html.haml b/app/views/shared/dossiers/editable_champs/_dgfip.html.haml
index f94edf0d3..47ecd18b2 100644
--- a/app/views/shared/dossiers/editable_champs/_dgfip.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_dgfip.html.haml
@@ -5,7 +5,7 @@
= form.text_field :numero_fiscal,
required: champ.mandatory?,
size: 14,
- aria: { describedby: describedby_id(champ) }
+ aria: { describedby: champ.describedby_id }
%div
= form.label :reference_avis, t('.reference_avis_label')
@@ -13,4 +13,4 @@
= form.text_field :reference_avis,
size: 14,
required: champ.mandatory?,
- aria: { describedby: describedby_id(champ) }
+ aria: { describedby: champ.describedby_id }
diff --git a/app/views/shared/dossiers/editable_champs/_dossier_link.html.haml b/app/views/shared/dossiers/editable_champs/_dossier_link.html.haml
index 37e67954c..2ac650300 100644
--- a/app/views/shared/dossiers/editable_champs/_dossier_link.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_dossier_link.html.haml
@@ -1,5 +1,7 @@
.dossier-link{ class: "dossier-link-#{form.index}" }
= form.number_field :value,
+ id: champ.input_id,
+ aria: { describedby: champ.describedby_id },
placeholder: "Numéro de dossier",
autocomplete: 'off',
required: champ.mandatory?,
diff --git a/app/views/shared/dossiers/editable_champs/_drop_down_list.html.haml b/app/views/shared/dossiers/editable_champs/_drop_down_list.html.haml
index ec7f81ee7..d374cd0cb 100644
--- a/app/views/shared/dossiers/editable_champs/_drop_down_list.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_drop_down_list.html.haml
@@ -16,7 +16,7 @@
= form.radio_button :value, Champs::DropDownListChamp::OTHER, checked: champ.other_value_present?
Autre
- else
- = form.select :value, champ.options, selected: champ.selected, required: champ.mandatory?
+ = form.select :value, champ.options, { selected: champ.selected}, required: champ.mandatory?, id: champ.input_id, aria: { describedby: champ.describedby_id }
- if champ.drop_down_other?
= render partial: "shared/dossiers/drop_down_other_input", locals: { form: form, champ: champ }
diff --git a/app/views/shared/dossiers/editable_champs/_editable_champ.html.haml b/app/views/shared/dossiers/editable_champs/_editable_champ.html.haml
index bb5100ae1..c449fd4ba 100644
--- a/app/views/shared/dossiers/editable_champs/_editable_champ.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_editable_champ.html.haml
@@ -1,4 +1,4 @@
-.editable-champ{ class: "editable-champ-#{champ.type_champ}", data: { 'champ-id': champ.id } }
+.editable-champ{ class: "editable-champ-#{champ.type_champ}", id: champ.input_group_id }
- if champ.repetition?
%h3.header-subsection= champ.libelle
- if champ.description.present?
diff --git a/app/views/shared/dossiers/editable_champs/_email.html.haml b/app/views/shared/dossiers/editable_champs/_email.html.haml
index ce589db77..f226889e5 100644
--- a/app/views/shared/dossiers/editable_champs/_email.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_email.html.haml
@@ -1,3 +1,5 @@
= form.email_field :value,
+ id: champ.input_id,
+ aria: { describedby: champ.describedby_id },
placeholder: champ.libelle,
required: champ.mandatory?
diff --git a/app/views/shared/dossiers/editable_champs/_engagement.html.haml b/app/views/shared/dossiers/editable_champs/_engagement.html.haml
index 707b2adcd..ca8743947 100644
--- a/app/views/shared/dossiers/editable_champs/_engagement.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_engagement.html.haml
@@ -1,4 +1,4 @@
= form.check_box :value,
- { required: champ.mandatory? },
+ { required: champ.mandatory?, id: champ.input_id, aria: { describedby: champ.describedby_id } },
'on',
'off'
diff --git a/app/views/shared/dossiers/editable_champs/_iban.html.haml b/app/views/shared/dossiers/editable_champs/_iban.html.haml
index 149b436db..04bf7d9f6 100644
--- a/app/views/shared/dossiers/editable_champs/_iban.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_iban.html.haml
@@ -1,4 +1,5 @@
= form.text_field :value,
+ id: champ.input_id,
placeholder: "27 caractères au format FR7630006000011234567890189",
required: champ.mandatory?,
- aria: { describedby: describedby_id(champ) }
+ aria: { describedby: champ.describedby_id }
diff --git a/app/views/shared/dossiers/editable_champs/_integer_number.html.haml b/app/views/shared/dossiers/editable_champs/_integer_number.html.haml
index d5aab710a..97b45497b 100644
--- a/app/views/shared/dossiers/editable_champs/_integer_number.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_integer_number.html.haml
@@ -1,3 +1,5 @@
= form.number_field :value,
+ id: champ.input_id,
+ aria: { describedby: champ.describedby_id },
placeholder: champ.libelle,
required: champ.mandatory?
diff --git a/app/views/shared/dossiers/editable_champs/_linked_drop_down_list.html.haml b/app/views/shared/dossiers/editable_champs/_linked_drop_down_list.html.haml
index 0f88b773e..aa3531c10 100644
--- a/app/views/shared/dossiers/editable_champs/_linked_drop_down_list.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_linked_drop_down_list.html.haml
@@ -1,16 +1,16 @@
- if champ.options?
= form.select :primary_value,
champ.primary_options,
- { required: champ.mandatory? },
- { data: { secondary_options: champ.secondary_options } }
- %span
- = form.label :secondary_value do
- = champ.drop_down_secondary_libelle.presence || "Valeur secondaire dépendant de la première"
- - if champ.mandatory?
- %span.mandatory *
- - if champ.drop_down_secondary_description.present?
- .notice= string_to_html(champ.drop_down_secondary_description)
+ {},
+ { data: { secondary_options: champ.secondary_options }, required: champ.mandatory?, id: champ.input_id, aria: { describedby: champ.describedby_id } }
+
+ = form.label :secondary_value, for: "#{champ.input_id}-secondary" do
+ = champ.drop_down_secondary_libelle.presence || "Valeur secondaire dépendant de la première"
+ - if champ.mandatory?
+ %span.mandatory *
+ - if champ.drop_down_secondary_description.present?
+ .notice{ id: "#{champ.describedby_id}-secondary" }= string_to_html(champ.drop_down_secondary_description)
= form.select :secondary_value,
champ.secondary_options[champ.primary_value],
- { required: champ.mandatory? },
- { data: { secondary: true } }
+ {},
+ { data: { secondary: true }, required: champ.mandatory?, id: "#{champ.input_id}-secondary", aria: { describedby: "#{champ.describedby_id}-secondary" } }
diff --git a/app/views/shared/dossiers/editable_champs/_mesri.html.haml b/app/views/shared/dossiers/editable_champs/_mesri.html.haml
index cf90abcb7..77d9e9453 100644
--- a/app/views/shared/dossiers/editable_champs/_mesri.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_mesri.html.haml
@@ -4,4 +4,4 @@
%p.notice= t('.ine_notice')
= form.text_field :ine,
required: champ.mandatory?,
- aria: { describedby: describedby_id(champ) }
+ aria: { describedby: champ.describedby_id }
diff --git a/app/views/shared/dossiers/editable_champs/_multiple_drop_down_list.html.haml b/app/views/shared/dossiers/editable_champs/_multiple_drop_down_list.html.haml
index d4f5c6788..5db86708c 100644
--- a/app/views/shared/dossiers/editable_champs/_multiple_drop_down_list.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_multiple_drop_down_list.html.haml
@@ -3,10 +3,15 @@
= form.collection_check_boxes(:value, champ.enabled_non_empty_options, :to_s, :to_s) do |b|
.editable-champ.editable-champ-checkbox
= b.label do
- = b.check_box({ multiple: true, checked: champ&.selected_options&.include?(b.value) })
+ = b.check_box({ multiple: true, checked: champ&.selected_options&.include?(b.value), aria: { describedby: champ.describedby_id } })
= b.text
- else
- - hidden_field_id = SecureRandom.uuid
- = form.hidden_field :value, { data: { uuid: hidden_field_id } }
- = react_component("ComboMultipleDropdownList", options: champ.options, selected: champ.selected_options, disabled: champ.disabled_options, hiddenFieldId: hidden_field_id, label: champ.libelle)
+ = form.hidden_field :value
+ = react_component("ComboMultipleDropdownList",
+ options: champ.options,
+ selected: champ.selected_options,
+ disabled: champ.disabled_options,
+ id: champ.input_id,
+ labelledby: champ.labelledby_id,
+ describedby: champ.describedby_id)
diff --git a/app/views/shared/dossiers/editable_champs/_number.html.haml b/app/views/shared/dossiers/editable_champs/_number.html.haml
index d5aab710a..97b45497b 100644
--- a/app/views/shared/dossiers/editable_champs/_number.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_number.html.haml
@@ -1,3 +1,5 @@
= form.number_field :value,
+ id: champ.input_id,
+ aria: { describedby: champ.describedby_id },
placeholder: champ.libelle,
required: champ.mandatory?
diff --git a/app/views/shared/dossiers/editable_champs/_pays.html.haml b/app/views/shared/dossiers/editable_champs/_pays.html.haml
index 241049b46..975cfee94 100644
--- a/app/views/shared/dossiers/editable_champs/_pays.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_pays.html.haml
@@ -1,4 +1,6 @@
-- hidden_field_id = SecureRandom.uuid
-= form.hidden_field :value, { value: champ.localized_value, data: { uuid: hidden_field_id } }
-= form.hidden_field :external_id, { data: { reference: true } }
-= react_component("ComboPaysSearch", mandatory: champ.mandatory?, hiddenFieldId: hidden_field_id)
+= form.hidden_field :value
+= form.hidden_field :external_id
+= react_component("ComboPaysSearch",
+ required: champ.mandatory?,
+ id: champ.input_id,
+ describedby: champ.describedby_id)
diff --git a/app/views/shared/dossiers/editable_champs/_phone.html.haml b/app/views/shared/dossiers/editable_champs/_phone.html.haml
index 888cdb944..289b1407d 100644
--- a/app/views/shared/dossiers/editable_champs/_phone.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_phone.html.haml
@@ -2,6 +2,8 @@
-# very light validation is made client-side
-# stronger validation is made server-side
= form.phone_field :value,
+ id: champ.input_id,
+ aria: { describedby: champ.describedby_id },
placeholder: champ.libelle,
required: champ.mandatory?,
pattern: "[^a-z^A-Z]+"
diff --git a/app/views/shared/dossiers/editable_champs/_pole_emploi.html.haml b/app/views/shared/dossiers/editable_champs/_pole_emploi.html.haml
index c8a6dff46..1da67dcf5 100644
--- a/app/views/shared/dossiers/editable_champs/_pole_emploi.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_pole_emploi.html.haml
@@ -4,4 +4,4 @@
%p.notice= t('.identifiant_notice')
= form.text_field :identifiant,
required: champ.mandatory?,
- aria: { describedby: describedby_id(champ) }
+ aria: { describedby: champ.describedby_id }
diff --git a/app/views/shared/dossiers/editable_champs/_regions.html.haml b/app/views/shared/dossiers/editable_champs/_regions.html.haml
index 72d0d84f9..2dadcf48c 100644
--- a/app/views/shared/dossiers/editable_champs/_regions.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_regions.html.haml
@@ -1,4 +1,6 @@
-- hidden_field_id = SecureRandom.uuid
-= form.hidden_field :value, { data: { uuid: hidden_field_id } }
-= form.hidden_field :external_id, { data: { reference: true } }
-= react_component("ComboRegionsSearch", mandatory: champ.mandatory?, hiddenFieldId: hidden_field_id)
+= form.hidden_field :value
+= form.hidden_field :external_id
+= react_component("ComboRegionsSearch",
+ required: champ.mandatory?,
+ id: champ.input_id,
+ describedby: champ.describedby_id)
diff --git a/app/views/shared/dossiers/editable_champs/_siret.html.haml b/app/views/shared/dossiers/editable_champs/_siret.html.haml
index b62efa342..2fdcb3e9c 100644
--- a/app/views/shared/dossiers/editable_champs/_siret.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_siret.html.haml
@@ -1,4 +1,6 @@
= form.text_field :value,
+ id: champ.input_id,
+ aria: { describedby: champ.describedby_id },
placeholder: champ.libelle,
data: { remote: true, debounce: true, url: champs_siret_path(form.index), params: { champ_id: champ&.id }.to_query, spinner: true },
required: champ.mandatory?,
diff --git a/app/views/shared/dossiers/editable_champs/_text.html.haml b/app/views/shared/dossiers/editable_champs/_text.html.haml
index 3a4fc2db5..ff4aee988 100644
--- a/app/views/shared/dossiers/editable_champs/_text.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_text.html.haml
@@ -1,4 +1,5 @@
= form.text_field :value,
+ id: champ.input_id,
placeholder: champ.libelle,
required: champ.mandatory?,
- aria: { describedby: describedby_id(champ) }
+ aria: { describedby: champ.describedby_id }
diff --git a/app/views/shared/dossiers/editable_champs/_textarea.html.haml b/app/views/shared/dossiers/editable_champs/_textarea.html.haml
index a4666f22e..e1fc24cda 100644
--- a/app/views/shared/dossiers/editable_champs/_textarea.html.haml
+++ b/app/views/shared/dossiers/editable_champs/_textarea.html.haml
@@ -1,4 +1,6 @@
~ form.text_area :value,
+ id: champ.input_id,
+ aria: { describedby: champ.describedby_id },
rows: 6,
required: champ.mandatory?,
value: html_to_string(champ.value)
diff --git a/spec/controllers/champs/piece_justificative_controller_spec.rb b/spec/controllers/champs/piece_justificative_controller_spec.rb
index 5c5fa202c..b279733b7 100644
--- a/spec/controllers/champs/piece_justificative_controller_spec.rb
+++ b/spec/controllers/champs/piece_justificative_controller_spec.rb
@@ -29,7 +29,7 @@ describe Champs::PieceJustificativeController, type: :controller do
it 'renders the attachment template as Javascript' do
subject
expect(response.status).to eq(200)
- expect(response.body).to include("editable-champ[data-champ-id=\"#{champ.id}\"]")
+ expect(response.body).to include("##{champ.input_group_id}")
end
end
From 968384952aade1bfd3058bd511b165d777631849 Mon Sep 17 00:00:00 2001
From: Paul Chavard
Date: Wed, 5 Jan 2022 11:44:49 +0100
Subject: [PATCH 5/6] a11y(capybara): enable use of aria-label in spec
---
spec/support/capybara.rb | 2 ++
1 file changed, 2 insertions(+)
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 912664fe0..dba0047bc 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -35,6 +35,8 @@ Capybara.default_max_wait_time = 2
Capybara.ignore_hidden_elements = false
+Capybara.enable_aria_label = true
+
# Save a snapshot of the HTML page when an integration test fails
Capybara::Screenshot.autosave_on_failure = true
# Keep only the screenshots generated from the last failing test suite
From cb663489160e11d4b9a9300fda311c18a68530d6 Mon Sep 17 00:00:00 2001
From: Paul Chavard
Date: Wed, 5 Jan 2022 11:46:49 +0100
Subject: [PATCH 6/6] a11y(select): cleanup select helpers in specs
---
spec/support/system_helpers.rb | 60 +++++++++----------
.../instructeurs/instructeur_creation_spec.rb | 2 +-
spec/system/instructeurs/instruction_spec.rb | 4 +-
.../instructeurs/procedure_filters_spec.rb | 4 +-
spec/system/routing/full_scenario_spec.rb | 10 ++--
spec/system/users/brouillon_spec.rb | 20 +++----
spec/system/users/linked_dropdown_spec.rb | 19 ++----
7 files changed, 51 insertions(+), 68 deletions(-)
diff --git a/spec/support/system_helpers.rb b/spec/support/system_helpers.rb
index 7c06cec6b..9dcd936f9 100644
--- a/spec/support/system_helpers.rb
+++ b/spec/support/system_helpers.rb
@@ -103,41 +103,27 @@ module SystemHelpers
end
end
- def select_combobox(champ, fill_with, value)
- fill_in champ, with: fill_with
+ def select_combobox(libelle, fill_with, value, check: true)
+ fill_in libelle, with: fill_with
selector = "li[data-option-value=\"#{value}\"]"
find(selector).click
- expect(page).to have_css(selector)
- expect(page).to have_css("[type=\"hidden\"][value=\"#{value}\"]")
+ if check
+ check_selected_value(libelle, with: value)
+ end
end
- def select_multi_combobox(champ, fill_with, value)
- input = find("input[aria-label=\"#{champ}\"")
- input.click
- input.fill_in with: fill_with
- selector = "li[data-option-value=\"#{value}\"]"
- find(selector).click
- check_selected_value(champ, value)
- end
-
- def check_selected_values(champ, values)
- combobox = find(:xpath, "//input[@aria-label=\"#{champ}\"]/ancestor::div[@data-react-class='ComboMultipleDropdownList']")
- hidden_field_id = JSON.parse(combobox["data-react-props"])["hiddenFieldId"]
- hidden_field = find("input[data-uuid=\"#{hidden_field_id}\"]")
- hidden_field_values = JSON.parse(hidden_field.value)
- expect(values.sort).to eq(hidden_field_values.sort)
- end
-
- def check_selected_value(champ, value)
- combobox = find(:xpath, "//input[@aria-label=\"#{champ}\"]/ancestor::div[@data-react-class='ComboMultipleDropdownList']")
- hidden_field_id = JSON.parse(combobox["data-react-props"])["hiddenFieldId"]
- hidden_field = find("input[data-uuid=\"#{hidden_field_id}\"]")
- hidden_field_values = JSON.parse(hidden_field.value)
- expect(hidden_field_values).to include(value)
- end
-
- def have_hidden_field(libelle, with:)
- have_css("##{form_id_for(libelle)}[value=\"#{with}\"]")
+ def check_selected_value(libelle, with:)
+ field = find_hidden_field_for(libelle)
+ value = field.value.starts_with?('[') ? JSON.parse(field.value) : field.value
+ if value.is_a?(Array)
+ if with.is_a?(Array)
+ expect(value.sort).to eq(with.sort)
+ else
+ expect(value).to include(with)
+ end
+ else
+ expect(value).to eq(with)
+ end
end
def log_out
@@ -172,6 +158,18 @@ module SystemHelpers
end
end
end
+
+ def find_hidden_field_for(libelle, name: 'value')
+ find("#{form_group_id_for(libelle)} input[type=\"hidden\"][name$=\"[#{name}]\"]")
+ end
+
+ def form_group_id_for(libelle)
+ "#champ-#{form_id_for(libelle).gsub('-input', '')}"
+ end
+
+ def form_id_for(libelle)
+ find(:xpath, ".//label[contains(text()[normalize-space()], '#{libelle}')]")[:for]
+ end
end
RSpec.configure do |config|
diff --git a/spec/system/instructeurs/instructeur_creation_spec.rb b/spec/system/instructeurs/instructeur_creation_spec.rb
index 400417e28..b3a78c574 100644
--- a/spec/system/instructeurs/instructeur_creation_spec.rb
+++ b/spec/system/instructeurs/instructeur_creation_spec.rb
@@ -9,7 +9,7 @@ describe 'As an instructeur', js: true do
visit admin_procedure_path(procedure)
find('#groupe-instructeurs').click
- find("input[aria-label='email instructeur'").send_keys(instructeur_email, :enter)
+ fill_in 'Emails', with: instructeur_email
perform_enqueued_jobs { click_on 'Affecter' }
expect(page).to have_text("Les instructeurs ont bien été affectés à la démarche")
diff --git a/spec/system/instructeurs/instruction_spec.rb b/spec/system/instructeurs/instruction_spec.rb
index 312d76dbe..556445210 100644
--- a/spec/system/instructeurs/instruction_spec.rb
+++ b/spec/system/instructeurs/instruction_spec.rb
@@ -165,8 +165,8 @@ describe 'Instructing a dossier:', js: true do
click_on 'Personnes impliquées'
- select_multi_combobox('email instructeur', instructeur_2.email, instructeur_2.id)
- select_multi_combobox('email instructeur', instructeur_3.email, instructeur_3.id)
+ select_combobox('Emails', instructeur_2.email, instructeur_2.id, check: false)
+ select_combobox('Emails', instructeur_3.email, instructeur_3.id, check: false)
click_on 'Envoyer'
diff --git a/spec/system/instructeurs/procedure_filters_spec.rb b/spec/system/instructeurs/procedure_filters_spec.rb
index fee58f35a..9253f1d46 100644
--- a/spec/system/instructeurs/procedure_filters_spec.rb
+++ b/spec/system/instructeurs/procedure_filters_spec.rb
@@ -125,13 +125,13 @@ describe "procedure filters" do
def add_column(column_name, column_path)
click_on 'Personnaliser'
- select_multi_combobox('colonne', column_name, column_path)
+ select_combobox('Colonne à afficher', column_name, column_path, check: false)
click_button "Enregistrer"
end
def remove_column(column_name)
click_on 'Personnaliser'
- find(:xpath, ".//li[contains(text(), \"#{column_name}\")]/button", text: 'Désélectionner').click
+ click_button column_name
find("body").native.send_key("Escape")
click_button "Enregistrer"
end
diff --git a/spec/system/routing/full_scenario_spec.rb b/spec/system/routing/full_scenario_spec.rb
index cc5bb2837..30778b0c6 100644
--- a/spec/system/routing/full_scenario_spec.rb
+++ b/spec/system/routing/full_scenario_spec.rb
@@ -30,14 +30,14 @@ describe 'The routing', js: true do
expect(page).to have_field('Nom du groupe', with: 'littéraire')
# add victor to littéraire groupe
- find("input[aria-label='email instructeur'").send_keys('victor@inst.com', :enter)
+ fill_in 'Emails', with: 'victor@inst.com'
perform_enqueued_jobs { click_on 'Affecter' }
expect(page).to have_text("L’instructeur victor@inst.com a été affecté au groupe « littéraire »")
victor = User.find_by(email: 'victor@inst.com').instructeur
# add superwoman to littéraire groupe
- find("input[aria-label='email instructeur'").send_keys('superwoman@inst.com', :enter)
+ fill_in 'Emails', with: 'superwoman@inst.com'
perform_enqueued_jobs { click_on 'Affecter' }
expect(page).to have_text("L’instructeur superwoman@inst.com a été affecté au groupe « littéraire »")
@@ -50,14 +50,14 @@ describe 'The routing', js: true do
expect(page).to have_text('Le groupe d’instructeurs « scientifique » a été créé.')
# add marie to scientifique groupe
- find("input[aria-label='email instructeur'").send_keys('marie@inst.com', :enter)
+ fill_in 'Emails', with: 'marie@inst.com'
perform_enqueued_jobs { click_on 'Affecter' }
expect(page).to have_text("L’instructeur marie@inst.com a été affecté")
marie = User.find_by(email: 'marie@inst.com').instructeur
# add superwoman to scientifique groupe
- find("input[aria-label='email instructeur'").send_keys('superwoman@inst.com', :enter)
+ fill_in 'Emails', with: 'superwoman@inst.com'
perform_enqueued_jobs { click_on 'Affecter' }
expect(page).to have_text("L’instructeur superwoman@inst.com a été affecté")
@@ -112,7 +112,7 @@ describe 'The routing', js: true do
click_on litteraire_user.dossiers.first.id.to_s
click_on 'Modifier mon dossier'
- fill_in 'dossier_champs_attributes_0_value', with: 'some value'
+ fill_in litteraire_user.dossiers.first.champs.first.libelle, with: 'some value'
click_on 'Enregistrer les modifications du dossier'
log_out
diff --git a/spec/system/users/brouillon_spec.rb b/spec/system/users/brouillon_spec.rb
index aa07cc222..58eb565b7 100644
--- a/spec/system/users/brouillon_spec.rb
+++ b/spec/system/users/brouillon_spec.rb
@@ -28,13 +28,13 @@ describe 'The user' do
check('val1')
check('val3')
select('bravo', from: form_id_for('simple_choice_drop_down_list_long'))
- select_multi_combobox('multiple_choice_drop_down_list_long', 'alp', 'alpha')
- select_multi_combobox('multiple_choice_drop_down_list_long', 'cha', 'charly')
+ select_combobox('multiple_choice_drop_down_list_long', 'alp', 'alpha')
+ select_combobox('multiple_choice_drop_down_list_long', 'cha', 'charly')
select_combobox('pays', 'aust', 'Australie')
select_combobox('regions', 'Ma', 'Martinique')
select_combobox('departements', 'Ai', '02 - Aisne')
- select_combobox('communes', 'Ai', '02 - Aisne')
+ select_combobox('communes', 'Ai', '02 - Aisne', check: false)
select_combobox('communes', 'Ambl', 'Ambléon (01300)')
check('engagement')
@@ -87,11 +87,11 @@ describe 'The user' do
expect(page).to have_checked_field('val1')
expect(page).to have_checked_field('val3')
expect(page).to have_selected_value('simple_choice_drop_down_list_long', selected: 'bravo')
- check_selected_values('multiple_choice_drop_down_list_long', ['alpha', 'charly'])
- expect(page).to have_hidden_field('pays', with: 'Australie')
- expect(page).to have_hidden_field('regions', with: 'Martinique')
- expect(page).to have_hidden_field('departements', with: '02 - Aisne')
- expect(page).to have_hidden_field('communes', with: 'Ambléon (01300)')
+ check_selected_value('multiple_choice_drop_down_list_long', with: ['alpha', 'charly'])
+ check_selected_value('pays', with: 'Australie')
+ check_selected_value('regions', with: 'Martinique')
+ check_selected_value('departements', with: '02 - Aisne')
+ check_selected_value('communes', with: 'Ambléon (01300)')
expect(page).to have_checked_field('engagement')
expect(page).to have_field('dossier_link', with: '123')
expect(page).to have_text('file.pdf')
@@ -337,10 +337,6 @@ describe 'The user' do
expect(page).to have_current_path(identite_dossier_path(user_dossier))
end
- def form_id_for(libelle)
- find(:xpath, ".//label[contains(text()[normalize-space()], '#{libelle}')]")[:for]
- end
-
def form_id_for_datetime(libelle)
# The HTML for datetime is a bit specific since it has 5 selects, below here is a sample HTML
# So, we want to find the partial id of a datetime (partial because there are 5 ids:
diff --git a/spec/system/users/linked_dropdown_spec.rb b/spec/system/users/linked_dropdown_spec.rb
index c4bf21a92..0ec35ac81 100644
--- a/spec/system/users/linked_dropdown_spec.rb
+++ b/spec/system/users/linked_dropdown_spec.rb
@@ -27,16 +27,16 @@ describe 'linked dropdown lists' do
fill_individual
# Select a primary value
- select('Primary 2', from: primary_id_for('linked dropdown'))
+ select('Primary 2', from: 'linked dropdown')
# Secondary menu reflects chosen primary value
- expect(page).to have_select(secondary_id_for('linked dropdown'), options: ['', 'Secondary 2.1', 'Secondary 2.2', 'Secondary 2.3'])
+ expect(page).to have_select("Valeur secondaire dépendant de la première", options: ['', 'Secondary 2.1', 'Secondary 2.2', 'Secondary 2.3'])
# Select another primary value
- select('Primary 1', from: primary_id_for('linked dropdown'))
+ select('Primary 1', from: 'linked dropdown')
# Secondary menu gets updated
- expect(page).to have_select(secondary_id_for('linked dropdown'), options: ['', 'Secondary 1.1', 'Secondary 1.2'])
+ expect(page).to have_select("Valeur secondaire dépendant de la première", options: ['', 'Secondary 1.1', 'Secondary 1.2'])
end
private
@@ -63,15 +63,4 @@ describe 'linked dropdown lists' do
click_on 'Continuer'
expect(page).to have_current_path(brouillon_dossier_path(user_dossier))
end
-
- def primary_id_for(libelle)
- find(:xpath, ".//label[contains(text()[normalize-space()], '#{libelle}')]")[:for]
- end
-
- def secondary_id_for(libelle)
- primary_id = primary_id_for(libelle)
- find("\##{primary_id}")
- .ancestor('.editable-champ')
- .find("[data-secondary]")['id']
- end
end