department-of-veterans-affairs/vets-website

View on GitHub
src/applications/mhv-secure-messaging/components/shared/AlertBackgroundBox.jsx

Summary

Maintainability
C
7 hrs
Test Coverage
/** TODO
 * This component requires a further decision on how to handle the alertList.
 * Currently, we are displaying the most recent alert marked as active.
 * We may want to display all active alerts, or only the most recent alert.
 * Multiple alerts are not currently supported.
 * They can be displayed as a list in one alert box, or as multiple alert boxes.
 */

/**
 * Added accessibility fix to ensure that the alert content and the current location are
 * announced to the user in a way that's accessible to screen readers.
 * This component uses @prop lastPathName to check if url location is on
 * the secure messages landing page so that if there's a service outage, a unique server
 * error message from api response content will be displayed only for that page.
 * Additionally, A11Y reccommends that the 503 error alert content should use an h1 tag
 * since in this case there are no other content on screen.
 */

import React, { useEffect, useState, useRef, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import PropTypes from 'prop-types';
import { VaAlert } from '@department-of-veterans-affairs/component-library/dist/react-bindings';
import { focusElement } from 'platform/utilities/ui';
import useInterval from '../../hooks/use-interval';
import { Alerts, Errors } from '../../util/constants';
import { closeAlert, focusOutAlert } from '../../actions/alerts';
import { retrieveFolder } from '../../actions/folders';
import { formatPathName } from '../../util/helpers';

const AlertBackgroundBox = props => {
  const dispatch = useDispatch();
  const alertList = useSelector(state => state.sm.alerts?.alertList);
  const folder = useSelector(state => state.sm.folders?.folder);
  const [alertContent, setAlertContent] = useState('');
  const alertRef = useRef();
  const [activeAlert, setActiveAlert] = useState(null);

  const {
    Message: { SERVER_ERROR_503 },
  } = Alerts;

  const {
    Code: { SERVICE_OUTAGE },
  } = Errors;

  const location = useLocation();
  const SrOnlyTag = alertContent === SERVER_ERROR_503 ? 'h1' : 'span';

  useEffect(
    () => {
      if (alertList?.length) {
        const filteredSortedAlerts = alertList
          .filter(alert => alert?.isActive)
          .sort((a, b) => {
            // Sort chronologically descending.
            return b.datestamp - a.datestamp;
          });
        // The activeAlert is the most recent alert marked as active.
        setActiveAlert(filteredSortedAlerts[0] || null);
      }
    },
    [alertList],
  );

  const handleShowIcon = () => {
    if (props.noIcon) {
      return 'false';
    }
    return 'true';
  };
  const closeAlertBox = () => {
    dispatch(closeAlert());
    dispatch(focusOutAlert());
  };

  const lastPathName = formatPathName(location.pathname, 'Messages');

  // these props check if the current page is the folder view page or thread view page
  const foldersViewPage = /folders\/\d+/.test(location.pathname);
  const threadViewPage = /thread\/\d+/.test(location.pathname);
  const contactListPage = /contact-list/.test(location.pathname);

  // sets custom server error messages for the landing page and folder view pages
  useEffect(
    () => {
      const isServiceOutage = activeAlert?.response?.code === SERVICE_OUTAGE;
      const isErrorAlert = activeAlert?.alertType === 'error';
      let content = activeAlert?.content;

      if (
        lastPathName !== 'Messages' &&
        !foldersViewPage &&
        !threadViewPage &&
        !contactListPage &&
        (isServiceOutage || isErrorAlert)
      ) {
        content = SERVER_ERROR_503;
      }
      setAlertContent(content);
    },
    [
      SERVER_ERROR_503,
      SERVICE_OUTAGE,
      activeAlert,
      foldersViewPage,
      lastPathName,
      location.pathname,
      threadViewPage,
    ],
  );

  useInterval(() => {
    const shouldRetrieveFolders =
      activeAlert?.response?.code === SERVICE_OUTAGE ||
      folder?.folderId === undefined;

    if (shouldRetrieveFolders) {
      dispatch(retrieveFolder(folder?.folderId));
      dispatch(closeAlert());
    }
  }, 60000); // 1 minute

  const handleAlertFocus = useCallback(
    () => {
      setTimeout(() => {
        focusElement(
          props.focus
            ? alertRef.current.shadowRoot.querySelector('button')
            : alertRef.current,
        );
      }, 500);
    },
    [props.focus],
  );

  const alertAriaLabel = `You are in ${(lastPathName === 'Folders' &&
    'My Folders') ||
    (foldersViewPage && 'a custom folder view page') ||
    lastPathName}.`;

  return (
    <>
      {activeAlert && (
        <VaAlert
          uswds
          ref={alertRef}
          background-only
          closeable={props.closeable}
          className="vads-u-margin-bottom--1 va-alert"
          close-btn-aria-label={`${alertContent}. ${alertAriaLabel} Close notification.`}
          disable-analytics="false"
          full-width="false"
          show-icon={handleShowIcon()}
          status={activeAlert.alertType}
          onCloseEvent={
            closeAlertBox // success, error, warning, info, continue
          }
          onVa-component-did-load={handleAlertFocus}
        >
          <div>
            <p className="vads-u-margin-y--0" data-testid="alert-text">
              {alertContent}
              <SrOnlyTag
                className="sr-only"
                aria-live="polite"
                aria-atomic="true"
              >
                {alertAriaLabel}
              </SrOnlyTag>
            </p>
          </div>
        </VaAlert>
      )}
    </>
  );
};

AlertBackgroundBox.propTypes = {
  activeAlert: PropTypes.object,
  closeable: PropTypes.bool,
  focus: PropTypes.bool,
  noIcon: PropTypes.bool,
};

export default AlertBackgroundBox;