🤲 feat(a11y): Initial a11y improvements, added linters, tests; fix: close sidebars in mobile view (#3536)

* chore: playwright setup update

* refactor: update ChatRoute component with accessible loading spinner with live region

* chore(Message): typing

* ci: first pass, a11y testing

* refactor: update lang attribute in index.html to "en-US"

* ci: jsx-a11y dev eslint plugin

* ci: jsx plugin

* fix: Exclude 'vite.config.ts' from TypeScript compilation for testing

* fix(a11y): Remove tabIndex from non-interactive element in MessagesView component

* fix(a11y):
- Visible, non-interactive elements with click handlers must have at least one keyboard listener.eslintjsx-a11y/click-events-have-key-events
- Avoid non-native interactive elements. If using native HTML is not possible, add an appropriate role and support for tabbing, mouse, keyboard, and touch inputs to an interactive content element.eslintjsx-a11y/no-static-element-interactions
chore: remove unused bookmarks panel
- fix some "Unexpected nullable boolean value in conditional" warnings

* fix(NewChat): a11y, nested button issue, add aria-label, remove implicit role

* fix(a11y):
- partially address #3515 with `main` landmark
other:
- eslint@typescript-eslint/strict-boolean-expressions

* chore(MenuButton): Use button element instead of div for accessibility

* chore: Update TitleButton to use button element for accessibility

* chore: Update TitleButton to use button element for accessibility

* refactor(ChatMenuItem): Improve focus accessibility and code readability

* chore(MenuButton): Update aria-label to dynamically include primaryText

* fix(a11y): SearchBar
- If a form control does not have a properly associated text label, the function or purpose of that form control may not be presented to screen reader users. Visible form labels also provide visible descriptions and larger clickable targets for form controls which placeholders do not.

* chore: remove duplicate SearchBar twcss

* fix(a11y):
- The edit and copy buttons that are visually hidden are exposed to Assistive technology and are announced to screen reader users.

* fix(a11y): visible focus outline

* fix(a11y): The button to select the LLM Model has the aria-haspopup and aria- expanded attributes which makes its role ambuguous and unclear. It functions like a combobox but doesn't fully support that interaction and also fucntions like a dialog but doesn't completely support that interaction either.

* fix(a11y): fix visible focus outline

* fix(a11y): Scroll to bottom button missing accessible name #3474

* fix(a11y): The page lacks any heading structure. There should be at least one H1 and other headings to help users understand the orgainzation of the page and the contents.

Note: h1 won't be correct here so made it h2

* fix(a11y): LLM controls aria-labels

* fix(a11y): There is no visible focus outline to the 'send message' button

* fix(a11y): fix visible focus outline for Fork button

* refactor(MessageRender): add focus ring to message cards, consolidate complex conditions, add logger for setting latest message, add tabindex for card

* fix: focus border color and fix set latest message card condition

* fix(a11y): Adequate contrast for MessageAudio buttton

* feat: Add GitHub Actions workflow for accessibility linting

* chore: Update GitHub Actions workflow for accessibility linting to include client/src/** path

* fix(Nav): navmask and accessibility

* fix: Update Nav component to handle potential undefined type in SearchContext

* fix(a11y): add focus visibility to attach files button #3475

* fix(a11y): discernible text for NewChat button

* fix(a11y): accessible landmark names, all page content in landmarks, ensures landmarks are unique #3514 #3515

* fix(Prompts): update isChatRoute prop to be required in List component

* fix(a11y): buttons must have discernible text
This commit is contained in:
Danny Avila 2024-08-04 20:39:52 -04:00 committed by GitHub
parent 433d8f832a
commit 11bfed7126
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 799 additions and 357 deletions

View file

@ -33,9 +33,10 @@ function AddMultiConvo({ className = '' }: { className?: string }) {
return (
<button
aria-label="Add multi-conversation"
onClick={clickHandler}
className={cn(
'group m-1.5 flex w-fit cursor-pointer items-center rounded text-sm hover:bg-border-medium focus-visible:bg-border-medium focus-visible:outline-0',
'group m-1.5 flex w-fit cursor-pointer items-center rounded text-sm hover:bg-border-medium focus-visible:bg-border-medium focus-visible:outline-offset-2',
className,
)}
>

View file

@ -78,6 +78,8 @@ export default function AudioRecorder({
<Tooltip>
<TooltipTrigger asChild>
<button
id="audio-recorder"
aria-label={localize('com_ui_use_micrphone')}
onClick={isListening ? handleStopRecording : handleStartRecording}
disabled={disabled}
className={cn(

View file

@ -45,8 +45,7 @@ const AttachFile = ({
<button
disabled={!!disabled}
type="button"
tabIndex={1}
className="btn relative p-0 text-black dark:text-white"
className="btn relative text-black focus:outline-none focus:ring-2 focus:ring-border-xheavy focus:ring-opacity-50 dark:text-white"
aria-label="Attach files"
style={{ padding: 0 }}
>

View file

@ -91,6 +91,7 @@ export default function HeaderOptions({
'hover:bg-gray-50 radix-state-open:bg-gray-50 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700',
)}
onClick={triggerAdvancedMode}
aria-label="Settings/parameters"
>
<Settings2 className="w-4 text-gray-600 dark:text-white" />
</Button>
@ -100,7 +101,7 @@ export default function HeaderOptions({
<OptionsPopover
visible={showPopover}
saveAsPreset={saveAsPreset}
presetsDisabled={!interfaceConfig?.presets}
presetsDisabled={!interfaceConfig.presets}
PopoverButtons={<PopoverButtons />}
closePopover={() => setShowPopover(false)}
>

View file

@ -22,9 +22,11 @@ const SubmitButton = React.memo(
<TooltipTrigger asChild>
<button
ref={ref}
aria-label={localize('com_nav_send_message')}
id="send-button"
disabled={props.disabled}
className={cn(
'absolute rounded-lg border border-black p-0.5 text-white transition-colors enabled:bg-black disabled:bg-black disabled:text-gray-400 disabled:opacity-10 dark:border-white dark:bg-white dark:disabled:bg-white',
'absolute rounded-lg border border-black p-0.5 text-white outline-offset-4 transition-colors enabled:bg-black disabled:bg-black disabled:text-gray-400 disabled:opacity-10 dark:border-white dark:bg-white dark:disabled:bg-white',
props.isRTL
? 'bottom-1.5 left-2 md:bottom-3 md:left-3'
: 'bottom-1.5 right-2 md:bottom-3 md:right-3',
@ -50,7 +52,7 @@ const SubmitButton = React.memo(
const SendButton = React.memo(
forwardRef((props: SendButtonProps, ref: React.ForwardedRef<HTMLButtonElement>) => {
const data = useWatch({ control: props.control });
return <SubmitButton ref={ref} disabled={props.disabled || !data?.text} isRTL={props.isRTL} />;
return <SubmitButton ref={ref} disabled={props.disabled || !data.text} isRTL={props.isRTL} />;
}),
);

View file

@ -31,10 +31,10 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint });
const isAssistant = isAssistantsEndpoint(endpoint);
const assistant = isAssistant && assistantMap?.[endpoint]?.[assistant_id ?? ''];
const assistantName = (assistant && assistant?.name) || '';
const assistantDesc = (assistant && assistant?.description) || '';
const avatar = (assistant && (assistant?.metadata?.avatar as string)) || '';
const assistant = isAssistant && assistantMap[endpoint][assistant_id ?? ''];
const assistantName = (assistant && assistant.name) || '';
const assistantDesc = (assistant && assistant.description) || '';
const avatar = (assistant && (assistant.metadata?.avatar as string)) || '';
const containerClassName =
'shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black';
@ -79,11 +79,11 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
</div> */}
</div>
) : (
<div className="mb-5 max-w-[75vh] px-12 text-center text-lg font-medium dark:text-white md:px-0 md:text-2xl">
<h2 className="mb-5 max-w-[75vh] px-12 text-center text-lg font-medium dark:text-white md:px-0 md:text-2xl">
{isAssistant
? conversation?.greeting ?? localize('com_nav_welcome_assistant')
: conversation?.greeting ?? localize('com_nav_welcome_message')}
</div>
</h2>
)}
</div>
</div>

View file

@ -86,9 +86,9 @@ const BookmarkMenu: FC = () => {
<Trigger asChild>
<button
className={cn(
'pointer-cursor relative flex flex-col rounded-md border border-gray-100 bg-white text-left focus:outline-none focus:ring-0 focus:ring-offset-0 dark:border-gray-700 dark:bg-gray-800 sm:text-sm',
'pointer-cursor relative flex flex-col rounded-md border border-gray-100 bg-white text-left focus:outline-none focus:ring-0 dark:border-gray-700 dark:bg-gray-800 sm:text-sm',
'hover:bg-gray-50 radix-state-open:bg-gray-50 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700',
'z-50 flex h-[40px] min-w-4 flex-none items-center justify-center px-3 focus:ring-0 focus:ring-offset-0',
'z-50 flex h-[40px] min-w-4 flex-none items-center justify-center px-3 focus:outline-offset-2 focus:ring-0 focus-visible:ring-2 focus-visible:ring-gray-500',
)}
title={localize('com_ui_bookmarks')}
>

View file

@ -97,10 +97,13 @@ const MenuItem: FC<MenuItemProps> = ({
<div
role="menuitem"
className={cn(
'group m-1.5 flex max-h-[40px] cursor-pointer gap-2 rounded px-5 py-2.5 !pr-3 text-sm !opacity-100 hover:bg-black/5 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-gray-600',
'focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900',
'group m-1.5 flex max-h-[40px] cursor-pointer gap-2 rounded px-5 py-2.5 !pr-3 text-sm !opacity-100',
'hover:bg-black/5 dark:hover:bg-gray-600',
'radix-disabled:pointer-events-none radix-disabled:opacity-50',
'focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2',
'dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900',
)}
tabIndex={1}
tabIndex={0}
{...rest}
onClick={() => onSelectEndpoint(endpoint)}
onKeyDown={(e) => {

View file

@ -22,14 +22,15 @@ export default function MenuButton({
const localize = useLocalize();
return (
<Trigger asChild>
<div
<button
className={cn(
'group flex cursor-pointer items-center gap-1 rounded-xl px-3 py-2 text-lg font-medium hover:bg-gray-50 radix-state-open:bg-gray-50 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700',
className,
)}
// type="button"
type="button"
aria-label={`Select ${primaryText}`}
>
{selected && selected.showIconInHeader && (
{selected && selected.showIconInHeader === true && (
<SpecIcon currentSpec={selected} endpointsConfig={endpointsConfig} />
)}
<div className={textClassName}>
@ -51,7 +52,7 @@ export default function MenuButton({
strokeLinejoin="round"
/>
</svg>
</div>
</button>
</Trigger>
);
}

View file

@ -27,13 +27,14 @@ const PresetsMenu: FC = () => {
<Trigger asChild>
<button
className={cn(
'pointer-cursor relative flex flex-col rounded-md border border-gray-100 bg-white text-left focus:outline-none focus:ring-0 focus:ring-offset-0 dark:border-gray-700 dark:bg-gray-800 sm:text-sm',
'pointer-cursor relative flex flex-col rounded-md border border-gray-100 bg-white text-left focus:ring-0 focus:ring-offset-0 dark:border-gray-700 dark:bg-gray-800 sm:text-sm',
'hover:bg-gray-50 radix-state-open:bg-gray-50 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700',
'z-50 flex h-[40px] min-w-4 flex-none items-center justify-center px-3 focus:ring-0 focus:ring-offset-0',
'z-50 flex h-[40px] min-w-4 flex-none items-center justify-center px-3',
)}
id="presets-button"
data-testid="presets-button"
title={localize('com_endpoint_examples')}
aria-label={localize('com_endpoint_examples')}
>
<BookCopy className="icon-sm" id="presets-button" />
</button>

View file

@ -33,18 +33,28 @@ const MenuItem: FC<MenuItemProps> = ({
return (
<div
role="menuitem"
aria-label={`menu item for ${title} ${description}`}
data-testid="chat-menu-item"
className={cn(
'group m-1.5 flex cursor-pointer gap-2 rounded px-5 py-2.5 !pr-3 text-sm !opacity-100 hover:bg-black/5 focus:ring-0 radix-disabled:pointer-events-none radix-disabled:opacity-50 dark:hover:bg-gray-600 md:min-w-[240px]',
className ?? '',
className || '',
)}
tabIndex={-1}
tabIndex={0} // Change to 0 to make it focusable
onClick={onClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (onClick) {
onClick();
}
}
}}
{...rest}
>
<div className="flex grow items-center justify-between gap-2">
<div>
<div className={cn('flex items-center gap-1 ')}>
{icon && icon}
{icon != null ? icon : null}
<div className={cn('truncate', textClassName)}>
{title}
<div className="text-token-text-tertiary">{description}</div>

View file

@ -3,9 +3,10 @@ import { Trigger } from '@radix-ui/react-popover';
export default function TitleButton({ primaryText = '', secondaryText = '' }) {
return (
<Trigger asChild>
<div
<button
className="group flex cursor-pointer items-center gap-1 rounded-xl px-3 py-2 text-lg font-medium hover:bg-gray-50 radix-state-open:bg-gray-50 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700"
// type="button"
type="button"
aria-label={`Select ${primaryText}`}
>
<div>
{primaryText}{' '}
@ -26,7 +27,7 @@ export default function TitleButton({ primaryText = '', secondaryText = '' }) {
strokeLinejoin="round"
/>
</svg>
</div>
</button>
</Trigger>
);
}

View file

@ -77,7 +77,7 @@ export default function HoverButtons({
{isEditableEndpoint && (
<button
className={cn(
'hover-button rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:group-hover:visible md:group-[.final-completion]:visible',
'hover-button rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:opacity-100 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:group-hover:visible md:group-[.final-completion]:visible',
isCreatedByUser ? '' : 'active',
hideEditButton ? 'opacity-0' : '',
isEditing ? 'active text-gray-700 dark:text-gray-200' : '',
@ -93,7 +93,7 @@ export default function HoverButtons({
)}
<button
className={cn(
'ml-0 flex items-center gap-1.5 rounded-md p-1 text-xs text-gray-400 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:group-hover:visible md:group-[.final-completion]:visible',
'ml-0 flex items-center gap-1.5 rounded-md p-1 text-xs text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:opacity-100 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:group-hover:visible md:group-[.final-completion]:visible',
isSubmitting && isCreatedByUser ? 'md:opacity-0 md:group-hover:opacity-100' : '',
!isLast ? 'md:opacity-0 md:group-hover:opacity-100' : '',
)}
@ -108,7 +108,7 @@ export default function HoverButtons({
{regenerateEnabled ? (
<button
className={cn(
'hover-button active rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible md:group-[.final-completion]:visible',
'hover-button active rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:opacity-100 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible md:group-[.final-completion]:visible',
!isLast ? 'md:opacity-0 md:group-hover:opacity-100' : '',
)}
onClick={regenerate}
@ -131,7 +131,7 @@ export default function HoverButtons({
{continueSupported ? (
<button
className={cn(
'hover-button active rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible ',
'hover-button active rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:opacity-100 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible',
!isLast ? 'md:opacity-0 md:group-hover:opacity-100' : '',
)}
onClick={handleContinue}

View file

@ -49,7 +49,7 @@ export default function MessageAudio({ index, message, isLast }: THoverButtons)
return (
<>
<button
className="hover-button rounded-md p-1 pl-0 text-gray-400 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:group-hover:visible md:group-[.final-completion]:visible"
className="hover-button rounded-md p-1 pl-0 text-gray-500 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:group-hover:visible md:group-[.final-completion]:visible"
// onMouseDownCapture={() => {
// if (audioRef.current) {
// audioRef.current.muted = false;

View file

@ -33,7 +33,6 @@ export default function MessagesView({
<div
onScroll={debouncedHandleScroll}
ref={scrollableRef}
tabIndex={0}
style={{
height: '100%',
overflowY: 'auto',
@ -41,7 +40,7 @@ export default function MessagesView({
}}
>
<div className="flex flex-col pb-9 text-sm dark:bg-transparent">
{(_messagesTree && _messagesTree?.length == 0) || _messagesTree === null ? (
{(_messagesTree && _messagesTree.length == 0) || _messagesTree === null ? (
<div className="flex w-full items-center justify-center gap-1 bg-gray-50 p-3 text-sm text-gray-500 dark:border-gray-800/50 dark:bg-gray-800 dark:text-gray-300">
Nothing found
</div>

View file

@ -9,7 +9,7 @@ import HoverButtons from '~/components/Chat/Messages/HoverButtons';
import Icon from '~/components/Chat/Messages/MessageIcon';
import { Plugin } from '~/components/Messages/Content';
import SubRow from '~/components/Chat/Messages/SubRow';
import { cn } from '~/utils';
import { cn, logger } from '~/utils';
type MessageRenderProps = {
message?: TMessage;
@ -65,28 +65,40 @@ const MessageRender = React.memo(
return null;
}
const isLatestCard =
isCard && !isSubmittingFamily && msg.messageId === latestMessage?.messageId;
const isLatestMessage = msg.messageId === latestMessage?.messageId;
const showCardRender = isLast && !(isSubmittingFamily === true) && isCard === true;
const isLatestCard = isCard === true && !(isSubmittingFamily === true) && isLatestMessage;
const clickHandler =
isLast && isCard && !isSubmittingFamily && msg.messageId !== latestMessage?.messageId
? () => setLatestMessage(msg)
showCardRender && !isLatestMessage
? () => {
logger.log(`Message Card click: Setting ${msg.messageId} as latest message`);
logger.dir(msg);
setLatestMessage(msg);
}
: undefined;
return (
<div
aria-label={`message-${msg.depth}-${msg.messageId}`}
className={cn(
'final-completion group mx-auto flex flex-1 gap-3 text-base',
isCard
isCard === true
? 'relative w-full gap-1 rounded-lg border border-border-medium bg-surface-primary-alt p-2 md:w-1/2 md:gap-3 md:p-4'
: 'md:max-w-3xl md:px-5 lg:max-w-[40rem] lg:px-1 xl:max-w-[48rem] xl:px-5',
isLatestCard ? 'bg-surface-secondary' : '',
isLast && !isSubmittingFamily && isCard
? 'cursor-pointer transition-colors duration-300'
: '',
isLatestCard === true ? 'bg-surface-secondary' : '',
showCardRender ? 'cursor-pointer transition-colors duration-300' : '',
'focus:outline-none focus:ring-2 focus:ring-border-xheavy',
)}
onClick={clickHandler}
onKeyDown={(e) => {
if ((e.key === 'Enter' || e.key === ' ') && clickHandler) {
clickHandler();
}
}}
role={showCardRender ? 'button' : undefined}
tabIndex={showCardRender ? 0 : undefined}
>
{isLatestCard && (
{isLatestCard === true && (
<div className="absolute right-0 top-0 m-2 h-3 w-3 rounded-full bg-text-primary"></div>
)}
<div className="relative flex flex-shrink-0 flex-col items-end">
@ -99,12 +111,12 @@ const MessageRender = React.memo(
</div>
</div>
<div
className={cn('relative flex w-11/12 flex-col', msg?.isCreatedByUser ? '' : 'agent-turn')}
className={cn('relative flex w-11/12 flex-col', msg.isCreatedByUser ? '' : 'agent-turn')}
>
<div className="select-none font-semibold">{messageLabel}</div>
<h2 className="select-none font-semibold">{messageLabel}</h2>
<div className="flex-col gap-1 md:gap-3">
<div className="flex max-w-full flex-grow flex-col gap-0">
{msg?.plugin && <Plugin plugin={msg?.plugin} />}
{msg.plugin && <Plugin plugin={msg.plugin} />}
<MessageContent
ask={ask}
edit={edit}
@ -121,7 +133,7 @@ const MessageRender = React.memo(
/>
</div>
</div>
{!msg?.children?.length && (isSubmittingFamily || isSubmitting) ? (
{!msg.children?.length && (isSubmittingFamily === true || isSubmitting) ? (
<PlaceholderRow isCard={isCard} />
) : (
<SubRow classes="text-xs">

View file

@ -62,24 +62,24 @@ export default function Presentation({
const defaultLayout = useMemo(() => {
const resizableLayout = localStorage.getItem('react-resizable-panels:layout');
return resizableLayout ? JSON.parse(resizableLayout) : undefined;
return typeof resizableLayout === 'string' ? JSON.parse(resizableLayout) : undefined;
}, []);
const defaultCollapsed = useMemo(() => {
const collapsedPanels = localStorage.getItem('react-resizable-panels:collapsed');
return collapsedPanels ? JSON.parse(collapsedPanels) : true;
return typeof collapsedPanels === 'string' ? JSON.parse(collapsedPanels) : true;
}, []);
const fullCollapse = useMemo(() => localStorage.getItem('fullPanelCollapse') === 'true', []);
const layout = () => (
<div className="transition-width relative flex h-full w-full flex-1 flex-col items-stretch overflow-hidden bg-white pt-0 dark:bg-gray-800">
<div className="flex h-full flex-col" role="presentation" tabIndex={0}>
<div className="flex h-full flex-col" role="presentation">
{children}
{isActive && <DragDropOverlay />}
</div>
</div>
);
if (useSidePanel && !hideSidePanel && interfaceConfig.sidePanel) {
if (useSidePanel && !hideSidePanel && interfaceConfig.sidePanel === true) {
return (
<div
ref={drop}
@ -90,10 +90,10 @@ export default function Presentation({
defaultCollapsed={defaultCollapsed}
fullPanelCollapse={fullCollapse}
>
<div className="flex h-full flex-col" role="presentation" tabIndex={0}>
<main className="flex h-full flex-col" role="main">
{children}
{isActive && <DragDropOverlay />}
</div>
</main>
</SidePanel>
</div>
);