]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22309 Fix a11y issues on Activity page master
authorstanislavh <stanislav.honcharov@sonarsource.com>
Fri, 22 Nov 2024 10:12:52 +0000 (11:12 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 22 Nov 2024 20:03:10 +0000 (20:03 +0000)
server/sonar-web/src/main/js/apps/projectActivity/components/Event.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityDateInput.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFilters.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx
server/sonar-web/src/main/js/components/activity-graph/EventInner.tsx
server/sonar-web/src/main/js/components/activity-graph/GraphHistory.tsx
server/sonar-web/src/main/js/components/activity-graph/GraphsHistory.tsx
server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx
server/sonar-web/src/main/js/design-system/components/input/DatePickerCustomCalendarNavigation.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 06d19395a9b90bc6c89f9113d972cdfcf53b624a..124763d4a57937f120870b8df9d0811b18ed2f77 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
+import {
+  ButtonIcon,
+  ButtonSize,
+  ButtonVariety,
+  IconDelete,
+  IconEdit,
+} from '@sonarsource/echoes-react';
 import * as React from 'react';
-import { DestructiveIcon, InteractiveIcon, PencilIcon, TrashIcon } from '~design-system';
+import { useIntl } from 'react-intl';
 import EventInner from '../../../components/activity-graph/EventInner';
-import Tooltip from '../../../components/controls/Tooltip';
 import { translate } from '../../../helpers/l10n';
 import { AnalysisEvent, ProjectAnalysisEventCategory } from '../../../types/project-activity';
 import ChangeEventForm from './forms/ChangeEventForm';
@@ -36,6 +42,7 @@ export interface EventProps {
 
 function Event(props: Readonly<EventProps>) {
   const { analysisKey, event, canAdmin, isFirst } = props;
+  const intl = useIntl();
 
   const [changing, setChanging] = React.useState(false);
   const [deleting, setDeleting] = React.useState(false);
@@ -46,6 +53,15 @@ function Event(props: Readonly<EventProps>) {
   const canDelete = isOther || (isVersion && !isFirst);
   const showActions = canAdmin && (canChange || canDelete);
 
+  const editEventLabel = intl.formatMessage(
+    { id: 'project_activity.events.tooltip.edit' },
+    { event: `${event.category} ${event.name}` },
+  );
+  const deleteEventLabel = intl.formatMessage(
+    { id: 'project_activity.events.tooltip.delete' },
+    { event: `${event.category} ${event.name}` },
+  );
+
   return (
     <div className="it__project-activity-event sw-flex sw-justify-between">
       <EventInner event={event} />
@@ -53,28 +69,28 @@ function Event(props: Readonly<EventProps>) {
       {showActions && (
         <div className="sw-grow-0 sw-shrink-0 sw-ml-2">
           {canChange && (
-            <Tooltip content={translate('project_activity.events.tooltip.edit')}>
-              <InteractiveIcon
-                Icon={PencilIcon}
-                aria-label={translate('project_activity.events.tooltip.edit')}
-                data-test="project-activity__edit-event"
-                onClick={() => setChanging(true)}
-                stopPropagation
-                size="small"
-              />
-            </Tooltip>
+            <ButtonIcon
+              Icon={IconEdit}
+              className="-sw-mt-1"
+              variety={ButtonVariety.PrimaryGhost}
+              size={ButtonSize.Medium}
+              tooltipContent={editEventLabel}
+              ariaLabel={editEventLabel}
+              data-test="project-activity__edit-event"
+              onClick={() => setChanging(true)}
+            />
           )}
           {canDelete && (
-            <Tooltip content={translate('project_activity.events.tooltip.delete')}>
-              <DestructiveIcon
-                Icon={TrashIcon}
-                aria-label={translate('project_activity.events.tooltip.delete')}
-                data-test="project-activity__delete-event"
-                onClick={() => setDeleting(true)}
-                stopPropagation
-                size="small"
-              />
-            </Tooltip>
+            <ButtonIcon
+              className="-sw-mt-1"
+              Icon={IconDelete}
+              size={ButtonSize.Medium}
+              variety={ButtonVariety.DangerGhost}
+              tooltipContent={deleteEventLabel}
+              ariaLabel={deleteEventLabel}
+              data-test="project-activity__delete-event"
+              onClick={() => setDeleting(true)}
+            />
           )}
         </div>
       )}
index c55aec00f8ac5f40b0b0338e8fe03391a9c7f20d..e155e6d07ae60ba8e3349cd2c6d9f9dc2c185a9a 100644 (file)
  */
 
 import styled from '@emotion/styled';
+import { Spinner, Text } from '@sonarsource/echoes-react';
 import classNames from 'classnames';
 import { isEqual } from 'date-fns';
 import * as React from 'react';
-import { Badge, HelperHintIcon, LightLabel, Spinner, themeColor } from '~design-system';
+import { Badge, HelperHintIcon, themeColor } from '~design-system';
 import Tooltip from '../../../components/controls/Tooltip';
 import DateFormatter from '../../../components/intl/DateFormatter';
 import { toShortISO8601String } from '../../../helpers/dates';
@@ -120,106 +121,107 @@ export default class ProjectActivityAnalysesList extends React.PureComponent<Pro
   }
 
   render() {
-    const byVersionByDay = getAnalysesByVersionByDay(this.props.analyses, this.props.query);
+    const { analyses, query, initializing } = this.props;
+    const byVersionByDay = getAnalysesByVersionByDay(analyses, query);
     const newCodePeriod = this.getNewCodePeriodStartKey(byVersionByDay);
     const hasFilteredData =
       byVersionByDay.length > 1 ||
       (byVersionByDay.length === 1 && Object.keys(byVersionByDay[0].byDay).length > 0);
-    if (this.props.analyses.length === 0 || !hasFilteredData) {
-      return (
-        <div>
-          {this.props.initializing ? (
-            <div className="sw-p-4 sw-typo-default">
-              <Spinner />
-            </div>
-          ) : (
-            <div className="sw-p-4 sw-typo-default">
-              <LightLabel>{translate('no_results')}</LightLabel>
-            </div>
-          )}
-        </div>
-      );
-    }
+    const hasData = analyses.length > 0 && hasFilteredData;
 
     return (
-      <ul
-        className="it__project-activity-versions-list sw-box-border sw-overflow-auto sw-grow sw-shrink-0 sw-py-0 sw-px-4"
-        ref={(element) => (this.scrollContainer = element)}
-        style={{
-          height: 'calc(100vh - 250px)',
-          marginTop:
-            this.props.project.qualifier === ComponentQualifier.Project
-              ? LIST_MARGIN_TOP
-              : undefined,
-        }}
-      >
-        {newCodePeriod.baselineAnalysisKey !== undefined &&
-          newCodePeriod.firstNewCodeAnalysisKey === undefined && (
-            <BaselineMarker className="sw-typo-default sw-mb-2">
-              <span className="sw-py-1/2 sw-px-1">
-                {translate('project_activity.new_code_period_start')}
-              </span>
-              <Tooltip
-                content={translate('project_activity.new_code_period_start.help')}
-                side="top"
-              >
-                <HelperHintIcon className="sw-ml-1" />
-              </Tooltip>
-            </BaselineMarker>
-          )}
-
-        {byVersionByDay.map((version, idx) => {
-          const days = Object.keys(version.byDay);
-          if (days.length <= 0) {
-            return null;
-          }
+      <>
+        <div aria-live="polite">
+          <Spinner isLoading={initializing}>
+            {!hasData && (
+              <div className="sw-p-4 sw-typo-default">
+                <Text isSubdued>{translate('no_results')}</Text>
+              </div>
+            )}
+          </Spinner>
+        </div>
 
-          return (
-            <li key={version.key || 'noversion'}>
-              {version.version && (
-                <VersionTagStyled
-                  className={classNames(
-                    'sw-sticky sw-top-0 sw-left-0 sw-pb-1 -sw-ml-4 sw-z-normal',
-                    {
-                      'sw-top-0 sw-pt-0': idx === 0,
-                    },
-                  )}
-                >
+        {hasData && (
+          <ul
+            className="it__project-activity-versions-list sw-box-border sw-overflow-auto sw-grow sw-shrink-0 sw-py-0 sw-px-4"
+            ref={(element) => (this.scrollContainer = element)}
+            style={{
+              height: 'calc(100vh - 250px)',
+              marginTop:
+                this.props.project.qualifier === ComponentQualifier.Project
+                  ? LIST_MARGIN_TOP
+                  : undefined,
+            }}
+          >
+            {newCodePeriod.baselineAnalysisKey !== undefined &&
+              newCodePeriod.firstNewCodeAnalysisKey === undefined && (
+                <BaselineMarker className="sw-typo-default sw-mb-2">
+                  <span className="sw-py-1/2 sw-px-1">
+                    {translate('project_activity.new_code_period_start')}
+                  </span>
                   <Tooltip
-                    mouseEnterDelay={0.5}
-                    content={`${translate('version')} ${version.version}`}
+                    content={translate('project_activity.new_code_period_start.help')}
+                    side="top"
                   >
-                    <Badge className="sw-p-1">{version.version}</Badge>
+                    <HelperHintIcon className="sw-ml-1" />
                   </Tooltip>
-                </VersionTagStyled>
+                </BaselineMarker>
               )}
-              <ul className="it__project-activity-days-list">
-                {days.map((day) => (
-                  <li
-                    className="it__project-activity-day sw-mt-1 sw-mb-4"
-                    data-day={toShortISO8601String(Number(day))}
-                    key={day}
-                  >
-                    <div className="sw-typo-lg-semibold sw-mb-3">
-                      <DateFormatter date={Number(day)} long />
-                    </div>
-                    <ul className="it__project-activity-analyses-list">
-                      {version.byDay[day]?.map((analysis) =>
-                        this.renderAnalysis(analysis, newCodePeriod.firstNewCodeAnalysisKey),
+
+            {byVersionByDay.map((version, idx) => {
+              const days = Object.keys(version.byDay);
+              if (days.length <= 0) {
+                return null;
+              }
+
+              return (
+                <li key={version.key || 'noversion'}>
+                  {version.version && (
+                    <VersionTagStyled
+                      className={classNames(
+                        'sw-sticky sw-top-0 sw-left-0 sw-pb-1 -sw-ml-4 sw-z-normal',
+                        {
+                          'sw-top-0 sw-pt-0': idx === 0,
+                        },
                       )}
-                    </ul>
-                  </li>
-                ))}
-              </ul>
-            </li>
-          );
-        })}
-        {this.props.analysesLoading && (
-          <li className="sw-text-center">
-            <Spinner />
-          </li>
+                    >
+                      <Tooltip
+                        mouseEnterDelay={0.5}
+                        content={`${translate('version')} ${version.version}`}
+                      >
+                        <Badge className="sw-p-1">{version.version}</Badge>
+                      </Tooltip>
+                    </VersionTagStyled>
+                  )}
+                  <ul className="it__project-activity-days-list">
+                    {days.map((day) => (
+                      <li
+                        className="it__project-activity-day sw-mt-1 sw-mb-4"
+                        data-day={toShortISO8601String(Number(day))}
+                        key={day}
+                      >
+                        <div className="sw-typo-lg-semibold sw-mb-3">
+                          <DateFormatter date={Number(day)} long />
+                        </div>
+                        <ul className="it__project-activity-analyses-list">
+                          {version.byDay[day]?.map((analysis) =>
+                            this.renderAnalysis(analysis, newCodePeriod.firstNewCodeAnalysisKey),
+                          )}
+                        </ul>
+                      </li>
+                    ))}
+                  </ul>
+                </li>
+              );
+            })}
+            {this.props.analysesLoading && (
+              <li className="sw-text-center">
+                <Spinner />
+              </li>
+            )}
+          </ul>
         )}
-      </ul>
+      </>
     );
   }
 }
index 51b7db8fee5c7264235539978a693a4c11ff0d9e..bef9e20b434ba2f26330774184e77883e6cb8010 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
-import { Button } from '@sonarsource/echoes-react';
+import { Button, ButtonGroup, Label } from '@sonarsource/echoes-react';
 import * as React from 'react';
 import { DateRangePicker, PopupZLevel } from '~design-system';
 import { translate } from '../../../helpers/l10n';
@@ -41,7 +41,8 @@ export default class ProjectActivityDateInput extends React.PureComponent<Props>
 
   render() {
     return (
-      <div className="sw-flex">
+      <ButtonGroup>
+        <Label htmlFor="date-from">{translate('project_activity.filter_date_range')}</Label>
         <DateRangePicker
           className="sw-w-abs-350"
           startClearButtonLabel={translate('clear.start')}
@@ -60,7 +61,7 @@ export default class ProjectActivityDateInput extends React.PureComponent<Props>
         >
           {translate('project_activity.reset_dates')}
         </Button>
-      </div>
+      </ButtonGroup>
     );
   }
 }
index 0ee0d13708c367be4d617de9b85002e7c9ef96c8..08a344243ab95503f7f6b6472e8c509215de5d6b 100644 (file)
@@ -18,8 +18,9 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
+import { ButtonGroup, InputSize, Label, Select } from '@sonarsource/echoes-react';
 import * as React from 'react';
-import { InputSelect, LabelValueSelectOption } from '~design-system';
+import { LabelValueSelectOption } from '~design-system';
 import { isPortfolioLike } from '~sonar-aligned/helpers/component';
 import { ComponentQualifier } from '~sonar-aligned/types/component';
 import { translate } from '../../../helpers/l10n';
@@ -52,8 +53,8 @@ export default function ProjectActivityPageFilters(props: ProjectActivityPageFil
   }));
 
   const handleCategoryChange = React.useCallback(
-    (option: { value: string } | null) => {
-      updateQuery({ category: option ? option.value : '' });
+    (option: string | null) => {
+      updateQuery({ category: option ?? '' });
     },
     [updateQuery],
   );
@@ -61,19 +62,19 @@ export default function ProjectActivityPageFilters(props: ProjectActivityPageFil
   return (
     <div className="sw-flex sw-mb-5 sw-items-center">
       {!isPortfolioLike(project.qualifier) && (
-        <InputSelect
-          aria-label={translate('project_activity.filter_events')}
-          className="sw-mr-8 sw-typo-default sw-w-abs-200"
-          isClearable
-          onChange={(data: LabelValueSelectOption) => handleCategoryChange(data)}
-          options={options}
-          placeholder={translate('project_activity.filter_events')}
-          size="full"
-          value={options.find((o) => o.value === category)}
-          classNames={{
-            menu: () => 'sw-z-dropdown-menu-page',
-          }}
-        />
+        <ButtonGroup>
+          <Label htmlFor="graph-type">{translate('project_activity.filter_events')}</Label>
+          <Select
+            id="events-filter"
+            className="sw-mr-8 sw-typo-default sw-w-abs-200"
+            hasDropdownAutoWidth
+            placeholder={translate('project_activity.filter_events.placeholder')}
+            onChange={(value) => handleCategoryChange(value)}
+            value={options.find((o) => o.value === category)?.value}
+            size={InputSize.Small}
+            data={options}
+          />
+        </ButtonGroup>
       )}
       <ProjectActivityDateInput from={from} onChange={props.updateQuery} to={to} />
     </div>
index 380572a181c919ecb6ec77aae7314f25136afd26..9be3a7fffe4ea06bb6e8b054db01572a969176d5 100644 (file)
@@ -325,10 +325,10 @@ describe('CRUD', () => {
     await ui.addVersionEvent('1.1.0.1', initialValue);
     expect(screen.getAllByText(initialValue).length).toBeGreaterThan(0);
 
-    await ui.updateEvent(1, updatedValue);
+    await ui.updateEvent(`VERSION ${initialValue}`, updatedValue);
     expect(screen.getAllByText(updatedValue).length).toBeGreaterThan(0);
 
-    await ui.deleteEvent(0);
+    await ui.deleteEvent(`VERSION ${updatedValue}`);
     expect(screen.queryByText(updatedValue)).not.toBeInTheDocument();
   });
 
@@ -351,10 +351,10 @@ describe('CRUD', () => {
     await ui.addCustomEvent('1.1.0.1', initialValue);
     expect(screen.getAllByText(initialValue).length).toBeGreaterThan(0);
 
-    await ui.updateEvent(1, updatedValue);
+    await ui.updateEvent(`OTHER ${initialValue}`, updatedValue);
     expect(screen.getAllByText(updatedValue).length).toBeGreaterThan(0);
 
-    await ui.deleteEvent(0);
+    await ui.deleteEvent(`OTHER ${updatedValue}`);
     expect(screen.queryByText(updatedValue)).not.toBeInTheDocument();
   });
 
@@ -799,8 +799,10 @@ function getPageObject() {
     addCustomEventBtn: byRole('menuitem', { name: 'project_activity.add_custom_event' }),
     addVersionEvenBtn: byRole('menuitem', { name: 'project_activity.add_version' }),
     deleteAnalysisBtn: byRole('menuitem', { name: 'project_activity.delete_analysis' }),
-    editEventBtn: byRole('button', { name: 'project_activity.events.tooltip.edit' }),
-    deleteEventBtn: byRole('button', { name: 'project_activity.events.tooltip.delete' }),
+    editEventBtn: (event: string) =>
+      byRole('button', { name: `project_activity.events.tooltip.edit.${event}` }),
+    deleteEventBtn: (event: string) =>
+      byRole('button', { name: `project_activity.events.tooltip.delete.${event}` }),
 
     // Event modal.
     nameInput: byLabelText('name'),
@@ -876,15 +878,15 @@ function getPageObject() {
         await user.click(ui.saveBtn.get());
       },
 
-      async updateEvent(index: number, value: string) {
-        await user.click(ui.editEventBtn.getAll()[index]);
+      async updateEvent(event: string, value: string) {
+        await user.click(ui.editEventBtn(event).get());
         await user.clear(ui.nameInput.get());
         await user.type(ui.nameInput.get(), value);
         await user.click(ui.changeBtn.get());
       },
 
-      async deleteEvent(index: number) {
-        await user.click(ui.deleteEventBtn.getAll()[index]);
+      async deleteEvent(event: string) {
+        await user.click(ui.deleteEventBtn(event).get());
         await user.click(ui.deleteBtn.get());
       },
 
index 050fac489ad6fa464f8ac95437c51b712fc04804..6fa0899a2a4140b01a2117a892f2ccece5d8b6ba 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
+import { Tooltip } from '@sonarsource/echoes-react';
 import * as React from 'react';
 import { Note } from '~design-system';
 import { ComponentContext } from '../../app/components/componentContext/ComponentContext';
@@ -25,7 +26,6 @@ import { translate } from '../../helpers/l10n';
 import { useCurrentBranchQuery } from '../../queries/branch';
 import { StaleTime } from '../../queries/common';
 import { AnalysisEvent, ProjectAnalysisEventCategory } from '../../types/project-activity';
-import Tooltip from '../controls/Tooltip';
 import { DefinitionChangeEventInner, isDefinitionChangeEvent } from './DefinitionChangeEventInner';
 import { RichQualityGateEventInner, isRichQualityGateEvent } from './RichQualityGateEventInner';
 import {
index b638899883351d53b1f1ebb0f820e2f984d4ba9b..d287575cefc7f240adab1f3c9affb4a009087ed9 100644 (file)
 
 import styled from '@emotion/styled';
 import * as React from 'react';
+import { useIntl } from 'react-intl';
 import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer';
-import { ButtonSecondary } from '~design-system';
 import { formatMeasure } from '~sonar-aligned/helpers/measures';
 import { AdvancedTimeline } from '../../components/charts/AdvancedTimeline';
-import { translate } from '../../helpers/l10n';
+import { KeyboardKeys } from '../../helpers/keycodes';
 import { getShortType } from '../../helpers/measures';
 import { MeasureHistory, ParsedAnalysis, Serie } from '../../types/project-activity';
-import ModalButton from '../controls/ModalButton';
 import DataTableModal from './DataTableModal';
 import GraphsLegendCustom from './GraphsLegendCustom';
 import GraphsLegendStatic from './GraphsLegendStatic';
@@ -70,8 +69,10 @@ export default function GraphHistory(props: Readonly<Props>) {
     showAreas,
     graphDescription,
   } = props;
+  const intl = useIntl();
   const [tooltipIdx, setTooltipIdx] = React.useState<number | undefined>(undefined);
   const [tooltipXPos, setTooltipXPos] = React.useState<number | undefined>(undefined);
+  const [tableIsVisible, setTableIsVisible] = React.useState(false);
 
   const formatValue = (tick: string | number) => {
     return formatMeasure(tick, getShortType(metricsType));
@@ -87,84 +88,88 @@ export default function GraphHistory(props: Readonly<Props>) {
     setTooltipXPos(tooltipXPos);
   };
 
-  const modalProp = ({ onClose }: { onClose: () => void }) => (
-    <DataTableModal
-      analyses={analyses}
-      graphEndDate={graphEndDate}
-      graphStartDate={graphStartDate}
-      series={series}
-      onClose={onClose}
-    />
-  );
-
   const events = getAnalysisEventsForDate(analyses, selectedDate);
 
   return (
-    <StyledGraphContainer className="sw-flex sw-flex-col sw-justify-center sw-items-stretch sw-grow sw-py-2">
-      {isCustom && props.removeCustomMetric ? (
-        <GraphsLegendCustom
-          leakPeriodDate={leakPeriodDate}
-          removeMetric={props.removeCustomMetric}
-          series={series}
-        />
-      ) : (
-        <GraphsLegendStatic leakPeriodDate={leakPeriodDate} series={series} />
-      )}
+    <>
+      <StyledGraphContainer
+        tabIndex={canShowDataAsTable ? 0 : -1}
+        aria-label={`${intl.formatMessage(
+          { id: 'project_activity.graphs.graph_shown_x' },
+          { '0': isCustom ? series.map((s) => s.translatedName).join(',') : graph },
+        )} ${intl.formatMessage({ id: 'project_activity.graphs.open_in_table' })}`}
+        onKeyUp={(event) => {
+          if (event.key === KeyboardKeys.Enter) {
+            setTableIsVisible(true);
+          }
+        }}
+        className="sw-flex sw-flex-col sw-justify-center sw-items-stretch sw-grow sw-py-2"
+      >
+        {isCustom && props.removeCustomMetric ? (
+          <GraphsLegendCustom
+            leakPeriodDate={leakPeriodDate}
+            removeMetric={props.removeCustomMetric}
+            series={series}
+          />
+        ) : (
+          <GraphsLegendStatic leakPeriodDate={leakPeriodDate} series={series} />
+        )}
 
-      <div className="sw-flex-1">
-        <AutoSizer>
-          {({ height, width }) => (
-            <div>
-              <AdvancedTimeline
-                endDate={graphEndDate}
-                formatYTick={formatValue}
-                height={height}
-                leakPeriodDate={leakPeriodDate}
-                splitPointDate={measuresHistory.find((m) => m.splitPointDate)?.splitPointDate}
-                metricType={metricsType}
-                selectedDate={selectedDate}
-                series={series}
-                showAreas={showAreas}
-                startDate={graphStartDate}
-                graphDescription={graphDescription}
-                updateSelectedDate={props.updateSelectedDate}
-                updateTooltip={updateTooltip}
-                updateZoom={props.updateGraphZoom}
-                width={width}
-              />
+        <div className="sw-flex-1">
+          <AutoSizer>
+            {({ height, width }) => (
+              <div>
+                <AdvancedTimeline
+                  endDate={graphEndDate}
+                  formatYTick={formatValue}
+                  height={height}
+                  leakPeriodDate={leakPeriodDate}
+                  splitPointDate={measuresHistory.find((m) => m.splitPointDate)?.splitPointDate}
+                  metricType={metricsType}
+                  selectedDate={selectedDate}
+                  series={series}
+                  showAreas={showAreas}
+                  startDate={graphStartDate}
+                  graphDescription={graphDescription}
+                  updateSelectedDate={props.updateSelectedDate}
+                  updateTooltip={updateTooltip}
+                  updateZoom={props.updateGraphZoom}
+                  width={width}
+                />
 
-              {selectedDate !== undefined &&
-                tooltipIdx !== undefined &&
-                tooltipXPos !== undefined && (
-                  <GraphsTooltips
-                    events={events}
-                    formatValue={formatTooltipValue}
-                    graph={graph}
-                    graphWidth={width}
-                    measuresHistory={measuresHistory}
-                    selectedDate={selectedDate}
-                    series={series}
-                    tooltipIdx={tooltipIdx}
-                    tooltipPos={tooltipXPos}
-                  />
-                )}
-            </div>
-          )}
-        </AutoSizer>
-      </div>
-      {canShowDataAsTable && (
-        <ModalButton modal={modalProp}>
-          {({ onClick }) => (
-            <ButtonSecondary className="sw-sr-only" onClick={onClick}>
-              {translate('project_activity.graphs.open_in_table')}
-            </ButtonSecondary>
-          )}
-        </ModalButton>
+                {selectedDate !== undefined &&
+                  tooltipIdx !== undefined &&
+                  tooltipXPos !== undefined && (
+                    <GraphsTooltips
+                      events={events}
+                      formatValue={formatTooltipValue}
+                      graph={graph}
+                      graphWidth={width}
+                      measuresHistory={measuresHistory}
+                      selectedDate={selectedDate}
+                      series={series}
+                      tooltipIdx={tooltipIdx}
+                      tooltipPos={tooltipXPos}
+                    />
+                  )}
+              </div>
+            )}
+          </AutoSizer>
+        </div>
+      </StyledGraphContainer>
+      {tableIsVisible && (
+        <DataTableModal
+          analyses={analyses}
+          graphEndDate={graphEndDate}
+          graphStartDate={graphStartDate}
+          series={series}
+          onClose={() => setTableIsVisible(false)}
+        />
       )}
-    </StyledGraphContainer>
+    </>
   );
 }
 
-const StyledGraphContainer = styled.div`
+const StyledGraphContainer = styled.section`
   height: 300px;
 `;
index d390e385f354faa42a42030a92e0dd4cdfe053b1..bbf04b4602581dbab96e5616c63a1837c612d467 100644 (file)
@@ -69,65 +69,60 @@ export default class GraphsHistory extends React.PureComponent<Props, State> {
   render() {
     const { analyses, graph, loading, series, ariaLabel, canShowDataAsTable } = this.props;
     const isCustom = isCustomGraph(graph);
+    const showAreas = [GraphType.coverage, GraphType.duplications].includes(graph);
 
-    if (loading) {
-      return (
-        <div className="sw-flex sw-justify-center sw-flex-col sw-items-stretch sw-grow">
-          <div className="sw-text-center">
-            <Spinner isLoading={loading} />
-          </div>
-        </div>
-      );
-    }
-
-    if (!hasHistoryData(series)) {
-      return (
-        <div className="sw-flex sw-items-center sw-justify-center sw-h-full">
-          <Text isSubdued>
-            {translate(
-              isCustom
-                ? 'project_activity.graphs.custom.no_history'
-                : 'component_measures.no_history',
+    return (
+      <div className="sw-flex sw-justify-center sw-flex-col sw-items-stretch sw-text-center sw-grow">
+        <div aria-live="polite" aria-busy={loading}>
+          <Spinner isLoading={loading}>
+            {!hasHistoryData(series) && (
+              <Text isSubdued className="sw-max-w-full">
+                {translate(
+                  isCustom
+                    ? 'project_activity.graphs.custom.no_history'
+                    : 'component_measures.no_history',
+                )}
+              </Text>
             )}
-          </Text>
+          </Spinner>
         </div>
-      );
-    }
-    const showAreas = [GraphType.coverage, GraphType.duplications].includes(graph);
-    return (
-      <div className="sw-flex sw-justify-center sw-flex-col sw-items-stretch sw-grow">
-        {this.props.graphs.map((graphSeries, idx) => {
-          return (
-            <GraphHistory
-              analyses={analyses}
-              canShowDataAsTable={canShowDataAsTable}
-              graph={graph}
-              graphEndDate={this.props.graphEndDate}
-              graphStartDate={this.props.graphStartDate}
-              isCustom={isCustom}
-              key={idx}
-              leakPeriodDate={this.props.leakPeriodDate}
-              measuresHistory={this.props.measuresHistory}
-              metricsType={getSeriesMetricType(graphSeries)}
-              removeCustomMetric={this.props.removeCustomMetric}
-              selectedDate={this.state.selectedDate}
-              series={graphSeries}
-              graphDescription={
-                ariaLabel ??
-                translateWithParameters(
-                  'project_activity.graphs.explanation_x',
-                  uniqBy(graphSeries, 'name')
-                    .map(({ translatedName }) => translatedName)
-                    .join(', '),
-                )
-              }
-              showAreas={showAreas}
-              updateGraphZoom={this.props.updateGraphZoom}
-              updateSelectedDate={this.props.updateSelectedDate}
-              updateTooltip={this.updateTooltip}
-            />
-          );
-        })}
+
+        {hasHistoryData(series) && !loading && (
+          <>
+            {this.props.graphs.map((graphSeries, idx) => {
+              return (
+                <GraphHistory
+                  analyses={analyses}
+                  canShowDataAsTable={canShowDataAsTable}
+                  graph={graph}
+                  graphEndDate={this.props.graphEndDate}
+                  graphStartDate={this.props.graphStartDate}
+                  isCustom={isCustom}
+                  key={idx}
+                  leakPeriodDate={this.props.leakPeriodDate}
+                  measuresHistory={this.props.measuresHistory}
+                  metricsType={getSeriesMetricType(graphSeries)}
+                  removeCustomMetric={this.props.removeCustomMetric}
+                  selectedDate={this.state.selectedDate}
+                  series={graphSeries}
+                  graphDescription={
+                    ariaLabel ??
+                    translateWithParameters(
+                      'project_activity.graphs.explanation_x',
+                      uniqBy(graphSeries, 'name')
+                        .map(({ translatedName }) => translatedName)
+                        .join(', '),
+                    )
+                  }
+                  showAreas={showAreas}
+                  updateGraphZoom={this.props.updateGraphZoom}
+                  updateSelectedDate={this.props.updateSelectedDate}
+                  updateTooltip={this.updateTooltip}
+                />
+              );
+            })}
+          </>
+        )}
       </div>
     );
   }
index 5e76f3e45c88c6a77fb4233af613286a1c0fcd38..058b8eb3ca91b7fc60c3adfbd56a5d0dc67d33d5 100644 (file)
@@ -101,7 +101,7 @@ describe('data table modal', () => {
     await ui.closeDataTable();
     await ui.changeGraphType(GraphType.coverage);
 
-    await ui.openDataTable();
+    await ui.openDataTable(true);
     expect(ui.dataTable.get()).toBeInTheDocument();
     expect(ui.dataTableColHeaders.getAll()).toHaveLength(4);
     expect(ui.dataTableRows.getAll()).toHaveLength(HISTORY_COUNT + 1);
@@ -229,7 +229,7 @@ function getPageObject() {
     noDataText: byText('project_activity.graphs.custom.no_history'),
 
     // Data in table.
-    openInTableBtn: byRole('button', { name: 'project_activity.graphs.open_in_table' }),
+    openInTableRegion: byRole('region', { name: /project_activity.graphs.open_in_table/ }),
     closeDataTableBtn: byRole('button', { name: 'close' }),
     dataTable: byRole('table'),
     dataTableRows: byRole('row'),
@@ -260,8 +260,15 @@ function getPageObject() {
       async removeMetric(metric: MetricKey) {
         await user.click(ui.legendRemoveMetricBtn(metric).get());
       },
-      async openDataTable() {
-        await user.click(ui.openInTableBtn.get());
+      async openDataTable(tabFromGraphSelection = false) {
+        // tab to graph selection and close popup
+        if (!tabFromGraphSelection) {
+          await user.tab();
+          await user.keyboard('{escape}');
+        }
+        // tab to graph region and open data table
+        await user.tab();
+        await user.keyboard('{enter}');
       },
       async closeDataTable() {
         await user.click(ui.closeDataTableBtn.get());
index 8020048a2bbc03aa1b7ae51ae723d5085a84093d..1ac08f07ceb61b0d67832b68d8e6889adea8e549 100644 (file)
@@ -52,9 +52,8 @@ export function CustomCalendarNavigation(props: CaptionProps) {
     if (date === undefined) {
       return intl.formatMessage({ id: 'disabled_' });
     }
-    return `${intl.formatDate(date, { month: 'long', format: 'M' })} ${intl.formatDate(date, {
+    return `${intl.formatDate(date, { month: 'long' })} ${intl.formatDate(date, {
       year: 'numeric',
-      format: 'y',
     })}`;
   };
 
index b802dd322a37bcc9c512d06debc2c48fc1e6b413..cd2165b253d3dae09bb23c9455c65c8100d87bdd 100644 (file)
@@ -2157,9 +2157,11 @@ project_activity.remove_custom_event.question=Are you sure you want to delete th
 project_activity.reset_dates=Reset dates
 project_activity.delete_analysis=Delete Analysis
 project_activity.delete_analysis.question=Are you sure you want to delete this analysis from the history?
-project_activity.filter_events=Filter events
-project_activity.events.tooltip.edit=Edit this event
-project_activity.events.tooltip.delete=Delete this event
+project_activity.filter_events=Events filter
+project_activity.filter_events.placeholder=Select an event
+project_activity.filter_date_range=Date range filter
+project_activity.events.tooltip.edit=Edit this event: {event}
+project_activity.events.tooltip.delete=Delete this event: {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
 
@@ -2178,7 +2180,8 @@ project_activity.graphs.custom.no_history=There isn't enough data to generate an
 project_activity.graphs.custom.metric_no_history=This metric has no historical data to display.
 project_activity.graphs.custom.search=Search for a metric by name
 project_activity.graphs.custom.type_x_message=Only "{0}" metrics are available with your current selection.
-project_activity.graphs.open_in_table=Show the graph data in a table
+project_activity.graphs.graph_shown_x=Graph shown for {0}.
+project_activity.graphs.open_in_table=Press 'Enter' to show the graph data in a table.
 project_activity.graphs.data_table.title=Graph data in table format
 project_activity.graphs.data_table.max_lines_warning=Only the {0} most recent data entries are shown. If you want to see different data, change the date filters on the main page.
 project_activity.graphs.data_table.no_data_warning=There is no data for the selected series.