aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2024-10-07 18:32:15 +0200
committersonartech <sonartech@sonarsource.com>2024-10-08 20:02:47 +0000
commitaef2ea10ecee8df9eadf3d3da4e63c06a9378b8b (patch)
tree840adef6a40df1e22a584f6e4a75b4cd77c1d814
parentf49678493a80f08984b13f8003fca362fe081c9f (diff)
downloadsonarqube-aef2ea10ecee8df9eadf3d3da4e63c06a9378b8b.tar.gz
sonarqube-aef2ea10ecee8df9eadf3d3da4e63c06a9378b8b.zip
SONAR-22290 Fix focus indicator in legacy components
-rw-r--r--server/sonar-web/design-system/src/components/Dropdown.tsx31
-rw-r--r--server/sonar-web/design-system/src/components/DropdownMenu.tsx9
-rw-r--r--server/sonar-web/design-system/src/components/FacetBox.tsx7
-rw-r--r--server/sonar-web/design-system/src/components/SpotlightTour.tsx53
-rw-r--r--server/sonar-web/design-system/src/components/Switch.tsx8
-rw-r--r--server/sonar-web/design-system/src/components/Tabs.tsx4
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/Dropdown-test.tsx17
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/SpotlightTour-test.tsx36
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap48
-rw-r--r--server/sonar-web/design-system/src/components/index.ts2
-rw-r--r--server/sonar-web/design-system/src/components/input/InputField.tsx5
-rw-r--r--server/sonar-web/design-system/src/components/input/InputSearch.tsx8
-rw-r--r--server/sonar-web/design-system/src/components/input/SearchSelectDropdownControl.tsx4
-rw-r--r--server/sonar-web/design-system/src/sonar-aligned/components/ToggleButton.tsx6
-rw-r--r--server/sonar-web/design-system/src/sonar-aligned/components/buttons/Button.tsx10
-rw-r--r--server/sonar-web/design-system/src/sonar-aligned/components/input/SelectCommon.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-it.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx104
-rw-r--r--server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx41
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx72
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx91
-rw-r--r--server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRow.tsx71
-rw-r--r--server/sonar-web/src/main/js/apps/projectLinks/ProjectLinkRow.tsx20
-rw-r--r--server/sonar-web/src/main/js/apps/projectNewCode/components/BranchListRow.tsx69
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx84
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx119
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx16
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfileApp-it.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx223
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.tsx15
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/Languages.tsx64
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx50
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/WebhookItemLatestDelivery.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-it.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/controls/Tooltip.tsx2
36 files changed, 701 insertions, 624 deletions
diff --git a/server/sonar-web/design-system/src/components/Dropdown.tsx b/server/sonar-web/design-system/src/components/Dropdown.tsx
index ab273c08f0c..cf972125f20 100644
--- a/server/sonar-web/design-system/src/components/Dropdown.tsx
+++ b/server/sonar-web/design-system/src/components/Dropdown.tsx
@@ -18,13 +18,10 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
-import { useIntl } from 'react-intl';
import { PopupPlacement, PopupZLevel } from '../helpers/positioning';
import { InputSizeKeys } from '../types/theme';
import { DropdownMenu } from './DropdownMenu';
import { DropdownToggler } from './DropdownToggler';
-import { InteractiveIcon } from './InteractiveIcon';
-import { MenuIcon } from './icons/MenuIcon';
type OnClickCallback = (event?: React.MouseEvent<HTMLElement>) => void;
type A11yAttrs = Pick<React.AriaAttributes, 'aria-controls' | 'aria-expanded' | 'aria-haspopup'> & {
@@ -137,31 +134,3 @@ export class Dropdown extends React.PureComponent<Readonly<Props>, State> {
);
}
}
-
-interface ActionsDropdownProps extends Omit<Props, 'children' | 'overlay'> {
- ariaLabel?: string;
- buttonSize?: 'small' | 'medium';
- children: React.ReactNode;
- toggleClassName?: string;
-}
-
-/** @deprecated Use DropdownMenu.Root and other DropdownMenu.* elements from Echoes instead.
- * See the {@link https://xtranet-sonarsource.atlassian.net/wiki/spaces/Platform/pages/3354918914/DropdownMenus | Migration Guide}
- */
-export function ActionsDropdown(props: Readonly<ActionsDropdownProps>) {
- const { children, buttonSize, ariaLabel, toggleClassName, ...dropdownProps } = props;
-
- const intl = useIntl();
-
- return (
- <Dropdown overlay={children} {...dropdownProps}>
- <InteractiveIcon
- Icon={MenuIcon}
- aria-label={ariaLabel ?? intl.formatMessage({ id: 'menu' })}
- className={toggleClassName}
- size={buttonSize}
- stopPropagation={false}
- />
- </Dropdown>
- );
-}
diff --git a/server/sonar-web/design-system/src/components/DropdownMenu.tsx b/server/sonar-web/design-system/src/components/DropdownMenu.tsx
index 82b89ee251e..dff4f3b11e6 100644
--- a/server/sonar-web/design-system/src/components/DropdownMenu.tsx
+++ b/server/sonar-web/design-system/src/components/DropdownMenu.tsx
@@ -23,7 +23,7 @@ import classNames from 'classnames';
import React, { ForwardedRef, forwardRef } from 'react';
import tw from 'twin.macro';
import { INPUT_SIZES } from '../helpers/constants';
-import { themeBorder, themeColor, themeContrast } from '../helpers/theme';
+import { themeColor, themeContrast } from '../helpers/theme';
import { InputSizeKeys, ThemedProps } from '../types/theme';
import { BaseLink, LinkProps } from './Link';
import NavLink from './NavLink';
@@ -370,12 +370,15 @@ const itemStyle = (props: ThemedProps) => css`
color: var(--color);
background-color: ${themeColor('dropdownMenuFocus')(props)};
text-decoration: none;
- outline: ${themeBorder('focus', 'dropdownMenuFocusBorder')(props)};
- outline-offset: -4px;
border: none;
border-bottom: none;
}
+ &:focus-visible {
+ borderLeft: '2px solid var(--echoes-color-focus-default)',
+ marginLeft: '-2px',
+ }
+
&:disabled,
&.disabled {
color: ${themeContrast('dropdownMenuDisabled')(props)};
diff --git a/server/sonar-web/design-system/src/components/FacetBox.tsx b/server/sonar-web/design-system/src/components/FacetBox.tsx
index 22c4e344ce1..acf9a366e1f 100644
--- a/server/sonar-web/design-system/src/components/FacetBox.tsx
+++ b/server/sonar-web/design-system/src/components/FacetBox.tsx
@@ -188,6 +188,13 @@ const ChevronAndTitle = styled(BareButton)<{
${tw`sw-items-center`};
cursor: ${({ expandable }) => (expandable ? 'pointer' : 'default')};
+
+ &:focus-visible {
+ background: transparent;
+ outline: var(--echoes-focus-border-width-default) solid var(--echoes-color-focus-default);
+ outline-offset: 4px;
+ border-radius: var(--echoes-border-radius-200);
+ }
`;
const ClearIcon = styled(DestructiveIcon)`
diff --git a/server/sonar-web/design-system/src/components/SpotlightTour.tsx b/server/sonar-web/design-system/src/components/SpotlightTour.tsx
index caa21689bb6..69131fcaec7 100644
--- a/server/sonar-web/design-system/src/components/SpotlightTour.tsx
+++ b/server/sonar-web/design-system/src/components/SpotlightTour.tsx
@@ -19,7 +19,14 @@
*/
import { keyframes } from '@emotion/react';
import styled from '@emotion/styled';
-import { LinkStandalone } from '@sonarsource/echoes-react';
+import {
+ Button,
+ ButtonIcon,
+ ButtonVariety,
+ IconX,
+ LinkStandalone,
+ TooltipProvider,
+} from '@sonarsource/echoes-react';
import React from 'react';
import { useIntl } from 'react-intl';
import ReactJoyride, {
@@ -31,9 +38,6 @@ import { LinkProps } from 'react-router-dom';
import tw from 'twin.macro';
import { GLOBAL_POPUP_Z_INDEX, PopupZLevel, themeColor } from '../helpers';
import { findAnchor } from '../helpers/dom';
-import { ButtonPrimary } from '../sonar-aligned/components/buttons';
-import { ButtonLink, WrapperButton } from './buttons';
-import { CloseIcon } from './icons';
import { PopupWrapper } from './popups';
type Placement = 'left' | 'right' | 'top' | 'bottom' | 'center';
@@ -72,7 +76,7 @@ function TooltipComponent({
size,
isLastStep,
backProps,
- skipProps,
+ skipProps: { 'aria-label': skipPropsAriaLabel, ...skipProps },
closeProps,
primaryProps,
stepXofYLabel,
@@ -162,12 +166,13 @@ function TooltipComponent({
}}
>
<strong className="sw-typo-lg-semibold sw-mb-2">{step.title}</strong>
- <WrapperButton
- className="sw-w-[30px] sw-h-[30px] sw--mt-2 sw--mr-2 sw-flex sw-justify-center"
+ <ButtonIcon
+ Icon={IconX}
+ ariaLabel={skipPropsAriaLabel}
+ className="sw--mt-2 sw--mr-2"
+ variety={ButtonVariety.DefaultGhost}
{...skipProps}
- >
- <CloseIcon className="sw-mr-0" />
- </WrapperButton>
+ />
</div>
<div>{step.content}</div>
@@ -188,15 +193,19 @@ function TooltipComponent({
<span />
<div>
{index > 0 && (
- <ButtonLink className="sw-mr-4" {...backProps}>
+ <Button className="sw-mr-4" variety={ButtonVariety.DefaultGhost} {...backProps}>
{backProps.title}
- </ButtonLink>
+ </Button>
)}
{continuous && !isLastStep && (
- <ButtonPrimary {...primaryProps}>{primaryProps.title}</ButtonPrimary>
+ <Button variety={ButtonVariety.Primary} {...primaryProps}>
+ {primaryProps.title}
+ </Button>
)}
{(!continuous || isLastStep) && (
- <ButtonPrimary {...closeProps}>{closeProps.title}</ButtonPrimary>
+ <Button variety={ButtonVariety.Primary} {...closeProps}>
+ {closeProps.title}
+ </Button>
)}
</div>
</div>
@@ -253,13 +262,15 @@ export function SpotlightTour(props: SpotlightTourProps) {
tooltipComponent={(
tooltipProps: React.PropsWithChildren<TooltipRenderProps & { step: SpotlightTourStep }>,
) => (
- <TooltipComponent
- actionLabel={actionLabel}
- actionPath={actionPath}
- stepXofYLabel={stepXofYLabel}
- width={width}
- {...tooltipProps}
- />
+ <TooltipProvider>
+ <TooltipComponent
+ actionLabel={actionLabel}
+ actionPath={actionPath}
+ stepXofYLabel={stepXofYLabel}
+ width={width}
+ {...tooltipProps}
+ />
+ </TooltipProvider>
)}
{...otherProps}
/>
diff --git a/server/sonar-web/design-system/src/components/Switch.tsx b/server/sonar-web/design-system/src/components/Switch.tsx
index b22e425e775..397338b3d04 100644
--- a/server/sonar-web/design-system/src/components/Switch.tsx
+++ b/server/sonar-web/design-system/src/components/Switch.tsx
@@ -20,7 +20,7 @@
import styled from '@emotion/styled';
import { ForwardedRef, forwardRef } from 'react';
import tw from 'twin.macro';
-import { themeBorder, themeColor, themeContrast, themeShadow } from '../helpers';
+import { themeColor, themeContrast, themeShadow } from '../helpers';
import { CheckIcon } from './icons';
interface Props {
@@ -95,7 +95,7 @@ const StyledSwitch = styled.button<StyledProps>`
background: ${({ active }) => (active ? themeColor('switchActive') : themeColor('switch'))};
border: none;
transition: 0.3s ease;
- transition-property: background, outline;
+ transition-property: background;
&:hover:not(:disabled),
&:active:not(:disabled),
@@ -113,8 +113,8 @@ const StyledSwitch = styled.button<StyledProps>`
&:focus:not(:disabled),
&:active:not(:disabled) {
- outline: ${({ active }) =>
- active ? themeBorder('focus', 'switchActive') : themeBorder('focus', 'switch')};
+ outline: var(--echoes-focus-border-width-default) solid var(--echoes-color-focus-default);
+ outline-offset: var(--echoes-focus-border-offset-default);
}
`;
diff --git a/server/sonar-web/design-system/src/components/Tabs.tsx b/server/sonar-web/design-system/src/components/Tabs.tsx
index e216eb2b33b..e498755333b 100644
--- a/server/sonar-web/design-system/src/components/Tabs.tsx
+++ b/server/sonar-web/design-system/src/components/Tabs.tsx
@@ -21,7 +21,7 @@ import styled from '@emotion/styled';
import { PropsWithChildren } from 'react';
import { FormattedMessage } from 'react-intl';
import tw from 'twin.macro';
-import { OPACITY_20_PERCENT, themeBorder, themeColor } from '../helpers';
+import { themeBorder, themeColor } from '../helpers';
import { BareButton } from '../sonar-aligned/components/buttons';
import { getTabId, getTabPanelId } from '../sonar-aligned/helpers/tabs';
import { Badge } from './Badge';
@@ -131,7 +131,7 @@ const TabButton = styled(BareButton)<{
}
&:active {
- outline: ${themeBorder('xsActive', 'tabSelected', OPACITY_20_PERCENT)};
+ outline: var(--echoes-focus-border-width-default) solid var(--echoes-color-focus-default);
z-index: 1;
}
diff --git a/server/sonar-web/design-system/src/components/__tests__/Dropdown-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Dropdown-test.tsx
index eb3c7cfd4d7..fce2fd2bc67 100644
--- a/server/sonar-web/design-system/src/components/__tests__/Dropdown-test.tsx
+++ b/server/sonar-web/design-system/src/components/__tests__/Dropdown-test.tsx
@@ -20,7 +20,7 @@
import { screen } from '@testing-library/react';
import { renderWithRouter } from '../../helpers/testUtils';
import { ButtonSecondary } from '../../sonar-aligned/components/buttons';
-import { ActionsDropdown, Dropdown } from '../Dropdown';
+import { Dropdown } from '../Dropdown';
describe('Dropdown', () => {
it('renders', async () => {
@@ -90,18 +90,3 @@ describe('Dropdown', () => {
);
}
});
-
-describe('ActionsDropdown', () => {
- it('renders', () => {
- setup();
- expect(screen.getByRole('button')).toHaveAccessibleName('menu');
- });
-
- function setup() {
- return renderWithRouter(
- <ActionsDropdown id="test-menu">
- <div id="overlay" />
- </ActionsDropdown>,
- );
- }
-});
diff --git a/server/sonar-web/design-system/src/components/__tests__/SpotlightTour-test.tsx b/server/sonar-web/design-system/src/components/__tests__/SpotlightTour-test.tsx
index 830e86bce97..f95b68f1a2a 100644
--- a/server/sonar-web/design-system/src/components/__tests__/SpotlightTour-test.tsx
+++ b/server/sonar-web/design-system/src/components/__tests__/SpotlightTour-test.tsx
@@ -28,42 +28,42 @@ it('should display the spotlight tour', async () => {
renderSpotlightTour({ callback });
expect(await screen.findByRole('alertdialog')).toBeInTheDocument();
- expect(screen.getByRole('alertdialog')).toHaveTextContent(
- 'Trust The FooFoo bar is bazstep 1 of 5next',
- );
+ let dialog = screen.getByRole('alertdialog');
+ expect(dialog).toHaveTextContent('Trust The Foo');
+ expect(dialog).toHaveTextContent('Foo bar is baz');
expect(screen.getByText('step 1 of 5')).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'next' }));
- expect(screen.getByRole('alertdialog')).toHaveTextContent(
- 'Trust The BazBaz foo is barstep 2 of 5go_backnext',
- );
+ dialog = screen.getByRole('alertdialog');
+ expect(dialog).toHaveTextContent('Trust The Baz');
+ expect(dialog).toHaveTextContent('Baz foo is bar');
expect(callback).toHaveBeenCalled();
await user.click(screen.getByRole('button', { name: 'next' }));
- expect(screen.getByRole('alertdialog')).toHaveTextContent(
- 'Trust The BarBar baz is foostep 3 of 5go_backnext',
- );
+ dialog = screen.getByRole('alertdialog');
+ expect(dialog).toHaveTextContent('Trust The Bar');
+ expect(dialog).toHaveTextContent('Bar baz is foo');
await user.click(screen.getByRole('button', { name: 'next' }));
- expect(screen.getByRole('alertdialog')).toHaveTextContent(
- 'Trust The Foo 2Foo baz is barstep 4 of 5go_backnext',
- );
+ dialog = screen.getByRole('alertdialog');
+ expect(dialog).toHaveTextContent('Trust The Foo 2');
+ expect(dialog).toHaveTextContent('Foo baz is bar');
await user.click(screen.getByRole('button', { name: 'go_back' }));
- expect(screen.getByRole('alertdialog')).toHaveTextContent(
- 'Trust The BarBar baz is foostep 3 of 5go_backnext',
- );
+ dialog = screen.getByRole('alertdialog');
+ expect(dialog).toHaveTextContent('Trust The Bar');
+ expect(dialog).toHaveTextContent('Bar baz is foo');
await user.click(screen.getByRole('button', { name: 'next' }));
await user.click(screen.getByRole('button', { name: 'next' }));
- expect(screen.getByRole('alertdialog')).toHaveTextContent(
- 'Trust The Baz 2Baz bar is foostep 5 of 5go_backclose',
- );
+ dialog = screen.getByRole('alertdialog');
+ expect(dialog).toHaveTextContent('Trust The Baz 2');
+ expect(dialog).toHaveTextContent('Baz bar is foo');
expect(screen.queryByRole('button', { name: 'next' })).not.toBeInTheDocument();
diff --git a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap
index 6b27c088c74..1d7aad782e8 100644
--- a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap
+++ b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap
@@ -24,12 +24,7 @@ exports[`should highlight code content correctly 1`] = `
border: var(--border);
color: var(--color);
background-color: var(--background);
- -webkit-transition: background-color 0.2s ease,outline 0.2s ease;
- transition: background-color 0.2s ease,outline 0.2s ease;
- display: -webkit-inline-box;
- display: -webkit-inline-flex;
- display: -ms-inline-flexbox;
- display: inline-flex;
+ transition:background-color 0.2s ease,display: inline-flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
@@ -64,9 +59,14 @@ exports[`should highlight code content correctly 1`] = `
}
.emotion-4:focus,
-.emotion-4:active {
+.emotion-4:active,
+.emotion-4:focus-visible {
color: var(--color);
- outline: 4px solid var(--focus);
+}
+
+.emotion-4:focus-visible {
+ outline: var(--echoes-focus-border-width-default) solid var(--echoes-color-focus-default);
+ outline-offset: var(--echoes-focus-border-offset-default);
}
.emotion-4:disabled,
@@ -226,12 +226,7 @@ exports[`should show full size when multiline with no editing 1`] = `
border: var(--border);
color: var(--color);
background-color: var(--background);
- -webkit-transition: background-color 0.2s ease,outline 0.2s ease;
- transition: background-color 0.2s ease,outline 0.2s ease;
- display: -webkit-inline-box;
- display: -webkit-inline-flex;
- display: -ms-inline-flexbox;
- display: inline-flex;
+ transition:background-color 0.2s ease,display: inline-flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
@@ -266,9 +261,14 @@ exports[`should show full size when multiline with no editing 1`] = `
}
.emotion-4:focus,
-.emotion-4:active {
+.emotion-4:active,
+.emotion-4:focus-visible {
color: var(--color);
- outline: 4px solid var(--focus);
+}
+
+.emotion-4:focus-visible {
+ outline: var(--echoes-focus-border-width-default) solid var(--echoes-color-focus-default);
+ outline-offset: var(--echoes-focus-border-offset-default);
}
.emotion-4:disabled,
@@ -430,12 +430,7 @@ exports[`should show reduced size when single line with no editing 1`] = `
border: var(--border);
color: var(--color);
background-color: var(--background);
- -webkit-transition: background-color 0.2s ease,outline 0.2s ease;
- transition: background-color 0.2s ease,outline 0.2s ease;
- display: -webkit-inline-box;
- display: -webkit-inline-flex;
- display: -ms-inline-flexbox;
- display: inline-flex;
+ transition:background-color 0.2s ease,display: inline-flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
@@ -472,9 +467,14 @@ exports[`should show reduced size when single line with no editing 1`] = `
}
.emotion-4:focus,
-.emotion-4:active {
+.emotion-4:active,
+.emotion-4:focus-visible {
color: var(--color);
- outline: 4px solid var(--focus);
+}
+
+.emotion-4:focus-visible {
+ outline: var(--echoes-focus-border-width-default) solid var(--echoes-color-focus-default);
+ outline-offset: var(--echoes-focus-border-offset-default);
}
.emotion-4:disabled,
diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts
index da0bc81f8ef..8d7292db47b 100644
--- a/server/sonar-web/design-system/src/components/index.ts
+++ b/server/sonar-web/design-system/src/components/index.ts
@@ -42,7 +42,7 @@ export * from './CodeSyntaxHighlighter';
export * from './ColorsLegend';
export * from './CoverageIndicator';
export * from './DonutChart';
-export { ActionsDropdown, Dropdown } from './Dropdown';
+export { Dropdown } from './Dropdown';
export * from './DropdownMenu';
export { DropdownToggler } from './DropdownToggler';
export * from './DuplicationsIndicator';
diff --git a/server/sonar-web/design-system/src/components/input/InputField.tsx b/server/sonar-web/design-system/src/components/input/InputField.tsx
index 2b6adb2bbff..5b3c9a04e5e 100644
--- a/server/sonar-web/design-system/src/components/input/InputField.tsx
+++ b/server/sonar-web/design-system/src/components/input/InputField.tsx
@@ -59,13 +59,13 @@ InputTextArea.displayName = 'InputTextArea';
const defaultStyle = (props: ThemedProps) => css`
--border: ${themeBorder('default', 'inputBorder')(props)};
--focusBorder: ${themeBorder('default', 'inputFocus')(props)};
- --focusOutline: ${themeBorder('focus', 'inputFocus')(props)};
+ --focusOutline: var(--echoes-focus-border-width-default) solid var(--echoes-color-focus-default);
`;
const dangerStyle = (props: ThemedProps) => css`
--border: ${themeBorder('default', 'inputDanger')(props)};
--focusBorder: ${themeBorder('default', 'inputDangerFocus')(props)};
- --focusOutline: ${themeBorder('focus', 'inputDangerFocus')(props)};
+ --focusOutline: var(--echoes-focus-border-width-default) solid var(--echoes-color-focus-default);
`;
const getInputVariant = (props: ThemedProps & { isInvalid?: boolean; isValid?: boolean }) => {
@@ -102,6 +102,7 @@ const baseStyle = (props: ThemedProps) => css`
&:focus-visible {
border: var(--focusBorder);
outline: var(--focusOutline);
+ outline-offset: var(--echoes-focus-border-offset-default);
}
&:disabled,
diff --git a/server/sonar-web/design-system/src/components/input/InputSearch.tsx b/server/sonar-web/design-system/src/components/input/InputSearch.tsx
index 9e9d5d76915..fd60b082ca5 100644
--- a/server/sonar-web/design-system/src/components/input/InputSearch.tsx
+++ b/server/sonar-web/design-system/src/components/input/InputSearch.tsx
@@ -226,7 +226,8 @@ export const StyledInputWrapper = styled.div`
&:focus,
&:active {
border: ${themeBorder('default', 'inputFocus')};
- outline: ${themeBorder('focus', 'inputFocus')};
+ outline: var(--echoes-focus-border-width-default) solid var(--echoes-color-focus-default);
+ outline-offset: var(--echoes-focus-border-offset-default);
}
&::-webkit-search-decoration,
@@ -251,6 +252,11 @@ export const StyledSearchIconWrapper = styled.div`
export const StyledInteractiveIcon = styled(InteractiveIcon)`
${tw`sw-absolute`}
${tw`sw-right-2`}
+
+ &:focus,
+ &:active {
+ outline: var(--echoes-focus-border-width-default) solid var(--echoes-color-focus-default);
+ }
`;
export const StyledNote = styled.span`
diff --git a/server/sonar-web/design-system/src/components/input/SearchSelectDropdownControl.tsx b/server/sonar-web/design-system/src/components/input/SearchSelectDropdownControl.tsx
index dbfcfb1854f..d5f40048bda 100644
--- a/server/sonar-web/design-system/src/components/input/SearchSelectDropdownControl.tsx
+++ b/server/sonar-web/design-system/src/components/input/SearchSelectDropdownControl.tsx
@@ -144,11 +144,11 @@ const StyledControl = styled.div`
&:focus-visible,
&:focus-within {
border: ${themeBorder('default', 'inputFocus')};
- outline: ${themeBorder('focus', 'inputFocus')};
+ outline: var(--echoes-focus-border-width-default) solid var(--echoes-color-focus-default);
&.is-discreet {
${tw`sw-rounded-1 sw-border-none`};
- outline: ${themeBorder('focus', 'discreetFocusBorder')};
+ outline: var(--echoes-focus-border-width-default) solid var(--echoes-color-focus-default);
}
}
`;
diff --git a/server/sonar-web/design-system/src/sonar-aligned/components/ToggleButton.tsx b/server/sonar-web/design-system/src/sonar-aligned/components/ToggleButton.tsx
index 9c882e50fbc..1c83381b8d7 100644
--- a/server/sonar-web/design-system/src/sonar-aligned/components/ToggleButton.tsx
+++ b/server/sonar-web/design-system/src/sonar-aligned/components/ToggleButton.tsx
@@ -113,9 +113,9 @@ const OptionButton = styled(ButtonSecondary)<{ selected: boolean }>`
color: ${themeContrast('toggleHover')};
}
- &:focus,
- &:active {
- outline: ${themeBorder('focus', 'toggleFocus')};
+ &:focus-visible {
+ outline: var(--echoes-focus-border-width-default) solid var(--echoes-color-focus-default);
+ outline-offset: var(--echoes-focus-border-offset-default);
z-index: 1;
}
`;
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 8c3a171086a..ee91b9109d5 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
@@ -128,7 +128,6 @@ export const buttonStyle = (props: ThemedProps) => css`
background-color: var(--background);
transition:
background-color 0.2s ease,
- outline 0.2s ease;
${tw`sw-inline-flex sw-items-center`}
${tw`sw-h-control`}
@@ -143,9 +142,14 @@ export const buttonStyle = (props: ThemedProps) => css`
}
&:focus,
- &:active {
+ &:active,
+ &:focus-visible {
color: var(--color);
- outline: ${themeBorder('focus', 'var(--focus)')(props)};
+ }
+
+ &:focus-visible {
+ outline: var(--echoes-focus-border-width-default) solid var(--echoes-color-focus-default);
+ outline-offset: var(--echoes-focus-border-offset-default);
}
&:disabled,
diff --git a/server/sonar-web/design-system/src/sonar-aligned/components/input/SelectCommon.tsx b/server/sonar-web/design-system/src/sonar-aligned/components/input/SelectCommon.tsx
index 8e89171aeb7..a5c8b75b996 100644
--- a/server/sonar-web/design-system/src/sonar-aligned/components/input/SelectCommon.tsx
+++ b/server/sonar-web/design-system/src/sonar-aligned/components/input/SelectCommon.tsx
@@ -147,7 +147,11 @@ export function selectStyle<
cursor: 'pointer',
background: themeColor('inputBackground')({ theme }),
transition: 'border 0.2s ease, outline 0.2s ease',
- outline: isFocused && !menuIsOpen ? themeBorder('focus', 'inputFocus')({ theme }) : 'none',
+ outline:
+ isFocused && !menuIsOpen
+ ? 'var(--echoes-focus-border-width-default) solid var(--echoes-color-focus-default)'
+ : 'none',
+ borderRadius: '4px',
...(isDisabled && {
color: themeContrast('inputDisabled')({ theme }),
background: themeColor('inputDisabled')({ theme }),
@@ -164,9 +168,11 @@ export function selectStyle<
}),
option: (base, { isFocused, isSelected }) => ({
...base,
+ borderLeft: '2px solid transparent',
...((isSelected || isFocused) && {
background: themeColor('selectOptionSelected')({ theme }),
color: themeContrast('primaryLight')({ theme }),
+ borderLeftColor: 'var(--echoes-color-focus-default)',
}),
}),
singleValue: (base) => ({
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-it.tsx b/server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-it.tsx
index 6355e3adc57..7077417fb09 100644
--- a/server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-it.tsx
+++ b/server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-it.tsx
@@ -414,7 +414,7 @@ function getPageObject() {
name: `background_tasks.show_actions_for_task_x_in_list.${rowIndex}`,
}),
);
- await user.click(within(row).getByRole('menuitem', { name: label }));
+ await user.click(screen.getByRole('menuitem', { name: label }));
},
};
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx
index da40c748a0b..47016006858 100644
--- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx
+++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx
@@ -17,7 +17,13 @@
* 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, ActionsDropdown, ItemButton, ItemDangerButton } from 'design-system';
+import {
+ ButtonIcon,
+ ButtonVariety,
+ DropdownMenu,
+ IconMoreVertical,
+} from '@sonarsource/echoes-react';
+import { ActionCell, ItemDangerButton } from 'design-system';
import * as React from 'react';
import ConfirmModal from '../../../components/controls/ConfirmModal';
import { translate, translateWithParameters } from '../../../helpers/l10n';
@@ -92,7 +98,7 @@ export default class TaskActions extends React.PureComponent<Props, State> {
render() {
const { component, task, taskIndex } = this.props;
- const canFilter = component === undefined && task.componentName;
+ const canFilter = component === undefined && Boolean(task.componentName);
const canCancel = task.status === TaskStatuses.Pending;
const canShowStacktrace = task.errorMessage !== undefined;
const canShowWarnings = task.warningCount !== undefined && task.warningCount > 0;
@@ -105,49 +111,63 @@ export default class TaskActions extends React.PureComponent<Props, State> {
return (
<ActionCell>
- <ActionsDropdown
+ <DropdownMenu.Root
id={`task-${task.id}-actions`}
- ariaLabel={translateWithParameters(
- 'background_tasks.show_actions_for_task_x_in_list',
- taskIndex,
- )}
className="js-task-action"
- >
- {canFilter && task.componentName && (
- <ItemButton className="js-task-filter" onClick={this.handleFilterClick}>
- {translateWithParameters(
- 'background_tasks.filter_by_component_x',
- task.componentName,
+ items={
+ <>
+ {canFilter && task.componentName && (
+ <DropdownMenu.ItemButton
+ className="js-task-filter"
+ onClick={this.handleFilterClick}
+ >
+ {translateWithParameters(
+ 'background_tasks.filter_by_component_x',
+ task.componentName,
+ )}
+ </DropdownMenu.ItemButton>
+ )}
+ {canCancel && (
+ <ItemDangerButton className="js-task-cancel" onClick={this.handleCancelClick}>
+ {translate('background_tasks.cancel_task')}
+ </ItemDangerButton>
+ )}
+ {task.hasScannerContext && (
+ <DropdownMenu.ItemButton
+ className="js-task-show-scanner-context"
+ onClick={this.handleShowScannerContextClick}
+ >
+ {translate('background_tasks.show_scanner_context')}
+ </DropdownMenu.ItemButton>
+ )}
+ {canShowStacktrace && (
+ <DropdownMenu.ItemButton
+ className="js-task-show-stacktrace"
+ onClick={this.handleShowStacktraceClick}
+ >
+ {translate('background_tasks.show_stacktrace')}
+ </DropdownMenu.ItemButton>
)}
- </ItemButton>
- )}
- {canCancel && (
- <ItemDangerButton className="js-task-cancel" onClick={this.handleCancelClick}>
- {translate('background_tasks.cancel_task')}
- </ItemDangerButton>
- )}
- {task.hasScannerContext && (
- <ItemButton
- className="js-task-show-scanner-context"
- onClick={this.handleShowScannerContextClick}
- >
- {translate('background_tasks.show_scanner_context')}
- </ItemButton>
- )}
- {canShowStacktrace && (
- <ItemButton
- className="js-task-show-stacktrace"
- onClick={this.handleShowStacktraceClick}
- >
- {translate('background_tasks.show_stacktrace')}
- </ItemButton>
- )}
- {canShowWarnings && (
- <ItemButton className="js-task-show-warnings" onClick={this.handleShowWarningsClick}>
- {translate('background_tasks.show_warnings')}
- </ItemButton>
- )}
- </ActionsDropdown>
+ {canShowWarnings && (
+ <DropdownMenu.ItemButton
+ className="js-task-show-warnings"
+ onClick={this.handleShowWarningsClick}
+ >
+ {translate('background_tasks.show_warnings')}
+ </DropdownMenu.ItemButton>
+ )}
+ </>
+ }
+ >
+ <ButtonIcon
+ Icon={IconMoreVertical}
+ ariaLabel={translateWithParameters(
+ 'background_tasks.show_actions_for_task_x_in_list',
+ taskIndex,
+ )}
+ variety={ButtonVariety.Default}
+ />
+ </DropdownMenu.Root>
<ConfirmModal
cancelButtonText={translate('close')}
diff --git a/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx b/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx
index f7aa1b2d1a9..5857c62f360 100644
--- a/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx
+++ b/server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx
@@ -18,16 +18,12 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { ButtonIcon, DropdownMenu, IconMoreVertical } from '@sonarsource/echoes-react';
import {
- ActionsDropdown,
Badge,
ContentCell,
DestructiveIcon,
- ItemButton,
- ItemDangerButton,
- ItemDivider,
NumericalCell,
- PopupZLevel,
Spinner,
TableRow,
TrashIcon,
@@ -108,23 +104,28 @@ export default function ListItem(props: Readonly<ListItemProps>) {
/>
)}
{!isManaged() && (
- <ActionsDropdown
- allowResizing
+ <DropdownMenu.Root
id={`group-actions-${group.name}`}
- ariaLabel={translateWithParameters('groups.edit', group.name)}
- zLevel={PopupZLevel.Global}
+ items={
+ <>
+ <DropdownMenu.ItemButton onClick={() => setGroupToEdit(group)}>
+ {translate('update_details')}
+ </DropdownMenu.ItemButton>
+ <DropdownMenu.Separator />
+ <DropdownMenu.ItemButtonDestructive
+ className="it__quality-profiles__delete"
+ onClick={() => setGroupToDelete(group)}
+ >
+ {translate('delete')}
+ </DropdownMenu.ItemButtonDestructive>
+ </>
+ }
>
- <ItemButton onClick={() => setGroupToEdit(group)}>
- {translate('update_details')}
- </ItemButton>
- <ItemDivider />
- <ItemDangerButton
- className="it__quality-profiles__delete"
- onClick={() => setGroupToDelete(group)}
- >
- {translate('delete')}
- </ItemDangerButton>
- </ActionsDropdown>
+ <ButtonIcon
+ Icon={IconMoreVertical}
+ ariaLabel={translateWithParameters('groups.edit', group.name)}
+ />
+ </DropdownMenu.Root>
)}
</>
)}
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx
index 4a4bb4090a2..b4b2447ac81 100644
--- a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx
+++ b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx
@@ -17,7 +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 { ActionsDropdown, ItemButton, ItemLink, PopupZLevel } from 'design-system';
+import { ButtonIcon, DropdownMenu, IconMoreVertical } from '@sonarsource/echoes-react';
import { difference } from 'lodash';
import * as React from 'react';
import { withRouter } from '~sonar-aligned/components/hoc/withRouter';
@@ -121,14 +121,14 @@ class ActionsCell extends React.PureComponent<Props, State> {
renderSetDefaultLink(qualifier: string, child: React.ReactNode) {
return (
- <ItemButton
+ <DropdownMenu.ItemButton
className="js-set-default"
data-qualifier={qualifier}
key={qualifier}
onClick={this.setDefault(qualifier)}
>
{child}
- </ItemButton>
+ </DropdownMenu.ItemButton>
);
}
@@ -157,38 +157,44 @@ class ActionsCell extends React.PureComponent<Props, State> {
return (
<>
- <ActionsDropdown
- allowResizing
+ <DropdownMenu.Root
id={`permission-template-actions-${t.id}`}
- zLevel={PopupZLevel.Global}
- toggleClassName="it__permission-actions"
- ariaLabel={translateWithParameters('permission_templates.show_actions_for_x', t.name)}
+ items={
+ <>
+ {this.renderSetDefaultsControl()}
+
+ {!this.props.fromDetails && (
+ <DropdownMenu.ItemLink
+ to={{
+ pathname: PERMISSION_TEMPLATES_PATH,
+ search: queryToSearchString({ id: t.id }),
+ }}
+ >
+ {translate('edit_permissions')}
+ </DropdownMenu.ItemLink>
+ )}
+
+ <DropdownMenu.ItemButton className="js-update" onClick={this.handleUpdateClick}>
+ {translate('update_details')}
+ </DropdownMenu.ItemButton>
+
+ {t.defaultFor.length === 0 && (
+ <DropdownMenu.ItemButtonDestructive
+ className="js-delete"
+ onClick={this.handleDeleteClick}
+ >
+ {translate('delete')}
+ </DropdownMenu.ItemButtonDestructive>
+ )}
+ </>
+ }
>
- <>
- {this.renderSetDefaultsControl()}
-
- {!this.props.fromDetails && (
- <ItemLink
- to={{
- pathname: PERMISSION_TEMPLATES_PATH,
- search: queryToSearchString({ id: t.id }),
- }}
- >
- {translate('edit_permissions')}
- </ItemLink>
- )}
-
- <ItemButton className="js-update" onClick={this.handleUpdateClick}>
- {translate('update_details')}
- </ItemButton>
-
- {t.defaultFor.length === 0 && (
- <ItemButton className="js-delete" onClick={this.handleDeleteClick}>
- {translate('delete')}
- </ItemButton>
- )}
- </>
- </ActionsDropdown>
+ <ButtonIcon
+ Icon={IconMoreVertical}
+ ariaLabel={translateWithParameters('permission_templates.show_actions_for_x', t.name)}
+ className="it__permission-actions"
+ />
+ </DropdownMenu.Root>
{this.state.updateModal && (
<Form
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx
index f7f790ca090..a95d3e5b059 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx
@@ -18,17 +18,15 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import styled from '@emotion/styled';
-import classNames from 'classnames';
import {
- ActionsDropdown,
- HelperHintIcon,
- ItemButton,
- ItemDangerButton,
- ItemDivider,
- PopupZLevel,
- themeBorder,
- themeColor,
-} from 'design-system';
+ ButtonIcon,
+ ButtonSize,
+ ButtonVariety,
+ DropdownMenu,
+ IconMoreVertical,
+} from '@sonarsource/echoes-react';
+import classNames from 'classnames';
+import { HelperHintIcon, themeBorder, themeColor } from 'design-system';
import * as React from 'react';
import { WrappedComponentProps, injectIntl } from 'react-intl';
import ClickEventBoundary from '../../../components/controls/ClickEventBoundary';
@@ -133,38 +131,51 @@ function ProjectActivityAnalysis(props: ProjectActivityAnalysisProps) {
{(canAddVersion || canAddEvent || canDeleteAnalyses) && (
<ClickEventBoundary>
<div className="sw-h-page sw-grow-0 sw-shrink-0 sw-mr-4 sw-relative">
- <ActionsDropdown
- ariaLabel={translateWithParameters(
- 'project_activity.analysis_X_actions',
- analysis.buildString ?? formatDate(parsedDate, formatterOption),
- )}
- buttonSize="small"
+ <DropdownMenu.Root
id="it__analysis-actions"
- zLevel={PopupZLevel.Absolute}
+ items={
+ <>
+ {canAddVersion && (
+ <DropdownMenu.ItemButton
+ className="js-add-version"
+ onClick={() => setDialog(Dialog.AddVersion)}
+ >
+ {translate('project_activity.add_version')}
+ </DropdownMenu.ItemButton>
+ )}
+ {canAddEvent && (
+ <DropdownMenu.ItemButton
+ className="js-add-event"
+ onClick={() => setDialog(Dialog.AddEvent)}
+ >
+ {translate('project_activity.add_custom_event')}
+ </DropdownMenu.ItemButton>
+ )}
+ {(canAddVersion || canAddEvent) && canDeleteAnalyses && (
+ <DropdownMenu.Separator />
+ )}
+ {canDeleteAnalyses && (
+ <DropdownMenu.ItemButtonDestructive
+ className="js-delete-analysis"
+ onClick={() => setDialog(Dialog.RemoveAnalysis)}
+ >
+ {translate('project_activity.delete_analysis')}
+ </DropdownMenu.ItemButtonDestructive>
+ )}
+ </>
+ }
>
- {canAddVersion && (
- <ItemButton
- className="js-add-version"
- onClick={() => setDialog(Dialog.AddVersion)}
- >
- {translate('project_activity.add_version')}
- </ItemButton>
- )}
- {canAddEvent && (
- <ItemButton className="js-add-event" onClick={() => setDialog(Dialog.AddEvent)}>
- {translate('project_activity.add_custom_event')}
- </ItemButton>
- )}
- {(canAddVersion || canAddEvent) && canDeleteAnalyses && <ItemDivider />}
- {canDeleteAnalyses && (
- <ItemDangerButton
- className="js-delete-analysis"
- onClick={() => setDialog(Dialog.RemoveAnalysis)}
- >
- {translate('project_activity.delete_analysis')}
- </ItemDangerButton>
- )}
- </ActionsDropdown>
+ <ButtonIcon
+ Icon={IconMoreVertical}
+ ariaLabel={translateWithParameters(
+ 'project_activity.analysis_X_actions',
+ analysis.buildString ?? formatDate(parsedDate, formatterOption),
+ )}
+ className="-sw-mt-1"
+ size={ButtonSize.Medium}
+ variety={ButtonVariety.PrimaryGhost}
+ />
+ </DropdownMenu.Root>
{[Dialog.AddEvent, Dialog.AddVersion].includes(dialog as Dialog) && (
<AddEventForm
diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRow.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRow.tsx
index 255e2c288fe..6d875243266 100644
--- a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRow.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRow.tsx
@@ -18,14 +18,12 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import {
- ActionCell,
- ActionsDropdown,
- Badge,
- ContentCell,
- ItemButton,
- ItemDangerButton,
- TableRowInteractive,
-} from 'design-system';
+ ButtonIcon,
+ ButtonVariety,
+ DropdownMenu,
+ IconMoreVertical,
+} from '@sonarsource/echoes-react';
+import { ActionCell, Badge, ContentCell, TableRowInteractive } from 'design-system';
import * as React from 'react';
import { isBranch, isMainBranch, isPullRequest } from '~sonar-aligned/helpers/branch-like';
import QualityGateStatus from '../../../app/components/nav/component/branch-like/QualityGateStatus';
@@ -75,34 +73,41 @@ function BranchLikeRow(props: BranchLikeRowProps) {
</ContentCell>
)}
<ActionCell>
- <ActionsDropdown
- allowResizing
+ <DropdownMenu.Root
id={`branch-settings-action-${branchLikeDisplayName}`}
- ariaLabel={translateWithParameters(
- 'project_branch_pull_request.branch.actions_label',
- branchLikeDisplayName,
- )}
- >
- {isBranch(branchLike) && !isMainBranch(branchLike) && (
- <ItemButton onClick={props.onSetAsMain}>
- {translate('project_branch_pull_request.branch.set_main')}
- </ItemButton>
- )}
+ items={
+ <>
+ {isBranch(branchLike) && !isMainBranch(branchLike) && (
+ <DropdownMenu.ItemButton onClick={props.onSetAsMain}>
+ {translate('project_branch_pull_request.branch.set_main')}
+ </DropdownMenu.ItemButton>
+ )}
- {isMainBranch(branchLike) ? (
- <ItemButton onClick={props.onRename}>
- {translate('project_branch_pull_request.branch.rename')}
- </ItemButton>
- ) : (
- <ItemDangerButton onClick={props.onDelete}>
- {translate(
- isPullRequest(branchLike)
- ? 'project_branch_pull_request.pull_request.delete'
- : 'project_branch_pull_request.branch.delete',
+ {isMainBranch(branchLike) ? (
+ <DropdownMenu.ItemButton onClick={props.onRename}>
+ {translate('project_branch_pull_request.branch.rename')}
+ </DropdownMenu.ItemButton>
+ ) : (
+ <DropdownMenu.ItemButtonDestructive onClick={props.onDelete}>
+ {translate(
+ isPullRequest(branchLike)
+ ? 'project_branch_pull_request.pull_request.delete'
+ : 'project_branch_pull_request.branch.delete',
+ )}
+ </DropdownMenu.ItemButtonDestructive>
)}
- </ItemDangerButton>
- )}
- </ActionsDropdown>
+ </>
+ }
+ >
+ <ButtonIcon
+ Icon={IconMoreVertical}
+ ariaLabel={translateWithParameters(
+ 'project_branch_pull_request.branch.actions_label',
+ branchLikeDisplayName,
+ )}
+ variety={ButtonVariety.Default}
+ />
+ </DropdownMenu.Root>
</ActionCell>
</TableRowInteractive>
);
diff --git a/server/sonar-web/src/main/js/apps/projectLinks/ProjectLinkRow.tsx b/server/sonar-web/src/main/js/apps/projectLinks/ProjectLinkRow.tsx
index babb3794adb..8f1de5d40c8 100644
--- a/server/sonar-web/src/main/js/apps/projectLinks/ProjectLinkRow.tsx
+++ b/server/sonar-web/src/main/js/apps/projectLinks/ProjectLinkRow.tsx
@@ -17,15 +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 {
- ActionCell,
- ContentCell,
- DestructiveIcon,
- Link,
- Note,
- TableRow,
- TrashIcon,
-} from 'design-system';
+import { ButtonIcon, ButtonSize, ButtonVariety, IconDelete } from '@sonarsource/echoes-react';
+import { ActionCell, ContentCell, Link, Note, TableRow } from 'design-system';
import * as React from 'react';
import isValidUri from '../../app/utils/isValidUri';
import ConfirmButton from '../../components/controls/ConfirmButton';
@@ -70,11 +63,12 @@ export default class LinkRow extends React.PureComponent<Props> {
onConfirm={this.props.onDelete}
>
{({ onClick }) => (
- <DestructiveIcon
- Icon={TrashIcon}
- aria-label={translateWithParameters('project_links.delete_x_link', link.name ?? '')}
+ <ButtonIcon
+ Icon={IconDelete}
+ ariaLabel={translateWithParameters('project_links.delete_x_link', link.name ?? '')}
onClick={onClick}
- size="small"
+ size={ButtonSize.Medium}
+ variety={ButtonVariety.DangerGhost}
/>
)}
</ConfirmButton>
diff --git a/server/sonar-web/src/main/js/apps/projectNewCode/components/BranchListRow.tsx b/server/sonar-web/src/main/js/apps/projectNewCode/components/BranchListRow.tsx
index 79709380d55..1e6e56b3ab8 100644
--- a/server/sonar-web/src/main/js/apps/projectNewCode/components/BranchListRow.tsx
+++ b/server/sonar-web/src/main/js/apps/projectNewCode/components/BranchListRow.tsx
@@ -18,18 +18,21 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import {
+ ButtonIcon,
+ ButtonSize,
+ DropdownMenu,
+ IconEdit,
+ IconMoreVertical,
+ Tooltip,
+} from '@sonarsource/echoes-react';
+import {
ActionCell,
- ActionsDropdown,
Badge,
ContentCell,
FlagWarningIcon,
- InteractiveIcon,
- ItemButton,
- PencilIcon,
TableRowInteractive,
} from 'design-system';
import * as React from 'react';
-import Tooltip from '../../../components/controls/Tooltip';
import BranchLikeIcon from '../../../components/icon-mappers/BranchLikeIcon';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import { translate, translateWithParameters } from '../../../helpers/l10n';
@@ -128,36 +131,44 @@ export default function BranchListRow(props: BranchListRowProps) {
</ContentCell>
<ActionCell>
{!branch.newCodePeriod && (
- <InteractiveIcon
- Icon={PencilIcon}
- aria-label={translateWithParameters('branch_list.edit_for_x', branch.name)}
+ <ButtonIcon
+ Icon={IconEdit}
+ ariaLabel={translateWithParameters('branch_list.edit_for_x', branch.name)}
onClick={() => props.onOpenEditModal(branch)}
- className="sw-mr-2"
- size="small"
+ size={ButtonSize.Medium}
/>
)}
{branch.newCodePeriod && (
- <ActionsDropdown
- allowResizing
+ <DropdownMenu.Root
id={`new-code-action-${branch.name}`}
- ariaLabel={translateWithParameters('branch_list.show_actions_for_x', branch.name)}
+ items={
+ <>
+ <Tooltip
+ content={
+ isCompliant
+ ? null
+ : translate('project_baseline.compliance.warning.title.project')
+ }
+ >
+ <DropdownMenu.ItemButton
+ isDisabled={!isCompliant}
+ onClick={() => props.onResetToDefault(branch.name)}
+ >
+ {translate('reset_to_default')}
+ </DropdownMenu.ItemButton>
+ </Tooltip>
+ <DropdownMenu.ItemButton onClick={() => props.onOpenEditModal(branch)}>
+ {translate('edit')}
+ </DropdownMenu.ItemButton>
+ </>
+ }
>
- <Tooltip
- content={
- isCompliant ? null : translate('project_baseline.compliance.warning.title.project')
- }
- >
- <ItemButton
- disabled={!isCompliant}
- onClick={() => props.onResetToDefault(branch.name)}
- >
- {translate('reset_to_default')}
- </ItemButton>
- </Tooltip>
- <ItemButton onClick={() => props.onOpenEditModal(branch)}>
- {translate('edit')}
- </ItemButton>
- </ActionsDropdown>
+ <ButtonIcon
+ Icon={IconMoreVertical}
+ ariaLabel={translateWithParameters('branch_list.show_actions_for_x', branch.name)}
+ size={ButtonSize.Medium}
+ />
+ </DropdownMenu.Root>
)}
</ActionCell>
</TableRowInteractive>
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
index b586d5a09a7..16feb810bb2 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
@@ -17,7 +17,13 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { ActionsDropdown, ItemButton, ItemLink, PopupZLevel, Spinner } from 'design-system';
+import {
+ ButtonIcon,
+ ButtonSize,
+ DropdownMenu,
+ IconMoreVertical,
+ Spinner,
+} from '@sonarsource/echoes-react';
import { noop } from 'lodash';
import React, { useState } from 'react';
import { throwGlobalError } from '~sonar-aligned/helpers/error';
@@ -68,44 +74,56 @@ export default function ProjectRowActions({ currentUser, project }: Props) {
return (
<>
- <ActionsDropdown
+ <DropdownMenu.Root
id="project-management-action-dropdown"
- toggleClassName="it__user-actions-toggle"
onOpen={handleDropdownOpen}
- allowResizing
- ariaLabel={translateWithParameters('projects_management.show_actions_for_x', project.name)}
- zLevel={PopupZLevel.Global}
- >
- <Spinner loading={loading} className="sw-flex sw-ml-3">
+ items={
<>
- {hasAccess === true && (
- <ItemLink to={getComponentPermissionsUrl(project.key)}>
- {translate(project.managed ? 'show_permissions' : 'edit_permissions')}
- </ItemLink>
- )}
- {hasAccess === false && (!project.managed || currentUser.local) ? (
- <ItemButton
- className="it__restore-access"
- onClick={() => setRestoreAccessModal(true)}
+ <Spinner isLoading={loading} className="sw-flex sw-ml-3 sw-my-2">
+ <>
+ {hasAccess === true && (
+ <DropdownMenu.ItemLink to={getComponentPermissionsUrl(project.key)}>
+ {translate(project.managed ? 'show_permissions' : 'edit_permissions')}
+ </DropdownMenu.ItemLink>
+ )}
+ {hasAccess === false && (!project.managed || currentUser.local) ? (
+ <DropdownMenu.ItemButton
+ className="it__restore-access"
+ onClick={() => setRestoreAccessModal(true)}
+ >
+ {translate('global_permissions.restore_access')}
+ </DropdownMenu.ItemButton>
+ ) : (
+ hasAccess === false && (
+ <DropdownMenu.ItemButton isDisabled onClick={noop}>
+ {translate('global_permissions.no_actions_available')}
+ </DropdownMenu.ItemButton>
+ )
+ )}
+ </>
+ </Spinner>
+
+ {!project.managed && (
+ <DropdownMenu.ItemButton
+ className="it__apply-template"
+ onClick={() => setApplyTemplateModal(true)}
>
- {translate('global_permissions.restore_access')}
- </ItemButton>
- ) : (
- hasAccess === false && (
- <ItemButton disabled onClick={noop}>
- {translate('global_permissions.no_actions_available')}
- </ItemButton>
- )
+ {translate('projects_role.apply_template')}
+ </DropdownMenu.ItemButton>
)}
</>
- </Spinner>
-
- {!project.managed && (
- <ItemButton className="it__apply-template" onClick={() => setApplyTemplateModal(true)}>
- {translate('projects_role.apply_template')}
- </ItemButton>
- )}
- </ActionsDropdown>
+ }
+ >
+ <ButtonIcon
+ Icon={IconMoreVertical}
+ className="it__user-actions-toggle"
+ ariaLabel={translateWithParameters(
+ 'projects_management.show_actions_for_x',
+ project.name,
+ )}
+ size={ButtonSize.Medium}
+ />
+ </DropdownMenu.Root>
{restoreAccessModal && (
<RestoreAccessModal
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx
index 8087e5f2d42..b023410d74b 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx
@@ -17,19 +17,11 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import {
- ActionsDropdown,
- Badge,
- ButtonSecondary,
- DangerButtonPrimary,
- ItemButton,
- ItemDangerButton,
- ItemDivider,
- SubTitle,
-} from 'design-system';
+import { ButtonIcon, DropdownMenu, IconMoreVertical, Tooltip } from '@sonarsource/echoes-react';
+import { Badge, ButtonSecondary, DangerButtonPrimary, SubTitle } from 'design-system';
import { countBy } from 'lodash';
import * as React from 'react';
-import Tooltip from '../../../components/controls/Tooltip';
+import LegacyTooltip from '../../../components/controls/Tooltip';
import { translate } from '../../../helpers/l10n';
import { useSetQualityGateAsDefaultMutation } from '../../../queries/quality-gates';
import { CaycStatus, QualityGate } from '../../../types/types';
@@ -81,7 +73,7 @@ export default function DetailsHeader({ qualityGate }: Readonly<Props>) {
</ButtonSecondary>
)}
{actions.copy && (
- <Tooltip
+ <LegacyTooltip
content={
qualityGate.caycStatus === CaycStatus.NonCompliant
? translate('quality_gates.cannot_copy_no_cayc')
@@ -94,10 +86,10 @@ export default function DetailsHeader({ qualityGate }: Readonly<Props>) {
>
{translate('copy')}
</ButtonSecondary>
- </Tooltip>
+ </LegacyTooltip>
)}
{actions.setAsDefault && (
- <Tooltip
+ <LegacyTooltip
content={
qualityGate.caycStatus === CaycStatus.NonCompliant
? translate('quality_gates.cannot_set_default_no_cayc')
@@ -110,7 +102,7 @@ export default function DetailsHeader({ qualityGate }: Readonly<Props>) {
>
{translate('set_as_default')}
</ButtonSecondary>
- </Tooltip>
+ </LegacyTooltip>
)}
{actions.delete && (
<DangerButtonPrimary onClick={() => setIsRemoveFormOpen(true)}>
@@ -121,53 +113,60 @@ export default function DetailsHeader({ qualityGate }: Readonly<Props>) {
)}
{actionsCount > 1 && (
- <ActionsDropdown allowResizing id="quality-gate-actions">
- {actions.rename && (
- <ItemButton onClick={() => setIsRenameFormOpen(true)}>
- {translate('rename')}
- </ItemButton>
- )}
- {actions.copy && (
- <Tooltip
- content={
- qualityGate.caycStatus === CaycStatus.NonCompliant
- ? translate('quality_gates.cannot_copy_no_cayc')
- : null
- }
- >
- <ItemButton
- disabled={qualityGate.caycStatus === CaycStatus.NonCompliant}
- onClick={() => setIsCopyFormOpen(true)}
- >
- {translate('copy')}
- </ItemButton>
- </Tooltip>
- )}
- {actions.setAsDefault && (
- <Tooltip
- content={
- qualityGate.caycStatus === CaycStatus.NonCompliant
- ? translate('quality_gates.cannot_set_default_no_cayc')
- : null
- }
- >
- <ItemButton
- disabled={qualityGate.caycStatus === CaycStatus.NonCompliant}
- onClick={handleSetAsDefaultClick}
- >
- {translate('set_as_default')}
- </ItemButton>
- </Tooltip>
- )}
- {actions.delete && (
+ <DropdownMenu.Root
+ id="quality-gate-actions"
+ items={
<>
- <ItemDivider />
- <ItemDangerButton onClick={() => setIsRemoveFormOpen(true)}>
- {translate('delete')}
- </ItemDangerButton>
+ {actions.rename && (
+ <DropdownMenu.ItemButton onClick={() => setIsRenameFormOpen(true)}>
+ {translate('rename')}
+ </DropdownMenu.ItemButton>
+ )}
+ {actions.copy && (
+ <Tooltip
+ content={
+ qualityGate.caycStatus === CaycStatus.NonCompliant
+ ? translate('quality_gates.cannot_copy_no_cayc')
+ : null
+ }
+ >
+ <DropdownMenu.ItemButton
+ isDisabled={qualityGate.caycStatus === CaycStatus.NonCompliant}
+ onClick={() => setIsCopyFormOpen(true)}
+ >
+ {translate('copy')}
+ </DropdownMenu.ItemButton>
+ </Tooltip>
+ )}
+ {actions.setAsDefault && (
+ <Tooltip
+ content={
+ qualityGate.caycStatus === CaycStatus.NonCompliant
+ ? translate('quality_gates.cannot_set_default_no_cayc')
+ : null
+ }
+ >
+ <DropdownMenu.ItemButton
+ isDisabled={qualityGate.caycStatus === CaycStatus.NonCompliant}
+ onClick={handleSetAsDefaultClick}
+ >
+ {translate('set_as_default')}
+ </DropdownMenu.ItemButton>
+ </Tooltip>
+ )}
+ {actions.delete && (
+ <>
+ <DropdownMenu.Separator />
+ <DropdownMenu.ItemButtonDestructive onClick={() => setIsRemoveFormOpen(true)}>
+ {translate('delete')}
+ </DropdownMenu.ItemButtonDestructive>
+ </>
+ )}
</>
- )}
- </ActionsDropdown>
+ }
+ >
+ <ButtonIcon Icon={IconMoreVertical} ariaLabel={translate('actions')} />
+ </DropdownMenu.Root>
)}
</div>
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx
index 61c35ff8e3e..8a25dbc04e0 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx
@@ -114,7 +114,7 @@ it('should be able to create a quality gate then delete it', async () => {
// Delete the quality gate
await user.click(newQG);
- await user.click(screen.getByLabelText('menu'));
+ await user.click(screen.getByLabelText('actions'));
const deleteButton = screen.getByRole('menuitem', { name: 'delete' });
await user.click(deleteButton);
const popup = screen.getByRole('dialog');
@@ -133,7 +133,7 @@ it('should be able to copy a quality gate which is CaYC compliant', async () =>
const notDefaultQualityGate = await screen.findByText('Sonar way');
await user.click(notDefaultQualityGate);
- await user.click(await screen.findByLabelText('menu'));
+ await user.click(await screen.findByLabelText('actions'));
const copyButton = screen.getByRole('menuitem', { name: 'copy' });
await user.click(copyButton);
@@ -151,17 +151,17 @@ it('should not be able to copy a quality gate which is not CaYC compliant', asyn
const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
await user.click(notDefaultQualityGate);
- await user.click(await screen.findByLabelText('menu'));
+ await user.click(await screen.findByLabelText('actions'));
const copyButton = screen.getByRole('menuitem', { name: 'copy' });
- expect(copyButton).toBeDisabled();
+ expect(copyButton).toHaveAttribute('aria-disabled', 'true');
});
it('should be able to rename a quality gate', async () => {
const user = userEvent.setup();
qualityGateHandler.setIsAdmin(true);
renderQualityGateApp();
- await user.click(await screen.findByLabelText('menu'));
+ await user.click(await screen.findByLabelText('actions'));
const renameButton = screen.getByRole('menuitem', { name: 'rename' });
await user.click(renameButton);
@@ -180,9 +180,9 @@ it('should not be able to set as default a quality gate which is not CaYC compli
const notDefaultQualityGate = await screen.findByText('SonarSource way - CFamily');
await user.click(notDefaultQualityGate);
- await user.click(await screen.findByLabelText('menu'));
+ await user.click(await screen.findByLabelText('actions'));
const setAsDefaultButton = screen.getByRole('menuitem', { name: 'set_as_default' });
- expect(setAsDefaultButton).toBeDisabled();
+ expect(setAsDefaultButton).toHaveAttribute('aria-disabled', 'true');
});
it('should be able to set as default a quality gate which is CaYC compliant', async () => {
@@ -192,7 +192,7 @@ it('should be able to set as default a quality gate which is CaYC compliant', as
const notDefaultQualityGate = await screen.findByRole('button', { name: /Sonar way/ });
await user.click(notDefaultQualityGate);
- await user.click(await screen.findByLabelText('menu'));
+ await user.click(await screen.findByLabelText('actions'));
const setAsDefaultButton = screen.getByRole('menuitem', { name: 'set_as_default' });
await user.click(setAsDefaultButton);
expect(await screen.findByRole('button', { name: /Sonar way default/ })).toBeInTheDocument();
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfileApp-it.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfileApp-it.tsx
index 08e81e903aa..18bf2780237 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfileApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfileApp-it.tsx
@@ -62,7 +62,7 @@ const ui = {
activateMoreRulesButton: byRole('button', { name: 'quality_profiles.activate_more' }),
activateMoreLink: byRole('link', { name: 'quality_profiles.activate_more' }),
activateMoreRulesLink: byRole('menuitem', { name: 'quality_profiles.activate_more_rules' }),
- backUpLink: byRole('menuitem', { name: 'backup_verb' }),
+ backUpLink: byRole('menuitem', { name: 'backup_verb open_in_new_tab' }),
compareLink: byRole('menuitem', { name: 'compare' }),
extendButton: byRole('menuitem', { name: 'extend' }),
copyButton: byRole('menuitem', { name: 'copy' }),
@@ -382,7 +382,7 @@ describe('Admin or user with permission', () => {
await ui.waitForDataLoaded();
await user.click(await ui.qualityProfileActions.find());
- expect(ui.setAsDefaultButton.get()).toBeDisabled();
+ expect(ui.setAsDefaultButton.get()).toHaveAttribute('aria-disabled', 'true');
});
it("should be able to delete a Quality Profile and it's children", async () => {
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx
index db150e2c0d6..f05d289b904 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx
@@ -17,16 +17,13 @@
* 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,
- ItemDangerButton,
- ItemDivider,
- ItemDownload,
- ItemLink,
- PopupZLevel,
-} from 'design-system';
+ ButtonIcon,
+ DropdownMenu,
+ IconMoreVertical,
+ Tooltip,
+ TooltipSide,
+} from '@sonarsource/echoes-react';
import { some } from 'lodash';
import * as React from 'react';
import { withRouter } from '~sonar-aligned/components/hoc/withRouter';
@@ -209,109 +206,123 @@ class ProfileActions extends React.PureComponent<Props, State> {
return (
<>
- <ActionsDropdown
- allowResizing
+ <DropdownMenu.Root
id={`quality-profile-actions-${profile.key}`}
className="it__quality-profiles__actions-dropdown"
- toggleClassName="it__quality-profiles__actions-dropdown-toggle"
- ariaLabel={translateWithParameters(
- 'quality_profiles.actions',
- profile.name,
- profile.languageName,
- )}
- zLevel={PopupZLevel.Global}
- >
- {actions.edit && (
- <ItemLink className="it__quality-profiles__activate-more-rules" to={activateMoreUrl}>
- {translate('quality_profiles.activate_more_rules')}
- </ItemLink>
- )}
-
- {!profile.isBuiltIn && (
- <ItemDownload
- download={`${profile.key}.xml`}
- href={backupUrl}
- className="it__quality-profiles__backup"
- >
- {translate('backup_verb')}
- </ItemDownload>
- )}
-
- {isComparable && (
- <ItemLink
- className="it__quality-profiles__compare"
- to={getProfileComparePath(profile.name, profile.language)}
- >
- {translate('compare')}
- </ItemLink>
- )}
-
- {actions.copy && (
+ items={
<>
- <Tooltip
- content={translateWithParameters('quality_profiles.extend_help', profile.name)}
- side={TooltipSide.Left}
- >
- <ItemButton
- className="it__quality-profiles__extend"
- onClick={this.handleExtendClick}
+ {actions.edit && (
+ <DropdownMenu.ItemLink
+ className="it__quality-profiles__activate-more-rules"
+ to={activateMoreUrl}
>
- {translate('extend')}
- </ItemButton>
- </Tooltip>
-
- <Tooltip
- content={translateWithParameters('quality_profiles.copy_help', profile.name)}
- side={TooltipSide.Left}
- >
- <ItemButton className="it__quality-profiles__copy" onClick={this.handleCopyClick}>
- {translate('copy')}
- </ItemButton>
- </Tooltip>
- </>
- )}
-
- {actions.edit && (
- <ItemButton className="it__quality-profiles__rename" onClick={this.handleRenameClick}>
- {translate('rename')}
- </ItemButton>
- )}
-
- {actions.setAsDefault &&
- (hasNoActiveRules ? (
- <Tooltip
- content={translate('quality_profiles.cannot_set_default_no_rules')}
- side={TooltipSide.Left}
- >
- <ItemButton
- className="it__quality-profiles__set-as-default"
- onClick={this.handleSetDefaultClick}
- disabled
+ {translate('quality_profiles.activate_more_rules')}
+ </DropdownMenu.ItemLink>
+ )}
+
+ {!profile.isBuiltIn && (
+ <DropdownMenu.ItemLinkDownload
+ download={`${profile.key}.xml`}
+ to={backupUrl}
+ className="it__quality-profiles__backup"
>
- {translate('set_as_default')}
- </ItemButton>
- </Tooltip>
- ) : (
- <ItemButton
- className="it__quality-profiles__set-as-default"
- onClick={this.handleSetDefaultClick}
- >
- {translate('set_as_default')}
- </ItemButton>
- ))}
-
- {actions.delete && (
- <>
- <ItemDivider />
- <ItemDangerButton
- className="it__quality-profiles__delete"
- onClick={this.handleDeleteClick}
- >
- {translate('delete')}
- </ItemDangerButton>
+ {translate('backup_verb')}
+ </DropdownMenu.ItemLinkDownload>
+ )}
+
+ {isComparable && (
+ <DropdownMenu.ItemLink
+ className="it__quality-profiles__compare"
+ to={getProfileComparePath(profile.name, profile.language)}
+ >
+ {translate('compare')}
+ </DropdownMenu.ItemLink>
+ )}
+
+ {actions.copy && (
+ <>
+ <Tooltip
+ content={translateWithParameters('quality_profiles.extend_help', profile.name)}
+ side={TooltipSide.Left}
+ >
+ <DropdownMenu.ItemButton
+ className="it__quality-profiles__extend"
+ onClick={this.handleExtendClick}
+ >
+ {translate('extend')}
+ </DropdownMenu.ItemButton>
+ </Tooltip>
+
+ <Tooltip
+ content={translateWithParameters('quality_profiles.copy_help', profile.name)}
+ side={TooltipSide.Left}
+ >
+ <DropdownMenu.ItemButton
+ className="it__quality-profiles__copy"
+ onClick={this.handleCopyClick}
+ >
+ {translate('copy')}
+ </DropdownMenu.ItemButton>
+ </Tooltip>
+ </>
+ )}
+
+ {actions.edit && (
+ <DropdownMenu.ItemButton
+ className="it__quality-profiles__rename"
+ onClick={this.handleRenameClick}
+ >
+ {translate('rename')}
+ </DropdownMenu.ItemButton>
+ )}
+
+ {actions.setAsDefault &&
+ (hasNoActiveRules ? (
+ <Tooltip
+ content={translate('quality_profiles.cannot_set_default_no_rules')}
+ side={TooltipSide.Left}
+ >
+ <DropdownMenu.ItemButton
+ className="it__quality-profiles__set-as-default"
+ onClick={this.handleSetDefaultClick}
+ isDisabled
+ >
+ {translate('set_as_default')}
+ </DropdownMenu.ItemButton>
+ </Tooltip>
+ ) : (
+ <DropdownMenu.ItemButton
+ className="it__quality-profiles__set-as-default"
+ onClick={this.handleSetDefaultClick}
+ >
+ {translate('set_as_default')}
+ </DropdownMenu.ItemButton>
+ ))}
+
+ {actions.delete && (
+ <>
+ <DropdownMenu.Separator />
+ <DropdownMenu.ItemButtonDestructive
+ className="it__quality-profiles__delete"
+ onClick={this.handleDeleteClick}
+ >
+ {translate('delete')}
+ </DropdownMenu.ItemButtonDestructive>
+ </>
+ )}
</>
- )}
- </ActionsDropdown>
+ }
+ >
+ <ButtonIcon
+ Icon={IconMoreVertical}
+ className="it__quality-profiles__actions-dropdown-toggle"
+ ariaLabel={translateWithParameters(
+ 'quality_profiles.actions',
+ profile.name,
+ profile.languageName,
+ )}
+ />
+ </DropdownMenu.Root>
{openModal === ProfileActionModals.Copy && (
<ProfileModalForm
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.tsx
index f36ea1f6328..1fe08b4a3f9 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.tsx
@@ -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 { TooltipProvider } from '@sonarsource/echoes-react';
import { render, screen } from '@testing-library/react';
import * as React from 'react';
import { HelmetProvider } from 'react-helmet-async';
@@ -87,13 +88,15 @@ function renderProfileContainer(path: string, overrides: Partial<QualityProfiles
<HelmetProvider context={{}}>
<MemoryRouter initialEntries={[path]}>
<IntlWrapper>
- <Routes>
- <Route element={<ProfileOutlet {...overrides} />}>
- <Route element={<ProfileContainer />}>
- <Route path="*" element={<WrappedChild />} />
+ <TooltipProvider>
+ <Routes>
+ <Route element={<ProfileOutlet {...overrides} />}>
+ <Route element={<ProfileContainer />}>
+ <Route path="*" element={<WrappedChild />} />
+ </Route>
</Route>
- </Route>
- </Routes>
+ </Routes>
+ </TooltipProvider>
</IntlWrapper>
</MemoryRouter>
</HelmetProvider>,
diff --git a/server/sonar-web/src/main/js/apps/settings/components/Languages.tsx b/server/sonar-web/src/main/js/apps/settings/components/Languages.tsx
index 851c2ef1d13..ffc76536a5d 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/Languages.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/Languages.tsx
@@ -17,9 +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 { PopupZLevel, SearchSelectDropdown, SubHeading } from 'design-system';
+import { InputSize, Select } from '@sonarsource/echoes-react';
+import { SubHeading } from 'design-system';
import * as React from 'react';
-import { Options } from 'react-select';
import { withRouter } from '~sonar-aligned/components/hoc/withRouter';
import { Location, Router } from '~sonar-aligned/types/router';
import { translate } from '../../../helpers/l10n';
@@ -33,53 +33,33 @@ export interface LanguagesProps extends AdditionalCategoryComponentProps {
router: Router;
}
-interface SelectOption {
- label: string;
- originalValue: string;
- value: string;
-}
-
export function Languages(props: Readonly<LanguagesProps>) {
const { categories, component, definitions, location, router, selectedCategory } = props;
- const { availableLanguages, selectedLanguage } = getLanguages(categories, selectedCategory);
+ const { availableLanguages, selectedLanguage } = React.useMemo(
+ () => getLanguages(categories, selectedCategory),
+ [categories, selectedCategory],
+ );
- const handleOnChange = (newOption: SelectOption) => {
+ const handleOnChange = (selection: string | null) => {
router.push({
...location,
- query: { ...location.query, category: newOption.originalValue },
+ query: { ...location.query, category: selection ?? LANGUAGES_CATEGORY },
});
};
- const handleLanguagesSearch = React.useCallback(
- (query: string, cb: (options: Options<SelectOption>) => void) => {
- const normalizedQuery = query.toLowerCase();
-
- cb(
- availableLanguages.filter(
- (lang) =>
- lang.label.toLowerCase().includes(normalizedQuery) ||
- lang.value.includes(normalizedQuery),
- ),
- );
- },
- [availableLanguages],
- );
-
return (
<>
<SubHeading id="languages-category-title">
{translate('property.category.languages')}
</SubHeading>
<div data-test="language-select">
- <SearchSelectDropdown
- defaultOptions={availableLanguages}
- controlAriaLabel={translate('property.category.languages')}
+ <Select
+ data={availableLanguages}
onChange={handleOnChange}
- loadOptions={handleLanguagesSearch}
- placeholder={translate('settings.languages.select_a_language_placeholder')}
- controlSize="medium"
- zLevel={PopupZLevel.Content}
- value={availableLanguages.find((language) => language.value === selectedLanguage)}
+ value={selectedLanguage ?? null /* null clears the input */}
+ ariaLabelledBy="languages-category-title"
+ isSearchable
+ size={InputSize.Medium}
/>
</div>
{selectedLanguage && (
@@ -101,17 +81,19 @@ function getLanguages(categories: string[], selectedCategory: string) {
.filter((c) => CATEGORY_OVERRIDES[c.toLowerCase()] === lowerCasedLanguagesCategory)
.map((c) => ({
label: getCategoryName(c),
- value: c.toLowerCase(),
- originalValue: c,
+ value: c,
}));
let selectedLanguage = undefined;
- if (
- lowerCasedSelectedCategory !== lowerCasedLanguagesCategory &&
- availableLanguages.find((c) => c.value === lowerCasedSelectedCategory)
- ) {
- selectedLanguage = lowerCasedSelectedCategory;
+ if (lowerCasedSelectedCategory !== lowerCasedLanguagesCategory) {
+ const match = availableLanguages.find(
+ (c) => c.value.toLowerCase() === lowerCasedSelectedCategory,
+ );
+
+ if (match) {
+ selectedLanguage = match.value;
+ }
}
return {
diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.tsx
index f210200a113..c50bc4e3835 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.tsx
@@ -17,7 +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 { DestructiveIcon, TrashIcon } from 'design-system';
+import { ButtonIcon, ButtonVariety, IconDelete } from '@sonarsource/echoes-react';
import * as React from 'react';
import { translateWithParameters } from '../../../../helpers/l10n';
import { DefaultSpecializedInputProps, getEmptyValue, getPropertyName } from '../../utils';
@@ -62,15 +62,16 @@ class MultiValueInput extends React.PureComponent<Props> {
{!isLast && (
<div className="sw-inline-block sw-ml-2">
- <DestructiveIcon
- Icon={TrashIcon}
+ <ButtonIcon
+ Icon={IconDelete}
className="js-remove-value"
- aria-label={translateWithParameters(
+ ariaLabel={translateWithParameters(
'settings.definition.delete_value',
getPropertyName(setting.definition),
value,
)}
onClick={() => this.handleDeleteValue(index)}
+ variety={ButtonVariety.DangerGhost}
/>
</div>
)}
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx
index ad2c93c4194..867d27d490e 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx
@@ -17,10 +17,15 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { ActionsDropdown, ItemButton, ItemDangerButton } from 'design-system';
import * as React from 'react';
import { useState } from 'react';
+import {
+ ButtonIcon,
+ ButtonVariety,
+ DropdownMenu,
+ IconMoreVertical,
+} from '@sonarsource/echoes-react';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { WebhookResponse, WebhookUpdatePayload } from '../../../types/webhook';
import CreateWebhookForm from './CreateWebhookForm';
@@ -46,21 +51,38 @@ export default function WebhookActions(props: Props) {
return (
<>
- <ActionsDropdown
- toggleClassName="it__webhook-actions"
+ <DropdownMenu.Root
+ className="it__webhook-actions"
id={webhook.key}
- ariaLabel={translateWithParameters('webhooks.show_actions', webhook.name)}
+ items={
+ <>
+ <DropdownMenu.ItemButton onClick={() => setUpdating(true)}>
+ {translate('update_verb')}
+ </DropdownMenu.ItemButton>
+ {webhook.latestDelivery && (
+ <DropdownMenu.ItemButton
+ className="it__webhook-deliveries"
+ onClick={() => setDeliveries(true)}
+ >
+ {translate('webhooks.deliveries.show')}
+ </DropdownMenu.ItemButton>
+ )}
+ <DropdownMenu.ItemButtonDestructive
+ className="it__webhook-delete"
+ onClick={() => setDeleting(true)}
+ >
+ {translate('delete')}
+ </DropdownMenu.ItemButtonDestructive>
+ </>
+ }
>
- <ItemButton onClick={() => setUpdating(true)}>{translate('update_verb')}</ItemButton>
- {webhook.latestDelivery && (
- <ItemButton className="it__webhook-deliveries" onClick={() => setDeliveries(true)}>
- {translate('webhooks.deliveries.show')}
- </ItemButton>
- )}
- <ItemDangerButton className="it__webhook-delete" onClick={() => setDeleting(true)}>
- {translate('delete')}
- </ItemDangerButton>
- </ActionsDropdown>
+ <ButtonIcon
+ className="it__webhook-actions"
+ Icon={IconMoreVertical}
+ ariaLabel={translateWithParameters('webhooks.show_actions', webhook.name)}
+ variety={ButtonVariety.Default}
+ />
+ </DropdownMenu.Root>
{deliveries && <DeliveriesForm onClose={() => setDeliveries(false)} webhook={webhook} />}
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItemLatestDelivery.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItemLatestDelivery.tsx
index b84cd1c9b4e..92c817525be 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItemLatestDelivery.tsx
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItemLatestDelivery.tsx
@@ -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 { FlagErrorIcon, FlagSuccessIcon, InteractiveIcon, MenuIcon } from 'design-system';
+import { ButtonIcon, ButtonSize, IconMoreVertical } from '@sonarsource/echoes-react';
+import { FlagErrorIcon, FlagSuccessIcon } from 'design-system';
import * as React from 'react';
import { useState } from 'react';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
@@ -42,12 +43,12 @@ export default function WebhookItemLatestDelivery({ webhook }: Props) {
<div className="sw-ml-2 sw-flex sw-items-center">
<DateTimeFormatter date={webhook.latestDelivery.at} />
<span title={translateWithParameters('webhooks.last_execution.open_for_x', webhook.name)}>
- <InteractiveIcon
+ <ButtonIcon
className="sw-ml-2"
- Icon={MenuIcon}
- aria-label={translateWithParameters('webhooks.last_execution.open_for_x', webhook.name)}
+ Icon={IconMoreVertical}
+ ariaLabel={translateWithParameters('webhooks.last_execution.open_for_x', webhook.name)}
onClick={() => setModalOpen(true)}
- size="small"
+ size={ButtonSize.Medium}
/>
</span>
</div>
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-it.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-it.tsx
index 0e2bcf81f78..8acc6c74d48 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-it.tsx
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-it.tsx
@@ -287,7 +287,7 @@ function getPageObject() {
await user.click(
within(row).getByRole('button', { name: `webhooks.show_actions.${webhookName}` }),
);
- await user.click(within(row).getByRole(role, { name: actionName }));
+ await user.click(screen.getByRole(role, { name: actionName }));
},
clickWebhookLatestDelivery: async (rowIndex: number, webhookName: string) => {
const row = ui.getWebhookRow(rowIndex);
diff --git a/server/sonar-web/src/main/js/components/controls/Tooltip.tsx b/server/sonar-web/src/main/js/components/controls/Tooltip.tsx
index 13f2648015a..e7bc5128b9f 100644
--- a/server/sonar-web/src/main/js/components/controls/Tooltip.tsx
+++ b/server/sonar-web/src/main/js/components/controls/Tooltip.tsx
@@ -90,7 +90,7 @@ function isMeasured(state: State): state is OwnState & Measurements {
* - `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) {
+export default function LegacyTooltip(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.
return props.content != null && props.content !== '' ? (