Merge pull request #4869 from betagouv/4839-accessibilite-formulaire

Usager : amélioration de l'accessibilité des labels du formulaire
This commit is contained in:
Pierre de La Morinerie 2020-03-24 18:04:15 +01:00 committed by GitHub
commit 77c81cd1ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 135 additions and 16 deletions

View file

@ -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;

View file

@ -26,7 +26,7 @@
input[type=email] { input[type=email] {
width: auto; width: auto;
margin-bottom: 0; margin-bottom: $default-spacer;
} }
.button { .button {

View file

@ -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;

View file

@ -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?

View file

@ -1,2 +1,5 @@
class Champs::CiviliteChamp < Champ class Champs::CiviliteChamp < Champ
def html_label?
false
end
end end

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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)}"

View file

@ -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

View file

@ -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? },

View file

@ -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

View file

@ -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

View 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

View file

@ -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