mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-12 04:54:45 +00:00
fix(webui): ARIA-valid hover text, alias metadata guard, context-chip polish
CompactNumber.vue and ProviderRateBadge.vue both put aria-label on a
plain <span> — axe's aria-prohibited-attr rule: a <span>'s implicit
ARIA role ("generic") does not support accessible-name computation at
all, so the attribute was simply invalid there, not just discouraged.
Both now use the visible-text-hidden-plus-sr-only-full-text pattern
instead: the compact/label text is marked aria-hidden="true", and a
sr-only sibling span carries the exact value/detail for assistive tech.
title stays on the outer element for mouse hover, unchanged. Folded
review minor.
ProvidersView.vue: the alias table's ModelChip cell read
row.original.modelMeta.contextTokens without a guard, unlike the
provider path's own modelMetaFor fallback — a response from a server
build older than this field (version skew) would throw instead of just
omitting the hover detail. Now optional-chained to match. The visible
context chip beside each model gains title="context window" and
text-xs (was an arbitrary text-[10px]), matching the rest of the panel's
muted-label sizing convention. Folded review minors.
This commit is contained in:
@@ -5,13 +5,22 @@ import { formatCompactCount, formatExactInt } from '@/lib/format'
|
||||
|
||||
/**
|
||||
* CompactNumber renders a large raw token/request count in its compact
|
||||
* SI-style form ("1.23M") while carrying the exact value via both
|
||||
* `title` (mouse hover) and `aria-label` (keyboard/screen-reader parity
|
||||
* — the same established pattern ProviderRateBadge.vue and ModelChip.vue
|
||||
* already apply to their own hover detail). Used anywhere a usage count
|
||||
* renders: DataTable numeric columns (usage-columns.ts, target-
|
||||
* columns.ts, via `h(CompactNumber, { value })`), the Usage tab's Total
|
||||
* card, and a group accordion's trigger/detail stat grid (UsageView.vue).
|
||||
* SI-style form ("1.23M") while carrying the exact value via `title`
|
||||
* (mouse hover) and a visually-hidden sibling span (keyboard/screen-
|
||||
* reader parity). Used anywhere a usage count renders: DataTable numeric
|
||||
* columns (usage-columns.ts, target-columns.ts, via
|
||||
* `h(CompactNumber, { value })`), the Usage tab's Total card, and a
|
||||
* group accordion's trigger/detail stat grid (UsageView.vue).
|
||||
*
|
||||
* Review fix (folded minor, ARIA): `aria-label` on a plain `<span>` is
|
||||
* an axe `aria-prohibited-attr` violation — a `<span>`'s implicit ARIA
|
||||
* role ("generic") does not support name computation at all, so
|
||||
* `aria-label`/`aria-labelledby` there is simply invalid, not merely
|
||||
* discouraged. The visible, compact text is marked `aria-hidden="true"`
|
||||
* so assistive tech skips the abbreviated form, and a `sr-only` sibling
|
||||
* span (visually hidden, still in the accessibility tree) carries the
|
||||
* exact value instead — the same visible-abbreviation-plus-hidden-full-
|
||||
* text pattern ProviderRateBadge.vue now applies too.
|
||||
*
|
||||
* Cost figures are NOT this component's concern — formatCost already
|
||||
* shows full 4-decimal precision, so there is nothing to compact or
|
||||
@@ -24,5 +33,8 @@ const exact = computed(() => formatExactInt(props.value))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :title="exact" :aria-label="exact">{{ compact }}</span>
|
||||
<span :title="exact">
|
||||
<span aria-hidden="true">{{ compact }}</span>
|
||||
<span class="sr-only">{{ exact }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -37,10 +37,16 @@ const label = computed(() => (status.value === 'no-traffic' ? 'no traffic' : for
|
||||
* detail carries the "right now" (provider) or "today" (model) counts —
|
||||
* the spec's minute-window requirement where that data exists at all
|
||||
* (minuteRateTitle), falling back to dayRateTitle when it does not. Used
|
||||
* as BOTH the badge's title (mouse hover) and its aria-label (folded
|
||||
* review minor, v0.22 review round: title alone is mouse-only — a
|
||||
* keyboard or screen-reader user gets none of it without an aria-label
|
||||
* carrying the same sentence).
|
||||
* as BOTH the badge's title (mouse hover) and, via the sr-only span in
|
||||
* the template, its accessible name for keyboard/screen-reader users
|
||||
* (folded review minor, v0.22 review round: title alone is mouse-only).
|
||||
*
|
||||
* Review fix (v0.23 round): this used to sit directly on `:aria-label`
|
||||
* on the Badge/`<span>` element, which axe flags as `aria-prohibited-
|
||||
* attr` — a `<span>`'s implicit role ("generic") does not support name
|
||||
* computation. See CompactNumber.vue's own doc comment for the identical
|
||||
* fix and the shared visible-abbreviation-plus-sr-only-full-text
|
||||
* pattern this component now uses too.
|
||||
*/
|
||||
const detail = computed(() =>
|
||||
props.attemptsMinute === undefined || props.failuresMinute === undefined
|
||||
@@ -68,7 +74,8 @@ const extraClass = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Badge as="span" :variant="variant" :class="extraClass" class="font-normal tabular-nums" :title="detail" :aria-label="detail">
|
||||
{{ label }}
|
||||
<Badge as="span" :variant="variant" :class="extraClass" class="font-normal tabular-nums" :title="detail">
|
||||
<span aria-hidden="true">{{ label }}</span>
|
||||
<span class="sr-only">{{ detail }}</span>
|
||||
</Badge>
|
||||
</template>
|
||||
|
||||
@@ -179,12 +179,18 @@ const aliasColumns: ColumnDef<AdminAliasView, unknown>[] = [
|
||||
id: 'alias',
|
||||
header: 'Alias',
|
||||
accessorFn: (a) => a.alias,
|
||||
// Optional-chained (review fix, folded minor) the same way
|
||||
// modelMetaFor guards the provider path below: AdminAliasView.
|
||||
// modelMeta is typed as always-present, but a real response from a
|
||||
// server build older than this field (version skew) would otherwise
|
||||
// throw on `.modelMeta.contextTokens` here instead of just omitting
|
||||
// the hover detail.
|
||||
cell: ({ row }) =>
|
||||
h(ModelChip, {
|
||||
id: row.original.alias,
|
||||
contextTokens: row.original.modelMeta.contextTokens,
|
||||
inputPerMTokUsd: row.original.modelMeta.inputPerMTokUsd,
|
||||
outputPerMTokUsd: row.original.modelMeta.outputPerMTokUsd,
|
||||
contextTokens: row.original.modelMeta?.contextTokens,
|
||||
inputPerMTokUsd: row.original.modelMeta?.inputPerMTokUsd,
|
||||
outputPerMTokUsd: row.original.modelMeta?.outputPerMTokUsd,
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -331,7 +337,8 @@ const aliasEmptyMessage = computed(() =>
|
||||
-->
|
||||
<span
|
||||
v-if="modelMetaFor(p, m).contextTokens !== undefined"
|
||||
class="text-[10px] text-muted-foreground tabular-nums"
|
||||
title="context window"
|
||||
class="text-xs text-muted-foreground tabular-nums"
|
||||
>
|
||||
{{ formatContextWindow(modelMetaFor(p, m).contextTokens as number) }}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user