Merge pull request #4869 from betagouv/4839-accessibilite-formulaire
Usager : amélioration de l'accessibilité des labels du formulaire
This commit is contained in:
commit
77c81cd1ae
18 changed files with 135 additions and 16 deletions
|
@ -44,6 +44,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: $default-padding;
|
||||||
|
}
|
||||||
|
|
||||||
.notice {
|
.notice {
|
||||||
@include notice-text-style;
|
@include notice-text-style;
|
||||||
margin-top: - $default-spacer;
|
margin-top: - $default-spacer;
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
input[type=email] {
|
input[type=email] {
|
||||||
width: auto;
|
width: auto;
|
||||||
margin-bottom: 0;
|
margin-bottom: $default-spacer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
@import "common";
|
@import "common";
|
||||||
@import "constants";
|
@import "constants";
|
||||||
@import "mixins";
|
@import "mixins";
|
||||||
|
@import "utils";
|
||||||
|
|
||||||
$header-landing-breakpoint: 1040px;
|
$header-landing-breakpoint: 1040px;
|
||||||
$header-mobile-breakpoint: 550px;
|
$header-mobile-breakpoint: 550px;
|
||||||
|
@ -148,6 +149,10 @@ $header-mobile-breakpoint: 550px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label.hidden {
|
||||||
|
@extend .hidden;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@extend %outline;
|
@extend %outline;
|
||||||
|
|
||||||
|
|
|
@ -80,6 +80,10 @@ class Champ < ApplicationRecord
|
||||||
type_de_champ.to_typed_id
|
type_de_champ.to_typed_id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def html_label?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def needs_dossier_id?
|
def needs_dossier_id?
|
||||||
|
|
|
@ -1,2 +1,5 @@
|
||||||
class Champs::CiviliteChamp < Champ
|
class Champs::CiviliteChamp < Champ
|
||||||
|
def html_label?
|
||||||
|
false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,6 +13,10 @@ class Champs::DatetimeChamp < Champ
|
||||||
value.present? ? I18n.l(Time.zone.parse(value)) : ""
|
value.present? ? I18n.l(Time.zone.parse(value)) : ""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def html_label?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def format_before_save
|
def format_before_save
|
||||||
|
|
|
@ -51,4 +51,8 @@ class Champs::PieceJustificativeChamp < Champ
|
||||||
piece_justificative_file.service_url
|
piece_justificative_file.service_url
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def html_label?
|
||||||
|
false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,8 +15,12 @@
|
||||||
= form_tag dossier_invites_path(dossier), remote: true, method: :post, class: 'form' do
|
= form_tag dossier_invites_path(dossier), remote: true, method: :post, class: 'form' do
|
||||||
.row
|
.row
|
||||||
.col
|
.col
|
||||||
= email_field_tag :invite_email, '', class: 'small', placeholder: 'adresse email', required: true
|
%span
|
||||||
|
= label_tag :invite_email, "Adresse email"
|
||||||
|
= email_field_tag :invite_email, '', class: 'small', placeholder: 'Adresse email', required: true
|
||||||
.col
|
.col
|
||||||
|
%span
|
||||||
|
= label_tag :invite_message, "Ajouter un message à la personne invitée (optionnel)"
|
||||||
= text_area_tag :invite_message, '', class: 'small', placeholder: 'Ajouter un message à la personne invitée (optionnel)'
|
= text_area_tag :invite_message, '', class: 'small', placeholder: 'Ajouter un message à la personne invitée (optionnel)'
|
||||||
.col
|
.col
|
||||||
= submit_tag 'Envoyer une invitation', class: 'button accepted'
|
= submit_tag 'Envoyer une invitation', class: 'button accepted'
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
.dropdown.header-menu-opener
|
.dropdown.header-menu-opener
|
||||||
%button.button.dropdown-button.header-menu-button{ title: "Mon compte" }
|
%button.button.dropdown-button.header-menu-button{ title: "Mon compte" }
|
||||||
|
.hidden Mon compte
|
||||||
= image_tag "icons/account-circle.svg", alt: ''
|
= image_tag "icons/account-circle.svg", alt: ''
|
||||||
%ul.header-menu.dropdown-content
|
%ul.header-menu.dropdown-content
|
||||||
%li
|
%li
|
||||||
|
|
|
@ -47,9 +47,10 @@
|
||||||
%li
|
%li
|
||||||
.header-search{ role: 'search' }
|
.header-search{ role: 'search' }
|
||||||
= form_tag recherche_dossiers_path, method: :post, class: "form" do
|
= form_tag recherche_dossiers_path, method: :post, class: "form" do
|
||||||
|
= label_tag :dossier_id, "Numéro de dossier", class: 'hidden'
|
||||||
= text_field_tag :dossier_id, "", placeholder: "Numéro de dossier"
|
= text_field_tag :dossier_id, "", placeholder: "Numéro de dossier"
|
||||||
%button{ title: "Rechercher" }
|
%button{ title: "Rechercher" }
|
||||||
= image_tag "icons/search-blue.svg", alt: ''
|
= image_tag "icons/search-blue.svg", alt: 'Rechercher', 'aria-hidden':'true'
|
||||||
|
|
||||||
- if instructeur_signed_in? || user_signed_in?
|
- if instructeur_signed_in? || user_signed_in?
|
||||||
%li
|
%li
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
= form.label champ.main_value_name do
|
= # we do this trick because some html elements should use 'label' and some should be plain paragraphs
|
||||||
#{champ.libelle}
|
- if champ.html_label?
|
||||||
- if champ.mandatory?
|
= form.label champ.main_value_name do
|
||||||
%span.mandatory *
|
= render partial: 'shared/dossiers/editable_champs/champ_label_content', locals: { champ: champ, seen_at: seen_at }
|
||||||
|
- else
|
||||||
- if champ.updated_at.present? && seen_at.present?
|
%h4.form-label
|
||||||
%span.updated-at{ class: highlight_if_unseen_class(seen_at, champ.updated_at) }
|
= render partial: 'shared/dossiers/editable_champs/champ_label_content', locals: { champ: champ, seen_at: seen_at }
|
||||||
= "modifié le #{try_format_datetime(champ.updated_at)}"
|
|
||||||
|
|
||||||
- if champ.description.present?
|
- if champ.description.present?
|
||||||
.notice{ id: describedby_id(champ) }= string_to_html(champ.description)
|
.notice{ id: describedby_id(champ) }= string_to_html(champ.description)
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
#{champ.libelle}
|
||||||
|
- if champ.mandatory?
|
||||||
|
%span.mandatory *
|
||||||
|
|
||||||
|
- if champ.updated_at.present? && seen_at.present?
|
||||||
|
%span.updated-at{ class: highlight_if_unseen_class(seen_at, champ.updated_at) }
|
||||||
|
= "modifié le #{try_format_datetime(champ.updated_at)}"
|
|
@ -1,4 +1,6 @@
|
||||||
.radios
|
%fieldset.radios
|
||||||
|
%legend.mandatory-explanation
|
||||||
|
Sélectionnez une des valeurs
|
||||||
%label
|
%label
|
||||||
= form.radio_button :value, Individual::GENDER_MALE
|
= form.radio_button :value, Individual::GENDER_MALE
|
||||||
Monsieur
|
Monsieur
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
champ.primary_options,
|
champ.primary_options,
|
||||||
{ required: champ.mandatory? },
|
{ required: champ.mandatory? },
|
||||||
{ data: { secondary_options: champ.secondary_options } }
|
{ data: { secondary_options: champ.secondary_options } }
|
||||||
|
%span
|
||||||
|
= form.label :secondary_value, "Valeur secondaire dépendant de la première", class: 'hidden'
|
||||||
= form.select :secondary_value,
|
= form.select :secondary_value,
|
||||||
champ.secondary_options[champ.primary_value],
|
champ.secondary_options[champ.primary_value],
|
||||||
{ required: champ.mandatory? },
|
{ required: champ.mandatory? },
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
.radios
|
%fieldset.radios
|
||||||
|
%legend.mandatory-explanation
|
||||||
|
Sélectionnez une des deux valeurs
|
||||||
%label
|
%label
|
||||||
= form.radio_button :value, true
|
= form.radio_button :value, true
|
||||||
Oui
|
Oui
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
%p.mb-1 Merci de remplir vos informations personnelles pour accéder à la démarche.
|
%p.mb-1 Merci de remplir vos informations personnelles pour accéder à la démarche.
|
||||||
|
|
||||||
%label
|
%span.form-label
|
||||||
%span.mandatory *
|
%span.mandatory *
|
||||||
champs requis
|
champs requis
|
||||||
|
|
||||||
|
|
49
config/initializers/date_select.rb
Normal file
49
config/initializers/date_select.rb
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
# We monkey patch the DateTimeSelector in order to add accessibility labels
|
||||||
|
# https://stackoverflow.com/a/47836699
|
||||||
|
module ActionView
|
||||||
|
module Helpers
|
||||||
|
class DateTimeSelector
|
||||||
|
# Given an ordering of datetime components, create the selection HTML
|
||||||
|
# and join them with their appropriate separators.
|
||||||
|
def build_selects_from_types(order)
|
||||||
|
select = ""
|
||||||
|
order.reverse_each do |type|
|
||||||
|
separator = separator(type)
|
||||||
|
select.insert(0, separator.to_s + send("select_#{type}").to_s)
|
||||||
|
end
|
||||||
|
# rubocop:disable Rails/OutputSafety
|
||||||
|
select.html_safe
|
||||||
|
# rubocop:enable Rails/OutputSafety
|
||||||
|
end
|
||||||
|
|
||||||
|
def datetime_accessibility_label(n, label)
|
||||||
|
prefix_re = @options[:prefix].match('(.*)\[(.*)\]\[(\d+)\]')
|
||||||
|
if prefix_re.nil? || prefix_re.size < 2
|
||||||
|
prefix = []
|
||||||
|
else
|
||||||
|
prefix = prefix_re.to_a.drop(1)
|
||||||
|
end
|
||||||
|
field_for = "#{prefix.join('_')}_#{@options[:field_name]}"
|
||||||
|
|
||||||
|
"<span class='hidden'><label for='#{field_for}_#{n}i'>#{label}</label></span>"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the separator for a given datetime component.
|
||||||
|
def separator(type)
|
||||||
|
return "" if @options[:use_hidden]
|
||||||
|
case type
|
||||||
|
when :year
|
||||||
|
datetime_accessibility_label(1, 'Année')
|
||||||
|
when :month
|
||||||
|
datetime_accessibility_label(2, 'Mois')
|
||||||
|
when :day
|
||||||
|
datetime_accessibility_label(3, 'Jour')
|
||||||
|
when :hour
|
||||||
|
(@options[:discard_year] && @options[:discard_day]) ? "" : @options[:datetime_separator] + datetime_accessibility_label(4, 'Heure')
|
||||||
|
when :minute, :second
|
||||||
|
@options[:"discard_#{type}"] ? "" : datetime_accessibility_label(5, 'Minute')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -19,7 +19,7 @@ feature 'The user' do
|
||||||
fill_in('text', with: 'super texte')
|
fill_in('text', with: 'super texte')
|
||||||
fill_in('textarea', with: 'super textarea')
|
fill_in('textarea', with: 'super textarea')
|
||||||
fill_in('date', with: '12-12-2012')
|
fill_in('date', with: '12-12-2012')
|
||||||
select_date_and_time(Time.zone.parse('06/01/1985 7h05'), form_id_for('datetime'))
|
select_date_and_time(Time.zone.parse('06/01/1985 7h05'), form_id_for_datetime('datetime'))
|
||||||
fill_in('number', with: '42')
|
fill_in('number', with: '42')
|
||||||
check('checkbox')
|
check('checkbox')
|
||||||
choose('Madame')
|
choose('Madame')
|
||||||
|
@ -74,7 +74,7 @@ feature 'The user' do
|
||||||
expect(page).to have_field('text', with: 'super texte')
|
expect(page).to have_field('text', with: 'super texte')
|
||||||
expect(page).to have_field('textarea', with: 'super textarea')
|
expect(page).to have_field('textarea', with: 'super textarea')
|
||||||
expect(page).to have_field('date', with: '2012-12-12')
|
expect(page).to have_field('date', with: '2012-12-12')
|
||||||
check_date_and_time(Time.zone.parse('06/01/1985 7:05'), form_id_for('datetime'))
|
check_date_and_time(Time.zone.parse('06/01/1985 7:05'), form_id_for_datetime('datetime'))
|
||||||
expect(page).to have_field('number', with: '42')
|
expect(page).to have_field('number', with: '42')
|
||||||
expect(page).to have_checked_field('checkbox')
|
expect(page).to have_checked_field('checkbox')
|
||||||
expect(page).to have_checked_field('Madame')
|
expect(page).to have_checked_field('Madame')
|
||||||
|
@ -250,6 +250,32 @@ feature 'The user' do
|
||||||
find(:xpath, ".//label[contains(text()[normalize-space()], '#{libelle}')]")[:for]
|
find(:xpath, ".//label[contains(text()[normalize-space()], '#{libelle}')]")[:for]
|
||||||
end
|
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:
|
||||||
|
# dossier_champs_attributes_3_value_1i, 2i, ... 5i) ; we are interested in the 'dossier_champs_attributes_3_value' part
|
||||||
|
# which is then completed in select_date_and_time and check_date_and_time
|
||||||
|
#
|
||||||
|
# We find the H2, find the first select in the next .datetime div, then strip the last 3 characters
|
||||||
|
#
|
||||||
|
# <h4 class="form-label">
|
||||||
|
# libelle
|
||||||
|
# </h4>
|
||||||
|
# <div class="datetime">
|
||||||
|
# <span class="hidden">
|
||||||
|
# <label for="dossier_champs_attributes_3_value_3i">Jour</label></span>
|
||||||
|
# <select id="dossier_champs_attributes_3_value_3i" name="dossier[champs_attributes][3][value(3i)]">
|
||||||
|
# <option value=""></option>
|
||||||
|
# <option value="1">1</option>
|
||||||
|
# <option value="2">2</option>
|
||||||
|
# <!-- … -->
|
||||||
|
# </select>
|
||||||
|
# <!-- … 4 other selects for month, year, minute and seconds -->
|
||||||
|
# </div>
|
||||||
|
e = find(:xpath, ".//h4[contains(text()[normalize-space()], '#{libelle}')]")
|
||||||
|
e.sibling('.datetime').first('select')[:id][0..-4]
|
||||||
|
end
|
||||||
|
|
||||||
def champ_value_for(libelle)
|
def champ_value_for(libelle)
|
||||||
champs = user_dossier.champs
|
champs = user_dossier.champs
|
||||||
champs.find { |c| c.libelle == libelle }.value
|
champs.find { |c| c.libelle == libelle }.value
|
||||||
|
|
Loading…
Reference in a new issue