fix(notifications): Mark all read waits for the fresh list, is not offered where it cannot act, and a row says it is unread
The mask over a stale read lifted when the write settled, before the refetch landed, and the band took the rows back. A band holding only what asks by its kind (an access-requests nudge) left the button enabled with nothing to do. The unread dot was only drawn: the spoken label now says it, apart from the name Back finds the row by, and follows the row while it holds the focus.
This commit is contained in:
@@ -38,7 +38,7 @@ export const entryKey = (e: InboxEntry): string => keyOf(e.src, e.n.id);
|
||||
/** Whether a client notice asks for the reader by what it is: an access-requests nudge for as long
|
||||
* as it exists (its producer removes it when nothing is pending), a notice that waits on an
|
||||
* interaction until it is dealt with. */
|
||||
const clientAsks = (n: ClientNotif): boolean =>
|
||||
export const clientAsks = (n: ClientNotif): boolean =>
|
||||
n.meta?.kind === "access-requests" || (n.requiresInteraction && !n.dismissed);
|
||||
|
||||
export function orderInbox(
|
||||
|
||||
@@ -42,6 +42,15 @@ const lists = new Map<HTMLElement, NavList>();
|
||||
// The item we gave a tabindex to, so the next move can take it back — and whether we named it too.
|
||||
let roved: HTMLElement | null = null;
|
||||
let named = false;
|
||||
// A named item's label follows its spoken state while it holds the focus: Mark read hands the
|
||||
// focus to the row BEFORE the row reads read, and the label given then said "unread" until the
|
||||
// keyboard moved on.
|
||||
const relabel =
|
||||
typeof MutationObserver === "undefined"
|
||||
? null
|
||||
: new MutationObserver(() => {
|
||||
if (named && roved) roved.setAttribute("aria-label", spokenName(roved));
|
||||
});
|
||||
|
||||
const INDEX = "data-nav-index";
|
||||
const LIST = "data-nav-list";
|
||||
@@ -56,14 +65,24 @@ const REVEAL_FRAMES = 10;
|
||||
* `inset` draws the keyboard ring INSIDE the item. The ring sits 2 px outside by default, which a
|
||||
* card with a gap round it has room for; an item flush against its neighbours (a log line) or against
|
||||
* the edge of a clipping card (a Plex playlist show header) does not: the ring ran past the list, or
|
||||
* was clipped away entirely. A table row needs no flag: index.css insets every `tr` by its tag. */
|
||||
* was clipped away entirely. A table row needs no flag: index.css insets every `tr` by its tag.
|
||||
*
|
||||
* `label` is what is SPOKEN instead of `name`, when it carries a state the item can lose while the
|
||||
* page is away (a notification's "unread"): `name` is also how Back finds the item again, and a
|
||||
* name that changed with the state would miss it. */
|
||||
export const navItemAttrs = (
|
||||
index: number,
|
||||
name?: string | null,
|
||||
{ inset = false }: { inset?: boolean } = {},
|
||||
): { "data-nav-index": number; "data-nav-name"?: string; "data-nav-inset"?: "" } => ({
|
||||
{ inset = false, label }: { inset?: boolean; label?: string } = {},
|
||||
): {
|
||||
"data-nav-index": number;
|
||||
"data-nav-name"?: string;
|
||||
"data-nav-label"?: string;
|
||||
"data-nav-inset"?: "";
|
||||
} => ({
|
||||
[INDEX]: index,
|
||||
...(name ? { "data-nav-name": name } : null),
|
||||
...(label ? { "data-nav-label": label } : null),
|
||||
...(inset ? { "data-nav-inset": "" } : null),
|
||||
});
|
||||
|
||||
@@ -204,11 +223,15 @@ function itemName(el: HTMLElement): string {
|
||||
).trim();
|
||||
}
|
||||
|
||||
/** What a screen reader hears while the item holds the focus: its `label` when it gives one. */
|
||||
const spokenName = (el: HTMLElement): string => el.getAttribute("data-nav-label") || itemName(el);
|
||||
|
||||
function focusItem(el: HTMLElement, reveal = true): void {
|
||||
if (roved !== el) {
|
||||
if (named) roved?.removeAttribute("role");
|
||||
if (named) roved?.removeAttribute("aria-label");
|
||||
roved?.removeAttribute("tabindex");
|
||||
relabel?.disconnect();
|
||||
roved = null;
|
||||
named = false;
|
||||
// An item that is a Tab stop in its own right (a native <button> tile, a role=button card with
|
||||
@@ -219,7 +242,7 @@ function focusItem(el: HTMLElement, reveal = true): void {
|
||||
// A card root is a plain <div>: focused, it has no role and no name, and a screen reader read it
|
||||
// as an unlabelled run of text. While it holds the focus it is a group named after what Enter
|
||||
// opens. A row (<tr>) keeps its own role.
|
||||
const name = itemName(el);
|
||||
const name = spokenName(el);
|
||||
if (
|
||||
el.tagName === "DIV" &&
|
||||
name &&
|
||||
@@ -229,6 +252,7 @@ function focusItem(el: HTMLElement, reveal = true): void {
|
||||
el.setAttribute("role", "group");
|
||||
el.setAttribute("aria-label", name);
|
||||
named = true;
|
||||
relabel?.observe(el, { attributeFilter: ["data-nav-label", "data-nav-name"] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user