PageSourceSearch

https://verveacu.com/js/jquery-accessible-accordion-aria/index.js

js verveacu.com collected 2026-09-24 10:52:12 UTC 8,597 bytes, 248 lines download raw bytes

1/*
2 * jQuery Accessible Accordion system, using ARIA
3 * @version v2.6.1
4 * Website: https://a11y.nicolas-hoffmann.net/accordion/
5 * License MIT: https://github.com/nico3333fr/jquery-accessible-accordion-aria/blob/master/LICENSE
6 */
7(function(factory) {
8    'use strict';
9    if (typeof define === 'function' && define.amd) {
10        define(['jquery'], factory);
11    } else if (typeof exports !== 'undefined') {
12        module.exports = factory(require('jquery'));
13    } else {
14        factory(window.jQuery);
15    }
16}(function($) {
17    'use strict';
18
19    var defaultConfig = {
20        headersSelector: '.js-accordion__header',
21        panelsSelector: '.js-accordion__panel',
22        buttonsSelector: 'button.js-accordion__header',
23        buttonsGeneratedContent: 'text',
24        button: $('<button></button>', {
25            class: 'js-accordion__header',
26            type: 'button'
27        }),
28        buttonSuffixId: '_tab',
29        multiselectable: true,
30        prefixClass: 'accordion',
31        headerSuffixClass: '__title',
32        buttonSuffixClass: '__header',
33        panelSuffixClass: '__panel',
34        direction: 'ltr',
35        accordionPrefixId: 'accordion'
36    };
37
38    var Accordion = function($el, options) {
39        this.options = $.extend({}, defaultConfig, options);
40
41        this.$wrapper = $el;
42        this.$panels = $(this.options.panelsSelector, this.$wrapper);
43
44        this.initAttributes();
45        this.initEvents();
46    };
47
48    Accordion.prototype.initAttributes = function() {
49        this.$wrapper.attr({
50            'role': 'tablist',
51            'aria-multiselectable': this.options.multiselectable.toString()
52        }).addClass(this.options.prefixClass);
53
54        // id generated if not present
55        if (!this.$wrapper.attr('id')) {
56            var index_lisible = Math.random().toString(32).slice(2, 12);
57            this.$wrapper.attr('id', this.options.accordionPrefixId + '-' + index_lisible);
58        }
59
60        this.$panels.each($.proxy(function(index, el) {
61            var $panel = $(el);
62            var $header = $(this.options.headersSelector, $panel);
63            var $button = this.options.buttonsGeneratedContent === 'html' ? this.options.button.clone().html($header.html()) : this.options.button.clone().text($header.text());
64
65            $header.addClass(this.options.prefixClass + this.options.headerSuffixClass);
66            $panel.before($button);
67
68            var panelId = $panel.attr('id') || this.$wrapper.attr('id') + '-' + index;
69            var buttonId = panelId + this.options.buttonSuffixId;
70
71            $button.attr({
72                'aria-controls': panelId,
73                'aria-expanded': 'false',
74                'role': 'tab',
75                'id': buttonId,
76                'aria-selected': 'false'
77            }).addClass(this.options.prefixClass + this.options.buttonSuffixClass);
78
79            $panel.attr({
80                'aria-labelledby': buttonId,
81                'role': 'tabpanel',
82                'id': panelId,
83                'aria-hidden': 'true'
84            }).addClass(this.options.prefixClass + this.options.panelSuffixClass);
85
86            // if opened by default
87            if ($panel.attr('data-accordion-opened') === 'true') {
88                $button.attr({
89                    'aria-expanded': 'true',
90                    'data-accordion-opened': null
91                });
92
93                $panel.attr({
94                    'aria-hidden': 'false'
95                });
96            }
97
98
99        }, this));
100
101        this.$buttons = $(this.options.buttonsSelector, this.$wrapper);
102    };
103
104    Accordion.prototype.initEvents = function() {
105        this.$wrapper.on('focus', this.options.buttonsSelector, $.proxy(this.focusButtonEventHandler, this));
106
107        this.$wrapper.on('click', this.options.buttonsSelector, $.proxy(this.clickButtonEventHandler, this));
108
109        this.$wrapper.on('keydown', this.options.buttonsSelector, $.proxy(this.keydownButtonEventHandler, this));
110    };
111
112    Accordion.prototype.focusButtonEventHandler = function(e) {
113        var $target = $(e.target);
114        var $button = $target.is('button') ? $target : $target.closest('button');
115
116        $(this.options.buttonsSelector, this.$wrapper).attr({
117            'aria-selected': 'false'
118        });
119
120        $button.attr({
121            'aria-selected': 'true'
122        });
123    };
124
125    Accordion.prototype.clickButtonEventHandler = function(e) {
126        var $target = $(e.target);
127        var $button = $target.is('button') ? $target : $target.closest('button');
128        var $panel = $('#' + $button.attr('aria-controls'));
129
130        this.$buttons.attr('aria-selected', 'false');
131        $button.attr('aria-selected', 'true');
132
133        // opened or closed?
134        if ($button.attr('aria-expanded') === 'false') { // closed
135            $button.attr('aria-expanded', 'true');
136            $panel.attr('aria-hidden', 'false');
137        } else { // opened
138            $button.attr('aria-expanded', 'false');
139            $panel.attr('aria-hidden', 'true');
140        }
141
142        if (this.options.multiselectable === false) {
143            this.$panels.not($panel).attr('aria-hidden', 'true');
144            this.$buttons.not($button).attr('aria-expanded', 'false');
145        }
146
147        setTimeout(function() {
148            $button.focus();
149        }, 0);
150
151        e.stopPropagation();
152        e.preventDefault();
153    };
154
155    Accordion.prototype.keydownButtonEventHandler = function(e) {
156        var $target = $(e.target);
157        var $button = $target.is('button') ? $target : $target.closest('button');
158        var $firstButton = this.$buttons.first();
159        var $lastButton = this.$buttons.last();
160        var index = this.$buttons.index($button);
161
162        $target = null;
163
164        var k = this.options.direction === 'ltr' ? {
165            prev: [38, 37], // up & left
166            next: [40, 39], // down & right
167            first: 36, // home
168            last: 35 // end
169        } : {
170            prev: [38, 39], // up & left
171            next: [40, 37], // down & right
172            first: 36, // home
173            last: 35 // end
174        };
175
176        var allKeyCode = [].concat(k.prev, k.next, k.first, k.last);
177
178        if ($.inArray(e.keyCode, allKeyCode) >= 0 && !e.ctrlKey) {
179            this.$buttons.attr({
180                'aria-selected': 'false'
181            });
182
183
184            if (e.keyCode === 36) {
185                $target = $firstButton;
186            }
187            // strike end in the tab => last tab
188            else if (e.keyCode === 35) {
189                $target = $lastButton;
190            }
191            // strike up or left in the tab => previous tab
192            else if ($.inArray(e.keyCode, k.prev) >= 0) {
193                // if we are on first one, activate last
194                $target = $button.is($firstButton) ? $lastButton : this.$buttons.eq(index - 1);
195            }
196            // strike down or right in the tab => next tab
197            else if ($.inArray(e.keyCode, k.next) >= 0) {
198                // if we are on last one, activate first
199                $target = $button.is($lastButton) ? $firstButton : this.$buttons.eq(index + 1);
200            }
201
202            if ($target !== null) {
203                this.goToHeader($target);
204            }
205
206            e.preventDefault();
207        }
208    };
209
210    Accordion.prototype.goToHeader = function($target) {
211        if ($target.length !== 1) {
212            return;
213        }
214
215        $target.attr({
216            'aria-selected': 'true'
217        });
218
219        setTimeout(function() {
220            $target.focus();
221        }, 0);
222    };
223
224
225    var PLUGIN = 'accordion';
226
227    $.fn[PLUGIN] = function(params) {
228        var options = $.extend({}, $.fn[PLUGIN].defaults, params);
229
230
231        return this.each(function() {
232            var $el = $(this);
233
234            var specificOptions = {
235                multiselectable: $el.attr('data-accordion-multiselectable') === 'none' ? false : options.multiselectable,
236                prefixClass: typeof($el.attr('data-accordion-prefix-classes')) !== 'undefined' ? $el.attr('data-accordion-prefix-classes') : options.prefixClass,
237                buttonsGeneratedContent: typeof($el.attr('data-accordion-button-generated-content')) !== 'undefined' ? $el.attr('data-accordion-button-generated-content') : options.buttonsGeneratedContent,
238                direction: $el.closest('[dir="rtl"]').length > 0 ? 'rtl' : options.direction
239            };
240            specificOptions = $.extend({}, options, specificOptions);
241
242            $el.data[PLUGIN] = new Accordion($el, specificOptions);
243        });
244    };
245
246    $.fn[PLUGIN].defaults = defaultConfig;
247
248}));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.