fix: resolve all Svelte a11y warnings

- Associate labels with controls in CaptionSettingsPanel (9 warnings)
- Add tabindex and keyboard handlers to dialog overlays (8 warnings)
- Add <track> to video element for caption accessibility
- Add keyboard support to resize handle separator

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-22 13:30:27 -04:00
parent 3eebe01d63
commit 9d1f1cfcf3
7 changed files with 116 additions and 36 deletions

View File

@@ -188,9 +188,23 @@
<div <div
class="resize-handle" class="resize-handle"
class:active={isResizing} class:active={isResizing}
role="separator" role="slider"
aria-orientation="horizontal" aria-orientation="horizontal"
aria-label="Resize timeline and clip list"
aria-valuenow={timelineHeight}
aria-valuemin={40}
aria-valuemax={500}
tabindex="0"
onmousedown={handleResizeStart} onmousedown={handleResizeStart}
onkeydown={(e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
timelineHeight = Math.min(500, timelineHeight + 10);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
timelineHeight = Math.max(40, timelineHeight - 10);
}
}}
></div> ></div>
<div class="cliplist-pane"> <div class="cliplist-pane">
<ClipList onExport={() => (showExportDialog = true)} /> <ClipList onExport={() => (showExportDialog = true)} />

View File

@@ -12,8 +12,25 @@
}); });
</script> </script>
<div class="overlay" role="presentation" onclick={onClose}> <div
<div class="dialog" role="dialog" onclick={(e) => e.stopPropagation()}> class="overlay"
role="presentation"
tabindex="-1"
onclick={onClose}
onkeydown={(e) => {
if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ') {
if (e.key !== 'Escape') e.preventDefault();
onClose();
}
}}
>
<div
class="dialog"
role="dialog"
tabindex="-1"
onclick={(e) => e.stopPropagation()}
onkeydown={(e) => e.stopPropagation()}
>
<img class="app-icon" src="/favicon.svg" alt="GUI Video Clipper icon" /> <img class="app-icon" src="/favicon.svg" alt="GUI Video Clipper icon" />
<h2>GUI Video Clipper</h2> <h2>GUI Video Clipper</h2>
{#if version} {#if version}

View File

@@ -45,8 +45,9 @@
<div class="scroll-area"> <div class="scroll-area">
<!-- Font Family --> <!-- Font Family -->
<div class="control"> <div class="control">
<label>Font</label> <label for="caption-font">Font</label>
<select <select
id="caption-font"
value={settings.fontFamily} value={settings.fontFamily}
onchange={(e) => update({ fontFamily: (e.target as HTMLSelectElement).value })} onchange={(e) => update({ fontFamily: (e.target as HTMLSelectElement).value })}
> >
@@ -61,19 +62,20 @@
<label> <label>
Font Size Font Size
<span class="value">{settings.fontSize}px</span> <span class="value">{settings.fontSize}px</span>
</label>
<input <input
type="range" min="12" max="36" step="1" type="range" min="12" max="36" step="1"
value={settings.fontSize} value={settings.fontSize}
oninput={(e) => update({ fontSize: parseInt((e.target as HTMLInputElement).value, 10) })} oninput={(e) => update({ fontSize: parseInt((e.target as HTMLInputElement).value, 10) })}
/> />
</label>
</div> </div>
<!-- Text Color + Bold --> <!-- Text Color + Bold -->
<div class="control row"> <div class="control row">
<label>Text Color</label> <label for="caption-text-color">Text Color</label>
<div class="row-controls"> <div class="row-controls">
<input <input
id="caption-text-color"
type="color" value={settings.textColor} type="color" value={settings.textColor}
oninput={(e) => update({ textColor: (e.target as HTMLInputElement).value })} oninput={(e) => update({ textColor: (e.target as HTMLInputElement).value })}
/> />
@@ -87,10 +89,10 @@
<!-- Dimmed Text Color --> <!-- Dimmed Text Color -->
<div class="control"> <div class="control">
<label>Dimmed Text</label> <label for="caption-dimmed-auto">Dimmed Text</label>
<div class="radio-row"> <div class="radio-row">
<label> <label>
<input type="radio" name="dimmed-mode" value="auto" <input id="caption-dimmed-auto" type="radio" name="dimmed-mode" value="auto"
checked={settings.dimmedColorMode === 'auto'} checked={settings.dimmedColorMode === 'auto'}
onchange={() => update({ dimmedColorMode: 'auto' })} /> onchange={() => update({ dimmedColorMode: 'auto' })} />
Auto Auto
@@ -108,18 +110,19 @@
<label> <label>
Dim Opacity Dim Opacity
<span class="value">{Math.round(settings.dimmedOpacity * 100)}%</span> <span class="value">{Math.round(settings.dimmedOpacity * 100)}%</span>
</label>
<input <input
type="range" min="10" max="90" step="5" type="range" min="10" max="90" step="5"
value={Math.round(settings.dimmedOpacity * 100)} value={Math.round(settings.dimmedOpacity * 100)}
oninput={(e) => update({ dimmedOpacity: parseInt((e.target as HTMLInputElement).value, 10) / 100 })} oninput={(e) => update({ dimmedOpacity: parseInt((e.target as HTMLInputElement).value, 10) / 100 })}
/> />
</label>
</div> </div>
{:else} {:else}
<div class="control row"> <div class="control row">
<label>Dimmed Color</label> <label for="caption-dimmed-color">Dimmed Color</label>
<div class="row-controls"> <div class="row-controls">
<input <input
id="caption-dimmed-color"
type="color" value={settings.dimmedColor} type="color" value={settings.dimmedColor}
oninput={(e) => update({ dimmedColor: (e.target as HTMLInputElement).value })} oninput={(e) => update({ dimmedColor: (e.target as HTMLInputElement).value })}
/> />
@@ -165,12 +168,12 @@
<label> <label>
BG Opacity BG Opacity
<span class="value">{Math.round(settings.backgroundOpacity * 100)}%</span> <span class="value">{Math.round(settings.backgroundOpacity * 100)}%</span>
</label>
<input <input
type="range" min="0" max="100" step="5" type="range" min="0" max="100" step="5"
value={Math.round(settings.backgroundOpacity * 100)} value={Math.round(settings.backgroundOpacity * 100)}
oninput={(e) => update({ backgroundOpacity: parseInt((e.target as HTMLInputElement).value, 10) / 100 })} oninput={(e) => update({ backgroundOpacity: parseInt((e.target as HTMLInputElement).value, 10) / 100 })}
/> />
</label>
</div> </div>
{/if} {/if}
@@ -195,21 +198,21 @@
<label> <label>
Shadow Depth Shadow Depth
<span class="value">{settings.shadowDepth}px</span> <span class="value">{settings.shadowDepth}px</span>
</label>
<input <input
type="range" min="1" max="5" step="1" type="range" min="1" max="5" step="1"
value={settings.shadowDepth} value={settings.shadowDepth}
oninput={(e) => update({ shadowDepth: parseInt((e.target as HTMLInputElement).value, 10) })} oninput={(e) => update({ shadowDepth: parseInt((e.target as HTMLInputElement).value, 10) })}
/> />
</label>
</div> </div>
{/if} {/if}
<!-- Position --> <!-- Position -->
<div class="control"> <div class="control">
<label>Position</label> <label for="caption-pos-bottom">Position</label>
<div class="radio-row"> <div class="radio-row">
<label> <label>
<input type="radio" name="caption-position" value="bottom" <input id="caption-pos-bottom" type="radio" name="caption-position" value="bottom"
checked={settings.position === 'bottom'} checked={settings.position === 'bottom'}
onchange={() => update({ position: 'bottom' })} /> onchange={() => update({ position: 'bottom' })} />
Bottom Bottom

View File

@@ -43,7 +43,17 @@
} }
</script> </script>
<div class="clip-list" role="listbox" onclick={handleContainerClick}> <div
class="clip-list"
role="listbox"
tabindex="0"
onclick={handleContainerClick}
onkeydown={(e) => {
if (e.key === 'Escape') {
selectClip(null);
}
}}
>
{#if clips.length === 0} {#if clips.length === 0}
<div class="empty">No clips yet — press I to mark in-point, O to mark out-point</div> <div class="empty">No clips yet — press I to mark in-point, O to mark out-point</div>
{:else} {:else}

View File

@@ -136,8 +136,25 @@
} }
</script> </script>
<div class="overlay" role="presentation" onclick={onClose}> <div
<div class="dialog" role="dialog" onclick={(e) => e.stopPropagation()}> class="overlay"
role="presentation"
tabindex="-1"
onclick={onClose}
onkeydown={(e) => {
if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ') {
if (e.key !== 'Escape') e.preventDefault();
onClose();
}
}}
>
<div
class="dialog"
role="dialog"
tabindex="-1"
onclick={(e) => e.stopPropagation()}
onkeydown={(e) => e.stopPropagation()}
>
<h2>Export Clips</h2> <h2>Export Clips</h2>
{#if !isExportReady} {#if !isExportReady}

View File

@@ -57,8 +57,25 @@
} }
</script> </script>
<div class="overlay" role="presentation" onclick={onClose}> <div
<div class="panel" role="dialog" onclick={(e) => e.stopPropagation()}> class="overlay"
role="presentation"
tabindex="-1"
onclick={onClose}
onkeydown={(e) => {
if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ') {
if (e.key !== 'Escape') e.preventDefault();
onClose();
}
}}
>
<div
class="panel"
role="dialog"
tabindex="-1"
onclick={(e) => e.stopPropagation()}
onkeydown={(e) => e.stopPropagation()}
>
<h2>Preferences</h2> <h2>Preferences</h2>
<section> <section>

View File

@@ -194,7 +194,9 @@
onloadeddata={handleLoadedData} onloadeddata={handleLoadedData}
preload="metadata" preload="metadata"
playsinline playsinline
></video> >
<track kind="captions" />
</video>
{#if activeCues.length > 0} {#if activeCues.length > 0}
<div class="caption-overlay" style={captionPosition}> <div class="caption-overlay" style={captionPosition}>
{#each activeCues as cue} {#each activeCues as cue}