All files / react/src/router link.tsx

100% Statements 49/49
100% Branches 33/33
100% Functions 2/2
100% Lines 49/49

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 771x 1x 1x   1x           1x 12x         12x 12x 12x 12x 12x 12x   12x         12x 1x 1x   3x 12x   12x   6x 3x 3x   3x   3x 3x 3x   6x 6x   12x 2x   2x 2x 2x   2x 2x   12x 12x 12x 12x 12x 12x 12x 12x 12x   12x 12x   12x 12x 1x  
import { derived } from '@anchorlib/core';
import { createUrl } from '@anchorlib/router';
import { navigate } from './navigate.js';
import type { MouseEventHandler, ReactNode } from 'react';
import { render, setup } from '../hoc.js';
import type { ComponentProps } from '../types.js';
import type { AnyRoute, LinkProps } from './types.js';
 
type LinkComponent = <T extends AnyRoute>(props: LinkProps<T>) => ReactNode;
 
export const Link = setup<LinkProps<AnyRoute>>((props) => {
  const $props = props as ComponentProps<LinkProps<AnyRoute>> & {
    query: Record<string, unknown>;
    params: Record<string, unknown>;
  };
 
  const query = derived(() => $props.query);
  const params = derived(() => $props.params);
  const href = derived(() => createUrl(props.href ?? $props.to?.index.path ?? '/', params.value, query.value));
  const isActive = derived(() => {
    const route = $props.to?.index;
    if (!route) return false;
 
    if (route.active) return true;
 
    // If this route is an Index child route, its native .active state drops when navigating 
    // into deep sibling dynamic routes (like /users/1). 
    // Visually, the NavLink should still be active if its true parent is active.
    if (route.parent && route.parent.index === route) {
      return !!route.parent.active;
    }
 
    return false;
  });
 
  const handleClick: MouseEventHandler<HTMLAnchorElement> = (e) => {
    // Let the browser handle standard "open in new tab/window" modifiers and custom targets.
    if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0 || $props.target) {
      return;
    }
 
    e.preventDefault();
 
    if (!location.href.endsWith(href.value)) {
      navigate(href.value, { query: query.value, params: params.value, replace: props.replace });
    }
 
    $props.onClick?.(e);
  };
 
  const handleHover: MouseEventHandler<HTMLAnchorElement> = (e) => {
    const { to } = $props;
 
    if (to && (props.preload === 'hover' || to.index.options.preloadMode === 'hover')) {
      to.index.router.preload(href.value);
    }
 
    $props.onMouseEnter?.(e);
  };
 
  return render(
    () => (
      <a
        href={href.value}
        onClick={handleClick}
        onMouseEnter={handleHover}
        aria-current={isActive.value ? 'page' : undefined}
        className={[props.className, isActive.value ? props.activeClass : ''].filter(Boolean).join(' ') || undefined}
        {...$props.$omit(['to', 'params' as never, 'query' as never, 'onClick', 'onMouseEnter', 'preload', 'replace', 'activeClass', 'className', 'children'])}
      >
        {props.children}
      </a>
    ),
    'Link'
  );
}, 'Link') as LinkComponent;