open: boolean;
}
+/** @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 class Dropdown extends React.PureComponent<Readonly<Props>, State> {
state: State = { open: false };
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;
size?: InputSizeKeys;
}
+/** @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 DropdownMenu({
children,
className,
innerRef?: React.Ref<HTMLAnchorElement>;
};
+/** @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 ItemLink(props: ItemLinkProps) {
const {
children,
end?: boolean;
}
+/** @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 ItemNavLink(props: ItemNavLinkProps) {
const { children, className, disabled, end, icon, onClick, selected, innerRef, to, ...liProps } =
props;
onClick: React.MouseEventHandler<HTMLButtonElement>;
}
+/** @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 const ItemButton = forwardRef(
(props: ItemButtonProps, ref: ForwardedRef<HTMLButtonElement>) => {
const { children, className, disabled, icon, innerRef, onClick, selected, ...liProps } = props;
font-weight: 600;
`;
+/** @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 const ItemHeader = styled.li`
background-color: ${themeColor('dropdownMenuHeader')};
color: ${themeContrast('dropdownMenuHeader')};
`;
ItemHeader.defaultProps = { className: 'dropdown-menu-header', role: 'menuitem' };
+/** @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 const ItemDivider = styled.li`
height: 1px;
background-color: ${themeColor('popupBorder')};
withFocusOutHandler?: boolean;
}
+/** @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 DropdownToggler(props: Props) {
const {
children,
}
&:hover,
- &.hover,
- &[aria-expanded='true'] {
+ &.hover {
border-bottom: ${themeBorder('active', 'menuBorder', 1)};
color: ${themeContrast('mainBarHover')};
}
}
+
+ &[aria-expanded='true'] a {
+ border-bottom: ${themeBorder('active', 'menuBorder', 1)};
+ color: ${themeContrast('mainBarHover')};
+ }
`;
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
import styled from '@emotion/styled';
import classNames from 'classnames';
-import React from 'react';
+import React, { forwardRef } from 'react';
import tw, { theme } from 'twin.macro';
import { themeBorder, themeColor, themeContrast } from '../helpers/theme';
import { isDefined } from '../helpers/types';
withChevron?: boolean;
}
-export function NavBarTabLink(props: NavBarTabLinkProps) {
- const { active, children, className, text, withChevron = false, ...linkProps } = props;
- return (
- <NavBarTabLinkWrapper>
- <NavLink
- className={({ isActive }) =>
- classNames(
- 'sw-flex sw-items-center',
- { active: isDefined(active) ? active : isActive },
- className,
- )
- }
- {...linkProps}
- >
- <span className="sw-inline-block sw-text-center" data-text={text}>
- {text}
- </span>
- {children}
- {withChevron && <ChevronDownIcon className="sw-ml-1" />}
- </NavLink>
- </NavBarTabLinkWrapper>
- );
-}
+export const NavBarTabLink = forwardRef<HTMLAnchorElement, NavBarTabLinkProps>(
+ (props: NavBarTabLinkProps, ref) => {
+ const { active, children, className, text, withChevron = false, ...linkProps } = props;
+ return (
+ <NavBarTabLinkWrapper>
+ <NavLink
+ className={({ isActive }) =>
+ classNames(
+ 'sw-flex sw-items-center',
+ { active: isDefined(active) ? active : isActive },
+ className,
+ )
+ }
+ ref={ref}
+ {...linkProps}
+ >
+ <span className="sw-inline-block sw-text-center" data-text={text}>
+ {text}
+ </span>
+
+ {children}
+
+ {withChevron && <ChevronDownIcon className="sw-ml-1" />}
+ </NavLink>
+ </NavBarTabLinkWrapper>
+ );
+ },
+);
+
+NavBarTabLink.displayName = 'NavBarTabLink';
export function DisabledTabLink(props: { label: string; overlay: React.ReactNode }) {
return (
& > a.active,
& > a:active,
& > a:hover,
- & > a:focus {
+ & > a:focus,
+ & > a[aria-expanded='true'] {
border-bottom-color: ${themeColor('tabBorder')};
}
& > a.active > span[data-text],
+ & > a[aria-expanded='true'] > span[data-text],
& > a:active > span {
${tw`sw-body-md-highlight`};
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import {
- DisabledTabLink,
- Dropdown,
- ItemNavLink,
- NavBarTabLink,
- NavBarTabs,
- PopupZLevel,
-} from 'design-system';
+
+import { DropdownMenu } from '@sonarsource/echoes-react';
+import { DisabledTabLink, NavBarTabLink, NavBarTabs } from 'design-system';
import * as React from 'react';
import { useLocation } from '~sonar-aligned/components/hoc/withRouter';
import { getBranchLikeQuery, isPullRequest } from '~sonar-aligned/helpers/branch-like';
isApplication(qualifier),
isPortfolioLike(qualifier),
);
+
if (!adminLinks.some((link) => link != null)) {
return null;
}
return (
- <Dropdown
+ <DropdownMenu.Root
data-test="administration"
id="component-navigation-admin"
- size="auto"
- zLevel={PopupZLevel.Global}
- overlay={adminLinks}
+ items={adminLinks}
>
- {({ onToggleClick, open, a11yAttrs }) => (
- <NavBarTabLink
- active={isSettingsActive || open}
- onClick={onToggleClick}
- text={
- hasMessage('layout.settings', component.qualifier)
- ? translate('layout.settings', component.qualifier)
- : translate('layout.settings')
- }
- withChevron
- to={{}}
- {...a11yAttrs}
- />
- )}
- </Dropdown>
+ <NavBarTabLink
+ active={isSettingsActive}
+ text={
+ hasMessage('layout.settings', component.qualifier)
+ ? translate('layout.settings', component.qualifier)
+ : translate('layout.settings')
+ }
+ withChevron
+ to={{}}
+ />
+ </DropdownMenu.Root>
);
};
return null;
}
return (
- <ItemNavLink
+ <DropdownMenu.ItemLink
key="settings"
to={{ pathname: '/project/settings', search: new URLSearchParams(query).toString() }}
>
{translate('project_settings.page')}
- </ItemNavLink>
+ </DropdownMenu.ItemLink>
);
};
}
return (
- <ItemNavLink
+ <DropdownMenu.ItemLink
key="branches"
to={{ pathname: '/project/branches', search: new URLSearchParams(query).toString() }}
>
{translate('project_branch_pull_request.page')}
- </ItemNavLink>
+ </DropdownMenu.ItemLink>
);
};
return null;
}
return (
- <ItemNavLink
+ <DropdownMenu.ItemLink
key="baseline"
to={{ pathname: '/project/baseline', search: new URLSearchParams(query).toString() }}
>
{translate('project_baseline.page')}
- </ItemNavLink>
+ </DropdownMenu.ItemLink>
);
};
return null;
}
return (
- <ItemNavLink
+ <DropdownMenu.ItemLink
key="import-export"
to={{
pathname: '/project/import_export',
}}
>
{translate('project_dump.page')}
- </ItemNavLink>
+ </DropdownMenu.ItemLink>
);
};
return null;
}
return (
- <ItemNavLink
+ <DropdownMenu.ItemLink
key="profiles"
to={{
pathname: '/project/quality_profiles',
}}
>
{translate('project_quality_profiles.page')}
- </ItemNavLink>
+ </DropdownMenu.ItemLink>
);
};
return null;
}
return (
- <ItemNavLink
+ <DropdownMenu.ItemLink
key="quality_gate"
to={{ pathname: '/project/quality_gate', search: new URLSearchParams(query).toString() }}
>
{translate('project_quality_gate.page')}
- </ItemNavLink>
+ </DropdownMenu.ItemLink>
);
};
return null;
}
return (
- <ItemNavLink
+ <DropdownMenu.ItemLink
key="links"
to={{ pathname: '/project/links', search: new URLSearchParams(query).toString() }}
>
{translate('project_links.page')}
- </ItemNavLink>
+ </DropdownMenu.ItemLink>
);
};
return null;
}
return (
- <ItemNavLink
+ <DropdownMenu.ItemLink
key="permissions"
to={{ pathname: '/project_roles', search: new URLSearchParams(query).toString() }}
>
{translate('permissions.page')}
- </ItemNavLink>
+ </DropdownMenu.ItemLink>
);
};
return null;
}
return (
- <ItemNavLink
+ <DropdownMenu.ItemLink
key="background_tasks"
to={{
pathname: '/project/background_tasks',
}}
>
{translate('background_tasks.page')}
- </ItemNavLink>
+ </DropdownMenu.ItemLink>
);
};
return null;
}
return (
- <ItemNavLink
+ <DropdownMenu.ItemLink
key="update_key"
to={{ pathname: '/project/key', search: new URLSearchParams(query).toString() }}
>
{translate('update_key.page')}
- </ItemNavLink>
+ </DropdownMenu.ItemLink>
);
};
return null;
}
return (
- <ItemNavLink
+ <DropdownMenu.ItemLink
key="webhooks"
to={{ pathname: '/project/webhooks', search: new URLSearchParams(query).toString() }}
>
{translate('webhooks.page')}
- </ItemNavLink>
+ </DropdownMenu.ItemLink>
);
};
}
return (
- <ItemNavLink
+ <DropdownMenu.ItemLink
key="project_delete"
to={{ pathname: '/project/deletion', search: new URLSearchParams(query).toString() }}
>
{translate('deletion.page')}
- </ItemNavLink>
+ </DropdownMenu.ItemLink>
);
};
const pathname = isAdmin ? `/project/admin/extension/${key}` : `/project/extension/${key}`;
const query = { ...baseQuery, qualifier };
return (
- <ItemNavLink key={key} to={{ pathname, search: new URLSearchParams(query).toString() }}>
+ <DropdownMenu.ItemLink
+ key={key}
+ to={{ pathname, search: new URLSearchParams(query).toString() }}
+ >
{name}
- </ItemNavLink>
+ </DropdownMenu.ItemLink>
);
};
}
return (
- <Dropdown
+ <DropdownMenu.Root
data-test="extensions"
id="component-navigation-more"
- size="auto"
- zLevel={PopupZLevel.Global}
- overlay={withoutSecurityExtension.map((e) => renderExtension(e, false, query))}
+ items={withoutSecurityExtension.map((e) => renderExtension(e, false, query))}
>
- {({ onToggleClick, open, a11yAttrs }) => (
- <NavBarTabLink
- active={open}
- onClick={onToggleClick}
- preventDefault
- text={translate('more')}
- withChevron
- to={{}}
- {...a11yAttrs}
- />
- )}
- </Dropdown>
+ <NavBarTabLink preventDefault text={translate('more')} withChevron to={{}} />
+ </DropdownMenu.Root>
);
};
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { screen } from '@testing-library/react';
+
+import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import { ComponentQualifier } from '~sonar-aligned/types/component';
expect(screen.getByRole('link', { name: 'layout.security_reports' })).toBeInTheDocument();
// Check the dropdown.
- const button = screen.getByRole('button', { name: 'more' });
+ const button = screen.getByRole('link', { name: 'more' });
expect(button).toBeInTheDocument();
await user.click(button);
expect(screen.getByRole('menuitem', { name: 'ComponentFoo' })).toBeInTheDocument();
extensions: [{ key: 'component-foo', name: 'ComponentFoo' }],
},
},
- 'branch=normal-branch',
+ 'id=foo&branch=normal-branch',
);
expect(await screen.findByRole('link', { name: 'overview.page' })).toBeInTheDocument();
extensions: [{ key: 'component-foo', name: 'ComponentFoo' }],
},
},
- 'pullRequest=01',
+ 'id=foo&pullRequest=01',
);
expect(await screen.findByRole('link', { name: 'overview.page' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'issues.page' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'layout.measures' })).toBeInTheDocument();
- expect(
- screen.queryByRole('link', { name: `layout.settings.${ComponentQualifier.Project}` }),
- ).not.toBeInTheDocument();
+ await waitFor(() => {
+ expect(
+ screen.queryByRole('link', { name: `layout.settings.${ComponentQualifier.Project}` }),
+ ).not.toBeInTheDocument();
+ });
+
expect(screen.queryByRole('button', { name: 'project.info.title' })).not.toBeInTheDocument();
});
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { Dropdown, ItemNavLink, MainMenuItem, PopupPlacement, PopupZLevel } from 'design-system';
+
+import { DropdownMenu } from '@sonarsource/echoes-react';
+import { MainMenuItem } from 'design-system';
import * as React from 'react';
import { translate } from '../../../../helpers/l10n';
import { AppState } from '../../../../types/appstate';
const renderGlobalPageLink = ({ key, name }: Extension) => {
return (
- <ItemNavLink key={key} to={`/extension/${key}`}>
+ <DropdownMenu.ItemLink key={key} to={`/extension/${key}`}>
{name}
- </ItemNavLink>
+ </DropdownMenu.ItemLink>
);
};
-function GlobalNavMore({ appState: { globalPages = [] } }: { appState: AppState }) {
+function GlobalNavMore({ appState: { globalPages = [] } }: Readonly<{ appState: AppState }>) {
const withoutPortfolios = globalPages.filter((page) => page.key !== 'governance/portfolios');
if (withoutPortfolios.length === 0) {
}
return (
- <Dropdown
+ <DropdownMenu.Root
+ align="start"
id="moreMenuDropdown"
- overlay={<ul>{withoutPortfolios.map(renderGlobalPageLink)}</ul>}
- placement={PopupPlacement.BottomLeft}
- zLevel={PopupZLevel.Global}
+ items={withoutPortfolios.map(renderGlobalPageLink)}
>
- {({ onToggleClick, open }) => (
- <ul>
- <MainMenuItem>
- <a
- aria-expanded={open}
- aria-haspopup="menu"
- href="#"
- id="global-navigation-more"
- onClick={onToggleClick}
- role="button"
- >
- {translate('more')}
- </a>
- </MainMenuItem>
- </ul>
- )}
- </Dropdown>
+ <MainMenuItem>
+ <a aria-haspopup="menu" href="#" id="global-navigation-more" role="button">
+ {translate('more')}
+ </a>
+ </MainMenuItem>
+ </DropdownMenu.Root>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import {
- Dropdown,
- ItemNavLink,
- LightLabel,
- NavBarTabLink,
- NavBarTabs,
- PopupZLevel,
- TopBar,
-} from 'design-system';
+
+import { DropdownMenu } from '@sonarsource/echoes-react';
+import { LightLabel, NavBarTabLink, NavBarTabs, TopBar } from 'design-system';
import * as React from 'react';
import { Location } from 'react-router-dom';
import withLocation from '../../../../components/hoc/withLocation';
renderExtension = ({ key, name }: Extension) => {
return (
- <ItemNavLink key={key} to={`/admin/extension/${key}`}>
+ <DropdownMenu.ItemLink isMatchingFullPath key={key} to={`/admin/extension/${key}`}>
{name}
- </ItemNavLink>
+ </DropdownMenu.ItemLink>
);
};
const extensionsWithoutSupport = this.props.extensions.filter(
(extension) => extension.key !== 'license/support',
);
+
return (
- <Dropdown
+ <DropdownMenu.Root
+ align="start"
id="settings-navigation-configuration-dropdown"
- overlay={
+ items={
<>
- <ItemNavLink end to="/admin/settings">
+ <DropdownMenu.ItemLink isMatchingFullPath to="/admin/settings">
{translate('settings.page')}
- </ItemNavLink>
+ </DropdownMenu.ItemLink>
- <ItemNavLink end to="/admin/settings/encryption">
+ <DropdownMenu.ItemLink isMatchingFullPath to="/admin/settings/encryption">
{translate('property.category.security.encryption')}
- </ItemNavLink>
+ </DropdownMenu.ItemLink>
- <ItemNavLink end to="/admin/webhooks">
+ <DropdownMenu.ItemLink isMatchingFullPath to="/admin/webhooks">
{translate('webhooks.page')}
- </ItemNavLink>
+ </DropdownMenu.ItemLink>
{extensionsWithoutSupport.map(this.renderExtension)}
</>
}
- size="auto"
- zLevel={PopupZLevel.Global}
>
- {({ onToggleClick, open }) => (
- <NavBarTabLink
- aria-expanded={open}
- aria-haspopup="menu"
- active={
- open ||
- (!this.isSecurityActive() &&
- !this.isProjectsActive() &&
- !this.isSystemActive() &&
- !this.isSomethingActive(['/admin/extension/license/support']) &&
- !this.isMarketplace() &&
- !this.isAudit())
- }
- to={{}}
- id="settings-navigation-configuration"
- onClick={onToggleClick}
- text={translate('sidebar.project_settings')}
- withChevron
- />
- )}
- </Dropdown>
+ <NavBarTabLink
+ aria-haspopup="menu"
+ active={
+ !this.isSecurityActive() &&
+ !this.isProjectsActive() &&
+ !this.isSystemActive() &&
+ !this.isSomethingActive(['/admin/extension/license/support']) &&
+ !this.isMarketplace() &&
+ !this.isAudit()
+ }
+ id="settings-navigation-configuration"
+ text={translate('sidebar.project_settings')}
+ to={{}}
+ withChevron
+ />
+ </DropdownMenu.Root>
);
}
renderProjectsTab() {
return (
- <Dropdown
+ <DropdownMenu.Root
id="settings-navigation-projects-dropdown"
- overlay={
+ items={
<>
- <ItemNavLink end to="/admin/projects_management">
+ <DropdownMenu.ItemLink isMatchingFullPath to="/admin/projects_management">
{translate('management')}
- </ItemNavLink>
+ </DropdownMenu.ItemLink>
- <ItemNavLink end to="/admin/background_tasks">
+ <DropdownMenu.ItemLink isMatchingFullPath to="/admin/background_tasks">
{translate('background_tasks.page')}
- </ItemNavLink>
+ </DropdownMenu.ItemLink>
</>
}
- size="auto"
- zLevel={PopupZLevel.Global}
>
- {({ onToggleClick, open }) => (
- <NavBarTabLink
- aria-expanded={open}
- aria-haspopup="menu"
- active={open || this.isProjectsActive()}
- to={{}}
- onClick={onToggleClick}
- text={translate('sidebar.projects')}
- withChevron
- />
- )}
- </Dropdown>
+ <NavBarTabLink
+ aria-haspopup="menu"
+ active={this.isProjectsActive()}
+ to={{}}
+ text={translate('sidebar.projects')}
+ withChevron
+ />
+ </DropdownMenu.Root>
);
}
renderSecurityTab() {
return (
- <Dropdown
+ <DropdownMenu.Root
id="settings-navigation-security-dropdown"
- overlay={
+ items={
<>
- <ItemNavLink to="/admin/users">{translate('users.page')}</ItemNavLink>
+ <DropdownMenu.ItemLink isMatchingFullPath to="/admin/users">
+ {translate('users.page')}
+ </DropdownMenu.ItemLink>
- <ItemNavLink to="/admin/groups">{translate('user_groups.page')}</ItemNavLink>
+ <DropdownMenu.ItemLink isMatchingFullPath to="/admin/groups">
+ {translate('user_groups.page')}
+ </DropdownMenu.ItemLink>
- <ItemNavLink to="/admin/permissions">
+ <DropdownMenu.ItemLink isMatchingFullPath to="/admin/permissions">
{translate('global_permissions.page')}
- </ItemNavLink>
+ </DropdownMenu.ItemLink>
- <ItemNavLink to="/admin/permission_templates">
+ <DropdownMenu.ItemLink isMatchingFullPath to="/admin/permission_templates">
{translate('permission_templates')}
- </ItemNavLink>
+ </DropdownMenu.ItemLink>
</>
}
- size="auto"
- zLevel={PopupZLevel.Global}
>
- {({ onToggleClick, open }) => (
- <NavBarTabLink
- aria-expanded={open}
- aria-haspopup="menu"
- active={open || this.isSecurityActive()}
- to={{}}
- onClick={onToggleClick}
- text={translate('sidebar.security')}
- withChevron
- />
- )}
- </Dropdown>
+ <NavBarTabLink
+ aria-haspopup="menu"
+ active={this.isSecurityActive()}
+ to={{}}
+ text={translate('sidebar.security')}
+ withChevron
+ />
+ </DropdownMenu.Root>
);
}
render() {
const { extensions, pendingPlugins } = this.props;
const hasSupportExtension = extensions.find((extension) => extension.key === 'license/support');
+
const hasGovernanceExtension = extensions.find(
(e) => e.key === AdminPageExtension.GovernanceConsole,
);
+
const totalPendingPlugins =
pendingPlugins.installing.length +
pendingPlugins.removing.length +
pendingPlugins.updating.length;
+
let notifComponent;
+
if (this.props.systemStatus === 'RESTARTING') {
notifComponent = <SystemRestartNotif />;
} else if (totalPendingPlugins > 0) {
)}
</NavBarTabs>
</TopBar>
+
{notifComponent}
</>
);
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import {
- ButtonSecondary,
- DropdownMenu,
- DropdownToggler,
- ItemButton,
- PopupPlacement,
- PopupZLevel,
- addGlobalErrorMessage,
- addGlobalSuccessMessage,
-} from 'design-system';
+import { DropdownMenu } from '@sonarsource/echoes-react';
+import { addGlobalErrorMessage, addGlobalSuccessMessage, ButtonSecondary } from 'design-system';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import DocumentationLink from '../../../components/common/DocumentationLink';
pullRequestID?: string;
}
-interface State {
- disabled?: boolean;
- ides: Ide[];
-}
-
const showError = () =>
addGlobalErrorMessage(
<FormattedMessage
projectKey,
pullRequestID,
}: Readonly<Props>) {
- const [state, setState] = React.useState<State>({ disabled: false, ides: [] });
+ const [isDisabled, setIsDisabled] = React.useState(false);
+ const [ides, setIdes] = React.useState<Ide[] | undefined>(undefined);
+ const ref = React.useRef<HTMLButtonElement>(null);
- const cleanState = () => {
- setState({ ...state, ides: [] });
- };
+ // to give focus back to the trigger button once it is re-rendered as a single button
+ const focusTriggerButton = React.useCallback(() => {
+ setTimeout(() => {
+ ref.current?.focus();
+ });
+ }, []);
const openIssue = async (ide: Ide) => {
- setState({ ...state, disabled: true, ides: [] }); // close the dropdown, disable the button
+ setIsDisabled(true);
let token: { name?: string; token?: string } = {};
setTimeout(
() => {
- setState({ ...state, disabled: false });
+ setIsDisabled(false);
+ focusTriggerButton();
},
ide.needsToken ? DELAY_AFTER_TOKEN_CREATION : 0,
);
};
- const onClick = async () => {
- setState({ ...state, ides: [] });
+ const findIDEs = async () => {
+ setIdes(undefined);
const ides = (await probeSonarLintServers()) ?? [];
} else if (ides.length === 1) {
openIssue(ides[0]);
} else {
- setState({ ...state, ides });
+ setIdes(ides);
}
};
- return (
- <div>
- <DropdownToggler
- allowResizing
- onRequestClose={cleanState}
- open={state.ides.length > 1}
- overlay={
- <DropdownMenu size="auto">
- {state.ides.map((ide) => {
- const { ideName, description } = ide;
-
- const label = ideName + (description ? ` - ${description}` : '');
-
- return (
- <ItemButton
- key={ide.port}
- onClick={() => {
- openIssue(ide);
- }}
- >
- {label}
- </ItemButton>
- );
- })}
- </DropdownMenu>
- }
- placement={PopupPlacement.BottomLeft}
- zLevel={PopupZLevel.Global}
- >
- <ButtonSecondary
- className="sw-whitespace-nowrap"
- disabled={state.disabled}
- onClick={onClick}
- >
- {translate('open_in_ide')}
- </ButtonSecondary>
- </DropdownToggler>
- </div>
+ const onClick = ides === undefined ? findIDEs : undefined;
+
+ const triggerButton = (
+ <ButtonSecondary
+ className="sw-whitespace-nowrap"
+ disabled={isDisabled}
+ onClick={onClick}
+ ref={ref}
+ >
+ {translate('open_in_ide')}
+ </ButtonSecondary>
+ );
+
+ return ides === undefined ? (
+ triggerButton
+ ) : (
+ <DropdownMenu.Root
+ isOpenOnMount
+ items={ides.map((ide) => {
+ const { ideName, description } = ide;
+
+ const label = ideName + (description ? ` - ${description}` : '');
+
+ return (
+ <DropdownMenu.ItemButton
+ key={ide.port}
+ onClick={() => {
+ openIssue(ide);
+ }}
+ >
+ {label}
+ </DropdownMenu.ItemButton>
+ );
+ })}
+ onClose={() => {
+ setIdes(undefined);
+ focusTriggerButton();
+ }}
+ onOpen={findIDEs}
+ >
+ {triggerButton}
+ </DropdownMenu.Root>
);
}
import { fillBranchLike, isSameBranchLike } from '../../../helpers/branch-like';
import handleRequiredAuthentication from '../../../helpers/handleRequiredAuthentication';
import { parseIssueFromResponse } from '../../../helpers/issues';
-import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers';
+import { isDropdown, isInput, isShortcut } from '../../../helpers/keyboardEventHelpers';
import { KeyboardKeys } from '../../../helpers/keycodes';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { serializeDate } from '../../../helpers/query';
return;
}
- if (isInput(event) || isShortcut(event)) {
+ if (isInput(event) || isShortcut(event) || isDropdown(event)) {
return;
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import {
- ButtonSecondary,
- ChevronDownIcon,
- Dropdown,
- ItemButton,
- PopupPlacement,
- PopupZLevel,
- TextMuted,
-} from 'design-system';
+
+import { DropdownMenu } from '@sonarsource/echoes-react';
+import { ButtonSecondary, ChevronDownIcon, TextMuted } from 'design-system';
import * as React from 'react';
import { translate } from '../../helpers/l10n';
import { GraphType } from '../../types/project-activity';
const label = translate('project_activity.graphs', type);
return (
- <ItemButton key={label} onClick={() => handleGraphChange(type)}>
+ <DropdownMenu.ItemButton key={label} onClick={() => handleGraphChange(type)}>
{label}
- </ItemButton>
+ </DropdownMenu.ItemButton>
);
});
}, [noCustomGraph, handleGraphChange]);
return (
<div className={className}>
<div className="sw-flex">
- <Dropdown
- id="activity-graph-type"
- size="auto"
- placement={PopupPlacement.BottomLeft}
- zLevel={PopupZLevel.Content}
- overlay={options}
- >
+ <DropdownMenu.Root id="activity-graph-type" align="start" items={options}>
<ButtonSecondary
aria-label={translate('project_activity.graphs.choose_type')}
className={
<TextMuted text={translate('project_activity.graphs', graph)} />
<ChevronDownIcon className="sw-ml-1 sw-mr-0 sw-pr-0" />
</ButtonSecondary>
- </Dropdown>
+ </DropdownMenu.Root>
{isCustomGraph(graph) &&
props.onAddCustomMetric !== undefined &&
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { ItemLink, OpenNewTabIcon } from 'design-system';
+import { DropdownMenu } from '@sonarsource/echoes-react';
import * as React from 'react';
import { DocLink } from '../../helpers/doc-links';
import { useDocUrl } from '../../helpers/docs';
interface Props {
children: React.ReactNode;
- innerRef?: React.Ref<HTMLAnchorElement>;
to: DocLink;
}
-export function DocItemLink({ to, innerRef, children }: Props) {
+export function DocItemLink({ to, children }: Readonly<Props>) {
const toStatic = useDocUrl(to);
- return (
- <ItemLink innerRef={innerRef} to={toStatic}>
- <OpenNewTabIcon />
- {children}
- </ItemLink>
- );
+ return <DropdownMenu.ItemLink to={toStatic}>{children}</DropdownMenu.ItemLink>;
}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { ItemDivider, ItemHeader, ItemLink, OpenNewTabIcon } from 'design-system';
+import { DropdownMenu } from '@sonarsource/echoes-react';
import * as React from 'react';
import { DocLink } from '../../helpers/doc-links';
import { translate } from '../../helpers/l10n';
text: string;
}) {
return (
- <ItemLink to={link}>
- <Image
- alt={text}
- aria-hidden
- className="sw-mr-2"
- height="18"
- src={`/images/${icon}`}
- width="18"
- />
+ <DropdownMenu.ItemLink
+ prefix={
+ <Image
+ alt={text}
+ aria-hidden
+ className="sw-mr-2"
+ height="18"
+ src={`/images/${icon}`}
+ width="18"
+ />
+ }
+ to={link}
+ >
{text}
- </ItemLink>
+ </DropdownMenu.ItemLink>
);
}
-function Suggestions({
- firstItemRef,
- suggestions,
-}: {
- firstItemRef: React.RefObject<HTMLAnchorElement>;
- suggestions: SuggestionLink[];
-}) {
+function Suggestions({ suggestions }: Readonly<{ suggestions: SuggestionLink[] }>) {
return (
<>
- <ItemHeader id="suggestion">{translate('docs.suggestion')}</ItemHeader>
- {suggestions.map((suggestion, i) => (
- <DocItemLink
- innerRef={i === 0 ? firstItemRef : undefined}
- key={suggestion.link}
- to={suggestion.link}
- >
+ <DropdownMenu.GroupLabel>{translate('docs.suggestion')}</DropdownMenu.GroupLabel>
+
+ {suggestions.map((suggestion) => (
+ <DocItemLink key={suggestion.link} to={suggestion.link}>
{suggestion.text}
</DocItemLink>
))}
- <ItemDivider />
+
+ <DropdownMenu.Separator />
</>
);
}
return (
<>
- {suggestions.length !== 0 && (
- <Suggestions firstItemRef={firstItemRef} suggestions={suggestions} />
- )}
- <DocItemLink innerRef={suggestions.length === 0 ? firstItemRef : undefined} to={DocLink.Root}>
- {translate('docs.documentation')}
- </DocItemLink>
- <ItemLink to="/web_api">{translate('api_documentation.page')}</ItemLink>
- <ItemLink to="/web_api_v2">{translate('api_documentation.page.v2')}</ItemLink>
- <ItemDivider />
- <ItemLink to="https://community.sonarsource.com/">
- <OpenNewTabIcon />
+ {suggestions.length !== 0 && <Suggestions suggestions={suggestions} />}
+
+ <DocItemLink to={DocLink.Root}>{translate('docs.documentation')}</DocItemLink>
+
+ <DropdownMenu.ItemLink to="/web_api">
+ {translate('api_documentation.page')}
+ </DropdownMenu.ItemLink>
+
+ <DropdownMenu.ItemLink to="/web_api_v2">
+ {translate('api_documentation.page.v2')}
+ </DropdownMenu.ItemLink>
+
+ <DropdownMenu.Separator />
+
+ <DropdownMenu.ItemLink to="https://community.sonarsource.com/">
{translate('docs.get_help')}
- </ItemLink>
- <ItemDivider />
- <ItemHeader id="stay_connected">{translate('docs.stay_connected')}</ItemHeader>
+ </DropdownMenu.ItemLink>
+
+ <DropdownMenu.Separator />
+
+ <DropdownMenu.GroupLabel>{translate('docs.stay_connected')}</DropdownMenu.GroupLabel>
+
<IconLink
link="https://www.sonarsource.com/products/sonarqube/whats-new/?referrer=sonarqube"
text={translate('docs.news')}
/>
+
<IconLink
link="https://www.sonarsource.com/products/sonarqube/roadmap/?referrer=sonarqube"
text={translate('docs.roadmap')}
/>
+
<IconLink
icon="embed-doc/x-icon-black.svg"
link="https://twitter.com/SonarQube"
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { IconQuestionMark } from '@sonarsource/echoes-react';
-import { Dropdown, InteractiveIcon, PopupPlacement, PopupZLevel } from 'design-system';
+import { DropdownMenu, IconQuestionMark, Tooltip } from '@sonarsource/echoes-react';
+import { InteractiveIcon } from 'design-system';
import * as React from 'react';
import { translate } from '../../helpers/l10n';
-import Tooltip from '../controls/Tooltip';
import { EmbedDocsPopup } from './EmbedDocsPopup';
export default function EmbedDocsPopupHelper() {
return (
<div className="dropdown">
- <Dropdown
- id="help-menu-dropdown"
- placement={PopupPlacement.BottomRight}
- overlay={<EmbedDocsPopup />}
- allowResizing
- zLevel={PopupZLevel.Global}
- >
- {({ onToggleClick, open }) => (
- <Tooltip mouseLeaveDelay={0.2} content={!open ? translate('help') : undefined}>
- <InteractiveIcon
- Icon={IconQuestionMark}
- aria-expanded={open}
- data-guiding-id="issue-5"
- aria-controls="help-menu-dropdown"
- aria-haspopup
- aria-label={translate('help')}
- currentColor
- onClick={onToggleClick}
- size="medium"
- stopPropagation={false}
- />
- </Tooltip>
- )}
- </Dropdown>
+ <DropdownMenu.Root align="end" id="help-menu-dropdown" items={<EmbedDocsPopup />}>
+ <Tooltip content={translate('help')}>
+ <InteractiveIcon
+ Icon={IconQuestionMark}
+ data-guiding-id="issue-5"
+ aria-controls="help-menu-dropdown"
+ aria-haspopup
+ aria-label={translate('help')}
+ currentColor
+ size="medium"
+ stopPropagation={false}
+ />
+ </Tooltip>
+ </DropdownMenu.Root>
</div>
);
}
expect(screen.getByText('docs.documentation')).toBeInTheDocument();
expect(screen.queryByText('docs.suggestion')).not.toBeInTheDocument();
-
- expect(screen.getByText('docs.documentation')).toHaveFocus();
});
it('should be able to render with suggestions and remove them', async () => {
expect(screen.getByText('docs.suggestion')).toBeInTheDocument();
expect(screen.getByText('About Background Tasks')).toBeInTheDocument();
- expect(screen.getByText('About Background Tasks')).toHaveFocus();
-
await user.click(screen.getByRole('button', { name: 'remove.suggestion' }));
await user.click(screen.getByRole('button', { name: 'help' }));
expect(screen.queryByText('docs.suggestion')).not.toBeInTheDocument();
-
- expect(screen.getByText('docs.documentation')).toHaveFocus();
});
function renderEmbedDocsPopup() {
event.target instanceof HTMLTextAreaElement
);
}
+
+export function isDropdown(event: KeyboardEvent) {
+ const role = (event.target as HTMLElement | null)?.role ?? '';
+
+ return ['menu', 'menuitem'].includes(role);
+}