notifying changes in computational notebooks¤
blind and low vision jupyter users frequently ask for audible (non-visual) announcements about changes to the state of the document. this demand introduces a new visual and nonvisual feature to computational notebooks that notify any user of an update.
this feature request starts as a nonvisual componment, but quickly we realize that a visual component would be assistive to all users.
toast components are common patterns for these kinds of notifications, but there are accessibility challenges. one specific challenge is that the aria live message could be destroyed it is announced. this demands the need for a more persistent message, but not immortal. the time delay should be configurable on the order of minutes.
another notification pattern to consider would use the notifications api. this approach would be for escalated notifications, or tens of minute notifications. the os will handle the accessibility of these messages.
cf: https://github.com/jupyterlab/jupyterlab/issues/16145
from nbconvert_a11y.tables import get_table, Config, new
shell.tangle.parser = midgy.language.python.Python()
%%
## creating a scenario that would be announced in a log
our approach maps [logging levels](https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels) - the visual log -
to [aria live](https://www.sarasoueidan.com/blog/accessible-notifications-with-aria-live-regions-part-1/) values - the nonvisual log[^log].
ultimately, we create a reference visual and non-visual logging component that will provide updates to assistive technology.
from logging import FATAL, CRITICAL, ERROR, WARN, INFO, DEBUG
FATAL / CRITICAL, ERROR, WARN, INFO, DEBUG;
activities:\
is a list of actions that may be encountered during interactive computing.
the list provides a log level, message, and call to action.\
=\
1. info
: starting python kernel
: - show kernel info
2. info
: python kernel started
: - execute cells
3. info
: cell 2 executed
: - cancel cell 2 execution
4. info
: cell 2 finished successfully
: - jump to cell output
5. info
: cell 7 executed
: - cancel cell 7 execution
6. error
: cell 7 failed with TypeError
: - jump to cell 7 traceback
7. critical
: python kernel restarting
: - queue cell execution
8. critical
: python kernel restarted
: - execute
___
[^log]: `log` is an aria role meant to mimic this pattern. support is generally better for `role=status` or `role=alert`.
%%
logging_roles:\
is a mapping of logging levels to aria live roles.\
=\
```yaml
ERROR: assertive
CRITICAL: assertive
INFO: polite
WARN: none
DEBUG: none
```
these levels should be configured in the user interface especially to meet the needs of developers using assistive technologies.
creating a dataframe about our synthetic scenario¤
%%
<details>
<summary>dataframe tidying</summary>
```ipython
df = pipe(
activities, partial(re.sub, "\s+:", ":"),
partial(re.sub, "[0-9].\s+|\s-\s", ""), str.splitlines, Series
).str.partition(": ")[[0, 2]].rename(columns=pipe(
"level message".split(), partial(zip, range(0, 3, 2)), dict,
))
df["timestamp"] =df.level.apply(
lambda x: random.randint(1, 10)
).cumsum().pipe(
pandas.to_datetime, unit="s"
)
df = df.set_index("timestamp")
df["level"] = df["level"].str.upper()
df["live"] = df["level"].apply(logging_roles.get)
df = df.drop(columns="message").assign(
**df.message.str.partition(":")[[0, 2]].rename(columns=pipe(
"message action".split(), partial(zip, range(0, 3, 2)), dict,
))
)
df["id"] = [uuid.uuid4() for _ in range(len(df))]
pipe(df, partial(get_table, config=Config(), caption="information needed to construct an accessible table representation"), display)
```
</details>
accessible table components¤
create components that convert the wide data above to a user interface component that can be tested.
# https://developer.mozilla.org/en-US/docs/Web/HTML/Element/time
def get_timestamp(s):
t = new("time", attrs=dict(datetime=s.name.isoformat()))
# t.append(s.name.strftime('%Y-%m-%d %X'))
t.append(s.name.strftime('%X'))
return t
there are a lot of shenangins to consider when implementing aria live across browsers.
we take an html5 forward design approach and restrict ourselves to the nuance around the native output element;
we take heavy advice from scott o'hara's html output
analysis.
def get_message(s):
object = new("output", attrs=dict(
role="status", id=s.loc["id"],
onload="alert(11);", **{"aria-live": s.live, "data-message": s.message}
))
# object.append(s.message)
return object
aria live seems most reliable when the text is injected directly into the element. to ensure aria-live
announces we must create an output element THEN dynamically text content to trigger an announcement.
def get_script(s):
script = new("script")
script.append(F""" setTimeout(() => {{
var out = document.getElementById("{s.id}");
out.parentElement.parentElement.classList.remove("nv");
out.textContent = out.dataset.message}}, {s.name.second * 1000})""")
return script
get_priority
is the visual log level.
def get_priority(s):
label = new("label", attrs={"for": s.loc["id"]})
label.append(s.loc["level"])
return label
a visual convention with toast notifications is to provide a call to action with a notification. we couple these visual concepts with non visual concepts. jupyter has toast notifications, but they would be compliments the activity log. the activity log is the domain of visual and nonvisual actions.
def get_action(s):
# might return a link or a button
butt = new("a" if s.action.startswith(("jump",)) else "button")
butt.append(s.action)
return butt
an example activity log table
that reveals it self and has aria live capabilities.
this is a first demonstration of a more robust activity log for visual and nonvisual users.
the table operates as a keyboard navigable component on assistive technology.
table = (t := df.apply(
lambda s: Series(dict(
priority=get_priority(s),
message=get_message(s),
action=get_action(s),
timestamp=get_timestamp(s), script=get_script(s)
)), axis=1
)).reset_index(drop=True).set_index("timestamp").pipe(get_table, id="log", **{"aria-labelledby": "log-nm"})
[x.attrs.update(**{"class": "nv"}) for x in table.select("tr")];
the table doesn't need to be seen, but it can not be removed from the visual domain otherwise announcements will be suppressed.
we can use the details
tag and the non visual css styling to address this.
by wrapping all of the following in a region we expose a landmark for the activity log enhancing discovery.
section = new("section", attrs={"aria-labelledby": "log-nm"})
section.append(details := new("details", attrs=dict(open="")))
details.append(summary := new("summary", attrs=dict(id="log-nm")))
summary.append("activity log")
section.append(table)
section
conclusion¤
it might be possible to create toast like behavior with persistance using a table. the row of a table could have css applied to appear as a toast message before returning back to its table.
the number of messages will grow, and it will be necessary to cull and throttle the messages. it will be important to do research into disabling/enabling aria live by changing attributes; in other words, it might be possible, in some stacks, to suppress a queued live message.
styling¤
%%
<figure>
<figcaption>
<a href="https://www.tpgi.com/the-anatomy-of-visually-hidden/">visually hidden css</a></figcaption>
<blockquote cite="https://www.tpgi.com/the-anatomy-of-visually-hidden/">
display\
```css
details:not([open]) + table, .nv, .non-visual {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
/** demo button **/
a[href="#log"] {
display: block;
height: 300px;
width: 400px;
background: -moz-element(#log);
background-size: content;
background-repeat: no-repeat;
font-size: 4rem;
}
```
</blockquote>
</figure>