Browse Source

feat(dashboards): Update widget actions to hide/display on hover/focus (#78231)

Updates widget actions behind hover/focus on the widget card to save
title space. Also moves the widget description to a info tooltip in the
widget menu.
edwardgou-sentry 5 months ago
parent
commit
396397967f

+ 1 - 0
static/app/views/dashboards/widgetCard/index.spec.tsx

@@ -163,6 +163,7 @@ describe('Dashboards > WidgetCard', function () {
       />
     );
 
+    await userEvent.hover(await screen.findByLabelText('Widget description'));
     expect(await screen.findByText('Valid widget description')).toBeInTheDocument();
   });
 

+ 42 - 30
static/app/views/dashboards/widgetCard/index.tsx

@@ -170,25 +170,29 @@ class WidgetCard extends Component<Props, State> {
     }
 
     return (
-      <WidgetCardContextMenu
-        organization={organization}
-        widget={widget}
-        selection={selection}
-        showContextMenu={showContextMenu}
-        isPreview={isPreview}
-        widgetLimitReached={widgetLimitReached}
-        onDuplicate={onDuplicate}
-        onEdit={onEdit}
-        onDelete={onDelete}
-        router={router}
-        location={location}
-        index={index}
-        seriesData={seriesData}
-        seriesResultsType={seriesResultsType}
-        tableData={tableData}
-        pageLinks={pageLinks}
-        totalIssuesCount={totalIssuesCount}
-      />
+      <StyledWidgetCardContextMenuContainer>
+        <WidgetCardContextMenu
+          organization={organization}
+          widget={widget}
+          selection={selection}
+          showContextMenu={showContextMenu}
+          isPreview={isPreview}
+          widgetLimitReached={widgetLimitReached}
+          onDuplicate={onDuplicate}
+          onEdit={onEdit}
+          onDelete={onDelete}
+          router={router}
+          location={location}
+          index={index}
+          seriesData={seriesData}
+          seriesResultsType={seriesResultsType}
+          tableData={tableData}
+          pageLinks={pageLinks}
+          totalIssuesCount={totalIssuesCount}
+          description={widget.description}
+          title={widget.title}
+        />
+      </StyledWidgetCardContextMenuContainer>
     );
   }
 
@@ -312,7 +316,7 @@ class WidgetCard extends Component<Props, State> {
               }
               disabled={Number(this.props.index) !== 0}
             >
-              <WidgetCardPanel isDragging={false}>
+              <WidgetCardPanel isDragging={false} aria-label={t('Widget panel')}>
                 <WidgetHeaderWrapper>
                   <WidgetHeaderDescription>
                     <WidgetTitleRow>
@@ -334,16 +338,6 @@ class WidgetCard extends Component<Props, State> {
                       <DisplayOnDemandWarnings widget={widget} />
                       <DiscoverSplitAlert widget={widget} />
                     </WidgetTitleRow>
-                    {widget.description && (
-                      <Tooltip
-                        title={widget.description}
-                        containerDisplayMode="grid"
-                        showOnlyOnOverflow
-                        isHoverable
-                      >
-                        <WidgetDescription>{widget.description}</WidgetDescription>
-                      </Tooltip>
-                    )}
                   </WidgetHeaderDescription>
                   {this.renderContextMenu()}
                 </WidgetHeaderWrapper>
@@ -499,6 +493,11 @@ const ErrorCard = styled(Placeholder)`
   margin-bottom: ${space(2)};
 `;
 
+const StyledWidgetCardContextMenuContainer = styled('div')`
+  opacity: 1;
+  transition: opacity 0.1s;
+`;
+
 export const WidgetCardPanel = styled(Panel, {
   shouldForwardProp: prop => prop !== 'isDragging',
 })<{
@@ -511,6 +510,19 @@ export const WidgetCardPanel = styled(Panel, {
   min-height: 96px;
   display: flex;
   flex-direction: column;
+
+  &:not(:hover):not(:focus-within) {
+    ${StyledWidgetCardContextMenuContainer} {
+      opacity: 0;
+      clip: rect(0 0 0 0);
+      clip-path: inset(50%);
+      height: 1px;
+      overflow: hidden;
+      position: absolute;
+      white-space: nowrap;
+      width: 1px;
+    }
+  }
 `;
 
 const StoredDataAlert = styled(Alert)`

+ 44 - 1
static/app/views/dashboards/widgetCard/widgetCardContextMenu.tsx

@@ -8,7 +8,8 @@ import {openConfirmModal} from 'sentry/components/confirm';
 import type {MenuItemProps} from 'sentry/components/dropdownMenu';
 import {DropdownMenu} from 'sentry/components/dropdownMenu';
 import {isWidgetViewerPath} from 'sentry/components/modals/widgetViewerModal/utils';
-import {IconEllipsis, IconExpand} from 'sentry/icons';
+import {Tooltip} from 'sentry/components/tooltip';
+import {IconEllipsis, IconExpand, IconInfo} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import type {PageFilters} from 'sentry/types/core';
@@ -42,6 +43,7 @@ type Props = {
   selection: PageFilters;
   widget: Widget;
   widgetLimitReached: boolean;
+  description?: string;
   index?: string;
   isPreview?: boolean;
   onDelete?: () => void;
@@ -52,6 +54,7 @@ type Props = {
   seriesResultsType?: Record<string, AggregationOutputType>;
   showContextMenu?: boolean;
   tableData?: TableDataWithTitle[];
+  title?: string;
   totalIssuesCount?: string;
 };
 
@@ -73,6 +76,8 @@ function WidgetCardContextMenu({
   pageLinks,
   totalIssuesCount,
   seriesResultsType,
+  description,
+  title,
 }: Props) {
   const {isMetricsData} = useDashboardsMEPContext();
 
@@ -276,6 +281,27 @@ function WidgetCardContextMenu({
                     {t('Indexed')}
                   </SampledTag>
                 )}
+              {title && (
+                <Tooltip
+                  title={
+                    <span>
+                      <WidgetTooltipTitle>{title}</WidgetTooltipTitle>
+                      {description && (
+                        <WidgetTooltipDescription>{description}</WidgetTooltipDescription>
+                      )}
+                    </span>
+                  }
+                  containerDisplayMode="grid"
+                  isHoverable
+                >
+                  <WidgetTooltipButton
+                    aria-label={t('Widget description')}
+                    borderless
+                    size="xs"
+                    icon={<IconInfo />}
+                  />
+                </Tooltip>
+              )}
               <StyledDropdownMenuControl
                 items={menuOptions}
                 triggerProps={{
@@ -332,3 +358,20 @@ const StyledDropdownMenuControl = styled(DropdownMenu)`
 const SampledTag = styled(Tag)`
   margin-right: ${space(0.5)};
 `;
+
+const WidgetTooltipTitle = styled('div')`
+  font-weight: bold;
+  font-size: ${p => p.theme.fontSizeMedium};
+  text-align: left;
+`;
+
+const WidgetTooltipDescription = styled('div')`
+  margin-top: ${space(0.5)};
+  font-size: ${p => p.theme.fontSizeSmall};
+  text-align: left;
+`;
+
+// We're using a button here to preserve tab accessibility
+const WidgetTooltipButton = styled(Button)`
+  pointer-events: none;
+`;

+ 12 - 0
tests/acceptance/test_organization_dashboards.py

@@ -377,6 +377,9 @@ class OrganizationDashboardsAcceptanceTest(AcceptanceTestCase):
         with self.feature(FEATURE_NAMES + EDIT_FEATURE):
             self.page.visit_dashboard_detail()
 
+            # Hover over the widget to show widget actions
+            self.browser.move_to('[aria-label="Widget panel"]')
+
             self.browser.element('[aria-label="Widget actions"]').click()
             self.browser.element('[data-test-id="duplicate-widget"]').click()
             self.page.wait_until_loaded()
@@ -410,6 +413,9 @@ class OrganizationDashboardsAcceptanceTest(AcceptanceTestCase):
         with self.feature(FEATURE_NAMES + EDIT_FEATURE):
             self.page.visit_dashboard_detail()
 
+            # Hover over the widget to show widget actions
+            self.browser.move_to('[aria-label="Widget panel"]')
+
             self.browser.element('[aria-label="Widget actions"]').click()
             self.browser.element('[data-test-id="delete-widget"]').click()
             self.browser.element('[data-test-id="confirm-button"]').click()
@@ -512,6 +518,9 @@ class OrganizationDashboardsAcceptanceTest(AcceptanceTestCase):
         with self.feature(FEATURE_NAMES + EDIT_FEATURE):
             self.page.visit_dashboard_detail()
 
+            # Hover over the widget to show widget actions
+            self.browser.move_to('[aria-label="Widget panel"]')
+
             dropdown_trigger = self.browser.element('[aria-label="Widget actions"]')
             dropdown_trigger.click()
 
@@ -563,6 +572,9 @@ class OrganizationDashboardsAcceptanceTest(AcceptanceTestCase):
         with self.feature(FEATURE_NAMES + EDIT_FEATURE):
             self.page.visit_dashboard_detail()
 
+            # Hover over the widget to show widget actions
+            self.browser.move_to('[aria-label="Widget panel"]')
+
             # Open edit modal for first widget
             dropdown_trigger = self.browser.element('[aria-label="Widget actions"]')
             dropdown_trigger.click()