mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-13 13:04:24 +01:00
♿ 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:
parent
299efc2ccb
commit
d6b6f191f7
13 changed files with 295 additions and 141 deletions
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue