Ctrl K
ComunidadGuías…

src/components/dives/autofilled-marks.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"use client";

import { createContext, useContext, type ReactNode } from "react";
import {
  useFormContext,
  useWatch,
  type Control,
  type FieldValues,
} from "react-hook-form";
import { FormLabelMarkContext } from "@/components/ui/form";
import { Attribution } from "@/components/attribution";
import { FETCHED_MARK_TITLE, type FetchedMarks } from "@/lib/dive-conditions";
import { isNonEmptyFieldValue } from "@/lib/dive-form-fields";

interface AutofilledMarksValue {
  control: unknown;
  isAutofilled: (name: string) => boolean;
  fetched?: FetchedMarks;
}

const AutofilledMarksContext = createContext<AutofilledMarksValue | null>(null);

const DOT = "ml-1.5 inline-block size-1.5 rounded-full bg-teal align-middle";

function AutofilledDot() {
  return <span aria-hidden title="Filled in for you" className={DOT} />;
}

/**
 * The mark on a dive's field that a weather service filled in: the form's dot,
 * titled for what filled it. Unlike the form's own, it is read aloud, since on the
 * dive page no legend explains it.
 */
export function FetchedMark() {
  return (
    <span title={FETCHED_MARK_TITLE} className={DOT}>
      <span className="sr-only">(filled in from weather data)</span>
    </span>
  );
}

// Whether a field still stands as the fetch wrote it once saved: an emptied field
// does, since the API keeps a fill it is sent a blank for.
const keepsFetched = (value: unknown, written: unknown) =>
  value === written || !isNonEmptyFieldValue(value);

// Watches the one field it marks, so the dot goes the moment the diver picks
// another value - which is what saving it would do to the stored mark.
function FetchedFormMark({
  name,
  marks,
  written,
}: {
  name: string;
  marks: AutofilledMarksValue;
  written: unknown;
}) {
  const value = useWatch({ control: marks.control as Control, name });
  if (keepsFetched(value, written)) return <FetchedMark />;
  return marks.isAutofilled(name) ? <AutofilledDot /> : null;
}

function AutofilledMark({ name }: { name: string }) {
  const marks = useContext(AutofilledMarksContext);
  const { control } = useFormContext();
  // A dialog opened from inside the dive form - a picker's "Add new..." - has a form
  // and labels of its own, and its fields' names can collide with the dive's.
  if (!marks || control !== marks.control) return null;
  if (marks.fetched && name in marks.fetched.values) {
    return (
      <FetchedFormMark
        name={name}
        marks={marks}
        written={marks.fetched.values[name]}
      />
    );
  }
  return marks.isAutofilled(name) ? <AutofilledDot /> : null;
}

/**
 * What the marks mean, said once above the form while any field carries one, so
 * no finger has to hover a dot to find out. Hidden from a screen reader along
 * with the dots it explains.
 */
export function AutofilledLegend() {
  return (
    <p
      aria-hidden
      className="flex items-center gap-1.5 px-(--card-pad) text-sm text-muted-foreground"
    >
      <span className="inline-block size-1.5 rounded-full bg-teal" />
      Filled in for you
    </p>
  );
}

function FetchedSourceLine({
  control,
  fetched,
}: {
  control: Control;
  fetched: FetchedMarks;
}) {
  const names = Object.keys(fetched.values);
  const values = useWatch({ control, name: names }) as unknown[];
  if (
    !names.some((name, index) =>
      keepsFetched(values[index], fetched.values[name]),
    )
  ) {
    return null;
  }
  return (
    <p className="text-xs text-muted-foreground">
      <Attribution value={fetched.attribution} />
    </p>
  );
}

/**
 * The weather service's credit, as visible text under a form's fields while any of
 * them would still hold the value it filled in once saved (`keepsFetched`) - the
 * licence asks for it beside the data wherever the data is shown.
 */
export function FetchedSource<TFieldValues extends FieldValues>({
  control,
}: {
  control: Control<TFieldValues>;
}) {
  const marks = useContext(AutofilledMarksContext);
  if (!marks?.fetched || marks.control !== control) return null;
  if (Object.keys(marks.fetched.values).length === 0) return null;
  return (
    <FetchedSourceLine
      control={control as unknown as Control}
      fetched={marks.fetched}
    />
  );
}

const renderMark = (name: string) => <AutofilledMark name={name} />;

/**
 * Puts a teal dot after the label of every field in `control`'s form that
 * `isAutofilled` says the form changed on its own and the diver has not since,
 * and after every field that would still hold the value `fetched` says a weather
 * service wrote into the stored dive once saved (`keepsFetched`).
 */
export function AutofilledMarks<TFieldValues extends object>({
  control,
  isAutofilled,
  fetched,
  children,
}: {
  control: Control<TFieldValues>;
  isAutofilled: (name: string) => boolean;
  fetched?: FetchedMarks;
  children: ReactNode;
}) {
  return (
    <AutofilledMarksContext.Provider value={{ control, isAutofilled, fetched }}>
      <FormLabelMarkContext.Provider value={renderMark}>
        {children}
      </FormLabelMarkContext.Provider>
    </AutofilledMarksContext.Provider>
  );
}