]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22301 Fix accessibility issues on project overview page (#11729)
authorStanislav <31501873+stanislavhh@users.noreply.github.com>
Thu, 12 Sep 2024 08:38:19 +0000 (10:38 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 12 Sep 2024 20:02:54 +0000 (20:02 +0000)
server/sonar-web/design-system/src/components/icons/TrendIcon.tsx
server/sonar-web/design-system/src/theme/light.ts
server/sonar-web/src/main/js/apps/overview/branches/AnalysisVariations.tsx
server/sonar-web/src/main/js/apps/overview/branches/QualityGateCondition.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx
server/sonar-web/src/main/js/components/activity-graph/AddGraphMetric.tsx
server/sonar-web/src/main/js/components/activity-graph/GraphsHeader.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index cd93d9c6ff382183055245fc9cb5954d18d5cf92..331c0b513d96aa119870cce58a31af648721219e 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+import {
+  IconArrowDownRight,
+  IconArrowUpRight,
+  IconEqual,
+  IconProps,
+} from '@sonarsource/echoes-react';
 import { themeColor } from '../../helpers/theme';
-import { CustomIcon, IconProps } from './Icon';
+import { ThemeColors } from '../../types';
 
 export const enum TrendDirection {
   Down = 'down',
@@ -40,46 +46,40 @@ interface Props extends IconProps {
 }
 
 export function TrendIcon(props: Readonly<Props>) {
-  const theme = useTheme();
   const { direction, type, ...iconProps } = props;
 
-  const fill = themeColor(
-    (
-      {
-        [TrendType.Positive]: 'iconTrendPositive',
-        [TrendType.Negative]: 'iconTrendNegative',
-        [TrendType.Neutral]: 'iconTrendNeutral',
-        [TrendType.Disabled]: 'iconTrendDisabled',
-      } as const
-    )[type],
-  )({ theme });
+  if (direction === TrendDirection.Up) {
+    return (
+      <TrendIconWrapper trendType={type}>
+        <IconArrowUpRight aria-label="trend-up" {...iconProps} />
+      </TrendIconWrapper>
+    );
+  }
+
+  if (direction === TrendDirection.Down) {
+    return (
+      <TrendIconWrapper trendType={type}>
+        <IconArrowDownRight aria-label="trend-down" {...iconProps} />
+      </TrendIconWrapper>
+    );
+  }
 
   return (
-    <CustomIcon {...iconProps}>
-      {direction === TrendDirection.Up && (
-        <path
-          aria-label="trend-up"
-          clipRule="evenodd"
-          d="M4.75802 4.3611a.74997.74997 0 0 1 .74953-.74953H11.518a.74985.74985 0 0 1 .5298.21967.74967.74967 0 0 1 .2197.52986v6.0104a.75043.75043 0 0 1-.2286.5132.75053.75053 0 0 1-.5209.2104.75017.75017 0 0 1-.5209-.2104.75054.75054 0 0 1-.2287-.5132V6.1713l-5.26085 5.2609a.75014.75014 0 0 1-1.06066 0 .75004.75004 0 0 1 0-1.0607l5.26088-5.26086H5.50755a.75001.75001 0 0 1-.74953-.74954Z"
-          fill={fill}
-          fillRule="evenodd"
-        />
-      )}
-      {direction === TrendDirection.Down && (
-        <path
-          aria-label="trend-down"
-          clipRule="evenodd"
-          d="M11.5052 4.14237a.75026.75026 0 0 1 .5299.21967.74997.74997 0 0 1 .2196.52986v6.0104a.7501.7501 0 0 1-.2196.5299.75027.75027 0 0 1-.5299.2196H5.49479a.74976.74976 0 0 1-.51314-.2286.75004.75004 0 0 1 0-1.0418.74976.74976 0 0 1 .51314-.2286h4.20022L4.43413 4.8919a.75001.75001 0 0 1 1.06066-1.06066l5.26091 5.26087V4.8919a.74997.74997 0 0 1 .2196-.52986.75008.75008 0 0 1 .5299-.21967Z"
-          fill={fill}
-          fillRule="evenodd"
-        />
-      )}
-      {direction === TrendDirection.Equal && (
-        <g aria-label="trend-equal">
-          <rect fill={fill} height="1.5" rx=".75" width="8" x="4" y="5" />
-          <rect fill={fill} height="1.5" rx=".75" width="8" x="4" y="9" />
-        </g>
-      )}
-    </CustomIcon>
+    <TrendIconWrapper trendType={type}>
+      <IconEqual aria-label="trend-equal" {...iconProps} />
+    </TrendIconWrapper>
   );
 }
+
+const ICON_COLORS: Record<TrendType, ThemeColors> = {
+  [TrendType.Positive]: 'iconTrendPositive',
+  [TrendType.Negative]: 'iconTrendNegative',
+  [TrendType.Neutral]: 'iconTrendNeutral',
+  [TrendType.Disabled]: 'iconTrendDisabled',
+};
+
+const TrendIconWrapper = styled.span<{
+  trendType: TrendType;
+}>`
+  color: ${({ trendType }) => themeColor(ICON_COLORS[trendType])};
+`;
index ddc35bf7215550d73b98e18fbd4b1a5256c19dc5..ef98fce9ac929883c97b0c0dd681613b94b8dbf5 100644 (file)
@@ -420,7 +420,7 @@ export const lightTheme = {
     iconNegativeUpdate: COLORS.red[300],
     iconTrendPositive: COLORS.green[400],
     iconTrendNegative: COLORS.red[400],
-    iconTrendNeutral: COLORS.blue[400],
+    iconTrendNeutral: COLORS.blue[600],
     iconTrendDisabled: COLORS.blueGrey[400],
     iconError: danger.default,
     iconWarning: COLORS.yellow[600],
index afcd4b455a9053a46603418029a2e440d87621dd..9c05d456efe761379a39b264ebdfe6e0521b5bef 100644 (file)
@@ -67,15 +67,7 @@ function Variation(props: Readonly<VariationProps>) {
     trendIconDirection = variation > 0 ? TrendDirection.Up : TrendDirection.Down;
     trendIconType = variation > 0 === isGoodIfGrowing ? TrendType.Positive : TrendType.Negative;
   }
-  const variationIcon = (
-    <TrendIcon
-      className="sw-text-lg"
-      direction={trendIconDirection}
-      height={20}
-      type={trendIconType}
-      width={20}
-    />
-  );
+  const variationIcon = <TrendIcon direction={trendIconDirection} type={trendIconType} />;
 
   const variationToDisplay = formattedValue.startsWith('-') ? formattedValue : `+${formattedValue}`;
 
index 1f4bbdcaede3aec22c6b81bbce6d9a26b7b5515d..e5c87c3ab8e25106e8187fbd1ff8f8f43f613c83 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import styled from '@emotion/styled';
 import { LinkBox, TextMuted } from 'design-system';
 import * as React from 'react';
 import { Path } from 'react-router-dom';
@@ -107,7 +108,11 @@ export class QualityGateCondition extends React.PureComponent<Props> {
     };
 
     if (METRICS_TO_URL_MAPPING[metricKey]) {
-      return <LinkBox to={METRICS_TO_URL_MAPPING[metricKey]()}>{children}</LinkBox>;
+      return (
+        <LinkBox className="link-box-wrapper" to={METRICS_TO_URL_MAPPING[metricKey]()}>
+          {children}
+        </LinkBox>
+      );
     }
 
     const url = isIssueMeasure(condition.measure.metric.key)
@@ -122,7 +127,11 @@ export class QualityGateCondition extends React.PureComponent<Props> {
           listView: true,
         });
 
-    return <LinkBox to={url}>{children}</LinkBox>;
+    return (
+      <LinkBox className="link-box-wrapper" to={url}>
+        {children}
+      </LinkBox>
+    );
   }
 
   getPrimaryText = () => {
@@ -173,7 +182,7 @@ export class QualityGateCondition extends React.PureComponent<Props> {
               {this.getPrimaryText()}
             </span>
           </div>
-          <TextMuted text={`${operator} ${formatMeasure(threshold, metric.type)}`} />
+          <StyledMutedText text={`${operator} ${formatMeasure(threshold, metric.type)}`} />
         </div>
       </div>,
     );
@@ -181,3 +190,9 @@ export class QualityGateCondition extends React.PureComponent<Props> {
 }
 
 export default withMetricsContext(QualityGateCondition);
+
+const StyledMutedText = styled(TextMuted)`
+  .link-box-wrapper:hover & {
+    color: unset;
+  }
+`;
index 4cf9ca58475b1f61165ea4b131d1abde21bf6e32..1e29192a07947f6bf4c1d1771429a23798d73ab6 100644 (file)
@@ -111,7 +111,7 @@ describe('rendering', () => {
     renderProjectActivityAppContainer();
 
     await ui.appLoaded();
-    expect(ui.graphTypeIssues.get()).toBeInTheDocument();
+    expect(ui.graphTypeSelect.get()).toHaveValue('project_activity.graphs.issues');
   });
 
   it('should render new code legend for applications', async () => {
@@ -402,7 +402,7 @@ describe('data loading', () => {
     renderProjectActivityAppContainer();
     await ui.appLoaded();
 
-    expect(ui.graphTypeCustom.get()).toBeInTheDocument();
+    expect(ui.graphTypeSelect.get()).toHaveValue('project_activity.graphs.custom');
   });
 
   it('should correctly fetch the top level component when dealing with sub portfolios', async () => {
@@ -756,8 +756,6 @@ function getPageObject() {
   const ui = {
     // Graph types.
     graphTypeSelect: byLabelText('project_activity.graphs.choose_type'),
-    graphTypeIssues: byText('project_activity.graphs.issues'),
-    graphTypeCustom: byText('project_activity.graphs.custom'),
 
     // Graphs.
     graphs: byLabelText('project_activity.graphs.explanation_x', { exact: false }),
index b79add8c0f4506c2123145dc4ffd906b1ad7fa5e..1ef69f5ddf121520d8b53444f8aa602b37423948 100644 (file)
@@ -19,7 +19,7 @@
  */
 
 import { Button, IconChevronDown } from '@sonarsource/echoes-react';
-import { Dropdown, TextMuted } from 'design-system';
+import { Dropdown } from 'design-system';
 import { sortBy } from 'lodash';
 import * as React from 'react';
 import { MetricKey, MetricType } from '~sonar-aligned/types/metrics';
@@ -165,10 +165,9 @@ export default class AddGraphMetric extends React.PureComponent<Props, State> {
         }
       >
         <Button suffix={<IconChevronDown />}>
-          <TextMuted
-            className="sw-body-sm sw-flex"
-            text={translate('project_activity.graphs.custom.add')}
-          />
+          <span className="sw-body-sm sw-flex">
+            {translate('project_activity.graphs.custom.add')}
+          </span>
         </Button>
       </Dropdown>
     );
index 8390f89174f890f7c4bcbf4886db0a7fa22f4fd0..b50b4413050c55651b9a6eb058973b10d39753c9 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
-import {
-  Button,
-  ButtonGroup,
-  DropdownMenu,
-  DropdownMenuAlign,
-  IconChevronDown,
-} from '@sonarsource/echoes-react';
-import { TextMuted } from 'design-system';
+import { ButtonGroup, InputSize, Select } from '@sonarsource/echoes-react';
 import * as React from 'react';
 import { translate } from '../../helpers/l10n';
 import { GraphType } from '../../types/project-activity';
@@ -66,34 +59,24 @@ export default function GraphsHeader(props: Props) {
   const noCustomGraph =
     props.onAddCustomMetric === undefined || props.onRemoveCustomMetric === undefined;
 
-  const options = React.useMemo(() => {
-    const types = getGraphTypes(noCustomGraph);
-
-    return types.map((type) => {
-      const label = translate('project_activity.graphs', type);
-
-      return (
-        <DropdownMenu.ItemButton key={label} onClick={() => handleGraphChange(type)}>
-          {label}
-        </DropdownMenu.ItemButton>
-      );
-    });
-  }, [noCustomGraph, handleGraphChange]);
-
   return (
     <div className={className}>
       <ButtonGroup>
-        <DropdownMenu.Root align={DropdownMenuAlign.Start} id="activity-graph-type" items={options}>
-          <Button
-            aria-label={translate('project_activity.graphs.choose_type')}
-            suffix={<IconChevronDown />}
-          >
-            <TextMuted
-              className="sw-body-sm sw-flex"
-              text={translate('project_activity.graphs', graph)}
-            />
-          </Button>
-        </DropdownMenu.Root>
+        <label htmlFor="graph-type" className="sw-body-sm-highlight">
+          {translate('project_activity.graphs.choose_type')}
+        </label>
+        <Select
+          id="graph-type"
+          hasDropdownAutoWidth
+          onChange={handleGraphChange}
+          isNotClearable
+          value={graph}
+          size={InputSize.Small}
+          data={getGraphTypes(noCustomGraph).map((type) => ({
+            value: type,
+            label: translate('project_activity.graphs', type),
+          }))}
+        />
 
         {isCustomGraph(graph) &&
           props.onAddCustomMetric !== undefined &&
index 8c12f3c2dd3cc7423715a7b957bbf521f34d77ad..d9d3219c06f420475d1944b473c43b3b6968ab4a 100644 (file)
@@ -2033,7 +2033,7 @@ project_activity.events.tooltip.delete=Delete this event
 project_activity.new_code_period_start=Everything above this line is New Code
 project_activity.new_code_period_start.help=The analysis below this mark is the baseline for New Code comparison
 
-project_activity.graphs.choose_type=Choose graph type
+project_activity.graphs.choose_type=Graph type
 project_activity.graphs.explanation_x=This interactive graph shows data for the following project measures over time: {0}
 project_activity.graphs.issues=Issues
 project_activity.graphs.coverage=Coverage