UNPKG

9.56 kBJavaScriptView Raw
1'use strict';
2
3
4import $ from 'jquery';
5import { Keyboard } from './foundation.util.keyboard';
6import { Nest } from './foundation.util.nest';
7import { GetYoDigits } from './foundation.util.core';
8import { Plugin } from './foundation.plugin';
9
10/**
11 * AccordionMenu module.
12 * @module foundation.accordionMenu
13 * @requires foundation.util.keyboard
14 * @requires foundation.util.nest
15 */
16
17class AccordionMenu extends Plugin {
18 /**
19 * Creates a new instance of an accordion menu.
20 * @class
21 * @name AccordionMenu
22 * @fires AccordionMenu#init
23 * @param {jQuery} element - jQuery object to make into an accordion menu.
24 * @param {Object} options - Overrides to the default plugin settings.
25 */
26 _setup(element, options) {
27 this.$element = element;
28 this.options = $.extend({}, AccordionMenu.defaults, this.$element.data(), options);
29 this.className = 'AccordionMenu'; // ie9 back compat
30
31 Nest.Feather(this.$element, 'accordion');
32
33 this._init();
34
35 Keyboard.register('AccordionMenu', {
36 'ENTER': 'toggle',
37 'SPACE': 'toggle',
38 'ARROW_RIGHT': 'open',
39 'ARROW_UP': 'up',
40 'ARROW_DOWN': 'down',
41 'ARROW_LEFT': 'close',
42 'ESCAPE': 'closeAll'
43 });
44 }
45
46
47
48 /**
49 * Initializes the accordion menu by hiding all nested menus.
50 * @private
51 */
52 _init() {
53 var _this = this;
54
55 this.$element.find('[data-submenu]').not('.is-active').slideUp(0);//.find('a').css('padding-left', '1rem');
56 this.$element.attr({
57 'role': 'tree',
58 'aria-multiselectable': this.options.multiOpen
59 });
60
61 this.$menuLinks = this.$element.find('.is-accordion-submenu-parent');
62 this.$menuLinks.each(function(){
63 var linkId = this.id || GetYoDigits(6, 'acc-menu-link'),
64 $elem = $(this),
65 $sub = $elem.children('[data-submenu]'),
66 subId = $sub[0].id || GetYoDigits(6, 'acc-menu'),
67 isActive = $sub.hasClass('is-active');
68
69
70 if(_this.options.submenuToggle) {
71 $elem.addClass('has-submenu-toggle');
72 $elem.children('a').after('<button id="' + linkId + '" class="submenu-toggle" aria-controls="' + subId + '" aria-expanded="' + isActive + '" title="' + _this.options.submenuToggleText + '"><span class="submenu-toggle-text">' + _this.options.submenuToggleText + '</span></button>');
73 } else {
74 $elem.attr({
75 'aria-controls': subId,
76 'aria-expanded': isActive,
77 'id': linkId
78 });
79 }
80 $sub.attr({
81 'aria-labelledby': linkId,
82 'aria-hidden': !isActive,
83 'role': 'group',
84 'id': subId
85 });
86 });
87 this.$element.find('li').attr({
88 'role': 'treeitem'
89 });
90 var initPanes = this.$element.find('.is-active');
91 if(initPanes.length){
92 var _this = this;
93 initPanes.each(function(){
94 _this.down($(this));
95 });
96 }
97 this._events();
98 }
99
100 /**
101 * Adds event handlers for items within the menu.
102 * @private
103 */
104 _events() {
105 var _this = this;
106
107 this.$element.find('li').each(function() {
108 var $submenu = $(this).children('[data-submenu]');
109
110 if ($submenu.length) {
111 if(_this.options.submenuToggle) {
112 $(this).children('.submenu-toggle').off('click.zf.accordionMenu').on('click.zf.accordionMenu', function(e) {
113 _this.toggle($submenu);
114 });
115 } else {
116 $(this).children('a').off('click.zf.accordionMenu').on('click.zf.accordionMenu', function(e) {
117 e.preventDefault();
118 _this.toggle($submenu);
119 });
120 }
121 }
122 }).on('keydown.zf.accordionmenu', function(e){
123 var $element = $(this),
124 $elements = $element.parent('ul').children('li'),
125 $prevElement,
126 $nextElement,
127 $target = $element.children('[data-submenu]');
128
129 $elements.each(function(i) {
130 if ($(this).is($element)) {
131 $prevElement = $elements.eq(Math.max(0, i-1)).find('a').first();
132 $nextElement = $elements.eq(Math.min(i+1, $elements.length-1)).find('a').first();
133
134 if ($(this).children('[data-submenu]:visible').length) { // has open sub menu
135 $nextElement = $element.find('li:first-child').find('a').first();
136 }
137 if ($(this).is(':first-child')) { // is first element of sub menu
138 $prevElement = $element.parents('li').first().find('a').first();
139 } else if ($prevElement.parents('li').first().children('[data-submenu]:visible').length) { // if previous element has open sub menu
140 $prevElement = $prevElement.parents('li').find('li:last-child').find('a').first();
141 }
142 if ($(this).is(':last-child')) { // is last element of sub menu
143 $nextElement = $element.parents('li').first().next('li').find('a').first();
144 }
145
146 return;
147 }
148 });
149
150 Keyboard.handleKey(e, 'AccordionMenu', {
151 open: function() {
152 if ($target.is(':hidden')) {
153 _this.down($target);
154 $target.find('li').first().find('a').first().focus();
155 }
156 },
157 close: function() {
158 if ($target.length && !$target.is(':hidden')) { // close active sub of this item
159 _this.up($target);
160 } else if ($element.parent('[data-submenu]').length) { // close currently open sub
161 _this.up($element.parent('[data-submenu]'));
162 $element.parents('li').first().find('a').first().focus();
163 }
164 },
165 up: function() {
166 $prevElement.focus();
167 return true;
168 },
169 down: function() {
170 $nextElement.focus();
171 return true;
172 },
173 toggle: function() {
174 if (_this.options.submenuToggle) {
175 return false;
176 }
177 if ($element.children('[data-submenu]').length) {
178 _this.toggle($element.children('[data-submenu]'));
179 return true;
180 }
181 },
182 closeAll: function() {
183 _this.hideAll();
184 },
185 handled: function(preventDefault) {
186 if (preventDefault) {
187 e.preventDefault();
188 }
189 e.stopImmediatePropagation();
190 }
191 });
192 });//.attr('tabindex', 0);
193 }
194
195 /**
196 * Closes all panes of the menu.
197 * @function
198 */
199 hideAll() {
200 this.up(this.$element.find('[data-submenu]'));
201 }
202
203 /**
204 * Opens all panes of the menu.
205 * @function
206 */
207 showAll() {
208 this.down(this.$element.find('[data-submenu]'));
209 }
210
211 /**
212 * Toggles the open/close state of a submenu.
213 * @function
214 * @param {jQuery} $target - the submenu to toggle
215 */
216 toggle($target){
217 if(!$target.is(':animated')) {
218 if (!$target.is(':hidden')) {
219 this.up($target);
220 }
221 else {
222 this.down($target);
223 }
224 }
225 }
226
227 /**
228 * Opens the sub-menu defined by `$target`.
229 * @param {jQuery} $target - Sub-menu to open.
230 * @fires AccordionMenu#down
231 */
232 down($target) {
233 var _this = this;
234
235 if(!this.options.multiOpen) {
236 this.up(this.$element.find('.is-active').not($target.parentsUntil(this.$element).add($target)));
237 }
238
239 $target.addClass('is-active').attr({'aria-hidden': false});
240
241 if(this.options.submenuToggle) {
242 $target.prev('.submenu-toggle').attr({'aria-expanded': true});
243 }
244 else {
245 $target.parent('.is-accordion-submenu-parent').attr({'aria-expanded': true});
246 }
247
248 $target.slideDown(_this.options.slideSpeed, function () {
249 /**
250 * Fires when the menu is done opening.
251 * @event AccordionMenu#down
252 */
253 _this.$element.trigger('down.zf.accordionMenu', [$target]);
254 });
255 }
256
257 /**
258 * Closes the sub-menu defined by `$target`. All sub-menus inside the target will be closed as well.
259 * @param {jQuery} $target - Sub-menu to close.
260 * @fires AccordionMenu#up
261 */
262 up($target) {
263 var _this = this;
264 $target.slideUp(_this.options.slideSpeed, function () {
265 /**
266 * Fires when the menu is done collapsing up.
267 * @event AccordionMenu#up
268 */
269 _this.$element.trigger('up.zf.accordionMenu', [$target]);
270 });
271
272 var $menus = $target.find('[data-submenu]').slideUp(0).addBack().attr('aria-hidden', true);
273
274 if(this.options.submenuToggle) {
275 $menus.prev('.submenu-toggle').attr('aria-expanded', false);
276 }
277 else {
278 $menus.parent('.is-accordion-submenu-parent').attr('aria-expanded', false);
279 }
280 }
281
282 /**
283 * Destroys an instance of accordion menu.
284 * @fires AccordionMenu#destroyed
285 */
286 _destroy() {
287 this.$element.find('[data-submenu]').slideDown(0).css('display', '');
288 this.$element.find('a').off('click.zf.accordionMenu');
289
290 if(this.options.submenuToggle) {
291 this.$element.find('.has-submenu-toggle').removeClass('has-submenu-toggle');
292 this.$element.find('.submenu-toggle').remove();
293 }
294
295 Nest.Burn(this.$element, 'accordion');
296 }
297}
298
299AccordionMenu.defaults = {
300 /**
301 * Amount of time to animate the opening of a submenu in ms.
302 * @option
303 * @type {number}
304 * @default 250
305 */
306 slideSpeed: 250,
307 /**
308 * Adds a separate submenu toggle button. This allows the parent item to have a link.
309 * @option
310 * @example true
311 */
312 submenuToggle: false,
313 /**
314 * The text used for the submenu toggle if enabled. This is used for screen readers only.
315 * @option
316 * @example true
317 */
318 submenuToggleText: 'Toggle menu',
319 /**
320 * Allow the menu to have multiple open panes.
321 * @option
322 * @type {boolean}
323 * @default true
324 */
325 multiOpen: true
326};
327
328export {AccordionMenu};