fix: React Console Warnings and Accessibility Issues in Langflow Frontend (#6950)
* fix errors on console jsx * ✨ (NodeOutputfield/index.tsx): Refactor InspectButton component to use forwardRef for better performance and maintainability ♻️ (NodeOutputfield/index.tsx): Refactor AlertDropdown component to use forwardRef for better performance and maintainability * 📝 (dialog.tsx): Add VisuallyHidden component for accessibility and improve semantics in DialogContent component 🔧 (dialog.tsx): Update DialogContent component to conditionally include VisuallyHidden component for DialogTitle 🔧 (dialog.tsx): Export VisuallyHidden component from dialog.tsx ♻️ (textAnimation.tsx): Refactor TextEffectPerChar component to use TextEffect component with per="char" and as="span" properties for consistency * ✨ (CustomEdges/index.tsx): Destructure props to extract specific properties and pass the rest as domSafeProps for cleaner code ♻️ (singleAlertComponent/index.tsx): Remove unnecessary aria-hidden attribute from IconComponent to improve accessibility and reduce redundancy * ✨ (index.tsx): introduce ShortUniqueId library to generate unique keys for alert items in the dropdown list * ✨ (frontend): add support for displaying a list of items in the NoticeAlert component 📝 (frontend): update NoticeAlertType interface to include a list property for displaying multiple items in the alert * ✨ (styleUtils.ts): introduce new icon PencilRuler to the list of nodeIconsLucide for use in the frontend styling utilities.
This commit is contained in:
parent
47753d37d3
commit
08f886f507
11 changed files with 243 additions and 197 deletions
|
|
@ -56,11 +56,26 @@ export function DefaultEdge({
|
||||||
targetY: targetYNew,
|
targetY: targetYNew,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
animated,
|
||||||
|
selectable,
|
||||||
|
deletable,
|
||||||
|
sourcePosition,
|
||||||
|
targetPosition,
|
||||||
|
pathOptions,
|
||||||
|
selected,
|
||||||
|
...domSafeProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseEdge
|
<BaseEdge
|
||||||
path={targetHandleObject.output_types ? edgePathLoop : edgePath}
|
path={targetHandleObject.output_types ? edgePathLoop : edgePath}
|
||||||
strokeDasharray={targetHandleObject.output_types ? "5 5" : "0"}
|
strokeDasharray={targetHandleObject.output_types ? "5 5" : "0"}
|
||||||
{...props}
|
{...domSafeProps}
|
||||||
|
data-animated={animated ? "true" : "false"}
|
||||||
|
data-selectable={selectable ? "true" : "false"}
|
||||||
|
data-deletable={deletable ? "true" : "false"}
|
||||||
|
data-selected={selected ? "true" : "false"}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,14 @@ import { ICON_STROKE_WIDTH } from "@/constants/constants";
|
||||||
import { targetHandleType } from "@/types/flow";
|
import { targetHandleType } from "@/types/flow";
|
||||||
import { useUpdateNodeInternals } from "@xyflow/react";
|
import { useUpdateNodeInternals } from "@xyflow/react";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
import { memo, useCallback, useEffect, useMemo, useRef } from "react";
|
import {
|
||||||
|
forwardRef,
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
} from "react";
|
||||||
import ForwardedIconComponent, {
|
import ForwardedIconComponent, {
|
||||||
default as IconComponent,
|
default as IconComponent,
|
||||||
} from "../../../../components/common/genericIconComponent";
|
} from "../../../../components/common/genericIconComponent";
|
||||||
|
|
@ -96,7 +103,9 @@ const HideShowButton = memo(
|
||||||
);
|
);
|
||||||
|
|
||||||
const InspectButton = memo(
|
const InspectButton = memo(
|
||||||
({
|
forwardRef(
|
||||||
|
(
|
||||||
|
{
|
||||||
disabled,
|
disabled,
|
||||||
displayOutputPreview,
|
displayOutputPreview,
|
||||||
unknownOutput,
|
unknownOutput,
|
||||||
|
|
@ -114,8 +123,11 @@ const InspectButton = memo(
|
||||||
title: string;
|
title: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
id: string;
|
id: string;
|
||||||
}) => (
|
},
|
||||||
|
ref: React.ForwardedRef<HTMLButtonElement>,
|
||||||
|
) => (
|
||||||
<Button
|
<Button
|
||||||
|
ref={ref}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
data-testid={`output-inspection-${title.toLowerCase()}-${id.toLowerCase()}`}
|
data-testid={`output-inspection-${title.toLowerCase()}-${id.toLowerCase()}`}
|
||||||
unstyled
|
unstyled
|
||||||
|
|
@ -138,7 +150,9 @@ const InspectButton = memo(
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
InspectButton.displayName = "InspectButton";
|
||||||
|
|
||||||
const MemoizedOutputComponent = memo(OutputComponent);
|
const MemoizedOutputComponent = memo(OutputComponent);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,7 @@ export default function SingleAlert({
|
||||||
key={dropItem.id}
|
key={dropItem.id}
|
||||||
>
|
>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<IconComponent
|
<IconComponent name="XCircle" className="h-5 w-5 text-status-red" />
|
||||||
name="XCircle"
|
|
||||||
className="h-5 w-5 text-status-red"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3">
|
<div className="ml-3">
|
||||||
<h3 className="text-sm font-medium text-error-foreground word-break-break-word">
|
<h3 className="text-sm font-medium text-error-foreground word-break-break-word">
|
||||||
|
|
@ -80,11 +76,7 @@ export default function SingleAlert({
|
||||||
className="inline-flex rounded-md p-1.5 text-status-red"
|
className="inline-flex rounded-md p-1.5 text-status-red"
|
||||||
>
|
>
|
||||||
<span className="sr-only">Dismiss</span>
|
<span className="sr-only">Dismiss</span>
|
||||||
<IconComponent
|
<IconComponent name="X" className="h-4 w-4 text-error-foreground" />
|
||||||
name="X"
|
|
||||||
className="h-4 w-4 text-error-foreground"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -95,11 +87,7 @@ export default function SingleAlert({
|
||||||
key={dropItem.id}
|
key={dropItem.id}
|
||||||
>
|
>
|
||||||
<div className="flex-shrink-0 cursor-help">
|
<div className="flex-shrink-0 cursor-help">
|
||||||
<IconComponent
|
<IconComponent name="Info" className="h-5 w-5 text-status-blue" />
|
||||||
name="Info"
|
|
||||||
className="h-5 w-5 text-status-blue"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3 flex-1 md:flex md:justify-between">
|
<div className="ml-3 flex-1 md:flex md:justify-between">
|
||||||
<p className="text-sm font-medium text-info-foreground">
|
<p className="text-sm font-medium text-info-foreground">
|
||||||
|
|
@ -131,11 +119,7 @@ export default function SingleAlert({
|
||||||
className="inline-flex rounded-md p-1.5 text-info-foreground"
|
className="inline-flex rounded-md p-1.5 text-info-foreground"
|
||||||
>
|
>
|
||||||
<span className="sr-only">Dismiss</span>
|
<span className="sr-only">Dismiss</span>
|
||||||
<IconComponent
|
<IconComponent name="X" className="h-4 w-4 text-info-foreground" />
|
||||||
name="X"
|
|
||||||
className="h-4 w-4 text-info-foreground"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -149,7 +133,6 @@ export default function SingleAlert({
|
||||||
<IconComponent
|
<IconComponent
|
||||||
name="CheckCircle2"
|
name="CheckCircle2"
|
||||||
className="h-5 w-5 text-status-green"
|
className="h-5 w-5 text-status-green"
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3">
|
<div className="ml-3">
|
||||||
|
|
@ -173,7 +156,6 @@ export default function SingleAlert({
|
||||||
<IconComponent
|
<IconComponent
|
||||||
name="X"
|
name="X"
|
||||||
className="h-4 w-4 text-success-foreground"
|
className="h-4 w-4 text-success-foreground"
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Cross2Icon } from "@radix-ui/react-icons";
|
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||||
import { useEffect, useState } from "react";
|
import { forwardRef, useEffect, useState } from "react";
|
||||||
|
import ShortUniqueId from "short-unique-id";
|
||||||
import IconComponent from "../../components/common/genericIconComponent";
|
import IconComponent from "../../components/common/genericIconComponent";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
|
|
@ -11,11 +12,8 @@ import useAlertStore from "../../stores/alertStore";
|
||||||
import { AlertDropdownType } from "../../types/alerts";
|
import { AlertDropdownType } from "../../types/alerts";
|
||||||
import SingleAlert from "./components/singleAlertComponent";
|
import SingleAlert from "./components/singleAlertComponent";
|
||||||
|
|
||||||
export default function AlertDropdown({
|
const AlertDropdown = forwardRef<HTMLDivElement, AlertDropdownType>(
|
||||||
children,
|
function AlertDropdown({ children, notificationRef, onClose }, ref) {
|
||||||
notificationRef,
|
|
||||||
onClose,
|
|
||||||
}: AlertDropdownType): JSX.Element {
|
|
||||||
const notificationList = useAlertStore((state) => state.notificationList);
|
const notificationList = useAlertStore((state) => state.notificationList);
|
||||||
const clearNotificationList = useAlertStore(
|
const clearNotificationList = useAlertStore(
|
||||||
(state) => state.clearNotificationList,
|
(state) => state.clearNotificationList,
|
||||||
|
|
@ -33,7 +31,9 @@ export default function AlertDropdown({
|
||||||
if (!open) {
|
if (!open) {
|
||||||
onClose?.();
|
onClose?.();
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open, onClose]);
|
||||||
|
|
||||||
|
const uid = new ShortUniqueId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
|
|
@ -48,11 +48,14 @@ export default function AlertDropdown({
|
||||||
>
|
>
|
||||||
<PopoverTrigger asChild>{children}</PopoverTrigger>
|
<PopoverTrigger asChild>{children}</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
ref={notificationRef}
|
ref={ref}
|
||||||
data-testid="notification-dropdown-content"
|
data-testid="notification-dropdown-content"
|
||||||
className="noflow nowheel nopan nodelete nodrag z-10 flex h-[500px] w-[500px] flex-col"
|
className="noflow nowheel nopan nodelete nodrag z-10 flex h-[500px] w-[500px] flex-col"
|
||||||
>
|
>
|
||||||
<div className="text-md flex flex-row justify-between pl-3 font-medium text-foreground">
|
<div
|
||||||
|
ref={notificationRef}
|
||||||
|
className="text-md flex flex-row justify-between pl-3 font-medium text-foreground"
|
||||||
|
>
|
||||||
Notifications
|
Notifications
|
||||||
<div className="flex gap-3 pr-3">
|
<div className="flex gap-3 pr-3">
|
||||||
<button
|
<button
|
||||||
|
|
@ -76,9 +79,9 @@ export default function AlertDropdown({
|
||||||
</div>
|
</div>
|
||||||
<div className="text-high-foreground mt-3 flex h-full w-full flex-col overflow-y-scroll scrollbar-hide">
|
<div className="text-high-foreground mt-3 flex h-full w-full flex-col overflow-y-scroll scrollbar-hide">
|
||||||
{notificationList.length !== 0 ? (
|
{notificationList.length !== 0 ? (
|
||||||
notificationList.map((alertItem, index) => (
|
notificationList.map((alertItem) => (
|
||||||
<SingleAlert
|
<SingleAlert
|
||||||
key={alertItem.id}
|
key={uid.randomUUID(10)}
|
||||||
dropItem={alertItem}
|
dropItem={alertItem}
|
||||||
removeAlert={removeFromNotificationList}
|
removeAlert={removeFromNotificationList}
|
||||||
/>
|
/>
|
||||||
|
|
@ -92,4 +95,7 @@ export default function AlertDropdown({
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default AlertDropdown;
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,35 @@
|
||||||
import { CustomLink } from "@/customization/components/custom-link";
|
import { CustomLink } from "@/customization/components/custom-link";
|
||||||
import { Transition } from "@headlessui/react";
|
import { Transition } from "@headlessui/react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import Markdown from "react-markdown";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
import IconComponent from "../../components/common/genericIconComponent";
|
import IconComponent from "../../components/common/genericIconComponent";
|
||||||
import { NoticeAlertType } from "../../types/alerts";
|
import { NoticeAlertType } from "../../types/alerts";
|
||||||
|
|
||||||
export default function NoticeAlert({
|
export default function NoticeAlert({
|
||||||
title,
|
title,
|
||||||
link,
|
list = [],
|
||||||
id,
|
id,
|
||||||
|
link,
|
||||||
removeAlert,
|
removeAlert,
|
||||||
}: NoticeAlertType): JSX.Element {
|
}: NoticeAlertType): JSX.Element {
|
||||||
const [show, setShow] = useState(true);
|
const [show, setShow] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeoutId = setTimeout(() => {
|
if (show) {
|
||||||
|
setTimeout(() => {
|
||||||
setShow(false);
|
setShow(false);
|
||||||
// Wait for the leave transition before calling removeAlert
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
removeAlert(id);
|
removeAlert(id);
|
||||||
}, 500); // match the duration of the leave transition
|
}, 500);
|
||||||
}, 5000); // auto-dismiss alert after 5 seconds
|
}, 5000);
|
||||||
|
}
|
||||||
return () => clearTimeout(timeoutId); // Cleanup timeout on component unmount or re-render
|
}, [id, removeAlert, show]);
|
||||||
}, [id, removeAlert]);
|
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
setShow(false);
|
setShow(false);
|
||||||
// Wait for the leave transition before calling removeAlert
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
removeAlert(id);
|
removeAlert(id);
|
||||||
}, 500); // Ensure the alert is removed after the animation
|
}, 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,34 @@ const DialogOverlay = React.forwardRef<
|
||||||
));
|
));
|
||||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
|
// Create a VisuallyHidden component for accessibility
|
||||||
|
const VisuallyHidden = React.forwardRef<
|
||||||
|
HTMLSpanElement,
|
||||||
|
React.HTMLAttributes<HTMLSpanElement>
|
||||||
|
>(({ children, ...props }, ref) => (
|
||||||
|
<span
|
||||||
|
ref={ref}
|
||||||
|
className="absolute h-px w-px overflow-hidden whitespace-nowrap border-0 p-0"
|
||||||
|
style={{ clip: "rect(0 0 0 0)", clipPath: "inset(50%)" }}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
));
|
||||||
|
VisuallyHidden.displayName = "VisuallyHidden";
|
||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
const DialogContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
||||||
>(({ className, children, ...props }, ref) => (
|
hideTitle?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, children, hideTitle = false, ...props }, ref) => {
|
||||||
|
// Check if DialogTitle is included in children
|
||||||
|
const hasDialogTitle = React.Children.toArray(children).some(
|
||||||
|
(child) => React.isValidElement(child) && child.type === DialogTitle,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
<DialogOverlay />
|
<DialogOverlay />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
|
|
@ -49,6 +73,11 @@ const DialogContent = React.forwardRef<
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
{!hasDialogTitle && (
|
||||||
|
<VisuallyHidden>
|
||||||
|
<DialogTitle>Dialog</DialogTitle>
|
||||||
|
</VisuallyHidden>
|
||||||
|
)}
|
||||||
{children}
|
{children}
|
||||||
<ShadTooltip
|
<ShadTooltip
|
||||||
styleClasses="z-50"
|
styleClasses="z-50"
|
||||||
|
|
@ -63,7 +92,8 @@ const DialogContent = React.forwardRef<
|
||||||
</ShadTooltip>
|
</ShadTooltip>
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
));
|
);
|
||||||
|
});
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
const DialogHeader = ({
|
const DialogHeader = ({
|
||||||
|
|
@ -126,4 +156,5 @@ export {
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
|
VisuallyHidden,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -111,11 +111,7 @@ const AnimationComponent: React.FC<{
|
||||||
{segment}
|
{segment}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
) : per === "word" ? (
|
) : per === "word" ? (
|
||||||
<motion.span
|
<motion.span variants={variants} className="inline-block whitespace-pre">
|
||||||
aria-hidden="true"
|
|
||||||
variants={variants}
|
|
||||||
className="inline-block whitespace-pre"
|
|
||||||
>
|
|
||||||
{segment}
|
{segment}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -123,7 +119,6 @@ const AnimationComponent: React.FC<{
|
||||||
{segment.split("").map((char, charIndex) => (
|
{segment.split("").map((char, charIndex) => (
|
||||||
<motion.span
|
<motion.span
|
||||||
key={`char-${charIndex}`}
|
key={`char-${charIndex}`}
|
||||||
aria-hidden="true"
|
|
||||||
variants={variants}
|
variants={variants}
|
||||||
className="inline-block whitespace-pre"
|
className="inline-block whitespace-pre"
|
||||||
>
|
>
|
||||||
|
|
@ -151,7 +146,7 @@ AnimationComponent.displayName = "AnimationComponent";
|
||||||
export function TextEffect({
|
export function TextEffect({
|
||||||
children,
|
children,
|
||||||
per = "word",
|
per = "word",
|
||||||
as = "p",
|
as = "span",
|
||||||
variants,
|
variants,
|
||||||
className,
|
className,
|
||||||
preset,
|
preset,
|
||||||
|
|
@ -224,7 +219,7 @@ export function TextEffect({
|
||||||
|
|
||||||
export function TextEffectPerChar({ children }: { children: string }) {
|
export function TextEffectPerChar({ children }: { children: string }) {
|
||||||
return (
|
return (
|
||||||
<TextEffect per="char" preset="fade">
|
<TextEffect per="char" preset="fade" as="span">
|
||||||
{children}
|
{children}
|
||||||
</TextEffect>
|
</TextEffect>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -9,6 +9,7 @@ export type NoticeAlertType = {
|
||||||
link?: string;
|
link?: string;
|
||||||
id: string;
|
id: string;
|
||||||
removeAlert: (id: string) => void;
|
removeAlert: (id: string) => void;
|
||||||
|
list?: Array<string>;
|
||||||
};
|
};
|
||||||
export type SuccessAlertType = {
|
export type SuccessAlertType = {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,7 @@ import {
|
||||||
Pen,
|
Pen,
|
||||||
Pencil,
|
Pencil,
|
||||||
PencilLine,
|
PencilLine,
|
||||||
|
PencilRuler,
|
||||||
PieChart,
|
PieChart,
|
||||||
Pin,
|
Pin,
|
||||||
Plane,
|
Plane,
|
||||||
|
|
@ -857,6 +858,7 @@ export const nodeIconsLucide: iconsType = {
|
||||||
UserMinus2,
|
UserMinus2,
|
||||||
UserPlus2,
|
UserPlus2,
|
||||||
Pencil,
|
Pencil,
|
||||||
|
PencilRuler,
|
||||||
ChevronsRight,
|
ChevronsRight,
|
||||||
ChevronsLeft,
|
ChevronsLeft,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue