Conversation
chore: back-merge 1.1.7 (main → dev)
A footnote marker in a table cell survived into the full-screen view as a dead "[1]": the definitions live in the note's `.footnotes` list, outside the `<table>` the overlay clones, so there was nothing to show and nothing to jump to. The overlay now carries in a copy of every definition the clone's markers actually point at, lists it under the table, and re-points each footnote id and `#`-anchor into an overlay-local namespace — so the marker jumps to its definition and the "↩︎" back-reference jumps back, without leaving full screen and without disturbing the note's own footnote navigation. The numbering is carried over explicitly, so a table referencing only footnote 3 still reads "3.", and the section is laid out at a readable measure instead of the table's width. Every failure path degrades to the overlay as it was built before: no markers, no note root, no rendered definition list (Live Preview renders a table in isolation; Reading view virtualises off-screen sections away), or nothing that resolves. An individually unresolvable marker in an otherwise resolved note stays visible but is made inert — the same thing the note itself shows in those modes. Clone hygiene also drops the `id` attributes the clone copied over, keeping only the footnote identity nodes that get re-minted. A table carrying a `^block-id` previously appeared twice under the same id while the overlay was open. Closes #28 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
manifest.json, versions.json and CHANGELOG.md in lockstep, per scripts/validate.mjs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A workflow-backed review at xhigh effort raised 15 findings against the
footnote work. Eleven were adjudicated real and are fixed here; four were
rejected with evidence (see below).
Clone hygiene
- `_scrub` no longer strips ids inside an SVG subtree. Removing them broke
id-scoped `<style>`, `<use>` and gradient/marker/clip-path references
outright — a Mermaid diagram in a cell rendered unstyled in the overlay —
whereas a duplicated svg-internal id is benign. The test is on
`namespaceURI`, not `closest("svg")`, so HTML inside a `<foreignObject>`
is still stripped.
- `_scrub` now also clears the inline sizing `DiagramView` stamps on a wrapped
svg before unwrapping the viewport that clipped it. A 1600px diagram inside
a cloned footnote definition used to render unscaled and overflow the
overlay.
Anchors
- The "cannot be resolved here" treatment is now a single site, `_makeInert`,
reached only through `_hardenAnchors`, which sweeps every footnote anchor —
back-references included. A definition carries one back-ref per reference
occurrence, including occurrences outside the table, so those extra
back-refs are always dead in the overlay; left live they looked clickable,
did nothing, and let the browser dirty the window URL and push a history
entry.
- The degradation paths (no definition list at all, nothing resolvable) now
run `_degradeFootnotes`, which strips the footnote identities `_scrub` was
holding for `_namespace` and makes the markers inert. Previously that clone
reached the document carrying the note's own ids — duplicate ids for as long
as the overlay was open — and a click fell through to the note behind it.
- `onFsAnchorClick` ignores the secondary button on `auxclick`. A right-click
on a marker used to get the context menu *and* a scroll and focus steal.
- The definition list is emitted in the note's document order, not
first-reference order, so a table referencing [^2] before [^1] no longer
renders "2." above "1.".
Focus, and accessibility
- Focus returns to the inline trigger only when the view was closed from the
keyboard. After a mouse close the trigger was focused while
`:focus-visible` did not match — invisible at `opacity: 0` — so the next
Space re-opened the overlay instead of scrolling the note. A focused trigger
is now always visible as well.
- An inert marker keeps an accessible name: an href-less `<a>` computes role
`generic`, where `aria-label` is prohibited, so `role="link"` is restored.
The name contains the visible "[1]" rather than replacing it, and a
back-ref — whose visible text is a glyph that names nothing — says what is
missing instead.
- The inert state no longer rests on colour alone (dotted underline), and a
keyboard-driven jump keeps a focus ring after the 1400ms highlight expires.
- Footnote prose is capped at 70ch, so a 2200px table no longer lays a
one-sentence footnote out as a single 2200px line.
Consistency
- The new code uses `classList` like the rest of the file; `addClass` is no
longer called. The e2e stub gained the polyfill anyway, since its absence is
what let the jump path throw under test while working in the app.
Rejected, with evidence: the widened backdrop-close test (those strips already
closed the overlay before this change — only the 20px gap band is new);
duplicate ids from a repeated reference (Obsidian mints a distinct
`data-footnote-id` per occurrence, and the preview post-processor sets each
element's id equal to it); a block id landing on a definition `<li>` (that
post-processor overwrites any pre-existing id, and the block-id transformer
does not reach a synthesized footnote list item); and `(ol.start || 1)`
mishandling `start="0"` (the renderer hard-codes `start: 1`).
Tests: the table suite now drives the behaviour instead of only measuring
layout — it clicks markers and back-references, asserts no page errors, and
covers the degraded, mixed, out-of-order, svg-in-cell and diagram-in-footnote
shapes. 51 table checks + 9 diagram checks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… DOM Real-vault testing showed the feature never worked. The overlay read the note's rendered `.footnotes` list, but Obsidian does not render off-screen sections: the definition list sits at the very bottom of a note and the table is above it, so the lookup found nothing and every marker fell into the degradation path. The only footnote that worked belonged to the note's last table, where the list happened to be on screen. The e2e suite passed 51 checks throughout, because its fixture always had a rendered `.footnotes` section — the harness encoded the very assumption that is false. Definitions now come from the note's source. The marker's `data-footref` joins to `metadataCache.getFileCache(file).footnotes`, the definition's `position` slices `vault.cachedRead(file)`, and the result is rendered with `MarkdownRenderer.render` into the overlay. Nothing depends on what Obsidian happens to have rendered, so a table resolves its footnotes wherever it sits. Slicing is where this silently breaks, so it is validated on both sides: - a reference-style slice carries its own `[^id]:` label, which is stripped; an inline slice is the body alone and is left untouched; - continuation lines are dedented by exactly one indent unit, or a fenced block inside a definition parses as indented code and a mermaid diagram renders as a grey blob; - a slice that does not match the shape its id implies is dropped rather than rendered — a cache indexing mid-update would otherwise paste somebody else's text under the marker. Live Preview is not supported, and this is deliberate. Obsidian parses each block there on its own: a reference-style marker is not rendered inside a table widget at all, and an inline marker's id is positional within its parse unit, so a widget's `[inline0` need not be the note's `[inline0`. Joining on it would render a different footnote's body under the right marker. Those markers are left visible but inert instead — wrong text is worse than none. Rendering is async, so `openFullscreen` shows the table immediately and appends the section when it arrives. One `Component` per overlay owns every MarkdownRenderChild and is assigned before the first await, so a close during rendering unloads it; a render that returns to find the field holding a different component bails without touching the DOM. Back-references are now built rather than cloned, one per marker occurrence present in the clone, so a dead back-ref cannot exist by construction. minAppVersion moves to 1.6.6, the release that exposes footnotes in the metadata cache. The version stays 1.2.0. Tests: the suite is rebuilt on the measured reality, led by the case this redesign exists for — a note with no `.footnotes` anywhere that still produces a correct definition list. 109 table checks. Verified by mutation that they discriminate: neutralising the dedent fails the mermaid-fence check, and corrupting the source lookup fails the central regression. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Obsidian gives a footnote no hover affordance of its own — its only footnote handler is a click delegated on the note's preview sizer, which never sees this overlay — and in full screen the definition can sit far below a long table, so a marker was a number with nothing behind it until clicked. The marker now carries the rendered definition as a `title`: native, no listener, nothing to clean up on close. Read before the back-references are appended so the preview does not trail a "↩︎" that means nothing out of context, and capped at 300 characters because a native tooltip does not scroll — the full text is one click away in the list below the table. Only the clone is touched; the note's own markers keep no tooltip. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A full-screen overlay covers the workspace, but Tab walked straight out of it onto the note's own links and buttons behind the overlay: focus landed where the reader could not see it, and the next Enter activated it. Measured with the trap disabled — Tab reaches BODY and then the note's own footnote links, underneath a still-open overlay. Tab and Shift+Tab now cycle within the overlay, and it identifies itself as a labelled modal dialog. This applies to the diagram overlay as well as the table one; it was never specific to footnotes. The focusable set is deliberately narrow. Footnote definitions carry `tabindex="-1"` as programmatic jump targets and stay out of the tab order, and an unresolvable marker has had its `href` stripped, which already makes it unfocusable. Visibility is tested with `getClientRects()`, not `offsetParent`, because the close button is `position: fixed` and its `offsetParent` is always null. The table overlay reads its focusables at keypress time rather than at open time, since footnote definitions render asynchronously and arrive after the overlay is already up. Obsidian's own modals set neither `role="dialog"` nor `aria-modal` — verified in the app bundle — but they are small centred dialogs a reader can see past. Ours is the whole screen. Also swept `window` to `activeWindow` where it is correct: `matchMedia` for the reduced-motion probe and `innerHeight` for the inline height cap. The timer functions are deliberately NOT swept: `eslint-plugin-obsidianmd`'s `prefer-window-timers` rule requires `window.setTimeout` / `window.clearTimeout` / `window.requestAnimationFrame`, so the existing calls were already correct and converting them fails lint. Tests: 4 checks per suite covering the dialog attributes, that Tab and Shift+Tab never escape, that Tab genuinely cycles through more than one stop (the footnoted table is used on purpose — a plain table offers only the close button and would pass without testing anything), and that Esc returns focus to the inline frame. Verified by mutation that disabling the trap fails them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat: footnotes in the full-screen table view (1.2.0)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Releases 1.2.0. Merging this pushes the bare
1.2.0tag and publishes the GitHub release with attestedmain.js/manifest.json/styles.css.Added
Footnotes in the full-screen table view (#28, PR #31). A cell's footnote marker used to arrive in full screen as a dead
[1]— the definitions live outside the<table>the overlay clones. They are now resolved from the note's own source through Obsidian's metadata cache and rendered under the clone, so they appear no matter where the table sits in the note. Numbering is preserved, the marker jumps to its definition, and the "↩︎" back-reference jumps back, all without leaving full screen. Hovering a marker previews its definition.Scope: Reading view. Live Preview is not supported — Obsidian parses each block there on its own, so a reference-style marker is not rendered inside a table widget at all and an inline one cannot be matched to a definition. Those markers stay visible but inert; wrong footnote text under a right marker is never shown.
Changed
minAppVersion1.0.0 → 1.6.6, the release that exposes footnotes in the metadata cache. Users below 1.6.6 will not receive this update.Tabused to walk out of the overlay onto the note's own controls behind it; it now cycles within, and the overlay identifies itself as a labelled modal dialog.Fixed
idattributes into the document — a table with a^block-idpreviously appeared twice under the same id while the overlay was open. Ids inside an SVG are deliberately kept, since id-scoped<style>/<use>/gradient references break without them.Verification
Manually verified in a real vault across Reading view and Live Preview. The e2e suite was mutation-checked rather than trusted: reverting individual fixes fails the matching assertion each time.
Full detail and the review history are in PR #31.