Skip to content

The Fluent UI demo's viewport disables pinch-zoom (WCAG 2.1 1.4.4) #341

Description

@phmatray

Problem / motivation

FormCraft.DemoFluentApp ships a viewport meta that disables pinch-zoom:

<!-- FormCraft.DemoFluentApp/wwwroot/index.html:6 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />

The established sibling demo does not:

<!-- FormCraft.DemoBlazorApp/wwwroot/index.html:6 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

Both verified on dev. So the newer demo app — added alongside the Fluent UI adapter (#278/#291) —
regressed against the reference demo rather than making a deliberate choice: this is the stock
Blazor WASM template line, carried in unedited.

maximum-scale=1.0, user-scalable=no prevents a low-vision user from pinch-zooming on iOS Safari and
older Android browsers. That is a zoom-suppression failure of WCAG 2.1 SC 1.4.4 Resize Text
(Level AA)
, and it lands in a repository that has repeatedly treated accessibility conformance as a
hard requirement rather than a nicety — #199 (required-field annotation, Level A), #262 (file-upload
required state), #281 (focus after clearing an upload), #285 (ARIA state in the demo). A showcase app
that cannot be zoomed undercuts the thing it is showcasing.

Modern engines increasingly ignore user-scalable=no, which is mitigation, not a fix: Safari on iOS
respects maximum-scale, and the repo cannot control which engine a visitor uses.

Proposed solution

Drop the two scale clauses so the Fluent demo matches the MudBlazor demo:

<meta name="viewport" content="width=device-width, initial-scale=1.0" />

One line, no behavioural risk — the layout is responsive either way; this only restores the user's
ability to zoom.

Then decide whether to guard it. The repo has the idiom for exactly this kind of
silently-regressing invariant (FormCraft.UnitTests/Ci/GitignoreTests,
FormCraft.UnitTests/Ci/ClaudeMdTestCommandsTests): a static assertion that no demo app's viewport
meta contains user-scalable=no or maximum-scale costs almost nothing and catches the next app
scaffolded from the same template. The plan below includes it, since a second demo app already
reproduced the defect once.

Alternatives considered

  • Fix the line, no guard. Smallest change. Rejected as the sole measure: this exact regression
    arrived by copying a template, and a third demo app would copy it again — the failure is invisible
    in review because the line looks like boilerplate.
  • Keep user-scalable=no and rely on modern browsers ignoring it. Rejected: iOS Safari honours
    maximum-scale, so the barrier is real for a substantial audience, and shipping a known
    accessibility defect in a showcase is the wrong signal for this repo.
  • Guard every accessibility attribute of the demo shells. Over-broad for a demo-only concern, and
    hard to keep meaningful; the viewport is the one that silently disables a whole user capability.

Area

Demo applications — FormCraft.DemoFluentApp shell markup; accessibility


Follow-up from #328. Related: #285, #281, #262, #199

🧠 Brainstorm

Problem / context

FormCraft.DemoFluentApp is the showcase for the Fluent UI adapter, added with #278/#291. Its
index.html came from the Blazor WASM template, whose default viewport meta suppresses zoom — a
long-standing template wart that predates the accessibility guidance now in WCAG.

The repo holds a high bar here: several of its recent fixes (#199, #262, #281, #285) exist purely to
correct accessibility defects, and CLAUDE.md records the reasoning for required-field annotation in
detail. Against that standard, shipping a demo that cannot be pinch-zoomed is an outlier — and it is
one nobody would spot in review, because the line reads as scaffolding.

Approaches

A. Fix the line. Delete maximum-scale=1.0, user-scalable=no. Pros: one line, removes the
barrier immediately, brings the app in line with its sibling. Cons: nothing stops the next
template-scaffolded app from reintroducing it.

B. Fix the line and add a Ci/ guard asserting no demo app's viewport disables scaling. Pros:
the repo already uses static Ci/ guards for invariants that regress silently, and this one has
already regressed once by exactly the mechanism a guard catches. Cheap: a string assertion over two
files. Cons: one more test to maintain; arguably heavyweight for a demo.

C. Fix it and note it in CLAUDE.md. Pros: documents intent. Cons: documentation does not
fail a build, and the whole point is that this is invisible to reviewers.

Recommendation

B. A alone is the fix, but the evidence for the guard is unusually direct: there are two demo
apps, one has the defect and one does not, and the difference arrived by copying a template. That is
the precise situation the repo's existing Ci/ guards were written for. C adds a sentence nothing
enforces.

📋 Spec

Goal

No demo application ships a viewport that disables user scaling, and a future one cannot silently
reintroduce it.

Scope

  • FormCraft.DemoFluentApp/wwwroot/index.html — the viewport meta.
  • A Ci/ guard asserting demo viewport metas do not suppress zoom.

Non-goals

Behaviour

flowchart TD
    A["DemoFluentApp index.html<br/>maximum-scale=1.0, user-scalable=no"] --> B["iOS Safari honours maximum-scale"]
    B --> C["low-vision user cannot pinch-zoom<br/>WCAG 2.1 SC 1.4.4 ❌"]
    D["DemoBlazorApp index.html<br/>width=device-width, initial-scale=1.0"] --> E["zoom works ✅"]
    F["Ci guard: no demo viewport may carry<br/>user-scalable=no / maximum-scale"] --> E
Loading

Key files

  • FormCraft.DemoFluentApp/wwwroot/index.html (line 6) — the file changed.
  • FormCraft.DemoBlazorApp/wwwroot/index.html (line 6) — the correct reference; not modified.
  • FormCraft.UnitTests/Ci/GitignoreTests.cs — the guard idiom to copy (repo-root resolution via
    WorkflowSource.RepoRoot).

Validation rules

Edge cases

  • Match case-insensitively and tolerate whitespace variations (user-scalable = no).
  • A future demo app must be picked up automatically — glob the demo wwwroot/index.html files rather
    than hard-coding two paths, or the guard silently ignores app number three.

Assumptions

  • Both demo apps keep a wwwroot/index.html shell (true for Blazor WASM).

🛠️ Implementation plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: restore pinch-zoom in the Fluent demo, and guard it so a scaffolded app cannot remove it again.

Architecture: demo shell markup plus one static Ci/ invariant test. No library code changes.

Tech stack: .NET 10 SDK pinned by global.json (10.0.302), Blazor WASM, xUnit + Shouldly.

Global constraints:

  • Base branch dev; commit as Philippe Matray <phmatray@gmail.com>; conventional commits.
  • ⛔ Do not modify library markup or FormCraft.DemoBlazorApp — it is already correct and serves as the reference.
  • TreatWarningsAsErrors=true is deliberate — do not relax it.
  • dotnet test --filter is inert here (MTP). Use dotnet test <csproj> -c Release -- --filter-class <FQN>, or the built host directly; see .claude/skills/repo-profile.mdBuild & test.

Task 1: Guard the invariant (failing test first)

Files: create FormCraft.UnitTests/Ci/DemoViewportTests.cs; read FormCraft.UnitTests/Ci/GitignoreTests.cs for the repo-root pattern.

Interfaces: xUnit facts over every demo wwwroot/index.html found under the repo root.

  • Step 1: Reuse WorkflowSource.RepoRoot (as GitignoreTests does) and glob every */wwwroot/index.html under the repo root, so a future demo app is covered without editing the test.
  • Step 2: Assert the scan found at least the two known shells — an empty glob must fail loudly rather than pass vacuously (the vacuity hole fixed in ClaudeMdTestCommandsTests under CLAUDE.md still teaches VSTest test commands that Microsoft.Testing.Platform silently ignores #299).
  • Step 3: Write the failing assertion: no viewport meta may contain user-scalable=no or maximum-scale (case-insensitive, whitespace-tolerant), naming the offending file.
  • Step 4: Run it → FAIL, naming FormCraft.DemoFluentApp/wwwroot/index.html. Record the output.
  • Step 5: Commit: test(ci): pin that no demo viewport disables user scaling.

Task 2: Restore pinch-zoom and verify

Files: modify FormCraft.DemoFluentApp/wwwroot/index.html (line 6).

Interfaces: none.

  • Step 1: Replace the viewport with <meta name="viewport" content="width=device-width, initial-scale=1.0" />, matching FormCraft.DemoBlazorApp.
  • Step 2: Re-run Task 1's test → PASS.
  • Step 3: Run ./build.sh Test (CI gate) → all suites green under TreatWarningsAsErrors.
  • Step 4: Commit: fix(demo): allow pinch-zoom in the Fluent UI demo.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions