Skip to content

fix(ZMSKVR-1229): changed div to p, added aria-atomic#1893

Open
ChristianVierthaler wants to merge 11 commits intonextfrom
bugfix-zmskvr-1229-html-structure-in-callout
Open

fix(ZMSKVR-1229): changed div to p, added aria-atomic#1893
ChristianVierthaler wants to merge 11 commits intonextfrom
bugfix-zmskvr-1229-html-structure-in-callout

Conversation

@ChristianVierthaler
Copy link
Contributor

@ChristianVierthaler ChristianVierthaler commented Feb 5, 2026

Pull Request Checklist (Feature Branch to next):

  • Ich habe die neuesten Änderungen aus dem next Branch in meinen Feature-Branch gemergt.
  • Das Code-Review wurde abgeschlossen.
  • Fachliche Tests wurden durchgeführt und sind abgeschlossen.

Summary by CodeRabbit

  • New Features

    • Updated appointment preview layout with a dedicated icon area and clearer headline/body structure.
  • Bug Fixes

    • Improved screen-reader behavior (more reliable announcements) and refined provider/date/time sections and spacing.
    • Changed inline appointment detail element to preserve intended DOM structure and avoid extra paragraph wrappers.
  • Tests

    • Added comprehensive tests for HTML wrapping/preservation to ensure consistent appointment detail rendering.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 5, 2026

📝 Walkthrough

Walkthrough

Reworked AppointmentPreview markup (removed MucCallout, added aria attributes and new inner layout with icon/blocks) and changed AppointmentSummary container element. sanitizeHtml gained block-level root detection and now wraps inline-only sanitized content in a <p>. Tests expanded for wrapping behavior.

Changes

Cohort / File(s) Summary
Appointment components
zmscitizenview/src/components/Appointment/AppointmentSelection/AppointmentPreview.vue, zmscitizenview/src/components/Appointment/AppointmentSummary.vue
Replaced MucCallout usage with plain callout markup and added aria-atomic="true" plus new icon/body layout in Preview; preserved conditional provider/time/hint rendering. In Summary changed appointment.scope.infoForAppointment host from <p> to <div>.
Sanitization utility
zmscitizenview/src/utils/sanitizeHtml.ts
Added internal hasBlockLevelRoot(html) using DOMParser to detect block-level root elements. sanitizeHtml trims/sanitizes input, returns block-root content unchanged, otherwise wraps non-empty inline-only content in <p>.
Unit tests (sanitization)
zmscitizenview/tests/unit/utils/sanitizeHtml.spec.ts
Added comprehensive tests for auto-wrapping plain text and inline-only HTML into <p>, and for preserving block-level roots (div, ul, h1, table, hr, address, section, dl, blockquote, pre). Edge cases with leading whitespace/text covered.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through tags and gave them a nudge,
Wrapped shy inline text in a cozy little budge.
Icons now nest where callouts once stood,
Blocks stand tall, and paragraphs hug good.
A rabbit’s small tweak — neat, gentle, and snug.

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title mentions 'changed div to p' and 'added aria-atomic', but the actual changeset shows the opposite: changing a p to a div in AppointmentSummary.vue, and replacing muc-callout with a div structure in AppointmentPreview.vue. The aria-atomic addition is accurate but only one of many substantial changes including sanitizeHtml refactoring and comprehensive test additions. Consider a more accurate title like 'fix(ZMSKVR-1229): refactor callout structure and improve HTML sanitization' that reflects the primary changes across multiple files.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into next

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bugfix-zmskvr-1229-html-structure-in-callout

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

ThomasAFink
ThomasAFink previously approved these changes Feb 5, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@zmscitizenview/src/components/Appointment/AppointmentSelection/AppointmentPreview.vue`:
- Around line 52-62: The block-level HTML produced by
sanitizeHtml(selectedProvider.scope.infoForAppointment) is currently placed
inside a <span> in AppointmentPreview.vue which violates HTML nesting rules;
replace the outer <span> with a block-level container (e.g., <p> or <div>) that
wraps the <strong>{{ t("hint") }}</strong>, <br />, and the element using
v-html="sanitizeHtml(selectedProvider.scope.infoForAppointment)" so the v-html
content can contain <p>, <div>, <ul>, etc.; mirror the approach used in
AppointmentSummary.vue to ensure correct semantics and accessibility.
🧹 Nitpick comments (1)
zmscitizenview/src/components/Appointment/AppointmentSelection/AppointmentPreview.vue (1)

17-51: Using <span> + <br /> for layout instead of semantic block elements.

The refactoring replaces block-level wrappers (<div>, <p>) with <span> elements and <br /> tags for vertical spacing. While this may fix a specific HTML validation issue (e.g., <p> nesting violations inside the callout), relying on <br /> for structural spacing is generally discouraged — it conflates content and presentation, and makes future styling adjustments harder.

If the root cause was invalid nesting (e.g., <div> inside <p> from MucCallout), consider using <div> wrappers with CSS margins instead, which preserves semantics and separation of concerns.

… rendering

- Replace <p> and <span> wrappers with <div> in AppointmentSummary and AppointmentPreview
- Enhance sanitizeHtml to auto-wrap plain text/inline content in <p> tags using DOM-based detection
- Use DOMParser to accurately detect block-level elements instead of fragile regex
- Add comprehensive tests for block-level element detection and auto-wrapping behavior
- Ensures valid HTML nesting and handles database inconsistency where some entries have <p> tags and others don't
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
zmscitizenview/tests/unit/utils/sanitizeHtml.spec.ts (1)

3-4: ⚠️ Potential issue | 🟡 Minor

Remove unnecessary @ts-expect-errorsanitizeHtml.ts is a plain TypeScript utility, not a Vue SFC.

The @/ path alias is properly configured in tsconfig.json, so this import should resolve without suppression. The comment message ("Vue SFC import") is also inaccurate for a plain .ts module. If a real type error exists here, it should be documented specifically rather than suppressed with a misleading comment.

🤖 Fix all issues with AI agents
In `@zmscitizenview/src/utils/sanitizeHtml.ts`:
- Around line 29-42: hasBlockLevelRoot currently only inspects firstElementChild
and thus ignores leading text nodes, causing mixed content like
"text<div>...</div>" to be treated as block-rooted; update hasBlockLevelRoot to
examine container.childNodes instead of firstElementChild and return false if
any leading child node before the first element is a text node containing
non-whitespace characters (use Node.TEXT_NODE checks and trim to detect
meaningful text), otherwise proceed to test the first element node as
block-level.
🧹 Nitpick comments (1)
zmscitizenview/src/utils/sanitizeHtml.ts (1)

46-86: Hoist blockLevelTags to module scope — it's a static constant recreated on every call.

This Set is invariant and doesn't need to be rebuilt each time hasBlockLevelRoot is invoked. Move it to module level for a minor perf win and cleaner intent.

♻️ Proposed refactor
+// Block-level HTML5 elements (static, used by hasBlockLevelRoot)
+const BLOCK_LEVEL_TAGS = new Set([
+  "address", "article", "aside", "blockquote", "details", "dialog",
+  "dd", "div", "dl", "dt", "fieldset", "figcaption", "figure",
+  "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6",
+  "header", "hgroup", "hr", "li", "main", "nav", "ol", "p",
+  "pre", "section", "table", "tbody", "td", "tfoot", "th",
+  "thead", "tr", "ul",
+]);
+
 function hasBlockLevelRoot(html: string): boolean {
   const parser = new DOMParser();
   const doc = parser.parseFromString(`<div>${html}</div>`, "text/html");
   const container = doc.body.firstElementChild;
 
   if (!container || container.children.length === 0) {
     return false;
   }
 
   const firstElement = container.firstElementChild;
   if (!firstElement) return false;
 
-  const blockLevelTags = new Set([
-    "address", "article", "aside", "blockquote", "details", "dialog",
-    ...
-    "ul",
-  ]);
-
-  return blockLevelTags.has(firstElement.tagName.toLowerCase());
+  return BLOCK_LEVEL_TAGS.has(firstElement.tagName.toLowerCase());
 }

ThomasAFink
ThomasAFink previously approved these changes Feb 16, 2026
- Check childNodes instead of firstElementChild to detect leading text
- Return false (needs wrapping) if meaningful text precedes any element
- Ignore whitespace-only text nodes before elements
- Add tests for mixed content edge cases (leading text before block/inline elements)
- Ensures all text content is properly wrapped even when mixed with block elements
@ThomasAFink ThomasAFink force-pushed the bugfix-zmskvr-1229-html-structure-in-callout branch from a68bf0d to bf81345 Compare February 16, 2026 09:55
ThomasAFink
ThomasAFink previously approved these changes Feb 16, 2026
- Fix mismatched closing tags: replace </span> with </p> and </div>
- Corrects HTML structure after previous refactoring to use <div> wrapper
- Resolves linting error: Unexpected closing tag 'span'
ThomasAFink
ThomasAFink previously approved these changes Feb 16, 2026
ThomasAFink
ThomasAFink previously approved these changes Feb 16, 2026
…-callout wrapping in <p>

- Wrap all content sections in a single container <div> within the content slot
- Prevents muc-callout from wrapping multiple top-level elements in <p> tag
- Ensures valid HTML structure when muc-callout processes the content slot
- Fixes invalid HTML nesting: <p><div>...</div></p>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@zmscitizenview/src/components/Appointment/AppointmentSelection/AppointmentPreview.vue`:
- Around line 14-15: The aria-label on the info callout is hardcoded as
"Information" and should use the app's translation function instead; update the
element in AppointmentPreview.vue to call the i18n translator (use the same
t(...) helper used elsewhere in this template/component) for the aria-label so
it becomes aria-label="t('Information')" or the appropriate translation key,
ensuring you import/receive the same t reference used by other strings in this
component.

In `@zmscitizenview/src/utils/sanitizeHtml.ts`:
- Around line 121-127: The current wrap logic returns `<p>${trimmed}</p>` when
hasBlockLevelRoot(trimmed) is false, which produces invalid HTML for mixed
content like "Some text<div>x</div>"; instead, parse trimmed (e.g., with
DOMParser or a lightweight regex split) and iterate child nodes: accumulate
consecutive text/inline nodes into a new <p> string and emit block-level
elements (div/section/article/etc.) as-is, flushing any accumulated inline text
as a <p> before each block; update the code around hasBlockLevelRoot and the
final return to build and return the concatenated serialized string (use the
existing hasBlockLevelRoot(trimmed), trimmed variable, and the
sanitizeHtml/sanitize function scope to locate where to implement the
node-iteration/accumulation logic).
🧹 Nitpick comments (2)
zmscitizenview/src/utils/sanitizeHtml.ts (2)

62-102: Move blockLevelTags to module scope to avoid repeated allocations.

This Set is a static constant but gets re-created on every call to hasBlockLevelRoot. Hoist it to module level.

♻️ Suggested refactor
+const BLOCK_LEVEL_TAGS = new Set([
+  "address", "article", "aside", "blockquote", "details", "dialog",
+  "dd", "div", "dl", "dt", "fieldset", "figcaption", "figure",
+  "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6",
+  "header", "hgroup", "hr", "li", "main", "nav", "ol", "p",
+  "pre", "section", "table", "tbody", "td", "tfoot", "th",
+  "thead", "tr", "ul",
+]);
+
 function hasBlockLevelRoot(html: string): boolean {
   // ...
-      const blockLevelTags = new Set([...]);
-      return blockLevelTags.has(element.tagName.toLowerCase());
+      return BLOCK_LEVEL_TAGS.has(element.tagName.toLowerCase());
   // ...
 }

As per coding guidelines, "Keep configurable data at high levels."


30-34: DOMParser instantiation on every call is heavier than needed.

Consider caching the parser at module level since DOMParser instances are stateless and reusable:

+const domParser = new DOMParser();
+
 function hasBlockLevelRoot(html: string): boolean {
-  const parser = new DOMParser();
-  const doc = parser.parseFromString(`<div>${html}</div>`, "text/html");
+  const doc = domParser.parseFromString(`<div>${html}</div>`, "text/html");

Comment on lines +14 to +15
class="m-callout m-callout--info"
aria-label="Information"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hardcoded aria-label="Information" should use the translation function.

Every other user-facing string in this template uses t(...), but this aria-label is a raw English string. Screen reader users in other locales will hear "Information" in English regardless of their language setting.

-      aria-label="Information"
+      :aria-label="t('information')"
🤖 Prompt for AI Agents
In
`@zmscitizenview/src/components/Appointment/AppointmentSelection/AppointmentPreview.vue`
around lines 14 - 15, The aria-label on the info callout is hardcoded as
"Information" and should use the app's translation function instead; update the
element in AppointmentPreview.vue to call the i18n translator (use the same
t(...) helper used elsewhere in this template/component) for the aria-label so
it becomes aria-label="t('Information')" or the appropriate translation key,
ensuring you import/receive the same t reference used by other strings in this
component.

Comment on lines +121 to +127
// Check if content already has a block-level root element
if (hasBlockLevelRoot(trimmed)) {
return trimmed;
}

// Wrap plain text or inline-only content in <p> tag
return `<p>${trimmed}</p>`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Wrapping mixed content (leading text + block element) in <p> produces invalid HTML.

When hasBlockLevelRoot returns false for input like "Some text<div>content</div>", the result is <p>Some text<div>content</div></p> — a <div> inside a <p> is invalid per the HTML spec (browsers will auto-close the <p> before the <div>, causing unpredictable DOM structure).

This edge case is tested and apparently intentional (spec line 164-171), but worth being aware that the browser will not render the markup as written. If this input pattern is realistic, consider splitting the text node and block element rather than wrapping everything in <p>.

🤖 Prompt for AI Agents
In `@zmscitizenview/src/utils/sanitizeHtml.ts` around lines 121 - 127, The current
wrap logic returns `<p>${trimmed}</p>` when hasBlockLevelRoot(trimmed) is false,
which produces invalid HTML for mixed content like "Some text<div>x</div>";
instead, parse trimmed (e.g., with DOMParser or a lightweight regex split) and
iterate child nodes: accumulate consecutive text/inline nodes into a new <p>
string and emit block-level elements (div/section/article/etc.) as-is, flushing
any accumulated inline text as a <p> before each block; update the code around
hasBlockLevelRoot and the final return to build and return the concatenated
serialized string (use the existing hasBlockLevelRoot(trimmed), trimmed
variable, and the sanitizeHtml/sanitize function scope to locate where to
implement the node-iteration/accumulation logic).

@ThomasAFink ThomasAFink force-pushed the bugfix-zmskvr-1229-html-structure-in-callout branch from d1c0b3e to 4219a4c Compare February 16, 2026 10:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants