Browse Source

feat(spans): Be able to horizontal scroll spans using trackpad or mouse wheel (#25736)

Alberto Leal 3 years ago
parent
commit
e3b7a93c94

+ 23 - 14
static/app/components/events/interfaces/spans/header.tsx

@@ -361,16 +361,24 @@ class TraceViewHeader extends React.Component<PropType, State> {
 
           return (
             <SecondaryHeader>
-              <ScrollbarContainer
-                ref={this.props.virtualScrollBarContainerRef}
-                style={{
-                  // the width of this component is shrunk to compensate for half of the width of the divider line
-                  width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
-                }}
-              >
-                <ScrollbarManager.Consumer>
-                  {({virtualScrollbarRef, onDragStart}) => {
-                    return (
+              <ScrollbarManager.Consumer>
+                {({virtualScrollbarRef, scrollBarAreaRef, onDragStart, onScroll}) => {
+                  return (
+                    <ScrollbarContainer
+                      ref={this.props.virtualScrollBarContainerRef}
+                      style={{
+                        // the width of this component is shrunk to compensate for half of the width of the divider line
+                        width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
+                      }}
+                      onScroll={onScroll}
+                    >
+                      <div
+                        style={{
+                          width: 0,
+                          height: '1px',
+                        }}
+                        ref={scrollBarAreaRef}
+                      />
                       <VirtualScrollbar
                         data-type="virtual-scrollbar"
                         ref={virtualScrollbarRef}
@@ -378,10 +386,10 @@ class TraceViewHeader extends React.Component<PropType, State> {
                       >
                         <VirtualScrollbarGrip />
                       </VirtualScrollbar>
-                    );
-                  }}
-                </ScrollbarManager.Consumer>
-              </ScrollbarContainer>
+                    </ScrollbarContainer>
+                  );
+                }}
+              </ScrollbarManager.Consumer>
               <DividerSpacer />
               {hasMeasurements ? (
                 <MeasurementsPanel
@@ -872,6 +880,7 @@ export const SecondaryHeader = styled('div')`
   background-color: ${p => p.theme.backgroundSecondary};
   display: flex;
   border-top: 1px solid ${p => p.theme.border};
+  overflow: hidden;
 `;
 
 const OperationsBreakdown = styled('div')`

+ 77 - 4
static/app/components/events/interfaces/spans/scrollbarManager.tsx

@@ -13,14 +13,18 @@ import {DragManagerChildrenProps} from './dragManager';
 export type ScrollbarManagerChildrenProps = {
   generateContentSpanBarRef: () => (instance: HTMLDivElement | null) => void;
   virtualScrollbarRef: React.RefObject<HTMLDivElement>;
+  scrollBarAreaRef: React.RefObject<HTMLDivElement>;
   onDragStart: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
+  onScroll: () => void;
   updateScrollState: () => void;
 };
 
 const ScrollbarManagerContext = React.createContext<ScrollbarManagerChildrenProps>({
   generateContentSpanBarRef: () => () => undefined,
   virtualScrollbarRef: React.createRef<HTMLDivElement>(),
+  scrollBarAreaRef: React.createRef<HTMLDivElement>(),
   onDragStart: () => {},
+  onScroll: () => {},
   updateScrollState: () => {},
 });
 
@@ -99,6 +103,7 @@ export class Provider extends React.Component<Props, State> {
 
   contentSpanBar: Set<HTMLDivElement> = new Set();
   virtualScrollbar: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>();
+  scrollBarArea: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>();
   isDragging: boolean = false;
   previousUserSelect: UserSelectValues | null = null;
 
@@ -153,6 +158,19 @@ export class Provider extends React.Component<Props, State> {
       spanBarDOM.style.overflow = 'hidden';
     });
 
+    // set inner width of scrollbar area
+    selectRefs(this.scrollBarArea, (scrollBarArea: HTMLDivElement) => {
+      scrollBarArea.style.width = `${maxContentWidth}px`;
+      scrollBarArea.style.maxWidth = `${maxContentWidth}px`;
+    });
+
+    selectRefs(
+      this.props.interactiveLayerRef,
+      (interactiveLayerRefDOM: HTMLDivElement) => {
+        interactiveLayerRefDOM.scrollLeft = 0;
+      }
+    );
+
     const spanBarDOM = this.getReferenceSpanBar();
 
     if (spanBarDOM) {
@@ -161,7 +179,7 @@ export class Provider extends React.Component<Props, State> {
   };
 
   syncVirtualScrollbar = (spanBar: HTMLDivElement) => {
-    // sync the virtual scrollbar's width scrolledSpanBar's width
+    // sync the virtual scrollbar's width to the spanBar's width
 
     if (!this.virtualScrollbar.current || !this.hasInteractiveLayer()) {
       return;
@@ -170,14 +188,14 @@ export class Provider extends React.Component<Props, State> {
     const virtualScrollbarDOM = this.virtualScrollbar.current;
 
     const maxContentWidth = spanBar.getBoundingClientRect().width;
-    const interactiveLayerRect = rectOfContent(this.props.interactiveLayerRef.current!);
 
     if (maxContentWidth === undefined || maxContentWidth <= 0) {
       virtualScrollbarDOM.style.width = '0';
       return;
     }
 
-    const visibleWidth = interactiveLayerRect.width;
+    const visibleWidth = this.props.interactiveLayerRef.current!.getBoundingClientRect()
+      .width;
 
     // This is the width of the content not visible.
     const maxScrollDistance = maxContentWidth - visibleWidth;
@@ -215,6 +233,44 @@ export class Provider extends React.Component<Props, State> {
   hasInteractiveLayer = (): boolean => !!this.props.interactiveLayerRef.current;
   initialMouseClickX: number | undefined = undefined;
 
+  onScroll = () => {
+    if (this.isDragging || !this.hasInteractiveLayer()) {
+      return;
+    }
+
+    const interactiveLayerRefDOM = this.props.interactiveLayerRef.current!;
+
+    const interactiveLayerRect = interactiveLayerRefDOM.getBoundingClientRect();
+    const scrollLeft = interactiveLayerRefDOM.scrollLeft;
+
+    // Update scroll position of the virtual scroll bar
+    selectRefs(this.scrollBarArea, (scrollBarAreaDOM: HTMLDivElement) => {
+      selectRefs(this.virtualScrollbar, (virtualScrollbarDOM: HTMLDivElement) => {
+        const scrollBarAreaRect = scrollBarAreaDOM.getBoundingClientRect();
+        const virtualScrollbarPosition = scrollLeft / scrollBarAreaRect.width;
+
+        const virtualScrollBarRect = rectOfContent(virtualScrollbarDOM);
+        const maxVirtualScrollableArea =
+          1 - virtualScrollBarRect.width / interactiveLayerRect.width;
+
+        const virtualLeft =
+          clamp(virtualScrollbarPosition, 0, maxVirtualScrollableArea) *
+          interactiveLayerRect.width;
+
+        virtualScrollbarDOM.style.transform = `translate3d(${virtualLeft}px, 0, 0)`;
+        virtualScrollbarDOM.style.transformOrigin = 'left';
+      });
+    });
+
+    // Update scroll positions of all the span bars
+    selectRefs(this.contentSpanBar, (spanBarDOM: HTMLDivElement) => {
+      const left = -scrollLeft;
+
+      spanBarDOM.style.transform = `translate3d(${left}px, 0, 0)`;
+      spanBarDOM.style.transformOrigin = 'left';
+    });
+  };
+
   onDragStart = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
     if (
       this.isDragging ||
@@ -268,7 +324,8 @@ export class Provider extends React.Component<Props, State> {
 
     const virtualScrollbarDOM = this.virtualScrollbar.current;
 
-    const interactiveLayerRect = rectOfContent(this.props.interactiveLayerRef.current!);
+    const interactiveLayerRect = this.props.interactiveLayerRef.current!.getBoundingClientRect();
+
     const virtualScrollBarRect = rectOfContent(virtualScrollbarDOM);
 
     // Mouse x-coordinate relative to the interactive layer's left side
@@ -302,6 +359,20 @@ export class Provider extends React.Component<Props, State> {
       spanBarDOM.style.transform = `translate3d(${left}px, 0, 0)`;
       spanBarDOM.style.transformOrigin = 'left';
     });
+
+    // Update the scroll position of the scroll bar area
+    selectRefs(
+      this.props.interactiveLayerRef,
+      (interactiveLayerRefDOM: HTMLDivElement) => {
+        selectRefs(this.scrollBarArea, (scrollBarAreaDOM: HTMLDivElement) => {
+          const maxScrollDistance =
+            scrollBarAreaDOM.getBoundingClientRect().width - interactiveLayerRect.width;
+          const left = lerp(0, maxScrollDistance, virtualScrollPercentage);
+
+          interactiveLayerRefDOM.scrollLeft = left;
+        });
+      }
+    );
   };
 
   onDragEnd = (event: MouseEvent) => {
@@ -342,7 +413,9 @@ export class Provider extends React.Component<Props, State> {
     const childrenProps: ScrollbarManagerChildrenProps = {
       generateContentSpanBarRef: this.generateContentSpanBarRef,
       onDragStart: this.onDragStart,
+      onScroll: this.onScroll,
       virtualScrollbarRef: this.virtualScrollbar,
+      scrollBarAreaRef: this.scrollBarArea,
       updateScrollState: this.initializeScrollState,
     };
 

+ 5 - 7
static/app/components/performance/waterfall/miniHeader.tsx

@@ -8,17 +8,15 @@ export const DividerSpacer = styled('div')`
 const MINI_HEADER_HEIGHT = 20;
 
 export const ScrollbarContainer = styled('div')`
-  display: flex;
-  align-items: center;
+  display: block;
   width: 100%;
-  height: ${MINI_HEADER_HEIGHT}px;
-  left: 0;
-  bottom: 0;
+  height: ${MINI_HEADER_HEIGHT + 50}px;
   & > div[data-type='virtual-scrollbar'].dragging > div {
     background-color: ${p => p.theme.textColor};
     opacity: 0.8;
     cursor: grabbing;
   }
+  overflow-x: scroll;
 `;
 
 export const VirtualScrollbar = styled('div')`
@@ -26,8 +24,8 @@ export const VirtualScrollbar = styled('div')`
   width: 0;
   padding-left: 4px;
   padding-right: 4px;
-  position: relative;
-  top: 0;
+  position: sticky;
+  top: ${(MINI_HEADER_HEIGHT - 8) / 2}px;
   left: 0;
   cursor: grab;
 `;