amelioration(menu): extraction des menu dans un composant ruby pour ne pas dupliquer les changements aria partout ds la codebase

This commit is contained in:
Martin 2023-01-04 06:10:24 +01:00 committed by mfo
parent 7b5aad5f47
commit 2be0f5aa99
5 changed files with 102 additions and 63 deletions

View file

@ -0,0 +1,43 @@
class Dropdown::MenuComponent < ApplicationComponent
renders_one :button_inner_html
# beware, items elements like button_to/link_to must include role: 'menuitem' for aria reason
renders_many :items, -> (options = {}, &block) do
tag.li({ role: 'none' }.merge(options), &block)
end
renders_many :forms
def initialize(wrapper:,
wrapper_options: {},
button_options: {},
menu_options: {})
@wrapper = wrapper
@wrapper_options = wrapper_options
@button_options = button_options
@menu_options = menu_options
end
def wrapper_class_names
['dropdown'] + Array(@wrapper_options[:class])
end
def button_id
"#{menu_id}_button"
end
def menu_id
@menu_options[:id] ||= SecureRandom.uuid
@menu_options[:id]
end
def menu_role
forms? ? :region : :menu
end
def menu_class_names
['dropdown-content'] + Array(@menu_options[:class])
end
def button_class_names
['fr-btn', 'dropdown-button'] + Array(@button_options[:class])
end
end

View file

@ -0,0 +1,16 @@
= content_tag(@wrapper, class: wrapper_class_names, data: { controller: 'menu-button' }) do
%button{ class: button_class_names, id: button_id, data: { menu_button_target: 'button' }, "aria-expanded": "false", 'aria-haspopup': 'true', 'aria-controls': menu_id }
= button_inner_html
%div{ data: { menu_button_target: 'menu' }, id: menu_id, 'aria-labelledby': button_id, role: menu_role, 'tab-index': -1, class: menu_class_names }
-# the dropdown can be a menu with a list of item
- if items?
%ul.dropdown-items.fr-pl-0{ role: 'none' }
- items.each do |dropdown_item|
= dropdown_item
-# the dropdown can be a menu with forms
- if forms?
- forms.each do |form|
= form

View file

@ -21,38 +21,17 @@ export class MenuButtonController extends ApplicationController {
}
private get isMenu() {
return !(this.element as HTMLElement).dataset.popover;
return this.menuTarget.getAttribute('role') == 'menu';
}
private setup() {
this.buttonTarget.setAttribute(
'aria-haspopup',
this.isMenu ? 'menu' : 'true'
);
this.buttonTarget.setAttribute('aria-controls', this.menuTarget.id);
if (!this.buttonTarget.id) {
this.buttonTarget.id = `${this.menuTarget.id}_button`;
}
this.menuTarget.setAttribute('aria-labelledby', this.buttonTarget.id);
this.menuTarget.setAttribute('role', this.isMenu ? 'menu' : 'region');
// see:
// To progressively enhance this navigation widget that is by default accessible,
// the class to hide the menu and the inclusion of tabindex="-1" on the interactive menuitem
// content should be added with JavaScript on load.
this.menuTarget.classList.add('fade-in-down');
this.menuTarget.setAttribute('tab-index', '-1');
if (this.isMenu) {
for (const menuItem of this.menuTarget.querySelectorAll('a')) {
menuItem.setAttribute('role', 'menuitem');
}
for (const dropdownItems of this.menuTarget.querySelectorAll(
'.dropdown-items'
)) {
dropdownItems.setAttribute('role', 'none');
}
for (const dropdownItems of this.menuTarget.querySelectorAll(
'.dropdown-items > li'
)) {
dropdownItems.setAttribute('role', 'none');
}
this.menuItems.map((menuItem) => menuItem.setAttribute('tabindex', '-1'));
}
this.on('click', (event) => {
@ -106,7 +85,7 @@ export class MenuButtonController extends ApplicationController {
}
private close() {
this.buttonTarget.removeAttribute('aria-expanded');
this.buttonTarget.setAttribute('aria-expanded', 'false');
this.menuTarget.parentElement?.classList.remove('open');
this.#teardown?.();
this.setFocusToMenuitem(null);

View file

@ -41,42 +41,43 @@
%li
= link_to admin_procedure_path(procedure), class: 'fr-btn fr-icon-draft-line fr-btn--tertiary' do
Modifier
%li.dropdown{ data: { controller: 'menu-button' } }
%button.fr-btn.fr-btn--tertiary.dropdown-button.procedures-actions-btn{ data: { menu_button_target: 'button' } }
= render Dropdown::MenuComponent.new(wrapper: :li, button_options: { class: ['fr-btn--tertiary'] }, menu_options: { id: dom_id(procedure, :actions_menu)}) do |menu|
- menu.with_button_inner_html do
Actions
.dropdown-content.fade-in-down{ data: { menu_button_target: 'menu' }, id: dom_id(procedure, :actions_menu) }
%ul.dropdown-items.pl-0
- if !procedure.close? && !procedure.discarded?
%li
= link_to sanitize_url(procedure.brouillon? ? commencer_test_url(path: procedure.path) : commencer_url(path: procedure.path)), target: :blank, rel: :noopener do
%span.icon.in-progress
.dropdown-description
%h4= t('administrateurs.dropdown_actions.to_test')
- unless procedure.discarded?
%li
= link_to admin_procedure_clone_path(procedure.id), class: 'clone-btn', data: { method: :put } do
%span.icon.new-folder
.dropdown-description
%h4= t('administrateurs.dropdown_actions.to_clone')
- if !procedure.close? && !procedure.discarded?
- menu.with_item do
= link_to(sanitize_url(procedure.brouillon? ? commencer_test_url(path: procedure.path) : commencer_url(path: procedure.path)), target: :blank, rel: :noopener, role: 'menuitem') do
%span.icon.in-progress
.dropdown-description
%h4= t('administrateurs.dropdown_actions.to_test')
- if procedure.publiee?
%li
= link_to admin_procedure_close_path(procedure_id: procedure.id) do
%span.icon.archive
.dropdown-description
%h4= t('administrateurs.dropdown_actions.to_close')
- if !procedure.discarded?
- menu.with_item do
= link_to(admin_procedure_clone_path(procedure.id), role: 'menuitem', class: 'clone-btn', data: { method: :put }) do
%span.icon.new-folder
.dropdown-description
%h4= t('administrateurs.dropdown_actions.to_clone')
- if procedure.can_be_deleted_by_administrateur? && !procedure.discarded?
%li
= link_to admin_procedure_path(procedure), method: :delete, data: { confirm: "Voulez-vous vraiment supprimer la démarche ? \nToute suppression est définitive et s'appliquera aux éventuels autres administrateurs de cette démarche !" } do
%span.icon.refuse
.dropdown-description
%h4= t('administrateurs.dropdown_actions.delete')
- if procedure.publiee?
- menu.with_item do
= link_to(admin_procedure_close_path(procedure_id: procedure.id), role: 'menuitem') do
%span.icon.archive
.dropdown-description
%h4= t('administrateurs.dropdown_actions.to_close')
- if procedure.can_be_deleted_by_administrateur? && !procedure.discarded?
- menu.with_item do
= link_to admin_procedure_path(procedure), role: 'menuitem', method: :delete, data: { confirm: "Voulez-vous vraiment supprimer la démarche ? \nToute suppression est définitive et s'appliquera aux éventuels autres administrateurs de cette démarche !" } do
%span.icon.refuse
.dropdown-description
%h4= t('administrateurs.dropdown_actions.delete')
- if procedure.discarded?
- menu.with_item do
= link_to restore_admin_procedure_path(procedure), role: 'menuitem', method: :put do
%span.icon.unarchive
.dropdown-description
%h4= t('administrateurs.dropdown_actions.restore')
- if procedure.discarded?
%li
= link_to restore_admin_procedure_path(procedure), method: :put do
%span.icon.unarchive
.dropdown-description
%h4= t('administrateurs.dropdown_actions.restore')

View file

@ -18,7 +18,7 @@ describe 'As an administrateur I wanna clone a procedure', js: true do
scenario do
visit admin_procedures_path
expect(page.find_by_id('procedures')['data-item-count']).to eq('1')
page.all('.procedures-actions-btn').first.click
page.all('.admin-procedures-list-row .dropdown .fr-btn').first.click
page.all('.clone-btn').first.click
visit admin_procedures_path(statut: "brouillons")
expect(page.find_by_id('procedures')['data-item-count']).to eq('1')