a11y: Select disabled (UI31), native colorScheme (UI32), touch targets (W16), High Contrast (W18)
UI31 — the native <Select> wrapper now takes a `disabled` prop that passes to the <select> and applies disabled foreground/background/stroke tokens + not-allowed cursor, so it can show a disabled state like Fluent controls. UI32 — dropped the datetime-local's explicit colorScheme:'light dark' so it inherits the resolved in-app scheme from the app root; the native calendar popup now follows the app's Light/Dark theme, consistent with the native <Select>, instead of tracking the OS preference. W16 — the icon-only row-action clusters (QueueItem + HistoryView) enforce a >=40x40px hit target via a `& button` min-size rule (glyph unchanged, subtle button just gains padding). Labels were already non-hover-only: the shared Hint shows on :focus-within and every action has an aria-label. LibraryView item rows have no icon-only actions, so nothing there needed changing. W18 — defended the two color-meaning surfaces against forced-colors: accent swatches set forced-color-adjust:none (they're color previews; otherwise all flatten to one system color), and the SegmentedControl active segment paints with system Highlight/HighlightText under @media (forced-colors: active) so the checked state stays visible. Status chips (Fluent Badge) and thumbnail tints correctly let the system palette win. typecheck + 253 tests + eslint + prettier + production build all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+27
-5
@@ -971,10 +971,17 @@ are an undefined spacing/radius/type scale and a drift between Fluent components
|
||||
- [x] **UI30 — Hand-rolled radiogroups aren't arrow-navigable.** DownloadBar kind and Sidebar theme
|
||||
use `role="radiogroup"`/`radio` but implement click only — no ←/→ roving focus a radiogroup implies.
|
||||
**Standard:** roving-tabindex arrow keys, or Fluent's RadioGroup. *(a11y)*
|
||||
- [ ] **UI31 — `Select` can't be disabled.** The native-`<select>` wrapper exposes no `disabled` prop,
|
||||
- [x] **UI31 — `Select` can't be disabled.** The native-`<select>` wrapper exposes no `disabled` prop,
|
||||
so that control can't show a disabled state while Fluent controls can. **Standard:** add `disabled` + styling.
|
||||
- [ ] **UI32 — Native-control dark mode is uneven.** The DownloadBar `datetime-local` sets
|
||||
*Fixed in [Select.tsx](src/renderer/src/components/Select.tsx): a `disabled` prop passes through to the
|
||||
native `<select>` and applies a disabled style (disabled foreground/background/stroke tokens + `not-allowed`
|
||||
cursor, hover suppressed) so it reads like a disabled Fluent control.*
|
||||
- [x] **UI32 — Native-control dark mode is uneven.** The DownloadBar `datetime-local` sets
|
||||
`colorScheme: 'light dark'` explicitly; the `Select` relies on the root `colorScheme`. **Standard:** set `colorScheme` consistently on both.
|
||||
*Fixed in [downloadBar/styles.ts](src/renderer/src/components/downloadBar/styles.ts): dropped the explicit
|
||||
`colorScheme: 'light dark'` on the `datetime-local` so it inherits the resolved in-app scheme set on the app
|
||||
root (App.tsx) — the native calendar popup now follows the app's Light/Dark theme, consistent with the
|
||||
native `<Select>`, rather than tracking the OS preference.*
|
||||
- [x] **UI33 — No semantic headings.** Titles render as `Subtitle2`/`Title2` (styled spans), so there's
|
||||
no h1–h6 hierarchy for screen-reader heading navigation (landmarks `<nav>`/`<main>` exist; headings
|
||||
don't). **Standard:** render titles as real headings (`as="h1"`/`"h2"` or `role="heading" aria-level`). *(a11y)*
|
||||
@@ -1192,17 +1199,32 @@ DPI handled by Chromium). The deviations:
|
||||
- [ ] **W15 — Software rendering is global.** Hardware acceleration is disabled for a documented GPU
|
||||
workaround; on high-DPI/large windows this trades GPU compositing for CPU, which can be sluggish.
|
||||
**Standard:** re-enable HW accel where the target GPU allows (the memo already flags revisiting this).
|
||||
- [ ] **W16 — Touch targets too small / hover-only labels.** Icon-only row actions are ~32px, 4px apart,
|
||||
- [x] **W16 — Touch targets too small / hover-only labels.** Icon-only row actions are ~32px, 4px apart,
|
||||
4 per row (below the ~40px touch guideline), and their only labels are hover tooltips (`Hint`) that
|
||||
never appear on touch. **Standard:** ≥40px touch targets and non-hover labels. *(extends UX17)*
|
||||
never appear on touch. **Standard:** ≥40px touch targets and non-hover labels. *(extends UX17)* *Fixed:
|
||||
the icon-only row-action clusters (QueueItem + HistoryView `actions` containers) now enforce a ≥40×40px
|
||||
hit target via a `& button` min-size rule — the glyph is unchanged, the subtle button just carries more
|
||||
padding. The label half was already covered: the shared [Hint](src/renderer/src/components/Hint.tsx) shows
|
||||
on `:focus-within`, not only `:hover`, so keyboard/AT users get the name (and every action also has an
|
||||
`aria-label`). (LibraryView's per-item rows have no icon-only actions — checkbox + status chip only; its
|
||||
header actions are labeled text buttons — so nothing there needed enlarging.)*
|
||||
|
||||
### Dark mode & accessibility (Windows-specific)
|
||||
|
||||
- [x] **W17 — No `aria-live` for status changes.** Narrator doesn't announce download progress,
|
||||
completion, or errors — there are no live regions. **Standard:** polite live regions for queue/status updates.
|
||||
- [ ] **W18 — Custom colors unverified under High Contrast.** The status pills, segmented controls, accent
|
||||
- [x] **W18 — Custom colors unverified under High Contrast.** The status pills, segmented controls, accent
|
||||
swatches, and thumbnail tints use explicit background colors that Windows forced-colors mode may not
|
||||
adapt (nothing sets `forced-color-adjust`). **Standard:** test under each Contrast theme; let system colors win.
|
||||
*Fixed the two surfaces that convey meaning through color and would otherwise break under `forced-colors`:
|
||||
the accent swatches ([settingsStyles.ts](src/renderer/src/components/settings/settingsStyles.ts)) now set
|
||||
`forced-color-adjust: none` so the accents stay distinguishable (they're color previews — otherwise all
|
||||
flatten to one system color), and the [SegmentedControl](src/renderer/src/components/ui/SegmentedControl.tsx)
|
||||
active segment paints with the system `Highlight`/`HighlightText` pair under `@media (forced-colors: active)`
|
||||
so the checked state stays visible once the brand background is flattened. The status chips are Fluent
|
||||
`Badge`s (Fluent supplies its own forced-colors handling) and the thumbnail tints are purely decorative
|
||||
placeholders, so both correctly let the system palette win. Live verification under each Contrast theme is
|
||||
still worthwhile but the color-meaning surfaces are now defended in code.*
|
||||
- [ ] **(ref) No semantic headings / radiogroup arrow-nav / focus rings** — UI33, UI30, UI28–29 (all bear on Narrator + keyboard users).
|
||||
|
||||
### Settings & standard conventions (mostly OK)
|
||||
|
||||
@@ -104,7 +104,12 @@ const useStyles = makeStyles({
|
||||
actions: {
|
||||
display: 'flex',
|
||||
gap: '4px',
|
||||
flexShrink: 0
|
||||
flexShrink: 0,
|
||||
// >=40px hit target for the icon-only row actions (W16).
|
||||
'& button': {
|
||||
minWidth: '40px',
|
||||
minHeight: '40px'
|
||||
}
|
||||
},
|
||||
empty: {
|
||||
display: 'flex',
|
||||
|
||||
@@ -94,7 +94,13 @@ const useStyles = makeStyles({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
flexShrink: 0
|
||||
flexShrink: 0,
|
||||
// Icon-only row actions get a >=40px hit target for touch/pen (W16); the
|
||||
// glyph stays its normal size, the subtle button just carries more padding.
|
||||
'& button': {
|
||||
minWidth: '40px',
|
||||
minHeight: '40px'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -32,6 +32,15 @@ const useStyles = makeStyles({
|
||||
height: '40px',
|
||||
padding: '0 12px',
|
||||
fontSize: tokens.fontSizeBase400
|
||||
},
|
||||
disabled: {
|
||||
cursor: 'not-allowed',
|
||||
color: tokens.colorNeutralForegroundDisabled,
|
||||
backgroundColor: tokens.colorNeutralBackgroundDisabled,
|
||||
...shorthands.borderColor(tokens.colorNeutralStrokeDisabled),
|
||||
':hover': {
|
||||
...shorthands.borderColor(tokens.colorNeutralStrokeDisabled)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -47,6 +56,7 @@ interface SelectProps {
|
||||
className?: string
|
||||
'aria-label'?: string
|
||||
size?: 'medium' | 'large'
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function Select({
|
||||
@@ -55,15 +65,22 @@ export function Select({
|
||||
onChange,
|
||||
className,
|
||||
'aria-label': ariaLabel,
|
||||
size
|
||||
size,
|
||||
disabled
|
||||
}: SelectProps): React.JSX.Element {
|
||||
const styles = useStyles()
|
||||
return (
|
||||
<select
|
||||
className={mergeClasses(styles.select, size === 'large' && styles.large, className)}
|
||||
className={mergeClasses(
|
||||
styles.select,
|
||||
size === 'large' && styles.large,
|
||||
disabled && styles.disabled,
|
||||
className
|
||||
)}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
aria-label={ariaLabel}
|
||||
disabled={disabled}
|
||||
>
|
||||
{options.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
|
||||
@@ -202,6 +202,10 @@ export const useDownloadBarStyles = makeStyles({
|
||||
border: `1px solid ${tokens.colorNeutralStroke2}`
|
||||
},
|
||||
// Native datetime-local input, themed to sit beside the Fluent controls.
|
||||
// No explicit `colorScheme` here (UI32): it inherits the resolved in-app scheme
|
||||
// set on the app root in App.tsx, so the native calendar popup follows the app's
|
||||
// Light/Dark theme — consistent with the native <Select> — rather than the OS
|
||||
// preference (which the former `'light dark'` value tied it to).
|
||||
dtInput: {
|
||||
fontFamily: tokens.fontFamilyBase,
|
||||
fontSize: tokens.fontSizeBase300,
|
||||
@@ -209,8 +213,7 @@ export const useDownloadBarStyles = makeStyles({
|
||||
...shorthands.borderRadius(tokens.borderRadiusMedium),
|
||||
border: `1px solid ${tokens.colorNeutralStroke1}`,
|
||||
backgroundColor: tokens.colorNeutralBackground1,
|
||||
color: tokens.colorNeutralForeground1,
|
||||
colorScheme: 'light dark'
|
||||
color: tokens.colorNeutralForeground1
|
||||
},
|
||||
// --- command preview & options panels ---
|
||||
commandBlock: {
|
||||
|
||||
@@ -54,7 +54,11 @@ export const useSettingsStyles = makeStyles({
|
||||
...shorthands.borderRadius(tokens.borderRadiusCircular),
|
||||
...shorthands.border('2px', 'solid', 'transparent'),
|
||||
padding: 0,
|
||||
cursor: 'pointer'
|
||||
cursor: 'pointer',
|
||||
// The swatch *is* a color preview, so keep the accent color under Windows
|
||||
// High Contrast / forced-colors (W18); otherwise every swatch flattens to one
|
||||
// system color and the accents become indistinguishable.
|
||||
forcedColorAdjust: 'none'
|
||||
},
|
||||
swatchActive: {
|
||||
...shorthands.borderColor(tokens.colorNeutralForeground1)
|
||||
|
||||
@@ -51,6 +51,14 @@ const useStyles = makeStyles({
|
||||
fontWeight: tokens.fontWeightSemibold,
|
||||
':hover': {
|
||||
backgroundColor: tokens.colorBrandBackgroundHover
|
||||
},
|
||||
// Under Windows High Contrast the brand background is flattened to the system
|
||||
// canvas, so the checked segment would be indistinguishable from the others
|
||||
// (W18). Paint it with the system Highlight pair so the selection stays visible.
|
||||
'@media (forced-colors: active)': {
|
||||
backgroundColor: 'Highlight',
|
||||
color: 'HighlightText',
|
||||
forcedColorAdjust: 'none'
|
||||
}
|
||||
},
|
||||
icon: {
|
||||
|
||||
Reference in New Issue
Block a user