Files
siftlode/frontend/src/components/ui/RenameField.tsx
T
peter 8b2260c9a8 fix(files): review round 1, the page — honest states, the keyboard, the tray's words
- A library past the list's cap is searched and sorted on the server; the count says "the newest".
- Each folder and each search opens on its first page (DataTable pageScope, useCardPager scope).
- A file dropped where the page takes none never reaches the browser (which would unload the app).
- The viewer's arrows wait while a layer opened from it is on top, and scroll a wide text line;
  a text shows its first 5000 lines and says where the rest is.
- Confirms name the file (no more "your downloads"); the asking menu rows end in "…".
- The tray names a running step beside its bar, shows a pause mark for paused work, polls slowly
  while only paused work waits, and forgets a finished file that was deleted.
- An unchanged rename sends nothing; a paste opens the download dialog only with nothing on top;
  the link box never replaces what was typed.
- Honest states: folders that failed to load, a file no longer there, the storage figure; the rail
  says "nothing downloaded" only when there is nothing.
- Focus after deleting the open folder and when the format form opens; the format name's label.
- ui/Meter (one danger line for both quota bars) and ui/RenameField in the platform layer.
- Copy fixes, stale comments and the speculative hub key; a contract test for the list's
  parameters; e2e: the Files page and the paste intake.
2026-10-13 00:14:09 +02:00

56 lines
2.0 KiB
TypeScript

import { useState } from "react";
import { Input } from "./Input";
// A name being typed in place of the name itself — a library file's in its row, a folder's in the rail
// or the page title, a new folder's. Enter or leaving the field saves, Escape cancels: unmounting the
// input fires no blur in React, so Escape really does cancel. The caller decides what an unchanged or
// empty name means.
//
// Ended by a KEY, the field leaves with the keyboard's focus and nothing to give it to: the caller
// puts it back on what stands there now (the row, the entry, the button that opened it) — or the
// keyboard user is dropped on <body> and loses their place (measured in Chrome, the playlist rail's
// rename). Ended by a blur, the user has put the focus somewhere on purpose, and it stays there.
export function RenameField({
initial,
label,
className = "w-full",
onDone,
}: {
initial: string;
/** The field's accessible name (it has no visible label: it stands where the name stood). */
label: string;
className?: string;
/** The typed name, or null when cancelled; `byKey`: Enter or Escape, so the focus is the caller's
* to put back. */
onDone: (name: string | null, byKey: boolean) => void;
}) {
const [draft, setDraft] = useState(initial);
return (
<Input
size="sm"
className={className}
autoFocus
value={draft}
aria-label={label}
onFocus={(e) => e.currentTarget.select()}
onChange={(e) => setDraft(e.target.value)}
onBlur={() => onDone(draft, false)}
onKeyDown={(e) => {
// preventDefault: a real Enter's own activation would otherwise press what focus lands on.
if (e.key === "Enter") {
e.preventDefault();
onDone(draft, true);
}
if (e.key === "Escape") {
e.preventDefault();
// Only the edit is cancelled: a drawer (the rail on a phone) or a dialog round the field
// must not close with it.
e.stopPropagation();
onDone(null, true);
}
}}
/>
);
}