Return

All tests / src/components/Dock/Folder index.tsx

92.3% Statements 36/39
66.66% Branches 6/9
90% Functions 9/10
94.11% Lines 32/34

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 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 974x 4x 4x 4x 4x   4x 4x                   4x                       4x 23x 23x 23x 23x 23x     23x 11x     23x 18x 4x 2x   4x 1x 1x   1x   4x 4x 4x 4x 4x                         5x             46x                                     13x  
import React from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useIntl } from 'react-intl';
import Icon from '@/components/Dock/Icon';
import type { Tool } from '@/constants/navMenu';
import { clx } from '@/helpers';
import styles from './folder.module.css';
 
type Props = {
    label: string;
    img: string;
    tools: Array<Tool>;
    /** The Dock closes its dialog when a link is followed; the folder does the same. */
    onNavigate: () => void;
};
 
const PANEL_ID = 'dock-tools-panel';
 
/**
 * A Dock folder: one slot that opens a small panel above the Dock listing the tools.
 *
 * It is not a route. `/next-leak` and `/next-coverage` stay the canonical entries, so the folder
 * must not take a URL of its own or appear in the sitemap.
 *
 * The panel is always in the DOM and only `hidden` when closed, so both anchors are in the server
 * HTML. That is how `/next-leak` is reachable from the home page today, and a panel that only
 * existed after hydration would take that away from the new page.
 */
const Folder = ({ label, img, tools, onNavigate }: Props) => {
    const { formatMessage: f } = useIntl();
    const { asPath } = useRouter();
    const [open, setOpen] = React.useState(false);
    const trigger = React.useRef<HTMLButtonElement>(null);
    const wrapper = React.useRef<HTMLDivElement>(null);
 
    // A route change means the panel did its job, or the user left by other means.
    React.useEffect(() => {
        setOpen(false);
    }, [asPath]);
 
    React.useEffect(() => {
        if (!open) return undefined;
        const onPointerDown = (event: MouseEvent) => {
            if (!wrapper.current?.contains(event.target as Node)) setOpen(false);
        };
        const onKeyDown = (event: KeyboardEvent) => {
            Iif (event.key !== 'Escape') return;
            setOpen(false);
            // Escape returns focus to the trigger; otherwise it lands on <body>.
            trigger.current?.focus();
        };
        document.addEventListener('mousedown', onPointerDown);
        document.addEventListener('keydown', onKeyDown);
        return () => {
            document.removeEventListener('mousedown', onPointerDown);
            document.removeEventListener('keydown', onKeyDown);
        };
    }, [open]);
 
    return (
        <div className={styles.folder} ref={wrapper}>
            <button
                ref={trigger}
                type="button"
                className={styles.trigger}
                aria-expanded={open}
                aria-controls={PANEL_ID}
                title={label}
                onClick={() => setOpen((was) => !was)}
            >
                <Icon src={img} alt="" />
                <span className={styles.label}>{label}</span>
            </button>
            <ul id={PANEL_ID} hidden={!open} className={clx(styles.panel, open ? styles.opened : '')}>
                {tools.map(({ labelId, link, testId }) => (
                    <li key={link} data-testid={`tools-${testId}`}>
                        <Link
                            href={link}
                            className={clx(styles.entry, asPath === link ? styles.current : '')}
                            aria-current={asPath === link ? 'page' : undefined}
                            onClick={() => {
                                setOpen(false);
                                onNavigate();
                            }}
                        >
                            {f({ id: labelId })}
                        </Link>
                    </li>
                ))}
            </ul>
        </div>
    );
};
 
export default Folder;