]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22290 Fix focus indicator in legacy components
authorJeremy Davis <jeremy.davis@sonarsource.com>
Mon, 7 Oct 2024 16:32:15 +0000 (18:32 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 8 Oct 2024 20:02:47 +0000 (20:02 +0000)
36 files changed:
server/sonar-web/design-system/src/components/Dropdown.tsx
server/sonar-web/design-system/src/components/DropdownMenu.tsx
server/sonar-web/design-system/src/components/FacetBox.tsx
server/sonar-web/design-system/src/components/SpotlightTour.tsx
server/sonar-web/design-system/src/components/Switch.tsx
server/sonar-web/design-system/src/components/Tabs.tsx
server/sonar-web/design-system/src/components/__tests__/Dropdown-test.tsx
server/sonar-web/design-system/src/components/__tests__/SpotlightTour-test.tsx
server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap
server/sonar-web/design-system/src/components/index.ts
server/sonar-web/design-system/src/components/input/InputField.tsx
server/sonar-web/design-system/src/components/input/InputSearch.tsx
server/sonar-web/design-system/src/components/input/SearchSelectDropdownControl.tsx
server/sonar-web/design-system/src/sonar-aligned/components/ToggleButton.tsx
server/sonar-web/design-system/src/sonar-aligned/components/buttons/Button.tsx
server/sonar-web/design-system/src/sonar-aligned/components/input/SelectCommon.tsx
server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-it.tsx
server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx
server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRow.tsx
server/sonar-web/src/main/js/apps/projectLinks/ProjectLinkRow.tsx
server/sonar-web/src/main/js/apps/projectNewCode/components/BranchListRow.tsx
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx
server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfileApp-it.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileContainer-test.tsx
server/sonar-web/src/main/js/apps/settings/components/Languages.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.tsx
server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx
server/sonar-web/src/main/js/apps/webhooks/components/WebhookItemLatestDelivery.tsx
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-it.tsx
server/sonar-web/src/main/js/components/controls/Tooltip.tsx

index ab273c08f0ce37b737990d2f8a72cc74affa7b67..cf972125f20a1e7557d1d777a146fe479ab7f6fa 100644 (file)
  * 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>
-  );
-}
index 82b89ee251eb781498b9a4bf83ae134bb3b45171..dff4f3b11e61d9ff31d2654307fd9d4bf846425d 100644 (file)
@@ -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)};
index 22c4e344ce180ac8497d11e75e04d2f0cf83845d..acf9a366e1f2419a415c3ab0539a886f8ec06352 100644 (file)
@@ -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)`
index caa21689bb672fb4fa2779b18e6638b121b9d1f3..69131fcaec7659f9c621186cce0e08c0a976bbe5 100644 (file)
  */
 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}
     />
index b22e425e775cc7a5138bc43a305694d0912d3c2d..397338b3d04c4644de2184c4b04d9bb7d7fd1c57 100644 (file)
@@ -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);
   }
 `;
 
index e216eb2b33bdb5af2eb2f5de8792a5ba6c729b4f..e498755333b1f31e0c4b54f120577d2720971372 100644 (file)
@@ -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;
   }
 
index eb3c7cfd4d7ffc4fb8053106db593280a3bd4843..fce2fd2bc6743db17c4c3a8be542c7f46922b785 100644 (file)
@@ -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>,
-    );
-  }
-});
index 830e86bce97e4906550be58896085d3a1f2f7388..f95b68f1a2adc2d1e300b223c50489fdff1dda8c 100644 (file)
@@ -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();
 
index 6b27c088c74ba939bb2709372b99f68b5d7297e0..1d7aad782e8da647c42e208aed21e9c91b54b38d 100644 (file)
@@ -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,
index da0bc81f8efdedcf84a5a74617763f66c5337bde..8d7292db47b9f7ad221b0cef34cb86d91d65430c 100644 (file)
@@ -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';
index 2b6adb2bbff62ab7898d28e4026bb60eba36b699..5b3c9a04e5eca5047267dfe1eb25fc736455374e 100644 (file)
@@ -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,
index 9e9d5d76915e949f278efce2d5ba5cd9135a2320..fd60b082ca5ab9dd4d1c0617c427d07b5ce4bf76 100644 (file)
@@ -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`
index dbfcfb1854f83a79147f1b66a5fa21487ccfad83..d5f40048bdaf2bb635c4cd20fe12fbdb6f692b2d 100644 (file)
@@ -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);
     }
   }
 `;
index 9c882e50fbc7f2d0cbd8d08a0cdf7ec66a155722..1c83381b8d7572bd8f0bafc690f3d0d3e003acae 100644 (file)
@@ -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;
   }
 `;
index 8c3a171086a567dfb5e12a427d079b6a351fc270..ee91b9109d5c09638f8cd5da6e077e311917df8f 100644 (file)
@@ -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,
index 8e89171aeb7ad922cd67c11a05ef79a5778b2fcb..a5c8b75b996d2267bd4c7bd8615fc68d3b5b2237 100644 (file)
@@ -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) => ({
index 6355e3adc57935d31363cc9777f777cd23a4a594..7077417fb09dbb0287105a72465fa4ff66910980 100644 (file)
@@ -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 }));
     },
   };
 
index da40c748a0b6a88e26514740f667732b7b8e3ab5..470160068589f8fde8d623e9b2a8d5a27eb87193 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { ActionCell, 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')}
index f7aa1b2d1a993c40b93252edb857f7b0a478b2c9..5857c62f360ec2b50cafcc79981af56924d35156 100644 (file)
  * 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>
             )}
           </>
         )}
index 4a4bb4090a28fa65627edd3b2e761d045a4b1d65..b4b2447ac8139319e51a936658bac4a66d3ec59a 100644 (file)
@@ -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
index f7f790ca090f4f6b1fe671d6753a9287fd761ee6..a95d3e5b059d1d88d80104473480509163a4751c 100644 (file)
  * 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
index 255e2c288fe54c553d9c72951f5ed8ae7097c867..6d875243266e16ebac0bbf738421a06b5c3b5fe5 100644 (file)
  * 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>
   );
index babb3794adb4aa6bd95a6cd6f00cb4c0af492463..8f1de5d40c84cb9755391419efc86fd13e68cb3e 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import {
-  ActionCell,
-  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>
index 79709380d5535a09289edd5123f678d25ed21d5d..1e6e56b3ab85308ba1def394dbb63e7696a643ab 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import {
+  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>
index b586d5a09a7728c21336c4c546d1aa0a5ec88a9e..16feb810bb270e93680d5bb7a5eb14de765b14bd 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { 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
index 8087e5f2d42482b41e48c2781f7dbf6bab5d9205..b023410d74b08fe5e120e33e43c297f1d391ad37 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import {
-  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>
 
index 61c35ff8e3eea356691b95b81b674f4095be957d..8a25dbc04e054cf6c53c3640fe98d3bc339ed967 100644 (file)
@@ -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();
index 08e81e903aa060d0a7fa7b4ebb676fc51be264d5..18bf2780237d693e872bc29f0ab9b55418cf25b3 100644 (file)
@@ -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 () => {
index db150e2c0d60008e17a69ab119533d0fbc1691a3..f05d289b90424ffaa16458e82bb410c4820f71b6 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { Tooltip, 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
index f36ea1f632868f4f29c507020a2ae7c3413ec0f9..1fe08b4a3f9efea5353f18febcf065cf69c70c2e 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { 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>,
index 851c2ef1d13dfab97419f3552a3dd455bd3b84c0..ffc76536a5db05b048f97cc3ced70564900a24dd 100644 (file)
@@ -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 {
index f210200a1134e4aebda44c6cbd5046c8a72f6efc..c50bc4e38359e3cbe229a4590f094c3c9c98f8bf 100644 (file)
@@ -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>
         )}
index ad2c93c4194238ca595d528f9e97014d4bfb8f89..867d27d490e50000e23f1462e9efe6219733fff9 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { 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} />}
 
index b84cd1c9b4e928495038b8f4c7516581960a5a68..92c817525be1f84cd7444c852b328cdde3169cec 100644 (file)
@@ -17,7 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { 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>
index 0e2bcf81f78263cae21f8324f732b7b8c3bd0c4f..8acc6c74d485c6d009dd3c05930db1d4e9438fe4 100644 (file)
@@ -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);
index 13f2648015a77a5549bc2f84cfe97605901a8a41..e7bc5128b9f3dac13de3c32dd19c38c8f10145bd 100644 (file)
@@ -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 !== '' ? (