Update Drawers to do a proper focus trap (#1290)

Improves user reported issues that clicking outside the modal would click elements unexpectedly, also improves accessibility (keyboard focus trap & esc to exit)

Fixes HDX-2642
This commit is contained in:
Brandon Pereira 2025-10-27 15:47:55 +01:00 committed by GitHub
parent ff86d40006
commit bb3539dd5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 136 additions and 118 deletions

View file

@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---
improve drawer a11y

View file

@ -84,7 +84,6 @@
"react-hotkeys-hook": "^4.3.7",
"react-json-tree": "^0.17.0",
"react-markdown": "^8.0.4",
"react-modern-drawer": "^1.2.0",
"react-papaparse": "^4.4.0",
"react-query": "^3.39.3",
"react-select": "^5.7.0",

View file

@ -51,8 +51,6 @@ import { parseTimeQuery, useTimeQuery } from './timeQuery';
import { KubePhase } from './types';
import { formatNumber, formatUptime } from './utils';
import 'react-modern-drawer/dist/index.css';
const makeId = () => Math.floor(100000000 * Math.random()).toString(36);
const getKubePhaseNumber = (phase: string) => {

View file

@ -1,6 +1,5 @@
import * as React from 'react';
import Link from 'next/link';
import Drawer from 'react-modern-drawer';
import { StringParam, useQueryParam, withDefault } from 'use-query-params';
import { tcFromSource } from '@hyperdx/common-utils/dist/metadata';
import { TSource } from '@hyperdx/common-utils/dist/types';
@ -9,6 +8,7 @@ import {
Badge,
Box,
Card,
Drawer,
Flex,
Grid,
ScrollArea,
@ -334,14 +334,17 @@ export default function NamespaceDetailsSidePanel({
return (
<Drawer
enableOverlay
overlayOpacity={0.1}
duration={0}
open={!!namespaceName}
opened={!!namespaceName}
onClose={handleClose}
direction="right"
size={'80vw'}
position="right"
size="80vw"
withCloseButton={false}
zIndex={drawerZIndex}
styles={{
body: {
padding: 0,
},
}}
>
<ZIndexContext.Provider value={drawerZIndex}>
<div className={styles.panel}>

View file

@ -1,6 +1,5 @@
import * as React from 'react';
import Link from 'next/link';
import Drawer from 'react-modern-drawer';
import { StringParam, useQueryParam, withDefault } from 'use-query-params';
import { tcFromSource } from '@hyperdx/common-utils/dist/metadata';
import {
@ -12,6 +11,7 @@ import {
Badge,
Box,
Card,
Drawer,
Flex,
Grid,
ScrollArea,
@ -350,14 +350,17 @@ export default function NodeDetailsSidePanel({
return (
<Drawer
enableOverlay
overlayOpacity={0.1}
duration={0}
open={!!nodeName}
opened={!!nodeName}
onClose={handleClose}
direction="right"
size={'80vw'}
position="right"
size="80vw"
withCloseButton={false}
zIndex={drawerZIndex}
styles={{
body: {
padding: 0,
},
}}
>
<ZIndexContext.Provider value={drawerZIndex}>
<div className={styles.panel} data-testid="k8s-node-details-panel">

View file

@ -1,6 +1,5 @@
import * as React from 'react';
import Link from 'next/link';
import Drawer from 'react-modern-drawer';
import { StringParam, useQueryParam, withDefault } from 'use-query-params';
import { tcFromSource } from '@hyperdx/common-utils/dist/metadata';
import { TSource } from '@hyperdx/common-utils/dist/types';
@ -8,6 +7,7 @@ import {
Anchor,
Box,
Card,
Drawer,
Flex,
Grid,
ScrollArea,
@ -342,14 +342,18 @@ export default function PodDetailsSidePanel({
return (
<Drawer
enableOverlay={rowId == null}
overlayOpacity={0.1}
duration={0}
open={!!podName}
opened={!!podName}
onClose={handleClose}
direction="right"
position="right"
size={isNested ? '70vw' : '80vw'}
withCloseButton={false}
zIndex={drawerZIndex}
styles={{
body: {
padding: 0,
height: '100vh',
},
}}
>
<ZIndexContext.Provider value={drawerZIndex}>
<div className={styles.panel} data-testid="k8s-pod-details-panel">

View file

@ -2,13 +2,13 @@ import { useState } from 'react';
import { Button } from 'react-bootstrap';
import CopyToClipboard from 'react-copy-to-clipboard';
import { useHotkeys } from 'react-hotkeys-hook';
import Drawer from 'react-modern-drawer';
import {
DateRange,
SearchCondition,
SearchConditionLanguage,
TSource,
} from '@hyperdx/common-utils/dist/types';
import { Drawer } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { Session } from './sessions';
@ -16,8 +16,6 @@ import SessionSubpanel from './SessionSubpanel';
import { formatDistanceToNowStrictShort } from './utils';
import { ZIndexContext } from './zIndex';
import 'react-modern-drawer/dist/index.css';
export default function SessionSidePanel({
traceSource,
sessionSource,
@ -74,20 +72,24 @@ export default function SessionSidePanel({
return (
<Drawer
customIdSuffix={`session-side-panel-${sessionId}`}
duration={0}
overlayOpacity={0.5}
open={sessionId != null}
opened={sessionId != null}
onClose={() => {
if (!subDrawerOpen) {
onClose();
}
}}
direction="right"
size={'82vw'}
style={{ background: '#0F1216' }}
className="border-start border-dark"
position="right"
size="82vw"
withCloseButton={false}
zIndex={zIndex}
styles={{
body: {
padding: 0,
background: '#0F1216',
height: '100vh',
},
}}
className="border-start border-dark"
>
<ZIndexContext.Provider value={zIndex}>
<div className="d-flex flex-column h-100">

View file

@ -12,10 +12,9 @@ import { isString } from 'lodash';
import { parseAsStringEnum, useQueryState } from 'nuqs';
import { ErrorBoundary } from 'react-error-boundary';
import { useHotkeys } from 'react-hotkeys-hook';
import Drawer from 'react-modern-drawer';
import { TSource } from '@hyperdx/common-utils/dist/types';
import { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types';
import { Box, OptionalPortal, Stack } from '@mantine/core';
import { Box, Drawer, Stack } from '@mantine/core';
import { useClickOutside } from '@mantine/hooks';
import DBRowSidePanelHeader, {
@ -36,7 +35,6 @@ import { RowOverviewPanel } from './DBRowOverviewPanel';
import { DBSessionPanel, useSessionId } from './DBSessionPanel';
import DBTracePanel from './DBTracePanel';
import 'react-modern-drawer/dist/index.css';
import styles from '@/../styles/LogSidePanel.module.scss';
export type RowSidePanelContextProps = {
@ -504,52 +502,57 @@ export default function DBRowSidePanelErrorBoundary({
}, ['mouseup', 'touchend']);
return (
<OptionalPortal withinPortal={!isNestedPanel}>
<Drawer
data-testid="row-side-panel"
customIdSuffix={`log-side-panel-${rowId}`}
duration={300}
open={rowId != null}
onClose={() => {
if (!subDrawerOpen) {
_onClose();
}
}}
direction="right"
size={`${width}vw`}
zIndex={drawerZIndex}
enableOverlay={subDrawerOpen}
>
<ZIndexContext.Provider value={drawerZIndex}>
<div className={styles.panel} ref={drawerRef}>
<Box className={styles.panelDragBar} onMouseDown={startResize} />
<Drawer
opened={rowId != null}
withCloseButton={false}
withinPortal={!isNestedPanel}
onClose={() => {
if (!subDrawerOpen) {
_onClose();
}
}}
position="right"
size={`${width}vw`}
styles={{
body: {
padding: '0',
height: '100vh',
},
}}
zIndex={drawerZIndex}
>
<ZIndexContext.Provider value={drawerZIndex}>
<div
className={styles.panel}
ref={drawerRef}
data-testid="row-side-panel"
>
<Box className={styles.panelDragBar} onMouseDown={startResize} />
<ErrorBoundary
fallbackRender={error => (
<Stack>
<div className="text-danger px-2 py-1 m-2 fs-7 font-monospace bg-danger-transparent p-4">
An error occurred while rendering this event.
</div>
<ErrorBoundary
fallbackRender={error => (
<Stack>
<div className="text-danger px-2 py-1 m-2 fs-7 font-monospace bg-danger-transparent p-4">
An error occurred while rendering this event.
</div>
<div className="px-2 py-1 m-2 fs-7 font-monospace bg-dark-grey p-4">
{error?.error?.message}
</div>
</Stack>
)}
>
<DBRowSidePanel
source={source}
rowId={rowId}
onClose={_onClose}
isNestedPanel={isNestedPanel}
breadcrumbPath={breadcrumbPath}
setSubDrawerOpen={setSubDrawerOpen}
onBreadcrumbClick={onBreadcrumbClick}
/>
</ErrorBoundary>
</div>
</ZIndexContext.Provider>
</Drawer>
</OptionalPortal>
<div className="px-2 py-1 m-2 fs-7 font-monospace bg-dark-grey p-4">
{error?.error?.message}
</div>
</Stack>
)}
>
<DBRowSidePanel
source={source}
rowId={rowId}
onClose={_onClose}
isNestedPanel={isNestedPanel}
breadcrumbPath={breadcrumbPath}
setSubDrawerOpen={setSubDrawerOpen}
onBreadcrumbClick={onBreadcrumbClick}
/>
</ErrorBoundary>
</div>
</ZIndexContext.Provider>
</Drawer>
);
}

View file

@ -897,6 +897,7 @@ export const RawLogTable = memo(
<DBRowTableFieldWithPopover
cellValue={cellValue}
wrapLinesEnabled={wrapLinesEnabled}
tableContainerRef={tableContainerRef}
columnName={
(cell.column.columnDef.meta as any)
?.column

View file

@ -15,12 +15,14 @@ export interface DBRowTableFieldWithPopoverProps {
cellValue: unknown;
wrapLinesEnabled: boolean;
columnName?: string;
tableContainerRef?: React.RefObject<HTMLDivElement>;
isChart?: boolean;
}
export const DBRowTableFieldWithPopover = ({
children,
cellValue,
tableContainerRef,
wrapLinesEnabled,
columnName,
isChart = false,
@ -151,7 +153,7 @@ export const DBRowTableFieldWithPopover = ({
position="top-start"
offset={5}
opened={opened}
zIndex={1}
portalProps={{ target: tableContainerRef?.current ?? undefined }}
>
<Popover.Target>
<span

View file

@ -1,8 +1,7 @@
import * as React from 'react';
import Drawer from 'react-modern-drawer';
import { JSDataType } from '@hyperdx/common-utils/dist/clickhouse';
import { TSource } from '@hyperdx/common-utils/dist/types';
import { Card, Stack, Text } from '@mantine/core';
import { Card, Drawer, Stack, Text } from '@mantine/core';
import DBRowSidePanel from '@/components/DBRowSidePanel';
import { RawLogTable } from '@/components/DBRowTable';
@ -96,14 +95,17 @@ export default function PatternSidePanel({
return (
<Drawer
open={isOpen}
opened={isOpen}
onClose={selectedRowWhere ? handleCloseRowSidePanel : onClose}
direction="right"
position="right"
size="70vw"
withCloseButton={false}
zIndex={drawerZIndex}
enableOverlay={selectedRowWhere == null}
overlayOpacity={0.1}
duration={0}
styles={{
body: {
padding: 0,
},
}}
>
<ZIndexContext.Provider value={drawerZIndex}>
<div className={styles.panel}>

View file

@ -1,8 +1,7 @@
import { useCallback, useMemo } from 'react';
import { parseAsString, useQueryState } from 'nuqs';
import Drawer from 'react-modern-drawer';
import type { Filter } from '@hyperdx/common-utils/dist/types';
import { Grid, Group, Text } from '@mantine/core';
import { Drawer, Grid, Group, Text } from '@mantine/core';
import { INTEGER_NUMBER_FORMAT, MS_NUMBER_FORMAT } from '@/ChartUtils';
import { ChartBox } from '@/components/ChartBox';
@ -14,7 +13,6 @@ import { getExpressions } from '@/serviceDashboard';
import { useSource } from '@/source';
import { useZIndex, ZIndexContext } from '@/zIndex';
import 'react-modern-drawer/dist/index.css';
import styles from '@/../styles/LogSidePanel.module.scss';
export default function ServiceDashboardDbQuerySidePanel({
@ -64,12 +62,17 @@ export default function ServiceDashboardDbQuerySidePanel({
return (
<Drawer
duration={0}
open
opened
onClose={onClose}
direction="right"
size={'80vw'}
position="right"
size="80vw"
withCloseButton={false}
zIndex={drawerZIndex}
styles={{
body: {
padding: 0,
},
}}
>
<ZIndexContext.Provider value={drawerZIndex}>
<div className={styles.panel}>

View file

@ -1,8 +1,7 @@
import { useCallback, useMemo } from 'react';
import { parseAsString, useQueryState } from 'nuqs';
import Drawer from 'react-modern-drawer';
import type { Filter } from '@hyperdx/common-utils/dist/types';
import { Grid, Group, Text } from '@mantine/core';
import { Drawer, Grid, Group, Text } from '@mantine/core';
import {
ERROR_RATE_PERCENTAGE_NUMBER_FORMAT,
@ -19,7 +18,6 @@ import { EndpointLatencyChart } from '@/ServicesDashboardPage';
import { useSource } from '@/source';
import { useZIndex, ZIndexContext } from '@/zIndex';
import 'react-modern-drawer/dist/index.css';
import styles from '@/../styles/LogSidePanel.module.scss';
export default function ServiceDashboardEndpointSidePanel({
@ -69,12 +67,17 @@ export default function ServiceDashboardEndpointSidePanel({
return (
<Drawer
duration={0}
open
opened
onClose={onClose}
direction="right"
size={'80vw'}
position="right"
size="80vw"
withCloseButton={false}
zIndex={drawerZIndex}
styles={{
body: {
padding: 0,
},
}}
>
<ZIndexContext.Provider value={drawerZIndex}>
<div className={styles.panel}>

View file

@ -150,7 +150,7 @@ test.describe('Search', { tag: '@search' }, () => {
);
await page.waitForTimeout(1000);
const sidePanel = page.locator('nav[class*="EZDrawer__container"]');
const sidePanel = page.locator('[data-testid="row-side-panel"]');
await expect(sidePanel).toBeVisible();
});

View file

@ -54,8 +54,8 @@ test.describe('Advanced Search Workflow - Traces', { tag: '@traces' }, () => {
await firstRow.click();
await page.waitForTimeout(1000);
// Use the main side panel container (nav element with EZDrawer__container class)
const sidePanel = page.locator('nav[class*="EZDrawer__container"]');
// Use the main side panel container to verify it is visible
const sidePanel = page.locator('[data-testid="row-side-panel"]');
await expect(sidePanel).toBeVisible();
});

View file

@ -4480,7 +4480,6 @@ __metadata:
react-hotkeys-hook: "npm:^4.3.7"
react-json-tree: "npm:^0.17.0"
react-markdown: "npm:^8.0.4"
react-modern-drawer: "npm:^1.2.0"
react-papaparse: "npm:^4.4.0"
react-query: "npm:^3.39.3"
react-select: "npm:^5.7.0"
@ -24660,15 +24659,6 @@ __metadata:
languageName: node
linkType: hard
"react-modern-drawer@npm:^1.2.0":
version: 1.2.0
resolution: "react-modern-drawer@npm:1.2.0"
peerDependencies:
react: ">16.0.0"
checksum: 10c0/41301a6da8daa899c1f36d0cb3b0258bd89b5109d7052912cd0027dfb626e4b38109b3c1c5d68aa1872f62feac791efe56737e7a220cad894186d3017b7acd76
languageName: node
linkType: hard
"react-number-format@npm:^5.3.1":
version: 5.3.1
resolution: "react-number-format@npm:5.3.1"