]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20487 Fix dropdown accessibility and drop isPortal usage
author7PH <benjamin.raymond@sonarsource.com>
Mon, 18 Sep 2023 12:18:07 +0000 (14:18 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 19 Sep 2023 20:02:46 +0000 (20:02 +0000)
server/sonar-web/design-system/src/components/Dropdown.tsx
server/sonar-web/design-system/src/components/__tests__/Dropdown-test.tsx
server/sonar-web/design-system/src/components/popups.tsx
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotOpenInIdeButton.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSidebarHeader.tsx

index 81012702d5570a6dd2a42b00778713b6d26353aa..6c03ec66f85bc9f0bb01fcfd7960d0d4a17aea30 100644 (file)
@@ -47,7 +47,6 @@ interface Props {
   className?: string;
   closeOnClick?: boolean;
   id: string;
-  isPortal?: boolean;
   onClose?: VoidFunction;
   onOpen?: VoidFunction;
   openDropdown?: boolean;
@@ -90,15 +89,7 @@ export class Dropdown extends React.PureComponent<Readonly<Props>, State> {
 
   render() {
     const { open } = this.state;
-    const {
-      allowResizing,
-      className,
-      closeOnClick = true,
-      id,
-      isPortal,
-      size = 'full',
-      zLevel,
-    } = this.props;
+    const { allowResizing, className, closeOnClick = true, id, size = 'full', zLevel } = this.props;
     const a11yAttrs: A11yAttrs = {
       'aria-controls': `${id}-dropdown`,
       'aria-expanded': open,
@@ -122,7 +113,6 @@ export class Dropdown extends React.PureComponent<Readonly<Props>, State> {
         aria-labelledby={`${id}-trigger`}
         className={className}
         id={`${id}-dropdown`}
-        isPortal={isPortal}
         onRequestClose={this.handleClose}
         open={open}
         overlay={
index da3060dbd72dca2bc2baee6e7aa5e06d807fbc04..34f43149e08d434c5a3a8945d593c100a1a3e8cf 100644 (file)
@@ -22,33 +22,6 @@ import { renderWithRouter } from '../../helpers/testUtils';
 import { ActionsDropdown, Dropdown } from '../Dropdown';
 import { ButtonSecondary } from '../buttons';
 
-describe('Dropdown with Portal Wrapper', () => {
-  it('renders', async () => {
-    const { user } = setupWithChildren();
-    expect(screen.getByRole('button')).toBeInTheDocument();
-
-    await user.click(screen.getByRole('button'));
-    expect(screen.getByRole('menu')).toBeInTheDocument();
-  });
-
-  it('toggles with render prop', async () => {
-    const { user } = setupWithChildren(({ onToggleClick }) => (
-      <ButtonSecondary onClick={onToggleClick} />
-    ));
-
-    await user.click(screen.getByRole('button'));
-    expect(screen.getByRole('menu')).toBeVisible();
-  });
-
-  function setupWithChildren(children?: Dropdown['props']['children']) {
-    return renderWithRouter(
-      <Dropdown id="test-menu" isPortal overlay={<div id="overlay" />}>
-        {children ?? <ButtonSecondary />}
-      </Dropdown>
-    );
-  }
-});
-
 describe('Dropdown', () => {
   it('renders', async () => {
     const { user } = setupWithChildren();
@@ -71,7 +44,7 @@ describe('Dropdown', () => {
     return renderWithRouter(
       <Dropdown id="test-menu" overlay={<div id="overlay" />}>
         {children ?? <ButtonSecondary />}
-      </Dropdown>
+      </Dropdown>,
     );
   }
 });
@@ -86,7 +59,7 @@ describe('ActionsDropdown', () => {
     return renderWithRouter(
       <ActionsDropdown id="test-menu">
         <div id="overlay" />
-      </ActionsDropdown>
+      </ActionsDropdown>,
     );
   }
 });
index d24f691777bf0d0c4f37801aad586cff4e47fae6..81fb65b9370bcee13052d49fb701ee36680824d9 100644 (file)
@@ -21,7 +21,7 @@ import styled from '@emotion/styled';
 import classNames from 'classnames';
 import { throttle } from 'lodash';
 import React, { AriaRole } from 'react';
-import { createPortal, findDOMNode } from 'react-dom';
+import { findDOMNode } from 'react-dom';
 import tw from 'twin.macro';
 import { THROTTLE_SCROLL_DELAY } from '../helpers/constants';
 import { PopupPlacement, PopupZLevel, popupPositioning } from '../helpers/positioning';
@@ -69,7 +69,6 @@ PopupWithRef.displayName = 'Popup';
 interface PopupProps extends Omit<PopupBaseProps, 'style'> {
   allowResizing?: boolean;
   children: React.ReactNode;
-  isPortal?: boolean;
   overlay: React.ReactNode;
 }
 
@@ -143,7 +142,7 @@ export class Popup extends React.PureComponent<PopupProps, State> {
         const { height, left, top, width } = popupPositioning(
           toggleNode,
           this.popupNode.current,
-          placement
+          placement,
         );
 
         // save width and height (and later set in `render`) to avoid resizing the popup element,
@@ -178,23 +177,11 @@ export class Popup extends React.PureComponent<PopupProps, State> {
     return (
       <>
         {this.props.children}
-        {this.props.overlay &&
-          (this.props.isPortal ? (
-            <PortalWrapper>
-              <PopupWithRef
-                placement={placement}
-                ref={this.popupNode}
-                style={style}
-                {...popupProps}
-              >
-                {overlay}
-              </PopupWithRef>
-            </PortalWrapper>
-          ) : (
-            <PopupWithRef placement={placement} ref={this.popupNode} style={style} {...popupProps}>
-              {overlay}
-            </PopupWithRef>
-          ))}
+        {this.props.overlay && (
+          <PopupWithRef placement={placement} ref={this.popupNode} style={style} {...popupProps}>
+            {overlay}
+          </PopupWithRef>
+        )}
       </>
     );
   }
@@ -217,7 +204,7 @@ export const PopupWrapper = styled.div<{ zLevel: PopupZLevel }>`
       [PopupZLevel.Global]: tw`sw-z-global-popup`,
       [PopupZLevel.Content]: tw`sw-z-content-popup`,
       [PopupZLevel.Absolute]: tw`sw-z-global-popup`,
-    }[zLevel])};
+    })[zLevel]};
 
   &.is-bottom,
   &.is-bottom-left,
@@ -243,24 +230,3 @@ export const PopupWrapper = styled.div<{ zLevel: PopupZLevel }>`
     ${tw`sw-ml-2`};
   }
 `;
-
-class PortalWrapper extends React.Component {
-  el: HTMLElement;
-
-  constructor(props: object) {
-    super(props);
-    this.el = document.createElement('div');
-  }
-
-  componentDidMount() {
-    document.body.appendChild(this.el);
-  }
-
-  componentWillUnmount() {
-    document.body.removeChild(this.el);
-  }
-
-  render() {
-    return createPortal(this.props.children, this.el);
-  }
-}
index cdcc9aa82d56f2022f3782c7900c36b6888421ef..1d2173c202c2f964cb6d25186d147a711b5a2ab5 100644 (file)
@@ -25,6 +25,7 @@ import {
   ItemDownload,
   ItemLink,
   PopupPlacement,
+  PopupZLevel,
   Tooltip,
 } from 'design-system';
 import { some } from 'lodash';
@@ -218,7 +219,7 @@ class ProfileActions extends React.PureComponent<Props, State> {
             profile.name,
             profile.languageName,
           )}
-          isPortal
+          zLevel={PopupZLevel.Global}
         >
           {actions.edit && (
             <ItemLink className="it__quality-profiles__activate-more-rules" to={activateMoreUrl}>
index 5229e07481ef55cc3131416c92df67360791ba31..d3b015d518645506f1a1befc71d61c83333fb650 100644 (file)
@@ -23,6 +23,7 @@ import {
   DropdownToggler,
   ItemButton,
   PopupPlacement,
+  PopupZLevel,
 } from 'design-system';
 import * as React from 'react';
 import Spinner from '../../../components/ui/Spinner';
@@ -100,7 +101,7 @@ export default class HotspotOpenInIdeButton extends React.PureComponent<Props, S
           allowResizing
           open={ides.length > 1}
           placement={PopupPlacement.BottomLeft}
-          isPortal
+          zLevel={PopupZLevel.Global}
           overlay={
             <DropdownMenu size="auto">
               {ides.map((ide) => {
index c1751a3a336d74d7c9de310ef8c049628918b01e..c79d37008544b209e3c1348e40d77dad9b7a609d 100644 (file)
@@ -28,6 +28,7 @@ import {
   ItemDangerButton,
   ItemDivider,
   ItemHeader,
+  PopupZLevel,
 } from 'design-system';
 import * as React from 'react';
 import withComponentContext from '../../../app/components/componentContext/withComponentContext';
@@ -107,7 +108,7 @@ function HotspotSidebarHeader(props: SecurityHotspotsAppRendererProps) {
             allowResizing
             closeOnClick={false}
             id="filter-hotspots-menu"
-            isPortal
+            zLevel={PopupZLevel.Global}
             overlay={
               <>
                 <ItemHeader>{translate('hotspot.filters.title')}</ItemHeader>