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