import { lightTheme, themeColor } from 'design-system';
import * as React from 'react';
import { Outlet, useLocation } from 'react-router-dom';
-import A11yProvider from '../../components/a11y/A11yProvider';
-import A11ySkipLinks from '../../components/a11y/A11ySkipLinks';
+import A11yProvider from '~sonar-aligned/components/a11y/A11yProvider';
+import A11ySkipLinks from '~sonar-aligned/components/a11y/A11ySkipLinks';
import SuggestionsProvider from '../../components/embed-docs-modal/SuggestionsProvider';
import NCDAutoUpdateMessage from '../../components/new-code-definition/NCDAutoUpdateMessage';
import Workspace from '../../components/workspace/Workspace';
*/
import * as React from 'react';
import { Outlet } from 'react-router-dom';
-import A11ySkipTarget from '../../components/a11y/A11ySkipTarget';
+import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
import { Component } from '../../types/types';
import handleRequiredAuthorization from '../utils/handleRequiredAuthorization';
import withComponentContext from './componentContext/withComponentContext';
import { createPortal } from 'react-dom';
import { Helmet } from 'react-helmet-async';
import { Outlet } from 'react-router-dom';
+import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
import { useCurrentLoginUser } from '../../app/components/current-user/CurrentUserContext';
-import A11ySkipTarget from '../../components/a11y/A11ySkipTarget';
import Suggestions from '../../components/embed-docs-modal/Suggestions';
import { translate, translateWithParameters } from '../../helpers/l10n';
import Nav from './components/Nav';
import { difference, intersection } from 'lodash';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
+import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
import HelpTooltip from '~sonar-aligned/components/controls/HelpTooltip';
import { Location } from '~sonar-aligned/types/router';
-import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
import ListFooter from '../../../components/controls/ListFooter';
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
import AnalysisMissingInfoMessage from '../../../components/shared/AnalysisMissingInfoMessage';
import { keyBy } from 'lodash';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
+import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
import { withRouter } from '~sonar-aligned/components/hoc/withRouter';
import { Location, RawQuery, Router } from '~sonar-aligned/types/router';
import { Profile, searchQualityProfiles } from '../../../api/quality-profiles';
import { getRulesApp, searchRules } from '../../../api/rules';
import { getValue } from '../../../api/settings';
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
-import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
import ListFooter from '../../../components/controls/ListFooter';
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
import '../../../components/search-navigator.css';
*/
import { Highlight, KeyboardHint } from 'design-system';
import * as React from 'react';
+import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like';
import { MetricKey } from '~sonar-aligned/types/metrics';
import { Router } from '~sonar-aligned/types/router';
import { getComponentTree } from '../../../api/components';
import { getMeasures } from '../../../api/measures';
import SourceViewer from '../../../components/SourceViewer/SourceViewer';
-import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
import FilesCounter from '../../../components/ui/FilesCounter';
import { isSameBranchLike } from '../../../helpers/branch-like';
import { getComponentMeasureUniqueKey } from '../../../helpers/component';
*/
import { Spinner } from 'design-system';
import * as React from 'react';
+import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like';
import { getComponentLeaves } from '../../../api/components';
import SourceViewer from '../../../components/SourceViewer/SourceViewer';
-import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
import { isSameBranchLike } from '../../../helpers/branch-like';
import { BranchLike } from '../../../types/branch-like';
import { isFile } from '../../../types/component';
themeColor,
} from 'design-system';
import * as React from 'react';
-import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
+import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
import { translate } from '../../../helpers/l10n';
import useFollowScroll from '../../../hooks/useFollowScroll';
import { Domain } from '../../../types/measures';
import { LargeCenteredLayout } from 'design-system';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
+import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
import { withRouter } from '~sonar-aligned/components/hoc/withRouter';
import { Location, Router } from '~sonar-aligned/types/router';
import { getDopSettings } from '../../../api/dop-translation';
import withAvailableFeatures, {
WithAvailableFeaturesProps,
} from '../../../app/components/available-features/withAvailableFeatures';
-import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
import { translate } from '../../../helpers/l10n';
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
import { DopSetting } from '../../../types/dop-translation';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import { FormattedMessage } from 'react-intl';
+import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
import { withRouter } from '~sonar-aligned/components/hoc/withRouter';
import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like';
import { ComponentQualifier } from '~sonar-aligned/types/component';
import { getRuleDetails } from '../../../api/rules';
import withComponentContext from '../../../app/components/componentContext/withComponentContext';
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
-import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
import EmptySearch from '../../../components/common/EmptySearch';
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
import ListFooter from '../../../components/controls/ListFooter';
PageContentFontWrapper,
} from 'design-system';
import * as React from 'react';
+import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
import { useLocation, useRouter } from '~sonar-aligned/components/hoc/withRouter';
import { ComponentQualifier } from '~sonar-aligned/types/component';
-import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
import AnalysisMissingInfoMessage from '../../../components/shared/AnalysisMissingInfoMessage';
import { parseDate } from '../../../helpers/dates';
import { areCCTMeasuresComputed, isDiffMetric } from '../../../helpers/measures';
} from 'design-system';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
+import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
import { ComponentQualifier } from '~sonar-aligned/types/component';
-import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
import { translate } from '../../../helpers/l10n';
import { MeasureHistory, ParsedAnalysis } from '../../../types/project-activity';
import { Helmet } from 'react-helmet-async';
import { FormattedMessage } from 'react-intl';
import { OptionProps, components } from 'react-select';
+import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
import HelpTooltip from '~sonar-aligned/components/controls/HelpTooltip';
-import A11ySkipTarget from '../../components/a11y/A11ySkipTarget';
import DisableableSelectOption from '../../components/common/DisableableSelectOption';
import Suggestions from '../../components/embed-docs-modal/Suggestions';
import { translate } from '../../helpers/l10n';
import { groupBy, orderBy } from 'lodash';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
+import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
import HelpTooltip from '~sonar-aligned/components/controls/HelpTooltip';
import { Profile } from '../../api/quality-profiles';
-import A11ySkipTarget from '../../components/a11y/A11ySkipTarget';
import Suggestions from '../../components/embed-docs-modal/Suggestions';
import { translate } from '../../helpers/l10n';
import { getRulesUrl } from '../../helpers/urls';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import { useSearchParams } from 'react-router-dom';
+import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
import { withRouter } from '~sonar-aligned/components/hoc/withRouter';
import { ComponentQualifier } from '~sonar-aligned/types/component';
import { MetricKey } from '~sonar-aligned/types/metrics';
import { searchProjects } from '../../../api/components';
import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
-import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
import '../../../components/search-navigator.css';
} from 'design-system';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
+import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
import { isBranch } from '~sonar-aligned/helpers/branch-like';
import { ComponentQualifier } from '~sonar-aligned/types/component';
import { MetricKey } from '~sonar-aligned/types/metrics';
-import A11ySkipTarget from '../../components/a11y/A11ySkipTarget';
import Suggestions from '../../components/embed-docs-modal/Suggestions';
import { translate } from '../../helpers/l10n';
import useFollowScroll from '../../hooks/useFollowScroll';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import { Params, useParams } from 'react-router-dom';
+import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
import { withRouter } from '~sonar-aligned/components/hoc/withRouter';
import { Location, Router } from '~sonar-aligned/types/router';
import { fetchWebApi } from '../../../api/web-api';
-import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
import { translate } from '../../../helpers/l10n';
import { WebApi } from '../../../types/types';
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2024 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { createContext } from 'react';
-import { A11ySkipLink } from '../../types/types';
-
-export interface A11yContextShape {
- addA11ySkipLink: (link: A11ySkipLink) => void;
- removeA11ySkipLink: (link: A11ySkipLink) => void;
- links: A11ySkipLink[];
-}
-
-export const A11yContext = createContext<A11yContextShape>({
- addA11ySkipLink: () => {
- /* Implemented by Provider */
- },
- removeA11ySkipLink: () => {
- /* Implemented by Provider */
- },
- links: [],
-});
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2024 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import { sortBy } from 'lodash';
-import * as React from 'react';
-import { A11ySkipLink } from '../../types/types';
-import { A11yContext } from './A11yContext';
-
-interface State {
- links: A11ySkipLink[];
-}
-
-export default class A11yProvider extends React.Component<React.PropsWithChildren, State> {
- keys: string[] = [];
- state: State = { links: [] };
-
- addA11ySkipLink = (link: A11ySkipLink) => {
- this.setState((prevState) => {
- const links = [...prevState.links];
- links.push({ ...link, weight: link.weight || 0 });
- return { links };
- });
- };
-
- removeA11ySkipLink = (link: A11ySkipLink) => {
- this.setState((prevState) => {
- const links = prevState.links.filter((l) => l.key !== link.key);
- return { links };
- });
- };
-
- render() {
- const links = sortBy(this.state.links, 'weight');
- return (
- <A11yContext.Provider
- value={{
- addA11ySkipLink: this.addA11ySkipLink,
- links,
- removeA11ySkipLink: this.removeA11ySkipLink,
- }}
- >
- {this.props.children}
- </A11yContext.Provider>
- );
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2024 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-.a11y-skip-link {
- width: 0;
- height: 0;
- padding: var(--gridSize);
- position: absolute;
- left: -9999px;
- top: -9999px;
- border: 0;
- font-size: 1rem;
- text-align: center;
- z-index: 999;
-}
-
-.a11y-skip-link:focus {
- width: auto;
- height: auto;
- left: 6px;
- top: 6px;
- color: white;
- background-color: var(--globalNavBarBg);
- text-decoration: underline;
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2024 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import { A11yContext } from './A11yContext';
-import './A11ySkipLinks.css';
-
-export default function A11ySkipLinks() {
- return (
- <A11yContext.Consumer>
- {({ links }) =>
- links.map((link) => (
- <a className="a11y-skip-link" href={`#a11y_target__${link.key}`} key={link.key}>
- {link.label}
- </a>
- ))
- }
- </A11yContext.Consumer>
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2024 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import * as React from 'react';
-import { translate } from '../../helpers/l10n';
-import { A11ySkipLink } from '../../types/types';
-import { A11yContext } from './A11yContext';
-
-interface Props {
- anchor: string;
- label?: string;
- weight?: number;
-}
-
-export default function A11ySkipTarget(props: Props) {
- return (
- <A11yContext.Consumer>
- {({ addA11ySkipLink, removeA11ySkipLink }) => (
- <A11ySkipTargetInner
- addA11ySkipLink={addA11ySkipLink}
- removeA11ySkipLink={removeA11ySkipLink}
- {...props}
- />
- )}
- </A11yContext.Consumer>
- );
-}
-
-interface InnerProps {
- addA11ySkipLink: (link: A11ySkipLink) => void;
- removeA11ySkipLink: (link: A11ySkipLink) => void;
-}
-
-export class A11ySkipTargetInner extends React.PureComponent<Props & InnerProps> {
- componentDidMount() {
- this.props.addA11ySkipLink(this.getLink());
- }
-
- componentWillUnmount() {
- this.props.removeA11ySkipLink(this.getLink());
- }
-
- getLink = (): A11ySkipLink => {
- const { anchor: key, label = translate('skip_to_content'), weight } = this.props;
- return { key, label, weight };
- };
-
- render() {
- const { anchor } = this.props;
- return <span id={`a11y_target__${anchor}`} />;
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2024 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import userEvent from '@testing-library/user-event';
-import * as React from 'react';
-import { renderComponent } from '../../../helpers/testReactTestingUtils';
-import { byRole } from '../../../helpers/testSelector';
-import { A11yContext } from '../A11yContext';
-import A11yProvider from '../A11yProvider';
-import A11ySkipLinks from '../A11ySkipLinks';
-
-const ui = {
- links: byRole('link'),
-
- // specific to test
- addButton: byRole('button', { name: 'Add' }),
- removeButton: byRole('button', { name: 'Remove' }),
-};
-
-it('should render correctly', async () => {
- const user = userEvent.setup();
-
- renderA11ySkipLinks();
-
- expect(ui.links.queryAll()).toHaveLength(0);
-
- await user.click(ui.addButton.get());
- expect(ui.links.getAll()).toHaveLength(1);
-
- await user.click(ui.addButton.get());
- expect(ui.links.getAll()).toHaveLength(2);
-
- await user.click(ui.removeButton.get());
- expect(ui.links.getAll()).toHaveLength(1);
-});
-
-function renderA11ySkipLinks() {
- return renderComponent(
- <A11yProvider>
- <A11ySkipLinks />
-
- <LinkTester />
- </A11yProvider>,
- '/',
- {},
- );
-}
-
-let count = 0;
-
-function LinkTester() {
- const { addA11ySkipLink, removeA11ySkipLink } = React.useContext(A11yContext);
-
- return (
- <>
- <button
- onClick={() => {
- count += 1;
- addA11ySkipLink({ key: `${count}`, label: `link #${count}` });
- }}
- type="button"
- >
- Add
- </button>
- <button
- onClick={() => {
- removeA11ySkipLink({ key: `${count}`, label: `link #${count}` });
- count -= 1;
- }}
- type="button"
- >
- Remove
- </button>
- </>
- );
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { createContext } from 'react';
+import { A11ySkipLink } from '../../../types/types';
+
+export interface A11yContextShape {
+ addA11ySkipLink: (link: A11ySkipLink) => void;
+ removeA11ySkipLink: (link: A11ySkipLink) => void;
+ links: A11ySkipLink[];
+}
+
+export const A11yContext = createContext<A11yContextShape>({
+ addA11ySkipLink: () => {
+ /* Implemented by Provider */
+ },
+ removeA11ySkipLink: () => {
+ /* Implemented by Provider */
+ },
+ links: [],
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { sortBy } from 'lodash';
+import * as React from 'react';
+import { A11ySkipLink } from '../../../types/types';
+import { A11yContext } from './A11yContext';
+
+interface State {
+ links: A11ySkipLink[];
+}
+
+export default class A11yProvider extends React.Component<React.PropsWithChildren, State> {
+ keys: string[] = [];
+ state: State = { links: [] };
+
+ addA11ySkipLink = (link: A11ySkipLink) => {
+ this.setState((prevState) => {
+ const links = [...prevState.links];
+ links.push({ ...link, weight: link.weight || 0 });
+ return { links };
+ });
+ };
+
+ removeA11ySkipLink = (link: A11ySkipLink) => {
+ this.setState((prevState) => {
+ const links = prevState.links.filter((l) => l.key !== link.key);
+ return { links };
+ });
+ };
+
+ render() {
+ const links = sortBy(this.state.links, 'weight');
+ return (
+ <A11yContext.Provider
+ value={{
+ addA11ySkipLink: this.addA11ySkipLink,
+ links,
+ removeA11ySkipLink: this.removeA11ySkipLink,
+ }}
+ >
+ {this.props.children}
+ </A11yContext.Provider>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+.a11y-skip-link {
+ width: 0;
+ height: 0;
+ padding: var(--gridSize);
+ position: absolute;
+ left: -9999px;
+ top: -9999px;
+ border: 0;
+ font-size: 1rem;
+ text-align: center;
+ z-index: 999;
+}
+
+.a11y-skip-link:focus {
+ width: auto;
+ height: auto;
+ left: 6px;
+ top: 6px;
+ color: white;
+ background-color: var(--globalNavBarBg);
+ text-decoration: underline;
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { A11yContext } from './A11yContext';
+import './A11ySkipLinks.css';
+
+export default function A11ySkipLinks() {
+ return (
+ <A11yContext.Consumer>
+ {({ links }) =>
+ links.map((link) => (
+ <a className="a11y-skip-link" href={`#a11y_target__${link.key}`} key={link.key}>
+ {link.label}
+ </a>
+ ))
+ }
+ </A11yContext.Consumer>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { translate } from '../../../helpers/l10n';
+import { A11ySkipLink } from '../../../types/types';
+import { A11yContext } from './A11yContext';
+
+interface Props {
+ anchor: string;
+ label?: string;
+ weight?: number;
+}
+
+export default function A11ySkipTarget(props: Props) {
+ return (
+ <A11yContext.Consumer>
+ {({ addA11ySkipLink, removeA11ySkipLink }) => (
+ <A11ySkipTargetInner
+ addA11ySkipLink={addA11ySkipLink}
+ removeA11ySkipLink={removeA11ySkipLink}
+ {...props}
+ />
+ )}
+ </A11yContext.Consumer>
+ );
+}
+
+interface InnerProps {
+ addA11ySkipLink: (link: A11ySkipLink) => void;
+ removeA11ySkipLink: (link: A11ySkipLink) => void;
+}
+
+export class A11ySkipTargetInner extends React.PureComponent<Props & InnerProps> {
+ componentDidMount() {
+ this.props.addA11ySkipLink(this.getLink());
+ }
+
+ componentWillUnmount() {
+ this.props.removeA11ySkipLink(this.getLink());
+ }
+
+ getLink = (): A11ySkipLink => {
+ const { anchor: key, label = translate('skip_to_content'), weight } = this.props;
+ return { key, label, weight };
+ };
+
+ render() {
+ const { anchor } = this.props;
+ return <span id={`a11y_target__${anchor}`} />;
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import userEvent from '@testing-library/user-event';
+import * as React from 'react';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { byRole } from '../../../../helpers/testSelector';
+import { A11yContext } from '../A11yContext';
+import A11yProvider from '../A11yProvider';
+import A11ySkipLinks from '../A11ySkipLinks';
+
+const ui = {
+ links: byRole('link'),
+
+ // specific to test
+ addButton: byRole('button', { name: 'Add' }),
+ removeButton: byRole('button', { name: 'Remove' }),
+};
+
+it('should render correctly', async () => {
+ const user = userEvent.setup();
+
+ renderA11ySkipLinks();
+
+ expect(ui.links.queryAll()).toHaveLength(0);
+
+ await user.click(ui.addButton.get());
+ expect(ui.links.getAll()).toHaveLength(1);
+
+ await user.click(ui.addButton.get());
+ expect(ui.links.getAll()).toHaveLength(2);
+
+ await user.click(ui.removeButton.get());
+ expect(ui.links.getAll()).toHaveLength(1);
+});
+
+function renderA11ySkipLinks() {
+ return renderComponent(
+ <A11yProvider>
+ <A11ySkipLinks />
+
+ <LinkTester />
+ </A11yProvider>,
+ '/',
+ {},
+ );
+}
+
+let count = 0;
+
+function LinkTester() {
+ const { addA11ySkipLink, removeA11ySkipLink } = React.useContext(A11yContext);
+
+ return (
+ <>
+ <button
+ onClick={() => {
+ count += 1;
+ addA11ySkipLink({ key: `${count}`, label: `link #${count}` });
+ }}
+ type="button"
+ >
+ Add
+ </button>
+ <button
+ onClick={() => {
+ removeA11ySkipLink({ key: `${count}`, label: `link #${count}` });
+ count -= 1;
+ }}
+ type="button"
+ >
+ Remove
+ </button>
+ </>
+ );
+}