Changes for page Menu Macro

Last modified by Sebastian Marsching on 2024/09/04 17:56

From version 5.1
edited by Sebastian Marsching
on 2023/08/13 16:14
Change comment: Migrated property [type] from class [XWiki.WikiMacroParameterClass]
To version 7.1
edited by Sebastian Marsching
on 2024/08/10 15:00
Change comment: Install extension [org.xwiki.platform:xwiki-platform-menu-ui/15.10.11]

Summary

Details

XWiki.JavaScriptExtension[0]
Code
... ... @@ -1,26 +1,85 @@
1 -require(['jquery'], function($) {
1 +define('menu-ui-translation-keys', {
2 + prefix: 'menu.ui.',
3 + keys: [
4 + "openSubMenu",
5 + "closeSubMenu"
6 + ]
7 +});
8 +require(['jquery','xwiki-l10n!menu-ui-translation-keys'], function($, l10n) {
2 2   // It's not possible to write a CSS selector that targets list items containing lists so we rely on JavaScript.
3 3   // The 'dropdown' CSS class is used only to display the down/left arrow.
4 - $('.menu-horizontal li ul').parent().addClass('xDropdown');
5 -
11 + // All nodes on the tree.
12 + $('.menu-horizontal ul , .menu-vertical ul')
13 + .attr('role', 'menu');
14 + // All leaves on the tree.
15 + $('.menu-horizontal li, .menu-vertical li')
16 + .attr('role', 'menuitem');
17 + $('.menu-horizontal li ul, .menu-vertical li ul')
18 + .parent()
19 + .addClass('xDropdown');
6 6   // Make sure the menu separators are really empty.
7 - $('.menu-horizontal, .menu-vertical').find('li > br:first-child').remove();
21 + var menus = $('.menu-horizontal, .menu-vertical');
22 + menus.find('li > br:first-child').remove();
8 8  
9 - // Collapsible menu bahavior.
10 - $('.menu-vertical.collapsible').each(function(){
24 + // Add aria attributes to the menu separators.
25 + menus.find('li')
26 + .filter(function() {return this.textContent.trim() === ""; })
27 + .attr('role', 'separator')
28 + .attr('aria-hidden', 'true');
29 +
30 + // Vertical menus are initially expanded
31 + $('.menu-vertical.collapsible').each(function() {
11 11   var open = $(this).hasClass('open');
12 12   $(this).find('li ul').each(function() {
13 13   $(this).addClass('xDropdown-menu').parent().addClass('xDropdown' + (open ? ' open' : ''));
14 - // Wrap everything (including text nodes) before the sub-menu in a DIV that will toggle its state.
15 - var toggle = this.ownerDocument.createElement('div');
16 - $(this).parent().prepend(toggle);
17 - for(var next = toggle.nextSibling; next != this; next = toggle.nextSibling) {
18 - toggle.appendChild(next);
35 + });
36 + });
37 +
38 + function setDropdownButtonTitle(dropDownButton) {
39 + var xDropdown = $(dropDownButton).parent().parent();
40 + if($(xDropdown).hasClass('open')) {
41 + $(dropDownButton).attr('title', l10n['closeSubMenu']);
42 + $(xDropdown).attr('aria-expanded', "true");
43 + } else {
44 + $(dropDownButton).attr('title', l10n['openSubMenu']);
45 + $(xDropdown).attr('aria-expanded', "false");
46 + }
47 + }
48 +
49 + $('.xDropdown').each(function() {
50 + var dropDownHeader = this.ownerDocument.createElement("div");
51 + $(dropDownHeader).addClass("xDropdown-header");
52 + var dropDownButton = this.ownerDocument.createElement("button");
53 + $(dropDownButton).addClass("xDropdown-header-toggle");
54 + setDropdownButtonTitle(dropDownButton);
55 + dropDownButton.addEventListener('click',function() {
56 + //Swaps the state of the submenu.
57 + var xDropdown = $(this).parent().parent();
58 + xDropdown.toggleClass('open');
59 + setDropdownButtonTitle(dropDownButton);
60 + });
61 + let dropDownContent = $(this).contents();
62 + // We put all the content of the entry in the header,
63 + // except for the last one which is the content of the dropdown. This dropdown stays where it is.
64 + for (let index = 0; index < dropDownContent.length - 1 ; index++) {
65 + let item = dropDownContent[index];
66 + dropDownHeader.append(item);
19 19   }
20 - $(toggle).addClass('xDropdown-toggle').on('click', function() {
21 - $(this).parent().toggleClass('open');
22 - });
68 + dropDownHeader.append(dropDownButton);
69 + $(this).prepend(dropDownHeader);
70 + $(dropDownHeader).next().addClass('xDropdown-menu');
71 + });
72 +
73 + $('.menu-horizontal .xDropdown').each(function() {
74 + // In case of horizontal menus, make it so that a class is added on hover, instead of using the :hover pseudo-class
75 + this.addEventListener("mouseover", function() {
76 + $(this).addClass('open');
77 + setDropdownButtonTitle(this.firstChild.lastChild);
23 23   });
79 + this.addEventListener("mouseout", function() {
80 + $(this).removeClass('open');
81 + setDropdownButtonTitle(this.firstChild.lastChild);
82 + });
24 24   });
25 25  
26 26   // In case of horizontal responsive menus, make sub-submenus in the navbar work on mobile devices
XWiki.StyleSheetExtension[1]
Code
... ... @@ -4,6 +4,32 @@
4 4   }
5 5  }
6 6  .menu {
7 + /* Rotate the carets when the menu is opened. */
8 + .xDropdown{
9 + > .xDropdown-header > .xDropdown-header-toggle:before {
10 + transform: rotate(90deg);
11 + }
12 + &.open > .xDropdown-header > .xDropdown-header-toggle:before {
13 + transform: rotate(0);
14 + }
15 + }
16 + .xDropdown-header-toggle {
17 + background: transparent;
18 + border:none;
19 + border-radius: @border-radius-base;
20 + margin: 0 .3em;
21 + line-height: (@line-height-computed / 2);
22 + min-width: 24px;
23 + min-height: 24px;
24 + &:hover, &:focus-within {
25 + background-color: @dropdown-bg;
26 + }
27 + &:before {
28 + .caret;
29 + margin-left: 0;
30 + content: '';
31 + }
32 + }
7 7   &.menu-vertical {
8 8   ul {
9 9   list-style-type: none;
... ... @@ -25,32 +25,8 @@
25 25   .xDropdown-menu {
26 26   display: none;
27 27   }
28 - .xDropdown-toggle {
29 - cursor: pointer;
30 - position: relative;
31 - &:hover {
32 - background-color: @nav-link-hover-bg;
33 - }
34 - &:after {
35 - .caret;
36 - content: '';
37 - /* Positioning */
38 - position: absolute;
39 - margin-top: @line-height-computed / 3;
40 - right: 1em;
41 - /* Collapsed arrow style */
42 - border-bottom: 4px solid transparent;
43 - border-right: 4px solid;
44 - border-top: 4px solid transparent;
45 - }
46 - }
47 47   .xDropdown.open {
48 - > .xDropdown-toggle:after {
49 - /* Expanded arrow style */
50 - .caret;
51 - margin-top: @line-height-computed / 2;
52 - }
53 - > .xDropdown-menu {
55 + > ul {
54 54   display: block;
55 55   }
56 56   }
... ... @@ -64,22 +64,35 @@
64 64   .box-shadow(0 2px 8px rgba(0,0,0,0.4) inset);
65 65   min-height: @navbar-height;
66 66   padding-left: 25px;
69 + .xDropdown.open {
70 + > .xDropdown-header > .xDropdown-header-toggle:before {
71 + transform: rotate(0);
72 + }
73 + > ul {
74 + display: block;
75 + }
76 + }
67 67   & > ul {
68 68   padding-left: 0;
69 69   list-style-type: none;
70 70   margin: 0;
81 + min-height: 50px;
82 + display: flex;
83 + align-items: stretch;
71 71   & > li {
72 72   position: relative;
73 - display: block;
86 + min-height: 50px;
87 + display: flex;
88 + align-items: center;
74 74   padding: @nav-link-padding;
75 - padding-top: @navbar-padding-vertical;
76 - padding-bottom: @navbar-padding-vertical;
90 + padding-top: 0;
91 + padding-bottom: 0;
77 77   @media (min-width: @grid-float-breakpoint) {
78 78   float: left;
79 79   }
80 80   line-height: @line-height-computed;
81 81   color: @navbar-default-link-color;
82 - &:hover {
97 + &:hover, &:focus-within {
83 83   color: @navbar-default-link-hover-color;
84 84   background-color: @navbar-default-link-hover-bg;
85 85   background-color: @navbar-default-link-active-bg;
... ... @@ -93,7 +93,7 @@
93 93   /* Links inside menu */
94 94   a {
95 95   color: @navbar-default-link-color;
96 - &:hover {
111 + &:hover, &:focus-within {
97 97   text-decoration: none;
98 98   }
99 99   }
... ... @@ -102,7 +102,18 @@
102 102   /* Limit the height to the nav height minus the padding and minus border */
103 103   max-height: @navbar-height - (2 * @navbar-padding-vertical) - 2px;
104 104   overflow: hidden;
120 + &.xDropdown-header{
121 + /* No border on the dropdown header */
122 + max-height: unset;
123 + }
105 105   }
125 + /* Horizontal menu top-level headers. */
126 + & > .xDropdown-header > .xDropdown-header-toggle {
127 + &:hover, &:focus-within {
128 + /* Change background color of the caret when hovering on the navbar. */
129 + background-color: @navbar-default-bg;
130 + }
131 + }
106 106   /* Separator vertical inside menu */
107 107   &:empty {
108 108   height: @navbar-height;
... ... @@ -146,7 +146,7 @@
146 146   color: @dropdown-link-color;
147 147   overflow: hidden;
148 148   text-overflow: ellipsis; // Displaying ... if the text is too long
149 - &:hover {
175 + &:hover, &:focus-within {
150 150   /* &:extend(.dropdown-menu>li>a:hover); */
151 151   text-decoration: none;
152 152   color: @dropdown-link-hover-color;
... ... @@ -164,7 +164,7 @@
164 164   color: @dropdown-link-color;
165 165   /* Empty dropdowns should have height in order to display the arrow */
166 166   min-height: 2 * @font-size-base;
167 - &:hover {
193 + &:hover, &:focus-within {
168 168   text-decoration: none;
169 169   color: @dropdown-link-hover-color;
170 170   background-color: @dropdown-link-hover-bg;
... ... @@ -174,15 +174,16 @@
174 174   }
175 175   }
176 176   /* When in dropdown we also have a link, reset the duplicated padding */
177 - & > span > a {
203 + & > .xDropdown-header > span > a {
178 178   padding: 0;
179 179   display: inherit;
180 180   }
181 - /* Place the arrow on the right */
182 - &:after {
207 + /* Reposition the toggle when in a dropdown of fixed size
208 + to avoid eating away at the bit of space we have for the text. */
209 + & > .xDropdown-header > .xDropdown-header-toggle {
183 183   position: absolute;
184 - margin-top: @line-height-computed / 2;
185 - right: 8px;
211 + right: 0;
212 + top: 0;
186 186   }
187 187   }
188 188   /* Separator horizontal inside menu */
... ... @@ -196,17 +196,9 @@
196 196   /* Stylization: Generic */
197 197   li {
198 198   /* Display submenus on hover */
199 - &:hover > ul {
226 + &.open > ul {
200 200   display: block;
201 201   }
202 - /* Display an arrow for expandable items */
203 - &.xDropdown {
204 - &:after {
205 - .caret;
206 - content: '';
207 - margin-left: .5em;
208 - }
209 - }
210 210   }
211 211   /* The only way to have a menu with more than 2 levels without JavaScript is to use a fixed width. */
212 212   &.fixedWidth {
... ... @@ -228,7 +228,7 @@
228 228   }
229 229   /* Resetting rules for mobile view */
230 230   @media (max-width: @screen-xs-max) {
231 - > ul {
250 + > ul {
232 232   margin: 0 0 0 -25px; /* Remove padding added in normal view */
233 233   > li {
234 234   &:empty {
... ... @@ -250,19 +250,16 @@
250 250   /* Links inside menu */
251 251   a {
252 252   color: @navbar-default-link-color;
253 - &:hover {
254 - /* Preserve the styling from dropdown */
255 - }
256 256   }
257 257   /* Submenus inside menu */
258 258   &.xDropdown {
259 259   color: @navbar-default-link-color;
260 - &:hover {
276 + &.open {
261 261   background-color: transparent;
262 262   color: inherit;
263 263   }
264 264   /* When in dropdown we also have a link */
265 - > span > a {
281 + > span > a {
266 266   color: @navbar-default-link-color;
267 267   }
268 268   }
XWiki.WikiMacroClass[0]
Macro code
... ... @@ -1,6 +1,7 @@
1 1  {{velocity}}
2 2  #set ($id = $xcontext.macro.params.id)
3 3  #set ($type = $xcontext.macro.params.type)
4 +#set ($label = $xcontext.macro.params.label)
4 4  #set ($colorTheme = $xwiki.getUserPreference('colorTheme'))
5 5  #if ("$!colorTheme" != '')
6 6   ## Make sure we use an absolute reference (see XWIKI-9672)
... ... @@ -8,16 +8,24 @@
8 8  #end
9 9  #set ($discard = $xwiki.ssx.use("$xcontext.macro.doc.prefixedFullName", {'colorTheme': $colorTheme}))
10 10  #set ($discard = $xwiki.jsx.use("$xcontext.macro.doc.prefixedFullName"))
12 +## Make sure the label is non-empty as otherwise the aria-label doesn't work.
13 +#if ("$!label" != '')
14 + #set ($label = $wikimacro.context.getXDOM().getIdGenerator().generateUniqueId('Menu',''))
15 +#end
11 11  #if($type.contains('horizontal'))
12 12   ## Make sure the id is non-empty for horizontal menus as otherwise the toggle doesn't work.
13 - #if ($stringtool.isBlank("$!id"))
18 + #if ("$!id" == '')
14 14   #set ($id = $wikimacro.context.getXDOM().getIdGenerator().generateUniqueId("M", "GeneratedMenuId"))
15 15   #end
16 - (% role="navigation" class="menu-horizontal-toggle" %)(((
21 + (% role='navigation' class='menu-horizontal-toggle'
22 + aria-label="${services.rendering.escape($label, 'xwiki/2.1')}" %)(((
17 17   (% class="navbar-header" %)(((
18 18   {{html}}
19 - <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#$!{escapetool.xml($id)}" aria-expanded="false">
20 - <span class="sr-only"></span>
25 + <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#$!{escapetool.xml($id)}"
26 + aria-expanded="false" aria-controls="$!{escapetool.xml($id)}">
27 + <span class="sr-only">
28 + $escapetool.xml($services.localization.render('menu.ui.horizontal.toggler.description'))
29 + </span>
21 21   <span class="icon-bar"></span>
22 22   <span class="icon-bar"></span>
23 23   <span class="icon-bar"></span>
... ... @@ -24,12 +24,13 @@
24 24   </button>
25 25   {{/html}}
26 26   )))
27 - (% id="$!{services.rendering.escape($id, 'xwiki/2.1')}" class="menu menu-${services.rendering.escape($!type, 'xwiki/2.1')} collapse navbar-collapse" %)(((
36 + (% id="$!{services.rendering.escape($id, 'xwiki/2.1')}" class="menu menu-${services.rendering.escape($!type, 'xwiki/2.1')} collapse navbar-collapse" role="navigation" %)(((
28 28   {{wikimacrocontent/}}
29 29   )))
30 30   )))
31 31  #else
32 - (% #if ("$!id" != '') id="${services.rendering.escape($id, 'xwiki/2.1')}"#end class="menu menu-${services.rendering.escape($!type, 'xwiki/2.1')}" %)(((
41 + (% role="navigation" #if ("$!id" != '') id="${services.rendering.escape($id, 'xwiki/2.1')}"#end class="menu menu-${services.rendering.escape($!type, 'xwiki/2.1')}"
42 + aria-label="${services.rendering.escape($label, 'xwiki/2.1')}" %)(((
33 33   {{wikimacrocontent/}}
34 34   )))
35 35  #end
XWiki.WikiMacroParameterClass[3]
Parameter description
... ... @@ -1,0 +1,1 @@
1 +Optional menu label used to describe the content of the menu.
Parameter mandatory
... ... @@ -1,0 +1,1 @@
1 +No
Parameter name
... ... @@ -1,0 +1,1 @@
1 +label