The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").
Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.
Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).
One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
116 lines
4.7 KiB
TypeScript
116 lines
4.7 KiB
TypeScript
import {
|
|
forwardRef,
|
|
useCallback,
|
|
useRef,
|
|
useState,
|
|
useSyncExternalStore,
|
|
type ButtonHTMLAttributes,
|
|
} from "react";
|
|
import clsx from "clsx";
|
|
import { endsTextSelection, movedPastSlop } from "../../lib/textSelection";
|
|
|
|
// A `<button>` whose `disabled` never throws the focus out of the page.
|
|
//
|
|
// A focused button that turns natively disabled loses the focus — Chrome moves it to <body>
|
|
// (measured) — and the app disables the very button that was just pressed on ~60 sites (a
|
|
// mutation pending, a form gone clean after its save), so a keyboard user was dropped out of the page
|
|
// on every submit. While THIS button holds the focus, `disabled` (and `busy`) is expressed as
|
|
// `aria-disabled` plus a click guard instead: still focusable, announced as unavailable, and not
|
|
// activatable — its preventDefault also cancels a form's implicit submission, which is a click on the
|
|
// default button. The native attribute applies again once the focus is elsewhere (at the blur; in
|
|
// a window without focus, which fires no blur, at the next render).
|
|
//
|
|
// Unstyled on purpose: `ui/Button` is built on it, and a hand-rolled `<button>` swaps its tag for
|
|
// this one keeping its classes. The dimmed look of the soft state is one rule in index.css
|
|
// (`[data-focus-safe][aria-disabled="true"]`), since a caller's `disabled:*` utilities match only the
|
|
// native attribute.
|
|
//
|
|
// `selectable` is for a button whose text is a title or a name (docs/DESIGN.md "Text stays
|
|
// selectable"): a row, a card, a name that opens its object. Chrome selects no text inside a
|
|
// <button> unless it is `user-select: text`, and then the mouseup that ends the drag still clicks the
|
|
// button (both measured, 2026-09-26), so the flag sets both halves: the text selects, and the click
|
|
// that ends a selection inside the button, or a press dragged past the slop, does nothing.
|
|
const noSubscription = () => () => {};
|
|
|
|
export const FocusSafeButton = forwardRef<
|
|
HTMLButtonElement,
|
|
ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
/** Working: unavailable and announced as busy, whether focused or not. */
|
|
busy?: boolean;
|
|
/** Its text is a title or a name: selectable, and the click that ends a selection is ignored. */
|
|
selectable?: boolean;
|
|
}
|
|
>(function FocusSafeButton(
|
|
{
|
|
disabled,
|
|
busy = false,
|
|
selectable = false,
|
|
className,
|
|
onClick,
|
|
onMouseDown,
|
|
onFocus,
|
|
onBlur,
|
|
...rest
|
|
},
|
|
ref,
|
|
) {
|
|
const [focusEvent, setFocusEvent] = useState(false);
|
|
const node = useRef<HTMLButtonElement | null>(null);
|
|
const pressedAt = useRef<{ x: number; y: number } | null>(null);
|
|
const setNode = useCallback(
|
|
(el: HTMLButtonElement | null) => {
|
|
node.current = el;
|
|
if (typeof ref === "function") ref(el);
|
|
else if (ref) ref.current = el;
|
|
},
|
|
[ref],
|
|
);
|
|
// Focused by the event OR by the document's own answer at render time: when the window itself is
|
|
// not focused (another app in front) `.focus()` moves `activeElement` but fires no focus event
|
|
// (measured in Chrome), and the button disabled in that state lost the focus all the same. Read
|
|
// through useSyncExternalStore, which React asks on EVERY render, never from a memo: a bare
|
|
// render-time DOM read made the React Compiler skip the component, and compiled it would be
|
|
// cached. Nothing to subscribe to — the focus events above already re-render it.
|
|
const activeNow = useSyncExternalStore(
|
|
noSubscription,
|
|
() => node.current !== null && node.current.ownerDocument.activeElement === node.current,
|
|
);
|
|
const focused = focusEvent || activeNow;
|
|
const soft = busy || (!!disabled && focused);
|
|
// A caller's own aria-disabled is the "unavailable, and here is why" state (DESIGN.md: the tooltip or
|
|
// label carries the reason, so the button stays focusable): it is kept, never overwritten by ours, and
|
|
// it is inert to a click like ours, so no caller has to guard its own handler.
|
|
const callerSoft = rest["aria-disabled"] === true || rest["aria-disabled"] === "true";
|
|
return (
|
|
<button
|
|
ref={setNode}
|
|
{...rest}
|
|
className={clsx(selectable && "select-text", className) || undefined}
|
|
data-focus-safe=""
|
|
disabled={!!disabled && !focused}
|
|
aria-disabled={soft || callerSoft || undefined}
|
|
aria-busy={busy || undefined}
|
|
onClick={(e) => {
|
|
if (soft || callerSoft) {
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
if (selectable && (endsTextSelection(e) || movedPastSlop(pressedAt.current, e))) return;
|
|
onClick?.(e);
|
|
}}
|
|
onMouseDown={(e) => {
|
|
pressedAt.current = { x: e.clientX, y: e.clientY };
|
|
onMouseDown?.(e);
|
|
}}
|
|
onFocus={(e) => {
|
|
setFocusEvent(true);
|
|
onFocus?.(e);
|
|
}}
|
|
onBlur={(e) => {
|
|
setFocusEvent(false);
|
|
onBlur?.(e);
|
|
}}
|
|
/>
|
|
);
|
|
});
|