v3: the editors stop fighting the person typing — a review of every edit box turned up an Enter that sent a chat message mid-IME, an emoji picker that grabbed focus on ":D" and never went away, markdown that folded every line break, and limits that were counted but not kept. TextEditor: an Enter confirming an IME candidate no longer also sends, saves or picks. The : trigger wants two letters (Unicode ones too), not one, so emoticons, "note:" and "12:30" type through, and it keeps focus in the textarea — the picker's search follows what's typed, Enter or Tab takes the best match (a name equal to the query, then one starting with it, rather than frimousse's first in emoji order, which made ":rocket" an astronaut), the arrows step into the grid and typing there carries on in the textarea, and Escape closes just the picker, not the chat around it. It closes on a press outside, which it never did, and neither autocomplete fires inside code or a ``` fence. A ☺ button in each editor's corner opens the picker for mouse users, with a skin-tone selector that's remembered, and its search takes focus only once the picker is placed (a hidden field can't). Emoji, mentions and pasted images go in through execCommand, so Ctrl+Z takes back the insertion instead of wrecking the undo stack. Uploading an image made the textarea disabled, which dropped focus for good; it's read-only now, says "Uploading image…" under the box rather than only in an empty one's placeholder, reports a failed upload instead of swallowing it, and holds Enter until it's done. Any dropped file is caught — a non-image used to be opened by the browser in place of the page, draft and all — and a paste carrying text (a spreadsheet's cells come with a picture of themselves) pastes the text. The mention list follows the caret (a stale lookup can't close a newer one, lookups abort), wraps, scrolls its selection into view, closes on blur, and is a real listbox with aria-activedescendant. The counter shows from 80% of the limit by default rather than a standing "0 / 5000". Limits: chat Send and edit Save refused nothing over 2000 and let the server bounce it; they're disabled past it now. RichTextField's bare maxLength rule failed without a message, so an over-long "Why?" made Save do nothing and say nothing; it says so now. Inline editors answer Escape through a new onCancel (comment reply and edit, profile description, playlist description and title), the playlist editor takes Ctrl+Enter and can't be saved without a title, and saving a comment unchanged no longer stamps it edited. Markdown: a single newline is a line break, so a message written over three lines stops reading as one; links into the app route in place instead of opening a tab; headings are sized for prose instead of 2em page titles, and dropped from card teasers; GFM tables scroll in their own box and task lists lose the bullet; images load lazily. A chat message of only an emoji or three is shown large. Themes: brutalist and geocities give the new buttons no chrome, and geocities' ghost-button list, split in two by the placeholder-thumbnail rules in 1cb904d, is whole again, so its modal ✕, emoji cells and the picker's ✕ are flat as intended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-09-27 10:24:30 +00:00
parent 5fd1d364da
commit 85d0703077
22 changed files with 1321 additions and 520 deletions

View File

@@ -1,4 +1,5 @@
import { useId } from "react";
import { t } from "@lingui/core/macro";
import {
Controller,
type FieldValues,
@@ -14,6 +15,7 @@ interface RichTextFieldProps<T extends FieldValues> {
label?: string;
placeholder?: string;
rows?: number;
/** Counted in the editor and enforced on submit, with a message saying so. */
maxLength?: number;
autoResize?: boolean;
disabled?: boolean;
@@ -42,8 +44,18 @@ export function RichTextField<T extends FieldValues>({
onSubmit,
}: RichTextFieldProps<T>) {
const id = useId();
const hintId = useId();
const { control, formState: { errors } } = useFormContext<T>();
const error = errors[name];
// A bare numeric rule fails without a message, which left a submit that did
// nothing and said nothing.
const allRules = maxLength == null ? rules : {
...rules,
maxLength: {
value: maxLength,
message: t`At most ${maxLength} characters.`,
},
};
return (
<div className="form-field">
@@ -55,7 +67,7 @@ export function RichTextField<T extends FieldValues>({
<Controller
name={name}
control={control}
rules={rules}
rules={allRules}
render={({ field }) => (
<TextEditor
ref={editorRef}
@@ -68,11 +80,13 @@ export function RichTextField<T extends FieldValues>({
autoResize={autoResize}
disabled={disabled}
onSubmit={onSubmit}
aria-invalid={error ? true : undefined}
aria-describedby={error?.message ? hintId : undefined}
/>
)}
/>
{error?.message && (
<span className="form-hint form-hint--error">
<span id={hintId} className="form-hint form-hint--error">
{String(error.message)}
</span>
)}