fix(a11y): enhance accessibility for chat settings components (#21715)

This commit adds aria-labels to the text inputs and textareas that previously lacked them, applies role=switch to inputs, and adds accessible titles to floating quick actions.
This commit is contained in:
Classic298
2026-02-22 14:33:07 -06:00
committed by GitHub
parent 1542dad51a
commit 16e567df57
8 changed files with 34 additions and 2 deletions
@@ -150,6 +150,7 @@
class="w-full text-sm dark:text-gray-300 bg-transparent outline-hidden" class="w-full text-sm dark:text-gray-300 bg-transparent outline-hidden"
type="text" type="text"
bind:value={name} bind:value={name}
aria-label={$i18n.t('Name')}
required required
placeholder={$i18n.t('Enter your name')} placeholder={$i18n.t('Enter your name')}
/> />
@@ -164,6 +165,7 @@
className="w-full text-sm dark:text-gray-300 bg-transparent outline-hidden" className="w-full text-sm dark:text-gray-300 bg-transparent outline-hidden"
minSize={60} minSize={60}
bind:value={bio} bind:value={bio}
ariaLabel={$i18n.t('Bio')}
placeholder={$i18n.t('Share your background and interests')} placeholder={$i18n.t('Share your background and interests')}
/> />
</div> </div>
@@ -176,6 +178,7 @@
<select <select
class="w-full text-sm dark:text-gray-300 bg-transparent outline-hidden" class="w-full text-sm dark:text-gray-300 bg-transparent outline-hidden"
bind:value={_gender} bind:value={_gender}
aria-label={$i18n.t('Gender')}
on:change={(e) => { on:change={(e) => {
console.log(_gender); console.log(_gender);
@@ -199,6 +202,7 @@
class="w-full text-sm dark:text-gray-300 bg-transparent outline-hidden mt-1" class="w-full text-sm dark:text-gray-300 bg-transparent outline-hidden mt-1"
type="text" type="text"
required required
aria-label={$i18n.t('Custom Gender')}
placeholder={$i18n.t('Enter your gender')} placeholder={$i18n.t('Enter your gender')}
bind:value={gender} bind:value={gender}
/> />
@@ -212,6 +216,7 @@
<input <input
class="w-full text-sm dark:text-gray-300 dark:placeholder:text-gray-300 bg-transparent outline-hidden" class="w-full text-sm dark:text-gray-300 dark:placeholder:text-gray-300 bg-transparent outline-hidden"
type="date" type="date"
aria-label={$i18n.t('Birth Date')}
bind:value={dateOfBirth} bind:value={dateOfBirth}
required required
/> />
@@ -232,6 +237,7 @@
class="w-full text-sm outline-hidden" class="w-full text-sm outline-hidden"
type="url" type="url"
placeholder={$i18n.t('Enter your webhook URL')} placeholder={$i18n.t('Enter your webhook URL')}
aria-label={$i18n.t('Notification Webhook')}
bind:value={webhookUrl} bind:value={webhookUrl}
required required
/> />
@@ -273,6 +279,7 @@
<button <button
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg" class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg"
aria-label={$i18n.t('Copy Token')}
on:click={() => { on:click={() => {
copyToClipboard(localStorage.token); copyToClipboard(localStorage.token);
JWTTokenCopied = true; JWTTokenCopied = true;
@@ -331,6 +338,7 @@
<button <button
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg" class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg"
aria-label={$i18n.t('Copy API Key')}
on:click={() => { on:click={() => {
copyToClipboard(APIKey); copyToClipboard(APIKey);
APIKeyCopied = true; APIKeyCopied = true;
@@ -376,6 +384,7 @@
<Tooltip content={$i18n.t('Create new key')}> <Tooltip content={$i18n.t('Create new key')}>
<button <button
class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg" class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg"
aria-label={$i18n.t('Create new key')}
on:click={() => { on:click={() => {
createAPIKeyHandler(); createAPIKeyHandler();
}} }}
@@ -186,6 +186,7 @@
<select <select
class="w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right" class="w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={STTEngine} bind:value={STTEngine}
aria-label={$i18n.t('Speech-to-Text Engine')}
placeholder={$i18n.t('Select an engine')} placeholder={$i18n.t('Select an engine')}
> >
<option value="">{$i18n.t('Default')}</option> <option value="">{$i18n.t('Default')}</option>
@@ -207,6 +208,7 @@
<input <input
type="text" type="text"
bind:value={STTLanguage} bind:value={STTLanguage}
aria-label={$i18n.t('Speech-to-Text Language')}
placeholder={$i18n.t('e.g. en')} placeholder={$i18n.t('e.g. en')}
class=" text-sm text-right bg-transparent dark:text-gray-300 outline-hidden" class=" text-sm text-right bg-transparent dark:text-gray-300 outline-hidden"
/> />
@@ -226,6 +228,8 @@
toggleSpeechAutoSend(); toggleSpeechAutoSend();
}} }}
type="button" type="button"
role="switch"
aria-checked={speechAutoSend}
> >
{#if speechAutoSend === true} {#if speechAutoSend === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span> <span class="ml-2 self-center">{$i18n.t('On')}</span>
@@ -245,6 +249,7 @@
<select <select
class="w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right" class="w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={TTSEngine} bind:value={TTSEngine}
aria-label={$i18n.t('Text-to-Speech Engine')}
placeholder={$i18n.t('Select an engine')} placeholder={$i18n.t('Select an engine')}
> >
<option value="">{$i18n.t('Default')}</option> <option value="">{$i18n.t('Default')}</option>
@@ -260,6 +265,7 @@
<select <select
class="w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right" class="w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={TTSEngineConfig.dtype} bind:value={TTSEngineConfig.dtype}
aria-label={$i18n.t('Kokoro.js Dtype')}
placeholder={$i18n.t('Select dtype')} placeholder={$i18n.t('Select dtype')}
> >
<option value="" disabled selected>{$i18n.t('Select dtype')}</option> <option value="" disabled selected>{$i18n.t('Select dtype')}</option>
@@ -281,6 +287,8 @@
toggleResponseAutoPlayback(); toggleResponseAutoPlayback();
}} }}
type="button" type="button"
role="switch"
aria-checked={responseAutoPlayback}
> >
{#if responseAutoPlayback === true} {#if responseAutoPlayback === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span> <span class="ml-2 self-center">{$i18n.t('On')}</span>
@@ -299,6 +307,7 @@
min="0" min="0"
step="0.01" step="0.01"
bind:value={playbackRate} bind:value={playbackRate}
aria-label={$i18n.t('Speech Playback Speed')}
class=" text-sm text-right bg-transparent dark:text-gray-300 outline-hidden" class=" text-sm text-right bg-transparent dark:text-gray-300 outline-hidden"
/> />
x x
@@ -318,6 +327,7 @@
list="voice-list" list="voice-list"
class="w-full text-sm bg-transparent dark:text-gray-300 outline-hidden" class="w-full text-sm bg-transparent dark:text-gray-300 outline-hidden"
bind:value={voice} bind:value={voice}
aria-label={$i18n.t('Voice')}
placeholder={$i18n.t('Select a voice')} placeholder={$i18n.t('Select a voice')}
/> />
@@ -355,6 +365,7 @@
<select <select
class="w-full text-sm bg-transparent dark:text-gray-300 outline-hidden" class="w-full text-sm bg-transparent dark:text-gray-300 outline-hidden"
bind:value={voice} bind:value={voice}
aria-label={$i18n.t('Voice')}
> >
<option value="" selected={voice !== ''}>{$i18n.t('Default')}</option> <option value="" selected={voice !== ''}>{$i18n.t('Default')}</option>
{#each voices.filter((v) => nonLocalVoices || v.localService === true) as _voice} {#each voices.filter((v) => nonLocalVoices || v.localService === true) as _voice}
@@ -386,6 +397,7 @@
list="voice-list" list="voice-list"
class="w-full text-sm bg-transparent dark:text-gray-300 outline-hidden" class="w-full text-sm bg-transparent dark:text-gray-300 outline-hidden"
bind:value={voice} bind:value={voice}
aria-label={$i18n.t('Voice')}
placeholder={$i18n.t('Select a voice')} placeholder={$i18n.t('Select a voice')}
/> />
@@ -87,6 +87,7 @@
<Tooltip content={$i18n.t(`Add Connection`)}> <Tooltip content={$i18n.t(`Add Connection`)}>
<button <button
class="px-1" class="px-1"
aria-label={$i18n.t('Add Connection')}
on:click={() => { on:click={() => {
showConnectionModal = true; showConnectionModal = true;
}} }}
@@ -269,6 +269,8 @@
toggleNotification(); toggleNotification();
}} }}
type="button" type="button"
role="switch"
aria-checked={notificationEnabled}
> >
{#if notificationEnabled === true} {#if notificationEnabled === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span> <span class="ml-2 self-center">{$i18n.t('On')}</span>
@@ -306,6 +308,7 @@
? 'text-gray-800 dark:text-gray-100' ? 'text-gray-800 dark:text-gray-100'
: 'text-gray-400 dark:text-gray-500'}" : 'text-gray-400 dark:text-gray-500'}"
type="button" type="button"
aria-expanded={showAdvanced}
on:click={() => { on:click={() => {
showAdvanced = !showAdvanced; showAdvanced = !showAdvanced;
}}>{showAdvanced ? $i18n.t('Hide') : $i18n.t('Show')}</button }}>{showAdvanced ? $i18n.t('Hide') : $i18n.t('Show')}</button
@@ -129,12 +129,14 @@
<input <input
class=" self-center text-xs outline-none w-20" class=" self-center text-xs outline-none w-20"
placeholder={$i18n.t('Button Label')} placeholder={$i18n.t('Button Label')}
aria-label={$i18n.t('Button Label')}
bind:value={button.label} bind:value={button.label}
/> />
<input <input
class=" self-center text-xs outline-none w-20 text-gray-600 dark:text-gray-400" class=" self-center text-xs outline-none w-20 text-gray-600 dark:text-gray-400"
placeholder={$i18n.t('Button ID')} placeholder={$i18n.t('Button ID')}
aria-label={$i18n.t('Button ID')}
bind:value={button.id} bind:value={button.id}
/> />
</div> </div>
@@ -143,12 +145,14 @@
<Textarea <Textarea
className=" self-center text-xs w-full outline-none" className=" self-center text-xs w-full outline-none"
placeholder={$i18n.t('Button Prompt')} placeholder={$i18n.t('Button Prompt')}
ariaLabel={$i18n.t('Button Prompt')}
minSize={30} minSize={30}
bind:value={button.prompt} bind:value={button.prompt}
/> />
</div> </div>
<button <button
class="pl-3 text-xs flex rounded-sm transition" class="pl-3 text-xs flex rounded-sm transition"
aria-label={$i18n.t('Remove action')}
on:click={() => { on:click={() => {
floatingActionButtons = floatingActionButtons.filter( floatingActionButtons = floatingActionButtons.filter(
(b) => b.id !== button.id (b) => b.id !== button.id
@@ -62,9 +62,9 @@
>{$i18n.t('Image Max Compression Size width')}</label >{$i18n.t('Image Max Compression Size width')}</label
> >
<input <input
id="image-comp-width"
bind:value={size.width} bind:value={size.width}
type="number" type="number"
aria-labelledby="image-comp-width"
class="w-full bg-transparent outline-hidden text-center" class="w-full bg-transparent outline-hidden text-center"
min="0" min="0"
placeholder={$i18n.t('Width')} placeholder={$i18n.t('Width')}
@@ -80,9 +80,9 @@
>{$i18n.t('Image Max Compression Size height')}</label >{$i18n.t('Image Max Compression Size height')}</label
> >
<input <input
id="image-comp-height"
bind:value={size.height} bind:value={size.height}
type="number" type="number"
aria-labelledby="image-comp-height"
class="w-full bg-transparent outline-hidden text-center" class="w-full bg-transparent outline-hidden text-center"
min="0" min="0"
placeholder={$i18n.t('Height')} placeholder={$i18n.t('Height')}
@@ -358,6 +358,7 @@
<div class="text-lg font-medium self-center">{$i18n.t('Sync Usage Stats')}</div> <div class="text-lg font-medium self-center">{$i18n.t('Sync Usage Stats')}</div>
<button <button
class="self-center" class="self-center"
aria-label={$i18n.t('Close modal')}
on:click={() => { on:click={() => {
show = false; show = false;
}} }}
@@ -10,6 +10,7 @@
export let readonly = false; export let readonly = false;
export let className = export let className =
'w-full rounded-lg px-3.5 py-2 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden h-full'; 'w-full rounded-lg px-3.5 py-2 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden h-full';
export let ariaLabel = null;
export let onInput = () => {}; export let onInput = () => {};
export let onBlur = () => {}; export let onBlur = () => {};
@@ -53,6 +54,7 @@
bind:this={textareaElement} bind:this={textareaElement}
bind:value bind:value
{placeholder} {placeholder}
aria-label={ariaLabel || placeholder}
class={className} class={className}
style="field-sizing: content;" style="field-sizing: content;"
{rows} {rows}