VPR-104 fix(a11y): establish shared accessibility foundation (PR 1 of 6)#130
VPR-104 fix(a11y): establish shared accessibility foundation (PR 1 of 6)#130
Conversation
Codecov Report❌ Patch coverage is 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
Bundle ReportChanges will increase total bundle size by 2.8kB (0.12%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: viper-frontend-esmAssets Changed:
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
|
There was a problem hiding this comment.
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 SPAApp.vuefiles. - Normalizes heading hierarchy (e.g., error/permission pages) and adds shared
h1styling for consistent presentation. - Improves accessibility metadata (image
alttext) 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
3351609 to
2e1df3d
Compare
…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)
…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
|
@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. 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). |
|
@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
|
@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. |
- 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
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
- 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.
- 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
- 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
- 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.
| <q-item | ||
| v-if="menuItem.routeTo != null" | ||
| :clickable="menuItem.clickable" | ||
| :v-ripple="menuItem.clickable" |
There was a problem hiding this comment.
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.
| :v-ripple="menuItem.clickable" | |
| v-ripple="menuItem.clickable" |
| <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 |
There was a problem hiding this comment.
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).
| <a | ||
| href="http://www.vetmed.ucdavis.edu/" | ||
| href="https://www.vetmed.ucdavis.edu/" | ||
| target="_blank" | ||
| rel="noopener" | ||
| class="text-primary" |
There was a problem hiding this comment.
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.
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
<main>landmark per page: Razor layouts get<main>directly; Vue SPAs get it fromViperLayout.vue/ViperLayoutSimple.vue(1.3.1 Info and Relationships)tabindex="-1"on the<main>target so focus reliably moves for keyboard/screen reader users (2.4.1 Bypass Blocks)useRouteFocuscomposable announces SPA page changes to screen readers (2.4.3 Focus Order)<nav>witharia-labelfrom menu header, change q-list role frompresentationtolistso listitem children have the required parent role, removerole="none"override from interactive q-items (conflicted with tabindex)Color Contrast (AA)
.text-grey,.bg-grey,.bg-greenoverridden with!importantto meet AA (Quasar defaults fail)color: redoverride on.mainLayoutViperModethat produced red-on-merlot (2.49:1) — badge now uses the default white-on-merlot (~7.2:1).q-field__focusable-actionso WCAG contrast holds.tabs-no-fadeclass to opt out of Quasar's opacity reduction on inactive tabscolor="red"/color="orange"with brandcolor="negative"/color="warning"on iconsScreen Reader & Keyboard
<h1>on error/permissions pages, shared heading styles) (1.3.1)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-labelon icon-only buttons (mobile menu, sidebar toggle, help link) (4.1.2 Name, Role, Value)aria-hiddenon decorative icons in footer linksv-overflow-titlesetstitleattribute only when nav text is truncated; updates on resize/drawer toggle viaResizeObserverInteractive Feedback
role="status"— Quasar Notify does not render in UMD body-mount setup) (4.1.3 Status Messages)Components
StatusBanner.vue: accessible tinted-background banner (success/error/warning/info) with colored left border, dark text on light background, dismissible option, and action slotConfig
Bug Fixes (discovered during review)
create/update/deletewere passing the table instance (this.load(this)) toload()as thevueAppargument, which then went intoviperFetchandshowViperFetchError. 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.StringComparison.Ordinal→StringComparison.OrdinalIgnoreCaseinDefault.cshtmlso menu highlight survives URL case differences