- 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.
56 lines
2.0 KiB
TypeScript
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);
|
|
}
|
|
}}
|
|
/>
|
|
);
|
|
}
|