]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19591 Show elevation of tab view in issues page when scrolling
authorstanislavh <stanislav.honcharov@sonarsource.com>
Thu, 6 Jul 2023 11:18:28 +0000 (13:18 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 6 Jul 2023 20:03:12 +0000 (20:03 +0000)
server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
server/sonar-web/src/main/js/apps/issues/components/StyledHeader.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx

index c3b1683cceba5655c9f48839880f9890b1c6fa21..927fbcc01dee52e891c580f9410b1d08cf41a786 100644 (file)
@@ -19,6 +19,7 @@
  */
 
 import styled from '@emotion/styled';
+import classNames from 'classnames';
 import {
   ButtonSecondary,
   Checkbox,
@@ -101,13 +102,13 @@ import {
   shouldOpenStandardsFacet,
 } from '../utils';
 import BulkChangeModal, { MAX_PAGE_SIZE } from './BulkChangeModal';
-import IssueHeader from './IssueHeader';
 import IssueReviewHistoryAndComments from './IssueReviewHistoryAndComments';
 import IssuesList from './IssuesList';
 import IssuesSourceViewer from './IssuesSourceViewer';
 import NoIssues from './NoIssues';
 import NoMyIssues from './NoMyIssues';
 import PageActions from './PageActions';
+import StyledHeader, { PSEUDO_SHADOW_HEIGHT } from './StyledHeader';
 
 interface Props {
   branchLike?: BranchLike;
@@ -1170,8 +1171,8 @@ export class App extends React.PureComponent<Props, State> {
     ) : (
       <>
         <A11ySkipTarget anchor="issues_main" />
-        <div className="sw-flex sw-mb-6 sw-gap-4">
-          <div className="sw-flex sw-w-full sw-items-center sw-justify-between">
+        <StyledHeader headerHeight={84}>
+          <div className="sw-p-6 sw-flex sw-w-full sw-items-center sw-justify-between">
             {this.renderBulkChange()}
 
             <PageActions
@@ -1181,7 +1182,7 @@ export class App extends React.PureComponent<Props, State> {
               selectedIndex={selectedIndex}
             />
           </div>
-        </div>
+        </StyledHeader>
       </>
     );
   }
@@ -1197,79 +1198,86 @@ export class App extends React.PureComponent<Props, State> {
       paging,
       loadingRule,
     } = this.state;
+
+    const selectedIndex = this.getSelectedIndex();
     const topMargin = openIssue ? '120px' : '150px'; // 60px(footer) + 90px(bulk change)/60px(navbar)
 
     return (
       <ScreenPositionHelper>
         {({ top }) => (
           <div
-            className="it__layout-page-main-inner sw-overflow-y-auto"
+            className={classNames('it__layout-page-main-inner sw-pt-0', {
+              'sw-overflow-y-auto': !(openIssue && openRuleDetails),
+            })}
             style={{ height: `calc((100vh - ${top}px) - ${topMargin})` }}
           >
+            {this.renderHeader({ openIssue, paging, selectedIndex })}
+
             <DeferredSpinner loading={loadingRule}>
               {/* eslint-disable-next-line local-rules/no-conditional-rendering-of-deferredspinner */}
               {openIssue && openRuleDetails ? (
-                <>
-                  <IssueHeader
-                    issue={openIssue}
-                    ruleDetails={openRuleDetails}
-                    branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)}
-                    onIssueChange={this.handleIssueChange}
-                  />
-
-                  <IssueTabViewer
-                    ruleDetails={openRuleDetails}
-                    extendedDescription={openRuleDetails.htmlNote}
-                    ruleDescriptionContextKey={openIssue.ruleDescriptionContextKey}
-                    issue={openIssue}
-                    selectedFlowIndex={this.state.selectedFlowIndex}
-                    selectedLocationIndex={this.state.selectedLocationIndex}
-                    codeTabContent={
-                      <IssuesSourceViewer
-                        branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)}
-                        issues={issues}
-                        locationsNavigator={this.state.locationsNavigator}
-                        onIssueSelect={this.openIssue}
-                        onLocationSelect={this.selectLocation}
-                        openIssue={openIssue}
-                        selectedFlowIndex={this.state.selectedFlowIndex}
-                        selectedLocationIndex={this.state.selectedLocationIndex}
-                      />
-                    }
-                    scrollInTab
-                    activityTabContent={
-                      <IssueReviewHistoryAndComments
-                        issue={openIssue}
-                        onChange={this.handleIssueChange}
-                      />
-                    }
-                  />
-                </>
+                <IssueTabViewer
+                  ruleDetails={openRuleDetails as RuleDetails}
+                  extendedDescription={openRuleDetails.htmlNote}
+                  ruleDescriptionContextKey={openIssue.ruleDescriptionContextKey}
+                  issue={openIssue}
+                  selectedFlowIndex={this.state.selectedFlowIndex}
+                  selectedLocationIndex={this.state.selectedLocationIndex}
+                  onIssueChange={this.handleIssueChange}
+                  codeTabContent={
+                    <IssuesSourceViewer
+                      branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)}
+                      issues={issues}
+                      locationsNavigator={this.state.locationsNavigator}
+                      onIssueSelect={this.openIssue}
+                      onLocationSelect={this.selectLocation}
+                      openIssue={openIssue}
+                      selectedFlowIndex={this.state.selectedFlowIndex}
+                      selectedLocationIndex={this.state.selectedLocationIndex}
+                    />
+                  }
+                  scrollInTab
+                  activityTabContent={
+                    <IssueReviewHistoryAndComments
+                      issue={openIssue}
+                      onChange={this.handleIssueChange}
+                    />
+                  }
+                />
               ) : (
-                <DeferredSpinner loading={loading} ariaLabel={translate('issues.loading_issues')}>
-                  {checkAll && paging && paging.total > MAX_PAGE_SIZE && (
-                    <FlagMessage variant="warning">
-                      <span>
-                        <FormattedMessage
-                          defaultMessage={translate('issue_bulk_change.max_issues_reached')}
-                          id="issue_bulk_change.max_issues_reached"
-                          values={{ max: <strong>{MAX_PAGE_SIZE}</strong> }}
-                        />
-                      </span>
-                    </FlagMessage>
-                  )}
-
-                  {cannotShowOpenIssue && (!paging || paging.total > 0) && (
-                    <FlagMessage className="sw-mb-4" variant="warning">
-                      {translateWithParameters(
-                        'issues.cannot_open_issue_max_initial_X_fetched',
-                        MAX_INITAL_FETCH
-                      )}
-                    </FlagMessage>
-                  )}
-
-                  {this.renderList()}
-                </DeferredSpinner>
+                <div
+                  className="sw-px-6 sw-pb-6"
+                  style={{ marginTop: `-${PSEUDO_SHADOW_HEIGHT}px` }}
+                >
+                  <DeferredSpinner
+                    className="sw-mt-4"
+                    loading={loading}
+                    ariaLabel={translate('issues.loading_issues')}
+                  >
+                    {checkAll && paging && paging.total > MAX_PAGE_SIZE && (
+                      <FlagMessage variant="warning">
+                        <span>
+                          <FormattedMessage
+                            defaultMessage={translate('issue_bulk_change.max_issues_reached')}
+                            id="issue_bulk_change.max_issues_reached"
+                            values={{ max: <strong>{MAX_PAGE_SIZE}</strong> }}
+                          />
+                        </span>
+                      </FlagMessage>
+                    )}
+
+                    {cannotShowOpenIssue && (!paging || paging.total > 0) && (
+                      <FlagMessage className="sw-mb-4" variant="warning">
+                        {translateWithParameters(
+                          'issues.cannot_open_issue_max_initial_X_fetched',
+                          MAX_INITAL_FETCH
+                        )}
+                      </FlagMessage>
+                    )}
+
+                    {this.renderList()}
+                  </DeferredSpinner>
+                </div>
               )}
             </DeferredSpinner>
           </div>
@@ -1279,8 +1287,7 @@ export class App extends React.PureComponent<Props, State> {
   }
 
   render() {
-    const { openIssue, paging } = this.state;
-    const selectedIndex = this.getSelectedIndex();
+    const { openIssue } = this.state;
 
     return (
       <PageWrapperStyle id="issues-page">
@@ -1306,9 +1313,7 @@ export class App extends React.PureComponent<Props, State> {
 
               {this.renderSide(openIssue)}
 
-              <MainContentStyle role="main" className="sw-relative sw-ml-12 sw-p-6 sw-flex-1">
-                {this.renderHeader({ openIssue, paging, selectedIndex })}
-
+              <MainContentStyle className="sw-relative sw-ml-12 sw-flex-1">
                 {this.renderPage()}
               </MainContentStyle>
             </div>
@@ -1328,7 +1333,7 @@ const PageWrapperStyle = styled.div`
   background-color: ${themeColor('backgroundPrimary')};
 `;
 
-const MainContentStyle = styled.div`
+const MainContentStyle = styled.main`
   background-color: ${themeColor('subnavigation')};
   border-left: ${themeBorder('default', 'filterbarBorder')};
   border-right: ${themeBorder('default', 'filterbarBorder')};
diff --git a/server/sonar-web/src/main/js/apps/issues/components/StyledHeader.tsx b/server/sonar-web/src/main/js/apps/issues/components/StyledHeader.tsx
new file mode 100644 (file)
index 0000000..9497ede
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import { Theme, themeColor, themeShadow } from 'design-system';
+
+interface StyledHeaderProps {
+  headerHeight: number;
+  theme?: Theme;
+}
+
+export const PSEUDO_SHADOW_HEIGHT = 16;
+
+// Box-shadow on scroll source: https://codepen.io/StijnDeWitt/pen/LryNxa
+export const StyledHeader = styled.div<StyledHeaderProps>`
+  position: sticky;
+  top: -${PSEUDO_SHADOW_HEIGHT}px;
+  z-index: 1;
+  -webkit-backface-visibility: hidden;
+  & > div {
+    position: sticky;
+    top: 0;
+    margin-top: -${PSEUDO_SHADOW_HEIGHT}px;
+    box-sizing: border-box;
+    background-color: ${themeColor('backgroundSecondary')};
+    z-index: 3;
+  }
+  &:before {
+    content: '';
+    display: block;
+    height: ${PSEUDO_SHADOW_HEIGHT}px;
+    position: sticky;
+    top: ${({ headerHeight }) => `calc(${headerHeight}px - ${PSEUDO_SHADOW_HEIGHT}px)`};
+    box-shadow: ${themeShadow('sm')};
+  }
+  &:after {
+    content: '';
+    display: block;
+    height: ${PSEUDO_SHADOW_HEIGHT}px;
+    position: sticky;
+    background: linear-gradient(
+      ${themeColor('backgroundSecondary')} 10%,
+      rgba(255, 255, 255, 0.8) 50%,
+      rgba(255, 255, 255, 0.4) 70%,
+      transparent
+    );
+    top: 0;
+    z-index: 2;
+  }
+`;
+
+export default StyledHeader;
index b9b7e58c7bdac186fc5e3f17e0a04c26e6886c03..a8f145d066c273a9b164f6025973ad0937310b56 100644 (file)
@@ -27,6 +27,9 @@ import { dismissNotice } from '../../api/users';
 import { CurrentUserContextInterface } from '../../app/components/current-user/CurrentUserContext';
 import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext';
 import { RuleDescriptionSections } from '../../apps/coding-rules/rule';
+import IssueHeader from '../../apps/issues/components/IssueHeader';
+import StyledHeader from '../../apps/issues/components/StyledHeader';
+import { fillBranchLike } from '../../helpers/branch-like';
 import { translate } from '../../helpers/l10n';
 import { Issue, RuleDetails } from '../../types/types';
 import { NoticeType } from '../../types/users';
@@ -34,7 +37,6 @@ import ScreenPositionHelper from '../common/ScreenPositionHelper';
 import withLocation from '../hoc/withLocation';
 import MoreInfoRuleDescription from './MoreInfoRuleDescription';
 import RuleDescription from './RuleDescription';
-
 import './style.css';
 
 interface IssueTabViewerProps extends CurrentUserContextInterface {
@@ -47,9 +49,9 @@ interface IssueTabViewerProps extends CurrentUserContextInterface {
   location: Location;
   selectedFlowIndex?: number;
   selectedLocationIndex?: number;
-  issue?: Issue;
+  issue: Issue;
+  onIssueChange: (issue: Issue) => void;
 }
-
 interface State {
   tabs: Tab[];
   selectedTab?: Tab;
@@ -77,6 +79,7 @@ export enum TabKeys {
 const DEBOUNCE_FOR_SCROLL = 250;
 
 export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, State> {
+  headerNode?: HTMLElement | null = null;
   state: State = {
     tabs: [],
   };
@@ -335,7 +338,7 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
   };
 
   render() {
-    const { scrollInTab } = this.props;
+    const { scrollInTab, issue, ruleDetails } = this.props;
     const { tabs, selectedTab } = this.state;
 
     if (!tabs || tabs.length === 0 || !selectedTab) {
@@ -343,24 +346,34 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
     }
 
     return (
-      <>
-        <div className="sw-mb-4">
-          <ToggleButton
-            role="tablist"
-            value={selectedTab.key}
-            options={tabs}
-            onChange={this.handleSelectTabs}
-          />
-        </div>
-        <ScreenPositionHelper>
-          {({ top }) => (
+      <ScreenPositionHelper>
+        {({ top }) => (
+          <div
+            style={{
+              // We substract the footer height with padding (80) and the main layout padding (20)
+              // and the tabs padding (20)
+              maxHeight: scrollInTab ? `calc(100vh - ${top + 120}px)` : 'initial',
+            }}
+            className="sw-overflow-y-auto"
+          >
+            <StyledHeader headerHeight={this.headerNode?.clientHeight ?? 0} className="sw-z-normal">
+              <div className="sw-p-6 sw-pb-4" ref={(node) => (this.headerNode = node)}>
+                <IssueHeader
+                  issue={issue}
+                  ruleDetails={ruleDetails}
+                  branchLike={fillBranchLike(issue.branch, issue.pullRequest)}
+                  onIssueChange={this.props.onIssueChange}
+                />
+                <ToggleButton
+                  role="tablist"
+                  value={selectedTab.key}
+                  options={tabs}
+                  onChange={this.handleSelectTabs}
+                />
+              </div>
+            </StyledHeader>
             <div
-              style={{
-                // We substract the footer height with padding (80) and the main layout padding (20)
-                // and the tabs padding (20)
-                maxHeight: scrollInTab ? `calc(100vh - ${top + 120}px)` : 'initial',
-              }}
-              className="sw-flex sw-flex-col"
+              className="sw-flex sw-flex-col sw-px-6"
               role="tabpanel"
               aria-labelledby={`tab-${selectedTab.key}`}
               id={`tabpanel-${selectedTab.key}`}
@@ -369,7 +382,7 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
                 // Preserve tabs state by always rendering all of them. Only hide them when not selected
                 tabs.map((tab) => (
                   <div
-                    className={classNames('sw-overflow-y-auto', {
+                    className={classNames({
                       hidden: tab.key !== selectedTab.key,
                     })}
                     key={tab.key}
@@ -379,9 +392,9 @@ export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, Sta
                 ))
               }
             </div>
-          )}
-        </ScreenPositionHelper>
-      </>
+          </div>
+        )}
+      </ScreenPositionHelper>
     );
   }
 }