sphinx-filter-tabs 0.7.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,79 +104,88 @@ 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
- # --- Backward-compatible CSS Handling ---
121
- # 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.
122
122
  css_rules = []
123
123
  for tab_name in self.tab_names:
124
124
  radio_id = f"{group_id}-{self._css_escape(tab_name)}"
125
- # This CSS rule shows a panel only when its corresponding radio button is checked.
126
- # 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.
127
128
  css_rules.append(
128
- f".{SFT_TAB_BAR}:has(#{radio_id}:checked) ~ "
129
- 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; }}"
130
130
  )
131
-
132
- # 1. Write the dynamic CSS to a temporary file in the build's static directory.
131
+
132
+ # Write the dynamic CSS to a temporary file and add it to the build.
133
133
  css_content = ''.join(css_rules)
134
134
  static_dir = Path(self.env.app.outdir) / '_static'
135
135
  static_dir.mkdir(parents=True, exist_ok=True)
136
136
  css_filename = f"dynamic-filter-tabs-{group_id}.css"
137
137
  (static_dir / css_filename).write_text(css_content, encoding='utf-8')
138
-
139
- # 2. Add the temporary CSS file using the backward-compatible app.add_css_file() method.
140
- # This correctly places a <link> tag in the HTML <head>.
141
138
  self.env.app.add_css_file(css_filename)
142
139
 
143
- # Create the tab bar, but without the role="tablist"
144
- # Screen Reader Ambiguity: Elements announced as group of radio buttons
145
- tab_bar = nodes.container(classes=[SFT_TAB_BAR])
146
- for tab_name in self.tab_names:
140
+ # --- ARIA-Compliant HTML Structure ---
141
+ # The tab bar container now gets the role="tablist".
142
+ tab_bar = nodes.container(classes=[SFT_TAB_BAR], role='tablist')
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):
147
154
  radio_id = f"{group_id}-{self._css_escape(tab_name)}"
148
-
149
- # Create the radio input, but without the role="tab"
155
+ panel_id = f"{radio_id}-panel"
156
+
157
+ # The radio button is for state management.
150
158
  radio = RadioInputNode(type='radio', name=group_id, ids=[radio_id])
151
159
 
152
- if tab_name == self.default_tab:
160
+ is_default = (self.default_tab == tab_name) or (i == 0 and not self.default_tab)
161
+ if is_default:
153
162
  radio['checked'] = 'checked'
154
163
  tab_bar += radio
155
164
 
156
- # The label correctly points to the radio input
157
- label = LabelNode(for_id=radio_id)
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'
158
169
  label += nodes.Text(tab_name)
159
170
  tab_bar += label
160
- fieldset += tab_bar
161
171
 
162
- # Create the content area where all panels will reside.
163
- content_area = nodes.container(classes=[SFT_CONTENT])
164
- # Map tab names to their content blocks for easy lookup.
165
- content_map = {block['filter-name']: block.children for block in self.temp_blocks}
166
- # Ensure we create panels for all declared tabs plus the "General" tab.
167
- 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
168
174
  for tab_name in all_tab_names:
169
- 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
+
170
184
  if tab_name in content_map:
171
- # Use deepcopy to prevent docutils node mutation bugs. Since the same content
172
- # might be referenced or processed multiple times, a deep copy ensures that
173
- # each panel gets a completely independent set of nodes.
174
185
  panel.extend(copy.deepcopy(content_map[tab_name]))
175
186
  content_area += panel
176
- fieldset += content_area
177
- container.children = [fieldset]
178
187
 
179
- # Return just the main container node. Sphinx handles adding the CSS file to the <head>.
188
+ container.children = [fieldset]
180
189
  return [container]
181
190
 
182
191
  def render_fallback(self) -> list[nodes.Node]:
@@ -239,9 +248,10 @@ class FilterTabsDirective(Directive):
239
248
  and delegates the final rendering to the FilterTabsRenderer.
240
249
  """
241
250
  env = self.state.document.settings.env
242
- # Prevent nesting of filter-tabs directives.
243
- if hasattr(env, 'sft_context') and env.sft_context:
244
- 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.")
245
255
 
246
256
  # Set a context flag to indicate that we are inside a filter-tabs block.
247
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.7.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=tU2ZBYWRmYHyvQh8NgPWWqFidunsL16KHhoyqMfboCw,20739
3
- filter_tabs/static/filter_tabs.css,sha256=btJoI-Q87e0XUQARwJUhM6RCfg5m2dvxKtfABZ2zRsc,3429
4
- sphinx_filter_tabs-0.7.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
5
- sphinx_filter_tabs-0.7.0.dist-info/METADATA,sha256=WJjRU9rRRzgFFKlheXL4QTa_c1vF0FWhd3bqMyiG_zA,2264
6
- sphinx_filter_tabs-0.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
- sphinx_filter_tabs-0.7.0.dist-info/entry_points.txt,sha256=za_bQcueY8AHyq7XnnjkW9X3C-LsZjeERVQ_ds7jV1A,62
8
- sphinx_filter_tabs-0.7.0.dist-info/top_level.txt,sha256=K0Iy-6EsYYdvlyXdsJT0SQg-BLDBgT5-Y8ZKy5SNAfc,12
9
- sphinx_filter_tabs-0.7.0.dist-info/RECORD,,