Skip to content

VPR-104 fix(a11y): establish shared accessibility foundation (PR 1 of 6)#130

Open
rlorenzo wants to merge 21 commits intomainfrom
VPR-104-accessibility-audit-base
Open

VPR-104 fix(a11y): establish shared accessibility foundation (PR 1 of 6)#130
rlorenzo wants to merge 21 commits intomainfrom
VPR-104-accessibility-audit-base

Conversation

@rlorenzo
Copy link
Copy Markdown
Contributor

@rlorenzo rlorenzo commented Mar 31, 2026

Summary

Accessibility foundation for shared layouts (Razor + Vue SPAs), covering WCAG 2.1 AA compliance across navigation, color contrast, landmarks, and interactive feedback.

Landmarks & Navigation

  • Single <main> landmark per page: Razor layouts get <main> directly; Vue SPAs get it from ViperLayout.vue / ViperLayoutSimple.vue (1.3.1 Info and Relationships)
  • Skip-to-content link on all four layouts (two Razor, two Vue), with tabindex="-1" on the <main> target so focus reliably moves for keyboard/screen reader users (2.4.1 Bypass Blocks)
  • Route focus management: useRouteFocus composable announces SPA page changes to screen readers (2.4.3 Focus Order)
  • LeftNav navigation landmark: wrap q-list in <nav> with aria-label from menu header, change q-list role from presentation to list so listitem children have the required parent role, remove role="none" override from interactive q-items (conflicted with tabindex)

Color Contrast (AA)

  • Status colors aligned to UC Davis secondary palette — positive: Redwood, negative: Merlot, info: Tahoe — all passing AA contrast with white text (1.4.3 Contrast (Minimum))
  • Quasar utility overrides: .text-grey, .bg-grey, .bg-green overridden with !important to meet AA (Quasar defaults fail)
  • Razor layout fallback colors synced to match the palette
  • Environment badge contrast: remove color: red override on .mainLayoutViperMode that produced red-on-merlot (2.49:1) — badge now uses the default white-on-merlot (~7.2:1)
  • Quasar clear button opacity: override Quasar's 0.6 opacity on .q-field__focusable-action so WCAG contrast holds
  • Inactive tab fade prevention: new .tabs-no-fade class to opt out of Quasar's opacity reduction on inactive tabs
  • q-tree arrow contrast: increase arrow color and font size for legibility
  • GenericError / SessionTimeout: replace color="red" / color="orange" with brand color="negative" / color="warning" on icons

Screen Reader & Keyboard

  • Heading hierarchy fixed (proper <h1> on error/permissions pages, shared heading styles) (1.3.1)
  • External link indicators: "opens in new window" sr-only text + icon on target="_blank" links in footer and LeftNav; rel="noopener noreferrer" to prevent reverse-tabnabbing; icon only shown for truly external sites (not same-hostname VIPER1 links) (3.2.5 Change on Request)
  • aria-label on icon-only buttons (mobile menu, sidebar toggle, help link) (4.1.2 Name, Role, Value)
  • aria-hidden on decorative icons in footer links
  • Overflow title directive: v-overflow-title sets title attribute only when nav text is truncated; updates on resize/drawer toggle via ResizeObserver

Interactive Feedback

  • CRUD status notifications on quasarTable create/update/delete (plain DOM toast with role="status" — Quasar Notify does not render in UMD body-mount setup) (4.1.3 Status Messages)
  • Delete confirmation dialog with cancel support; VMACS exports guarded so they only fire on confirmed deletes

Components

  • StatusBanner.vue: accessible tinted-background banner (success/error/warning/info) with colored left border, dark text on light background, dismissible option, and action slot
  • Alt text added to logo, profile avatar, and student photo images (1.1.1 Non-text Content)

Config

  • ESLint CSHTML config: browser/project globals added, files with unparseable Razor syntax excluded

Bug Fixes (discovered during review)

  • quasarTable reload callbacks: create/update/delete were passing the table instance (this.load(this)) to load() as the vueApp argument, which then went into viperFetch and showViperFetchError. A failed reload after a mutation silently wrote the error flag to the table instance instead of the Vue app, leaving the user with a stale grid and no error dialog. Pattern was pre-existing since the original qtable.js commit.
  • Case-insensitive LeftNav path matching: StringComparison.OrdinalStringComparison.OrdinalIgnoreCase in Default.cshtml so menu highlight survives URL case differences

Copilot AI review requested due to automatic review settings March 31, 2026 20:11
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Mar 31, 2026

Codecov Report

❌ Patch coverage is 0% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 42.66%. Comparing base (e595a95) to head (3e96a7a).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
web/Views/Shared/_VIPERLayout.cshtml 0.00% 7 Missing ⚠️
web/Views/Shared/Components/LeftNav/Default.cshtml 0.00% 1 Missing ⚠️
web/Views/Shared/Components/MainNav/MainNav.cs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #130      +/-   ##
==========================================
- Coverage   42.66%   42.66%   -0.01%     
==========================================
  Files         802      802              
  Lines       48448    48449       +1     
  Branches     4446     4446              
==========================================
  Hits        20672    20672              
- Misses      27285    27286       +1     
  Partials      491      491              
Flag Coverage Δ
backend 42.58% <0.00%> (-0.01%) ⬇️
frontend 44.74% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Mar 31, 2026

Bundle Report

Changes will increase total bundle size by 2.8kB (0.12%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
viper-frontend-esm 2.32MB 2.8kB (0.12%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: viper-frontend-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/GenericError-*.css 525 bytes 203.83kB 0.26%
assets/effort-*.js -17 bytes 23.24kB -0.07%
assets/GenericError-*.js 1.91kB 20.86kB 10.07% ⚠️
assets/cts-*.js 419 bytes 10.12kB 4.32%
assets/StudentSelect-*.js 85 bytes 5.39kB 1.6%
assets/cahfs-*.js -24 bytes 1.82kB -1.3%
assets/clinicalscheduler-*.js -24 bytes 1.75kB -1.35%
assets/cms-*.js -24 bytes 1.55kB -1.52%
assets/students-*.js -24 bytes 1.48kB -1.6%
assets/computing-*.js -24 bytes 1.16kB -2.03%
assets/main-*.js -1 bytes 848 bytes -0.12%

Files in assets/effort-*.js:

  • ./src/Effort/App.vue → Total Size: 109 bytes

Files in assets/GenericError-*.js:

  • ./src/layouts/LeftNav.vue → Total Size: 122 bytes

  • ./src/layouts/MainNav.vue → Total Size: 122 bytes

  • ./src/config/colors.ts → Total Size: 3.7kB

  • ./src/components/SessionTimeout.vue → Total Size: 146 bytes

  • ./src/components/GenericError.vue → Total Size: 140 bytes

  • ./src/layouts/ViperLayout.vue → Total Size: 134 bytes

Files in assets/cts-*.js:

  • ./src/CTS/App.vue → Total Size: 106 bytes

  • ./src/layouts/ViperLayoutSimple.vue → Total Size: 152 bytes

Files in assets/StudentSelect-*.js:

  • ./src/components/StudentSelect.vue → Total Size: 143 bytes

Files in assets/cahfs-*.js:

  • ./src/CAHFS/App.vue → Total Size: 108 bytes

Files in assets/clinicalscheduler-*.js:

  • ./src/ClinicalScheduler/App.vue → Total Size: 120 bytes

Files in assets/cms-*.js:

  • ./src/CMS/App.vue → Total Size: 106 bytes

Files in assets/students-*.js:

  • ./src/Students/App.vue → Total Size: 111 bytes

Files in assets/computing-*.js:

  • ./src/Computing/App.vue → Total Size: 112 bytes

Files in assets/main-*.js:

  • ./src/App.vue → Total Size: 910 bytes

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Establishes a shared accessibility foundation across the VIPER Razor views and Vue SPAs by standardizing semantic landmarks, heading hierarchy, image alt text, and linting support for Razor-embedded scripts.

Changes:

  • Moves the <main> landmark into shared Vue/Razor layouts and removes redundant <main> wrappers from SPA App.vue files.
  • Normalizes heading hierarchy (e.g., error/permission pages) and adds shared h1 styling for consistent presentation.
  • Improves accessibility metadata (image alt text) and adds a route-focus composable intended for screen reader navigation; updates ESLint configuration to better handle .cshtml.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
web/wwwroot/css/site.css Adds shared h1 styling in Razor/Quasar page containers.
web/Views/Shared/VIPERLayoutSimplified.cshtml Wraps rendered body content in a <main> landmark.
web/Views/Shared/Components/ProfilePic/Default.cshtml Adds alt text to the profile avatar image.
web/Views/Shared/_VIPERLayout.cshtml Adds alt text to the header logo and introduces a <main> landmark around page content.
web/Views/Home/MyPermissions.cshtml Introduces an h1 to fix page heading hierarchy.
web/Views/Home/Error.cshtml Updates headings to h1/h2 for correct hierarchy.
web/Views/Home/403.cshtml Updates headings to h1/h2 and demotes dynamic message to paragraph text.
VueApp/src/styles/base.css Adds heading styles for dialogs/page containers (including h1).
VueApp/src/Students/App.vue Removes local <main> wrapper (landmark moved to layout).
VueApp/src/pages/Error404.vue Promotes 404 heading to h1.
VueApp/src/layouts/ViperLayoutSimple.vue Adds <main> landmark inside the shared Vue layout.
VueApp/src/layouts/ViperLayout.vue Adds <main> landmark inside the shared Vue layout.
VueApp/src/Effort/App.vue Removes local <main> wrapper (landmark moved to layout).
VueApp/src/CTS/App.vue Removes local <main> wrapper (landmark moved to layout).
VueApp/src/Computing/App.vue Removes local <main> wrapper (landmark moved to layout).
VueApp/src/composables/use-route-focus.ts Adds a composable for focusing main content after navigation.
VueApp/src/components/StudentSelect.vue Adds dynamic alt text for the selected student photo.
VueApp/src/CMS/App.vue Removes local <main> wrapper (landmark moved to layout).
VueApp/src/ClinicalScheduler/App.vue Removes local <main> wrapper (landmark moved to layout).
VueApp/src/CAHFS/App.vue Removes local <main> wrapper (landmark moved to layout).
VueApp/src/App.vue Removes an empty <main> element from the base Vue template.
scripts/lint-staged-cshtml.js Adjusts ESLint CLI args to reduce noise for ignored files.
eslint.config.mjs Adds Razor-specific ignores and defines globals for linting .cshtml script blocks.

- Move <main> landmark from SPA App.vue files to shared layouts
- Add <main> to ViperLayout.vue and ViperLayoutSimple.vue for Vue SPAs
- Add <main> to _VIPERLayout.cshtml and VIPERLayoutSimplified.cshtml for Razor pages
- Fix heading hierarchy on error and permissions pages (h3/h4 → h1/h2)
- Add h1 styles to shared CSS for Razor and Vue pages
- Add alt text to logo, profile avatar, and student photo images
- Add useRouteFocus composable for screen reader navigation (WCAG 2.4.3)
- Configure ESLint globals and ignores for cshtml Razor files
@rlorenzo rlorenzo force-pushed the VPR-104-accessibility-audit-base branch from 3351609 to 2e1df3d Compare April 1, 2026 00:53
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated 3 comments.

…palette

- Fix MainNav help link: add missing Vue binding (:href), aria-label,
  and correct helpNav type from string to NavItem
- Align status colors to UC Davis secondary palette:
  positive: Redwood (#266041), negative: Merlot (#79242F), info: Putah Creek (#008EAA)
- Update both colors.ts (Vue SPAs) and ucdavis-colors.js (Razor pages)
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 1 comment.

…or alignment

- Fix S3: add aria-label="Help" to help link in MainNav (Vue + Razor)
- Fix S4: add skip-to-content link to all 4 layouts (Vue + Razor)
- Fix S8: align status colors to UC Davis palette (Redwood, Merlot, Rec Pool),
  override text-grey/bg-grey to UC Davis Black 60 for AA contrast
- Fix S11: set role="presentation" on LeftNav q-list, role="none" on clickable
  q-items to prevent invalid aria-allowed-role on anchor elements
- Fix S2: add "(opens in new window)" to external links in LeftNav and footer
- Fix M1: add aria-hidden="true" to footer icon elements
- Add aria-label to mobile menu and sidebar toggle buttons
- Add .sr-only and .skip-to-content CSS classes to site.css for Razor pages
@rlorenzo
Copy link
Copy Markdown
Contributor Author

rlorenzo commented Apr 1, 2026

@bsedwards This PR updates the colors for the success, error, info, and warning to be accessible and match the UC Davis Color guidelines: https://communicationsguide.ucdavis.edu/sites/g/files/dgvnsk6246/files/files/page/UC-Davis-Colors-Palette-Accessibility_1.pdf

Here is a doc to compare the colors against the previous value.
https://www.dropbox.com/scl/fi/5s57bzsu8jivg6nr2bngr/color-review.html?rlkey=ipmi4hvfdvxxy4vqgm7x39s7l&e=1&st=13o7aelf&dl=0

I am unsure about the info color, but the others have color matches close to the guidelines.

The new text-gray color is much darker than before, but it is the lightest (60%) black level that is AA accessible (https://communicationsguide.ucdavis.edu/brand-guide/colors).

@bsedwards
Copy link
Copy Markdown
Collaborator

@rlorenzo New colors look good. I like the rec pool color but either that or Tahoe would work

…o Tahoe

- Create shared StatusBanner.vue with accessible tinted-background design
  (colored left border, dark text on light bg — all types pass AA contrast)
- Supports success, error, warning, info types with default icons
- Includes dismissible option, proper ARIA roles, and action slot
- Update info color from Rec Pool (#6FCFEB) to Tahoe (#00B2E3) for better
  button contrast with dark text
- Fix LeftNav aria-required-parent: add role="none" to non-clickable items
@rlorenzo
Copy link
Copy Markdown
Contributor Author

rlorenzo commented Apr 1, 2026

@bsedwards Using it a big more, the Rec Pool color is too bright. Planning on using Tahoe color now. Updated the color doc: https://www.dropbox.com/scl/fi/5s57bzsu8jivg6nr2bngr/color-review.html?rlkey=ipmi4hvfdvxxy4vqgm7x39s7l&e=1&st=fco7gbr1&dl=0

Also, the banners in the previous doc did not match the existing banners. So now the before-and-after images better reflect what this branch will change.

rlorenzo added 2 commits April 1, 2026 17:09
- Update stale fallback values in _VIPERLayout.cshtml inline script:
  positive: #226e34 → #266041 (Redwood)
  negative: #6e2222 → #79242F (Merlot)
  info: #289094 → #00B2E3 (Tahoe)
…nd layout accessibility

- Add status toast notifications on quasarTable create/update/delete
  (plain DOM; Quasar Notify silently fails in UMD body-mount setup)
- Add delete confirmation dialog; guard VMACS exports on cancel
- Show external-link icon only for truly external sites in LeftNav
- Add aria-labels, aria-hidden, and sr-only text to layout buttons/links
- Override .text-grey/.bg-grey/.bg-green with !important for AA contrast
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 32 out of 32 changed files in this pull request and generated 5 comments.

rlorenzo added 2 commits April 3, 2026 11:42
The icon prop defaulted to "", which prevented the ?? fallback from
reaching the type-based default icon. Omitting the prop now correctly
shows the default icon (check_circle, error, warning, info).
…d DELETE

- viperFetch returns undefined on failure instead of throwing, so
  resolve(true) ran unconditionally after the DELETE request
- Callers gating on the boolean (e.g. VMACS export pushes) incorrectly
  proceeded as if the delete succeeded
- Changed resolve(true) to resolve(result !== undefined) to match the
  existing notification guard
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 32 out of 32 changed files in this pull request and generated 7 comments.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 32 out of 32 changed files in this pull request and generated 4 comments.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 32 out of 32 changed files in this pull request and generated 1 comment.

- Add rel="noopener noreferrer" to external nav links in LeftNav to
  prevent reverse-tabnabbing (opened page could access window.opener)
- Pass vueApp (not `this`) to quasarTable.load() from create/update/
  delete reload callbacks. Passing the table instance silently swapped
  the Vue app for the table in viperFetch/showViperFetchError, so a
  failed reload after a mutation left the user with a stale table and
  no error dialog.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 32 out of 32 changed files in this pull request and generated 1 comment.

- transitionend does not fire reliably (reduced-motion, CSS overrides,
  detached elements), leaving invisible toasts accumulating in <body>
- Switch to a deterministic setTimeout matching the CSS transition
  duration plus a small buffer
- Matches how Quasar's own Notify plugin handles dismissal
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 32 out of 32 changed files in this pull request and generated 5 comments.

- Mark StatusBanner q-icon as aria-hidden so screen readers skip the
  decorative glyph name before reading the message
- Add rel="noreferrer" to footer external links in both shared layouts
  to match LeftNav and suppress Referer header to external sites
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 32 out of 32 changed files in this pull request and generated 6 comments.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 32 out of 32 changed files in this pull request and generated 2 comments.

rlorenzo added 6 commits April 7, 2026 06:49
- Add "policy" => "Policies" case to MainNav SelectedTopNav switch so
  the Policies header link highlights when visiting /Policy
- Map indentLevel from API to leftNavIndent CSS classes in LeftNav so
  sub-items (e.g. ClinicalScheduler rotation/clinician) render indented
- Suppress focus outline on #main-content in both Vue and Razor styles;
  useRouteFocus sets focus programmatically for screen reader
  announcements only, not as a visual indicator
Red text on dark primary header failed WCAG AA (~3.4:1). Replace
<span class="mainLayoutViperMode"> with <q-badge color="warning"
text-color="dark"> across all Razor and Vue layouts for high contrast.
Make Development/Test badge more visually distinct from the gold header
bar. Use color="negative" (Merlot) with role="presentation" so screen
readers don't announce it as an error — it's purely a visual indicator.
- Fix Quasar clear button opacity for WCAG contrast compliance
- Add tab fade prevention CSS for inactive tab contrast
- Increase q-tree arrow contrast and size
- Use brand colors (negative/warning) in GenericError and SessionTimeout
- Fix case-insensitive LeftNav path matching
The .mainLayoutViperMode class forced text color to pure red (#ff0000),
overriding the white text that q-badge color="negative" provides.
This produced red-on-merlot (2.49:1 contrast) which fails WCAG AA.

Removing the override lets the badge use its default white text on the
merlot background, matching the standard error/negative badge style.
- Wrap q-list in <nav> with aria-label from menu header
- Change q-list role from presentation to list so listitem children
  have the required parent role
- Remove role="none" override from interactive q-items (it conflicted
  with tabindex); fall back to Quasar defaults

Note: Quasar q-item hardcodes role="listitem" on clickable anchors,
which still triggers an axe aria-allowed-role warning. That's a
framework issue to resolve upstream.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 36 out of 36 changed files in this pull request and generated 3 comments.

<q-item
v-if="menuItem.routeTo != null"
:clickable="menuItem.clickable"
:v-ripple="menuItem.clickable"
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

In this q-item branch, :v-ripple is being used like a bound prop/attribute, but elsewhere in the repo v-ripple is consistently applied as a directive (e.g., the external-link branch a few lines below). As written, ripple may not activate for internal route items. Switch to v-ripple (optionally v-ripple="menuItem.clickable") so the ripple behavior works consistently.

Suggested change
:v-ripple="menuItem.clickable"
v-ripple="menuItem.clickable"

Copilot uses AI. Check for mistakes.
Comment on lines 108 to +114
<span class="mainLayoutViper">VIPER 2.0</span>
<span
<q-badge
v-if="environment == 'DEVELOPMENT'"
color="warning"
text-color="dark"
class="mainLayoutViperMode"
>Development</span
>Development</q-badge
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

In this layout header, the environment badges use color="warning" + text-color="dark", while other layouts in this PR (and even the hidden placeholder header above) use color="negative". This looks unintentional and creates inconsistent environment styling/contrast behavior between layouts; consider standardizing the badge color (or documenting why this layout differs).

Copilot uses AI. Check for mistakes.
Comment on lines 205 to 209
<a
href="http://www.vetmed.ucdavis.edu/"
href="https://www.vetmed.ucdavis.edu/"
target="_blank"
rel="noopener"
class="text-primary"
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

These footer links open in a new tab (target="_blank") but currently lack the same external-link indicator used elsewhere in this PR (sr-only “opens in new window” text + decorative icon marked aria-hidden). Also, the PR description mentions rel="noopener noreferrer", but this layout still uses only rel="noopener". Consider aligning this footer with the other shared layouts for consistent a11y + security messaging.

Copilot uses AI. Check for mistakes.
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.

4 participants