refactor: improve accessibility and i18n for search components

• Replace label with ariaLabel prop
• Add searchPlaceholder support
• Use i18n keys for messages
• Improve aria-label attributes
• Standardize noResultsMessage fallback
This commit is contained in:
yangdx 2025-09-24 19:59:56 +08:00
parent ed5911f656
commit ad247f2fc1
4 changed files with 20 additions and 12 deletions

View file

@ -185,9 +185,11 @@ const GraphLabels = () => {
renderOption={(item) => <div style={{ whiteSpace: 'pre' }}>{item}</div>} renderOption={(item) => <div style={{ whiteSpace: 'pre' }}>{item}</div>}
getOptionValue={(item) => item} getOptionValue={(item) => item}
getDisplayValue={(item) => <div style={{ whiteSpace: 'pre' }}>{item}</div>} getDisplayValue={(item) => <div style={{ whiteSpace: 'pre' }}>{item}</div>}
notFound={<div className="py-6 text-center text-sm">No labels found</div>} notFound={<div className="py-6 text-center text-sm">{t('graphPanel.graphLabels.noLabels')}</div>}
label={t('graphPanel.graphLabels.label')} ariaLabel={t('graphPanel.graphLabels.label')}
placeholder={t('graphPanel.graphLabels.placeholder')} placeholder={t('graphPanel.graphLabels.placeholder')}
searchPlaceholder={t('graphPanel.graphLabels.placeholder')}
noResultsMessage={t('graphPanel.graphLabels.noLabels')}
value={label !== null ? label : '*'} value={label !== null ? label : '*'}
onChange={(newLabel) => { onChange={(newLabel) => {
const currentLabel = useSettingsStore.getState().queryLabel; const currentLabel = useSettingsStore.getState().queryLabel;

View file

@ -215,8 +215,9 @@ export const GraphSearchInput = ({
onFocus={(id) => { onFocus={(id) => {
if (id !== messageId && onFocus) onFocus(id ? { id, type: 'nodes' } : null) if (id !== messageId && onFocus) onFocus(id ? { id, type: 'nodes' } : null)
}} }}
label={'item'} ariaLabel={t('graphPanel.search.placeholder')}
placeholder={t('graphPanel.search.placeholder')} placeholder={t('graphPanel.search.placeholder')}
noResultsMessage={t('graphPanel.search.placeholder')}
/> />
) )
} }

View file

@ -41,8 +41,8 @@ export interface AsyncSearchProps<T> {
onChange: (value: string) => void onChange: (value: string) => void
/** Callback when focus changes */ /** Callback when focus changes */
onFocus: (value: string) => void onFocus: (value: string) => void
/** Label for the select field */ /** Accessibility label for the search field */
label: string ariaLabel?: string
/** Placeholder text when no selection */ /** Placeholder text when no selection */
placeholder?: string placeholder?: string
/** Disable the entire select */ /** Disable the entire select */
@ -67,7 +67,7 @@ export function AsyncSearch<T>({
getOptionValue, getOptionValue,
notFound, notFound,
loadingSkeleton, loadingSkeleton,
label, ariaLabel,
placeholder = 'Select...', placeholder = 'Select...',
value, value,
onChange, onChange,
@ -179,6 +179,7 @@ export function AsyncSearch<T>({
placeholder={placeholder} placeholder={placeholder}
value={searchTerm} value={searchTerm}
className="max-h-8" className="max-h-8"
aria-label={ariaLabel}
onFocus={handleFocus} onFocus={handleFocus}
onValueChange={(value) => { onValueChange={(value) => {
setSearchTerm(value) setSearchTerm(value)
@ -198,7 +199,7 @@ export function AsyncSearch<T>({
!error && !error &&
options.length === 0 && options.length === 0 &&
(notFound || ( (notFound || (
<CommandEmpty>{noResultsMessage ?? `No ${label.toLowerCase()} found.`}</CommandEmpty> <CommandEmpty>{noResultsMessage || 'No results found.'}</CommandEmpty>
))} ))}
<CommandGroup> <CommandGroup>
{options.map((option, idx) => ( {options.map((option, idx) => (

View file

@ -43,10 +43,12 @@ export interface AsyncSelectProps<T> {
value: string value: string
/** Callback when selection changes */ /** Callback when selection changes */
onChange: (value: string) => void onChange: (value: string) => void
/** Label for the select field */ /** Accessibility label for the select field */
label: string ariaLabel?: string
/** Placeholder text when no selection */ /** Placeholder text when no selection */
placeholder?: string placeholder?: string
/** Display text for search placeholder */
searchPlaceholder?: string
/** Disable the entire select */ /** Disable the entire select */
disabled?: boolean disabled?: boolean
/** Custom width for the popover * /** Custom width for the popover *
@ -76,8 +78,9 @@ export function AsyncSelect<T>({
getDisplayValue, getDisplayValue,
notFound, notFound,
loadingSkeleton, loadingSkeleton,
label, ariaLabel,
placeholder = 'Select...', placeholder = 'Select...',
searchPlaceholder,
value, value,
onChange, onChange,
disabled = false, disabled = false,
@ -200,6 +203,7 @@ export function AsyncSelect<T>({
variant="outline" variant="outline"
role="combobox" role="combobox"
aria-expanded={open} aria-expanded={open}
aria-label={ariaLabel}
className={cn( className={cn(
'justify-between', 'justify-between',
disabled && 'cursor-not-allowed opacity-50', disabled && 'cursor-not-allowed opacity-50',
@ -223,7 +227,7 @@ export function AsyncSelect<T>({
<Command shouldFilter={false}> <Command shouldFilter={false}>
<div className="relative w-full border-b"> <div className="relative w-full border-b">
<CommandInput <CommandInput
placeholder={`Search ${label.toLowerCase()}...`} placeholder={searchPlaceholder || 'Search...'}
value={searchTerm} value={searchTerm}
onValueChange={(value) => { onValueChange={(value) => {
setSearchTerm(value) setSearchTerm(value)
@ -244,7 +248,7 @@ export function AsyncSelect<T>({
options.length === 0 && options.length === 0 &&
(notFound || ( (notFound || (
<CommandEmpty> <CommandEmpty>
{noResultsMessage ?? `No ${label.toLowerCase()} found.`} {noResultsMessage || 'No results found.'}
</CommandEmpty> </CommandEmpty>
))} ))}
<CommandGroup> <CommandGroup>