All files / virtual-keyboard/src/components/Trigger Trigger.tsx

100% Statements 3/3
100% Branches 0/0
100% Functions 1/1
100% Lines 3/3

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                                                                                    1x         27x                               1x  
import { forwardRef, type ReactNode } from "react";
import { clsx } from "clsx";
import styles from "./Trigger.module.scss";
 
export interface TriggerProps {
  /**
   * The visual affordance (an icon, text, or any **non-interactive** node — it is
   * wrapped in a `<button>`, so it must not itself be a button/link/input).
   */
  children: ReactNode;
  /** Current open state — reflected as `aria-expanded`. */
  open: boolean;
  /** The id of the keyboard element this trigger controls (`aria-controls`). */
  controls: string;
  /** Toggle the keyboard open/closed. */
  onToggle: () => void;
  /**
   * Explicit accessible name. Leave `undefined` when the trigger has visible text
   * (its content becomes the name); set it for an icon-only trigger.
   */
  label?: string;
  /** Extra class on the trigger button. */
  className?: string;
}
 
/**
 * The "keyboard icon" affordance. Wraps arbitrary **non-interactive** content in
 * a real `<button>` so it is keyboard-operable (Enter/Space), and wires the ARIA
 * relationship to the keyboard it toggles: `aria-expanded` reflects the open
 * state and `aria-controls` points at the keyboard's id.
 *
 * The accessible name comes from the visible content by default; pass `label`
 * only for an icon-only trigger (it becomes `aria-label`). It never forces an
 * `aria-label` over visible text — that would break voice control (WCAG 2.5.3).
 *
 * @example
 * ```tsx
 * <Trigger open={open} controls={id} onToggle={toggle} label="Open keyboard">
 *   <KeyboardIcon />
 * </Trigger>
 * ```
 */
export const Trigger = forwardRef<HTMLButtonElement, TriggerProps>(
  function Trigger(
    { children, open, controls, onToggle, label, className },
    ref
  ) {
    return (
      <button
        ref={ref}
        type="button"
        aria-expanded={open}
        aria-controls={controls}
        aria-label={label}
        onClick={onToggle}
        className={clsx(styles.trigger, className)}
      >
        {children}
      </button>
    );
  }
);
 
Trigger.displayName = "Trigger";