fix: improve form accessibility with proper label associations

Added missing htmlFor attributes to labels and corresponding IDs to form elements throughout the web UI to enhance accessibility. This ensures screen readers can correctly identify form controls and improves browser autofill functionality. Changes include:

- Fixed label associations in login form
- Added proper IDs to form elements in Settings component
- Replaced decorative labels with semantic headings in PropertiesView
- Added screen reader accessible labels in RetrievalTesting
- Improved checkbox accessibility in QuerySettings
This commit is contained in:
yangdx
2025-04-07 05:20:12 +08:00
parent 01fc513621
commit 46ffb6afa4
5 changed files with 168 additions and 108 deletions

View File

@@ -184,9 +184,11 @@ const PropertyRow = ({
return translation === translationKey ? name : translation return translation === translationKey ? name : translation
} }
// Since Text component uses a label internally, we'll use a span here instead of a label
// to avoid nesting labels which is not recommended for accessibility
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<label className="text-primary/60 tracking-wide whitespace-nowrap">{getPropertyNameTranslation(name)}</label>: <span className="text-primary/60 tracking-wide whitespace-nowrap">{getPropertyNameTranslation(name)}</span>:
<Text <Text
className="hover:bg-primary/20 rounded p-1 overflow-hidden text-ellipsis" className="hover:bg-primary/20 rounded p-1 overflow-hidden text-ellipsis"
tooltipClassName="max-w-80" tooltipClassName="max-w-80"
@@ -213,7 +215,7 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<label className="text-md pl-1 font-bold tracking-wide text-blue-700">{t('graphPanel.propertiesView.node.title')}</label> <h3 className="text-md pl-1 font-bold tracking-wide text-blue-700">{t('graphPanel.propertiesView.node.title')}</h3>
<div className="flex gap-3"> <div className="flex gap-3">
<Button <Button
size="icon" size="icon"
@@ -246,7 +248,7 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
/> />
<PropertyRow name={t('graphPanel.propertiesView.node.degree')} value={node.degree} /> <PropertyRow name={t('graphPanel.propertiesView.node.degree')} value={node.degree} />
</div> </div>
<label className="text-md pl-1 font-bold tracking-wide text-amber-700">{t('graphPanel.propertiesView.node.properties')}</label> <h3 className="text-md pl-1 font-bold tracking-wide text-amber-700">{t('graphPanel.propertiesView.node.properties')}</h3>
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1"> <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
{Object.keys(node.properties) {Object.keys(node.properties)
.sort() .sort()
@@ -256,9 +258,9 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
</div> </div>
{node.relationships.length > 0 && ( {node.relationships.length > 0 && (
<> <>
<label className="text-md pl-1 font-bold tracking-wide text-emerald-700"> <h3 className="text-md pl-1 font-bold tracking-wide text-emerald-700">
{t('graphPanel.propertiesView.node.relationships')} {t('graphPanel.propertiesView.node.relationships')}
</label> </h3>
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1"> <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
{node.relationships.map(({ type, id, label }) => { {node.relationships.map(({ type, id, label }) => {
return ( return (
@@ -283,7 +285,7 @@ const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-md pl-1 font-bold tracking-wide text-violet-700">{t('graphPanel.propertiesView.edge.title')}</label> <h3 className="text-md pl-1 font-bold tracking-wide text-violet-700">{t('graphPanel.propertiesView.edge.title')}</h3>
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1"> <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
<PropertyRow name={t('graphPanel.propertiesView.edge.id')} value={edge.id} /> <PropertyRow name={t('graphPanel.propertiesView.edge.id')} value={edge.id} />
{edge.type && <PropertyRow name={t('graphPanel.propertiesView.edge.type')} value={edge.type} />} {edge.type && <PropertyRow name={t('graphPanel.propertiesView.edge.type')} value={edge.type} />}
@@ -302,7 +304,7 @@ const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => {
}} }}
/> />
</div> </div>
<label className="text-md pl-1 font-bold tracking-wide text-amber-700">{t('graphPanel.propertiesView.edge.properties')}</label> <h3 className="text-md pl-1 font-bold tracking-wide text-amber-700">{t('graphPanel.propertiesView.edge.properties')}</h3>
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1"> <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
{Object.keys(edge.properties) {Object.keys(edge.properties)
.sort() .sort()

View File

@@ -23,11 +23,14 @@ const LabeledCheckBox = ({
onCheckedChange: () => void onCheckedChange: () => void
label: string label: string
}) => { }) => {
// Create unique ID using the label text converted to lowercase with spaces removed
const id = `checkbox-${label.toLowerCase().replace(/\s+/g, '-')}`;
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Checkbox checked={checked} onCheckedChange={onCheckedChange} /> <Checkbox id={id} checked={checked} onCheckedChange={onCheckedChange} />
<label <label
htmlFor="terms" htmlFor={id}
className="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70" className="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
> >
{label} {label}
@@ -56,6 +59,8 @@ const LabeledNumberInput = ({
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [currentValue, setCurrentValue] = useState<number | null>(value) const [currentValue, setCurrentValue] = useState<number | null>(value)
// Create unique ID using the label text converted to lowercase with spaces removed
const id = `input-${label.toLowerCase().replace(/\s+/g, '-')}`;
const onValueChange = useCallback( const onValueChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => { (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -94,13 +99,14 @@ const LabeledNumberInput = ({
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label <label
htmlFor="terms" htmlFor={id}
className="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70" className="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
> >
{label} {label}
</label> </label>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Input <Input
id={id}
type="number" type="number"
value={currentValue === null ? '' : currentValue} value={currentValue === null ? '' : currentValue}
onChange={onValueChange} onChange={onValueChange}
@@ -295,11 +301,12 @@ export default function Settings() {
/> />
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"> <label htmlFor="edge-size-min" className="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
{t('graphPanel.sideBar.settings.edgeSizeRange')} {t('graphPanel.sideBar.settings.edgeSizeRange')}
</label> </label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Input <Input
id="edge-size-min"
type="number" type="number"
value={minEdgeSize} value={minEdgeSize}
onChange={(e) => { onChange={(e) => {
@@ -315,6 +322,7 @@ export default function Settings() {
<span>-</span> <span>-</span>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Input <Input
id="edge-size-max"
type="number" type="number"
value={maxEdgeSize} value={maxEdgeSize}
onChange={(e) => { onChange={(e) => {

View File

@@ -93,14 +93,19 @@ export default function QuerySettings() {
tooltip={t('retrievePanel.querySettings.topKTooltip')} tooltip={t('retrievePanel.querySettings.topKTooltip')}
side="left" side="left"
/> />
<NumberInput <div>
id="top_k" <label htmlFor="top_k" className="sr-only">
stepper={1} {t('retrievePanel.querySettings.topK')}
value={querySettings.top_k} </label>
onValueChange={(v) => handleChange('top_k', v)} <NumberInput
min={1} id="top_k"
placeholder={t('retrievePanel.querySettings.topKPlaceholder')} stepper={1}
/> value={querySettings.top_k}
onValueChange={(v) => handleChange('top_k', v)}
min={1}
placeholder={t('retrievePanel.querySettings.topKPlaceholder')}
/>
</div>
</> </>
{/* Max Tokens */} {/* Max Tokens */}
@@ -112,14 +117,19 @@ export default function QuerySettings() {
tooltip={t('retrievePanel.querySettings.maxTokensTextUnitTooltip')} tooltip={t('retrievePanel.querySettings.maxTokensTextUnitTooltip')}
side="left" side="left"
/> />
<NumberInput <div>
id="max_token_for_text_unit" <label htmlFor="max_token_for_text_unit" className="sr-only">
stepper={500} {t('retrievePanel.querySettings.maxTokensTextUnit')}
value={querySettings.max_token_for_text_unit} </label>
onValueChange={(v) => handleChange('max_token_for_text_unit', v)} <NumberInput
min={1} id="max_token_for_text_unit"
placeholder={t('retrievePanel.querySettings.maxTokensTextUnit')} stepper={500}
/> value={querySettings.max_token_for_text_unit}
onValueChange={(v) => handleChange('max_token_for_text_unit', v)}
min={1}
placeholder={t('retrievePanel.querySettings.maxTokensTextUnit')}
/>
</div>
</> </>
<> <>
@@ -128,14 +138,19 @@ export default function QuerySettings() {
tooltip={t('retrievePanel.querySettings.maxTokensGlobalContextTooltip')} tooltip={t('retrievePanel.querySettings.maxTokensGlobalContextTooltip')}
side="left" side="left"
/> />
<NumberInput <div>
id="max_token_for_global_context" <label htmlFor="max_token_for_global_context" className="sr-only">
stepper={500} {t('retrievePanel.querySettings.maxTokensGlobalContext')}
value={querySettings.max_token_for_global_context} </label>
onValueChange={(v) => handleChange('max_token_for_global_context', v)} <NumberInput
min={1} id="max_token_for_global_context"
placeholder={t('retrievePanel.querySettings.maxTokensGlobalContext')} stepper={500}
/> value={querySettings.max_token_for_global_context}
onValueChange={(v) => handleChange('max_token_for_global_context', v)}
min={1}
placeholder={t('retrievePanel.querySettings.maxTokensGlobalContext')}
/>
</div>
</> </>
<> <>
@@ -145,14 +160,19 @@ export default function QuerySettings() {
tooltip={t('retrievePanel.querySettings.maxTokensLocalContextTooltip')} tooltip={t('retrievePanel.querySettings.maxTokensLocalContextTooltip')}
side="left" side="left"
/> />
<NumberInput <div>
id="max_token_for_local_context" <label htmlFor="max_token_for_local_context" className="sr-only">
stepper={500} {t('retrievePanel.querySettings.maxTokensLocalContext')}
value={querySettings.max_token_for_local_context} </label>
onValueChange={(v) => handleChange('max_token_for_local_context', v)} <NumberInput
min={1} id="max_token_for_local_context"
placeholder={t('retrievePanel.querySettings.maxTokensLocalContext')} stepper={500}
/> value={querySettings.max_token_for_local_context}
onValueChange={(v) => handleChange('max_token_for_local_context', v)}
min={1}
placeholder={t('retrievePanel.querySettings.maxTokensLocalContext')}
/>
</div>
</> </>
</> </>
@@ -164,16 +184,21 @@ export default function QuerySettings() {
tooltip={t('retrievePanel.querySettings.historyTurnsTooltip')} tooltip={t('retrievePanel.querySettings.historyTurnsTooltip')}
side="left" side="left"
/> />
<NumberInput <div>
className="!border-input" <label htmlFor="history_turns" className="sr-only">
id="history_turns" {t('retrievePanel.querySettings.historyTurns')}
stepper={1} </label>
type="text" <NumberInput
value={querySettings.history_turns} className="!border-input"
onValueChange={(v) => handleChange('history_turns', v)} id="history_turns"
min={0} stepper={1}
placeholder={t('retrievePanel.querySettings.historyTurnsPlaceholder')} type="text"
/> value={querySettings.history_turns}
onValueChange={(v) => handleChange('history_turns', v)}
min={0}
placeholder={t('retrievePanel.querySettings.historyTurnsPlaceholder')}
/>
</div>
</> </>
{/* Keywords */} {/* Keywords */}
@@ -185,19 +210,24 @@ export default function QuerySettings() {
tooltip={t('retrievePanel.querySettings.hlKeywordsTooltip')} tooltip={t('retrievePanel.querySettings.hlKeywordsTooltip')}
side="left" side="left"
/> />
<Input <div>
id="hl_keywords" <label htmlFor="hl_keywords" className="sr-only">
type="text" {t('retrievePanel.querySettings.hlKeywords')}
value={querySettings.hl_keywords?.join(', ')} </label>
onChange={(e) => { <Input
const keywords = e.target.value id="hl_keywords"
.split(',') type="text"
.map((k) => k.trim()) value={querySettings.hl_keywords?.join(', ')}
.filter((k) => k !== '') onChange={(e) => {
handleChange('hl_keywords', keywords) const keywords = e.target.value
}} .split(',')
placeholder={t('retrievePanel.querySettings.hlkeywordsPlaceHolder')} .map((k) => k.trim())
/> .filter((k) => k !== '')
handleChange('hl_keywords', keywords)
}}
placeholder={t('retrievePanel.querySettings.hlkeywordsPlaceHolder')}
/>
</div>
</> </>
<> <>
@@ -207,32 +237,38 @@ export default function QuerySettings() {
tooltip={t('retrievePanel.querySettings.llKeywordsTooltip')} tooltip={t('retrievePanel.querySettings.llKeywordsTooltip')}
side="left" side="left"
/> />
<Input <div>
id="ll_keywords" <label htmlFor="ll_keywords" className="sr-only">
type="text" {t('retrievePanel.querySettings.llKeywords')}
value={querySettings.ll_keywords?.join(', ')} </label>
onChange={(e) => { <Input
const keywords = e.target.value id="ll_keywords"
.split(',') type="text"
.map((k) => k.trim()) value={querySettings.ll_keywords?.join(', ')}
.filter((k) => k !== '') onChange={(e) => {
handleChange('ll_keywords', keywords) const keywords = e.target.value
}} .split(',')
placeholder={t('retrievePanel.querySettings.hlkeywordsPlaceHolder')} .map((k) => k.trim())
/> .filter((k) => k !== '')
handleChange('ll_keywords', keywords)
}}
placeholder={t('retrievePanel.querySettings.hlkeywordsPlaceHolder')}
/>
</div>
</> </>
</> </>
{/* Toggle Options */} {/* Toggle Options */}
<> <>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Text <label htmlFor="only_need_context" className="flex-1">
className="ml-1" <Text
text={t('retrievePanel.querySettings.onlyNeedContext')} className="ml-1"
tooltip={t('retrievePanel.querySettings.onlyNeedContextTooltip')} text={t('retrievePanel.querySettings.onlyNeedContext')}
side="left" tooltip={t('retrievePanel.querySettings.onlyNeedContextTooltip')}
/> side="left"
<div className="grow" /> />
</label>
<Checkbox <Checkbox
className="mr-1 cursor-pointer" className="mr-1 cursor-pointer"
id="only_need_context" id="only_need_context"
@@ -242,13 +278,14 @@ export default function QuerySettings() {
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Text <label htmlFor="only_need_prompt" className="flex-1">
className="ml-1" <Text
text={t('retrievePanel.querySettings.onlyNeedPrompt')} className="ml-1"
tooltip={t('retrievePanel.querySettings.onlyNeedPromptTooltip')} text={t('retrievePanel.querySettings.onlyNeedPrompt')}
side="left" tooltip={t('retrievePanel.querySettings.onlyNeedPromptTooltip')}
/> side="left"
<div className="grow" /> />
</label>
<Checkbox <Checkbox
className="mr-1 cursor-pointer" className="mr-1 cursor-pointer"
id="only_need_prompt" id="only_need_prompt"
@@ -258,13 +295,14 @@ export default function QuerySettings() {
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Text <label htmlFor="stream" className="flex-1">
className="ml-1" <Text
text={t('retrievePanel.querySettings.streamResponse')} className="ml-1"
tooltip={t('retrievePanel.querySettings.streamResponseTooltip')} text={t('retrievePanel.querySettings.streamResponse')}
side="left" tooltip={t('retrievePanel.querySettings.streamResponseTooltip')}
/> side="left"
<div className="grow" /> />
</label>
<Checkbox <Checkbox
className="mr-1 cursor-pointer" className="mr-1 cursor-pointer"
id="stream" id="stream"

View File

@@ -507,8 +507,14 @@ export default function DocumentManager() {
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-gray-500">{t('documentPanel.documentManager.fileNameLabel')}</span> <label
htmlFor="toggle-filename-btn"
className="text-sm text-gray-500"
>
{t('documentPanel.documentManager.fileNameLabel')}
</label>
<Button <Button
id="toggle-filename-btn"
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setShowFileName(!showFileName)} onClick={() => setShowFileName(!showFileName)}

View File

@@ -147,13 +147,19 @@ export default function RetrievalTesting() {
<EraserIcon /> <EraserIcon />
{t('retrievePanel.retrieval.clear')} {t('retrievePanel.retrieval.clear')}
</Button> </Button>
<Input <div className="flex-1 relative">
className="flex-1" <label htmlFor="query-input" className="sr-only">
value={inputValue} {t('retrievePanel.retrieval.placeholder')}
onChange={(e) => setInputValue(e.target.value)} </label>
placeholder={t('retrievePanel.retrieval.placeholder')} <Input
disabled={isLoading} id="query-input"
/> className="w-full"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder={t('retrievePanel.retrieval.placeholder')}
disabled={isLoading}
/>
</div>
<Button type="submit" variant="default" disabled={isLoading} size="sm"> <Button type="submit" variant="default" disabled={isLoading} size="sm">
<SendIcon /> <SendIcon />
{t('retrievePanel.retrieval.send')} {t('retrievePanel.retrieval.send')}