fix(painter-dom): honor appearance:hidden on inline SDTs (SD-3110)#3293
Conversation
ECMA-376 §17.5.2.6 (w15:appearance val="hidden") means the SDT is
present in the document for anchoring but visually transparent. Two
prior leaks made hidden SDTs anything but:
1. The renderer painted full chrome (padding/border/hover/selected
outline) regardless of appearance, leaving a visible bracket
around the wrapped span.
2. The alias label was stamped into the DOM as a <span> child whose
textContent included the alias. That text leaked into copy-paste
(selecting and copying the wrapped phrase pulled in 'Inline
content' / 'Harvey citation' / whatever the SDT's alias was) AND
into screen-reader output.
The data was already correct end-to-end on the converter side: import
parses w15:appearance into the node attrs (extractAppearance in
handle-structured-content-node.js), and the Document API surfaces it.
The gap was that StructuredContentMetadata in @superdoc/contracts
didn't carry the field, so the pm-adapter -> renderer bridge stripped
it.
Four-file fix:
- contracts: add appearance?: StructuredContentAppearance to
StructuredContentMetadata.
- style-engine: read attrs.appearance in
normalizeStructuredContentMetadata, validating against the three
spec values (boundingBox | tags | hidden); unknown values are
dropped rather than poisoning rendering.
- painter-dom renderer: createInlineSdtWrapper now stamps
data-appearance="hidden" on the wrapper AND skips appending the
alias <span> entirely when hidden.
- painter-dom styles: CSS rule keyed off
[data-appearance='hidden'] zeroes padding/border/border-radius
and neutralizes hover and selected states.
Tests:
- style-engine: appearance carries through, unknown values are
dropped, omitted attr stays undefined.
- painter-dom: render a hidden inline SDT and assert
(a) data-appearance='hidden' is on the wrapper,
(b) no .superdoc-structured-content-inline__label child exists,
(c) wrapper.textContent equals exactly the wrapped phrase with
the alias text nowhere in it.
Verified:
- @superdoc/painter-dom: 1071/1071
- @superdoc/style-engine: 132/132
- @superdoc/contracts: 232/232
- @superdoc/pm-adapter: 1838/1838
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 408b268a65
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
End-to-end coverage for the painter-dom fix in #3293. Fixture is a 5-paragraph DOCX with inline SDTs across the appearance matrix: hidden, boundingBox, default (omitted), and two adjacent hidden. Five assertions, one per claim the PR makes plus a copy-paste smoke test: - data-appearance="hidden" stamped on hidden wrappers, absent on others - no __label child inside hidden wrappers; present on others - hidden wrappers omit the alias canary from textContent - no hidden-alias canary appears in the painted layout root - selection.toString() over a hidden wrapper returns only the wrapped phrase Visual coverage follow-up: drop a slim variant in tests/visual/test-data/ via pnpm docs:upload (corpus is R2-backed, not in-tree).
…SD-3110) Caught in review: the lock-hover rule .superdoc-structured-content-inline[data-lock-mode]:hover:not(.ProseMirror-selectednode) has (0,4,0) specificity, one higher than the hidden-appearance rule's (0,3,0). Hovering a hidden inline SDT therefore re-introduced the blue background and z-index 9999999 boost the rule meant to suppress. Exclude data-appearance="hidden" from the inline branch of the lock-hover rule. Block-level branch is untouched; block hidden isn't a render path yet. Adds a behavior regression assertion: hover a hidden wrapper and verify the computed backgroundColor doesn't pick up the lock-hover blue and z-index doesn't jump to 9999999. 18/18 behavior cases × 3 browsers green; painter-dom unit tests still 1071/1071.
…Ts (SD-3110) The previous fix added a second chained :not([data-appearance='hidden']) to the inline lock-hover rule, which bumped its specificity from (0,4,0) to (0,5,0). The viewing-mode suppression rule below sits at (0,4,0), so it lost the cascade and the lock-hover blue re-appeared on hover in viewing mode — regressing the SD-2232 behavior test "inline SDT hover does not show background in viewing mode". Collapse the two predicates into a single :not(a, b). Comma-list :not() takes the max specificity of its arguments, not the sum, so the selector stays at (0,4,0), viewing-mode suppression wins again, and the hidden-appearance exclusion is preserved. Verified: 22/22 SDT behavior cases on chromium, 44/44 on firefox+webkit; painter-dom unit tests still 1071/1071.
|
🎉 This PR is included in @superdoc-dev/mcp v0.3.0-next.101 The release is available on GitHub release |
|
🎉 This PR is included in vscode-ext v2.3.0-next.147 |
|
🎉 This PR is included in @superdoc-dev/react v1.2.0-next.145 The release is available on GitHub release |
|
🎉 This PR is included in superdoc-cli v0.8.0-next.116 The release is available on GitHub release |
|
🎉 This PR is included in superdoc-sdk v1.8.0-next.99 |
|
🎉 This PR is included in superdoc v1.30.0-next.96 The release is available on GitHub release |
…uperdoc-dev#3293) * fix(painter-dom): honor appearance:hidden on inline SDTs (SD-3110) ECMA-376 §17.5.2.6 (w15:appearance val="hidden") means the SDT is present in the document for anchoring but visually transparent. Two prior leaks made hidden SDTs anything but: 1. The renderer painted full chrome (padding/border/hover/selected outline) regardless of appearance, leaving a visible bracket around the wrapped span. 2. The alias label was stamped into the DOM as a <span> child whose textContent included the alias. That text leaked into copy-paste (selecting and copying the wrapped phrase pulled in 'Inline content' / 'Harvey citation' / whatever the SDT's alias was) AND into screen-reader output. The data was already correct end-to-end on the converter side: import parses w15:appearance into the node attrs (extractAppearance in handle-structured-content-node.js), and the Document API surfaces it. The gap was that StructuredContentMetadata in @superdoc/contracts didn't carry the field, so the pm-adapter -> renderer bridge stripped it. Four-file fix: - contracts: add appearance?: StructuredContentAppearance to StructuredContentMetadata. - style-engine: read attrs.appearance in normalizeStructuredContentMetadata, validating against the three spec values (boundingBox | tags | hidden); unknown values are dropped rather than poisoning rendering. - painter-dom renderer: createInlineSdtWrapper now stamps data-appearance="hidden" on the wrapper AND skips appending the alias <span> entirely when hidden. - painter-dom styles: CSS rule keyed off [data-appearance='hidden'] zeroes padding/border/border-radius and neutralizes hover and selected states. Tests: - style-engine: appearance carries through, unknown values are dropped, omitted attr stays undefined. - painter-dom: render a hidden inline SDT and assert (a) data-appearance='hidden' is on the wrapper, (b) no .superdoc-structured-content-inline__label child exists, (c) wrapper.textContent equals exactly the wrapped phrase with the alias text nowhere in it. Verified: - @superdoc/painter-dom: 1071/1071 - @superdoc/style-engine: 132/132 - @superdoc/contracts: 232/232 - @superdoc/pm-adapter: 1838/1838 * test(behavior): cover hidden-appearance inline SDT (SD-3110) End-to-end coverage for the painter-dom fix in superdoc-dev#3293. Fixture is a 5-paragraph DOCX with inline SDTs across the appearance matrix: hidden, boundingBox, default (omitted), and two adjacent hidden. Five assertions, one per claim the PR makes plus a copy-paste smoke test: - data-appearance="hidden" stamped on hidden wrappers, absent on others - no __label child inside hidden wrappers; present on others - hidden wrappers omit the alias canary from textContent - no hidden-alias canary appears in the painted layout root - selection.toString() over a hidden wrapper returns only the wrapped phrase Visual coverage follow-up: drop a slim variant in tests/visual/test-data/ via pnpm docs:upload (corpus is R2-backed, not in-tree). * fix(painter-dom): keep hidden inline SDTs out of lock-hover styling (SD-3110) Caught in review: the lock-hover rule .superdoc-structured-content-inline[data-lock-mode]:hover:not(.ProseMirror-selectednode) has (0,4,0) specificity, one higher than the hidden-appearance rule's (0,3,0). Hovering a hidden inline SDT therefore re-introduced the blue background and z-index 9999999 boost the rule meant to suppress. Exclude data-appearance="hidden" from the inline branch of the lock-hover rule. Block-level branch is untouched; block hidden isn't a render path yet. Adds a behavior regression assertion: hover a hidden wrapper and verify the computed backgroundColor doesn't pick up the lock-hover blue and z-index doesn't jump to 9999999. 18/18 behavior cases × 3 browsers green; painter-dom unit tests still 1071/1071. * fix(painter-dom): restore viewing-mode hover suppression on inline SDTs (SD-3110) The previous fix added a second chained :not([data-appearance='hidden']) to the inline lock-hover rule, which bumped its specificity from (0,4,0) to (0,5,0). The viewing-mode suppression rule below sits at (0,4,0), so it lost the cascade and the lock-hover blue re-appeared on hover in viewing mode — regressing the SD-2232 behavior test "inline SDT hover does not show background in viewing mode". Collapse the two predicates into a single :not(a, b). Comma-list :not() takes the max specificity of its arguments, not the sum, so the selector stays at (0,4,0), viewing-mode suppression wins again, and the hidden-appearance exclusion is preserved. Verified: 22/22 SDT behavior cases on chromium, 44/44 on firefox+webkit; painter-dom unit tests still 1071/1071.
Hidden inline content controls (
<w15:appearance w15:val="hidden"/>) were rendering with visible chrome and the alias label was stamped as a child<span>, so the alias text appeared in painter-domtextContentand copy-paste pulled it along with the wrapped phrase.The import side was already correct: the converter parses
w15:appearanceinto node attrs and the Document API surfaces it. The gap was thatStructuredContentMetadatain@superdoc/contractsdidn't carry the field, so the pm-adapter → renderer bridge dropped it. The fix adds it there, threads it throughstyle-engineandpainter-dom, and uses it to (a) stampdata-appearance="hidden"for a CSS rule that strips chrome and (b) skip emitting the alias<span>entirely.Scope is the painted DomPainter surface (what sighted users see and what their selection/copy operates on). The hidden ProseMirror editable still includes the alias as PM text nodes; the screen-reader / keyboard-focus path is a separate concern and is tracked as a follow-up.
Verified:
pnpm --filter @superdoc/painter-dom test→ 1071/1071;style-engine→ 132/132;contracts→ 232/232;pm-adapter→ 1838/1838. New end-to-end coverage intests/behavior/tests/sdt/inline-sdt-appearance.spec.ts(5 cases × 3 browsers, all green) loads a 5-paragraph fixture covering the appearance matrix (hidden / boundingBox / default / two adjacent hidden) and asserts data-attr, missing label child, missing alias in textContent, missing alias anywhere in the painted layout root, and cleanselection.toString().Review: check that no other call site of
createInlineSdtWrapperrelied on the alias label always being present (I confirmed two call sites; both just consume the returned wrapper).