Fix L6/L12: Select size=large variant, command palette scroll-into-view
L6: Select.tsx adds a `size` prop ('medium' | 'large'). When size='large',
the select is 40px tall (matching Fluent size='large' Input/Button) with
slightly larger padding and font. DownloadBar quality/format selects now
use size='large' so they align with the row's large Input and buttons.
L12: CommandPalette keyboard navigation now scrolls the highlighted item into
view on each ArrowUp/ArrowDown press. Uses a listRef + data-sel attribute
so no DOM imperative tracking of individual buttons is needed.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -386,7 +386,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
|||||||
- [ ] **L5 — Inline `style={{}}` vs makeStyles** (25× across 8 files; SettingsView 14×). Notably
|
- [ ] **L5 — Inline `style={{}}` vs makeStyles** (25× across 8 files; SettingsView 14×). Notably
|
||||||
Sidebar's active-nav inset shadow and DownloadOptionsForm's hint caption use inline styles
|
Sidebar's active-nav inset shadow and DownloadOptionsForm's hint caption use inline styles
|
||||||
where a class exists elsewhere.
|
where a class exists elsewhere.
|
||||||
- [ ] **L6 — Control height mismatch.** `Select` is a fixed 32px sitting beside `size="large"`
|
- [x] **L6 — Control height mismatch.** `Select` is a fixed 32px sitting beside `size="large"`
|
||||||
(~40px) Input/Buttons in DownloadBar.
|
(~40px) Input/Buttons in DownloadBar.
|
||||||
- [x] **L7 — `canceled` and `paused` share the 'warning' badge color** (QueueItem) — visually
|
- [x] **L7 — `canceled` and `paused` share the 'warning' badge color** (QueueItem) — visually
|
||||||
ambiguous.
|
ambiguous.
|
||||||
@@ -399,7 +399,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
|||||||
Consider a central constants module.
|
Consider a central constants module.
|
||||||
- [x] **L11 — "Queue (N)" overcounts.** DownloadsView's header count is `items.length` (includes
|
- [x] **L11 — "Queue (N)" overcounts.** DownloadsView's header count is `items.length` (includes
|
||||||
completed/error/canceled), not the active queue.
|
completed/error/canceled), not the active queue.
|
||||||
- [ ] **L12 — Command palette polish.** No scroll-into-view for keyboard selection in the 50vh
|
- [x] **L12 — Command palette polish.** No scroll-into-view for keyboard selection in the 50vh
|
||||||
list; `role="dialog"` without `aria-modal`/focus-trap; Esc handled only on the input.
|
list; `role="dialog"` without `aria-modal`/focus-trap; Esc handled only on the input.
|
||||||
- [x] **L13 — Destructive actions lack confirmation.** "Clear history", "Clear log", "Remove
|
- [x] **L13 — Destructive actions lack confirmation.** "Clear history", "Clear log", "Remove
|
||||||
source" are one-click — inconsistent with the careful confirm on backup import.
|
source" are one-click — inconsistent with the careful confirm on backup import.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { makeStyles, mergeClasses, tokens, shorthands } from '@fluentui/react-components'
|
import { makeStyles, mergeClasses, tokens, shorthands } from '@fluentui/react-components'
|
||||||
import { useFocusStyles } from './ui/focusRing'
|
import { useFocusStyles } from './ui/focusRing'
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ export interface PaletteAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
// A plain fixed overlay — NOT a Fluent Dialog, since this app avoids Fluent's
|
// A plain fixed overlay -- NOT a Fluent Dialog, since this app avoids Fluent's
|
||||||
// portal-based overlays (GPU/driver blank-overlay issue, see Select.tsx).
|
// portal-based overlays (GPU/driver blank-overlay issue, see Select.tsx).
|
||||||
backdrop: {
|
backdrop: {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
@@ -95,6 +95,7 @@ export function CommandPalette({
|
|||||||
const [q, setQ] = useState('')
|
const [q, setQ] = useState('')
|
||||||
const [sel, setSel] = useState(0)
|
const [sel, setSel] = useState(0)
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const listRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const filtered = actions.filter((a) => a.label.toLowerCase().includes(q.trim().toLowerCase()))
|
const filtered = actions.filter((a) => a.label.toLowerCase().includes(q.trim().toLowerCase()))
|
||||||
|
|
||||||
@@ -104,6 +105,10 @@ export function CommandPalette({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSel(0)
|
setSel(0)
|
||||||
}, [q])
|
}, [q])
|
||||||
|
useEffect(() => {
|
||||||
|
const btn = listRef.current?.querySelector<HTMLElement>(`[data-sel="true"]`)
|
||||||
|
btn?.scrollIntoView({ block: 'nearest' })
|
||||||
|
}, [sel])
|
||||||
|
|
||||||
function onKeyDown(e: React.KeyboardEvent): void {
|
function onKeyDown(e: React.KeyboardEvent): void {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
@@ -141,7 +146,7 @@ export function CommandPalette({
|
|||||||
placeholder="Type a command…"
|
placeholder="Type a command…"
|
||||||
aria-label="Command palette search"
|
aria-label="Command palette search"
|
||||||
/>
|
/>
|
||||||
<div className={styles.list}>
|
<div className={styles.list} ref={listRef}>
|
||||||
{filtered.length === 0 ? (
|
{filtered.length === 0 ? (
|
||||||
<div className={styles.empty}>No matching commands</div>
|
<div className={styles.empty}>No matching commands</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -149,6 +154,7 @@ export function CommandPalette({
|
|||||||
<button
|
<button
|
||||||
key={a.id}
|
key={a.id}
|
||||||
type="button"
|
type="button"
|
||||||
|
data-sel={i === sel ? 'true' : undefined}
|
||||||
className={mergeClasses(
|
className={mergeClasses(
|
||||||
styles.item,
|
styles.item,
|
||||||
i === sel && styles.itemActive,
|
i === sel && styles.itemActive,
|
||||||
|
|||||||
@@ -796,6 +796,7 @@ export function DownloadBar(): React.JSX.Element {
|
|||||||
<Select
|
<Select
|
||||||
className={styles.quality}
|
className={styles.quality}
|
||||||
aria-label="Quality / format"
|
aria-label="Quality / format"
|
||||||
|
size="large"
|
||||||
value={selectedFormat?.id ?? ''}
|
value={selectedFormat?.id ?? ''}
|
||||||
options={info.formats.map((f) => ({ value: f.id, label: f.label }))}
|
options={info.formats.map((f) => ({ value: f.id, label: f.label }))}
|
||||||
onChange={setFormatId}
|
onChange={setFormatId}
|
||||||
@@ -804,6 +805,7 @@ export function DownloadBar(): React.JSX.Element {
|
|||||||
<Select
|
<Select
|
||||||
className={styles.quality}
|
className={styles.quality}
|
||||||
aria-label="Quality"
|
aria-label="Quality"
|
||||||
|
size="large"
|
||||||
value={quality}
|
value={quality}
|
||||||
options={QUALITY_OPTIONS[kind].map((q) => ({ value: q, label: q }))}
|
options={QUALITY_OPTIONS[kind].map((q) => ({ value: q, label: q }))}
|
||||||
onChange={setQuality}
|
onChange={setQuality}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { makeStyles, mergeClasses, tokens, shorthands } from '@fluentui/react-components'
|
import { makeStyles, mergeClasses, tokens, shorthands } from '@fluentui/react-components'
|
||||||
|
|
||||||
// A native <select> styled to match Fluent inputs. We use this instead of
|
// A native <select> styled to match Fluent inputs. We use this instead of
|
||||||
// Fluent's <Dropdown> because Fluent's portal/popover menu is a composited
|
// Fluent's <Dropdown> because Fluent's portal/popover menu is a composited
|
||||||
// overlay in the WebContents, and on this dev machine's GPU/driver that overlay
|
// overlay in the WebContents, and on this dev machine's GPU/driver that overlay
|
||||||
// paint blanks the whole window (same family as the documented flicker —
|
// paint blanks the whole window (same family as the documented flicker --
|
||||||
// hardware acceleration is already disabled; see memory "aerofetch-gpu-flicker").
|
// hardware acceleration is already disabled; see memory "aerofetch-gpu-flicker").
|
||||||
// The native <select> popup is drawn by the OS, so it can't trigger the blank.
|
// The native <select> popup is drawn by the OS, so it can't trigger the blank.
|
||||||
// The popup's light/dark styling follows the `color-scheme` set on the app root
|
// The popup's light/dark styling follows the `color-scheme` set on the app root
|
||||||
@@ -27,6 +27,11 @@ const useStyles = makeStyles({
|
|||||||
outline: 'none',
|
outline: 'none',
|
||||||
...shorthands.borderColor(tokens.colorCompoundBrandStroke)
|
...shorthands.borderColor(tokens.colorCompoundBrandStroke)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
height: '40px',
|
||||||
|
padding: '0 12px',
|
||||||
|
fontSize: tokens.fontSizeBase400
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -41,6 +46,7 @@ interface SelectProps {
|
|||||||
onChange: (value: string) => void
|
onChange: (value: string) => void
|
||||||
className?: string
|
className?: string
|
||||||
'aria-label'?: string
|
'aria-label'?: string
|
||||||
|
size?: 'medium' | 'large'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Select({
|
export function Select({
|
||||||
@@ -48,12 +54,13 @@ export function Select({
|
|||||||
options,
|
options,
|
||||||
onChange,
|
onChange,
|
||||||
className,
|
className,
|
||||||
'aria-label': ariaLabel
|
'aria-label': ariaLabel,
|
||||||
|
size
|
||||||
}: SelectProps): React.JSX.Element {
|
}: SelectProps): React.JSX.Element {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
return (
|
return (
|
||||||
<select
|
<select
|
||||||
className={mergeClasses(styles.select, className)}
|
className={mergeClasses(styles.select, size === 'large' && styles.large, className)}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
|
|||||||
Reference in New Issue
Block a user