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:
2026-07-01 08:11:30 -04:00
parent 4aec32e3c2
commit 29ce270d46
7 changed files with 77 additions and 12 deletions
+6 -1
View File
@@ -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',
+7 -1
View File
@@ -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'
}
}
})
+19 -2
View File
@@ -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: {