sphinx-filter-tabs 0.9.2__py3-none-any.whl → 1.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,261 @@
1
+ # filter_tabs/renderer.py
2
+ """
3
+ Renders the HTML and fallback output for the filter-tabs directive.
4
+ Consolidated version including parsing utilities and content type inference.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import copy
10
+ from docutils import nodes
11
+ from sphinx.util import logging
12
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
13
+
14
+ # Import models from extension.py (after consolidation)
15
+ from .extension import TabData, FilterTabsConfig, IDGenerator
16
+ from .extension import ContainerNode, FieldsetNode, LegendNode, RadioInputNode, LabelNode, PanelNode
17
+
18
+ if TYPE_CHECKING:
19
+ from sphinx.environment import BuildEnvironment
20
+ from docutils.parsers.rst import Directive
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # Constants
25
+ SFT_CONTAINER = "sft-container"
26
+ SFT_FIELDSET = "sft-fieldset"
27
+ SFT_LEGEND = "sft-legend"
28
+ SFT_RADIO_GROUP = "sft-radio-group"
29
+ SFT_CONTENT = "sft-content"
30
+ SFT_PANEL = "sft-panel"
31
+
32
+
33
+ # =============================================================================
34
+ # Content Type Inference Utilities (moved from parsers.py)
35
+ # =============================================================================
36
+
37
+ class ContentTypeInferrer:
38
+ """
39
+ Infers the type of content based on tab names to generate meaningful legends.
40
+ """
41
+ PATTERNS = [
42
+ (['python', 'javascript', 'java', 'c++', 'rust', 'go', 'ruby', 'php'], 'programming language'),
43
+ (['windows', 'mac', 'macos', 'linux', 'ubuntu', 'debian', 'fedora'], 'operating system'),
44
+ (['pip', 'conda', 'npm', 'yarn', 'cargo', 'gem', 'composer'], 'package manager'),
45
+ (['cli', 'gui', 'terminal', 'command', 'console', 'graphical'], 'interface'),
46
+ (['development', 'staging', 'production', 'test', 'local'], 'environment'),
47
+ (['source', 'binary', 'docker', 'manual', 'automatic'], 'installation method'),
48
+ ]
49
+
50
+ @classmethod
51
+ def infer_type(cls, tab_names: List[str]) -> str:
52
+ """
53
+ Infer content type from a list of tab names.
54
+
55
+ Args:
56
+ tab_names: List of tab names to analyze
57
+
58
+ Returns:
59
+ Inferred content type string (e.g., 'programming language', 'operating system')
60
+ """
61
+ lower_names = [name.lower() for name in tab_names]
62
+
63
+ # First pass: exact matches
64
+ for keywords, content_type in cls.PATTERNS:
65
+ if any(name in keywords for name in lower_names):
66
+ return content_type
67
+
68
+ # Second pass: substring matches
69
+ for keywords, content_type in cls.PATTERNS:
70
+ for name in lower_names:
71
+ if any(keyword in name for keyword in keywords):
72
+ return content_type
73
+
74
+ # Default fallback
75
+ return 'option'
76
+
77
+
78
+ # =============================================================================
79
+ # Main Renderer Class
80
+ # =============================================================================
81
+
82
+ class FilterTabsRenderer:
83
+ """
84
+ Renders filter tabs with a focus on accessibility and browser compatibility.
85
+ Consolidated version with integrated content type inference.
86
+ """
87
+
88
+ def __init__(self, directive: Directive, tab_data: List[TabData], general_content: List[nodes.Node], custom_legend: Optional[str] = None):
89
+ self.directive = directive
90
+ self.env: BuildEnvironment = directive.state.document.settings.env
91
+ self.app = self.env.app
92
+ self.tab_data = tab_data
93
+ self.general_content = general_content
94
+ self.custom_legend = custom_legend
95
+
96
+ # 1. Load configuration first
97
+ self.config = FilterTabsConfig.from_sphinx_config(self.app.config)
98
+
99
+ # 2. Safely initialize the counter on the environment if it doesn't exist
100
+ if not hasattr(self.env, 'filter_tabs_counter'):
101
+ self.env.filter_tabs_counter = 0
102
+
103
+ # 3. Increment the counter for this new tab group
104
+ self.env.filter_tabs_counter += 1
105
+
106
+ # 4. Generate the unique group ID and the ID generator instance
107
+ self.group_id = f"filter-group-{self.env.filter_tabs_counter}"
108
+ self.id_gen = IDGenerator(self.group_id)
109
+
110
+ # 5. Perform debug logging now that config and group_id are set
111
+ if self.config.debug_mode:
112
+ logger.info(f"Initialized new tab group with id: '{self.group_id}'")
113
+
114
+ def render_html(self) -> List[nodes.Node]:
115
+ """Render HTML with a browser-compatible CSS approach."""
116
+ if self.config.debug_mode:
117
+ logger.info(f"Rendering filter-tabs group {self.group_id}")
118
+
119
+ css_node = self._generate_compatible_css()
120
+
121
+ container_attrs = self._get_container_attributes()
122
+ container = ContainerNode(**container_attrs)
123
+
124
+ fieldset = self._create_fieldset()
125
+ container.children = [fieldset]
126
+
127
+ return [css_node, container]
128
+
129
+ def render_fallback(self) -> List[nodes.Node]:
130
+ """Render for non-HTML builders (e.g., LaTeX)."""
131
+ output_nodes: List[nodes.Node] = []
132
+
133
+ if self.general_content:
134
+ output_nodes.extend(copy.deepcopy(self.general_content))
135
+
136
+ for tab in self.tab_data:
137
+ admonition = nodes.admonition()
138
+ admonition += nodes.title(text=tab.name)
139
+ admonition.extend(copy.deepcopy(tab.content))
140
+ output_nodes.append(admonition)
141
+
142
+ return output_nodes
143
+
144
+ def _get_container_attributes(self) -> Dict[str, Any]:
145
+ """Get container attributes, including the style for custom properties."""
146
+ return {
147
+ 'classes': [SFT_CONTAINER],
148
+ 'role': 'region',
149
+ 'aria-labelledby': self.id_gen.legend_id(),
150
+ 'style': self.config.to_css_properties()
151
+ }
152
+
153
+ def _generate_compatible_css(self) -> nodes.raw:
154
+ """Generate CSS using sibling selectors to show/hide panels."""
155
+ css_rules = []
156
+ for i, tab in enumerate(self.tab_data):
157
+ radio_id = self.id_gen.radio_id(i)
158
+ panel_id = self.id_gen.panel_id(i)
159
+ css_rules.append(
160
+ f"#{radio_id}:checked ~ .{SFT_CONTENT} #{panel_id} {{ display: block; }}"
161
+ )
162
+
163
+ css_content = "\n".join(css_rules)
164
+ return nodes.raw(text=f"<style>\n{css_content}\n</style>", format='html')
165
+
166
+ def _create_fieldset(self) -> FieldsetNode:
167
+ """Create the main fieldset containing the legend, radio buttons, and panels."""
168
+ fieldset = FieldsetNode(role="radiogroup")
169
+
170
+ fieldset += self._create_legend()
171
+
172
+ radio_group = ContainerNode(classes=[SFT_RADIO_GROUP])
173
+ self._populate_radio_group(radio_group)
174
+
175
+ content_area = ContainerNode(classes=[SFT_CONTENT])
176
+ self._populate_content_area(content_area)
177
+
178
+ # This is the fix: place the content_area inside the radio_group.
179
+ radio_group += content_area
180
+
181
+ fieldset += radio_group
182
+
183
+ return fieldset
184
+
185
+ def _create_legend(self) -> LegendNode:
186
+ """Create a meaningful, visible legend for the tab group."""
187
+ legend = LegendNode(classes=[SFT_LEGEND], ids=[self.id_gen.legend_id()])
188
+
189
+ # Use the custom legend if it exists
190
+ if self.custom_legend:
191
+ legend_text = self.custom_legend
192
+ else:
193
+ # Fallback to the auto-generated legend using content type inference
194
+ tab_names = [tab.name for tab in self.tab_data]
195
+ content_type = ContentTypeInferrer.infer_type(tab_names)
196
+ legend_text = f"Choose {content_type}: {', '.join(tab_names)}"
197
+
198
+ legend += nodes.Text(legend_text)
199
+ return legend
200
+
201
+ def _populate_radio_group(self, radio_group: ContainerNode) -> None:
202
+ """Create and add all radio buttons and labels to the radio group container."""
203
+ default_index = next((i for i, tab in enumerate(self.tab_data) if tab.is_default), 0)
204
+
205
+ for i, tab in enumerate(self.tab_data):
206
+ radio_group += self._create_radio_button(i, tab, is_checked=(i == default_index))
207
+ radio_group += self._create_label(i, tab)
208
+ radio_group += self._create_screen_reader_description(i, tab)
209
+
210
+ def _create_radio_button(self, index: int, tab: TabData, is_checked: bool) -> RadioInputNode:
211
+ """Create a single radio button input."""
212
+ radio = RadioInputNode(
213
+ classes=['sr-only'],
214
+ type='radio',
215
+ name=self.group_id,
216
+ ids=[self.id_gen.radio_id(index)],
217
+ **{'aria-describedby': self.id_gen.desc_id(index)}
218
+ )
219
+ if tab.aria_label:
220
+ radio['aria-label'] = tab.aria_label
221
+ if is_checked:
222
+ radio['checked'] = 'checked'
223
+ return radio
224
+
225
+ def _create_label(self, index: int, tab: TabData) -> LabelNode:
226
+ """Create a label for a radio button."""
227
+ label = LabelNode(for_id=self.id_gen.radio_id(index))
228
+ label += nodes.Text(tab.name)
229
+ return label
230
+
231
+ def _create_screen_reader_description(self, index: int, tab: TabData) -> ContainerNode:
232
+ """Create the hidden description for screen readers."""
233
+ desc_text = f"Show content for {tab.name}"
234
+ description_node = ContainerNode(classes=['sr-only'], ids=[self.id_gen.desc_id(index)])
235
+ description_node += nodes.Text(desc_text)
236
+ return description_node
237
+
238
+ def _populate_content_area(self, content_area: ContainerNode) -> None:
239
+ """Create and add all general and tab-specific content panels."""
240
+ if self.general_content:
241
+ general_panel = PanelNode(classes=[SFT_PANEL], **{'data-filter': 'General'})
242
+ general_panel.extend(copy.deepcopy(self.general_content))
243
+ content_area += general_panel
244
+
245
+ for i, tab in enumerate(self.tab_data):
246
+ content_area += self._create_tab_panel(i, tab)
247
+
248
+ def _create_tab_panel(self, index: int, tab: TabData) -> PanelNode:
249
+ """Create a single content panel for a tab."""
250
+ panel_attrs = {
251
+ 'classes': [SFT_PANEL],
252
+ 'ids': [self.id_gen.panel_id(index)],
253
+ 'role': 'region',
254
+ 'aria-labelledby': self.id_gen.radio_id(index),
255
+ 'tabindex': '0',
256
+ 'data-tab': tab.name.lower().replace(' ', '-')
257
+ }
258
+ panel = PanelNode(**panel_attrs)
259
+ panel.extend(copy.deepcopy(tab.content))
260
+ return panel
261
+
@@ -1,112 +1,129 @@
1
- /* Sphinx Filter Tabs -- Complete Stylesheet */
2
- /* This file provides both structural and accessibility styles */
1
+ /* Sphinx Filter Tabs - Simplified Accessibility-First Stylesheet */
3
2
 
4
- /* Main container for the tab component */
3
+ /* Main container */
5
4
  .sft-container {
6
5
  border: 1px solid #e0e0e0;
7
6
  border-radius: var(--sft-border-radius, 8px);
8
7
  overflow: hidden;
9
8
  margin: 1.5em 0;
9
+ background: white;
10
10
  }
11
11
 
12
- /* The <fieldset> provides semantic grouping for accessibility. */
13
- .sft-fieldset {
14
- border: none;
15
- padding: 0;
16
- margin: 0;
12
+ /* Semantic structure */
13
+ .sft-fieldset {
14
+ border: none;
15
+ padding: 0;
16
+ margin: 0;
17
17
  }
18
18
 
19
- /* The <legend> is visually hidden but essential for screen reader users. */
20
- .sft-legend {
21
- position: absolute;
22
- width: 1px;
23
- height: 1px;
24
- padding: 0;
25
- margin: -1px;
26
- overflow: hidden;
27
- clip: rect(0, 0, 0, 0);
28
- white-space: nowrap;
29
- border: 0;
19
+ /* Visible, meaningful legend */
20
+ .sft-legend {
21
+ background: #f8f9fa;
22
+ padding: 12px 20px;
23
+ margin: 0;
24
+ font-weight: 600;
25
+ color: #495057;
26
+ border-bottom: 1px solid #e0e0e0;
27
+ width: 100%;
28
+ font-size: 0.95em;
30
29
  }
31
30
 
32
- /* Hide radio buttons completely but keep them accessible */
33
- .sft-tab-bar > input[type="radio"] {
34
- position: absolute;
35
- width: 1px;
36
- height: 1px;
37
- padding: 0;
38
- margin: -1px;
39
- overflow: hidden;
40
- clip: rect(0, 0, 0, 0);
41
- white-space: nowrap;
42
- border: 0;
43
- opacity: 0;
31
+ /* Radio group container */
32
+ .sft-radio-group {
33
+ display: flex;
34
+ flex-wrap: wrap;
35
+ background: #f8f9fa;
36
+ border-bottom: 1px solid #e0e0e0;
37
+ padding: 0 10px;
44
38
  }
45
39
 
46
- /* The tab bar containing the clickable labels. */
47
- .sft-tab-bar {
48
- display: flex;
49
- flex-wrap: wrap;
50
- background-color: var(--sft-tab-background, #f0f0f0);
51
- border-bottom: 1px solid #e0e0e0;
52
- padding: 0 10px;
40
+ /* Hidden radio buttons (accessible to screen readers) */
41
+ .sft-radio-group input[type="radio"] {
42
+ width: 1px;
43
+ height: 1px;
44
+ opacity: 0.01;
45
+ position: absolute;
46
+ z-index: -1;
47
+ margin: 0;
53
48
  }
54
49
 
55
- .sft-tab-bar > label {
56
- padding: 12px 18px;
57
- cursor: pointer;
58
- transition: border-color 0.2s ease-in-out, color 0.2s ease-in-out;
59
- border-bottom: 3px solid transparent;
60
- color: #555;
61
- font-weight: 500;
62
- font-size: var(--sft-tab-font-size, 1em);
63
- line-height: 1.5;
50
+ /* Tab labels */
51
+ .sft-radio-group label {
52
+ display: block;
53
+ padding: 12px 18px;
54
+ cursor: pointer;
55
+ transition: all 0.2s ease;
56
+ border-bottom: 3px solid transparent;
57
+ color: #6c757d;
58
+ font-weight: 500;
59
+ font-size: var(--sft-tab-font-size, 1em);
60
+ position: relative;
61
+ background: transparent;
64
62
  }
65
63
 
66
- /* Style for checked state - using adjacent sibling selector */
67
- .sft-tab-bar > input[type="radio"]:checked + label {
68
- border-bottom-color: var(--sft-tab-highlight-color, #007bff);
69
- color: #000;
64
+ /* Active tab styling */
65
+ .sft-radio-group input[type="radio"]:checked + label {
66
+ color: var(--sft-highlight-color, #007bff);
67
+ border-bottom-color: var(--sft-highlight-color, #007bff);
68
+ background: white;
70
69
  font-weight: 600;
71
70
  }
72
71
 
73
- /* Hover state for labels */
74
- .sft-tab-bar > label:hover {
75
- color: #333;
76
- border-bottom-color: rgba(0, 123, 255, 0.3);
72
+ /* Focus styling for keyboard navigation */
73
+ .sft-radio-group input[type="radio"]:focus + label {
74
+ outline: 2px solid var(--sft-highlight-color, #007bff);
75
+ outline-offset: 2px;
76
+ box-shadow: 0 0 0 4px rgba(0, 123, 255, 0.25);
77
+ z-index: 1;
77
78
  }
78
79
 
79
- /* Focus state for keyboard navigation */
80
- .sft-tab-bar > input[type="radio"]:focus + label {
81
- box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.6);
82
- border-radius: 3px;
83
- outline: none;
84
- z-index: 1;
85
- position: relative;
80
+ /* Hover effects */
81
+ .sft-radio-group label:hover {
82
+ color: #495057;
83
+ background: rgba(0, 123, 255, 0.05);
86
84
  }
87
85
 
88
- /* The container for all content panels. */
89
- .sft-content {
90
- padding: 20px;
86
+ /* Content area */
87
+ .sft-content {
88
+ padding: 20px;
89
+ flex-basis: 100%; /* Add this line */
91
90
  }
92
91
 
93
- /* By default, hide all tab-specific panels. The dynamic CSS will show the active one. */
94
- .sft-content > .sft-panel {
92
+ /* Panels - hidden by default */
93
+ .sft-panel {
95
94
  display: none;
95
+ outline: none;
96
96
  }
97
97
 
98
- /* Always show the "General" panel, as it has no radio button to control it. */
99
- .sft-content > .sft-panel[data-filter="General"] {
98
+ /* Focus styling for panels */
99
+ .sft-panel:focus {
100
+ outline: 2px solid var(--sft-highlight-color, #007bff);
101
+ outline-offset: -2px;
102
+ border-radius: 4px;
103
+ }
104
+
105
+ /* General content panel - always visible */
106
+ .sft-panel[data-filter="General"] {
100
107
  display: block;
108
+ margin-bottom: 20px;
109
+ padding-bottom: 20px;
110
+ border-bottom: 1px solid #eee;
101
111
  }
102
112
 
103
- /* Ensure panels have proper focus styles for screen readers */
104
- .sft-content > .sft-panel[role="tabpanel"]:focus {
105
- outline: 2px solid var(--sft-tab-highlight-color, #007bff);
106
- outline-offset: 2px;
113
+ /* Screen reader only content */
114
+ .sr-only {
115
+ position: absolute;
116
+ width: 1px;
117
+ height: 1px;
118
+ padding: 0;
119
+ margin: -1px;
120
+ overflow: hidden;
121
+ clip: rect(0, 0, 0, 0);
122
+ white-space: nowrap;
123
+ border: 0;
107
124
  }
108
125
 
109
- /* Styles for collapsible sections */
126
+ /* Collapsible sections */
110
127
  .collapsible-section {
111
128
  border: 1px solid #e0e0e0;
112
129
  border-left: 4px solid var(--sft-collapsible-accent-color, #17a2b8);
@@ -147,20 +164,20 @@
147
164
  padding: 15px;
148
165
  }
149
166
 
150
- /* Support for high contrast mode */
167
+ /* High contrast mode support */
151
168
  @media (prefers-contrast: high) {
152
- .sft-tab-bar > input[type="radio"]:checked + label {
169
+ .sft-radio-group input[type="radio"]:checked + label {
153
170
  border-bottom-width: 4px;
154
171
  }
155
172
 
156
- .sft-tab-bar > input[type="radio"]:focus + label {
173
+ .sft-radio-group input[type="radio"]:focus + label {
157
174
  box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.8);
158
175
  }
159
176
  }
160
177
 
161
- /* Support for reduced motion preferences */
178
+ /* Reduced motion preferences */
162
179
  @media (prefers-reduced-motion: reduce) {
163
- .sft-tab-bar > label,
180
+ .sft-radio-group label,
164
181
  .custom-arrow {
165
182
  transition: none;
166
183
  }
@@ -1,17 +1,70 @@
1
- // Progressive enhancement for keyboard navigation
2
- // This file ensures proper keyboard navigation while maintaining CSS-only fallback
1
+ // Progressive enhancement for keyboard navigation and accessibility.
2
+ // This file ensures proper keyboard navigation and focus management,
3
+ // while maintaining a CSS-only fallback.
3
4
 
4
5
  (function() {
5
6
  'use strict';
6
7
 
7
- // Only enhance if the extension is present
8
+ // Only enhance if the extension's HTML is present on the page.
8
9
  if (!document.querySelector('.sft-container')) return;
9
10
 
11
+ /**
12
+ * Moves focus to the content panel associated with a given radio button.
13
+ * This improves accessibility by directing screen reader users to the new content.
14
+ * @param {HTMLInputElement} radio The radio button that was selected.
15
+ */
16
+ function focusOnPanel(radio) {
17
+ if (!radio.checked) return;
18
+
19
+ // Derive the panel's ID from the radio button's ID.
20
+ // e.g., 'filter-group-1-radio-0' becomes 'filter-group-1-panel-0'
21
+ const panelId = radio.id.replace('-radio-', '-panel-');
22
+ const panel = document.getElementById(panelId);
23
+
24
+ if (panel) {
25
+ panel.focus();
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Creates or updates a live region to announce tab changes to screen readers.
31
+ * @param {string} tabName The name of the selected tab.
32
+ */
33
+ function announceTabChange(tabName) {
34
+ // Create or find the live region for screen reader announcements.
35
+ let liveRegion = document.getElementById('tab-live-region');
36
+ if (!liveRegion) {
37
+ liveRegion = document.createElement('div');
38
+ liveRegion.id = 'tab-live-region';
39
+ liveRegion.setAttribute('role', 'status');
40
+ liveRegion.setAttribute('aria-live', 'polite');
41
+ liveRegion.setAttribute('aria-atomic', 'true');
42
+ // Hide the element visually but keep it accessible.
43
+ liveRegion.style.position = 'absolute';
44
+ liveRegion.style.left = '-10000px';
45
+ liveRegion.style.width = '1px';
46
+ liveRegion.style.height = '1px';
47
+ liveRegion.style.overflow = 'hidden';
48
+ document.body.appendChild(liveRegion);
49
+ }
50
+
51
+ // Update the announcement text.
52
+ liveRegion.textContent = `${tabName} tab selected`;
53
+
54
+ // Clear the announcement after a short delay to prevent clutter.
55
+ setTimeout(() => {
56
+ liveRegion.textContent = '';
57
+ }, 1000);
58
+ }
59
+
60
+ /**
61
+ * Initializes keyboard navigation for all filter-tab components on the page.
62
+ */
10
63
  function initTabKeyboardNavigation() {
11
64
  const containers = document.querySelectorAll('.sft-container');
12
65
 
13
66
  containers.forEach(container => {
14
- const tabBar = container.querySelector('.sft-tab-bar');
67
+ const tabBar = container.querySelector('.sft-radio-group');
15
68
  if (!tabBar) return;
16
69
 
17
70
  const radios = tabBar.querySelectorAll('input[type="radio"]');
@@ -19,14 +72,14 @@
19
72
 
20
73
  if (radios.length === 0 || labels.length === 0) return;
21
74
 
22
- // Make labels focusable for keyboard navigation
75
+ // Make labels focusable to act as keyboard navigation targets.
23
76
  labels.forEach(label => {
24
77
  if (!label.hasAttribute('tabindex')) {
25
78
  label.setAttribute('tabindex', '0');
26
79
  }
27
80
  });
28
-
29
- // Handle keyboard navigation on labels
81
+
82
+ // Handle keyboard navigation on the tab labels.
30
83
  labels.forEach((label, index) => {
31
84
  label.addEventListener('keydown', (event) => {
32
85
  let targetIndex = index;
@@ -59,7 +112,7 @@
59
112
 
60
113
  case 'Enter':
61
114
  case ' ':
62
- // Activate the associated radio button
115
+ // Activate the associated radio button on Enter/Space.
63
116
  event.preventDefault();
64
117
  if (radios[index]) {
65
118
  radios[index].checked = true;
@@ -72,7 +125,7 @@
72
125
  }
73
126
 
74
127
  if (handled) {
75
- // Move focus to target label and activate its radio
128
+ // Move focus to the target label and activate its radio button.
76
129
  labels[targetIndex].focus();
77
130
  if (radios[targetIndex]) {
78
131
  radios[targetIndex].checked = true;
@@ -82,46 +135,23 @@
82
135
  });
83
136
  });
84
137
 
85
- // Optional: Announce tab changes for screen readers
86
- if (window.config && window.config.filter_tabs_announce_changes !== false) {
87
- radios.forEach((radio, index) => {
88
- radio.addEventListener('change', () => {
89
- if (radio.checked && labels[index]) {
138
+ // Add listeners for announcements and focus management.
139
+ radios.forEach((radio, index) => {
140
+ radio.addEventListener('change', () => {
141
+ if (radio.checked) {
142
+ // Announce the change to screen readers.
143
+ if (labels[index]) {
90
144
  announceTabChange(labels[index].textContent.trim());
91
145
  }
92
- });
146
+ // Move focus to the newly visible panel.
147
+ focusOnPanel(radio);
148
+ }
93
149
  });
94
- }
150
+ });
95
151
  });
96
152
  }
97
153
 
98
- function announceTabChange(tabName) {
99
- // Create or update live region for screen reader announcements
100
- let liveRegion = document.getElementById('tab-live-region');
101
- if (!liveRegion) {
102
- liveRegion = document.createElement('div');
103
- liveRegion.id = 'tab-live-region';
104
- liveRegion.setAttribute('role', 'status');
105
- liveRegion.setAttribute('aria-live', 'polite');
106
- liveRegion.setAttribute('aria-atomic', 'true');
107
- liveRegion.style.position = 'absolute';
108
- liveRegion.style.left = '-10000px';
109
- liveRegion.style.width = '1px';
110
- liveRegion.style.height = '1px';
111
- liveRegion.style.overflow = 'hidden';
112
- document.body.appendChild(liveRegion);
113
- }
114
-
115
- // Update the announcement
116
- liveRegion.textContent = `${tabName} tab selected`;
117
-
118
- // Clear the announcement after a short delay
119
- setTimeout(() => {
120
- liveRegion.textContent = '';
121
- }, 1000);
122
- }
123
-
124
- // Initialize when DOM is ready
154
+ // Initialize the script once the DOM is ready.
125
155
  if (document.readyState === 'loading') {
126
156
  document.addEventListener('DOMContentLoaded', initTabKeyboardNavigation);
127
157
  } else {