aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorstanislavh <stanislav.honcharov@sonarsource.com>2024-07-23 13:19:34 +0200
committersonartech <sonartech@sonarsource.com>2024-07-24 20:02:47 +0000
commit012bf4a5f40dfa9034e534f81941ce3cdbd5ac13 (patch)
treee1baebc0be7079dc22fecd4027f838b2f41ba77f /server/sonar-web
parent1e4b5a6f824d15e4f1103cf64cf0dd2175b59195 (diff)
downloadsonarqube-012bf4a5f40dfa9034e534f81941ce3cdbd5ac13.tar.gz
sonarqube-012bf4a5f40dfa9034e534f81941ce3cdbd5ac13.zip
SONAR-22543 Improve SR navigation
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/design-system/src/components/subnavigation/SubnavigationItem.tsx30
-rw-r--r--server/sonar-web/src/main/js/components/templates/FilterBarTemplate.tsx187
-rw-r--r--server/sonar-web/src/main/js/components/templates/__tests__/FilterBarTemplate-test.tsx43
3 files changed, 253 insertions, 7 deletions
diff --git a/server/sonar-web/design-system/src/components/subnavigation/SubnavigationItem.tsx b/server/sonar-web/design-system/src/components/subnavigation/SubnavigationItem.tsx
index f2c6208e513..0d03117246a 100644
--- a/server/sonar-web/design-system/src/components/subnavigation/SubnavigationItem.tsx
+++ b/server/sonar-web/design-system/src/components/subnavigation/SubnavigationItem.tsx
@@ -17,11 +17,14 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { css } from '@emotion/react';
import styled from '@emotion/styled';
import classNames from 'classnames';
import { ReactNode, SyntheticEvent, useCallback } from 'react';
import tw, { theme as twTheme } from 'twin.macro';
import { themeBorder, themeColor, themeContrast } from '../../helpers/theme';
+import { ThemedProps } from '../../types';
+import NavLink, { NavLinkProps } from '../NavLink';
interface Props {
active?: boolean;
@@ -54,7 +57,11 @@ export function SubnavigationItem(props: Readonly<Props>) {
);
}
-const StyledSubnavigationItem = styled.a`
+export function SubnavigationLinkItem({ children, ...props }: NavLinkProps) {
+ return <SubnavigationLinkItemStyled {...props}>{children}</SubnavigationLinkItemStyled>;
+}
+
+const ItemBaseStyle = (props: ThemedProps) => css`
${tw`sw-flex sw-items-center sw-justify-between`}
${tw`sw-box-border`}
${tw`sw-body-sm`}
@@ -63,21 +70,30 @@ const StyledSubnavigationItem = styled.a`
${tw`sw-cursor-pointer`}
padding-left: calc(${twTheme('spacing.4')} - 3px);
- color: ${themeContrast('subnavigation')};
- background-color: ${themeColor('subnavigation')};
+ color: ${themeContrast('subnavigation')(props)};
+ background-color: ${themeColor('subnavigation')(props)};
border-bottom: none;
- border-left: ${themeBorder('active', 'transparent')};
+ border-left: ${themeBorder('active', 'transparent')(props)};
transition: 0.2 ease;
transition-property: border-left, background-color, color;
&:hover,
&:focus,
&.active {
- background-color: ${themeColor('subnavigationHover')};
+ background-color: ${themeColor('subnavigationHover')(props)};
}
&.active {
- color: ${themeContrast('subnavigationHover')};
- border-left: ${themeBorder('active')};
+ color: ${themeContrast('subnavigationHover')(props)};
+ border-left: ${themeBorder('active')(props)};
}
`;
+
+const StyledSubnavigationItem = styled.a`
+ ${ItemBaseStyle};
+`;
+
+const SubnavigationLinkItemStyled = styled(NavLink)`
+ ${ItemBaseStyle};
+ ${tw`sw-no-underline`}
+`;
diff --git a/server/sonar-web/src/main/js/components/templates/FilterBarTemplate.tsx b/server/sonar-web/src/main/js/components/templates/FilterBarTemplate.tsx
new file mode 100644
index 00000000000..a0ddf19618b
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/templates/FilterBarTemplate.tsx
@@ -0,0 +1,187 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 classNames from 'classnames';
+import {
+ LAYOUT_FILTERBAR_HEADER,
+ LAYOUT_FOOTER_HEIGHT,
+ LAYOUT_GLOBAL_NAV_HEIGHT,
+ LAYOUT_PROJECT_NAV_HEIGHT,
+ themeBorder,
+ themeColor,
+} from 'design-system';
+import React from 'react';
+import { translate } from '../../helpers/l10n';
+
+import useFollowScroll from '../../hooks/useFollowScroll';
+
+export type LayoutFilterBarSize = 'default' | 'large';
+
+const HEADER_PADDING_BOTTOM = 24;
+const HEADER_PADDING = 32 + HEADER_PADDING_BOTTOM; //32 padding top and 24 padding bottom
+
+interface Props {
+ className?: string;
+ content: React.ReactNode;
+ contentClassName?: string;
+ filterbar: React.ReactNode;
+ filterbarContentClassName?: string;
+ filterbarHeader?: React.ReactNode;
+ filterbarHeaderClassName?: string;
+ filterbarRef?: React.RefObject<HTMLDivElement>;
+ header?: React.ReactNode;
+ headerHeight?: number;
+ id?: string;
+ size?: LayoutFilterBarSize;
+ withBorderLeft?: boolean;
+}
+
+export default function FilterBarTemplate(props: Readonly<Props>) {
+ const {
+ className,
+ content,
+ contentClassName,
+ header,
+ headerHeight = 0,
+ id,
+ filterbarRef,
+ filterbar,
+ filterbarHeader,
+ filterbarHeaderClassName,
+ filterbarContentClassName,
+ size = 'default',
+ withBorderLeft = false,
+ } = props;
+
+ const headerHeightWithPadding = headerHeight ? headerHeight + HEADER_PADDING : 0;
+ const { top: topScroll, scrolledOnce } = useFollowScroll();
+ const distanceFromBottom = topScroll + window.innerHeight - document.body.scrollHeight;
+ const footerVisibleHeight =
+ (scrolledOnce &&
+ (distanceFromBottom > -LAYOUT_FOOTER_HEIGHT
+ ? LAYOUT_FOOTER_HEIGHT + distanceFromBottom
+ : 0)) ||
+ 0;
+
+ return (
+ <>
+ {header && (
+ <div
+ className="sw-flex sw-pb-6 sw-box-border"
+ style={{
+ height: `${headerHeight + HEADER_PADDING_BOTTOM}px`,
+ }}
+ >
+ {header}
+ </div>
+ )}
+ <div
+ className={classNames(
+ 'sw-grid sw-grid-cols-12 sw-w-full sw-px-14 sw-box-border',
+ className,
+ )}
+ id={id}
+ >
+ <Filterbar
+ className={classNames('sw--mt-8 sw-z-filterbar', {
+ 'sw-col-span-3': size === 'default',
+ 'sw-col-span-4': size === 'large',
+ bordered: Boolean(header),
+ 'sw-mt-0': Boolean(header),
+ 'sw-rounded-t-1': Boolean(header),
+ 'border-left': withBorderLeft,
+ })}
+ ref={filterbarRef}
+ style={{
+ height: `calc(100vh - ${
+ LAYOUT_GLOBAL_NAV_HEIGHT + LAYOUT_PROJECT_NAV_HEIGHT
+ }px - ${footerVisibleHeight}px)`,
+ top: LAYOUT_GLOBAL_NAV_HEIGHT + LAYOUT_PROJECT_NAV_HEIGHT + headerHeightWithPadding,
+ }}
+ >
+ {filterbarHeader && (
+ <FilterbarHeader
+ className={classNames(
+ 'sw-w-full sw-top-0 sw-px-4 sw-py-2 sw-z-filterbar-header',
+ filterbarHeaderClassName,
+ )}
+ >
+ {filterbarHeader}
+ </FilterbarHeader>
+ )}
+ <FilterbarContent
+ aria-label={translate('secondary')}
+ className={classNames('sw-p-4 js-page-filter', filterbarContentClassName)}
+ >
+ {filterbar}
+ </FilterbarContent>
+ </Filterbar>
+ <Main
+ className={classNames(
+ 'sw-relative sw-pl-12',
+ {
+ 'sw-col-span-9': size === 'default',
+ 'sw-col-span-8': size === 'large',
+ },
+ 'js-page-main',
+ contentClassName,
+ )}
+ >
+ {content}
+ </Main>
+ </div>
+ </>
+ );
+}
+
+const Filterbar = styled.div`
+ position: sticky;
+ box-sizing: border-box;
+ overflow-x: hidden;
+ overflow-y: auto;
+ background-color: ${themeColor('filterbar')};
+ border-right: ${themeBorder('default', 'filterbarBorder')};
+
+ &.border-left {
+ border-left: ${themeBorder('default', 'filterbarBorder')};
+ }
+
+ &.bordered {
+ border: ${themeBorder('default', 'filterbarBorder')};
+ }
+`;
+
+const FilterbarContent = styled.nav`
+ position: relative;
+ box-sizing: border-box;
+ width: 100%;
+`;
+
+const FilterbarHeader = styled.div`
+ position: sticky;
+ box-sizing: border-box;
+ height: ${LAYOUT_FILTERBAR_HEADER}px;
+ background-color: inherit;
+ border-bottom: ${themeBorder('default')};
+`;
+
+const Main = styled.div`
+ flex-grow: 1;
+`;
diff --git a/server/sonar-web/src/main/js/components/templates/__tests__/FilterBarTemplate-test.tsx b/server/sonar-web/src/main/js/components/templates/__tests__/FilterBarTemplate-test.tsx
new file mode 100644
index 00000000000..a6c9cea12a5
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/templates/__tests__/FilterBarTemplate-test.tsx
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 { screen } from '@testing-library/react';
+import React from 'react';
+import { renderComponent } from '../../../helpers/testReactTestingUtils';
+import { FCProps } from '../../../types/misc';
+import FilterBarTemplate from '../FilterBarTemplate';
+
+it('should render with filter header', () => {
+ setupWithProps({ header: <span data-testid="filter-header">header</span>, headerHeight: 16 });
+
+ expect(screen.getByTestId('filter-header')).toHaveTextContent('header');
+});
+
+function setupWithProps(props: Partial<FCProps<typeof FilterBarTemplate>> = {}) {
+ return renderComponent(
+ <FilterBarTemplate
+ className="custom-class"
+ content={<div data-testid="content" />}
+ filterbar={<div data-testid="side" />}
+ filterbarHeader={<div data-testid="side-header" />}
+ size="large"
+ {...props}
+ />,
+ );
+}