sphinx-filter-tabs 1.2.2__py3-none-any.whl → 1.2.6__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/extension.py +22 -7
- filter_tabs/renderer.py +48 -51
- filter_tabs/static/filter_tabs.css +146 -6
- sphinx_filter_tabs-1.2.6.dist-info/METADATA +267 -0
- sphinx_filter_tabs-1.2.6.dist-info/RECORD +10 -0
- filter_tabs/static/filter_tabs.js +0 -160
- sphinx_filter_tabs-1.2.2.dist-info/METADATA +0 -70
- sphinx_filter_tabs-1.2.2.dist-info/RECORD +0 -11
- {sphinx_filter_tabs-1.2.2.dist-info → sphinx_filter_tabs-1.2.6.dist-info}/WHEEL +0 -0
- {sphinx_filter_tabs-1.2.2.dist-info → sphinx_filter_tabs-1.2.6.dist-info}/entry_points.txt +0 -0
- {sphinx_filter_tabs-1.2.2.dist-info → sphinx_filter_tabs-1.2.6.dist-info}/licenses/LICENSE +0 -0
- {sphinx_filter_tabs-1.2.2.dist-info → sphinx_filter_tabs-1.2.6.dist-info}/top_level.txt +0 -0
filter_tabs/extension.py
CHANGED
|
@@ -419,12 +419,31 @@ def depart_summary_node(self: HTML5Translator, node: SummaryNode) -> None:
|
|
|
419
419
|
self.body.append('</summary>')
|
|
420
420
|
|
|
421
421
|
|
|
422
|
+
# =============================================================================
|
|
423
|
+
# Use ARIA to Improve Strong Element Behavior
|
|
424
|
+
# =============================================================================
|
|
425
|
+
|
|
426
|
+
def improve_inline_formatting(app: Sphinx, doctree: nodes.document, docname: str):
|
|
427
|
+
"""Improve screen reader handling of inline formatting."""
|
|
428
|
+
if app.builder.name != 'html':
|
|
429
|
+
return
|
|
430
|
+
|
|
431
|
+
# Find all strong/emphasis nodes and add ARIA attributes
|
|
432
|
+
for node in doctree.findall(nodes.strong):
|
|
433
|
+
# Add aria-label to make the content read as one unit
|
|
434
|
+
text_content = node.astext()
|
|
435
|
+
node['aria-label'] = text_content
|
|
436
|
+
|
|
437
|
+
for node in doctree.findall(nodes.emphasis):
|
|
438
|
+
text_content = node.astext()
|
|
439
|
+
node['aria-label'] = text_content
|
|
440
|
+
|
|
422
441
|
# =============================================================================
|
|
423
442
|
# Static File Handling
|
|
424
443
|
# =============================================================================
|
|
425
444
|
|
|
426
445
|
def copy_static_files(app: Sphinx):
|
|
427
|
-
"""Copy CSS
|
|
446
|
+
"""Copy CSS file to the build directory."""
|
|
428
447
|
if app.builder.name != 'html':
|
|
429
448
|
return
|
|
430
449
|
|
|
@@ -436,11 +455,7 @@ def copy_static_files(app: Sphinx):
|
|
|
436
455
|
css_file = static_source_dir / "filter_tabs.css"
|
|
437
456
|
if css_file.exists():
|
|
438
457
|
shutil.copy(css_file, dest_dir)
|
|
439
|
-
|
|
440
|
-
# Copy JS file if it exists
|
|
441
|
-
js_file = static_source_dir / "filter_tabs.js"
|
|
442
|
-
if js_file.exists():
|
|
443
|
-
shutil.copy(js_file, dest_dir)
|
|
458
|
+
|
|
444
459
|
|
|
445
460
|
|
|
446
461
|
# =============================================================================
|
|
@@ -456,7 +471,6 @@ def setup(app: Sphinx) -> Dict[str, Any]:
|
|
|
456
471
|
|
|
457
472
|
# Add static files
|
|
458
473
|
app.add_css_file('filter_tabs.css')
|
|
459
|
-
app.add_js_file('filter_tabs.js')
|
|
460
474
|
|
|
461
475
|
# Register custom nodes (keep existing node registration code)
|
|
462
476
|
app.add_node(ContainerNode, html=(visit_container_node, depart_container_node))
|
|
@@ -475,6 +489,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
|
|
|
475
489
|
# Connect event handlers
|
|
476
490
|
app.connect('builder-inited', copy_static_files)
|
|
477
491
|
app.connect('doctree-resolved', setup_collapsible_admonitions)
|
|
492
|
+
app.connect('doctree-resolved', improve_inline_formatting)
|
|
478
493
|
|
|
479
494
|
return {
|
|
480
495
|
'version': __version__,
|
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
|
|
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
|
-
|
|
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,29 @@ 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')
|
|
165
|
-
|
|
166
152
|
def _create_fieldset(self) -> FieldsetNode:
|
|
167
153
|
"""Create the main fieldset containing the legend, radio buttons, and panels."""
|
|
168
154
|
fieldset = FieldsetNode(role="radiogroup")
|
|
169
|
-
|
|
155
|
+
|
|
170
156
|
fieldset += self._create_legend()
|
|
171
|
-
|
|
157
|
+
|
|
172
158
|
radio_group = ContainerNode(classes=[SFT_RADIO_GROUP])
|
|
173
159
|
self._populate_radio_group(radio_group)
|
|
174
|
-
|
|
160
|
+
|
|
175
161
|
content_area = ContainerNode(classes=[SFT_CONTENT])
|
|
176
162
|
self._populate_content_area(content_area)
|
|
177
|
-
|
|
163
|
+
|
|
178
164
|
# This is the fix: place the content_area inside the radio_group.
|
|
179
165
|
radio_group += content_area
|
|
180
|
-
|
|
166
|
+
|
|
181
167
|
fieldset += radio_group
|
|
182
|
-
|
|
168
|
+
|
|
183
169
|
return fieldset
|
|
184
170
|
|
|
185
171
|
def _create_legend(self) -> LegendNode:
|
|
186
172
|
"""Create a meaningful, visible legend for the tab group."""
|
|
187
173
|
legend = LegendNode(classes=[SFT_LEGEND], ids=[self.id_gen.legend_id()])
|
|
188
|
-
|
|
174
|
+
|
|
189
175
|
# Use the custom legend if it exists
|
|
190
176
|
if self.custom_legend:
|
|
191
177
|
legend_text = self.custom_legend
|
|
@@ -194,27 +180,30 @@ class FilterTabsRenderer:
|
|
|
194
180
|
tab_names = [tab.name for tab in self.tab_data]
|
|
195
181
|
content_type = ContentTypeInferrer.infer_type(tab_names)
|
|
196
182
|
legend_text = f"Choose {content_type}: {', '.join(tab_names)}"
|
|
197
|
-
|
|
183
|
+
|
|
198
184
|
legend += nodes.Text(legend_text)
|
|
199
185
|
return legend
|
|
200
186
|
|
|
201
187
|
def _populate_radio_group(self, radio_group: ContainerNode) -> None:
|
|
202
188
|
"""Create and add all radio buttons and labels to the radio group container."""
|
|
203
189
|
default_index = next((i for i, tab in enumerate(self.tab_data) if tab.is_default), 0)
|
|
204
|
-
|
|
190
|
+
|
|
205
191
|
for i, tab in enumerate(self.tab_data):
|
|
206
192
|
radio_group += self._create_radio_button(i, tab, is_checked=(i == default_index))
|
|
207
193
|
radio_group += self._create_label(i, tab)
|
|
208
194
|
radio_group += self._create_screen_reader_description(i, tab)
|
|
209
195
|
|
|
210
196
|
def _create_radio_button(self, index: int, tab: TabData, is_checked: bool) -> RadioInputNode:
|
|
211
|
-
"""Create a single radio button input."""
|
|
197
|
+
"""Create a single radio button input with data attribute."""
|
|
212
198
|
radio = RadioInputNode(
|
|
213
199
|
classes=['sr-only'],
|
|
214
200
|
type='radio',
|
|
215
201
|
name=self.group_id,
|
|
216
202
|
ids=[self.id_gen.radio_id(index)],
|
|
217
|
-
**{
|
|
203
|
+
**{
|
|
204
|
+
'aria-describedby': self.id_gen.desc_id(index),
|
|
205
|
+
'data-tab-index': str(index) # FIXED: Add data attribute
|
|
206
|
+
}
|
|
218
207
|
)
|
|
219
208
|
if tab.aria_label:
|
|
220
209
|
radio['aria-label'] = tab.aria_label
|
|
@@ -236,26 +225,34 @@ class FilterTabsRenderer:
|
|
|
236
225
|
return description_node
|
|
237
226
|
|
|
238
227
|
def _populate_content_area(self, content_area: ContainerNode) -> None:
|
|
239
|
-
"""Create and add all general and tab-specific content panels."""
|
|
228
|
+
"""Create and add all general and tab-specific content panels with accessibility enhancements."""
|
|
240
229
|
if self.general_content:
|
|
241
|
-
general_panel = PanelNode(
|
|
230
|
+
general_panel = PanelNode(
|
|
231
|
+
classes=[SFT_PANEL],
|
|
232
|
+
**{
|
|
233
|
+
'data-filter': 'General',
|
|
234
|
+
'aria-label': 'General information',
|
|
235
|
+
'role': 'region'
|
|
236
|
+
}
|
|
237
|
+
)
|
|
242
238
|
general_panel.extend(copy.deepcopy(self.general_content))
|
|
243
239
|
content_area += general_panel
|
|
244
|
-
|
|
240
|
+
|
|
245
241
|
for i, tab in enumerate(self.tab_data):
|
|
246
242
|
content_area += self._create_tab_panel(i, tab)
|
|
247
243
|
|
|
248
244
|
def _create_tab_panel(self, index: int, tab: TabData) -> PanelNode:
|
|
249
|
-
"""Create a single content panel for a tab."""
|
|
245
|
+
"""Create a single content panel for a tab - CSS only version."""
|
|
250
246
|
panel_attrs = {
|
|
251
247
|
'classes': [SFT_PANEL],
|
|
252
248
|
'ids': [self.id_gen.panel_id(index)],
|
|
253
|
-
'role': '
|
|
249
|
+
'role': 'tabpanel',
|
|
254
250
|
'aria-labelledby': self.id_gen.radio_id(index),
|
|
255
|
-
'tabindex': '0',
|
|
256
|
-
'data-tab': tab.name.lower().replace(' ', '-')
|
|
251
|
+
'tabindex': '0', # Keep for keyboard accessibility
|
|
252
|
+
'data-tab': tab.name.lower().replace(' ', '-'),
|
|
253
|
+
'data-tab-index': str(index)
|
|
257
254
|
}
|
|
258
255
|
panel = PanelNode(**panel_attrs)
|
|
259
256
|
panel.extend(copy.deepcopy(tab.content))
|
|
260
257
|
return panel
|
|
261
|
-
|
|
258
|
+
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Sphinx Filter Tabs -
|
|
1
|
+
/* Sphinx Filter Tabs - Enhanced CSS with improved accessibility */
|
|
2
2
|
|
|
3
3
|
/* Main container */
|
|
4
4
|
.sft-container {
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
/* Content area */
|
|
87
87
|
.sft-content {
|
|
88
88
|
padding: 20px;
|
|
89
|
-
flex-basis: 100%;
|
|
89
|
+
flex-basis: 100%;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/* Panels - hidden by default */
|
|
@@ -95,11 +95,39 @@
|
|
|
95
95
|
outline: none;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
/*
|
|
98
|
+
/* Enhanced focus styling for CSS-only version */
|
|
99
99
|
.sft-panel:focus {
|
|
100
|
-
outline:
|
|
101
|
-
outline-offset:
|
|
100
|
+
outline: 3px solid var(--sft-highlight-color, #007bff);
|
|
101
|
+
outline-offset: 2px;
|
|
102
102
|
border-radius: 4px;
|
|
103
|
+
background: rgba(0, 123, 255, 0.05);
|
|
104
|
+
/* Ensure smooth transition */
|
|
105
|
+
transition: background-color 0.2s ease;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* Visual focus indicator for better UX */
|
|
109
|
+
.sft-panel:focus::before {
|
|
110
|
+
content: "→ ";
|
|
111
|
+
color: var(--sft-highlight-color, #007bff);
|
|
112
|
+
font-weight: bold;
|
|
113
|
+
margin-right: 0.5em;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Improve screen reader flow for inline formatting */
|
|
117
|
+
strong, b, em, i {
|
|
118
|
+
speak: normal;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* Ensure tab panels are easily discoverable when focused */
|
|
122
|
+
.sft-panel[role="tabpanel"]:focus {
|
|
123
|
+
position: relative;
|
|
124
|
+
z-index: 1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* ENHANCED: Focus styling for content within panels */
|
|
128
|
+
.sft-panel:focus-within {
|
|
129
|
+
outline: 1px solid var(--sft-highlight-color, #007bff);
|
|
130
|
+
outline-offset: -1px;
|
|
103
131
|
}
|
|
104
132
|
|
|
105
133
|
/* General content panel - always visible */
|
|
@@ -110,6 +138,72 @@
|
|
|
110
138
|
border-bottom: 1px solid #eee;
|
|
111
139
|
}
|
|
112
140
|
|
|
141
|
+
/* Panel visibility using data attributes */
|
|
142
|
+
.sft-radio-group input[type="radio"][data-tab-index="0"]:checked ~ .sft-content .sft-panel[data-tab-index="0"],
|
|
143
|
+
.sft-radio-group input[type="radio"][data-tab-index="1"]:checked ~ .sft-content .sft-panel[data-tab-index="1"],
|
|
144
|
+
.sft-radio-group input[type="radio"][data-tab-index="2"]:checked ~ .sft-content .sft-panel[data-tab-index="2"],
|
|
145
|
+
.sft-radio-group input[type="radio"][data-tab-index="3"]:checked ~ .sft-content .sft-panel[data-tab-index="3"],
|
|
146
|
+
.sft-radio-group input[type="radio"][data-tab-index="4"]:checked ~ .sft-content .sft-panel[data-tab-index="4"],
|
|
147
|
+
.sft-radio-group input[type="radio"][data-tab-index="5"]:checked ~ .sft-content .sft-panel[data-tab-index="5"],
|
|
148
|
+
.sft-radio-group input[type="radio"][data-tab-index="6"]:checked ~ .sft-content .sft-panel[data-tab-index="6"],
|
|
149
|
+
.sft-radio-group input[type="radio"][data-tab-index="7"]:checked ~ .sft-content .sft-panel[data-tab-index="7"],
|
|
150
|
+
.sft-radio-group input[type="radio"][data-tab-index="8"]:checked ~ .sft-content .sft-panel[data-tab-index="8"],
|
|
151
|
+
.sft-radio-group input[type="radio"][data-tab-index="9"]:checked ~ .sft-content .sft-panel[data-tab-index="9"] {
|
|
152
|
+
display: block;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* ENHANCED: Accessibility improvements for content within panels */
|
|
156
|
+
|
|
157
|
+
/* Make paragraphs within panels more accessible */
|
|
158
|
+
.sft-panel p {
|
|
159
|
+
margin: 0.75em 0;
|
|
160
|
+
line-height: 1.5;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.sft-panel p:first-child {
|
|
164
|
+
margin-top: 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.sft-panel p:last-child {
|
|
168
|
+
margin-bottom: 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* Enhanced code block accessibility */
|
|
172
|
+
.sft-panel .highlight pre,
|
|
173
|
+
.sft-panel code {
|
|
174
|
+
/* Ensure code blocks are focusable and readable */
|
|
175
|
+
border-radius: 4px;
|
|
176
|
+
position: relative;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.sft-panel .highlight pre:focus,
|
|
180
|
+
.sft-panel code:focus {
|
|
181
|
+
outline: 2px solid var(--sft-highlight-color, #007bff);
|
|
182
|
+
outline-offset: 2px;
|
|
183
|
+
/* Ensure the content is announced to screen readers */
|
|
184
|
+
z-index: 1;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* Make lists more accessible */
|
|
188
|
+
.sft-panel ul,
|
|
189
|
+
.sft-panel ol {
|
|
190
|
+
margin: 0.75em 0;
|
|
191
|
+
padding-left: 2em;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.sft-panel li {
|
|
195
|
+
margin: 0.25em 0;
|
|
196
|
+
line-height: 1.4;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* Enhanced blockquote accessibility */
|
|
200
|
+
.sft-panel blockquote {
|
|
201
|
+
margin: 1em 0;
|
|
202
|
+
padding: 0.5em 1em;
|
|
203
|
+
border-left: 4px solid #ddd;
|
|
204
|
+
background: #f9f9f9;
|
|
205
|
+
}
|
|
206
|
+
|
|
113
207
|
/* Screen reader only content */
|
|
114
208
|
.sr-only {
|
|
115
209
|
position: absolute;
|
|
@@ -123,6 +217,23 @@
|
|
|
123
217
|
border: 0;
|
|
124
218
|
}
|
|
125
219
|
|
|
220
|
+
/* ENHANCED: Skip link for better keyboard navigation */
|
|
221
|
+
.sft-container .skip-to-content {
|
|
222
|
+
position: absolute;
|
|
223
|
+
top: -40px;
|
|
224
|
+
left: 6px;
|
|
225
|
+
background: var(--sft-highlight-color, #007bff);
|
|
226
|
+
color: white;
|
|
227
|
+
padding: 8px;
|
|
228
|
+
text-decoration: none;
|
|
229
|
+
border-radius: 4px;
|
|
230
|
+
z-index: 1000;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.sft-container .skip-to-content:focus {
|
|
234
|
+
top: 6px;
|
|
235
|
+
}
|
|
236
|
+
|
|
126
237
|
/* Collapsible sections */
|
|
127
238
|
.collapsible-section {
|
|
128
239
|
border: 1px solid #e0e0e0;
|
|
@@ -164,14 +275,22 @@
|
|
|
164
275
|
padding: 15px;
|
|
165
276
|
}
|
|
166
277
|
|
|
167
|
-
/* High contrast mode support */
|
|
278
|
+
/* ENHANCED: High contrast mode support with better visibility */
|
|
168
279
|
@media (prefers-contrast: high) {
|
|
169
280
|
.sft-radio-group input[type="radio"]:checked + label {
|
|
170
281
|
border-bottom-width: 4px;
|
|
282
|
+
background: #000;
|
|
283
|
+
color: #fff;
|
|
171
284
|
}
|
|
172
285
|
|
|
173
286
|
.sft-radio-group input[type="radio"]:focus + label {
|
|
174
287
|
box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.8);
|
|
288
|
+
outline: 3px solid #fff;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.sft-panel:focus {
|
|
292
|
+
outline: 3px solid #000;
|
|
293
|
+
background: #fff;
|
|
175
294
|
}
|
|
176
295
|
}
|
|
177
296
|
|
|
@@ -182,3 +301,24 @@
|
|
|
182
301
|
transition: none;
|
|
183
302
|
}
|
|
184
303
|
}
|
|
304
|
+
|
|
305
|
+
/* ENHANCED: Screen reader specific improvements */
|
|
306
|
+
@media (prefers-reduced-motion: reduce) {
|
|
307
|
+
/* Ensure focus changes are immediate for screen reader users */
|
|
308
|
+
.sft-panel {
|
|
309
|
+
transition: none;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/* Force visibility for screen reader testing */
|
|
314
|
+
@media (forced-colors: active) {
|
|
315
|
+
.sft-panel {
|
|
316
|
+
border: 1px solid ButtonText;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.sft-radio-group input[type="radio"]:checked + label {
|
|
320
|
+
border: 2px solid Highlight;
|
|
321
|
+
background: Highlight;
|
|
322
|
+
color: HighlightText;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sphinx-filter-tabs
|
|
3
|
+
Version: 1.2.6
|
|
4
|
+
Summary: A Sphinx extension for accessible, CSS-first filterable content tabs.
|
|
5
|
+
Author-email: Aputsiak Niels Janussen <aputtu+sphinx@gmail.com>
|
|
6
|
+
License: GNU General Public License v3.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/aputtu/sphinx-filter-tabs
|
|
8
|
+
Project-URL: Repository, https://github.com/aputtu/sphinx-filter-tabs.git
|
|
9
|
+
Project-URL: Issues, https://github.com/aputtu/sphinx-filter-tabs/issues
|
|
10
|
+
Keywords: sphinx,extension,tabs,filter,documentation,css-only,accessibility,keyboard-navigation
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Framework :: Sphinx :: Extension
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Documentation :: Sphinx
|
|
20
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: Sphinx<9.0,>=7.0
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# Sphinx Filter Tabs Extension
|
|
28
|
+
|
|
29
|
+
[](https://github.com/aputtu/sphinx-filter-tabs/actions/workflows/test.yml)
|
|
30
|
+
[](https://pypi.org/project/sphinx-filter-tabs/)
|
|
31
|
+
[](https://pypi.org/project/sphinx-filter-tabs/)
|
|
32
|
+
[](https://github.com/aputtu/sphinx-filter-tabs/blob/main/LICENSE)
|
|
33
|
+
|
|
34
|
+
A robust Sphinx extension for creating accessible, filterable content tabs using pure CSS and semantic HTML.
|
|
35
|
+
|
|
36
|
+
**📖 View extension and documentation at: https://aputtu.github.io/sphinx-filter-tabs/**
|
|
37
|
+
|
|
38
|
+
This extension provides `filter-tabs` and `tab` directives to create user-friendly, switchable content blocks. Perfect for showing code examples in multiple languages, installation instructions for different platforms, or any content that benefits from organized, filterable presentation.
|
|
39
|
+
|
|
40
|
+
## Key Features
|
|
41
|
+
|
|
42
|
+
- **Pure CSS Implementation:** Zero JavaScript dependencies for maximum compatibility and performance
|
|
43
|
+
- **Fully Accessible:** WAI-ARIA compliant with native keyboard navigation and screen reader support
|
|
44
|
+
- **Semantic HTML:** Uses standard form controls (radio buttons) for robust, predictable behavior
|
|
45
|
+
- **Universal Compatibility:** Works in all environments where CSS is supported, including strict CSP policies
|
|
46
|
+
- **Easy Customization:** Theme colors and styling through simple CSS custom properties
|
|
47
|
+
- **Multiple Output Formats:** Graceful fallback to sequential content in PDF/LaTeX builds
|
|
48
|
+
- **Proven Reliability:** Comprehensive test suite across multiple Python and Sphinx versions
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
### Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install sphinx-filter-tabs
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Enable the Extension
|
|
59
|
+
|
|
60
|
+
Add to your `conf.py`:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
extensions = [
|
|
64
|
+
# ... your other extensions ...
|
|
65
|
+
'filter_tabs.extension',
|
|
66
|
+
]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Basic Usage
|
|
70
|
+
|
|
71
|
+
```rst
|
|
72
|
+
.. filter-tabs::
|
|
73
|
+
|
|
74
|
+
This content appears above all tabs.
|
|
75
|
+
|
|
76
|
+
.. tab:: Python
|
|
77
|
+
|
|
78
|
+
Install using pip:
|
|
79
|
+
|
|
80
|
+
.. code-block:: bash
|
|
81
|
+
|
|
82
|
+
pip install my-package
|
|
83
|
+
|
|
84
|
+
.. tab:: Conda (default)
|
|
85
|
+
|
|
86
|
+
Install using conda:
|
|
87
|
+
|
|
88
|
+
.. code-block:: bash
|
|
89
|
+
|
|
90
|
+
conda install my-package
|
|
91
|
+
|
|
92
|
+
.. tab:: From Source
|
|
93
|
+
|
|
94
|
+
Build from source:
|
|
95
|
+
|
|
96
|
+
.. code-block:: bash
|
|
97
|
+
|
|
98
|
+
git clone https://github.com/user/repo.git
|
|
99
|
+
cd repo
|
|
100
|
+
pip install -e .
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Configuration Options
|
|
104
|
+
|
|
105
|
+
Add these optional settings to your `conf.py`:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
# Customize the active tab highlight color
|
|
109
|
+
filter_tabs_highlight_color = '#007bff' # Default: '#007bff'
|
|
110
|
+
|
|
111
|
+
# Enable debug logging during development
|
|
112
|
+
filter_tabs_debug_mode = False # Default: False
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Advanced Usage
|
|
116
|
+
|
|
117
|
+
### Custom Legend
|
|
118
|
+
|
|
119
|
+
Override the auto-generated legend:
|
|
120
|
+
|
|
121
|
+
```rst
|
|
122
|
+
.. filter-tabs::
|
|
123
|
+
:legend: Select Your Installation Method
|
|
124
|
+
|
|
125
|
+
.. tab:: Quick Install
|
|
126
|
+
Content here...
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### ARIA Labels for Accessibility
|
|
130
|
+
|
|
131
|
+
Provide descriptive labels for screen readers:
|
|
132
|
+
|
|
133
|
+
```rst
|
|
134
|
+
.. filter-tabs::
|
|
135
|
+
|
|
136
|
+
.. tab:: CLI
|
|
137
|
+
:aria-label: Command line installation instructions
|
|
138
|
+
|
|
139
|
+
Content for command line users...
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Nested Tabs
|
|
143
|
+
|
|
144
|
+
Create complex layouts with nested tab groups:
|
|
145
|
+
|
|
146
|
+
```rst
|
|
147
|
+
.. filter-tabs::
|
|
148
|
+
|
|
149
|
+
.. tab:: Windows
|
|
150
|
+
|
|
151
|
+
Choose your package manager:
|
|
152
|
+
|
|
153
|
+
.. filter-tabs::
|
|
154
|
+
|
|
155
|
+
.. tab:: Chocolatey
|
|
156
|
+
choco install my-package
|
|
157
|
+
|
|
158
|
+
.. tab:: Scoop
|
|
159
|
+
scoop install my-package
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## How It Works
|
|
163
|
+
|
|
164
|
+
This extension uses a **pure CSS architecture** with semantic HTML:
|
|
165
|
+
|
|
166
|
+
- **Radio buttons** provide the selection mechanism (hidden but accessible)
|
|
167
|
+
- **CSS `:checked` selectors** control panel visibility
|
|
168
|
+
- **Fieldset/legend** structure provides semantic grouping
|
|
169
|
+
- **ARIA attributes** enhance screen reader support
|
|
170
|
+
- **Native keyboard navigation** works through standard form controls
|
|
171
|
+
|
|
172
|
+
This approach ensures:
|
|
173
|
+
- **Maximum compatibility** across all browsers and assistive technologies
|
|
174
|
+
- **Better performance** with no JavaScript parsing or execution
|
|
175
|
+
- **Enhanced security** for environments with strict Content Security Policies
|
|
176
|
+
- **Simplified maintenance** with fewer dependencies and potential conflicts
|
|
177
|
+
|
|
178
|
+
## Browser Support
|
|
179
|
+
|
|
180
|
+
Works in all modern browsers that support:
|
|
181
|
+
- CSS3 selectors (`:checked`, attribute selectors)
|
|
182
|
+
- Basic ARIA attributes
|
|
183
|
+
- HTML5 form elements
|
|
184
|
+
|
|
185
|
+
This includes all browsers from the last 10+ years.
|
|
186
|
+
|
|
187
|
+
## Development
|
|
188
|
+
|
|
189
|
+
### Quick Setup
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
git clone https://github.com/aputtu/sphinx-filter-tabs.git
|
|
193
|
+
cd sphinx-filter-tabs
|
|
194
|
+
./scripts/setup_dev.sh
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
This creates a virtual environment and builds the documentation.
|
|
198
|
+
|
|
199
|
+
### Development Commands
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
# Activate virtual environment
|
|
203
|
+
source venv/bin/activate
|
|
204
|
+
|
|
205
|
+
# Run tests
|
|
206
|
+
pytest
|
|
207
|
+
|
|
208
|
+
# Build documentation
|
|
209
|
+
./scripts/dev.sh html
|
|
210
|
+
|
|
211
|
+
# Run tests across multiple Sphinx versions
|
|
212
|
+
tox
|
|
213
|
+
|
|
214
|
+
# Clean build and start fresh
|
|
215
|
+
./scripts/dev.sh clean-all
|
|
216
|
+
|
|
217
|
+
# Export project structure for analysis
|
|
218
|
+
./scripts/export-project.sh
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Testing
|
|
222
|
+
|
|
223
|
+
The project includes comprehensive tests covering:
|
|
224
|
+
- Basic tab functionality and content visibility
|
|
225
|
+
- Accessibility features and ARIA compliance
|
|
226
|
+
- Nested tabs and complex layouts
|
|
227
|
+
- Multiple output formats (HTML, LaTeX)
|
|
228
|
+
- Cross-browser compatibility
|
|
229
|
+
|
|
230
|
+
Tests run automatically on:
|
|
231
|
+
- Python 3.10, 3.12
|
|
232
|
+
- Sphinx 7.0, 7.4, 8.0, 8.2
|
|
233
|
+
- Multiple operating systems via GitHub Actions
|
|
234
|
+
|
|
235
|
+
## Architecture
|
|
236
|
+
|
|
237
|
+
The extension consists of three main components:
|
|
238
|
+
|
|
239
|
+
- **`extension.py`** - Sphinx integration, directives, and node definitions
|
|
240
|
+
- **`renderer.py`** - HTML generation and output formatting
|
|
241
|
+
- **`static/filter_tabs.css`** - Pure CSS styling and functionality
|
|
242
|
+
|
|
243
|
+
This clean separation makes the code easy to understand, test, and maintain.
|
|
244
|
+
|
|
245
|
+
## Contributing
|
|
246
|
+
|
|
247
|
+
Contributions are welcome! Please:
|
|
248
|
+
|
|
249
|
+
1. Fork the repository
|
|
250
|
+
2. Create a feature branch
|
|
251
|
+
3. Add tests for new functionality
|
|
252
|
+
4. Ensure all tests pass: `pytest`
|
|
253
|
+
5. Submit a pull request
|
|
254
|
+
|
|
255
|
+
## License
|
|
256
|
+
|
|
257
|
+
GNU General Public License v3.0. See [LICENSE](LICENSE) for details.
|
|
258
|
+
|
|
259
|
+
## Changelog
|
|
260
|
+
|
|
261
|
+
See [CHANGELOG.md](docs/changelog.rst) for version history and migration notes.
|
|
262
|
+
|
|
263
|
+
## Support
|
|
264
|
+
|
|
265
|
+
- **Documentation**: https://aputtu.github.io/sphinx-filter-tabs/
|
|
266
|
+
- **Issues**: https://github.com/aputtu/sphinx-filter-tabs/issues
|
|
267
|
+
- **PyPI**: https://pypi.org/project/sphinx-filter-tabs/
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
filter_tabs/__init__.py,sha256=VPpIhj4HaLeMX7ai7dZFkUm81ii2ePPGjCd9hsMjsN4,397
|
|
2
|
+
filter_tabs/extension.py,sha256=6mna59Qg1zdk6Vn9eXU_CaaQg1yBq4ktx2jx9O01frM,18679
|
|
3
|
+
filter_tabs/renderer.py,sha256=ublmQSoFewdPGRtEO7gLrxGqCZCjlooeom0wudx4phk,10100
|
|
4
|
+
filter_tabs/static/filter_tabs.css,sha256=ZoiSWcn2YBEWgkQ-vPbIPHwQ7s2TG4aUikyxM1A8b9I,7956
|
|
5
|
+
sphinx_filter_tabs-1.2.6.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
6
|
+
sphinx_filter_tabs-1.2.6.dist-info/METADATA,sha256=Wk2ZurjL5XIgRdHoyqtcjwFBGXft5K0aKIREoZWHAMA,7608
|
|
7
|
+
sphinx_filter_tabs-1.2.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
sphinx_filter_tabs-1.2.6.dist-info/entry_points.txt,sha256=za_bQcueY8AHyq7XnnjkW9X3C-LsZjeERVQ_ds7jV1A,62
|
|
9
|
+
sphinx_filter_tabs-1.2.6.dist-info/top_level.txt,sha256=K0Iy-6EsYYdvlyXdsJT0SQg-BLDBgT5-Y8ZKy5SNAfc,12
|
|
10
|
+
sphinx_filter_tabs-1.2.6.dist-info/RECORD,,
|
|
@@ -1,160 +0,0 @@
|
|
|
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.
|
|
4
|
-
|
|
5
|
-
(function() {
|
|
6
|
-
'use strict';
|
|
7
|
-
|
|
8
|
-
// Only enhance if the extension's HTML is present on the page.
|
|
9
|
-
if (!document.querySelector('.sft-container')) return;
|
|
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
|
-
*/
|
|
63
|
-
function initTabKeyboardNavigation() {
|
|
64
|
-
const containers = document.querySelectorAll('.sft-container');
|
|
65
|
-
|
|
66
|
-
containers.forEach(container => {
|
|
67
|
-
const tabBar = container.querySelector('.sft-radio-group');
|
|
68
|
-
if (!tabBar) return;
|
|
69
|
-
|
|
70
|
-
const radios = tabBar.querySelectorAll('input[type="radio"]');
|
|
71
|
-
const labels = tabBar.querySelectorAll('label');
|
|
72
|
-
|
|
73
|
-
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
|
-
|
|
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.
|
|
139
|
-
radios.forEach((radio, index) => {
|
|
140
|
-
radio.addEventListener('change', () => {
|
|
141
|
-
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.
|
|
147
|
-
focusOnPanel(radio);
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Initialize the script once the DOM is ready.
|
|
155
|
-
if (document.readyState === 'loading') {
|
|
156
|
-
document.addEventListener('DOMContentLoaded', initTabKeyboardNavigation);
|
|
157
|
-
} else {
|
|
158
|
-
initTabKeyboardNavigation();
|
|
159
|
-
}
|
|
160
|
-
})();
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: sphinx-filter-tabs
|
|
3
|
-
Version: 1.2.2
|
|
4
|
-
Summary: A Sphinx extension for accessible, CSS-first filterable content tabs.
|
|
5
|
-
Author-email: Aputsiak Niels Janussen <aputtu+sphinx@gmail.com>
|
|
6
|
-
License: GNU General Public License v3.0
|
|
7
|
-
Project-URL: Homepage, https://github.com/aputtu/sphinx-filter-tabs
|
|
8
|
-
Project-URL: Repository, https://github.com/aputtu/sphinx-filter-tabs.git
|
|
9
|
-
Project-URL: Issues, https://github.com/aputtu/sphinx-filter-tabs/issues
|
|
10
|
-
Keywords: sphinx,extension,tabs,filter,documentation,css-only,accessibility,keyboard-navigation
|
|
11
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
-
Classifier: Framework :: Sphinx :: Extension
|
|
13
|
-
Classifier: Intended Audience :: Developers
|
|
14
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
15
|
-
Classifier: Operating System :: OS Independent
|
|
16
|
-
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
-
Classifier: Topic :: Documentation :: Sphinx
|
|
20
|
-
Classifier: Topic :: Software Development :: Documentation
|
|
21
|
-
Requires-Python: >=3.10
|
|
22
|
-
Description-Content-Type: text/markdown
|
|
23
|
-
License-File: LICENSE
|
|
24
|
-
Requires-Dist: Sphinx<9.0,>=7.0
|
|
25
|
-
Dynamic: license-file
|
|
26
|
-
|
|
27
|
-
# Sphinx Filter Tabs Extension
|
|
28
|
-
|
|
29
|
-
[](https://github.com/aputtu/sphinx-filter-tabs/actions/workflows/test.yml)
|
|
30
|
-
[](https://pypi.org/project/sphinx-filter-tabs/)
|
|
31
|
-
[](https://pypi.org/project/sphinx-filter-tabs/)
|
|
32
|
-
[](https://github.com/aputtu/sphinx-filter-tabs/blob/main/LICENSE)
|
|
33
|
-
|
|
34
|
-
A robust Sphinx extension for creating accessible, JavaScript-free, filterable content tabs.
|
|
35
|
-
|
|
36
|
-
**📖 View extension and documentation at: https://aputtu.github.io/sphinx-filter-tabs/**
|
|
37
|
-
|
|
38
|
-
This extension provides `filter-tabs` and `tab` directives to create user-friendly, switchable content blocks, ideal for showing code examples in multiple languages or instructions for different platforms.
|
|
39
|
-
|
|
40
|
-
## Features
|
|
41
|
-
|
|
42
|
-
- **No JavaScript:** Pure CSS implementation ensures maximum compatibility, speed, and accessibility.
|
|
43
|
-
- **WAI-ARIA Compliant:** The generated HTML follows accessibility best practices for keyboard navigation and screen readers.
|
|
44
|
-
- **Highly Customizable:** Easily theme colors, fonts, and sizes directly from your `conf.py` using CSS Custom Properties.
|
|
45
|
-
- **Graceful Fallback:** Renders content as simple admonitions in non-HTML outputs like PDF/LaTeX.
|
|
46
|
-
- **Automated Testing:** CI/CD pipeline tests against multiple Sphinx versions to ensure compatibility.
|
|
47
|
-
|
|
48
|
-
## Installation
|
|
49
|
-
|
|
50
|
-
You can install this extension using `pip`:
|
|
51
|
-
```bash
|
|
52
|
-
pip install sphinx-filter-tabs
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## Development
|
|
56
|
-
|
|
57
|
-
1. You can install a local version of the Sphinx with extension using:
|
|
58
|
-
```bash
|
|
59
|
-
./scripts/setup_dev.sh # Initially cleans previous folders in _docs/build and venv.
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
Command to enter venv is provided.
|
|
63
|
-
|
|
64
|
-
2. Once inside virtual environment, you can use following commands:
|
|
65
|
-
```bash
|
|
66
|
-
pytest # Runs test suite on configured version of Sphinx.
|
|
67
|
-
tox # Check across multiple Sphinx versions. Manual install of tox required.
|
|
68
|
-
./scripts/export-project.sh # Outputs directory structure and code to txt
|
|
69
|
-
./dev.sh [options] # Allows for faster generation for html, pdf, clean up
|
|
70
|
-
```
|
|
@@ -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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|