aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/design-system
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/design-system')
-rw-r--r--server/sonar-web/design-system/src/components/DropdownMenu.tsx38
-rw-r--r--server/sonar-web/design-system/src/components/InteractiveIcon.tsx96
-rw-r--r--server/sonar-web/design-system/src/components/Tooltip.tsx26
-rw-r--r--server/sonar-web/design-system/src/components/index.ts1
-rw-r--r--server/sonar-web/design-system/src/sonar-aligned/components/MetricsRatingBadge.tsx49
-rw-r--r--server/sonar-web/design-system/src/sonar-aligned/components/buttons/Button.tsx105
6 files changed, 155 insertions, 160 deletions
diff --git a/server/sonar-web/design-system/src/components/DropdownMenu.tsx b/server/sonar-web/design-system/src/components/DropdownMenu.tsx
index d5ba01e2485..162b11f9c0b 100644
--- a/server/sonar-web/design-system/src/components/DropdownMenu.tsx
+++ b/server/sonar-web/design-system/src/components/DropdownMenu.tsx
@@ -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')};
diff --git a/server/sonar-web/design-system/src/components/InteractiveIcon.tsx b/server/sonar-web/design-system/src/components/InteractiveIcon.tsx
index accd153c954..9458645fa21 100644
--- a/server/sonar-web/design-system/src/components/InteractiveIcon.tsx
+++ b/server/sonar-web/design-system/src/components/InteractiveIcon.tsx
@@ -20,13 +20,12 @@
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;
+`;
diff --git a/server/sonar-web/design-system/src/components/Tooltip.tsx b/server/sonar-web/design-system/src/components/Tooltip.tsx
index b92c841ad68..053c54d9033 100644
--- a/server/sonar-web/design-system/src/components/Tooltip.tsx
+++ b/server/sonar-web/design-system/src/components/Tooltip.tsx
@@ -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')};
diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts
index a2d565d15f2..f5155fd7f1e 100644
--- a/server/sonar-web/design-system/src/components/index.ts
+++ b/server/sonar-web/design-system/src/components/index.ts
@@ -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';
diff --git a/server/sonar-web/design-system/src/sonar-aligned/components/MetricsRatingBadge.tsx b/server/sonar-web/design-system/src/sonar-aligned/components/MetricsRatingBadge.tsx
index 9bdae72f604..bf1555b1b93 100644
--- a/server/sonar-web/design-system/src/sonar-aligned/components/MetricsRatingBadge.tsx
+++ b/server/sonar-web/design-system/src/sonar-aligned/components/MetricsRatingBadge.tsx
@@ -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;
diff --git a/server/sonar-web/design-system/src/sonar-aligned/components/buttons/Button.tsx b/server/sonar-web/design-system/src/sonar-aligned/components/buttons/Button.tsx
index db5693e7f63..28fb7f87113 100644
--- a/server/sonar-web/design-system/src/sonar-aligned/components/buttons/Button.tsx
+++ b/server/sonar-web/design-system/src/sonar-aligned/components/buttons/Button.tsx
@@ -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;