Merge pull request #5903 from betagouv/4865-multi-select-accessible
rend accessible le multi-select
This commit is contained in:
commit
62a1716067
26 changed files with 459 additions and 213 deletions
|
@ -5,34 +5,4 @@
|
||||||
.select-instructeurs {
|
.select-instructeurs {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select2-container--default {
|
|
||||||
.select2-selection--multiple {
|
|
||||||
border: solid 1px $border-grey;
|
|
||||||
|
|
||||||
.select2-selection__choice, // scss-lint:disable SelectorFormat
|
|
||||||
.select2-search--inline {
|
|
||||||
padding: $default-spacer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.select2-container--focus {
|
|
||||||
.select2-selection--multiple {
|
|
||||||
border: 1px solid $blue;
|
|
||||||
box-shadow: 0px 0px 2px 1px $blue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-results__option { // scss-lint:disable SelectorFormat
|
|
||||||
padding: $default-spacer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-select2-option {
|
|
||||||
.icon {
|
|
||||||
margin-right: $default-spacer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,5 @@
|
||||||
// = require ./utils
|
// = require ./utils
|
||||||
// = require ./fonts
|
// = require ./fonts
|
||||||
// = require leaflet
|
// = require leaflet
|
||||||
// = require select2
|
|
||||||
// = require_tree .
|
// = require_tree .
|
||||||
// = stub ./print.scss
|
// = stub ./print.scss
|
||||||
|
|
|
@ -281,18 +281,6 @@
|
||||||
.dropdown-form {
|
.dropdown-form {
|
||||||
padding: 2 * $default-spacer;
|
padding: 2 * $default-spacer;
|
||||||
|
|
||||||
.select2-container {
|
|
||||||
margin-bottom: 2 * $default-spacer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-selection {
|
|
||||||
border: 1px solid $border-grey;
|
|
||||||
|
|
||||||
&.select2-selection--multiple {
|
|
||||||
border: 1px solid $border-grey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.large {
|
&.large {
|
||||||
width: 340px;
|
width: 340px;
|
||||||
}
|
}
|
||||||
|
@ -310,10 +298,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.select2-dropdown {
|
|
||||||
border: 1px solid $border-grey;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
color: $blue;
|
color: $blue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -260,8 +260,7 @@
|
||||||
max-width: 180px;
|
max-width: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
select,
|
select {
|
||||||
.select2-selection {
|
|
||||||
// hack found here: https://stackoverflow.com/questions/1895476/how-to-style-a-select-dropdown-with-css-only-without-javascript
|
// hack found here: https://stackoverflow.com/questions/1895476/how-to-style-a-select-dropdown-with-css-only-without-javascript
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
|
@ -305,39 +304,26 @@
|
||||||
border-color: $blue;
|
border-color: $blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select2 {
|
[data-reach-combobox-token-list] {
|
||||||
min-width: 50%;
|
padding: $default-padding;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select2-container {
|
[data-reach-combobox-token] {
|
||||||
display: block;
|
border: solid 1px $border-grey;
|
||||||
margin-bottom: $default-fields-spacer;
|
color: $black;
|
||||||
|
margin-top: $default-padding;
|
||||||
|
margin-bottom: $default-padding;
|
||||||
|
margin-right: 0.5 * $default-padding;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: $default-padding;
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
&.select2-container--focus {
|
[data-reach-combobox-token]:focus {
|
||||||
.select2-selection {
|
background-color: $black;
|
||||||
border-color: $border-grey;
|
color: $white;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-selection--single {
|
|
||||||
min-height: 62px;
|
|
||||||
|
|
||||||
// scss-lint:disable SelectorFormat
|
|
||||||
.select2-selection__arrow {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
// scss-lint:enable
|
|
||||||
}
|
|
||||||
|
|
||||||
// scss-lint:disable SelectorFormat
|
|
||||||
.select2-selection__rendered {
|
|
||||||
padding: $default-padding;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-selection__choice {
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
}
|
|
||||||
// scss-lint:enable
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.editable-champ {
|
.editable-champ {
|
||||||
|
@ -481,11 +467,55 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-react-class="ComboMultipleDropdownList"] {
|
||||||
|
margin-bottom: $default-fields-spacer;
|
||||||
|
|
||||||
|
[data-reach-combobox-input] {
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0.25rem;
|
||||||
|
background-image: image-url("icons/chevron-down");
|
||||||
|
background-size: 14px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 10px center;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-reach-combobox-input]:focus {
|
||||||
|
outline: solid;
|
||||||
|
outline-color: $light-blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-combobox-token-label] {
|
||||||
|
border: 1px solid #CCCCCC;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
[data-reach-combobox-option] {
|
[data-reach-combobox-option] {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
list-style-type: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-reach-combobox-option][aria-selected="true"] {
|
[data-reach-combobox-option][aria-selected="true"] {
|
||||||
background: $light-blue !important;
|
background: $light-blue !important;
|
||||||
color: $white;
|
color: $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-combobox-separator] {
|
||||||
|
font-size: 16px;
|
||||||
|
color: $dark-grey;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-combobox-remove-token] {
|
||||||
|
color: $dark-grey;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[data-reach-combobox-input]:focus {
|
||||||
|
outline-color: $light-blue;
|
||||||
|
}
|
||||||
|
|
|
@ -1,14 +1,50 @@
|
||||||
|
@import "constants";
|
||||||
|
@import "colors";
|
||||||
|
|
||||||
.personnes-impliquees {
|
.personnes-impliquees {
|
||||||
padding-bottom: 50px;
|
padding-bottom: 50px;
|
||||||
|
|
||||||
ul {
|
ul.tab-list {
|
||||||
list-style-type: disc;
|
list-style-type: disc;
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// scss-lint:disable SelectorFormat
|
[data-react-class="ComboMultipleDropdownList"] {
|
||||||
.form .select2-container .select2-selection__rendered {
|
margin-bottom: $default-fields-spacer;
|
||||||
padding: 12px;
|
|
||||||
|
[data-reach-combobox-token-list] {
|
||||||
|
padding: 0.5 * $default-padding;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-reach-combobox-token] {
|
||||||
|
border: solid 1px $border-grey;
|
||||||
|
color: $black;
|
||||||
|
margin-top: 0.5 * $default-padding;
|
||||||
|
margin-bottom: 0.5 * $default-padding;
|
||||||
|
margin-right: 0.5 * $default-padding;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.5 * $default-padding;
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-reach-combobox-token]:focus {
|
||||||
|
background-color: $black;
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[data-reach-combobox-input] {
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-reach-combobox-input]:focus {
|
||||||
|
outline: solid;
|
||||||
|
outline-color: $light-blue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// scss-lint:enable
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,4 +61,42 @@
|
||||||
margin-bottom: 3 * $default-spacer;
|
margin-bottom: 3 * $default-spacer;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-react-class="ComboMultipleDropdownList"] {
|
||||||
|
margin-bottom: $default-fields-spacer;
|
||||||
|
|
||||||
|
[data-reach-combobox-token-list] {
|
||||||
|
padding: 0.25 * $default-padding;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-reach-combobox-token] {
|
||||||
|
border: solid 1px $border-grey;
|
||||||
|
color: $black;
|
||||||
|
margin: 0.25 * $default-padding;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 0.25 * $default-padding;
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-reach-combobox-token]:focus {
|
||||||
|
background-color: $black;
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[data-reach-combobox-input] {
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-reach-combobox-input]:focus {
|
||||||
|
outline: solid;
|
||||||
|
outline-color: $light-blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,7 @@ module Instructeurs
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_to_instructeurs
|
def send_to_instructeurs
|
||||||
recipients = Instructeur.find(params[:recipients])
|
recipients = Instructeur.find(JSON.parse(params[:recipients]))
|
||||||
|
|
||||||
recipients.each do |recipient|
|
recipients.each do |recipient|
|
||||||
recipient.follow(dossier)
|
recipient.follow(dossier)
|
||||||
|
|
|
@ -138,7 +138,7 @@ module Instructeurs
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_displayed_fields
|
def update_displayed_fields
|
||||||
procedure_presentation.update_displayed_fields(params[:values])
|
procedure_presentation.update_displayed_fields(JSON.parse(params[:values]))
|
||||||
|
|
||||||
redirect_back(fallback_location: instructeur_procedure_url(procedure))
|
redirect_back(fallback_location: instructeur_procedure_url(procedure))
|
||||||
end
|
end
|
||||||
|
|
|
@ -80,8 +80,8 @@ module NewAdministrateur
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_instructeur
|
def add_instructeur
|
||||||
emails = params['emails'].presence || []
|
emails = params['emails'].presence || [].to_json
|
||||||
emails = emails.map(&:strip).map(&:downcase)
|
emails = JSON.parse(emails).map(&:strip).map(&:downcase)
|
||||||
|
|
||||||
correct_emails, bad_emails = emails
|
correct_emails, bad_emails = emails
|
||||||
.partition { |email| URI::MailTo::EMAIL_REGEXP.match?(email) }
|
.partition { |email| URI::MailTo::EMAIL_REGEXP.match?(email) }
|
||||||
|
|
250
app/javascript/components/ComboMultipleDropdownList.js
Normal file
250
app/javascript/components/ComboMultipleDropdownList.js
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
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 '@reach/combobox/styles.css';
|
||||||
|
import matchSorter from 'match-sorter';
|
||||||
|
import { fire } from '@utils';
|
||||||
|
|
||||||
|
const Context = createContext();
|
||||||
|
|
||||||
|
function ComboMultipleDropdownList({
|
||||||
|
options,
|
||||||
|
hiddenFieldId,
|
||||||
|
selected,
|
||||||
|
label,
|
||||||
|
acceptNewValues = false
|
||||||
|
}) {
|
||||||
|
if (label == undefined) {
|
||||||
|
label = 'Choisir une option';
|
||||||
|
}
|
||||||
|
if (Array.isArray(options[0]) == false) {
|
||||||
|
options = options.map((o) => [o, o]);
|
||||||
|
}
|
||||||
|
const inputRef = useRef();
|
||||||
|
const [term, setTerm] = useState('');
|
||||||
|
const [selections, setSelections] = useState(selected);
|
||||||
|
const [newValues, setNewValues] = useState([]);
|
||||||
|
const results = useMemo(
|
||||||
|
() =>
|
||||||
|
(term
|
||||||
|
? matchSorter(
|
||||||
|
options.filter((o) => !o[0].startsWith('--')),
|
||||||
|
term
|
||||||
|
)
|
||||||
|
: options
|
||||||
|
).filter((o) => o[0] && !selections.includes(o[1])),
|
||||||
|
[term, selections.join(',')]
|
||||||
|
);
|
||||||
|
const hiddenField = useMemo(
|
||||||
|
() => document.querySelector(`input[data-uuid="${hiddenFieldId}"]`),
|
||||||
|
[hiddenFieldId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = (event) => {
|
||||||
|
setTerm(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyDown = (event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
if (term && options.map((o) => o[0]).includes(term)) {
|
||||||
|
event.preventDefault();
|
||||||
|
return onSelect(term);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
acceptNewValues &&
|
||||||
|
term &&
|
||||||
|
matchSorter(
|
||||||
|
options.map((o) => o[0]),
|
||||||
|
term
|
||||||
|
).length == 0 // ignore when was pressed for selecting popover option
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
setNewValues([...newValues, term]);
|
||||||
|
saveSelection([...selections, term]);
|
||||||
|
setTerm('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveSelection = (selections) => {
|
||||||
|
setSelections(selections);
|
||||||
|
if (hiddenField) {
|
||||||
|
hiddenField.setAttribute('value', JSON.stringify(selections));
|
||||||
|
fire(hiddenField, 'autosave:trigger');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelect = (value) => {
|
||||||
|
let sel = options.find((o) => o[0] == value)[1];
|
||||||
|
saveSelection([...selections, sel]);
|
||||||
|
setTerm('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemove = (value) => {
|
||||||
|
saveSelection(
|
||||||
|
selections.filter((s) =>
|
||||||
|
newValues.includes(value)
|
||||||
|
? s != value
|
||||||
|
: s !== options.find((o) => o[0] == value)[1]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
inputRef.current.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Combobox openOnFocus={true} onSelect={onSelect} aria-label={label}>
|
||||||
|
<ComboboxTokenLabel onRemove={onRemove}>
|
||||||
|
<ul
|
||||||
|
aria-live="polite"
|
||||||
|
aria-atomic={true}
|
||||||
|
data-reach-combobox-token-list
|
||||||
|
>
|
||||||
|
{selections.map((selection) => (
|
||||||
|
<ComboboxToken
|
||||||
|
key={selection}
|
||||||
|
value={
|
||||||
|
newValues.find((newValue) => newValue == selection) ||
|
||||||
|
options.find((o) => o[1] == selection)[0]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<ComboboxInput
|
||||||
|
ref={inputRef}
|
||||||
|
value={term}
|
||||||
|
onChange={handleChange}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
autocomplete={false}
|
||||||
|
/>
|
||||||
|
</ComboboxTokenLabel>
|
||||||
|
{results && (
|
||||||
|
<ComboboxPopover portal={false}>
|
||||||
|
{results.length === 0 && (
|
||||||
|
<p>
|
||||||
|
Aucun résultat{' '}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setTerm('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Effacer
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<ComboboxList>
|
||||||
|
{results.map((value, index) => {
|
||||||
|
if (value[0].startsWith('--')) {
|
||||||
|
return <ComboboxSeparator key={index} value={value[0]} />;
|
||||||
|
}
|
||||||
|
return <ComboboxOption key={index} value={value[0]} />;
|
||||||
|
})}
|
||||||
|
</ComboboxList>
|
||||||
|
</ComboboxPopover>
|
||||||
|
)}
|
||||||
|
</Combobox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ComboboxTokenLabel({ onRemove, ...props }) {
|
||||||
|
const selectionsRef = useRef([]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
selectionsRef.current = [];
|
||||||
|
return () => (selectionsRef.current = []);
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
onRemove,
|
||||||
|
selectionsRef
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Context.Provider value={context}>
|
||||||
|
<div data-combobox-token-label {...props} />
|
||||||
|
</Context.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ComboboxTokenLabel.propTypes = {
|
||||||
|
onRemove: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
function ComboboxSeparator({ value }) {
|
||||||
|
return (
|
||||||
|
<li aria-disabled="true" role="option" data-combobox-separator>
|
||||||
|
{value.slice(2, -2)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ComboboxSeparator.propTypes = {
|
||||||
|
value: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
function ComboboxToken({ value, ...props }) {
|
||||||
|
const { selectionsRef, onRemove } = useContext(Context);
|
||||||
|
useEffect(() => {
|
||||||
|
selectionsRef.current.push(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
data-reach-combobox-token
|
||||||
|
tabIndex="0"
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Backspace') {
|
||||||
|
onRemove(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
role="presentation"
|
||||||
|
data-combobox-remove-token
|
||||||
|
onClick={() => {
|
||||||
|
onRemove(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
x
|
||||||
|
</span>
|
||||||
|
{value}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ComboMultipleDropdownList;
|
5
app/javascript/loaders/ComboMultipleDropdownList.js
Normal file
5
app/javascript/loaders/ComboMultipleDropdownList.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import Loadable from '../components/Loadable';
|
||||||
|
|
||||||
|
export default Loadable(() =>
|
||||||
|
import('../components/ComboMultipleDropdownList')
|
||||||
|
);
|
|
@ -1,80 +0,0 @@
|
||||||
import $ from 'jquery';
|
|
||||||
import 'select2';
|
|
||||||
|
|
||||||
const language = {
|
|
||||||
errorLoading: function () {
|
|
||||||
return 'Les résultats ne peuvent pas être chargés.';
|
|
||||||
},
|
|
||||||
inputTooLong: function (args) {
|
|
||||||
var overChars = args.input.length - args.maximum;
|
|
||||||
|
|
||||||
return 'Supprimez ' + overChars + ' caractère' + (overChars > 1 ? 's' : '');
|
|
||||||
},
|
|
||||||
inputTooShort: function (args) {
|
|
||||||
var remainingChars = args.minimum - args.input.length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
'Saisissez au moins ' +
|
|
||||||
remainingChars +
|
|
||||||
' caractère' +
|
|
||||||
(remainingChars > 1 ? 's' : '')
|
|
||||||
);
|
|
||||||
},
|
|
||||||
loadingMore: function () {
|
|
||||||
return 'Chargement de résultats supplémentaires…';
|
|
||||||
},
|
|
||||||
maximumSelected: function (args) {
|
|
||||||
return (
|
|
||||||
'Vous pouvez seulement sélectionner ' +
|
|
||||||
args.maximum +
|
|
||||||
' élément' +
|
|
||||||
(args.maximum > 1 ? 's' : '')
|
|
||||||
);
|
|
||||||
},
|
|
||||||
noResults: function () {
|
|
||||||
return 'Aucun résultat trouvé';
|
|
||||||
},
|
|
||||||
searching: function () {
|
|
||||||
return 'Recherche en cours…';
|
|
||||||
},
|
|
||||||
removeAllItems: function () {
|
|
||||||
return 'Supprimer tous les éléments';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const baseOptions = {
|
|
||||||
language,
|
|
||||||
width: '100%'
|
|
||||||
};
|
|
||||||
|
|
||||||
const templateOption = ({ text }) =>
|
|
||||||
$(
|
|
||||||
`<span class="custom-select2-option"><span class="icon person"></span>${text}</span>`
|
|
||||||
);
|
|
||||||
|
|
||||||
addEventListener('ds:page:update', () => {
|
|
||||||
$('select.select2').select2(baseOptions);
|
|
||||||
|
|
||||||
$('.columns-form select.select2-limited').select2({
|
|
||||||
width: '300px',
|
|
||||||
placeholder: 'Sélectionnez des colonnes',
|
|
||||||
maximumSelectionLength: '5'
|
|
||||||
});
|
|
||||||
|
|
||||||
$('.recipients-form select.select2-limited').select2({
|
|
||||||
language,
|
|
||||||
width: '300px',
|
|
||||||
placeholder: 'Sélectionnez des instructeurs',
|
|
||||||
maximumSelectionLength: '30'
|
|
||||||
});
|
|
||||||
|
|
||||||
$('select.select2-limited.select-instructeurs').select2({
|
|
||||||
language,
|
|
||||||
dropdownParent: $('.instructeur-wrapper'),
|
|
||||||
placeholder: 'Saisir l’adresse email de l’instructeur',
|
|
||||||
tags: true,
|
|
||||||
tokenSeparators: [',', ' '],
|
|
||||||
templateResult: templateOption,
|
|
||||||
templateSelection: templateOption
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -17,7 +17,6 @@ import '../new_design/dropdown';
|
||||||
import '../new_design/form-validation';
|
import '../new_design/form-validation';
|
||||||
import '../new_design/procedure-context';
|
import '../new_design/procedure-context';
|
||||||
import '../new_design/procedure-form';
|
import '../new_design/procedure-form';
|
||||||
import '../new_design/select2';
|
|
||||||
import '../new_design/spinner';
|
import '../new_design/spinner';
|
||||||
import '../new_design/support';
|
import '../new_design/support';
|
||||||
import '../new_design/dossiers/auto-save';
|
import '../new_design/dossiers/auto-save';
|
||||||
|
|
|
@ -8,9 +8,8 @@
|
||||||
Le destinataire suivra automatiquement le dossier
|
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|
|
= 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
|
.flex.justify-start.align-start
|
||||||
= select_tag(:recipients,
|
- hidden_field_id = SecureRandom.uuid
|
||||||
options_from_collection_for_select(potential_recipients, :id, :email),
|
= hidden_field_tag :recipients, nil, data: { uuid: hidden_field_id }
|
||||||
multiple: true,
|
= react_component("ComboMultipleDropdownList", options: potential_recipients.map{|r| [r.email, r.id]}, selected: [], disabled: [], hiddenFieldId: hidden_field_id, label: "email instructeur")
|
||||||
class: 'select2-limited',
|
|
||||||
placeholder: '')
|
|
||||||
= f.submit "Envoyer", class: "button large send gap-left"
|
= f.submit "Envoyer", class: "button large send gap-left"
|
||||||
|
|
|
@ -122,10 +122,10 @@
|
||||||
Personnaliser
|
Personnaliser
|
||||||
#custom-menu.dropdown-content.fade-in-down
|
#custom-menu.dropdown-content.fade-in-down
|
||||||
= form_tag update_displayed_fields_instructeur_procedure_path(@procedure), method: :patch, class: 'dropdown-form columns-form' do
|
= form_tag update_displayed_fields_instructeur_procedure_path(@procedure), method: :patch, class: 'dropdown-form columns-form' do
|
||||||
= select_tag :values,
|
- hidden_field_id = SecureRandom.uuid
|
||||||
options_for_select(@displayed_fields_options, selected: @displayed_fields_selected),
|
= hidden_field_tag :values, nil, data: { uuid: hidden_field_id }
|
||||||
multiple: true,
|
= react_component("ComboMultipleDropdownList", options: @displayed_fields_options, selected: @displayed_fields_selected, disabled: [], hiddenFieldId: hidden_field_id, label: 'colonne')
|
||||||
class: 'select2-limited'
|
|
||||||
= submit_tag "Enregistrer", class: 'button'
|
= submit_tag "Enregistrer", class: 'button'
|
||||||
|
|
||||||
%tbody
|
%tbody
|
||||||
|
|
|
@ -25,12 +25,15 @@
|
||||||
.instructeur-wrapper
|
.instructeur-wrapper
|
||||||
- if !@procedure.routee?
|
- if !@procedure.routee?
|
||||||
%p.notice Entrez les adresses email des instructeurs que vous souhaitez affecter à cette démarche
|
%p.notice Entrez les adresses email des instructeurs que vous souhaitez affecter à cette démarche
|
||||||
= select_tag :emails,
|
- hidden_field_id = SecureRandom.uuid
|
||||||
options_for_select(@available_instructeur_emails),
|
= hidden_field_tag :emails, nil, data: { uuid: hidden_field_id }
|
||||||
multiple: true,
|
= react_component("ComboMultipleDropdownList",
|
||||||
class: 'select-instructeurs select2-limited'
|
options: @available_instructeur_emails, selected: [], disabled: [],
|
||||||
|
hiddenFieldId: hidden_field_id,
|
||||||
|
label: 'email instructeur',
|
||||||
|
acceptNewValues: true)
|
||||||
|
|
||||||
= f.submit 'Affecter', class: 'button primary send'
|
= f.submit 'Affecter', class: 'button primary send'
|
||||||
|
|
||||||
%table.table.mt-2
|
%table.table.mt-2
|
||||||
%thead
|
%thead
|
||||||
|
|
|
@ -7,10 +7,7 @@
|
||||||
= b.text
|
= b.text
|
||||||
|
|
||||||
- else
|
- else
|
||||||
= form.select :value,
|
- hidden_field_id = SecureRandom.uuid
|
||||||
champ.options,
|
= form.hidden_field :value, { data: { uuid: hidden_field_id } }
|
||||||
{ selected: champ.selected_options,
|
= react_component("ComboMultipleDropdownList", options: champ.options, selected: champ.selected_options, disabled: champ.disabled_options, hiddenFieldId: hidden_field_id, label: champ.libelle)
|
||||||
disabled: champ.disabled_options },
|
|
||||||
multiple: true,
|
|
||||||
class: 'select2'
|
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ describe Instructeurs::DossiersController, type: :controller do
|
||||||
post(
|
post(
|
||||||
:send_to_instructeurs,
|
:send_to_instructeurs,
|
||||||
params: {
|
params: {
|
||||||
recipients: [recipient],
|
recipients: [recipient.id].to_json,
|
||||||
procedure_id: procedure.id,
|
procedure_id: procedure.id,
|
||||||
dossier_id: dossier.id
|
dossier_id: dossier.id
|
||||||
}
|
}
|
||||||
|
|
|
@ -207,11 +207,11 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do
|
||||||
|
|
||||||
describe '#add_instructeur_procedure_non_routee' do
|
describe '#add_instructeur_procedure_non_routee' do
|
||||||
let(:procedure) { create :procedure, administrateur: admin }
|
let(:procedure) { create :procedure, administrateur: admin }
|
||||||
let(:emails) { ['instructeur_3@ministere_a.gouv.fr', 'instructeur_4@ministere_b.gouv.fr'] }
|
let(:emails) { ['instructeur_3@ministere_a.gouv.fr', 'instructeur_4@ministere_b.gouv.fr'].to_json }
|
||||||
subject { post :add_instructeur, params: { emails: emails, procedure_id: procedure.id, id: gi_1_1.id } }
|
subject { post :add_instructeur, params: { emails: emails, procedure_id: procedure.id, id: gi_1_1.id } }
|
||||||
|
|
||||||
context 'when all emails are valid' do
|
context 'when all emails are valid' do
|
||||||
let(:emails) { ['test@b.gouv.fr', 'test2@b.gouv.fr'] }
|
let(:emails) { ['test@b.gouv.fr', 'test2@b.gouv.fr'].to_json }
|
||||||
it { expect(response.status).to eq(200) }
|
it { expect(response.status).to eq(200) }
|
||||||
it { expect(subject.request.flash[:alert]).to be_nil }
|
it { expect(subject.request.flash[:alert]).to be_nil }
|
||||||
it { expect(subject.request.flash[:notice]).to be_present }
|
it { expect(subject.request.flash[:notice]).to be_present }
|
||||||
|
@ -219,7 +219,7 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when there is at least one bad email' do
|
context 'when there is at least one bad email' do
|
||||||
let(:emails) { ['badmail', 'instructeur2@gmail.com'] }
|
let(:emails) { ['badmail', 'instructeur2@gmail.com'].to_json }
|
||||||
it { expect(response.status).to eq(200) }
|
it { expect(response.status).to eq(200) }
|
||||||
it { expect(subject.request.flash[:alert]).to be_present }
|
it { expect(subject.request.flash[:alert]).to be_present }
|
||||||
it { expect(subject.request.flash[:notice]).to be_present }
|
it { expect(subject.request.flash[:notice]).to be_present }
|
||||||
|
@ -227,7 +227,7 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the admin wants to assign an instructor who is already assigned on this procedure' do
|
context 'when the admin wants to assign an instructor who is already assigned on this procedure' do
|
||||||
let(:emails) { ['instructeur_1@ministere_a.gouv.fr'] }
|
let(:emails) { ['instructeur_1@ministere_a.gouv.fr'].to_json }
|
||||||
it { expect(subject.request.flash[:alert]).to be_present }
|
it { expect(subject.request.flash[:alert]).to be_present }
|
||||||
it { expect(subject).to redirect_to admin_procedure_groupe_instructeur_path(procedure, procedure.defaut_groupe_instructeur) }
|
it { expect(subject).to redirect_to admin_procedure_groupe_instructeur_path(procedure, procedure.defaut_groupe_instructeur) }
|
||||||
end
|
end
|
||||||
|
@ -247,7 +247,7 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do
|
||||||
params: {
|
params: {
|
||||||
procedure_id: procedure.id,
|
procedure_id: procedure.id,
|
||||||
id: gi_1_2.id,
|
id: gi_1_2.id,
|
||||||
emails: new_instructeur_emails
|
emails: new_instructeur_emails.to_json
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -281,7 +281,7 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'of an empty string' do
|
context 'of an empty string' do
|
||||||
let(:new_instructeur_emails) { '' }
|
let(:new_instructeur_emails) { [''] }
|
||||||
|
|
||||||
it { expect(response).to redirect_to(admin_procedure_groupe_instructeur_path(procedure, gi_1_2)) }
|
it { expect(response).to redirect_to(admin_procedure_groupe_instructeur_path(procedure, gi_1_2)) }
|
||||||
end
|
end
|
||||||
|
|
|
@ -128,10 +128,8 @@ feature 'Instructing a dossier:' do
|
||||||
|
|
||||||
click_on 'Personnes impliquées'
|
click_on 'Personnes impliquées'
|
||||||
|
|
||||||
first('.select2-container', minimum: 1).click
|
select_multi('email instructeur', instructeur_2.email)
|
||||||
find('li.select2-results__option[role="option"]', text: instructeur_2.email).click
|
select_multi('email instructeur', instructeur_3.email)
|
||||||
first('.select2-container', minimum: 1).click
|
|
||||||
find('li.select2-results__option[role="option"]', text: instructeur_3.email).click
|
|
||||||
|
|
||||||
click_on 'Envoyer'
|
click_on 'Envoyer'
|
||||||
|
|
||||||
|
|
|
@ -125,15 +125,13 @@ feature "procedure filters" do
|
||||||
|
|
||||||
def add_column(column_name)
|
def add_column(column_name)
|
||||||
click_on 'Personnaliser'
|
click_on 'Personnaliser'
|
||||||
find("span.select2-container").click
|
select_multi('colonne', column_name)
|
||||||
find(:xpath, "//li[text()='#{column_name}']").click
|
|
||||||
click_button "Enregistrer"
|
click_button "Enregistrer"
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_column(column_name)
|
def remove_column(column_name)
|
||||||
click_on 'Personnaliser'
|
click_on 'Personnaliser'
|
||||||
find(:xpath, "//li[contains(@title, '#{column_name}')]/span[contains(text(), '×')]").click
|
find(:xpath, "//li[contains(text(), '#{column_name}')]/span[contains(text(), 'x')]").click
|
||||||
find(:xpath, "//form[contains(@class, 'columns-form')]//span[contains(@class, 'select2-container')]").click
|
|
||||||
click_button "Enregistrer"
|
click_button "Enregistrer"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -30,14 +30,14 @@ feature 'The routing', js: true do
|
||||||
expect(page).to have_field('Nom du groupe', with: 'littéraire')
|
expect(page).to have_field('Nom du groupe', with: 'littéraire')
|
||||||
|
|
||||||
# add victor to littéraire groupe
|
# add victor to littéraire groupe
|
||||||
find('input.select2-search__field').send_keys('victor@inst.com', :enter)
|
find("input[aria-label='email instructeur'").send_keys('victor@inst.com', :enter)
|
||||||
perform_enqueued_jobs { click_on 'Affecter' }
|
perform_enqueued_jobs { click_on 'Affecter' }
|
||||||
expect(page).to have_text("Les instructeurs ont bien été affectés à la démarche")
|
expect(page).to have_text("Les instructeurs ont bien été affectés à la démarche")
|
||||||
|
|
||||||
victor = User.find_by(email: 'victor@inst.com').instructeur
|
victor = User.find_by(email: 'victor@inst.com').instructeur
|
||||||
|
|
||||||
# add superwoman to littéraire groupe
|
# add superwoman to littéraire groupe
|
||||||
find('input.select2-search__field').send_keys('superwoman@inst.com', :enter)
|
find("input[aria-label='email instructeur'").send_keys('superwoman@inst.com', :enter)
|
||||||
perform_enqueued_jobs { click_on 'Affecter' }
|
perform_enqueued_jobs { click_on 'Affecter' }
|
||||||
expect(page).to have_text("Les instructeurs ont bien été affectés à la démarche")
|
expect(page).to have_text("Les instructeurs ont bien été affectés à la démarche")
|
||||||
|
|
||||||
|
@ -50,14 +50,14 @@ feature 'The routing', js: true do
|
||||||
expect(page).to have_text('Le groupe d’instructeurs « scientifique » a été créé.')
|
expect(page).to have_text('Le groupe d’instructeurs « scientifique » a été créé.')
|
||||||
|
|
||||||
# add marie to scientifique groupe
|
# add marie to scientifique groupe
|
||||||
find('input.select2-search__field').send_keys('marie@inst.com', :enter)
|
find("input[aria-label='email instructeur'").send_keys('marie@inst.com', :enter)
|
||||||
perform_enqueued_jobs { click_on 'Affecter' }
|
perform_enqueued_jobs { click_on 'Affecter' }
|
||||||
expect(page).to have_text("L’instructeur marie@inst.com a été affecté")
|
expect(page).to have_text("L’instructeur marie@inst.com a été affecté")
|
||||||
|
|
||||||
marie = User.find_by(email: 'marie@inst.com').instructeur
|
marie = User.find_by(email: 'marie@inst.com').instructeur
|
||||||
|
|
||||||
# add superwoman to scientifique groupe
|
# add superwoman to scientifique groupe
|
||||||
find('input.select2-search__field').send_keys('superwoman@inst.com', :enter)
|
find("input[aria-label='email instructeur'").send_keys('superwoman@inst.com', :enter)
|
||||||
perform_enqueued_jobs { click_on 'Affecter' }
|
perform_enqueued_jobs { click_on 'Affecter' }
|
||||||
expect(page).to have_text("L’instructeur superwoman@inst.com a été affecté")
|
expect(page).to have_text("L’instructeur superwoman@inst.com a été affecté")
|
||||||
|
|
||||||
|
|
|
@ -25,8 +25,9 @@ feature 'The user' do
|
||||||
check('val1')
|
check('val1')
|
||||||
check('val3')
|
check('val3')
|
||||||
select('bravo', from: form_id_for('simple_choice_drop_down_list_long'))
|
select('bravo', from: form_id_for('simple_choice_drop_down_list_long'))
|
||||||
select('alpha', from: form_id_for('multiple_choice_drop_down_list_long'))
|
select_multi('multiple_choice_drop_down_list_long', 'alpha')
|
||||||
select('charly', from: form_id_for('multiple_choice_drop_down_list_long'))
|
select_multi('multiple_choice_drop_down_list_long', 'charly')
|
||||||
|
|
||||||
select_champ_geo('pays', 'aust', 'AUSTRALIE')
|
select_champ_geo('pays', 'aust', 'AUSTRALIE')
|
||||||
|
|
||||||
select_champ_geo('regions', 'Ma', 'Martinique')
|
select_champ_geo('regions', 'Ma', 'Martinique')
|
||||||
|
@ -83,7 +84,7 @@ feature 'The user' do
|
||||||
expect(page).to have_checked_field('val1')
|
expect(page).to have_checked_field('val1')
|
||||||
expect(page).to have_checked_field('val3')
|
expect(page).to have_checked_field('val3')
|
||||||
expect(page).to have_selected_value('simple_choice_drop_down_list_long', selected: 'bravo')
|
expect(page).to have_selected_value('simple_choice_drop_down_list_long', selected: 'bravo')
|
||||||
expect(page).to have_selected_value('multiple_choice_drop_down_list_long', selected: ['alpha', 'charly'])
|
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('pays', with: 'AUSTRALIE')
|
||||||
expect(page).to have_hidden_field('regions', with: 'Martinique')
|
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('departements', with: '02 - Aisne')
|
||||||
|
|
|
@ -103,6 +103,25 @@ module FeatureHelpers
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def select_multi(champ, with)
|
||||||
|
input = find("input[aria-label='#{champ}'")
|
||||||
|
input.click
|
||||||
|
|
||||||
|
# hack because for unknown reason, the click on input doesn't show combobox-popover with selenium driver
|
||||||
|
script = "document.evaluate(\"//input[@aria-label='#{champ}']//ancestor::div[@data-reach-combobox]/div[@data-reach-combobox-popover]\", document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null).iterateNext().removeAttribute(\"hidden\")"
|
||||||
|
execute_script(script)
|
||||||
|
|
||||||
|
element = find(:xpath, "//input[@aria-label='#{champ}']/ancestor::div[@data-reach-combobox]//div[@data-reach-combobox-popover]//li/span[normalize-space(text())='#{with}']")
|
||||||
|
element.click
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_selected_values(champ, values)
|
||||||
|
combobox = find(:xpath, "//input[@aria-label='#{champ}']/ancestor::div[@data-react-class='ComboMultipleDropdownList']")
|
||||||
|
hiddenFieldId = JSON.parse(combobox["data-react-props"])["hiddenFieldId"]
|
||||||
|
hiddenField = find("input[data-uuid='#{hiddenFieldId}']")
|
||||||
|
expect(values.sort).to eq(JSON.parse(hiddenField.value).sort)
|
||||||
|
end
|
||||||
|
|
||||||
# Keep the brower window open after a test success of failure, to
|
# Keep the brower window open after a test success of failure, to
|
||||||
# allow inspecting the page or the console.
|
# allow inspecting the page or the console.
|
||||||
#
|
#
|
||||||
|
|
|
@ -13,7 +13,7 @@ describe 'instructeurs/dossiers/envoyer_dossier_block.html.haml', type: :view do
|
||||||
let(:instructeur) { create(:instructeur, email: 'yop@totomail.fr') }
|
let(:instructeur) { create(:instructeur, email: 'yop@totomail.fr') }
|
||||||
let(:potential_recipients) { [instructeur] }
|
let(:potential_recipients) { [instructeur] }
|
||||||
|
|
||||||
it { is_expected.to have_css("select > option[value='#{instructeur.id}']") }
|
it { is_expected.to match(/data-react-props.*#{instructeur.email}/) }
|
||||||
it { is_expected.to have_css(".button.send") }
|
it { is_expected.to have_css(".button.send") }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -82,7 +82,7 @@ describe 'shared/dossiers/edit.html.haml', type: :view do
|
||||||
let(:type_de_champ) { create(:type_de_champ_multiple_drop_down_list, :long, procedure: dossier.procedure) }
|
let(:type_de_champ) { create(:type_de_champ_multiple_drop_down_list, :long, procedure: dossier.procedure) }
|
||||||
|
|
||||||
it 'renders the list as a multiple-selection dropdown' do
|
it 'renders the list as a multiple-selection dropdown' do
|
||||||
expect(subject).to have_selector('select.select2')
|
expect(subject).to have_selector('[data-react-class="ComboMultipleDropdownList"]')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue