sphinx-filter-tabs 1.2.2__py3-none-any.whl → 1.2.5__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.
filter_tabs/renderer.py CHANGED
@@ -46,31 +46,31 @@ class ContentTypeInferrer:
46
46
  (['development', 'staging', 'production', 'test', 'local'], 'environment'),
47
47
  (['source', 'binary', 'docker', 'manual', 'automatic'], 'installation method'),
48
48
  ]
49
-
49
+
50
50
  @classmethod
51
51
  def infer_type(cls, tab_names: List[str]) -> str:
52
52
  """
53
53
  Infer content type from a list of tab names.
54
-
54
+
55
55
  Args:
56
56
  tab_names: List of tab names to analyze
57
-
57
+
58
58
  Returns:
59
59
  Inferred content type string (e.g., 'programming language', 'operating system')
60
60
  """
61
61
  lower_names = [name.lower() for name in tab_names]
62
-
62
+
63
63
  # First pass: exact matches
64
64
  for keywords, content_type in cls.PATTERNS:
65
65
  if any(name in keywords for name in lower_names):
66
66
  return content_type
67
-
67
+
68
68
  # Second pass: substring matches
69
69
  for keywords, content_type in cls.PATTERNS:
70
70
  for name in lower_names:
71
71
  if any(keyword in name for keyword in keywords):
72
72
  return content_type
73
-
73
+
74
74
  # Default fallback
75
75
  return 'option'
76
76
 
@@ -92,53 +92,52 @@ class FilterTabsRenderer:
92
92
  self.tab_data = tab_data
93
93
  self.general_content = general_content
94
94
  self.custom_legend = custom_legend
95
-
95
+
96
96
  # 1. Load configuration first
97
97
  self.config = FilterTabsConfig.from_sphinx_config(self.app.config)
98
-
98
+
99
99
  # 2. Safely initialize the counter on the environment if it doesn't exist
100
100
  if not hasattr(self.env, 'filter_tabs_counter'):
101
101
  self.env.filter_tabs_counter = 0
102
-
102
+
103
103
  # 3. Increment the counter for this new tab group
104
104
  self.env.filter_tabs_counter += 1
105
-
105
+
106
106
  # 4. Generate the unique group ID and the ID generator instance
107
107
  self.group_id = f"filter-group-{self.env.filter_tabs_counter}"
108
108
  self.id_gen = IDGenerator(self.group_id)
109
-
109
+
110
110
  # 5. Perform debug logging now that config and group_id are set
111
111
  if self.config.debug_mode:
112
112
  logger.info(f"Initialized new tab group with id: '{self.group_id}'")
113
113
 
114
114
  def render_html(self) -> List[nodes.Node]:
115
- """Render HTML with a browser-compatible CSS approach."""
115
+ """Render HTML with CSS-only approach (no inline styles)."""
116
116
  if self.config.debug_mode:
117
117
  logger.info(f"Rendering filter-tabs group {self.group_id}")
118
118
 
119
- css_node = self._generate_compatible_css()
120
-
121
119
  container_attrs = self._get_container_attributes()
122
120
  container = ContainerNode(**container_attrs)
123
121
 
124
122
  fieldset = self._create_fieldset()
125
123
  container.children = [fieldset]
126
-
127
- return [css_node, container]
124
+
125
+ # FIXED: No more inline CSS generation
126
+ return [container]
128
127
 
129
128
  def render_fallback(self) -> List[nodes.Node]:
130
129
  """Render for non-HTML builders (e.g., LaTeX)."""
131
130
  output_nodes: List[nodes.Node] = []
132
-
131
+
133
132
  if self.general_content:
134
133
  output_nodes.extend(copy.deepcopy(self.general_content))
135
-
134
+
136
135
  for tab in self.tab_data:
137
136
  admonition = nodes.admonition()
138
137
  admonition += nodes.title(text=tab.name)
139
138
  admonition.extend(copy.deepcopy(tab.content))
140
139
  output_nodes.append(admonition)
141
-
140
+
142
141
  return output_nodes
143
142
 
144
143
  def _get_container_attributes(self) -> Dict[str, Any]:
@@ -150,42 +149,31 @@ class FilterTabsRenderer:
150
149
  'style': self.config.to_css_properties()
151
150
  }
152
151
 
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')
152
+ # REMOVED: _generate_compatible_css method - no longer needed!
165
153
 
166
154
  def _create_fieldset(self) -> FieldsetNode:
167
155
  """Create the main fieldset containing the legend, radio buttons, and panels."""
168
156
  fieldset = FieldsetNode(role="radiogroup")
169
-
157
+
170
158
  fieldset += self._create_legend()
171
-
159
+
172
160
  radio_group = ContainerNode(classes=[SFT_RADIO_GROUP])
173
161
  self._populate_radio_group(radio_group)
174
-
162
+
175
163
  content_area = ContainerNode(classes=[SFT_CONTENT])
176
164
  self._populate_content_area(content_area)
177
-
165
+
178
166
  # This is the fix: place the content_area inside the radio_group.
179
167
  radio_group += content_area
180
-
168
+
181
169
  fieldset += radio_group
182
-
170
+
183
171
  return fieldset
184
172
 
185
173
  def _create_legend(self) -> LegendNode:
186
174
  """Create a meaningful, visible legend for the tab group."""
187
175
  legend = LegendNode(classes=[SFT_LEGEND], ids=[self.id_gen.legend_id()])
188
-
176
+
189
177
  # Use the custom legend if it exists
190
178
  if self.custom_legend:
191
179
  legend_text = self.custom_legend
@@ -194,14 +182,14 @@ class FilterTabsRenderer:
194
182
  tab_names = [tab.name for tab in self.tab_data]
195
183
  content_type = ContentTypeInferrer.infer_type(tab_names)
196
184
  legend_text = f"Choose {content_type}: {', '.join(tab_names)}"
197
-
185
+
198
186
  legend += nodes.Text(legend_text)
199
187
  return legend
200
188
 
201
189
  def _populate_radio_group(self, radio_group: ContainerNode) -> None:
202
190
  """Create and add all radio buttons and labels to the radio group container."""
203
191
  default_index = next((i for i, tab in enumerate(self.tab_data) if tab.is_default), 0)
204
-
192
+
205
193
  for i, tab in enumerate(self.tab_data):
206
194
  radio_group += self._create_radio_button(i, tab, is_checked=(i == default_index))
207
195
  radio_group += self._create_label(i, tab)
@@ -241,7 +229,7 @@ class FilterTabsRenderer:
241
229
  general_panel = PanelNode(classes=[SFT_PANEL], **{'data-filter': 'General'})
242
230
  general_panel.extend(copy.deepcopy(self.general_content))
243
231
  content_area += general_panel
244
-
232
+
245
233
  for i, tab in enumerate(self.tab_data):
246
234
  content_area += self._create_tab_panel(i, tab)
247
235
 
@@ -250,7 +238,7 @@ class FilterTabsRenderer:
250
238
  panel_attrs = {
251
239
  'classes': [SFT_PANEL],
252
240
  'ids': [self.id_gen.panel_id(index)],
253
- 'role': 'region',
241
+ 'role': 'tabpanel',
254
242
  'aria-labelledby': self.id_gen.radio_id(index),
255
243
  'tabindex': '0',
256
244
  'data-tab': tab.name.lower().replace(' ', '-')
@@ -258,4 +246,3 @@ class FilterTabsRenderer:
258
246
  panel = PanelNode(**panel_attrs)
259
247
  panel.extend(copy.deepcopy(tab.content))
260
248
  return panel
261
-
@@ -86,7 +86,7 @@
86
86
  /* Content area */
87
87
  .sft-content {
88
88
  padding: 20px;
89
- flex-basis: 100%; /* Add this line */
89
+ flex-basis: 100%;
90
90
  }
91
91
 
92
92
  /* Panels - hidden by default */
@@ -110,6 +110,32 @@
110
110
  border-bottom: 1px solid #eee;
111
111
  }
112
112
 
113
+ /*
114
+ * FIXED: Panel visibility using CSS without inline styles
115
+ * This uses a general approach with nth-child selectors
116
+ * No more inline <style> elements needed!
117
+ */
118
+
119
+ /* Show panels when corresponding radio is checked */
120
+ .sft-radio-group input[type="radio"]:nth-child(1):checked ~ .sft-content .sft-panel:nth-of-type(1),
121
+ .sft-radio-group input[type="radio"]:nth-child(3):checked ~ .sft-content .sft-panel:nth-of-type(2),
122
+ .sft-radio-group input[type="radio"]:nth-child(5):checked ~ .sft-content .sft-panel:nth-of-type(3),
123
+ .sft-radio-group input[type="radio"]:nth-child(7):checked ~ .sft-content .sft-panel:nth-of-type(4),
124
+ .sft-radio-group input[type="radio"]:nth-child(9):checked ~ .sft-content .sft-panel:nth-of-type(5),
125
+ .sft-radio-group input[type="radio"]:nth-child(11):checked ~ .sft-content .sft-panel:nth-of-type(6),
126
+ .sft-radio-group input[type="radio"]:nth-child(13):checked ~ .sft-content .sft-panel:nth-of-type(7),
127
+ .sft-radio-group input[type="radio"]:nth-child(15):checked ~ .sft-content .sft-panel:nth-of-type(8),
128
+ .sft-radio-group input[type="radio"]:nth-child(17):checked ~ .sft-content .sft-panel:nth-of-type(9),
129
+ .sft-radio-group input[type="radio"]:nth-child(19):checked ~ .sft-content .sft-panel:nth-of-type(10) {
130
+ display: block;
131
+ }
132
+
133
+ /* Alternative approach using CSS custom properties (more elegant) */
134
+ /*
135
+ * If you prefer a more modern approach, you could use CSS custom properties
136
+ * and update the renderer to set --active-tab instead of generating CSS rules
137
+ */
138
+
113
139
  /* Screen reader only content */
114
140
  .sr-only {
115
141
  position: absolute;
@@ -1,13 +1,12 @@
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.
1
+ // Progressive enhancement for focus management and accessibility announcements.
2
+ // This file provides enhancements while maintaining native radio button keyboard behavior.
4
3
 
5
4
  (function() {
6
5
  'use strict';
7
-
6
+
8
7
  // Only enhance if the extension's HTML is present on the page.
9
8
  if (!document.querySelector('.sft-container')) return;
10
-
9
+
11
10
  /**
12
11
  * Moves focus to the content panel associated with a given radio button.
13
12
  * This improves accessibility by directing screen reader users to the new content.
@@ -15,12 +14,12 @@
15
14
  */
16
15
  function focusOnPanel(radio) {
17
16
  if (!radio.checked) return;
18
-
17
+
19
18
  // Derive the panel's ID from the radio button's ID.
20
19
  // e.g., 'filter-group-1-radio-0' becomes 'filter-group-1-panel-0'
21
20
  const panelId = radio.id.replace('-radio-', '-panel-');
22
21
  const panel = document.getElementById(panelId);
23
-
22
+
24
23
  if (panel) {
25
24
  panel.focus();
26
25
  }
@@ -47,10 +46,10 @@
47
46
  liveRegion.style.overflow = 'hidden';
48
47
  document.body.appendChild(liveRegion);
49
48
  }
50
-
49
+
51
50
  // Update the announcement text.
52
51
  liveRegion.textContent = `${tabName} tab selected`;
53
-
52
+
54
53
  // Clear the announcement after a short delay to prevent clutter.
55
54
  setTimeout(() => {
56
55
  liveRegion.textContent = '';
@@ -58,103 +57,45 @@
58
57
  }
59
58
 
60
59
  /**
61
- * Initializes keyboard navigation for all filter-tab components on the page.
60
+ * Initializes progressive enhancements for all filter-tab components.
61
+ * REMOVED: Custom keyboard navigation (now uses native radio button behavior)
62
+ * KEPT: Focus management and screen reader announcements
62
63
  */
63
- function initTabKeyboardNavigation() {
64
+ function initTabEnhancements() {
64
65
  const containers = document.querySelectorAll('.sft-container');
65
-
66
+
66
67
  containers.forEach(container => {
67
68
  const tabBar = container.querySelector('.sft-radio-group');
68
69
  if (!tabBar) return;
69
-
70
+
70
71
  const radios = tabBar.querySelectorAll('input[type="radio"]');
71
72
  const labels = tabBar.querySelectorAll('label');
72
-
73
+
73
74
  if (radios.length === 0 || labels.length === 0) return;
74
-
75
- // Make labels focusable to act as keyboard navigation targets.
76
- labels.forEach(label => {
77
- if (!label.hasAttribute('tabindex')) {
78
- label.setAttribute('tabindex', '0');
79
- }
80
- });
81
75
 
82
- // Handle keyboard navigation on the tab labels.
83
- labels.forEach((label, index) => {
84
- label.addEventListener('keydown', (event) => {
85
- let targetIndex = index;
86
- let handled = false;
87
-
88
- switch (event.key) {
89
- case 'ArrowRight':
90
- event.preventDefault();
91
- targetIndex = (index + 1) % labels.length;
92
- handled = true;
93
- break;
94
-
95
- case 'ArrowLeft':
96
- event.preventDefault();
97
- targetIndex = (index - 1 + labels.length) % labels.length;
98
- handled = true;
99
- break;
100
-
101
- case 'Home':
102
- event.preventDefault();
103
- targetIndex = 0;
104
- handled = true;
105
- break;
106
-
107
- case 'End':
108
- event.preventDefault();
109
- targetIndex = labels.length - 1;
110
- handled = true;
111
- break;
112
-
113
- case 'Enter':
114
- case ' ':
115
- // Activate the associated radio button on Enter/Space.
116
- event.preventDefault();
117
- if (radios[index]) {
118
- radios[index].checked = true;
119
- radios[index].dispatchEvent(new Event('change', { bubbles: true }));
120
- }
121
- return;
122
-
123
- default:
124
- return;
125
- }
126
-
127
- if (handled) {
128
- // Move focus to the target label and activate its radio button.
129
- labels[targetIndex].focus();
130
- if (radios[targetIndex]) {
131
- radios[targetIndex].checked = true;
132
- radios[targetIndex].dispatchEvent(new Event('change', { bubbles: true }));
133
- }
134
- }
135
- });
136
- });
137
-
138
- // Add listeners for announcements and focus management.
76
+ // Add change listeners for announcements and focus management
139
77
  radios.forEach((radio, index) => {
140
78
  radio.addEventListener('change', () => {
141
79
  if (radio.checked) {
142
- // Announce the change to screen readers.
143
- if (labels[index]) {
144
- announceTabChange(labels[index].textContent.trim());
145
- }
146
- // Move focus to the newly visible panel.
80
+ // Get the tab name from the associated label
81
+ const label = labels[index];
82
+ const tabName = label ? label.textContent.trim() : 'Unknown';
83
+
84
+ // Announce the change to screen readers
85
+ announceTabChange(tabName);
86
+
87
+ // Move focus to the newly visible panel
147
88
  focusOnPanel(radio);
148
89
  }
149
90
  });
150
91
  });
151
92
  });
152
93
  }
153
-
154
- // Initialize the script once the DOM is ready.
94
+
95
+ // Initialize the enhancements once the DOM is ready
155
96
  if (document.readyState === 'loading') {
156
- document.addEventListener('DOMContentLoaded', initTabKeyboardNavigation);
97
+ document.addEventListener('DOMContentLoaded', initTabEnhancements);
157
98
  } else {
158
- initTabKeyboardNavigation();
99
+ initTabEnhancements();
159
100
  }
160
101
  })();
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sphinx-filter-tabs
3
- Version: 1.2.2
3
+ Version: 1.2.5
4
4
  Summary: A Sphinx extension for accessible, CSS-first filterable content tabs.
5
5
  Author-email: Aputsiak Niels Janussen <aputtu+sphinx@gmail.com>
6
6
  License: GNU General Public License v3.0
@@ -0,0 +1,11 @@
1
+ filter_tabs/__init__.py,sha256=VPpIhj4HaLeMX7ai7dZFkUm81ii2ePPGjCd9hsMjsN4,397
2
+ filter_tabs/extension.py,sha256=Dspt9r8TVoYChHFvGsncuV01GEga7BOOsPaMwYdMQcg,18014
3
+ filter_tabs/renderer.py,sha256=m0_sD5ujtT4rmhYlhm6vMgSn0W1WAJPJWaTjh6zXezY,9730
4
+ filter_tabs/static/filter_tabs.css,sha256=EbsG5zdEv8g3rdV7y76CXSwEMiBaBTX01qFYDiXeJt0,5379
5
+ filter_tabs/static/filter_tabs.js,sha256=URduEo1P8y_-TaT485U6APGXsdQg6rFBpib3yaNc-9g,3989
6
+ sphinx_filter_tabs-1.2.5.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
7
+ sphinx_filter_tabs-1.2.5.dist-info/METADATA,sha256=nH86wxyNL4OAwRnPL0W8k4vJcj0mxKpoc_bk_2b2cv0,3483
8
+ sphinx_filter_tabs-1.2.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ sphinx_filter_tabs-1.2.5.dist-info/entry_points.txt,sha256=za_bQcueY8AHyq7XnnjkW9X3C-LsZjeERVQ_ds7jV1A,62
10
+ sphinx_filter_tabs-1.2.5.dist-info/top_level.txt,sha256=K0Iy-6EsYYdvlyXdsJT0SQg-BLDBgT5-Y8ZKy5SNAfc,12
11
+ sphinx_filter_tabs-1.2.5.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- filter_tabs/__init__.py,sha256=VPpIhj4HaLeMX7ai7dZFkUm81ii2ePPGjCd9hsMjsN4,397
2
- filter_tabs/extension.py,sha256=Dspt9r8TVoYChHFvGsncuV01GEga7BOOsPaMwYdMQcg,18014
3
- filter_tabs/renderer.py,sha256=e_nUV67ojDHfEXWB7jFkclzy4fOLotluKBg573fGng8,10436
4
- filter_tabs/static/filter_tabs.css,sha256=V7y8BCp0UTQ1KXZNeIYtfz0Zt6b-uJPKYeXcx4PHz4A,3913
5
- filter_tabs/static/filter_tabs.js,sha256=-d_TJREyNxu9TtQNDLvQ0G5qCVWR2TxwrWmApGs2CBo,6559
6
- sphinx_filter_tabs-1.2.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
7
- sphinx_filter_tabs-1.2.2.dist-info/METADATA,sha256=qX7ycCD1JfwXgju5W5pnlhoN1p09cnR_U9yUQZrFpBk,3483
8
- sphinx_filter_tabs-1.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
- sphinx_filter_tabs-1.2.2.dist-info/entry_points.txt,sha256=za_bQcueY8AHyq7XnnjkW9X3C-LsZjeERVQ_ds7jV1A,62
10
- sphinx_filter_tabs-1.2.2.dist-info/top_level.txt,sha256=K0Iy-6EsYYdvlyXdsJT0SQg-BLDBgT5-Y8ZKy5SNAfc,12
11
- sphinx_filter_tabs-1.2.2.dist-info/RECORD,,