diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2024-10-07 18:32:15 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-10-08 20:02:47 +0000 |
commit | aef2ea10ecee8df9eadf3d3da4e63c06a9378b8b (patch) | |
tree | 840adef6a40df1e22a584f6e4a75b4cd77c1d814 | |
parent | f49678493a80f08984b13f8003fca362fe081c9f (diff) | |
download | sonarqube-aef2ea10ecee8df9eadf3d3da4e63c06a9378b8b.tar.gz sonarqube-aef2ea10ecee8df9eadf3d3da4e63c06a9378b8b.zip |
SONAR-22290 Fix focus indicator in legacy components
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 !== '' ? ( |