fix: tag counts, chip focus, upload naming, share links and unsized clips hold after round 2
- A tag counts the followed channels the Subscriptions tab shows: hidden ones are left out, and a subscribe, unsubscribe, hide or tag toggle refreshes the count. The Tag manager's row and its hover list count the same channels as the chip; /api/tags sends both counts under their names. - A chip's count keeps the chip's colour after a dot (a dimmed count fell under 4.5:1). A pick in the open "+N" fold closes it; a chip that leaves the list hands the focus to the one in its place. The fold is a dialog of toggle buttons, not an empty menu. - Upload: a folder name left by a press is made once the press is over, if the dialog is still open, instead of guessing a dismissal from Modal's markup. While the folder is made, its place holds the focus and a live region says so. - Share: an expired link has no Copy and a warn badge; the link made on open goes into the cached list, so a reopen makes no second one; the New link form survives the link made on open. - Watch: a repeated Enter sends one unlock while one is on its way. See all buttons are named with their rail. - An unsized clip of a rotated file gets the upright size its crop cut.
This commit is contained in:
@@ -82,19 +82,34 @@ export default function UploadDialog({
|
||||
const [naming, setNaming] = useState(false);
|
||||
const targetBox = useRef<HTMLDivElement>(null);
|
||||
const refocusTarget = useRef(false);
|
||||
// A press on Cancel, the ✕ or the backdrop blurs the name field before the dialog closes, and the
|
||||
// field saves on blur: that press is "never mind", not "make it". The press is seen before the
|
||||
// blur it causes, in every browser (which control takes the focus, if any, differs: Safari
|
||||
// focuses no button).
|
||||
const dismissing = useRef(false);
|
||||
// The name field saves on blur, and a press blurs it before the press does anything: a press on
|
||||
// Cancel, the ✕ or the backdrop that then closes the dialog is "never mind", not "make it". So a
|
||||
// name left by a press is made once the press is over, and only if the dialog is still here — its
|
||||
// close unmounts this and drops the wait. A name left without a press (Tab) is made at once.
|
||||
const pressing = useRef(false);
|
||||
const afterPress = useRef<(() => void) | null>(null);
|
||||
useEffect(() => {
|
||||
const onPress = (e: PointerEvent) => {
|
||||
const el = e.target instanceof Element ? e.target : null;
|
||||
dismissing.current =
|
||||
!!el && (!!el.closest("[data-dismiss]") || el.matches('[role="dialog"]'));
|
||||
let wait = 0;
|
||||
const down = () => {
|
||||
pressing.current = true;
|
||||
};
|
||||
const up = () => {
|
||||
pressing.current = false;
|
||||
const run = afterPress.current;
|
||||
afterPress.current = null;
|
||||
// After the click the press ends in, where a dismissal closes the dialog.
|
||||
if (run) wait = window.setTimeout(run);
|
||||
};
|
||||
document.addEventListener("pointerdown", down, true);
|
||||
document.addEventListener("pointerup", up, true);
|
||||
document.addEventListener("pointercancel", up, true);
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", down, true);
|
||||
document.removeEventListener("pointerup", up, true);
|
||||
document.removeEventListener("pointercancel", up, true);
|
||||
window.clearTimeout(wait);
|
||||
afterPress.current = null;
|
||||
};
|
||||
document.addEventListener("pointerdown", onPress, true);
|
||||
return () => document.removeEventListener("pointerdown", onPress, true);
|
||||
}, []);
|
||||
const makeFolder = useMutation({
|
||||
mutationFn: (folder: string) => api.createFolder(folder),
|
||||
@@ -106,12 +121,13 @@ export default function UploadDialog({
|
||||
},
|
||||
meta: { failTitle: "errors.writeFailed.folder.create" },
|
||||
});
|
||||
// A new folder's name ended by a key leaves the keyboard's focus nowhere: back on the list — once
|
||||
// it is back, as the folder is made (or fails): until then its place says it is being made.
|
||||
// A new folder's name ended by a key leaves the keyboard's focus nowhere: back on the list once it
|
||||
// is back, as the folder is made (or fails). Until then on its place, which says it is being made.
|
||||
useEffect(() => {
|
||||
if (naming || !refocusTarget.current) return;
|
||||
const list = targetBox.current?.querySelector("button");
|
||||
if (!list) return;
|
||||
const box = targetBox.current;
|
||||
const list = box?.querySelector("button");
|
||||
if (!list) return box?.focus();
|
||||
refocusTarget.current = false;
|
||||
list.focus();
|
||||
}, [naming, makeFolder.isPending]);
|
||||
@@ -131,7 +147,6 @@ export default function UploadDialog({
|
||||
// After the menu has closed: it hands the focus back as it goes, which took it off a field already
|
||||
// there, and a name field that loses the focus is done, with no name.
|
||||
if (value === NEW_FOLDER) {
|
||||
dismissing.current = false; // a press before the naming began (Cancel aborting an upload) is not this one
|
||||
window.setTimeout(() => setNaming(true));
|
||||
} else if (value === UPLOADS) setTarget(null);
|
||||
else setTarget(Number(value));
|
||||
@@ -268,19 +283,22 @@ export default function UploadDialog({
|
||||
onDone={(folder, byKey) => {
|
||||
refocusTarget.current = byKey;
|
||||
setNaming(false);
|
||||
if (!byKey && dismissing.current) return;
|
||||
if (folder?.trim()) makeFolder.mutate(folder.trim());
|
||||
const name = folder?.trim();
|
||||
if (!name) return;
|
||||
if (!byKey && pressing.current) afterPress.current = () => makeFolder.mutate(name);
|
||||
else makeFolder.mutate(name);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
// The select's own height held while the list loads, so it does not push the form down.
|
||||
<div ref={targetBox} className="min-h-8 flex items-center">
|
||||
{makeFolder.isPending && (
|
||||
// Upload waits for it (`targetSettling`): this says why.
|
||||
<span className="text-sm text-muted">
|
||||
{t("downloads.upload.makingFolder", { name: makeFolder.variables })}
|
||||
</span>
|
||||
)}
|
||||
// It holds the keyboard's focus while a folder is made (tabIndex -1: never a Tab stop), and
|
||||
// says it in a live region, as the list it replaces is gone until then.
|
||||
<div ref={targetBox} tabIndex={-1} className="min-h-8 flex items-center outline-none">
|
||||
<span role="status" className="text-sm text-muted">
|
||||
{/* Upload waits for it (`targetSettling`): this says why. */}
|
||||
{makeFolder.isPending &&
|
||||
t("downloads.upload.makingFolder", { name: makeFolder.variables })}
|
||||
</span>
|
||||
{foldersQ.data && !busy && !makeFolder.isPending && (
|
||||
<FilterSelect
|
||||
hideLabel
|
||||
@@ -327,7 +345,6 @@ export default function UploadDialog({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => (busy ? abortRef.current?.abort() : onClose())}
|
||||
data-dismiss=""
|
||||
>
|
||||
{t("downloads.actions.cancel")}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user