diff options
author | stanislavh <stanislav.honcharov@sonarsource.com> | 2024-07-23 13:19:34 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-07-24 20:02:47 +0000 |
commit | 012bf4a5f40dfa9034e534f81941ce3cdbd5ac13 (patch) | |
tree | e1baebc0be7079dc22fecd4027f838b2f41ba77f /server/sonar-web | |
parent | 1e4b5a6f824d15e4f1103cf64cf0dd2175b59195 (diff) | |
download | sonarqube-012bf4a5f40dfa9034e534f81941ce3cdbd5ac13.tar.gz sonarqube-012bf4a5f40dfa9034e534f81941ce3cdbd5ac13.zip |
SONAR-22543 Improve SR navigation
Diffstat (limited to 'server/sonar-web')
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} + />, + ); +} |