]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22218 Deprecate old tooltips and migrate some of them
authorJeremy Davis <jeremy.davis@sonarsource.com>
Tue, 21 May 2024 15:05:03 +0000 (17:05 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 27 May 2024 20:02:40 +0000 (20:02 +0000)
24 files changed:
server/sonar-web/config/jest/SetupReactTestingLibrary.ts
server/sonar-web/design-system/src/components/DropdownMenu.tsx
server/sonar-web/design-system/src/components/InteractiveIcon.tsx
server/sonar-web/design-system/src/components/Tooltip.tsx
server/sonar-web/design-system/src/components/index.ts
server/sonar-web/design-system/src/sonar-aligned/components/MetricsRatingBadge.tsx
server/sonar-web/design-system/src/sonar-aligned/components/buttons/Button.tsx
server/sonar-web/src/main/js/app/utils/startReactApp.tsx
server/sonar-web/src/main/js/apps/issues/components/IssueHeaderMeta.tsx
server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureBreakdownCard.tsx
server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureCard.tsx
server/sonar-web/src/main/js/apps/overview/branches/SoftwareImpactMeasureRating.tsx
server/sonar-web/src/main/js/apps/overview/pullRequests/IssueMeasuresCard.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx
server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationDetails.tsx
server/sonar-web/src/main/js/apps/users/components/UserListItem.tsx
server/sonar-web/src/main/js/components/controls/Tooltip.css
server/sonar-web/src/main/js/components/controls/Tooltip.tsx
server/sonar-web/src/main/js/components/issue/components/IssueTransitionItem.tsx
server/sonar-web/src/main/js/components/tags/TagsList.css [deleted file]
server/sonar-web/src/main/js/components/tags/TagsList.tsx
server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx
server/sonar-web/src/main/js/sonar-aligned/components/measure/Measure.tsx

index 35347ca7ead57b2502a926958908b94b28533630..346d4a2cb873d0ce0a430c8a742ad843952cfd59 100644 (file)
@@ -18,7 +18,8 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import '@testing-library/jest-dom';
-import { configure, fireEvent, screen, waitFor } from '@testing-library/react';
+import { configure, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
 
 configure({
   asyncUtilTimeout: 3000,
@@ -26,6 +27,8 @@ configure({
 
 expect.extend({
   async toHaveATooltipWithContent(received: any, content: string) {
+    const user = userEvent.setup();
+
     if (!(received instanceof Element)) {
       return {
         pass: false,
@@ -33,7 +36,8 @@ expect.extend({
       };
     }
 
-    fireEvent.pointerEnter(received);
+    await user.hover(received);
+
     const tooltip = await screen.findByRole('tooltip');
 
     const result = tooltip.textContent?.includes(content)
@@ -47,7 +51,7 @@ expect.extend({
             `Tooltip content "${tooltip.textContent}" does not contain expected "${content}"`,
         };
 
-    fireEvent.pointerLeave(received);
+    await user.keyboard('{Escape}');
 
     await waitFor(() => {
       expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
index d5ba01e248570dd1494ac34035e5bbbc0ecd66f1..162b11f9c0b48840a94aa68dbeff473ed62939c8 100644 (file)
@@ -20,7 +20,7 @@
 import { css } from '@emotion/react';
 import styled from '@emotion/styled';
 import classNames from 'classnames';
-import React from 'react';
+import React, { ForwardedRef, forwardRef } from 'react';
 import tw from 'twin.macro';
 import { INPUT_SIZES } from '../helpers/constants';
 import { themeBorder, themeColor, themeContrast } from '../helpers/theme';
@@ -138,22 +138,26 @@ interface ItemButtonProps extends ListItemProps {
   onClick: React.MouseEventHandler<HTMLButtonElement>;
 }
 
-export function ItemButton(props: ItemButtonProps) {
-  const { children, className, disabled, icon, innerRef, onClick, selected, ...liProps } = props;
-  return (
-    <li ref={innerRef} role="none" {...liProps}>
-      <ItemButtonStyled
-        className={classNames(className, { disabled, selected })}
-        disabled={disabled}
-        onClick={onClick}
-        role="menuitem"
-      >
-        {icon}
-        {children}
-      </ItemButtonStyled>
-    </li>
-  );
-}
+export const ItemButton = forwardRef(
+  (props: ItemButtonProps, ref: ForwardedRef<HTMLButtonElement>) => {
+    const { children, className, disabled, icon, innerRef, onClick, selected, ...liProps } = props;
+    return (
+      <li ref={innerRef} role="none" {...liProps}>
+        <ItemButtonStyled
+          className={classNames(className, { disabled, selected })}
+          disabled={disabled}
+          onClick={onClick}
+          ref={ref}
+          role="menuitem"
+        >
+          {icon}
+          {children}
+        </ItemButtonStyled>
+      </li>
+    );
+  },
+);
+ItemButton.displayName = 'ItemButton';
 
 export const ItemDangerButton = styled(ItemButton)`
   --color: ${themeContrast('dropdownMenuDanger')};
index accd153c954c1ce983bdc9e976dfbd9894f193b9..9458645fa21d5ece3e9ccd0e9c815c5270bfec9f 100644 (file)
 import { css } from '@emotion/react';
 import styled from '@emotion/styled';
 import classNames from 'classnames';
-import React from 'react';
+import React, { ForwardedRef, MouseEvent, forwardRef, useCallback } from 'react';
 import tw from 'twin.macro';
 import { OPACITY_20_PERCENT } from '../helpers/constants';
 import { themeBorder, themeColor, themeContrast } from '../helpers/theme';
 import { isDefined } from '../helpers/types';
 import { ThemedProps } from '../types/theme';
-import { BaseLink, LinkProps } from './Link';
 import { IconProps } from './icons/Icon';
 
 export type InteractiveIconSize = 'small' | 'medium';
@@ -41,63 +40,54 @@ export interface InteractiveIconProps {
   iconProps?: IconProps;
   id?: string;
   innerRef?: React.Ref<HTMLButtonElement>;
-  onClick?: VoidFunction;
+  onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
   size?: InteractiveIconSize;
   stopPropagation?: boolean;
-  to?: LinkProps['to'];
 }
 
-export class InteractiveIconBase extends React.PureComponent<InteractiveIconProps> {
-  handleClick = (event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
-    const { disabled, onClick, stopPropagation = true } = this.props;
-
-    if (stopPropagation) {
-      event.stopPropagation();
-    }
-
-    if (onClick && !disabled) {
-      onClick();
-    }
-  };
-
-  render() {
+export const InteractiveIconBase = forwardRef(
+  (props: InteractiveIconProps, ref: ForwardedRef<HTMLButtonElement>) => {
     const {
       Icon,
       children,
       disabled,
-      innerRef,
       onClick,
       size = 'medium',
-      to,
       iconProps = {},
+      stopPropagation = true,
       ...htmlProps
-    } = this.props;
+    } = props;
+
+    const handleClick = useCallback(
+      (event: React.MouseEvent<HTMLButtonElement>) => {
+        if (stopPropagation) {
+          event.stopPropagation();
+        }
+
+        if (onClick && !disabled) {
+          onClick(event);
+        }
+      },
+      [disabled, onClick, stopPropagation],
+    );
 
-    const props = {
+    const propsForInteractiveWrapper = {
       ...htmlProps,
       'aria-disabled': disabled,
       disabled,
       size,
-      type: 'button' as const,
     };
 
-    if (to) {
-      return (
-        <IconLink {...props} onClick={onClick} showExternalIcon={false} stopPropagation to={to}>
-          <Icon className={classNames({ 'sw-mr-1': isDefined(children) })} {...iconProps} />
-          {children}
-        </IconLink>
-      );
-    }
-
     return (
-      <IconButton {...props} onClick={this.handleClick} ref={innerRef}>
+      <IconButton {...propsForInteractiveWrapper} onClick={handleClick} ref={ref} type="button">
         <Icon className={classNames({ 'sw-mr-1': isDefined(children) })} {...iconProps} />
         {children}
       </IconButton>
     );
-  }
-}
+  },
+);
+
+InteractiveIconBase.displayName = 'InteractiveIconBase';
 
 const buttonIconStyle = (props: ThemedProps & { size: InteractiveIconSize }) => css`
   box-sizing: border-box;
@@ -141,17 +131,11 @@ const buttonIconStyle = (props: ThemedProps & { size: InteractiveIconSize }) =>
   }
 `;
 
-const IconLink = styled(BaseLink)`
-  ${buttonIconStyle}
-`;
-
 const IconButton = styled.button`
   ${buttonIconStyle}
 `;
 
-export const InteractiveIcon: React.FC<React.PropsWithChildren<InteractiveIconProps>> = styled(
-  InteractiveIconBase,
-)`
+export const InteractiveIcon = styled(InteractiveIconBase)`
   --background: ${themeColor('interactiveIcon')};
   --backgroundHover: ${themeColor('interactiveIconHover')};
   --color: ${({ currentColor, theme }) =>
@@ -160,14 +144,11 @@ export const InteractiveIcon: React.FC<React.PropsWithChildren<InteractiveIconPr
   --focus: ${themeColor('interactiveIconFocus', OPACITY_20_PERCENT)};
 `;
 
-export const DiscreetInteractiveIcon: React.FC<React.PropsWithChildren<InteractiveIconProps>> =
-  styled(InteractiveIcon)`
-    --color: ${themeColor('discreetInteractiveIcon')};
-  `;
+export const DiscreetInteractiveIcon = styled(InteractiveIcon)`
+  --color: ${themeColor('discreetInteractiveIcon')};
+`;
 
-export const DestructiveIcon: React.FC<React.PropsWithChildren<InteractiveIconProps>> = styled(
-  InteractiveIconBase,
-)`
+export const DestructiveIcon = styled(InteractiveIconBase)`
   --background: ${themeColor('destructiveIcon')};
   --backgroundHover: ${themeColor('destructiveIconHover')};
   --color: ${themeContrast('destructiveIcon')};
@@ -175,13 +156,12 @@ export const DestructiveIcon: React.FC<React.PropsWithChildren<InteractiveIconPr
   --focus: ${themeColor('destructiveIconFocus', OPACITY_20_PERCENT)};
 `;
 
-export const DismissProductNewsIcon: React.FC<React.PropsWithChildren<InteractiveIconProps>> =
-  styled(InteractiveIcon)`
-    --background: ${themeColor('productNews')};
-    --backgroundHover: ${themeColor('productNewsHover')};
-    --color: ${themeContrast('productNews')};
-    --colorHover: ${themeContrast('productNewsHover')};
-    --focus: ${themeColor('interactiveIconFocus', OPACITY_20_PERCENT)};
+export const DismissProductNewsIcon = styled(InteractiveIcon)`
+  --background: ${themeColor('productNews')};
+  --backgroundHover: ${themeColor('productNewsHover')};
+  --color: ${themeContrast('productNews')};
+  --colorHover: ${themeContrast('productNewsHover')};
+  --focus: ${themeColor('interactiveIconFocus', OPACITY_20_PERCENT)};
 
-    height: 28px;
-  `;
+  height: 28px;
+`;
index b92c841ad687d4c12cd7cfaf7662217cec609375..053c54d9033080e3f09e330be7cff2bd3a89348c 100644 (file)
@@ -31,11 +31,11 @@ import {
   PopupPlacement,
   popupPositioning,
 } from '../helpers/positioning';
-import { themeColor, themeContrast } from '../helpers/theme';
+import { themeColor } from '../helpers/theme';
 
 const MILLISECONDS_IN_A_SECOND = 1000;
 
-export interface TooltipProps {
+interface TooltipProps {
   children: React.ReactElement;
   mouseEnterDelay?: number;
   mouseLeaveDelay?: number;
@@ -67,6 +67,19 @@ function isMeasured(state: State): state is OwnState & Measurements {
   return state.height !== undefined;
 }
 
+/** @deprecated Use {@link Echoes.Tooltip | Tooltip} from Echoes instead.
+ *
+ * Echoes Tooltip component should mainly be used on interactive element and contain very simple text based content.
+ * If the content is more complex use a Popover component instead (not available yet).
+ *
+ * Some of the props have changed or been renamed:
+ * - `children` is the trigger for the tooltip, should be an interactive Element. If not an Echoes component, make sure the component forwards the props and the ref to an interactive DOM node, it's needed by the tooltip to position itself.
+ * - `overlay` is now `content`, that's the tooltip content. It's a ReactNode for convenience but should render only text based content, no interactivity is allowed inside the tooltip.
+ * - ~`mouseEnterDelay`~ doesn't exist anymore, was mostly used in situation that should be replaced by a Popover component.
+ * - ~`mouseLeaveDelay`~ doesn't exist anymore, was mostly used in situation that should be replaced by a Popover component.
+ * - `placement` is now `align` and `side`, based on the {@link Echoes.TooltipAlign | TooltipAlign} and {@link Echoes.TooltipSide | TooltipSide} enums.
+ * - `visible` is now `isOpen`
+ */
 export function Tooltip(props: TooltipProps) {
   // overlay is a ReactNode, so it can be a boolean, `undefined` or `null`
   // this allows to easily render a tooltip conditionally
@@ -517,16 +530,17 @@ const TooltipWrapperArrow = styled.div`
 `;
 
 export const TooltipWrapperInner = styled.div`
-  color: ${themeContrast('tooltipBackground')};
-  background-color: ${themeColor('tooltipBackground')};
+  font: var(--echoes-typography-paragraph-small-regular);
+  padding: var(--echoes-dimension-space-50) var(--echoes-dimension-space-150);
+  color: var(--echoes-color-text-on-color);
+  background-color: var(--echoes-color-background-inverse);
+  border-radius: var(--echoes-border-radius-200);
 
   ${tw`sw-max-w-[22rem]`}
-  ${tw`sw-py-3 sw-px-4`};
   ${tw`sw-overflow-hidden`};
   ${tw`sw-text-left`};
   ${tw`sw-no-underline`};
   ${tw`sw-break-words`};
-  ${tw`sw-rounded-2`};
 
   hr {
     background-color: ${themeColor('tooltipSeparator')};
index a2d565d15f2a988a8004e95429bff76c2cde522a..f5155fd7f1efd6ec5ad6d2d6176823a5e678e6c5 100644 (file)
@@ -74,7 +74,6 @@ export * from './Tabs';
 export * from './Tags';
 export * from './Text';
 export * from './TextAccordion';
-export { Tooltip } from './Tooltip';
 export { TopBar } from './TopBar';
 export * from './TreeMap';
 export * from './TreeMapRect';
index 9bdae72f60477e256a3474ed618c00db59345082..bf1555b1b939b56af4b6373da010260ee4386998 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import styled from '@emotion/styled';
+import { forwardRef } from 'react';
 import tw from 'twin.macro';
 import { getProp, themeColor, themeContrast } from '../../helpers/theme';
 import { RatingLabel } from '../types/measures';
@@ -38,37 +39,37 @@ const SIZE_MAPPING = {
   xl: '4rem',
 };
 
-export function MetricsRatingBadge({
-  className,
-  size = 'sm',
-  label,
-  rating,
-  ...ariaAttrs
-}: Readonly<Props>) {
-  if (!rating) {
+export const MetricsRatingBadge = forwardRef<HTMLDivElement, Props>(
+  ({ className, size = 'sm', label, rating, ...ariaAttrs }: Readonly<Props>, ref) => {
+    if (!rating) {
+      return (
+        <StyledNoRatingBadge
+          aria-label={label}
+          className={className}
+          ref={ref}
+          size={SIZE_MAPPING[size]}
+          {...ariaAttrs}
+        >
+          â€”
+        </StyledNoRatingBadge>
+      );
+    }
     return (
-      <StyledNoRatingBadge
+      <MetricsRatingBadgeStyled
         aria-label={label}
         className={className}
+        rating={rating}
+        ref={ref}
         size={SIZE_MAPPING[size]}
         {...ariaAttrs}
       >
-        â€”
-      </StyledNoRatingBadge>
+        {rating}
+      </MetricsRatingBadgeStyled>
     );
-  }
-  return (
-    <MetricsRatingBadgeStyled
-      aria-label={label}
-      className={className}
-      rating={rating}
-      size={SIZE_MAPPING[size]}
-      {...ariaAttrs}
-    >
-      {rating}
-    </MetricsRatingBadgeStyled>
-  );
-}
+  },
+);
+
+MetricsRatingBadge.displayName = 'MetricsRatingBadge';
 
 const StyledNoRatingBadge = styled.div<{ size: string }>`
   display: inline-flex;
index db5693e7f636a450aa2665dfe0461c1a872239e8..28fb7f87113c327f0e2cea9f11bcc59ffd8d1fd5 100644 (file)
@@ -19,7 +19,7 @@
  */
 import { css } from '@emotion/react';
 import styled from '@emotion/styled';
-import React from 'react';
+import React, { MouseEvent, ReactNode, forwardRef, useCallback } from 'react';
 import tw from 'twin.macro';
 import { BaseLink, LinkProps } from '../../../components/Link';
 import { themeBorder, themeColor, themeContrast } from '../../../helpers/theme';
@@ -31,15 +31,14 @@ type AllowedButtonAttributes = Pick<
 >;
 
 export interface ButtonProps extends AllowedButtonAttributes {
-  children?: React.ReactNode;
+  children?: ReactNode;
   className?: string;
   disabled?: boolean;
   download?: string;
-  icon?: React.ReactNode;
-  innerRef?: React.Ref<HTMLButtonElement>;
+  icon?: ReactNode;
   isExternal?: LinkProps['isExternal'];
+  onClick?: (event: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => unknown;
 
-  onClick?: (event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => unknown;
   preventDefault?: boolean;
   reloadDocument?: LinkProps['reloadDocument'];
   showExternalIcon?: boolean;
@@ -48,62 +47,60 @@ export interface ButtonProps extends AllowedButtonAttributes {
   to?: LinkProps['to'];
 }
 
-export class Button extends React.PureComponent<ButtonProps> {
-  handleClick = (event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
-    const { disabled, onClick, stopPropagation = false, type } = this.props;
-    const { preventDefault = type !== 'submit' } = this.props;
-
-    if (preventDefault || disabled) {
-      event.preventDefault();
-    }
-
-    if (stopPropagation) {
-      event.stopPropagation();
-    }
-
-    if (onClick && !disabled) {
-      onClick(event);
-    }
+export const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
+  const {
+    children,
+    disabled,
+    icon,
+    onClick,
+    preventDefault = props.type !== 'submit',
+    stopPropagation = false,
+    to,
+    type = 'button',
+    ...htmlProps
+  } = props;
+
+  const handleClick = useCallback(
+    (event: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
+      if (preventDefault || disabled) {
+        event.preventDefault();
+      }
+
+      if (stopPropagation) {
+        event.stopPropagation();
+      }
+
+      if (onClick && !disabled) {
+        onClick(event);
+      }
+    },
+    [disabled, onClick, preventDefault, stopPropagation],
+  );
+
+  const buttonProps = {
+    ...htmlProps,
+    'aria-disabled': disabled,
+    disabled,
+    type,
   };
 
-  render() {
-    const {
-      children,
-      disabled,
-      icon,
-      innerRef,
-      onClick,
-      preventDefault,
-      stopPropagation,
-      to,
-      type = 'button',
-      ...htmlProps
-    } = this.props;
-
-    const props = {
-      ...htmlProps,
-      'aria-disabled': disabled,
-      disabled,
-      type,
-    };
-
-    if (to) {
-      return (
-        <BaseButtonLink {...props} onClick={onClick} to={to}>
-          {icon}
-          {children}
-        </BaseButtonLink>
-      );
-    }
-
+  if (to) {
     return (
-      <BaseButton {...props} onClick={this.handleClick} ref={innerRef}>
+      <BaseButtonLink {...buttonProps} onClick={onClick} to={to}>
         {icon}
         {children}
-      </BaseButton>
+      </BaseButtonLink>
     );
   }
-}
+
+  return (
+    <BaseButton {...buttonProps} onClick={handleClick} ref={ref}>
+      {icon}
+      {children}
+    </BaseButton>
+  );
+});
+Button.displayName = 'Button';
 
 export const buttonStyle = (props: ThemedProps) => css`
   box-sizing: border-box;
index cb034260408a643d52382812774954c647c001a8..5116cc33a10f5f6a7b3740a86c92c3a7482b8ee8 100644 (file)
@@ -19,6 +19,8 @@
  */
 
 import { ThemeProvider } from '@emotion/react';
+import styled from '@emotion/styled';
+import { TooltipProvider } from '@sonarsource/echoes-react';
 import { QueryClientProvider } from '@tanstack/react-query';
 import { ToastMessageContainer, lightTheme } from 'design-system';
 import * as React from 'react';
@@ -275,7 +277,11 @@ export default function startReactApp(
                   <GlobalStyles />
                   <ToastMessageContainer />
                   <Helmet titleTemplate={translate('page_title.template.default')} />
-                  <RouterProvider router={router} />
+                  <StackContext>
+                    <TooltipProvider>
+                      <RouterProvider router={router} />
+                    </TooltipProvider>
+                  </StackContext>
                 </QueryClientProvider>
               </ThemeProvider>
             </RawIntlProvider>
@@ -285,3 +291,12 @@ export default function startReactApp(
     </HelmetProvider>,
   );
 }
+
+/*
+ * This ensures tooltips and other "floating" elements appended to the body are placed on top
+ * of the rest of the UI.
+ */
+const StackContext = styled.div`
+  z-index: 0;
+  position: relative;
+`;
index 833934390739e361e05ab42252076cf46913c40a..f4320b01d5310caa1ec8257d3d421ac631fee29a 100644 (file)
@@ -17,7 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { LightLabel, Note, SeparatorCircleIcon, Tooltip } from 'design-system';
+import { Tooltip } from '@sonarsource/echoes-react';
+import { LightLabel, Note, SeparatorCircleIcon } from 'design-system';
 import React from 'react';
 import DateFromNow from '../../../components/intl/DateFromNow';
 import IssueSeverity from '../../../components/issue/components/IssueSeverity';
@@ -62,11 +63,11 @@ export default function IssueHeaderMeta({ issue }: Readonly<Props>) {
       </div>
       <SeparatorCircleIcon />
 
-      {!!issue.codeVariants?.length && (
+      {(issue.codeVariants?.length ?? 0) > 0 && (
         <>
           <div className="sw-flex sw-gap-1">
             <span>{translate('issue.code_variants')}</span>
-            <Tooltip overlay={issue.codeVariants?.join(', ')}>
+            <Tooltip content={issue.codeVariants?.join(', ')}>
               <span className="sw-font-semibold">
                 <LightLabel>{issue.codeVariants?.join(', ')}</LightLabel>
               </span>
index 97a2b330269ab6eda421dffe48954ee5a6b52ce9..73d7c60c3ee9de1de3c2f2fe9b390670ecb155ba 100644 (file)
@@ -18,8 +18,9 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import styled from '@emotion/styled';
+import { Tooltip } from '@sonarsource/echoes-react';
 import classNames from 'classnames';
-import { DiscreetLinkBox, Tooltip, themeColor, themeContrast } from 'design-system';
+import { DiscreetLinkBox, themeColor, themeContrast } from 'design-system';
 import * as React from 'react';
 import { useIntl } from 'react-intl';
 import { formatMeasure } from '~sonar-aligned/helpers/measures';
@@ -72,7 +73,7 @@ export function SoftwareImpactMeasureBreakdownCard(
 
   return (
     <Tooltip
-      overlay={intl.formatMessage({
+      content={intl.formatMessage({
         id: `overview.measures.software_impact.severity.${severity}.tooltip`,
       })}
     >
index 0586663c04d27c313ca3b0729ddd3f05ad8edf9e..00093074e7c8c0ed1f223a6f28d381dcab537fbc 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import styled from '@emotion/styled';
-import { LinkHighlight, LinkStandalone } from '@sonarsource/echoes-react';
+import { LinkHighlight, LinkStandalone, Tooltip } from '@sonarsource/echoes-react';
 import { Badge, LightGreyCard, LightGreyCardTitle, TextBold, TextSubdued } from 'design-system';
 import * as React from 'react';
 import { FormattedMessage, useIntl } from 'react-intl';
 import { formatMeasure } from '~sonar-aligned/helpers/measures';
 import { getComponentIssuesUrl } from '~sonar-aligned/helpers/urls';
 import { MetricKey, MetricType } from '~sonar-aligned/types/metrics';
-import Tooltip from '../../../components/controls/Tooltip';
 import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils';
 import {
   SOFTWARE_QUALITIES_METRIC_KEYS_MAP,
@@ -109,7 +108,7 @@ export function SoftwareImpactMeasureCard(props: Readonly<SoftwareImpactBreakdow
         <div className="sw-flex sw-mt-4">
           <div className="sw-flex sw-gap-1 sw-items-center">
             {count ? (
-              <Tooltip overlay={countTooltipOverlay}>
+              <Tooltip content={countTooltipOverlay}>
                 <LinkStandalone
                   data-testid={`overview__software-impact-${softwareQuality}`}
                   aria-label={intl.formatMessage(
index 1f6c0edaaa47832be981f7614c7908d7284b0211..6f0f1f4bc7f2ac0da3369b191aa2c065ca6f41de 100644 (file)
@@ -17,7 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { MetricsRatingBadge, Tooltip } from 'design-system';
+import { Tooltip } from '@sonarsource/echoes-react';
+import { MetricsRatingBadge } from 'design-system';
 import * as React from 'react';
 import { useIntl } from 'react-intl';
 import { formatRating } from '../../../helpers/measures';
@@ -36,28 +37,33 @@ export function SoftwareImpactMeasureRating(props: Readonly<SoftwareImpactMeasur
 
   const rating = formatRating(value);
 
+  const additionalInfo =
+    SoftwareImpactRatingTooltip({
+      rating,
+      softwareQuality,
+    }) ?? undefined;
+
   return (
-    <Tooltip
-      overlay={SoftwareImpactRatingTooltip({
-        rating,
-        softwareQuality,
-      })}
-    >
-      <MetricsRatingBadge
-        size="md"
-        className="sw-text-sm"
-        rating={rating}
-        label={intl.formatMessage(
-          {
-            id: 'overview.project.software_impact.has_rating',
-          },
-          {
-            softwareQuality: intl.formatMessage({ id: `software_quality.${softwareQuality}` }),
-            rating,
-          },
-        )}
-      />
-    </Tooltip>
+    <>
+      <Tooltip content={additionalInfo}>
+        <MetricsRatingBadge
+          size="md"
+          className="sw-text-sm"
+          rating={rating}
+          label={intl.formatMessage(
+            {
+              id: 'overview.project.software_impact.has_rating',
+            },
+            {
+              softwareQuality: intl.formatMessage({ id: `software_quality.${softwareQuality}` }),
+              rating,
+            },
+          )}
+        />
+      </Tooltip>
+      {/* The badge is not interactive, so show the tooltip content for screen-readers only */}
+      <span className="sw-sr-only">{additionalInfo}</span>
+    </>
   );
 }
 
index 6dc229906c6d95bfc0b696ed6a69d7736fcd8c11..1036ac37e3a2011f62b2d855c9c6d69f83115714 100644 (file)
@@ -22,11 +22,9 @@ import {
   HelperHintIcon,
   LightGreyCard,
   LightLabel,
-  PopupPlacement,
   SnoozeCircleIcon,
   TextError,
   TextSubdued,
-  Tooltip,
   TrendDownCircleIcon,
   TrendUpCircleIcon,
   themeColor,
@@ -37,6 +35,7 @@ import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like';
 import { formatMeasure } from '~sonar-aligned/helpers/measures';
 import { getComponentIssuesUrl } from '~sonar-aligned/helpers/urls';
 import { MetricKey, MetricType } from '~sonar-aligned/types/metrics';
+import Tooltip from '../../../components/controls/Tooltip';
 import { getLeakValue } from '../../../components/measure/utils';
 import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils';
 import { findMeasure } from '../../../helpers/measures';
@@ -134,17 +133,19 @@ export default function IssueMeasuresCard(
           <>
             {intl.formatMessage({ id: 'overview.pull_request.fixed_issues' })}
             <Tooltip
-              overlay={
+              content={
                 <div className="sw-flex sw-flex-col sw-gap-4">
                   <span>
                     {intl.formatMessage({ id: 'overview.pull_request.fixed_issues.disclaimer' })}
                   </span>
                   <span>
-                    {intl.formatMessage({ id: 'overview.pull_request.fixed_issues.disclaimer.2' })}
+                    {intl.formatMessage({
+                      id: 'overview.pull_request.fixed_issues.disclaimer.2',
+                    })}
                   </span>
                 </div>
               }
-              placement={PopupPlacement.Top}
+              side="top"
             >
               <HelperHintIcon raised />
             </Tooltip>
index 92132eabcb18f88806fd073c55967c62ffa28f1d..49b037cc9f8106fe3255f0023d1497effd85f36d 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { Tooltip, TooltipSide } from '@sonarsource/echoes-react';
 import {
   ActionsDropdown,
   ItemButton,
@@ -24,9 +25,7 @@ import {
   ItemDivider,
   ItemDownload,
   ItemLink,
-  PopupPlacement,
   PopupZLevel,
-  Tooltip,
 } from 'design-system';
 import { some } from 'lodash';
 import * as React from 'react';
@@ -250,8 +249,8 @@ class ProfileActions extends React.PureComponent<Props, State> {
           {actions.copy && (
             <>
               <Tooltip
-                overlay={translateWithParameters('quality_profiles.extend_help', profile.name)}
-                placement={PopupPlacement.Left}
+                content={translateWithParameters('quality_profiles.extend_help', profile.name)}
+                side={TooltipSide.Left}
               >
                 <ItemButton
                   className="it__quality-profiles__extend"
@@ -262,8 +261,8 @@ class ProfileActions extends React.PureComponent<Props, State> {
               </Tooltip>
 
               <Tooltip
-                overlay={translateWithParameters('quality_profiles.copy_help', profile.name)}
-                placement={PopupPlacement.Left}
+                content={translateWithParameters('quality_profiles.copy_help', profile.name)}
+                side={TooltipSide.Left}
               >
                 <ItemButton className="it__quality-profiles__copy" onClick={this.handleCopyClick}>
                   {translate('copy')}
@@ -281,8 +280,8 @@ class ProfileActions extends React.PureComponent<Props, State> {
           {actions.setAsDefault &&
             (hasNoActiveRules ? (
               <Tooltip
-                placement={PopupPlacement.Left}
-                overlay={translate('quality_profiles.cannot_set_default_no_rules')}
+                content={translate('quality_profiles.cannot_set_default_no_rules')}
+                side={TooltipSide.Left}
               >
                 <ItemButton
                   className="it__quality-profiles__set-as-default"
index 08309dd5457c03de46621c997d134eaeb1ac1fa6..672acc4d5745a6c38e4e829775038d69da7d3c12 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import {
-  Badge,
-  ButtonSecondary,
-  ContentCell,
-  Link,
-  Spinner,
-  SubTitle,
-  Table,
-  TableRow,
-  Tooltip,
-} from 'design-system';
+import { Link, Spinner } from '@sonarsource/echoes-react';
+import { Badge, ButtonSecondary, ContentCell, SubTitle, Table, TableRow } from 'design-system';
 import * as React from 'react';
 import { getProfileProjects } from '../../../api/quality-profiles';
 import ListFooter from '../../../components/controls/ListFooter';
@@ -191,21 +182,13 @@ export default class ProfileProjects extends React.PureComponent<Props, State> {
             <SubTitle className="sw-mb-0">{translate('projects')}</SubTitle>
           }
           {profile.actions?.associateProjects && (
-            <Tooltip
-              overlay={
-                hasNoActiveRules
-                  ? translate('quality_profiles.cannot_associate_projects_no_rules')
-                  : null
-              }
+            <ButtonSecondary
+              className="it__quality-profiles__change-projects"
+              onClick={this.handleChangeClick}
+              disabled={hasNoActiveRules}
             >
-              <ButtonSecondary
-                className="it__quality-profiles__change-projects"
-                onClick={this.handleChangeClick}
-                disabled={hasNoActiveRules}
-              >
-                {translate('quality_profiles.change_projects')}
-              </ButtonSecondary>
-            </Tooltip>
+              {translate('quality_profiles.change_projects')}
+            </ButtonSecondary>
           )}
         </div>
 
index 7d8308754e05c92337898dfea905708c6f99958d..b2bd2d3948c9715917d232e59ba17ebc257a7241 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { Tooltip } from '@sonarsource/echoes-react';
 import {
   ButtonPrimary,
   ButtonSecondary,
   DangerButtonSecondary,
+  FlagMessage,
   SubHeading,
-  Tooltip,
 } from 'design-system';
 import React, { ReactElement } from 'react';
 import { translate } from '../../../../helpers/l10n';
@@ -50,19 +51,20 @@ export default function ConfigurationDetails(props: Readonly<Props>) {
           {title}
         </SubHeading>
         <p>{url}</p>
-        <Tooltip
-          overlay={!canDisable ? translate('settings.authentication.form.disable.tooltip') : null}
-        >
-          {enabled ? (
-            <ButtonSecondary className="sw-mt-4" onClick={onToggle} disabled={!canDisable}>
-              {translate('settings.authentication.form.disable')}
-            </ButtonSecondary>
-          ) : (
-            <ButtonPrimary className="sw-mt-4" onClick={onToggle} disabled={!canDisable}>
-              {translate('settings.authentication.form.enable')}
-            </ButtonPrimary>
-          )}
-        </Tooltip>
+        {enabled ? (
+          <ButtonSecondary className="sw-mt-4" onClick={onToggle} disabled={!canDisable}>
+            {translate('settings.authentication.form.disable')}
+          </ButtonSecondary>
+        ) : (
+          <ButtonPrimary className="sw-mt-4" onClick={onToggle} disabled={!canDisable}>
+            {translate('settings.authentication.form.enable')}
+          </ButtonPrimary>
+        )}
+        {!canDisable && (
+          <FlagMessage className="sw-mt-2" variant="warning">
+            {translate('settings.authentication.form.disable.tooltip')}
+          </FlagMessage>
+        )}
       </div>
       <div className="sw-flex sw-gap-2 sw-flex-nowrap sw-shrink-0">
         {extraActions}
@@ -70,7 +72,7 @@ export default function ConfigurationDetails(props: Readonly<Props>) {
           {translate('settings.authentication.form.edit')}
         </ButtonSecondary>
         <Tooltip
-          overlay={
+          content={
             enabled || isDeleting ? translate('settings.authentication.form.delete.tooltip') : null
           }
         >
index 22a28cdb0f299ff03595ae63fff246557e6cb3bc..c3f434223a05b539349ddc12b070bd2b81bd5f6b 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import {
-  ActionCell,
-  Avatar,
-  ContentCell,
-  InteractiveIcon,
-  MenuIcon,
-  Spinner,
-  TableRow,
-  Tooltip,
-} from 'design-system';
+import { IconMoreVertical, Spinner, Tooltip } from '@sonarsource/echoes-react';
+import { ActionCell, Avatar, ContentCell, InteractiveIcon, TableRow } from 'design-system';
 import * as React from 'react';
 import DateFromNow from '../../../components/intl/DateFromNow';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
@@ -85,12 +77,12 @@ export default function UserListItem(props: Readonly<UserListItemProps>) {
         <DateFromNow date={sonarLintLastConnectionDate ?? ''} hourPrecision />
       </ContentCell>
       <ContentCell>
-        <Spinner loading={groupsAreLoading}>
+        <Spinner isLoading={groupsAreLoading}>
           {groupsCount}
           {manageProvider === undefined && (
-            <Tooltip overlay={translate('users.update_groups')}>
+            <Tooltip content={translate('users.update_groups')}>
               <InteractiveIcon
-                Icon={MenuIcon}
+                Icon={IconMoreVertical}
                 className="it__user-groups sw-ml-2"
                 aria-label={translateWithParameters('users.update_users_groups', user.login)}
                 onClick={() => setOpenGroupForm(true)}
@@ -101,11 +93,11 @@ export default function UserListItem(props: Readonly<UserListItemProps>) {
         </Spinner>
       </ContentCell>
       <ContentCell>
-        <Spinner loading={tokensAreLoading}>
+        <Spinner isLoading={tokensAreLoading}>
           {tokens?.length}
-          <Tooltip overlay={translateWithParameters('users.update_tokens')}>
+          <Tooltip content={translateWithParameters('users.update_tokens')}>
             <InteractiveIcon
-              Icon={MenuIcon}
+              Icon={IconMoreVertical}
               className="it__user-tokens sw-ml-2"
               aria-label={translateWithParameters('users.update_tokens_for_x', name ?? login)}
               onClick={() => setOpenTokenForm(true)}
index 413629c903a30f64c0f852e7c79969a61fa0a8ea..343208ac95c687bdf9abfc253dbeef81bd42304c 100644 (file)
 }
 
 .tooltip-inner {
+  font: var(--echoes-typography-paragraph-small-regular);
   max-width: 22rem;
   text-align: left;
   text-decoration: none;
-  border-radius: 8px;
+  border-radius: var(--echoes-border-radius-200);
   overflow: hidden;
   word-break: break-word;
-  padding: 12px 17px;
-  color: #eff2f9;
-  background-color: #2a2f40;
+  padding: var(--echoes-dimension-space-50) var(--echoes-dimension-space-150);
+  color: var(--echoes-color-text-on-color);
+  background-color: var(--echoes-color-background-inverse);
 }
 
 .tooltip-inner .alert {
index a9b2ab472debf055bb97fc841743b4fb6f60e063..56e463fb7f1460296ffa529ae1058b1679076834 100644 (file)
@@ -31,7 +31,7 @@ import './Tooltip.css';
 
 export type Placement = 'bottom' | 'right' | 'left' | 'top';
 
-export interface TooltipProps {
+interface TooltipProps {
   classNameSpace?: string;
   children: React.ReactElement;
   mouseEnterDelay?: number;
@@ -74,6 +74,19 @@ function isMeasured(state: State): state is OwnState & Measurements {
   return state.height !== undefined;
 }
 
+/** @deprecated Use {@link Echoes.Tooltip | Tooltip} from Echoes instead.
+ *
+ * Echoes Tooltip component should mainly be used on interactive element and contain very simple text based content.
+ * If the content is more complex use a Popover component instead (not available yet).
+ *
+ * Some of the props have changed or been renamed:
+ * - `children` is the trigger for the tooltip, should be an interactive Element. If not an Echoes component, make sure the component forwards the props and the ref to an interactive DOM node, it's needed by the tooltip to position itself.
+ * - `overlay` is now `content`, that's the tooltip content. It's a ReactNode for convenience but should render only text based content, no interactivity is allowed inside the tooltip.
+ * - ~`mouseEnterDelay`~ doesn't exist anymore, was mostly used in situation that should be replaced by a Popover component.
+ * - ~`mouseLeaveDelay`~ doesn't exist anymore, was mostly used in situation that should be replaced by a Popover component.
+ * - `placement` is now `align` and `side`, based on the {@link Echoes.TooltipAlign | TooltipAlign} and {@link Echoes.TooltipSide | TooltipSide} enums.
+ * - `visible` is now `isOpen`
+ */
 export default function Tooltip(props: TooltipProps) {
   // `overlay` is a ReactNode, so it can be `undefined` or `null`. This allows to easily
   // render a tooltip conditionally. More generally, we avoid rendering empty tooltips.
index 81cab9095c35ccc0fa197a53d4b196d6a1818791..3777c8b68ad11c95fb2eef5392f1aa31b9c16b25 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import {
-  HelperHintIcon,
-  ItemButton,
-  PageContentFontWrapper,
-  PopupPlacement,
-  TextBold,
-  TextMuted,
-  Tooltip,
-} from 'design-system';
+import { IconQuestionMark } from '@sonarsource/echoes-react';
+import { ItemButton, PageContentFontWrapper, TextBold, TextMuted } from 'design-system';
 import * as React from 'react';
 import { useIntl } from 'react-intl';
 import { translate } from '../../../helpers/l10n';
+import HelpTooltip from '../../../sonar-aligned/components/controls/HelpTooltip';
 import { IssueTransition } from '../../../types/issues';
 
 type Props = {
@@ -68,9 +62,9 @@ export function IssueTransitionItem({ transition, selected, onSelectTransition }
         <PageContentFontWrapper className="sw-font-semibold sw-flex sw-gap-1 sw-items-center">
           <TextBold name={intl.formatMessage({ id: `issue.transition.${transition}` })} />
           {tooltips[transition] && (
-            <Tooltip overlay={<div>{tooltips[transition]}</div>} placement={PopupPlacement.Right}>
-              <HelperHintIcon />
-            </Tooltip>
+            <HelpTooltip overlay={tooltips[transition]} placement="right">
+              <IconQuestionMark />
+            </HelpTooltip>
           )}
         </PageContentFontWrapper>
         <TextMuted text={translate('issue.transition', transition, 'description')} />
diff --git a/server/sonar-web/src/main/js/components/tags/TagsList.css b/server/sonar-web/src/main/js/components/tags/TagsList.css
deleted file mode 100644 (file)
index 2d6c7b6..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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.
- */
-.tags-list {
-  white-space: nowrap;
-  line-height: 16px;
-}
-
-.tags-list i::before {
-  font-size: var(--smallFontSize);
-}
-
-.tags-list span {
-  display: inline-block;
-  vertical-align: middle;
-  text-align: left;
-  max-width: 220px;
-  padding-left: 4px;
-  padding-right: 4px;
-}
index 6c75f6a59784317a17e3057b8c06f7a76e94dba1..a24db5de119ae18c9cdc24f3ffa1df319fe2cbc4 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { PopupPlacement, Tags, Tooltip } from 'design-system';
+import { PopupPlacement, Tags } from 'design-system';
 import * as React from 'react';
 import { translate, translateWithParameters } from '../../helpers/l10n';
-import './TagsList.css';
+import Tooltip from '../controls/Tooltip';
 
 interface Props {
   allowUpdate?: boolean;
index 2faaba061ded3cbcf472966d0574ff4d36c2cd12..e4eb8400ac54964e2115ac99db1094190a4adc64 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
+import { TooltipProvider } from '@sonarsource/echoes-react';
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
 import { Matcher, RenderResult, render, screen, within } from '@testing-library/react';
 import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
@@ -126,11 +127,13 @@ export function renderComponent(
             <AvailableFeaturesContext.Provider value={featureList}>
               <CurrentUserContextProvider currentUser={currentUser}>
                 <AppStateContextProvider appState={appState}>
-                  <MemoryRouter initialEntries={[pathname]}>
-                    <Routes>
-                      <Route path="*" element={children} />
-                    </Routes>
-                  </MemoryRouter>
+                  <TooltipProvider delayDuration={0}>
+                    <MemoryRouter initialEntries={[pathname]}>
+                      <Routes>
+                        <Route path="*" element={children} />
+                      </Routes>
+                    </MemoryRouter>
+                  </TooltipProvider>
                 </AppStateContextProvider>
               </CurrentUserContextProvider>
             </AvailableFeaturesContext.Provider>
@@ -238,7 +241,9 @@ function renderRoutedApp(
                     <QueryClientProvider client={queryClient}>
                       <ToastMessageContainer />
 
-                      <RouterProvider router={router} />
+                      <TooltipProvider delayDuration={0}>
+                        <RouterProvider router={router} />
+                      </TooltipProvider>
                     </QueryClientProvider>
                   </IndexationContextProvider>
                 </AppStateContextProvider>
index b36e8009bd5a7d8766bf0966071bc2402da2d74f..0ab20b45acf83f0759738e1b7fd7336cbbc0d917 100644 (file)
@@ -17,8 +17,9 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { Tooltip } from '@sonarsource/echoes-react';
 import classNames from 'classnames';
-import { MetricsRatingBadge, QualityGateIndicator, RatingLabel, Tooltip } from 'design-system';
+import { MetricsRatingBadge, QualityGateIndicator, RatingLabel } from 'design-system';
 import React from 'react';
 import { useIntl } from 'react-intl';
 import { formatMeasure } from '~sonar-aligned/helpers/measures';
@@ -105,8 +106,11 @@ export default function Measure({
   );
 
   return (
-    <Tooltip overlay={tooltip}>
-      <span className={className}>{rating}</span>
+    <Tooltip content={tooltip}>
+      {/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}
+      <span className={className} tabIndex={0}>
+        {rating}
+      </span>
     </Tooltip>
   );
 }