sphinx-filter-tabs 0.6.0__py3-none-any.whl → 0.8.0__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 CHANGED
@@ -85,7 +85,7 @@ class FilterTabsRenderer:
85
85
  self.temp_blocks: list[nodes.Node] = temp_blocks
86
86
 
87
87
  def render_html(self) -> list[nodes.Node]:
88
- """Constructs the complete docutils node tree for the HTML output."""
88
+ """Constructs the complete, W3C valid, and ARIA-compliant docutils node tree."""
89
89
  # Ensure a unique ID for each filter-tabs instance on a page.
90
90
  if not hasattr(self.env, 'filter_tabs_counter'):
91
91
  self.env.filter_tabs_counter = 0
@@ -104,67 +104,89 @@ class FilterTabsRenderer:
104
104
  }
105
105
  style_string = "; ".join([f"{key}: {value}" for key, value in style_vars.items()])
106
106
 
107
- # If debug mode is on, log the generated ID and styles for easier troubleshooting.
107
+ # If debug mode is on, log the generated ID and styles.
108
108
  if config.filter_tabs_debug_mode:
109
109
  logger.info(f"[sphinx-filter-tabs] ID: {group_id}, Styles: '{style_string}'")
110
110
 
111
111
  # Create the main container node with the inline style for theming.
112
112
  container = ContainerNode(classes=[SFT_CONTAINER], style=style_string)
113
113
 
114
- # Build the semantic structure using fieldset and a hidden legend for accessibility.
114
+ # Build the semantic structure using fieldset and a hidden legend.
115
115
  fieldset = FieldsetNode()
116
116
  legend = LegendNode()
117
117
  legend += nodes.Text(f"Filter by: {', '.join(self.tab_names)}")
118
118
  fieldset += legend
119
-
120
- # Generate the dynamic CSS that handles the core filtering logic.
119
+
120
+ # --- CSS Generation ---
121
+ # This generates the dynamic CSS that handles the core filtering logic.
121
122
  css_rules = []
122
123
  for tab_name in self.tab_names:
123
124
  radio_id = f"{group_id}-{self._css_escape(tab_name)}"
124
- # This CSS rule shows a panel only when its corresponding radio button is checked.
125
- # The modern :has() selector makes this possible without any JavaScript.
125
+ panel_id = f"{radio_id}-panel"
126
+ # This rule finds the tab bar that contains the checked radio button,
127
+ # then finds its sibling content area and shows the correct panel inside.
126
128
  css_rules.append(
127
- f".{SFT_TAB_BAR}:has(#{radio_id}:checked) ~ "
128
- f".{SFT_CONTENT} > .{SFT_PANEL}[data-filter='{tab_name}'] {{ display: block; }}"
129
+ f".{SFT_TAB_BAR}:has(#{radio_id}:checked) ~ .sft-content > #{panel_id} {{ display: block; }}"
129
130
  )
130
- # Embed the generated CSS directly into the HTML.
131
- style_node = nodes.raw(text=f"<style>{''.join(css_rules)}</style>", format="html")
132
-
133
- # Create the tab bar with radio inputs and labels.
131
+
132
+ # Write the dynamic CSS to a temporary file and add it to the build.
133
+ css_content = ''.join(css_rules)
134
+ static_dir = Path(self.env.app.outdir) / '_static'
135
+ static_dir.mkdir(parents=True, exist_ok=True)
136
+ css_filename = f"dynamic-filter-tabs-{group_id}.css"
137
+ (static_dir / css_filename).write_text(css_content, encoding='utf-8')
138
+ self.env.app.add_css_file(css_filename)
139
+
140
+ # --- ARIA-Compliant HTML Structure ---
141
+ # The tab bar container now gets the role="tablist".
134
142
  tab_bar = nodes.container(classes=[SFT_TAB_BAR], role='tablist')
135
- for tab_name in self.tab_names:
143
+ fieldset += tab_bar
144
+
145
+ # The content area holds all the panels.
146
+ content_area = nodes.container(classes=[SFT_CONTENT])
147
+ fieldset += content_area
148
+
149
+ # Map tab names to their content blocks for easy lookup.
150
+ content_map = {block['filter-name']: block.children for block in self.temp_blocks}
151
+
152
+ # 1. Create all radio buttons and labels first and add them to the tab_bar.
153
+ for i, tab_name in enumerate(self.tab_names):
136
154
  radio_id = f"{group_id}-{self._css_escape(tab_name)}"
137
- # The radio buttons are functionally necessary but visually hidden.
155
+ panel_id = f"{radio_id}-panel"
156
+
157
+ # The radio button is for state management.
138
158
  radio = RadioInputNode(type='radio', name=group_id, ids=[radio_id])
139
- if tab_name == self.default_tab:
140
- radio['checked'] = 'checked' # Set the default tab.
159
+
160
+ is_default = (self.default_tab == tab_name) or (i == 0 and not self.default_tab)
161
+ if is_default:
162
+ radio['checked'] = 'checked'
141
163
  tab_bar += radio
142
164
 
143
- # The labels are the visible, clickable tabs.
144
- label = LabelNode(for_id=radio_id, role='tab')
165
+ # The label is the visible tab. It gets role="tab" and aria-controls.
166
+ label = LabelNode(for_id=radio_id, role='tab', **{'aria-controls': panel_id})
167
+ if is_default:
168
+ label['aria-selected'] = 'true'
145
169
  label += nodes.Text(tab_name)
146
170
  tab_bar += label
147
- fieldset += tab_bar
148
171
 
149
- # Create the content area where all panels will reside.
150
- content_area = nodes.container(classes=[SFT_CONTENT])
151
- # Map tab names to their content blocks for easy lookup.
152
- content_map = {block['filter-name']: block.children for block in self.temp_blocks}
153
- # Ensure we create panels for all declared tabs plus the "General" tab.
154
- all_tab_names = self.tab_names + ["General"]
172
+ # 2. Create all tab panels and add them to the content_area.
173
+ all_tab_names = ["General"] + self.tab_names
155
174
  for tab_name in all_tab_names:
156
- panel = PanelNode(classes=[SFT_PANEL], **{'data-filter': tab_name, 'role': 'tabpanel'})
175
+ # The "General" panel does not correspond to a specific tab control.
176
+ if tab_name == "General":
177
+ panel = PanelNode(classes=[SFT_PANEL], **{'data-filter': tab_name})
178
+ else:
179
+ radio_id = f"{group_id}-{self._css_escape(tab_name)}"
180
+ panel_id = f"{radio_id}-panel"
181
+ # The panel gets role="tabpanel" and is linked back to the label.
182
+ panel = PanelNode(classes=[SFT_PANEL], ids=[panel_id], role='tabpanel', **{'aria-labelledby': radio_id})
183
+
157
184
  if tab_name in content_map:
158
- # Use deepcopy to prevent docutils node mutation bugs. Since the same content
159
- # might be referenced or processed multiple times, a deep copy ensures that
160
- # each panel gets a completely independent set of nodes.
161
185
  panel.extend(copy.deepcopy(content_map[tab_name]))
162
186
  content_area += panel
163
- fieldset += content_area
164
- container.children = [fieldset]
165
187
 
166
- # The final structure is the dynamic style block followed by the main container.
167
- return [style_node, container]
188
+ container.children = [fieldset]
189
+ return [container]
168
190
 
169
191
  def render_fallback(self) -> list[nodes.Node]:
170
192
  """Renders content as a series of simple admonitions for non-HTML builders (e.g., LaTeX/PDF)."""
@@ -226,9 +248,10 @@ class FilterTabsDirective(Directive):
226
248
  and delegates the final rendering to the FilterTabsRenderer.
227
249
  """
228
250
  env = self.state.document.settings.env
229
- # Prevent nesting of filter-tabs directives.
230
- if hasattr(env, 'sft_context') and env.sft_context:
231
- raise self.error("Nesting `filter-tabs` is not supported.")
251
+
252
+ # Remove comments to prevent nesting of filter-tabs directives.
253
+ #if hasattr(env, 'sft_context') and env.sft_context:
254
+ # raise self.error("Nesting `filter-tabs` is not supported.")
232
255
 
233
256
  # Set a context flag to indicate that we are inside a filter-tabs block.
234
257
  if not hasattr(env, 'sft_context'):
@@ -1,7 +1,5 @@
1
1
  /* Sphinx Filter Tabs -- Static Stylesheet */
2
- /* This file provides the structural and base theme styles for the component.
3
- The core filtering logic (showing the active panel) is handled by a small,
4
- dynamically generated <style> block created by extension.py. */
2
+ /* This file provides the structural and base theme styles for the component. */
5
3
 
6
4
  /* Main container for the tab component */
7
5
  .sft-container {
@@ -18,9 +16,19 @@
18
16
  /* The <legend> is visually hidden but essential for screen reader users. */
19
17
  .sft-legend { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
20
18
 
21
- /* The <input type="radio"> buttons are the functional core, but are not visible. */
19
+ /* The <input type="radio"> buttons are the functional core.
20
+ They are hidden accessibly, not with display:none, so they remain
21
+ focusable for keyboard users. */
22
22
  .sft-tab-bar > input[type="radio"] {
23
- display: none;
23
+ position: absolute;
24
+ width: 1px;
25
+ height: 1px;
26
+ padding: 0;
27
+ margin: -1px;
28
+ overflow: hidden;
29
+ clip: rect(0, 0, 0, 0);
30
+ white-space: nowrap;
31
+ border: 0;
24
32
  }
25
33
 
26
34
  /* The tab bar containing the clickable labels. */
@@ -40,32 +48,38 @@
40
48
  color: #555;
41
49
  font-weight: 500;
42
50
  /* THEME: The font size is controlled by a CSS variable. */
43
- font-size: var(--sft-tab-font-size, 1em);
51
+ font-size: var(--sft-font-size, 1em);
44
52
  line-height: 1.5;
45
53
  }
46
54
 
47
- /* THEME: The active tab label is highlighted using a CSS variable for the color.
48
- The adjacent sibling selector (+) is robust and efficient. */
55
+ /* THEME: The active tab label is highlighted using a CSS variable for the color. */
49
56
  .sft-tab-bar > input[type="radio"]:checked + label {
50
57
  border-bottom-color: var(--sft-tab-highlight-color, #007bff);
51
58
  color: #000;
52
59
  }
53
60
 
61
+ /* Add a clear, visible focus ring for keyboard navigation. */
62
+ .sft-tab-bar > input[type="radio"]:focus + label {
63
+ box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.6);
64
+ border-radius: 5px;
65
+ outline: none;
66
+ }
67
+
54
68
  /* The container for all content panels. */
55
69
  .sft-content { padding: 20px; }
56
70
 
57
- /* By default, hide all tab-specific panels. */
58
- .sft-container .sft-panel {
71
+ /* By default, hide all tab-specific panels. The dynamic CSS will show the active one. */
72
+ .sft-content > .sft-panel {
59
73
  display: none;
60
74
  }
61
- /* The "General" panel, however, is always visible. */
62
- .sft-container .sft-panel[data-filter="General"] {
75
+
76
+ /* Always show the "General" panel, as it has no radio button to control it. */
77
+ .sft-content > .sft-panel[data-filter="General"] {
63
78
  display: block;
64
79
  }
65
- /* The dynamically generated <style> block handles showing the one active panel. */
66
80
 
67
81
 
68
- /* Styles for collapsible sections, matching the golden record. */
82
+ /* Styles for collapsible sections */
69
83
  .collapsible-section {
70
84
  border: 1px solid #e0e0e0;
71
85
  /* THEME: The accent color is controlled by a CSS variable. */
@@ -81,11 +95,9 @@
81
95
  background-color: #f9f9f9;
82
96
  outline: none;
83
97
  }
84
- /* Hide the default disclosure triangle in browsers like Chrome/Safari. */
85
98
  .collapsible-section summary::-webkit-details-marker {
86
99
  display: none;
87
100
  }
88
- /* Hide the default disclosure triangle in browsers like Firefox. */
89
101
  .collapsible-section summary {
90
102
  list-style: none;
91
103
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sphinx-filter-tabs
3
- Version: 0.6.0
3
+ Version: 0.8.0
4
4
  Summary: A Sphinx extension for accessible, JS-free filterable content tabs.
5
5
  Author-email: Aputsiak Niels Janussen <aputsiak+sphinx@gmail.com>
6
6
  License: GNU General Public License v3.0
@@ -45,3 +45,19 @@ This extension provides `filter-tabs` and `tab` directives to create user-friend
45
45
  You can install this extension using `pip`:
46
46
  ```bash
47
47
  pip install sphinx-filter-tabs
48
+ ```
49
+
50
+ ## Development
51
+
52
+ 1. You can install a local version of the Sphinx with extension using:
53
+ ```bash
54
+ ./scripts/setup_dev.sh # Initially cleans previous folders in _docs/build and venv.
55
+ ```
56
+
57
+ Command to enter venv is provided.
58
+
59
+ 2. Once inside virtual environment, you can use following commands:
60
+ ```bash
61
+ pytest # Runs test suite on configured version of Sphinx.
62
+ tox # Check across multiple Sphinx versions. Manual install of tox required.
63
+ ```
@@ -0,0 +1,9 @@
1
+ filter_tabs/__init__.py,sha256=VPpIhj4HaLeMX7ai7dZFkUm81ii2ePPGjCd9hsMjsN4,397
2
+ filter_tabs/extension.py,sha256=K7w7iRN72PGVvswDuULBxxxam4zvPF1ZulsAxeHlvz8,20937
3
+ filter_tabs/static/filter_tabs.css,sha256=gXQk2ceyjAj9p3G_k-mrcPdxq0ggIXSMuXHyUE5mPP0,3486
4
+ sphinx_filter_tabs-0.8.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
5
+ sphinx_filter_tabs-0.8.0.dist-info/METADATA,sha256=SjLifkJnouYp8U4_te9JgpPtELGgQcaaeXEXoGRSCkU,2705
6
+ sphinx_filter_tabs-0.8.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ sphinx_filter_tabs-0.8.0.dist-info/entry_points.txt,sha256=za_bQcueY8AHyq7XnnjkW9X3C-LsZjeERVQ_ds7jV1A,62
8
+ sphinx_filter_tabs-0.8.0.dist-info/top_level.txt,sha256=K0Iy-6EsYYdvlyXdsJT0SQg-BLDBgT5-Y8ZKy5SNAfc,12
9
+ sphinx_filter_tabs-0.8.0.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- filter_tabs/__init__.py,sha256=VPpIhj4HaLeMX7ai7dZFkUm81ii2ePPGjCd9hsMjsN4,397
2
- filter_tabs/extension.py,sha256=ZWq-MCft-RvZtx4xzVyDGXGGtgddh8dZdeHTivIcN34,20204
3
- filter_tabs/static/filter_tabs.css,sha256=btJoI-Q87e0XUQARwJUhM6RCfg5m2dvxKtfABZ2zRsc,3429
4
- sphinx_filter_tabs-0.6.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
5
- sphinx_filter_tabs-0.6.0.dist-info/METADATA,sha256=msO5UEdrQPPLpUF3d0aRI-HaO9gqLccVfTbw3bMGtVQ,2264
6
- sphinx_filter_tabs-0.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
- sphinx_filter_tabs-0.6.0.dist-info/entry_points.txt,sha256=za_bQcueY8AHyq7XnnjkW9X3C-LsZjeERVQ_ds7jV1A,62
8
- sphinx_filter_tabs-0.6.0.dist-info/top_level.txt,sha256=K0Iy-6EsYYdvlyXdsJT0SQg-BLDBgT5-Y8ZKy5SNAfc,12
9
- sphinx_filter_tabs-0.6.0.dist-info/RECORD,,