Merge pull request #10609 from tchak/champs-selection-multiple-combobox
Utiliser les nouvelles combobox pour les champs de sélection multiple
This commit is contained in:
commit
be83f2c7d1
11 changed files with 80 additions and 66 deletions
|
@ -30,6 +30,8 @@ class EditableChamp::DropDownListComponent < EditableChamp::EditableChampBaseCom
|
||||||
name: @form.field_name(:value),
|
name: @form.field_name(:value),
|
||||||
selected_key: @champ.selected,
|
selected_key: @champ.selected,
|
||||||
items: @champ.enabled_non_empty_options(other: true).map { _1.is_a?(Array) ? _1 : [_1, _1] },
|
items: @champ.enabled_non_empty_options(other: true).map { _1.is_a?(Array) ? _1 : [_1, _1] },
|
||||||
empty_filter_key: @champ.drop_down_other? ? Champs::DropDownListChamp::OTHER : nil)
|
empty_filter_key: @champ.drop_down_other? ? Champs::DropDownListChamp::OTHER : nil,
|
||||||
|
'aria-describedby': @champ.describedby_id,
|
||||||
|
'aria-labelledby': @champ.labelledby_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,7 +9,14 @@ class EditableChamp::MultipleDropDownListComponent < EditableChamp::EditableCham
|
||||||
@champ.render_as_checkboxes? ? :fieldset : :div
|
@champ.render_as_checkboxes? ? :fieldset : :div
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_path(option)
|
def react_props
|
||||||
champs_options_path(@champ.dossier, @champ.stable_id, row_id: @champ.row_id, option:)
|
react_input_opts(id: @champ.input_id,
|
||||||
|
class: 'fr-mt-1w',
|
||||||
|
name: @form.field_name(:value, multiple: true),
|
||||||
|
selected_keys: @champ.selected_options,
|
||||||
|
items: @champ.enabled_non_empty_options,
|
||||||
|
'aria-label': @champ.libelle,
|
||||||
|
'aria-describedby': @champ.describedby_id,
|
||||||
|
'aria-labelledby': @champ.labelledby_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,11 +9,5 @@
|
||||||
= b.text
|
= b.text
|
||||||
|
|
||||||
- else
|
- else
|
||||||
%div{ 'data-turbo-focus-group': true }
|
%react-fragment
|
||||||
- if @champ.selected_options.present?
|
= render ReactComponent.new "ComboBox/MultiComboBox", **react_props
|
||||||
.fr-mb-2w.fr-mt-2w{ "data-turbo": "true" }
|
|
||||||
- @champ.selected_options.each do |option|
|
|
||||||
= render NestedForms::OwnedButtonComponent.new(formaction: update_path(option), http_method: :delete, opt: { aria: {pressed: true }, class: 'fr-tag fr-tag-bug fr-mb-1w fr-mr-1w', id: @champ.checkbox_id(option) }) do
|
|
||||||
= option
|
|
||||||
- if @champ.unselected_options.present?
|
|
||||||
= @form.select :value, @champ.unselected_options, { selected: '', include_blank: false, prompt: t('.prompt') }, id: @champ.input_id, aria: { describedby: @champ.describedby_id }, class: 'fr-select fr-mt-2v'
|
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
class Champs::OptionsController < Champs::ChampController
|
|
||||||
include TurboChampsConcern
|
|
||||||
|
|
||||||
def remove
|
|
||||||
@champ.remove_option([params[:option]].compact, true)
|
|
||||||
@dossier = @champ.private? ? nil : @champ.dossier
|
|
||||||
champs_attributes = { @champ.public_id => {} }
|
|
||||||
@to_show, @to_hide, @to_update = champs_to_turbo_update(champs_attributes, @champ.dossier.champs)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -147,6 +147,7 @@ export function MultiComboBox(maybeProps: MultiComboBoxProps) {
|
||||||
formValue,
|
formValue,
|
||||||
allowsCustomValue,
|
allowsCustomValue,
|
||||||
valueSeparator,
|
valueSeparator,
|
||||||
|
className,
|
||||||
...props
|
...props
|
||||||
} = useMemo(() => s.create(maybeProps, MultiComboBoxProps), [maybeProps]);
|
} = useMemo(() => s.create(maybeProps, MultiComboBoxProps), [maybeProps]);
|
||||||
|
|
||||||
|
@ -174,7 +175,7 @@ export function MultiComboBox(maybeProps: MultiComboBoxProps) {
|
||||||
const formResetRef = useOnFormReset(onReset);
|
const formResetRef = useOnFormReset(onReset);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fr-ds-combobox__multiple">
|
<div className={`fr-ds-combobox__multiple ${className}`}>
|
||||||
{selectedItems.length > 0 ? (
|
{selectedItems.length > 0 ? (
|
||||||
<TagGroup onRemove={onRemove} aria-label={props['aria-label']}>
|
<TagGroup onRemove={onRemove} aria-label={props['aria-label']}>
|
||||||
<TagList items={selectedItems} className="fr-tag-list">
|
<TagList items={selectedItems} className="fr-tag-list">
|
||||||
|
@ -203,7 +204,16 @@ export function MultiComboBox(maybeProps: MultiComboBoxProps) {
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
{name ? (
|
{name ? (
|
||||||
<span ref={ref}>
|
<span ref={ref}>
|
||||||
{hiddenInputValues.map((value, i) => (
|
{hiddenInputValues.length == 0 ? (
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
value=""
|
||||||
|
name={name}
|
||||||
|
form={form}
|
||||||
|
ref={formResetRef}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
hiddenInputValues.map((value, i) => (
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
value={value}
|
value={value}
|
||||||
|
@ -212,7 +222,8 @@ export function MultiComboBox(maybeProps: MultiComboBoxProps) {
|
||||||
ref={i == 0 ? formResetRef : undefined}
|
ref={i == 0 ? formResetRef : undefined}
|
||||||
key={value}
|
key={value}
|
||||||
/>
|
/>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -23,6 +23,7 @@ export interface ComboBoxProps
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputMap = new WeakMap<HTMLInputElement, string>();
|
const inputMap = new WeakMap<HTMLInputElement, string>();
|
||||||
|
const inputCountMap = new WeakMap<HTMLSpanElement, number>();
|
||||||
export function useDispatchChangeEvent() {
|
export function useDispatchChangeEvent() {
|
||||||
const ref = useRef<HTMLSpanElement>(null);
|
const ref = useRef<HTMLSpanElement>(null);
|
||||||
|
|
||||||
|
@ -30,12 +31,15 @@ export function useDispatchChangeEvent() {
|
||||||
ref,
|
ref,
|
||||||
dispatch: () => {
|
dispatch: () => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const input = ref.current?.querySelector('input');
|
if (ref.current) {
|
||||||
if (input) {
|
const container = ref.current;
|
||||||
const value = input.value;
|
const inputs = Array.from(container.querySelectorAll('input'));
|
||||||
const prevValue = inputMap.get(input) || '';
|
const input = inputs.at(0);
|
||||||
if (value != prevValue) {
|
if (input && inputChanged(container, inputs)) {
|
||||||
inputMap.set(input, value);
|
inputCountMap.set(container, inputs.length);
|
||||||
|
for (const input of inputs) {
|
||||||
|
inputMap.set(input, input.value.trim());
|
||||||
|
}
|
||||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,6 +48,23 @@ export function useDispatchChangeEvent() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// I am not proude of this code. We have to tack values and number of values to deal with multi select combobox.
|
||||||
|
// I have a plan to remove this code. Soon.
|
||||||
|
function inputChanged(container: HTMLSpanElement, inputs: HTMLInputElement[]) {
|
||||||
|
const prevCount = inputCountMap.get(container) ?? 0;
|
||||||
|
if (prevCount != inputs.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (const input of inputs) {
|
||||||
|
const value = input.value.trim();
|
||||||
|
const prevValue = inputMap.get(input);
|
||||||
|
if (prevValue == null || prevValue != value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export function useSingleList({
|
export function useSingleList({
|
||||||
defaultItems,
|
defaultItems,
|
||||||
defaultSelectedKey,
|
defaultSelectedKey,
|
||||||
|
@ -174,9 +195,13 @@ export function useMultiList({
|
||||||
const filteredItems = useMemo(
|
const filteredItems = useMemo(
|
||||||
() =>
|
() =>
|
||||||
inputValue.length == 0
|
inputValue.length == 0
|
||||||
? items
|
? items.filter((item) => !selectedKeys.has(item.value))
|
||||||
: matchSorter(items, inputValue, { keys: ['label'] }),
|
: matchSorter(
|
||||||
[items, inputValue]
|
items.filter((item) => !selectedKeys.has(item.value)),
|
||||||
|
inputValue,
|
||||||
|
{ keys: ['label'] }
|
||||||
|
),
|
||||||
|
[items, inputValue, selectedKeys]
|
||||||
);
|
);
|
||||||
const selectedItems = useMemo(() => {
|
const selectedItems = useMemo(() => {
|
||||||
const selectedItems: Item[] = [];
|
const selectedItems: Item[] = [];
|
||||||
|
|
|
@ -73,7 +73,7 @@ class Champs::MultipleDropDownListChamp < Champ
|
||||||
end
|
end
|
||||||
|
|
||||||
def value=(value)
|
def value=(value)
|
||||||
return super(nil) if value.nil?
|
return super(nil) if value.blank?
|
||||||
|
|
||||||
values = if value.is_a?(Array)
|
values = if value.is_a?(Array)
|
||||||
value
|
value
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
describe Champs::OptionsController, type: :controller do
|
|
||||||
let(:user) { create(:user) }
|
|
||||||
let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :multiple_drop_down_list }]) }
|
|
||||||
|
|
||||||
describe '#remove' do
|
|
||||||
let(:dossier) { create(:dossier, user:, procedure:) }
|
|
||||||
let(:champ) { dossier.champs.first }
|
|
||||||
|
|
||||||
before {
|
|
||||||
sign_in user
|
|
||||||
champ.update(value: ['toto', 'tata'].to_json)
|
|
||||||
}
|
|
||||||
|
|
||||||
context 'with stable_id' do
|
|
||||||
subject { delete :remove, params: { dossier_id: dossier, stable_id: champ.stable_id, option: 'tata' }, format: :turbo_stream }
|
|
||||||
|
|
||||||
it 'remove option' do
|
|
||||||
expect { subject }.to change { champ.reload.selected_options.size }.from(2).to(1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -41,8 +41,6 @@ describe Champs::MultipleDropDownListChamp do
|
||||||
expect(champ.value).to eq("[\"val1\"]")
|
expect(champ.value).to eq("[\"val1\"]")
|
||||||
champ.value = 'val2'
|
champ.value = 'val2'
|
||||||
expect(champ.value).to eq("[\"val1\",\"val2\"]")
|
expect(champ.value).to eq("[\"val1\",\"val2\"]")
|
||||||
champ.value = ''
|
|
||||||
expect(champ.value).to eq("[\"val1\",\"val2\"]")
|
|
||||||
champ.value = "[brackets] val4"
|
champ.value = "[brackets] val4"
|
||||||
expect(champ.value).to eq("[\"val1\",\"val2\",\"[brackets] val4\"]")
|
expect(champ.value).to eq("[\"val1\",\"val2\",\"[brackets] val4\"]")
|
||||||
champ.value = nil
|
champ.value = nil
|
||||||
|
@ -51,6 +49,10 @@ describe Champs::MultipleDropDownListChamp do
|
||||||
expect(champ.value).to eq("[\"val1\"]")
|
expect(champ.value).to eq("[\"val1\"]")
|
||||||
champ.value = []
|
champ.value = []
|
||||||
expect(champ.value).to be_nil
|
expect(champ.value).to be_nil
|
||||||
|
champ.value = ["val1"]
|
||||||
|
expect(champ.value).to eq("[\"val1\"]")
|
||||||
|
champ.value = ''
|
||||||
|
expect(champ.value).to be_nil
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -34,8 +34,13 @@ describe 'The user' do
|
||||||
find('.fr-checkbox-group label', text: 'val1').click
|
find('.fr-checkbox-group label', text: 'val1').click
|
||||||
find('.fr-checkbox-group label', text: 'val3').click
|
find('.fr-checkbox-group label', text: 'val3').click
|
||||||
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('charly', from: form_id_for('multiple_choice_drop_down_list_long'))
|
scroll_to(find_field('multiple_choice_drop_down_list_long'), align: :center)
|
||||||
|
fill_in('multiple_choice_drop_down_list_long', with: 'alpha')
|
||||||
|
find('.fr-menu__item', text: 'alpha').click
|
||||||
|
fill_in('multiple_choice_drop_down_list_long', with: 'charly')
|
||||||
|
find('.fr-menu__item', text: 'charly').click
|
||||||
|
wait_until { champ_value_for('multiple_choice_drop_down_list_long') == ['alpha', 'charly'].to_json }
|
||||||
|
|
||||||
select('Australie', from: form_id_for('pays'))
|
select('Australie', from: form_id_for('pays'))
|
||||||
select('Martinique', from: form_id_for('regions'))
|
select('Martinique', from: form_id_for('regions'))
|
||||||
|
@ -109,8 +114,8 @@ describe 'The user' do
|
||||||
expect(page).to have_selected_value('regions', selected: 'Martinique')
|
expect(page).to have_selected_value('regions', selected: 'Martinique')
|
||||||
expect(page).to have_selected_value('departements', selected: '02 – Aisne')
|
expect(page).to have_selected_value('departements', selected: '02 – Aisne')
|
||||||
within("##{champ_for('multiple_choice_drop_down_list_long').input_group_id}") do
|
within("##{champ_for('multiple_choice_drop_down_list_long').input_group_id}") do
|
||||||
expect(page).to have_button('alpha')
|
expect(page).to have_text('alpha')
|
||||||
expect(page).to have_button('charly')
|
expect(page).to have_text('charly')
|
||||||
end
|
end
|
||||||
expect(page).to have_field('communes', with: 'Brétigny (60400)')
|
expect(page).to have_field('communes', with: 'Brétigny (60400)')
|
||||||
expect(page).to have_selected_value('pays', selected: 'Australie')
|
expect(page).to have_selected_value('pays', selected: 'Australie')
|
||||||
|
|
|
@ -103,7 +103,7 @@ describe 'shared/dossiers/edit', type: :view do
|
||||||
let(:options) { ['peach', 'banana', 'pear', 'apricot', 'apple', 'grapefruit'] }
|
let(:options) { ['peach', 'banana', 'pear', 'apricot', 'apple', 'grapefruit'] }
|
||||||
|
|
||||||
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')
|
expect(subject).to have_selector('react-fragment > react-component[name="ComboBox/MultiComboBox"]')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue