department-of-veterans-affairs/vets-website

View on GitHub
src/platform/utilities/ui/scroll.js

Summary

Maintainability
A
40 mins
Test Coverage
import Scroll from 'react-scroll';

import { focusElement } from './focus';
import { ERROR_ELEMENTS } from '../constants';

const { scroller } = Scroll;

// Allows smooth scrolling to be overridden by our E2E tests
export function getScrollOptions(additionalOptions) {
  const globals = window.Forms || {};
  const defaults = {
    duration: 500,
    delay: 0,
    smooth: true,
  };
  const reducedMotion = window?.matchMedia('(prefers-reduced-motion: reduce)')
    ?.matches;
  const motionPreference = reducedMotion
    ? {
        duration: 0,
        delay: 0,
        smooth: false,
      }
    : {};

  return {
    ...defaults,
    ...globals.scroll,
    ...additionalOptions,
    ...motionPreference,
  };
}

export function scrollTo(elem, options = getScrollOptions()) {
  scroller.scrollTo(elem, options);
}

export function scrollToTop(position = 0, options = getScrollOptions()) {
  scroller.scrollTo(position, options);
}

/**
 * scrollToFirstError options
 * @typedef scrollToFirstErrorOptions
 * @type {Object}
 * @property {Boolean} focusOnAlertRole=false - When a web component is targetted, find
 *  and focus on the element with role=alert to ensure screen readers are
 *  reading out the correct content; it's set default to false while we perform
 *  further accessibility evaluation
 */
/**
 * Find first error and scroll it to the top of the page, then focus on it
 * @param {scrollToFirstErrorOptions} options
 */
export function scrollToFirstError({ focusOnAlertRole = false } = {}) {
  setTimeout(() => {
    // [error] will focus any web-components with an error message
    const errorEl = document.querySelector(ERROR_ELEMENTS.join(','));
    if (errorEl) {
      // document.body.scrollTop doesn’t work with all browsers, so we’ll cover them all like so:
      const currentPosition =
        window.pageYOffset ||
        document.documentElement.scrollTop ||
        document.body.scrollTop ||
        0;
      const position = errorEl.getBoundingClientRect().top + currentPosition;
      // Don't animate the scrolling if there is an open modal on the page. This
      // prevents the page behind the modal from scrolling if there is an error in
      // modal's form.

      // We have to search the shadow root of web components that have a slotted va-modal
      const isShadowRootModalOpen = Array.from(
        document.querySelectorAll('va-omb-info'),
      ).some(ombInfo =>
        ombInfo.shadowRoot?.querySelector(
          'va-modal[visible]:not([visible="false"])',
        ),
      );

      const isModalOpen =
        document.body.classList.contains('modal-open') ||
        document.querySelector('va-modal[visible]:not([visible="false"])') ||
        isShadowRootModalOpen;

      if (!isModalOpen) {
        Scroll.animateScroll.scrollTo(position - 10, getScrollOptions());

        if (focusOnAlertRole && errorEl.tagName.startsWith('VA-')) {
          focusElement('[role="alert"]', {}, errorEl.shadowRoot);
        } else {
          focusElement(errorEl);
        }
      }
    } else {
      // eslint-disable-next-line no-console
      console.error('scrollToFirstError: No error found');
    }
  });
}

export function scrollAndFocus(errorEl) {
  if (errorEl) {
    const currentPosition =
      window.pageYOffset ||
      document.documentElement.scrollTop ||
      document.body.scrollTop ||
      0;
    const position = errorEl.getBoundingClientRect().top + currentPosition;
    Scroll.animateScroll.scrollTo(position - 10, getScrollOptions());
    focusElement(errorEl);
  }
}