Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions VueApp/src/CAHFS/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from "vue-router"
import { routes } from "./routes"
import { useRequireLogin } from "@/composables/RequireLogin"
import { checkHasOnePermission } from "@/composables/CheckPagePermission"
import { useRouteFocus } from "@/composables/use-route-focus"

const baseUrl = import.meta.env.VITE_VIPER_HOME
const router = createRouter({
Expand All @@ -24,4 +25,6 @@ router.beforeEach(async (to) => {
}
})

useRouteFocus(router)

export { router as CAHFSRouter }
2 changes: 1 addition & 1 deletion VueApp/src/CMS/components/Link.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const props = defineProps<{
}>()

function getLinkCollectionTagColor(order: number) {
const colors = ["orange", "grey", "purple", "green", "blue"]
const colors = ["warning", "secondary", "negative", "positive", "info"]
return colors.length >= order ? colors[order - 1] : colors[colors.length - 1]
}

Expand Down
12 changes: 6 additions & 6 deletions VueApp/src/CMS/pages/ManageLinkCollections.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</div>
<div class="col-auto">
<q-btn
color="green"
color="positive"
icon="add"
label="New Collection"
no-caps
Expand Down Expand Up @@ -80,7 +80,7 @@
label="Add"
dense
no-caps
color="green"
color="positive"
class="q-ml-lg q-pr-md"
icon="add"
@click="createTag"
Expand Down Expand Up @@ -116,7 +116,7 @@
label="Delete"
dense
no-caps
color="red-5"
color="negative"
class="q-ml-lg q-pr-md"
icon="delete"
@click="deleteTag(element.linkCollectionTagCategoryId)"
Expand Down Expand Up @@ -150,7 +150,7 @@
@click="deleteCollection"
dense
no-caps
color="red-5"
color="negative"
class="q-pr-md"
/>
</q-card-actions>
Expand All @@ -161,7 +161,7 @@
<h2 class="text-primary q-mt-lg q-mb-sm">Links in {{ collection?.linkCollection }}</h2>
<q-btn
dense
color="green"
color="positive"
icon="add"
label="Add Link"
@click="showLinkDialog = true"
Expand Down Expand Up @@ -235,7 +235,7 @@
@click="deleteLink"
dense
no-caps
color="red-5"
color="negative"
class="q-pr-md"
/>
</q-card-actions>
Expand Down
3 changes: 3 additions & 0 deletions VueApp/src/CMS/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from "vue-router"
import { routes } from "./routes"
import { useRequireLogin } from "@/composables/RequireLogin"
import { checkHasOnePermission } from "@/composables/CheckPagePermission"
import { useRouteFocus } from "@/composables/use-route-focus"

const baseUrl = import.meta.env.VITE_VIPER_HOME
const router = createRouter({
Expand All @@ -24,4 +25,6 @@ router.beforeEach(async (to) => {
}
})

useRouteFocus(router)

export { router as cmsRouter }
3 changes: 1 addition & 2 deletions VueApp/src/ClinicalScheduler/components/RecentSelections.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,8 @@
<q-chip
v-for="item in items"
:key="getItemKey(item)"
:color="isItemSelected(item) ? 'primary' : undefined"
:color="isItemSelected(item) ? 'primary' : 'grey-4'"
:text-color="isItemSelected(item) ? 'white' : 'dark'"
:outline="!isItemSelected(item)"
clickable
size="sm"
class="q-mr-xs"
Expand Down
6 changes: 5 additions & 1 deletion VueApp/src/ClinicalScheduler/components/ScheduleBanner.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
<q-banner
v-if="shouldShow"
:class="bannerClass"
:role="type === 'error' ? 'alert' : 'status'"
rounded
>
<template #avatar>
<q-icon :name="iconName" />
<q-icon
:name="iconName"
aria-hidden="true"
/>
</template>
<div class="text-body2">
<strong v-if="title">{{ title }}</strong>
Expand Down
14 changes: 10 additions & 4 deletions VueApp/src/ClinicalScheduler/components/ScheduleLegend.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
</div>
<div
class="q-mb-xs q-mt-xs text-caption"
style="color: var(--ucdavis-poppy)"
style="color: var(--ucdavis-black-80)"
>
<strong>Tip:</strong> When selecting weeks that already contain your selected
{{ itemType }}(s), the button changes to "Delete Selected" for bulk removal
Expand Down Expand Up @@ -77,8 +77,8 @@
<q-icon
name="star"
size="xs"
color="amber"
class="legend-icon"
color="warning"
class="legend-icon legend-star-outlined"
aria-hidden="true"
/>
<span class="legend-text">Current primary evaluator</span>
Expand Down Expand Up @@ -149,13 +149,19 @@ const isMac = computed(() => {
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--ucdavis-black-50);
color: var(--ucdavis-black-80);
}

.legend-icon {
flex-shrink: 0;
}

/* Dark outline on gold star icon for WCAG contrast on white backgrounds */
.legend-star-outlined {
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.

Same cross-browser concern here: -webkit-text-stroke won’t apply in Firefox for font icons, so the star may still fail contrast there. Consider a more broadly supported outlining technique (e.g., text-shadow) or a different icon treatment that maintains contrast across browsers.

Suggested change
.legend-star-outlined {
.legend-star-outlined {
/* Cross-browser fallback for icon/text outlining, including Firefox */
text-shadow:
-1px 0 0 var(--ucdavis-black-80),
1px 0 0 var(--ucdavis-black-80),
0 -1px 0 var(--ucdavis-black-80),
0 1px 0 var(--ucdavis-black-80),
-1px -1px 0 var(--ucdavis-black-80),
1px -1px 0 var(--ucdavis-black-80),
-1px 1px 0 var(--ucdavis-black-80),
1px 1px 0 var(--ucdavis-black-80);

Copilot uses AI. Check for mistakes.
-webkit-text-stroke: 1px var(--ucdavis-black-80);
paint-order: stroke fill;
}

.legend-text {
line-height: 1.4;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<!-- Navigation tabs with proper active state for parameterized routes -->
<q-tabs
class="text-grey q-mb-md"
class="text-grey-8 tabs-no-fade q-mb-md"
active-color="primary"
indicator-color="primary"
align="left"
Expand All @@ -14,7 +14,6 @@
label="Home"
to="/ClinicalScheduler/"
:exact="true"
:aria-controls="`home-panel`"
:id="`home-tab`"
role="tab"
/>
Expand All @@ -24,7 +23,6 @@
label="Schedule by Rotation"
:to="{ name: 'RotationSchedule' }"
:class="rotationTabClass"
:aria-controls="`rotation-panel`"
:id="`rotation-tab`"
role="tab"
/>
Expand All @@ -34,7 +32,6 @@
:label="permissionsStore.clinicianViewLabel"
:to="{ name: 'ClinicianSchedule' }"
:class="clinicianTabClass"
:aria-controls="`clinician-panel`"
:id="`clinician-tab`"
role="tab"
/>
Expand Down
25 changes: 20 additions & 5 deletions VueApp/src/ClinicalScheduler/components/WeekCell.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
<q-card
:class="cardClasses"
clickable
tabindex="0"
role="group"
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.

The card is interactive (click + keyboard handlers) but is given role="group". This role doesn’t convey that the element is actionable to assistive tech and also overrides Quasar’s default semantics for clickable. Consider removing the explicit role or switching it to role="button" (and keeping the aria-label) so the announced role matches the behavior.

Suggested change
role="group"
role="button"

Copilot uses AI. Check for mistakes.
:aria-label="`Week ${week.weekNumber} starting ${formatDate(week.dateStart)}, ${assignments.length} ${inflect('assignment', assignments.length)}`"
@click="handleClick"
@keyup.enter.self.prevent="handleClick"
@keyup.space.self.prevent="handleClick"
Comment on lines +9 to +10
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.

Keyboard activation is handled on keyup, including Space. For non-native buttons, Space’s default page scroll typically occurs on keydown, so preventing only keyup may not stop scrolling and can make activation unreliable. Prefer handling Enter/Space on keydown (and preventDefault there) to align with ARIA button behavior.

Suggested change
@keyup.enter.self.prevent="handleClick"
@keyup.space.self.prevent="handleClick"
@keydown.enter.self.prevent="handleClick"
@keydown.space.self.prevent="handleClick"

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +10
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.

WeekCell’s new keyboard interaction (Enter/Space activation on the card) is important a11y behavior but isn’t covered by tests. Given the existing Vitest suite under VueApp/src/ClinicalScheduler/__tests__, consider adding a component test that verifies keyboard activation triggers the same emit/click behavior and that Space does not scroll the page (preventDefault).

Copilot uses AI. Check for mistakes.
@touchstart="handleTouchStart"
@touchend="handleTouchEnd"
@touchmove="handleTouchMove"
Expand Down Expand Up @@ -94,8 +99,8 @@
dense
icon="star"
size="sm"
color="amber"
class="week-cell__primary-btn"
color="warning"
class="week-cell__primary-btn week-cell__star-outlined"
aria-label="Primary evaluator"
:disable="true"
>
Expand All @@ -108,8 +113,11 @@
dense
:icon="assignment.isPrimary ? 'star' : 'star_outline'"
size="sm"
:color="assignment.isPrimary ? 'amber' : 'grey-5'"
class="week-cell__primary-btn"
:color="assignment.isPrimary ? 'warning' : 'grey-8'"
:class="[
'week-cell__primary-btn',
assignment.isPrimary ? 'week-cell__star-outlined' : '',
]"
:aria-label="
assignment.isPrimary
? 'Primary evaluator. Click another clinician\'s star to transfer primary status.'
Expand Down Expand Up @@ -151,6 +159,7 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue"
import { useTimeoutFn } from "@vueuse/core"
import { inflect } from "inflection"
import { useDateFunctions } from "@/composables/DateFunctions"
import { ANIMATIONS } from "../constants/app-constants"

Expand Down Expand Up @@ -450,6 +459,12 @@ const cardClasses = computed(() => {
flex-shrink: 0;
}

/* Dark outline on gold star icons for WCAG contrast on white backgrounds */
.week-cell__star-outlined :deep(.q-icon) {
-webkit-text-stroke: 1px var(--ucdavis-black-80);
paint-order: stroke fill;
Comment on lines +464 to +465
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.

-webkit-text-stroke is non-standard and isn’t supported in Firefox for font-based icons, so the contrast ‘outline’ may not appear for a portion of users. Consider a more cross-browser approach (e.g., text-shadow outline, swapping to an outlined SVG/icon variant, or adding a contrasting background) so the WCAG contrast improvement applies consistently.

Suggested change
-webkit-text-stroke: 1px var(--ucdavis-black-80);
paint-order: stroke fill;
text-shadow:
-1px 0 0 var(--ucdavis-black-80),
1px 0 0 var(--ucdavis-black-80),
0 -1px 0 var(--ucdavis-black-80),
0 1px 0 var(--ucdavis-black-80),
-1px -1px 0 var(--ucdavis-black-80),
1px -1px 0 var(--ucdavis-black-80),
-1px 1px 0 var(--ucdavis-black-80),
1px 1px 0 var(--ucdavis-black-80);

Copilot uses AI. Check for mistakes.
}

.week-cell__empty {
display: flex;
align-items: center;
Expand All @@ -459,7 +474,7 @@ const cardClasses = computed(() => {

.week-cell__empty-text {
font-size: 12px;
color: var(--ucdavis-black-40);
color: var(--ucdavis-black-60);
font-style: italic;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
:week="week"
>
<div class="text-center q-py-sm">
<div class="text-grey-6 text-caption">
<div class="text-grey-7 text-caption">
{{ !isPastYear ? "Click to add assignment" : "No assignments" }}
</div>
</div>
Expand Down
6 changes: 4 additions & 2 deletions VueApp/src/ClinicalScheduler/pages/ClinicalSchedulerHome.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
class="opacity-70"
/>
</div>
<p class="text-grey-7 q-mb-sm">Schedule clinicians for a specific rotation</p>
<p class="text-grey-8 q-mb-sm">Schedule clinicians for a specific rotation</p>
</q-card-section>
</q-card>
</div>
Expand Down Expand Up @@ -107,17 +107,19 @@
class="opacity-70"
/>
</div>
<p class="text-grey-7 q-mb-sm">Schedule rotations for a specific clinician</p>
<p class="text-grey-8 q-mb-sm">Schedule rotations for a specific clinician</p>
<q-banner
v-if="permissionsStore.hasOnlyOwnSchedulePermission"
dense
inline-actions
class="text-primary bg-primary-1 rounded-borders"
role="status"
>
<template #avatar>
<q-icon
name="info"
size="14px"
aria-hidden="true"
/>
</template>
<span class="text-caption">Your schedule only</span>
Expand Down
8 changes: 5 additions & 3 deletions VueApp/src/ClinicalScheduler/pages/ClinicianScheduleView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,12 @@
/>

<!-- No clinician selected -->
<ScheduleBanner
<StatusBanner
v-if="!selectedClinician && !loadingSchedule"
type="info"
:custom-message="SCHEDULE_MESSAGES.SELECTION.NO_CLINICIAN"
/>
>
{{ SCHEDULE_MESSAGES.SELECTION.NO_CLINICIAN }}
</StatusBanner>

<!-- Schedule display -->
<div v-if="selectedClinician">
Expand Down Expand Up @@ -169,6 +170,7 @@ import { useScheduleNormalization } from "../composables/use-schedule-normalizat
import { useDeleteMode } from "../composables/use-delete-mode"
import type { RotationWithService } from "../types/rotation-types"
import ScheduleBanner from "../components/ScheduleBanner.vue"
import StatusBanner from "@/components/StatusBanner.vue"
import RecentSelections from "../components/RecentSelections.vue"
import AccessDeniedCard from "../components/AccessDeniedCard.vue"
import {
Expand Down
19 changes: 12 additions & 7 deletions VueApp/src/ClinicalScheduler/pages/RotationScheduleView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,22 @@
/>

<!-- No rotation selected message -->
<ScheduleBanner
<StatusBanner
v-else-if="!selectedRotation"
type="info"
custom-message="Please select a rotation to view its schedule."
/>
>
Please select a rotation to view its schedule.
</StatusBanner>

<!-- Instructions (only show when rotation is selected and not past year) -->
<ScheduleBanner
<StatusBanner
v-else-if="selectedRotation && !isPastYear"
type="instructions"
custom-message="This list of clinicians should contain any clinician scheduled for the rotation in the current or previous year. The user can click on a clinician to select them, and then click on any week to schedule them."
/>
type="info"
>
This list of clinicians should contain any clinician scheduled for the rotation in the current or
previous year. The user can click on a clinician to select them, and then click on any week to schedule
them.
</StatusBanner>

<!-- Clinician selector section (only show when rotation is selected and not past year) -->
<RecentSelections
Expand Down Expand Up @@ -214,6 +218,7 @@ import YearSelector from "../components/YearSelector.vue"
import SchedulerNavigation from "../components/SchedulerNavigation.vue"
import type { WeekItem } from "../components/WeekScheduleCard.vue"
import ScheduleBanner from "../components/ScheduleBanner.vue"
import StatusBanner from "@/components/StatusBanner.vue"
import RecentSelections from "../components/RecentSelections.vue"
import AccessDeniedCard from "../components/AccessDeniedCard.vue"
import { ACCESS_DENIED_MESSAGES, ACCESS_DENIED_SUBTITLES } from "../constants/permission-messages"
Expand Down
3 changes: 3 additions & 0 deletions VueApp/src/ClinicalScheduler/router/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from "vue-router"
import { clinicalSchedulerRoutes as routes } from "./routes"
import { useRequireLogin } from "@/composables/RequireLogin"
import { useRouteFocus } from "@/composables/use-route-focus"

const baseUrl = import.meta.env.VITE_VIPER_HOME
const router = createRouter({
Expand All @@ -14,4 +15,6 @@ router.beforeEach((to) => {
return requireLogin(true, "SVMSecure.ClnSched")
})

useRouteFocus(router)

export { router as clinicalSchedulerRouter }
3 changes: 3 additions & 0 deletions VueApp/src/Computing/router/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from "vue-router"
import { routes } from "./routes"
import { useRequireLogin } from "@/composables/RequireLogin"
import { useRouteFocus } from "@/composables/use-route-focus"

const baseUrl = import.meta.env.VITE_VIPER_HOME
const router = createRouter({
Expand All @@ -14,4 +15,6 @@ router.beforeEach((to) => {
return requireLogin()
})

useRouteFocus(router)

export { router }
5 changes: 4 additions & 1 deletion web/Areas/Computing/Views/Index.cshtml
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
@await Component.InvokeAsync("CMSBlocks", new {friendlyName = "computing-home"})
@{
ViewData["Title"] = "Computing";
}
@await Component.InvokeAsync("CMSBlocks", new {friendlyName = "computing-home"})