]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-18847 Help tooltips are not read by screen readers in narration mode
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Thu, 23 Mar 2023 11:28:13 +0000 (12:28 +0100)
committersonartech <sonartech@sonarsource.com>
Mon, 27 Mar 2023 20:03:02 +0000 (20:03 +0000)
26 files changed:
server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx
server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/ComponentMeasuresApp-test.tsx.snap
server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx
server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-it.tsx
server/sonar-web/src/main/js/apps/overview/components/__tests__/IssueLabel-test.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/PermissionTemplatesApp-it.tsx
server/sonar-web/src/main/js/apps/permission-templates/components/__tests__/__snapshots__/PermissionTemplatesApp-it.tsx.snap
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx
server/sonar-web/src/main/js/components/common/__tests__/DocumentationTooltip-test.tsx
server/sonar-web/src/main/js/components/controls/HelpTooltip.tsx
server/sonar-web/src/main/js/components/controls/Tooltip.tsx
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/HelpTooltip-test.tsx.snap
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Tooltip-test.tsx.snap
server/sonar-web/src/main/js/components/controls/clipboard.tsx
server/sonar-web/src/main/js/components/facet/FacetHeader.tsx
server/sonar-web/src/main/js/components/facet/__tests__/Facet-it.tsx
server/sonar-web/src/main/js/components/icons/Icon.tsx
server/sonar-web/src/main/js/components/tutorials/other/__tests__/OtherTutorial-it.tsx
server/sonar-web/src/main/js/components/ui/PageShortcutsTooltip.tsx
server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 5bd09e3c39d5483d5933855811675f5633e85681..281d41988b37a16e914299ac9bc7aec39902124d 100644 (file)
@@ -300,7 +300,6 @@ class CodeApp extends React.Component<Props, State> {
               {translate('code_viewer.not_all_measures_are_shown')}
               <HelpTooltip
                 className="spacer-left"
-                ariaLabel={translate('code_viewer.not_all_measures_are_shown.help')}
                 overlay={translate('code_viewer.not_all_measures_are_shown.help')}
               />
             </AlertContent>
index b5d7f9da79c15615b062a223b80f1030051a8481..36f66810afc55682a58bbfc0ecfae576fe82e6cd 100644 (file)
@@ -309,9 +309,6 @@ export class ComponentMeasuresApp extends React.PureComponent<Props, State> {
                           {translate('component_measures.not_all_measures_are_shown')}
                           <HelpTooltip
                             className="spacer-left"
-                            ariaLabel={translate(
-                              'component_measures.not_all_measures_are_shown.help'
-                            )}
                             overlay={translate(
                               'component_measures.not_all_measures_are_shown.help'
                             )}
index 671b12b6fdf4cf2124862f80da35388aa84802f6..5799f077206148ab7ff38a96b4abdfb0f6779cea 100644 (file)
@@ -77,7 +77,6 @@ exports[`should render a warning message when user does not have access to all p
         <Styled(div)>
           component_measures.not_all_measures_are_shown
           <HelpTooltip
-            ariaLabel="component_measures.not_all_measures_are_shown.help"
             className="spacer-left"
             overlay="component_measures.not_all_measures_are_shown.help"
           />
index f1043548c4400559d34528836655029e12f99954..76629db8f9111a0c3d710ed1651af15483914821 100644 (file)
@@ -288,7 +288,7 @@ describe('issues app', () => {
       );
       await user.click(screen.getByRole('button', { name: 'issues.bulk_change_X_issues.1' }));
 
-      await user.click(screen.getByRole('textbox', { name: 'issue.comment.formlink' }));
+      await user.click(screen.getByRole('textbox', { name: /issue.comment.formlink/ }));
       await user.keyboard('New Comment');
       expect(screen.getByRole('button', { name: 'apply' })).toBeDisabled();
 
index 3a7ffd27df26c5a6f0e0d823434539b5b2b8abdd..1cdb44ae166f8643436d9c54bf7c2ccac3b62010 100644 (file)
@@ -991,7 +991,6 @@ export class App extends React.PureComponent<Props, State> {
                       {translate('issues.not_all_issue_show')}
                       <HelpTooltip
                         className="spacer-left"
-                        ariaLabel={translate('issues.not_all_issue_show_why')}
                         overlay={translate('issues.not_all_issue_show_why')}
                       />
                     </AlertContent>
index bb0566cb831a62e731b1609d8f80e59580f0d7a4..d629ed13c86ea3ed23ea12dc12c320cca4673b06 100644 (file)
@@ -90,7 +90,7 @@ it('should disable the submit button unless some change is configured', async ()
   expect(await screen.findByRole('button', { name: 'apply' })).toBeDisabled();
 
   // Adding a comment should not enable the submit button
-  await user.type(screen.getByRole('textbox', { name: 'issue.comment.formlink' }), 'some comment');
+  await user.type(screen.getByRole('textbox', { name: /issue.comment.formlink/ }), 'some comment');
   expect(screen.getByRole('button', { name: 'apply' })).toBeDisabled();
 
   // Select a severity
@@ -159,7 +159,7 @@ it('should properly submit', async () => {
   ]);
 
   // Comment
-  await user.type(screen.getByRole('textbox', { name: 'issue.comment.formlink' }), 'some comment');
+  await user.type(screen.getByRole('textbox', { name: /issue.comment.formlink/ }), 'some comment');
 
   // Send notification
   await user.click(screen.getByRole('checkbox', { name: 'issue.send_notifications' }));
index f09ee72491c6e45b2fc365099dfaa368c7430762..8b349a7ae4d3601486e31597d8271a16631a3c2d 100644 (file)
@@ -22,7 +22,7 @@ import * as React from 'react';
 import { mockPullRequest } from '../../../../helpers/mocks/branch-like';
 import { mockComponent } from '../../../../helpers/mocks/component';
 import { mockMeasureEnhanced, mockMetric } from '../../../../helpers/testMocks';
-import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { findTooltipWithContent, renderComponent } from '../../../../helpers/testReactTestingUtils';
 import { IssueType } from '../../../../types/issues';
 import { MetricKey } from '../../../../types/metrics';
 import { IssueLabel, IssueLabelProps } from '../IssueLabel';
@@ -71,7 +71,7 @@ it('should render correctly for hotspots with tooltip', async () => {
     })
   ).toBeInTheDocument();
 
-  expect(await screen.findByText('tooltip text')).toBeInTheDocument();
+  expect(findTooltipWithContent('tooltip text')).toBeInTheDocument();
 });
 
 function renderIssueLabel(props: Partial<IssueLabelProps> = {}) {
index 97b58d1a82beb71846174f9870422818f08d7b74..a4d8002edb9a28964a51584d810719f70afbb00a 100644 (file)
@@ -26,7 +26,10 @@ import PermissionsServiceMock from '../../../../api/mocks/PermissionsServiceMock
 import { mockPermissionGroup, mockPermissionUser } from '../../../../helpers/mocks/permissions';
 import { PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE } from '../../../../helpers/permissions';
 import { mockAppState } from '../../../../helpers/testMocks';
-import { renderAppWithAdminContext } from '../../../../helpers/testReactTestingUtils';
+import {
+  findTooltipWithContent,
+  renderAppWithAdminContext,
+} from '../../../../helpers/testReactTestingUtils';
 import { ComponentQualifier } from '../../../../types/component';
 import { Permissions } from '../../../../types/permissions';
 import { PermissionGroup, PermissionUser } from '../../../../types/types';
@@ -51,16 +54,16 @@ describe('rendering', () => {
 
     // Shows all permission table headers.
     PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE.forEach((permission, i) => {
-      expect(ui.getTableHeaderHelpTooltip(i + 1)).toHaveTextContent(
-        `projects_role.${permission}.desc`
-      );
+      expect(
+        ui.getTableHeaderHelpTooltip(i + 1, `projects_role.${permission}.desc`)
+      ).toBeInTheDocument();
     });
 
     // Shows warning for browse and code viewer permissions.
     [Permissions.Browse, Permissions.CodeViewer].forEach((_permission, i) => {
-      expect(ui.getTableHeaderHelpTooltip(i + 1)).toHaveTextContent(
-        'projects_role.public_projects_warning'
-      );
+      expect(
+        ui.getTableHeaderHelpTooltip(i + 1, 'projects_role.public_projects_warning')
+      ).toBeInTheDocument();
     });
 
     // Check summaries.
@@ -94,7 +97,9 @@ describe('rendering', () => {
     expect(screen.getByText('This is permission template 1')).toBeInTheDocument();
     PERMISSIONS_ORDER_FOR_PROJECT_TEMPLATE.forEach((permission, i) => {
       expect(ui.permissionCheckbox('johndoe', permission).get()).toBeInTheDocument();
-      expect(ui.getTableHeaderHelpTooltip(i)).toHaveTextContent(`projects_role.${permission}.desc`);
+      expect(
+        ui.getTableHeaderHelpTooltip(i, `projects_role.${permission}.desc`)
+      ).toBeInTheDocument();
     });
   });
 });
@@ -511,13 +516,16 @@ function getPageObject(user: UserEvent) {
       await user.click(ui.cogMenuBtn(name).get());
       await user.click(ui.setDefaultBtn(qualifier).get());
     },
-    getTableHeaderHelpTooltip(i: number) {
+    getTableHeaderHelpTooltip(i: number, text: string) {
       const th = byRole('columnheader').getAll().at(i);
       if (th === undefined) {
         throw new Error(`Couldn't locate the <th> at index ${i}`);
       }
-      within(th).getByTestId('help-tooltip-activator').focus();
-      return screen.getByRole('tooltip');
+      return findTooltipWithContent((_content, element) => {
+        // For some reason, using the `content` parameter doesn't work for 1 of the
+        // tests. Explicitly using the element's `textContent` always works.
+        return Boolean(element?.textContent?.includes(text));
+      }, th);
     },
   };
 }
index e05b7037600234022dc99ef4c047a1c1bead14da..eebb77c2e35823a8d080114d690ae68186d8e806 100644 (file)
@@ -14,7 +14,7 @@ exports[`rendering should render the list of templates: Permission Template 1: u
 
 exports[`rendering should render the list of templates: Permission Template 2: admin 1`] = `"0  user(s)0 group(s)"`;
 
-exports[`rendering should render the list of templates: Permission Template 2: codeviewer 1`] = `"permission_templates.project_creatorspermission_templates.project_creators.explanation0  user(s)0 group(s)"`;
+exports[`rendering should render the list of templates: Permission Template 2: codeviewer 1`] = `"permission_templates.project_creatorspermission_templates.project_creators.explanationpermission_templates.project_creators.explanation0  user(s)0 group(s)"`;
 
 exports[`rendering should render the list of templates: Permission Template 2: issueadmin 1`] = `"0  user(s)0 group(s)"`;
 
@@ -22,4 +22,4 @@ exports[`rendering should render the list of templates: Permission Template 2: s
 
 exports[`rendering should render the list of templates: Permission Template 2: securityhotspotadmin 1`] = `"0  user(s)0 group(s)"`;
 
-exports[`rendering should render the list of templates: Permission Template 2: user 1`] = `"permission_templates.project_creatorspermission_templates.project_creators.explanation0  user(s)0 group(s)"`;
+exports[`rendering should render the list of templates: Permission Template 2: user 1`] = `"permission_templates.project_creatorspermission_templates.project_creators.explanationpermission_templates.project_creators.explanation0  user(s)0 group(s)"`;
index 0ed90728d12b881c81920d6e6d06a655a345a79c..cca136bfc21d64eac84fcf00c81c592f3e05544d 100644 (file)
@@ -74,7 +74,7 @@ export default class DetailsHeader extends React.PureComponent<Props> {
               {qualityGate.isBuiltIn && <BuiltInQualityGateBadge className="spacer-left" />}
               {qualityGate.caycStatus === CaycStatus.NonCompliant && (
                 <Tooltip overlay={<CaycBadgeTooltip />} mouseLeaveDelay={TOOLTIP_MOUSE_LEAVE_DELAY}>
-                  <AlertWarnIcon className="spacer-left" />
+                  <AlertWarnIcon className="spacer-left" description={<CaycBadgeTooltip />} />
                 </Tooltip>
               )}
             </div>
@@ -114,7 +114,6 @@ export default class DetailsHeader extends React.PureComponent<Props> {
                           ? translate('quality_gates.cannot_copy_no_cayc')
                           : null
                       }
-                      accessible={false}
                     >
                       <Button
                         className="little-spacer-left"
@@ -135,7 +134,6 @@ export default class DetailsHeader extends React.PureComponent<Props> {
                       ? translate('quality_gates.cannot_set_default_no_cayc')
                       : null
                   }
-                  accessible={false}
                 >
                   <Button
                     className="little-spacer-left"
index b3fc20a85b496de4f625cdb1a027f59afd85ae4f..979aff85c9c97a2b1b09b0caecfa5bd86b6de3a5 100644 (file)
@@ -50,13 +50,12 @@ export default function List({ qualityGates, currentQualityGate }: Props) {
           {qualityGate.isBuiltIn && <BuiltInQualityGateBadge className="little-spacer-left" />}
 
           {qualityGate.caycStatus === CaycStatus.NonCompliant && (
-            <>
-              {/* Adding a11y-hidden span for accessibility */}
-              <span className="a11y-hidden">{translate('quality_gates.cayc.tooltip.message')}</span>
-              <Tooltip overlay={translate('quality_gates.cayc.tooltip.message')} accessible={false}>
-                <AlertWarnIcon className="spacer-left" />
-              </Tooltip>
-            </>
+            <Tooltip overlay={translate('quality_gates.cayc.tooltip.message')}>
+              <AlertWarnIcon
+                className="spacer-left"
+                description={translate('quality_gates.cayc.tooltip.message')}
+              />
+            </Tooltip>
           )}
         </NavLink>
       ))}
index d98d0b873e97fb6abacea1c3877be4918e301a92..e1e0b8dfcd4fc12b30df20c533150ae72989e631 100644 (file)
@@ -169,11 +169,11 @@ it('should be able to set as default a quality gate which is CAYC compliant', as
   handler.setIsAdmin(true);
   renderQualityGateApp();
 
-  const notDefaultQualityGate = await screen.findByText('Sonar way');
+  const notDefaultQualityGate = await screen.findByRole('link', { name: /Sonar way/ });
   await user.click(notDefaultQualityGate);
   const setAsDefaultButton = screen.getByRole('button', { name: 'set_as_default' });
   await user.click(setAsDefaultButton);
-  expect(screen.getAllByRole('link')[2]).toHaveTextContent('default');
+  expect(screen.getByRole('link', { name: /Sonar way/ })).toHaveTextContent('default');
 });
 
 it('should be able to add a condition', async () => {
index bc4b9c4ff56a15101082d10ffe7a4a340f6c9be1..86d2fde8b8cbb848cbaa27c800b5821989e365da 100644 (file)
@@ -48,7 +48,7 @@ export function LineDuplicationBlock(props: LineDuplicationBlockProps) {
 
   return duplicated ? (
     <td className={className} data-index={index} data-line-number={line.line}>
-      <Tooltip overlay={tooltip} placement="right" accessible={false}>
+      <Tooltip overlay={tooltip} placement="right">
         <div>
           <Toggler
             onRequestClose={() => setDropdownOpen(false)}
index 38cd4696e82ade0e7875b15a523174f18ad06208..cfb21c7bf121acf5bb956e6acc735ecd64f887a8 100644 (file)
@@ -43,9 +43,9 @@ it('should correctly navigate through TAB', async () => {
   await user.tab();
   expect(ui.helpIcon.get()).toHaveFocus();
   await user.tab();
-  expect(ui.linkInTooltip.get()).toHaveFocus();
+  expect(ui.linkInTooltip.getAll().at(1)).toHaveFocus();
   await user.tab();
-  expect(ui.linkInTooltip2.get()).toHaveFocus();
+  expect(ui.linkInTooltip2.getAll().at(1)).toHaveFocus();
   // Looks like RTL tab event ignores any custom focuses during the events phase,
   // unless preventDefault is specified
   await user.tab();
index f99ef57da0717e926d7be30e004a1e1ca695c7ba..1ee3753ad9e8cb3f4eb60504b5b34f04bfca9698 100644 (file)
@@ -20,6 +20,7 @@
 import classNames from 'classnames';
 import * as React from 'react';
 import { colors } from '../../app/theme';
+import { translate } from '../../helpers/l10n';
 import HelpIcon from '../icons/HelpIcon';
 import { IconProps } from '../icons/Icon';
 import './HelpTooltip.css';
@@ -32,42 +33,45 @@ interface Props extends Pick<IconProps, 'size'> {
   onHide?: () => void;
   overlay: React.ReactNode;
   placement?: Placement;
-  ariaLabel?: string;
-  ariaLabelledby?: string;
   isInteractive?: boolean;
   innerRef?: React.Ref<HTMLSpanElement>;
 }
 
 const DEFAULT_SIZE = 12;
 
-export default function HelpTooltip({
-  size = DEFAULT_SIZE,
-  ariaLabel,
-  ariaLabelledby,
-  ...props
-}: Props) {
-  const role = ariaLabel || ariaLabelledby ? 'note' : undefined;
+export default function HelpTooltip(props: Props) {
+  const { size = DEFAULT_SIZE, overlay, placement, isInteractive, innerRef, children } = props;
   return (
-    <div
-      className={classNames('help-tooltip', props.className)}
-      aria-labelledby={ariaLabelledby}
-      aria-label={ariaLabel}
-      role={role}
-    >
+    <div className={classNames('help-tooltip', props.className)}>
       <Tooltip
         mouseLeaveDelay={0.25}
         onShow={props.onShow}
         onHide={props.onHide}
-        overlay={props.overlay}
-        placement={props.placement}
-        isInteractive={props.isInteractive}
+        overlay={overlay}
+        placement={placement}
+        isInteractive={isInteractive}
       >
         <span
           className="display-inline-flex-center"
           data-testid="help-tooltip-activator"
-          ref={props.innerRef}
+          ref={innerRef}
         >
-          {props.children || <HelpIcon fill={colors.gray60} size={size} />}
+          {children ?? (
+            <HelpIcon
+              fill={colors.gray60}
+              size={size}
+              description={
+                isInteractive ? (
+                  <>
+                    {translate('tooltip_is_interactive')}
+                    {overlay}
+                  </>
+                ) : (
+                  overlay
+                )
+              }
+            />
+          )}
         </span>
       </Tooltip>
     </div>
index f0c013aca299fbfd9df70cf1bf6fa208ead52f62..eac57073053b4cbd5a6b343b53a225304144d63e 100644 (file)
@@ -22,6 +22,7 @@ import { throttle, uniqueId } from 'lodash';
 import * as React from 'react';
 import { createPortal, findDOMNode } from 'react-dom';
 import { rawSizes } from '../../app/theme';
+import { translate } from '../../helpers/l10n';
 import EscKeydownHandler from './EscKeydownHandler';
 import FocusOutHandler from './FocusOutHandler';
 import ScreenPositionFixer from './ScreenPositionFixer';
@@ -30,9 +31,8 @@ import './Tooltip.css';
 export type Placement = 'bottom' | 'right' | 'left' | 'top';
 
 export interface TooltipProps {
-  accessible?: boolean;
   classNameSpace?: string;
-  children: React.ReactElement<{}>;
+  children: React.ReactElement;
   mouseEnterDelay?: number;
   mouseLeaveDelay?: number;
   onShow?: () => void;
@@ -398,23 +398,26 @@ export class TooltipInner extends React.Component<TooltipProps, State> {
 
   renderOverlay() {
     const isVisible = this.isVisible();
-    const { classNameSpace = 'tooltip', accessible = true } = this.props;
+    const { classNameSpace = 'tooltip', isInteractive, overlay } = this.props;
 
     return (
       <div
         className={classNames(`${classNameSpace}-inner`, { hidden: !isVisible })}
         id={this.id}
         role="tooltip"
-        aria-hidden={!accessible || !isVisible}
+        aria-hidden={!isInteractive || !isVisible}
       >
-        {this.props.overlay}
+        {isInteractive && (
+          <span className="a11y-hidden">{translate('tooltip_is_interactive')}</span>
+        )}
+        {overlay}
       </div>
     );
   }
 
   render() {
     const isVisible = this.isVisible();
-    const { accessible = true } = this.props;
+    const { isInteractive } = this.props;
     return (
       <>
         {React.cloneElement(this.props.children, {
@@ -422,12 +425,14 @@ export class TooltipInner extends React.Component<TooltipProps, State> {
           onPointerLeave: this.handleMouseLeave,
           onFocus: this.handleFocus,
           onBlur: this.handleBlur,
-          tabIndex: accessible ? 0 : undefined,
+          tabIndex: isInteractive ? 0 : undefined,
           // aria-describedby is the semantically correct property to use, but it's not
-          // always well supported. As a fallback, we use aria-labelledby as well.
-          // See https://sarahmhigley.com/writing/tooltips-in-wcag-21/
-          // See https://css-tricks.com/accessible-svgs/
-          'aria-describedby': accessible ? this.id : undefined,
+          // always well supported. We sometimes need to handle this differently, depending
+          // on the triggering element. For example, we can add a child <description> element
+          // if the triggering element is an SVG. See HelpTooltip for an example.
+          // We should NOT use aria-labelledby, as this can have unintended effects (e.g., this
+          // can mess up buttons that need a tooltip).
+          'aria-describedby': this.id,
         })}
         {!isVisible && this.renderOverlay()}
         {isVisible && (
index e5d600bdd42667144ea3e05f8fd2e0aad5b5ef34..34f670866ac198597ebb101116c9b4c7c0e9df88 100644 (file)
@@ -33,6 +33,11 @@ exports[`should render properly: default 1`] = `
       data-testid="help-tooltip-activator"
     >
       <HelpIcon
+        description={
+          <div
+            className="my-overlay"
+          />
+        }
         fill="#888"
         size={12}
       />
index 1af0c5401400453255cd9916a517228e6a5adcb9..b6373fd9dd39bc948a87266f7f521d37fdce0e2a 100644 (file)
@@ -21,7 +21,6 @@ exports[`should render 1`] = `
     onFocus={[Function]}
     onPointerEnter={[Function]}
     onPointerLeave={[Function]}
-    tabIndex={0}
   />
   <div
     aria-hidden={true}
@@ -45,7 +44,6 @@ exports[`should render 2`] = `
     onFocus={[Function]}
     onPointerEnter={[Function]}
     onPointerLeave={[Function]}
-    tabIndex={0}
   />
   <EscKeydownHandler
     onKeydown={[Function]}
index f8a2192044a343bbe4a668de7c322d4a2918c8b8..f7f4204a07eecdbdd44e0d6ebe2df07dc681e1c4 100644 (file)
@@ -111,14 +111,16 @@ export function ClipboardButton({
   return (
     <ClipboardBase>
       {({ setCopyButton, copySuccess }) => (
-        <Tooltip overlay={translate('copied_action')} visible={copySuccess} accessible={false}>
+        <Tooltip overlay={translate('copied_action')} visible={copySuccess}>
           <Button
             className={classNames('no-select', className)}
             data-clipboard-text={copyValue}
             innerRef={setCopyButton}
-            aria-label={ariaLabel ?? translate('copy_to_clipboard')}
+            aria-label={
+              copySuccess ? translate('copied_action') : ariaLabel ?? translate('copy_to_clipboard')
+            }
           >
-            {children || (
+            {children ?? (
               <>
                 <CopyIcon className="little-spacer-right" />
                 {translate('copy')}
@@ -138,18 +140,20 @@ export interface ClipboardIconButtonProps {
 }
 
 export function ClipboardIconButton(props: ClipboardIconButtonProps) {
-  const { className, copyValue } = props;
+  const { 'aria-label': ariaLabel, className, copyValue } = props;
   return (
     <ClipboardBase>
       {({ setCopyButton, copySuccess }) => {
         return (
           <ButtonIcon
-            aria-label={props['aria-label'] ?? translate('copy_to_clipboard')}
+            aria-label={
+              copySuccess ? translate('copied_action') : ariaLabel ?? translate('copy_to_clipboard')
+            }
             className={classNames('no-select', className)}
             data-clipboard-text={copyValue}
             innerRef={setCopyButton}
             tooltip={copySuccess ? translate('copied_action') : undefined}
-            tooltipProps={copySuccess ? { visible: copySuccess, accessible: false } : undefined}
+            tooltipProps={copySuccess ? { visible: true } : undefined}
           >
             <CopyIcon />
           </ButtonIcon>
index ab17be11ee5aa1f8ce80d7584f7c29a6ef71fdff..512b3e59f6ff643def4d2d831125ec8f04106988 100644 (file)
@@ -73,7 +73,7 @@ export default class FacetHeader extends React.PureComponent<Props> {
     const { disabled, values, disabledHelper, name, open, children, fetching } = this.props;
     const showClearButton = values != null && values.length > 0 && this.props.onClear != null;
     const header = disabled ? (
-      <Tooltip overlay={disabledHelper} accessible={false}>
+      <Tooltip overlay={disabledHelper}>
         <ButtonLink
           className="disabled"
           aria-disabled={true}
index d60d764bfd98ffbc955bd87127dfebc3a7f840b6..fe4030f2433aedcef3787ee0a64fcebba707447d 100644 (file)
@@ -20,7 +20,7 @@
 import { screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import * as React from 'react';
-import { renderComponent } from '../../../helpers/testReactTestingUtils';
+import { findTooltipWithContent, renderComponent } from '../../../helpers/testReactTestingUtils';
 import FacetBox, { FacetBoxProps } from '../FacetBox';
 import FacetHeader from '../FacetHeader';
 import FacetItem from '../FacetItem';
@@ -60,7 +60,7 @@ it('should correctly render a header with helper text', async () => {
   renderFacet(undefined, { helper: 'Help text' });
   await userEvent.tab();
   await userEvent.tab();
-  expect(screen.getByText('Help text')).toBeInTheDocument();
+  expect(findTooltipWithContent('Help text')).toBeInTheDocument();
 });
 
 it('should correctly render a header with value data', () => {
index e55755c6fb4bb1b18b98beaf97aac3c1d3c40729..54faa9dccf9e958b03c65edbcd2d24dcd4a333fc 100644 (file)
@@ -23,7 +23,8 @@ export interface IconProps extends React.AriaAttributes {
   className?: string;
   fill?: string;
   size?: number;
-  label?: string;
+  label?: React.ReactNode;
+  description?: React.ReactNode;
 }
 
 interface Props extends React.AriaAttributes {
@@ -31,7 +32,8 @@ interface Props extends React.AriaAttributes {
   className?: string;
   size?: number;
   style?: React.CSSProperties;
-  label?: string;
+  label?: React.ReactNode;
+  description?: React.ReactNode;
 
   // try to avoid using these:
   width?: number;
@@ -48,6 +50,7 @@ export default function Icon({
   width = size,
   viewBox = '0 0 16 16',
   label,
+  description,
   'aria-hidden': hidden,
   ...iconProps
 }: Props) {
@@ -70,6 +73,7 @@ export default function Icon({
       {...iconProps}
     >
       {label && !hidden && <title>{label}</title>}
+      {description && !hidden && <desc>{description}</desc>}
       {children}
     </svg>
   );
index 42d50464aae9f54416438bb62d139736e793c462..f91ff4835cef7bd8392bd0a5c928d55534af6101 100644 (file)
@@ -50,9 +50,9 @@ const ui = {
   runAnalysisTitle: byRole('heading', { name: 'onboarding.analysis.header' }),
   generateTokenRadio: byRole('radio', { name: 'onboarding.token.generate.PROJECT_ANALYSIS_TOKEN' }),
   existingTokenRadio: byRole('radio', { name: 'onboarding.token.use_existing_token' }),
-  tokenNameInput: byRole('textbox', { name: 'onboarding.token.name.label' }),
+  tokenNameInput: byRole('textbox', { name: /onboarding.token.name.label/ }),
   expiresInSelect: byRole('combobox', { name: '' }),
-  tokenValueInput: byRole('textbox', { name: 'onboarding.token.use_existing_token.label' }),
+  tokenValueInput: byRole('textbox', { name: /onboarding.token.use_existing_token.label/ }),
   invalidTokenValueMessage: byText('onboarding.token.invalid_format'),
   ...getTutorialActionButtons(),
   ...getTutorialBuildButtons(),
@@ -67,7 +67,7 @@ it('should generate/delete a new token or use existing one', async () => {
   expect(ui.runAnalysisTitle.get()).toBeInTheDocument();
 
   // Generating token
-  user.type(ui.tokenNameInput.get(), 'Testing token');
+  await user.type(ui.tokenNameInput.get(), 'Testing token');
   await selectEvent.select(ui.expiresInSelect.get(), 'users.tokens.expiration.365');
   await user.click(ui.generateTokenButton.get());
 
@@ -80,7 +80,7 @@ it('should generate/delete a new token or use existing one', async () => {
   await user.type(ui.tokenValueInput.get(), 'INVALID TOKEN VALUE');
   expect(ui.invalidTokenValueMessage.get()).toBeInTheDocument();
 
-  user.clear(ui.tokenValueInput.get());
+  await user.clear(ui.tokenValueInput.get());
   await user.type(ui.tokenValueInput.get(), 'validtokenvalue');
   expect(ui.continueButton.get()).toBeEnabled();
 
index 6dfdf6809b2908f6ccc9db04e397ea4c9e3c44f6..6667b3ba2bf79259de2efaa972ffdd37d03e4c0f 100644 (file)
@@ -34,7 +34,6 @@ export default function PageShortcutsTooltip(props: PageShortcutsTooltipProps) {
   const { className, leftAndRightLabel, leftLabel, upAndDownLabel, metaModifierLabel } = props;
   return (
     <Tooltip
-      accessible={false}
       overlay={
         <div className="small nowrap">
           <div>
index 1f4fc07da7022a2556ef319ac4c664fef38d2914..55e58f6a28ced09ba2c639a4421d95559c8bf6f4 100644 (file)
@@ -17,7 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { fireEvent, render, RenderResult } from '@testing-library/react';
+import { fireEvent, Matcher, render, RenderResult, screen, within } from '@testing-library/react';
 import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
 import { omit } from 'lodash';
 import * as React from 'react';
@@ -247,3 +247,9 @@ export function dateInputEvent(user: UserEvent) {
   };
 }
 /* eslint-enable testing-library/no-node-access */
+
+export function findTooltipWithContent(text: Matcher, target?: HTMLElement) {
+  return target
+    ? within(target).getByText(text, { selector: 'svg > desc' })
+    : screen.getByText(text, { selector: 'svg > desc' });
+}
index 62622ab265063299ef0b26052c4b3f155a7ddf3c..d7ca7d522f7ba04a583a6476a4e1486ef07f06ba 100644 (file)
@@ -312,6 +312,7 @@ since_previous_version_detailed=since previous version ({0} - {1})
 since_previous_version_with_only_date=since previous version ({0})
 since_previous_version_detailed.short=\u0394 version ({0})
 this_name_is_already_taken=This name is already taken.
+tooltip_is_interactive=This is a tooltip with interactive elements. Use the TAB key to cycle through the interactive elements.
 update_details=Update details
 work_duration.x_days={0}d
 work_duration.x_hours={0}h