Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve the screen reader experience for html exported notebooks #2137

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions nbconvert/exporters/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ class HTMLExporter(TemplateExporter):
"*", help="Semver range for Jupyter widgets HTML manager"
).tag(config=True)

postprocess_html = Bool(False, help="Add interactive elements and missing alt text.").tag(
config=True
)

@default("file_extension")
def _file_extension_default(self):
return ".html"
Expand Down Expand Up @@ -265,7 +269,15 @@ def from_notebook_node( # type:ignore[explicit-override, override]

self.register_filter("highlight_code", highlight_code)
self.register_filter("filter_data_type", filter_data_type)

html, resources = super().from_notebook_node(nb, resources, **kw)
if self.postprocess_html:
html, resources = self.postprocess(html, resources)

return html, resources

def postprocess(self, html, resources):
"""Use beautifulsoup to make notebooks interactive and replace missing alt text"""
soup = BeautifulSoup(html, features="html.parser")
# Add image's alternative text
missing_alt = 0
Expand Down
42 changes: 30 additions & 12 deletions share/templates/lab/base.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@
{% from 'celltags.j2' import celltags %}
{% from 'cell_id_anchor.j2' import cell_id_anchor %}

{%- block body_loop -%}
<div class="jp-Notebook jp-NotebookPanel-notebook" role="list" aria-label="Notebook Cells">
{{super()}}
</div>
{%- endblock body_loop -%}

{% block codecell %}
{%- if not cell.outputs -%}
{%- set no_output_class="jp-mod-noOutputs" -%}
{%- endif -%}
{%- if not resources.global_content_filter.include_input -%}
{%- set no_input_class="jp-mod-noInput" -%}
{%- endif -%}
<div {{ cell_id_anchor(cell) }} class="jp-Cell jp-CodeCell jp-Notebook-cell {{ no_output_class }} {{ no_input_class }} {{ celltags(cell) }}">
<div id="{{loop.index}}" role="listitem" class="jp-Cell jp-CodeCell jp-Notebook-cell {{ no_output_class }} {{ no_input_class }} {{ celltags(cell) }}" aria-labelledby="jp-cellNumber-{{loop.index}} jp-cellType-{{loop.index}}">
<div class="jp-cellType" id="jp-cellType-{{loop.index}}" hidden>Code</div>
{# the kernel name could also be the label here. #}
{{ super() }}
</div>
{%- endblock codecell %}
Expand All @@ -18,7 +26,7 @@
<div class="jp-Cell-inputWrapper">
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
<div class="jp-InputArea jp-Cell-inputArea" role="group" aria-label="cell input">
{{ super() }}
</div>
</div>
Expand All @@ -41,18 +49,23 @@
{% endblock output_group %}

{% block outputs %}
<div class="jp-OutputArea jp-Cell-outputArea">
<div class="jp-OutputArea jp-Cell-outputArea" role="group" aria-label="cell outputs">
{{ super() }}
</div>
{% endblock outputs %}

{% block in_prompt -%}
<div class="jp-InputPrompt jp-InputArea-prompt">
{%- if cell.execution_count is defined -%}
In&nbsp;[{{ cell.execution_count|replace(None, "&nbsp;") }}]:
{%- else -%}
In&nbsp;[&nbsp;]:
{%- endif -%}
<div class="jp-InputPrompt-Group jp-InputPrompt jp-InputArea-prompt">
<a href="{{loop.index}}" id="jp-cellNumber-{{loop.index}}" class="jp-CellNumber visually-hidden" aria-describedby="jp-cellType-{{loop.index}} jp-executionCount-{{loop.index}}">Cell {{loop.index}}</a>
<span id="jp-executionCount-{{loop.index}}">
{%- if cell.execution_count is defined and cell.execution_count != None -%}
In<span aria-hidden="true">[</span>{{ cell.execution_count|replace(None, "&nbsp;") }}<span
aria-hidden="true">]:</span>
{%- else -%}
In<span aria-hidden="true">[</span>&nbsp;<span class="visually-hidden">unexecuted</span><span
aria-hidden="true">]:</span>
{%- endif -%}
</span>
</div>
{%- endblock in_prompt %}

Expand All @@ -72,9 +85,9 @@
<div class="jp-OutputPrompt jp-OutputArea-prompt">
{%- if output.output_type == 'execute_result' -%}
{%- if cell.execution_count is defined -%}
Out[{{ cell.execution_count|replace(None, "&nbsp;") }}]:
Out<span aria-hidden="true">[</span>{{ cell.execution_count|replace(None, "&nbsp;") }}<span aria-hidden="true">]:</span>
{%- else -%}
Out[&nbsp;]:
Out<span aria-hidden="true">[</span>&nbsp;<span class="visually-hidden">unexecuted</span><span aria-hidden="true">]:</span>
{%- endif -%}
{%- endif -%}
</div>
Expand All @@ -90,12 +103,17 @@
{{ self.output_area_prompt() }}
{% endif %}
{{ super() }}
{% if not cell.outputs %}
<span class="visually-hidden">Zero outputs displayed.</span>
{% endif %}
</div>
{% endblock output %}

{% block markdowncell scoped %}
<div {{ cell_id_anchor(cell) }} class="jp-Cell jp-MarkdownCell jp-Notebook-cell">
<div id="{{loop.index}}" class="jp-Cell jp-MarkdownCell jp-Notebook-cell" role="listitem" aria-labelledby="jp-cellNumber-{{loop.index}} jp-cellType-{{loop.index}}">
<div class="jp-cellType" id="jp-cellType-{{loop.index}}" hidden>Markdown</div>
<div class="jp-Cell-inputWrapper">
<a href="{{loop.index}}" class="jp-cellNumber visually-hidden" id="jp-cellNumber-{{loop.index}}" aria-describedby="jp-cellType-{{loop.index}}">Cell {{loop.index}}</a>
<div class="jp-Collapser jp-InputCollapser jp-Cell-inputCollapser">
</div>
<div class="jp-InputArea jp-Cell-inputArea">
Expand Down
24 changes: 24 additions & 0 deletions share/templates/lab/index.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,30 @@ a.anchor-link {
display: block;
}
}

{# this fails SC 2.4.7 https://www.w3.org/WAI/WCAG21/Understanding/focus-visible.html
because the focus wont be visible when the cell is active.
the commented selector would allow visible focus if chosen. #}
/**.visually-hidden:not(:focus):not(:active),**/
.visually-hidden {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}

:root {
--nb-focus-width: 2px;
}

.jp-Cell:focus-within,
:focus-visible {
outline: max(var(--nb-focus-width), 1px) solid;
box-shadow: 0 0 0 calc(2 * max(var(--nb-focus-width), 1px));
}
</style>

{% endblock notebook_css %}
Expand Down
Loading