PluginProbe
Contact Forms by Cimatti / 2.2.0
Contact Forms by Cimatti v2.2.0
2.3.6 2.3.5 2.3.0 2.2.32 2.2.4 2.2.0 2.1.2 2.1.1 trunk 1.0 1.1 1.2 1.2.1 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 All 62 releases
contact-forms / assets / js / admin / accua-tabs.js

accua-tabs.js in Contact Forms by Cimatti 2.2.0, at assets/js/admin/accua-tabs.js

129 lines 3.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * AccuaTabs — Reusable tab component for WordPress admin.
3 *
4 * Implements the W3C ARIA Tabs pattern with keyboard navigation.
5 * Supports nesting (each instance scopes to its own direct tabs/panels).
6 *
7 * HTML structure:
8 * <div class="accua-tabs" data-default-tab="first-tab-id">
9 * <div class="accua-tabs__tablist" role="tablist">
10 * <button class="accua-tabs__tab" role="tab" data-tab="tab-id">Label</button>
11 * ...
12 * </div>
13 * <div class="accua-tabs__panel" role="tabpanel" data-tab="tab-id">
14 * ...content...
15 * </div>
16 * ...
17 * </div>
18 *
19 * Usage:
20 * $('.accua-tabs').accuaTabs();
21 * $('.accua-tabs').accuaTabs({ onSelect: function(tabId, $panel) { ... } });
22 */
23 (function($) {
24 'use strict';
25
26 $.fn.accuaTabs = function(options) {
27 var defaults = {
28 onSelect: null
29 };
30 var settings = $.extend({}, defaults, options);
31
32 return this.each(function() {
33 var $container = $(this);
34
35 // Prevent double-init
36 if ($container.hasClass('is-initialized')) {
37 return;
38 }
39
40 // Scope: only direct tablist and panels (not nested ones)
41 // Tablist may be a direct child or inside a header wrapper
42 var $tablist = $container.children('[role="tablist"]');
43 if (!$tablist.length) {
44 $tablist = $container.children('.accua-tabs__header').children('[role="tablist"]');
45 }
46 var $tabs = $tablist.find('> [role="tab"]');
47 var $panels = $container.children('[role="tabpanel"]');
48
49 if (!$tabs.length || !$panels.length) {
50 return;
51 }
52
53 // Wire up ARIA ids
54 $tabs.each(function() {
55 var $tab = $(this);
56 var tabId = $tab.attr('data-tab');
57 var tabElId = $container.attr('id') + '-tab-' + tabId;
58 var panelElId = $container.attr('id') + '-panel-' + tabId;
59 $tab.attr('id', tabElId);
60 $tab.attr('aria-controls', panelElId);
61 $panels.filter('[data-tab="' + tabId + '"]')
62 .attr('id', panelElId)
63 .attr('aria-labelledby', tabElId);
64 });
65
66 // Determine default tab
67 var defaultTab = $container.attr('data-default-tab') || $tabs.first().attr('data-tab');
68
69 function selectTab(tabId, focus) {
70 // Deselect all
71 $tabs.attr('aria-selected', 'false').attr('tabindex', '-1');
72 $panels.attr('hidden', '');
73
74 // Select target
75 var $activeTab = $tabs.filter('[data-tab="' + tabId + '"]');
76 var $activePanel = $panels.filter('[data-tab="' + tabId + '"]');
77 $activeTab.attr('aria-selected', 'true').attr('tabindex', '0');
78 $activePanel.removeAttr('hidden');
79
80 if (focus) {
81 $activeTab.trigger('focus');
82 }
83
84 if (typeof settings.onSelect === 'function') {
85 settings.onSelect(tabId, $activePanel);
86 }
87 }
88
89 // Click handler
90 $tabs.on('click', function(e) {
91 e.preventDefault();
92 selectTab($(this).attr('data-tab'), false);
93 });
94
95 // Keyboard navigation (roving tabindex)
96 $tabs.on('keydown', function(e) {
97 var index = $tabs.index(this);
98 var newIndex = null;
99
100 switch (e.key) {
101 case 'ArrowLeft':
102 case 'ArrowUp':
103 newIndex = index > 0 ? index - 1 : $tabs.length - 1;
104 break;
105 case 'ArrowRight':
106 case 'ArrowDown':
107 newIndex = index < $tabs.length - 1 ? index + 1 : 0;
108 break;
109 case 'Home':
110 newIndex = 0;
111 break;
112 case 'End':
113 newIndex = $tabs.length - 1;
114 break;
115 }
116
117 if (newIndex !== null) {
118 e.preventDefault();
119 selectTab($tabs.eq(newIndex).attr('data-tab'), true);
120 }
121 });
122
123 // Initialize: select default tab, mark container
124 selectTab(defaultTab, false);
125 $container.addClass('is-initialized');
126 });
127 };
128 })(jQuery);
129