department-of-veterans-affairs/vets-website

View on GitHub
src/applications/_mock-form-ae-design-patterns/vadx/hooks/useFocusedElement.js

Summary

Maintainability
A
3 hrs
Test Coverage
import React, { useState, useEffect, useRef } from 'react';

/**
 * Creates a friendly, human-readable string for a DOM element
 * combines tag name, ID, classes, and truncated text content.
 *
 * @param {HTMLElement} element - The DOM element to create a selector for
 * @param {number} [truncateInnerTextLength] - The maximum length of the inner text to display default 30
 * @returns {string} A concatenated string of element identifiers
 *
 * @example
 * // Returns "button.primary"Hello World""
 * getFriendlySelector(document.querySelector('button.primary'))
 */
const getFriendlySelector = (element, truncateInnerTextLength = 30) => {
  const parts = [];

  if (element.tagName) {
    parts.push(element.tagName.toLowerCase());
  }

  if (element.id) {
    parts.push(`#${element.id}`);
  }
  if (element.className && typeof element.className === 'string') {
    // for multiple classes, join them with dots
    parts.push(`.${element.className.split(' ').join('.')}`);
  }

  // Add truncated text content if present
  if (element.textContent) {
    const trimmedText = element.textContent.trim();
    if (trimmedText.length > truncateInnerTextLength) {
      parts.push(`"${trimmedText.slice(0, truncateInnerTextLength)}..."`);
    } else {
      parts.push(`"${trimmedText}"`);
    }
  }

  return parts.join('');
};

/**
 * React hook that tracks the currently focused element in the DOM
 * and can help to highlight focused element on hover.
 *
 * @returns {Object} An object containing:
 *   - displayString: A user friendly selector string for the focused element
 *   - onMouseEnter: Function to handle mouse enter events and highlight the element
 *   - onMouseLeave: Function to handle mouse leave events and remove highlighting
 *
 * @example
 * function MyComponent() {
 *   const { displayString, onMouseEnter, onMouseLeave } = useFocusedElement();
 *   return (
 *     <div onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
 *       Currently focused: {displayString}
 *     </div>
 *   );
 * }
 */
export function useFocusedElement() {
  const [focusedElement, setFocusedElement] = useState({
    element: null,
    displayString: '',
  });

  // Storing cleanup func as a ref to avoid unnecessary re-renders
  const cleanupRef = useRef(null);

  const cleanupHighlight = () => {
    if (cleanupRef.current) {
      // Cleans up by restoring original styles
      // if a cleanup function is stored in the ref
      cleanupRef.current();
      cleanupRef.current = null;
    }
  };

  useEffect(() => {
    const handleFocus = () => {
      const element = document.activeElement;
      if (element && element !== document.body) {
        // cleanup needed to remove previous highlights in case
        // there was an edge case where the highlight was not removed
        cleanupHighlight();
        setFocusedElement({
          element,
          displayString: getFriendlySelector(element),
        });
      }
    };

    const handleBlur = () => {
      cleanupHighlight();
      setFocusedElement({
        element: null,
        displayString: '',
      });
    };

    document.addEventListener('focusin', handleFocus);
    document.addEventListener('focusout', handleBlur);

    // Cleanup function to remove event listeners and highlights
    return () => {
      cleanupHighlight();
      document.removeEventListener('focusin', handleFocus);
      document.removeEventListener('focusout', handleBlur);
    };
  }, []);

  return {
    displayString: focusedElement.displayString,

    onMouseEnter: () => {
      if (focusedElement.element) {
        cleanupHighlight();

        const { element } = focusedElement;
        // Store original styles
        const previousBackground = element.style.backgroundColor;
        const previousOutline = element.style.outline;

        // Apply highlight styles
        // TODO: make these colors more subtle or align with VADS better
        element.style.backgroundColor = 'rgba(255, 255, 0, 0.3)';
        element.style.outline = '2px solid #007AFF';

        // Store cleanup function in ref to restore original styles
        cleanupRef.current = () => {
          element.style.backgroundColor = previousBackground;
          element.style.outline = previousOutline;
        };
      }
    },

    onMouseLeave: () => {
      cleanupHighlight();
    },
  };
}

/**
 * Example component demonstrating usage of the useFocusedElement hook
 * Displays the currently focused element and highlights it on hover
 */
export const FocusDebugger = () => {
  const { displayString, onMouseEnter, onMouseLeave } = useFocusedElement();

  return (
    <div onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
      {displayString || 'No element focused'}
    </div>
  );
};