mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-04-01 05:17:19 +02:00
* fix: remove side panel elements from screen reader when hidden There's both left & right side panels; elements of both of them are hidden when dismissed. However, currently they are being hidden by using classes to hide their UI (such as making the sidebar zero width). That works for visually dismissing these elements, but they can still be viewed by a screen reader (using the tab key to jump between interactable elements). That can be a rather confusing experience for anyone visually impaired (such as duplicate buttons, or buttons that do nothing). -------- I've changed it so hidden elements are fully removed from the render. This prevents them from being interactable via keyboard. I leveraged Motion to duplicate the animations as they happened before. I subtly cleaned up the animations while I was at it. * Implemented reasonable suggestions from Copilot review
164 lines
5.5 KiB
TypeScript
164 lines
5.5 KiB
TypeScript
import { useState, useRef, useCallback, useEffect, useMemo, memo } from 'react';
|
|
import throttle from 'lodash/throttle';
|
|
import { useRecoilValue } from 'recoil';
|
|
import { getConfigDefaults } from 'librechat-data-provider';
|
|
import { ResizablePanel, ResizablePanelGroup, useMediaQuery } from '@librechat/client';
|
|
import type { ImperativePanelHandle } from 'react-resizable-panels';
|
|
import { useGetStartupConfig } from '~/data-provider';
|
|
import ArtifactsPanel from './ArtifactsPanel';
|
|
import { normalizeLayout } from '~/utils';
|
|
import SidePanel from './SidePanel';
|
|
import store from '~/store';
|
|
|
|
interface SidePanelProps {
|
|
defaultLayout?: number[] | undefined;
|
|
defaultCollapsed?: boolean;
|
|
navCollapsedSize?: number;
|
|
fullPanelCollapse?: boolean;
|
|
artifacts?: React.ReactNode;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
const defaultMinSize = 20;
|
|
const defaultInterface = getConfigDefaults().interface;
|
|
|
|
const SidePanelGroup = memo(
|
|
({
|
|
defaultLayout = [97, 3],
|
|
defaultCollapsed = false,
|
|
fullPanelCollapse = false,
|
|
navCollapsedSize = 3,
|
|
artifacts,
|
|
children,
|
|
}: SidePanelProps) => {
|
|
const { data: startupConfig } = useGetStartupConfig();
|
|
const interfaceConfig = useMemo(
|
|
() => startupConfig?.interface ?? defaultInterface,
|
|
[startupConfig],
|
|
);
|
|
|
|
const panelRef = useRef<ImperativePanelHandle>(null);
|
|
const [minSize, setMinSize] = useState(defaultMinSize);
|
|
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
|
|
const [fullCollapse, setFullCollapse] = useState(fullPanelCollapse);
|
|
const [collapsedSize, setCollapsedSize] = useState(navCollapsedSize);
|
|
const [shouldRenderArtifacts, setShouldRenderArtifacts] = useState(artifacts != null);
|
|
|
|
const isSmallScreen = useMediaQuery('(max-width: 767px)');
|
|
const hideSidePanel = useRecoilValue(store.hideSidePanel);
|
|
|
|
const calculateLayout = useCallback(() => {
|
|
if (artifacts == null) {
|
|
const navSize = defaultLayout.length === 2 ? defaultLayout[1] : defaultLayout[2];
|
|
return [100 - navSize, navSize];
|
|
} else {
|
|
const navSize = 0;
|
|
const remainingSpace = 100 - navSize;
|
|
const newMainSize = Math.floor(remainingSpace / 2);
|
|
const artifactsSize = remainingSpace - newMainSize;
|
|
return [newMainSize, artifactsSize, navSize];
|
|
}
|
|
}, [artifacts, defaultLayout]);
|
|
|
|
const currentLayout = useMemo(() => normalizeLayout(calculateLayout()), [calculateLayout]);
|
|
|
|
const throttledSaveLayout = useMemo(
|
|
() =>
|
|
throttle((sizes: number[]) => {
|
|
const normalizedSizes = normalizeLayout(sizes);
|
|
localStorage.setItem('react-resizable-panels:layout', JSON.stringify(normalizedSizes));
|
|
}, 350),
|
|
[],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (isSmallScreen) {
|
|
setIsCollapsed(true);
|
|
setCollapsedSize(0);
|
|
setMinSize(defaultMinSize);
|
|
setFullCollapse(true);
|
|
localStorage.setItem('fullPanelCollapse', 'true');
|
|
panelRef.current?.collapse();
|
|
return;
|
|
} else {
|
|
setIsCollapsed(defaultCollapsed);
|
|
setCollapsedSize(navCollapsedSize);
|
|
setMinSize(defaultMinSize);
|
|
}
|
|
}, [isSmallScreen, defaultCollapsed, navCollapsedSize, fullPanelCollapse]);
|
|
|
|
const minSizeMain = useMemo(() => (artifacts != null ? 15 : 30), [artifacts]);
|
|
|
|
/** Memoized close button handler to prevent re-creating it */
|
|
const handleClosePanel = useCallback(() => {
|
|
setIsCollapsed(() => {
|
|
localStorage.setItem('fullPanelCollapse', 'true');
|
|
setFullCollapse(true);
|
|
setCollapsedSize(0);
|
|
setMinSize(0);
|
|
return false;
|
|
});
|
|
panelRef.current?.collapse();
|
|
}, []);
|
|
|
|
return (
|
|
<>
|
|
<ResizablePanelGroup
|
|
direction="horizontal"
|
|
onLayout={(sizes) => throttledSaveLayout(sizes)}
|
|
className="relative h-full w-full flex-1 overflow-auto bg-presentation"
|
|
>
|
|
<ResizablePanel
|
|
defaultSize={currentLayout[0]}
|
|
minSize={minSizeMain}
|
|
order={1}
|
|
id="messages-view"
|
|
>
|
|
{children}
|
|
</ResizablePanel>
|
|
|
|
{!isSmallScreen && (
|
|
<ArtifactsPanel
|
|
artifacts={artifacts}
|
|
currentLayout={currentLayout}
|
|
minSizeMain={minSizeMain}
|
|
shouldRender={shouldRenderArtifacts}
|
|
onRenderChange={setShouldRenderArtifacts}
|
|
/>
|
|
)}
|
|
|
|
{!hideSidePanel && interfaceConfig.sidePanel === true && (
|
|
<SidePanel
|
|
panelRef={panelRef}
|
|
minSize={minSize}
|
|
setMinSize={setMinSize}
|
|
isCollapsed={isCollapsed}
|
|
setIsCollapsed={setIsCollapsed}
|
|
collapsedSize={collapsedSize}
|
|
setCollapsedSize={setCollapsedSize}
|
|
fullCollapse={fullCollapse}
|
|
setFullCollapse={setFullCollapse}
|
|
interfaceConfig={interfaceConfig}
|
|
hasArtifacts={shouldRenderArtifacts}
|
|
defaultSize={currentLayout[currentLayout.length - 1]}
|
|
/>
|
|
)}
|
|
</ResizablePanelGroup>
|
|
{artifacts != null && isSmallScreen && (
|
|
<div className="fixed inset-0 z-[100]">{artifacts}</div>
|
|
)}
|
|
{!hideSidePanel && interfaceConfig.sidePanel === true && (
|
|
<button
|
|
aria-label="Close right side panel"
|
|
className={`nav-mask ${!isCollapsed ? 'active' : ''}`}
|
|
onClick={handleClosePanel}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
},
|
|
);
|
|
|
|
SidePanelGroup.displayName = 'SidePanelGroup';
|
|
|
|
export default SidePanelGroup;
|