Skip to the content

Capsid menu

app/components/admin/media-grid.tsx

131 lines and 5 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.

Functions

The functions of media-grid.tsx
FunctionLinesComplexityCoveredCRAPMutation
MediaGrid14 to 1375No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:73>73 to 974No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:84>84 to 951No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:102>102 to 1326No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:118>118 to 1291No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code

Imports

It imports app/components/admin/media-bulk-bar.tsx, app/components/admin/media-keyboard.tsx, app/components/admin/media-list-header.tsx, app/components/admin/media-tile.tsx, app/lib/media/view.mjs. Imported by app/routes/admin.media._index.tsx.

Source

import { Form } from "react-router";

import { BulkBar } from "~/components/admin/media-bulk-bar";
import { useGridKeyboard } from "~/components/admin/media-keyboard";
import { MediaListHeader } from "~/components/admin/media-list-header";
import { MediaRow, MediaTile } from "~/components/admin/media-tile";
import { groupRows } from "~/lib/media/view.mjs";
import type { hrefWith, sortHref } from "~/lib/media/view.mjs";

import type { Route } from "../../routes/+types/admin.media._index";

type Listing = Extract<Route.ComponentProps["loaderData"], { detail: unknown }>;

export function MediaGrid({
  objects,
  chosen,
  selectRange,
  setSelected,
  setConfirmingTrash,
  linkTo,
  view,
  pending,
  scanComplete,
  tagCounts,
  tabStop,
  setActive,
}: {
  objects: Listing["objects"];
  chosen: string[];
  selectRange: (key: string, shift: boolean) => void;
  setSelected: (keys: string[]) => void;
  setConfirmingTrash: (value: boolean) => void;
  linkTo: (over?: Parameters<typeof hrefWith>[1]) => string;
  view: Parameters<typeof sortHref>[0];
  pending: boolean;
  scanComplete: boolean;
  tagCounts: Listing["tagCounts"];
  /** The one tile whose controls take Tab in the grid view. */
  tabStop: string;
  setActive: (key: string) => void;
}) {
  const keys = useGridKeyboard({ setActive, setSelected });
  const list = view.view === "list";
  const buckets = groupRows(objects, view.group);

  return (
    // Not role="grid": the column count follows the container width, so positions mean nothing to a screen reader.
    <Form method="post">
      {/* The form wraps the grid so the checkboxes submit with it; nested in the toolbar it would be
          a form inside a form, which the browser drops. */}
      {/* In the document before anything is selected, so the first count is announced too; the bar
          below mounts with its number. */}
      <p className="cap-sr-only" role="status">
        {chosen.length > 0 ? `${chosen.length} selected` : ""}
      </p>
      {chosen.length > 0 ? (
        <BulkBar
          objects={objects}
          chosen={chosen}
          setSelected={setSelected}
          setConfirmingTrash={setConfirmingTrash}
          tagCounts={tagCounts}
        />
      ) : null}

      {list ? (
        <div className="cap-table-wrap" role="region" tabIndex={0} aria-label="Files">
          <table className="cap-table" data-view="list" aria-busy={pending || undefined}>
            <thead>
              <MediaListHeader view={view} />
            </thead>
            {/* Grouped page-locally: a group never spans a page, so the heading counts this page. */}
            {buckets.map((bucket) => (
              <tbody key={bucket.label || "ungrouped"}>
                {bucket.label ? (
                  <tr>
                    <th scope="colgroup" colSpan={7}>
                      {bucket.label}{" "}
                      <span className="cap-muted">{bucket.rows.length} on this page</span>
                      {bucket.note ? <span className="cap-muted"> {bucket.note}</span> : null}
                    </th>
                  </tr>
                ) : null}
                {bucket.rows.map((object) => (
                  <MediaRow
                    key={object.key}
                    object={object}
                    chosen={chosen}
                    selectRange={selectRange}
                    linkTo={linkTo}
                    view={view}
                    scanComplete={scanComplete}
                    tabStop
                  />
                ))}
              </tbody>
            ))}
          </table>
        </div>
      ) : (
        <div className="cap-media" data-view="grid" data-size={view.size}>
          {buckets.map((bucket) => (
            <section key={bucket.label || "ungrouped"} className="cap-media-group">
              {bucket.label ? (
                <h2 className="cap-media-group-title">
                  {bucket.label}{" "}
                  <span className="cap-media-group-count">{bucket.rows.length} on this page</span>
                  {bucket.note ? <span className="cap-media-group-count"> {bucket.note}</span> : null}
                </h2>
              ) : null}
              <ul
                className="cap-media-grid"
                role="list"
                aria-label={bucket.label ? `Files, ${bucket.label}` : "Files"}
                aria-busy={pending || undefined}
                {...keys}
              >
                {bucket.rows.map((object) => (
                  <MediaTile
                    key={object.key}
                    object={object}
                    chosen={chosen}
                    selectRange={selectRange}
                    linkTo={linkTo}
                    view={view}
                    scanComplete={scanComplete}
                    tabStop={object.key === tabStop}
                  />
                ))}
              </ul>
            </section>
          ))}
        </div>
      )}
    </Form>
  );
}