Skip to the content

Capsid menu

components/chips/chips.react.tsx

65 lines and 12 functions in DrDustinEdwards/capsomer, at commit d3966cc on 2026-10-24. This file on GitHub.

Functions

The functions of chips.react.tsx
FunctionLinesComplexityCoveredCRAPMutation
FilterChips24 to 692No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:31>31 to 311No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:32>32 to 321No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
set33 to 331No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:33>33 to 331No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:33>33 to 331No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
toggle34 to 392No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
clear40 to 441No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:51>51 to 554No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:52>52 to 522No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:56>56 to 601No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:57>57 to 571No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet

Imports

It imports no other module of this repo. No module of this repo imports it.

Source

import { useId, useRef, type ReactNode } from "react";

export interface ChipOption<V extends string> {
  value: V;
  label: ReactNode;
  disabled?: boolean;
}

export interface FilterChipsProps<V extends string> {
  // The visible label of the group: "Status".
  label: ReactNode;
  // Up to six.
  options: Array<ChipOption<V>>;
  // Controlled: the pressed values, in any order.
  value: V[];
  // Called with the pressed values in the options' order. Keep them in the address
  // (writeParam from "capsomer/behaviour/chips" makes ?status=blocked,running).
  onChange: (value: V[]) => void;
  // What the filter leaves, in words: "3 of 14 jobs". Announced politely as it changes.
  count: ReactNode;
  size?: "sm" | "default" | "lg";
}

export function FilterChips<V extends string>({ label, options, value, onChange, count, size = "default" }: FilterChipsProps<V>) {
  const labelId = useId();
  const first = useRef<HTMLButtonElement>(null);
  if (options.length > 6) console.warn("FilterChips: chips hold at most six values; use a select or a combobox for more.");
  const pressed = new Set(value);
  // A pressed value that no option carries (from a shared address) still shows, pressed,
  // after the options, so a filter is never active without a chip to say so and release it.
  const known = new Set(options.map((o) => o.value));
  const extra = value.filter((v) => !known.has(v));
  const set = (next: Set<V>) => onChange([...options.map((o) => o.value), ...extra].filter((v) => next.has(v)));
  const toggle = (v: V) => {
    const next = new Set(pressed);
    if (next.has(v)) next.delete(v);
    else next.add(v);
    set(next);
  };
  const clear = () => {
    set(new Set());
    // Clear hides itself; focus goes to the first chip rather than being lost.
    first.current?.focus();
  };

  return (
    <div className="cap-chips" role="group" aria-labelledby={labelId}>
      <span className="cap-chips-label" id={labelId}>
        {label}
      </span>
      {options.map((o, i) => (
        <button key={o.value} ref={i === 0 ? first : undefined} type="button" className="cap-chip" data-size={size === "default" ? undefined : size} aria-pressed={pressed.has(o.value)} aria-disabled={o.disabled || undefined} data-value={o.value} onClick={() => (o.disabled ? undefined : toggle(o.value))}>
          {o.label}
        </button>
      ))}
      {extra.map((v) => (
        <button key={v} type="button" className="cap-chip" aria-pressed="true" data-value={v} onClick={() => toggle(v)}>
          {v}
        </button>
      ))}
      <button type="button" className="cap-link-btn" data-cap-part="clear" hidden={pressed.size === 0} onClick={clear}>
        Clear
      </button>
      <span className="cap-chips-count" role="status">
        {count}
      </span>
    </div>
  );
}