feat(design): titles in click surfaces stay selectable (E28 S3e4b)
A button whose text is a title or a name is now `selectable` (FocusSafeButton, under Pressable and Button): Chrome selects inside a button once it is user-select: text (measured), and the click that ends a selection, or a press dragged past the slop, opens nothing. 23 surfaces carry it (cards, rows, people, channels, playlists, users, views, messages, log lines, description links); the Plex caption div gets the same guard. A contract test pins every plain Pressable with its reason, so a new one has to choose.
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CheckCircle2, Circle, Film, ListPlus, Play, Star, Tv2 } from "lucide-react";
|
||||
import { type CSSProperties, type ReactNode } from "react";
|
||||
import { type CSSProperties, type ReactNode, useRef } from "react";
|
||||
import { formatRuntime } from "../lib/format";
|
||||
import { Carousel } from "./ui/Carousel";
|
||||
import type { PlexCard, PlexCastMember, PlexFilters, PlexSeasonDetail } from "../lib/api";
|
||||
import { navItemAttrs } from "../lib/listNav";
|
||||
import { endsTextSelection, movedPastSlop } from "../lib/textSelection";
|
||||
import { PLAYABLE_TINT, playableClass } from "./plexPlayable";
|
||||
import { plexAddLabelKey } from "./usePlexCardActions";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
@@ -97,9 +98,22 @@ function Caption({
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const pressedAt = useRef<{ x: number; y: number } | null>(null);
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
// The click that ends selecting the title's words, or a press dragged past the slop, does not
|
||||
// open it (DESIGN "Text stays selectable"); a div's text selects on its own, so only the click
|
||||
// needs the guard.
|
||||
onClick={
|
||||
onClick &&
|
||||
((e) => {
|
||||
if (!endsTextSelection(e.currentTarget) && !movedPastSlop(pressedAt.current, e))
|
||||
onClick();
|
||||
})
|
||||
}
|
||||
onMouseDown={(e) => {
|
||||
pressedAt.current = { x: e.clientX, y: e.clientY };
|
||||
}}
|
||||
className={`mt-1.5 flex items-start gap-1.5 rounded-lg px-2 py-1.5 ${onClick ? "cursor-pointer" : ""} ${className}`}
|
||||
style={{ background: "color-mix(in srgb, var(--card) 60%, transparent)" }}
|
||||
>
|
||||
@@ -443,6 +457,7 @@ export function CastStrip({
|
||||
);
|
||||
return clickable ? (
|
||||
<Pressable
|
||||
selectable
|
||||
key={i}
|
||||
align="center"
|
||||
{...navItemAttrs(i)}
|
||||
|
||||
Reference in New Issue
Block a user