All files / src/components/Expandable Expandable.tsx

100% Statements 24/24
92.3% Branches 12/13
100% Functions 8/8
100% Lines 20/20

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78                        1x           71x   71x 71x   71x 71x   71x   71x 28x     71x 53x 28x     71x   71x 19x     71x 4x 4x 4x       71x                                                      
import React, { KeyboardEventHandler, MouseEventHandler, PropsWithChildren,  useEffect,  useId,  useRef,  useState } from 'react';
import styles from './Expandable.module.css';
import getString from '../../strings/getString';
 
// let expandableCounter = 0; // unique ID suffix for aria components
 
export type ExpandablePropsType = {
    startExpanded?: boolean;
    expandPrompt?: string;
    collapsePrompt?: string;
}
 
const Expandable: React.FC<PropsWithChildren<ExpandablePropsType>> = ({
    startExpanded = false,
    expandPrompt,
    collapsePrompt,
    children
}) => {
    const idDiscriminator = useId();
 
    const contentRef = useRef(null);
    const buttonRef = useRef(null);
 
    const effectiveExpandPrompt = expandPrompt || getString('expandable-component-default-expand-prompt');
    const effectiveCollapsePrompt = collapsePrompt || getString('expandable-component-default-collapse-prompt');
 
    const [isExpanded, setExpanded] = useState(startExpanded);
 
    useEffect(() => {
        setExpanded(() => startExpanded);
    }, [startExpanded])
 
    useEffect(() => {
        if(isExpanded) { contentRef.current.focus(); }
        else { buttonRef.current.focus(); }
    })
 
    const toggleExpanded = () => setExpanded(prevExpanded => !prevExpanded);
 
    const handleClick: MouseEventHandler = (e) => {
        toggleExpanded();
    }
 
    const handleKeyDown: KeyboardEventHandler = (e) => {
        Eif(['Enter', ' '].includes(e.key)) {
            e.preventDefault();
            toggleExpanded();
        }
    }
 
    return (
        <div className={styles.container}>
            <div
                ref={contentRef}
                tabIndex={isExpanded ? 0 : 1}
                data-testid={`expandable-section-${idDiscriminator}`}
                id={`expandable-section-${idDiscriminator}`}
                className={ styles.expandableBlock }
                hidden={!isExpanded}
            >
                {children}
            </div>
            <button
                ref={buttonRef}
                tabIndex={0}
                className={styles.toggle}
                onClick={handleClick}
                onKeyDown={handleKeyDown}
                aria-expanded={isExpanded}
                aria-controls={`expandable-section-toggle-${idDiscriminator}`}
            >
                { isExpanded ? effectiveCollapsePrompt : effectiveExpandPrompt }
            </button>
        </div>
    )
}
 
export default Expandable;