Browse Source

feat(replay): When hovering over an accessibility row, highlight the element in the replay (#60698)

![SCR-20231128-jhhh](https://github.com/getsentry/sentry/assets/187460/ac75b7f2-2f0b-444d-a903-b9dd8656bb1c)
Ryan Albrecht 1 year ago
parent
commit
b389d67a5e

+ 15 - 6
static/app/components/codeSnippet.tsx

@@ -14,6 +14,7 @@ interface CodeSnippetProps {
   language: string;
   className?: string;
   dark?: boolean;
+  ['data-render-inline']?: boolean;
   /**
    * Makes the text of the element and its sub-elements not selectable.
    * Userful when loading parts of a code snippet, and
@@ -42,17 +43,18 @@ interface CodeSnippetProps {
 
 export function CodeSnippet({
   children,
-  language,
+  className,
   dark,
+  'data-render-inline': dataRenderInline,
+  disableUserSelection,
   filename,
   hideCopyButton,
+  language,
+  onAfterHighlight,
   onCopy,
-  className,
   onSelectAndCopy,
-  disableUserSelection,
-  onAfterHighlight,
-  selectedTab,
   onTabClick,
+  selectedTab,
   tabs,
 }: CodeSnippetProps) {
   const ref = useRef<HTMLModElement | null>(null);
@@ -99,7 +101,10 @@ export function CodeSnippet({
       : t('Unable to copy');
 
   return (
-    <Wrapper className={`${dark ? 'prism-dark ' : ''}${className ?? ''}`}>
+    <Wrapper
+      className={`${dark ? 'prism-dark ' : ''}${className ?? ''}`}
+      data-render-inline={dataRenderInline}
+    >
       <Header isSolid={hasSolidHeader}>
         {hasTabs && (
           <Fragment>
@@ -160,6 +165,10 @@ const Wrapper = styled('div')`
   pre {
     margin: 0;
   }
+
+  &[data-render-inline='true'] pre {
+    padding: 0;
+  }
 `;
 
 const Header = styled('div')<{isSolid: boolean}>`

+ 3 - 3
static/app/components/replays/virtualizedGrid/bodyCell.tsx

@@ -6,7 +6,7 @@ import {space} from 'sentry/styles/space';
 
 const cellBackground = (p: CellProps & {theme: Theme}) => {
   if (p.isSelected) {
-    return `background-color: ${p.theme.textColor};`;
+    return `background-color: ${p.theme.black};`;
   }
   if (p.isStatusError) {
     return `background-color: ${p.theme.red100};`;
@@ -23,7 +23,7 @@ const cellColor = (p: CellProps & {theme: Theme}) => {
       ? p.theme.red300
       : p.isStatusWarning
       ? p.theme.yellow300
-      : p.theme.background;
+      : p.theme.white;
     return `color: ${color};`;
   }
   const colors = p.isStatusError
@@ -76,7 +76,7 @@ export const CodeHighlightCell = styled(CodeSnippet)`
   text-overflow: ellipsis;
   white-space: nowrap;
   overflow: hidden;
-  padding: ${space(0.75)} ${space(1.5)};
+  padding: ${space(0.75)} 0;
   display: flex;
   gap: ${space(0.5)};
   --prism-block-background: transparent;

+ 23 - 2
static/app/utils/replays/hooks/useCrumbHandlers.tsx

@@ -9,12 +9,33 @@ type RecordType = {
     | {
         nodeId: number;
         label?: string;
+      }
+    | {
+        element: {
+          element: string;
+          target: string[];
+        };
+        label: string;
       };
 };
 
 function getNodeIdAndLabel(record: RecordType) {
-  if (record.data && typeof record.data === 'object' && 'nodeId' in record.data) {
-    return {nodeId: record.data.nodeId, annotation: record.data.label};
+  if (!record.data || typeof record.data !== 'object') {
+    return undefined;
+  }
+  const data = record.data;
+  if (
+    'element' in data &&
+    'target' in data.element &&
+    Array.isArray(data.element.target)
+  ) {
+    return {
+      selector: data.element.target.join(' '),
+      annotation: data.label,
+    };
+  }
+  if ('nodeId' in data) {
+    return {nodeId: data.nodeId, annotation: record.data.label};
   }
   return undefined;
 }

+ 16 - 1
static/app/utils/replays/hydrateA11yFrame.tsx

@@ -27,6 +27,13 @@ type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U;
 export type HydratedA11yFrame = Overwrite<
   Omit<RawA11yFrame, 'elements' | 'help'>,
   {
+    /**
+     * For compatibility with Frames, to highlight the element within the replay
+     */
+    data: {
+      element: A11yIssueElement;
+      label: string;
+    };
     /**
      * Rename `help` to conform to ReplayFrame basics.
      */
@@ -57,9 +64,17 @@ export default function hydrateA11yFrame(
   return raw.elements.map((element): HydratedA11yFrame => {
     const timestamp = new Date(raw.timestamp);
     const timestampMs = timestamp.getTime();
+    const elementWithoutIframe = {
+      ...element,
+      target: element.target[0] === 'iframe' ? element.target.slice(1) : element.target,
+    };
     return {
+      data: {
+        element: elementWithoutIframe,
+        label: raw.id,
+      },
       description: raw.help,
-      element,
+      element: elementWithoutIframe,
       help_url: raw.help_url,
       id: raw.id,
       impact: raw.impact,

+ 1 - 0
static/app/views/replays/detail/accessibility/accessibilityHeaderCell.tsx

@@ -27,6 +27,7 @@ const COLUMNS: {
     label: t('Type'),
   },
   {field: 'element', label: t('Element')},
+  {field: '', label: ''},
 ];
 
 export const COLUMN_COUNT = COLUMNS.length;

+ 19 - 3
static/app/views/replays/detail/accessibility/accessibilityTableCell.tsx

@@ -1,13 +1,15 @@
 import {ComponentProps, CSSProperties, forwardRef} from 'react';
 import classNames from 'classnames';
 
+import {Button} from 'sentry/components/button';
 import {
   Cell,
   CodeHighlightCell,
   Text,
 } from 'sentry/components/replays/virtualizedGrid/bodyCell';
 import {Tooltip} from 'sentry/components/tooltip';
-import {IconFire, IconInfo, IconWarning} from 'sentry/icons';
+import {IconFire, IconInfo, IconPlay, IconWarning} from 'sentry/icons';
+import {t} from 'sentry/locale';
 import type useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
 import {HydratedA11yFrame} from 'sentry/utils/replays/hydrateA11yFrame';
 import {Color} from 'sentry/utils/theme';
@@ -31,7 +33,6 @@ interface Props extends ReturnType<typeof useCrumbHandlers> {
   onClickCell: (props: {dataIndex: number; rowIndex: number}) => void;
   rowIndex: number;
   sortConfig: ReturnType<typeof useSortAccessibility>['sortConfig'];
-  // startTimestampMs: number;
   style: CSSProperties;
 }
 
@@ -43,6 +44,7 @@ const AccessibilityTableCell = forwardRef<HTMLDivElement, Props>(
       currentHoverTime,
       currentTime,
       onClickCell,
+      onClickTimestamp,
       onMouseEnter,
       onMouseLeave,
       rowIndex,
@@ -118,11 +120,25 @@ const AccessibilityTableCell = forwardRef<HTMLDivElement, Props>(
       ),
       () => (
         <Cell {...columnProps}>
-          <CodeHighlightCell language="html" hideCopyButton>
+          <CodeHighlightCell language="html" hideCopyButton data-render-inline>
             {a11yIssue.element.element ?? EMPTY_CELL}
           </CodeHighlightCell>
         </Cell>
       ),
+      () => (
+        <Cell {...columnProps}>
+          <Button
+            size="xs"
+            borderless
+            aria-label={t('See in replay')}
+            icon={<IconPlay size="xs" color={isSelected ? 'white' : 'black'} />}
+            onClick={e => {
+              e.stopPropagation();
+              onClickTimestamp(a11yIssue);
+            }}
+          />
+        </Cell>
+      ),
     ];
 
     return renderFns[columnIndex]();