style(MCP): Enhance dialog accessibility and styling consistency (#11585)

* style: update input IDs in BasicInfoSection for consistency and improve accessibility

* style: add border-destructive variable for improved design consistency

* style: update error border color for title input in BasicInfoSection

* style: update delete confirmation dialog title and description for MCP Server

* style: add text-destructive variable for improved design consistency

* style: update error message and border color for URL and trust fields for consistency

* style: reorder imports and update error message styling for consistency across sections

* style: enhance MCPServerDialog with copy link functionality and UI improvements

* style: enhance MCPServerDialog with improved accessibility and loading indicators

* style: bump @librechat/client to 0.4.51 and enhance OGDialogTemplate for improved selection handling

* a11y: enhance accessibility and error handling in MCPServerDialog sections

* style: enhance MCPServerDialog accessibility and improve resource name handling

* style: improve accessibility in MCPServerDialog and AuthSection, update translation for delete confirmation

* style: update aria-invalid attributes to use string values for improved accessibility in form sections

* style: enhance accessibility in AuthSection by updating aria attributes and adding error messages

* style: remove unnecessary aria-hidden attributes from Spinner components in MCPServerDialog

* style: simplify legacy selection check in OGDialogTemplate
This commit is contained in:
Marco Beretta 2026-02-12 04:08:40 +01:00 committed by GitHub
parent 299efc2ccb
commit d6b6f191f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 295 additions and 141 deletions

View file

@ -1,4 +1,4 @@
import { forwardRef, ReactNode, Ref } from 'react';
import { forwardRef, isValidElement, ReactNode, Ref } from 'react';
import {
OGDialogTitle,
OGDialogClose,
@ -19,13 +19,39 @@ type SelectionProps = {
isLoading?: boolean;
};
/**
* Type guard to check if selection is a legacy SelectionProps object
*/
function isSelectionProps(selection: unknown): selection is SelectionProps {
return (
typeof selection === 'object' &&
selection !== null &&
!isValidElement(selection) &&
('selectHandler' in selection ||
'selectClasses' in selection ||
'selectText' in selection ||
'isLoading' in selection)
);
}
type DialogTemplateProps = {
title: string;
description?: string;
main?: ReactNode;
buttons?: ReactNode;
leftButtons?: ReactNode;
selection?: SelectionProps;
/**
* Selection button configuration. Can be either:
* - An object with selectHandler, selectClasses, selectText, isLoading (legacy)
* - A ReactNode for custom selection component
* @example
* // Legacy usage
* selection={{ selectHandler: () => {}, selectText: 'Confirm' }}
* @example
* // Custom component
* selection={<Button onClick={handleConfirm}>Confirm</Button>}
*/
selection?: SelectionProps | ReactNode;
className?: string;
overlayClassName?: string;
headerClassName?: string;
@ -49,14 +75,39 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDi
mainClassName,
headerClassName,
footerClassName,
showCloseButton,
showCloseButton = false,
overlayClassName,
showCancelButton = true,
} = props;
const { selectHandler, selectClasses, selectText, isLoading } = selection || {};
const isLegacySelection = isSelectionProps(selection);
const { selectHandler, selectClasses, selectText, isLoading } = isLegacySelection
? selection
: {};
const defaultSelect =
'bg-gray-800 text-white transition-colors hover:bg-gray-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-200 dark:text-gray-800 dark:hover:bg-gray-200';
let selectionContent = null;
if (isLegacySelection) {
selectionContent = (
<OGDialogClose
onClick={selectHandler}
disabled={isLoading}
className={`${
selectClasses ?? defaultSelect
} flex h-10 items-center justify-center rounded-lg border-none px-4 py-2 text-sm disabled:opacity-80 max-sm:order-first max-sm:w-full sm:order-none`}
>
{isLoading === true ? (
<Spinner className="size-4 text-text-primary" />
) : (
(selectText as React.JSX.Element)
)}
</OGDialogClose>
);
} else if (selection) {
selectionContent = selection;
}
return (
<OGDialogContent
overlayClassName={overlayClassName}
@ -75,38 +126,18 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDi
</OGDialogHeader>
<div className={cn('px-0 py-2', mainClassName)}>{main != null ? main : null}</div>
<OGDialogFooter className={footerClassName}>
<div>
{leftButtons != null ? (
<div className="mt-3 flex h-auto gap-3 max-sm:w-full max-sm:flex-col sm:mt-0 sm:flex-row">
{leftButtons}
</div>
) : null}
</div>
<div className="flex h-auto gap-3 max-sm:w-full max-sm:flex-col sm:flex-row">
{showCancelButton && (
<OGDialogClose asChild>
<Button variant="outline" aria-label={localize('com_ui_cancel')}>
{localize('com_ui_cancel')}
</Button>
</OGDialogClose>
)}
{buttons != null ? buttons : null}
{selection ? (
<OGDialogClose
onClick={selectHandler}
disabled={isLoading}
className={`${
selectClasses ?? defaultSelect
} flex h-10 items-center justify-center rounded-lg border-none px-4 py-2 text-sm disabled:opacity-80 max-sm:order-first max-sm:w-full sm:order-none`}
>
{isLoading === true ? (
<Spinner className="size-4 text-white" />
) : (
(selectText as React.JSX.Element)
)}
</OGDialogClose>
) : null}
</div>
{leftButtons != null ? (
<div className="mr-auto flex flex-row gap-2">{leftButtons}</div>
) : null}
{showCancelButton && (
<OGDialogClose asChild>
<Button variant="outline" aria-label={localize('com_ui_cancel')}>
{localize('com_ui_cancel')}
</Button>
</OGDialogClose>
)}
{buttons != null ? buttons : null}
{selectionContent}
</OGDialogFooter>
</OGDialogContent>
);

View file

@ -14,6 +14,7 @@ interface RadioProps {
disabled?: boolean;
className?: string;
fullWidth?: boolean;
'aria-labelledby'?: string;
}
const Radio = memo(function Radio({
@ -23,6 +24,7 @@ const Radio = memo(function Radio({
disabled = false,
className = '',
fullWidth = false,
'aria-labelledby': ariaLabelledBy,
}: RadioProps) {
const localize = useLocalize();
const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);
@ -79,6 +81,7 @@ const Radio = memo(function Radio({
<div
className="relative inline-flex items-center rounded-lg bg-muted p-1 opacity-50"
role="radiogroup"
aria-labelledby={ariaLabelledBy}
>
<span className="px-4 py-2 text-xs text-muted-foreground">
{localize('com_ui_no_options')}
@ -93,6 +96,7 @@ const Radio = memo(function Radio({
<div
className={`relative ${fullWidth ? 'flex' : 'inline-flex'} items-center rounded-lg bg-muted p-1 ${className}`}
role="radiogroup"
aria-labelledby={ariaLabelledBy}
>
{selectedIndex >= 0 && isMounted && (
<div