Line data Source code
1 : import * as React from 'react';
2 : import * as styles from './session_feed.module.css';
3 :
4 : import { renderScriptReference, SCRIPT_REFERENCE_PREFIX } from '../../agent/script_references.js';
5 :
6 : interface Props {
7 : /// The display name of the script
8 : scriptName: string;
9 : /// Can the script be mentioned?
10 : canTarget: boolean;
11 : /// Mention the script at the cursor of the last focused editor, a script or the prompt
12 : onTarget: () => void;
13 : /// Is the name being edited?
14 : renaming: boolean;
15 : /// Start or stop editing the name
16 : onRenamingChange: (renaming: boolean) => void;
17 : /// Rename the script, the name is the new display name
18 : onRename: (name: string) => void;
19 : /// What the table parameter reads, for the labels: a script by default, or an attachment
20 : subject?: 'script' | 'attachment';
21 : }
22 :
23 : /// The ":<name>" tag of a table parameter, of a script card or of an attachment card.
24 : /// A click mentions the table parameter at the cursor of the editor that had the focus last, a script or the prompt.
25 : /// While the name is being edited, the tag is an input.
26 1 : export const ScriptMentionTag: React.FC<Props> = (props) => {
27 1 : if (props.renaming) {
28 1 : return <ScriptNameInput {...props} />;
29 : }
30 1 : return (
31 1 : <button
32 1 : type="button"
33 1 : className={styles.feed_entry_mention}
34 : // The editor keeps the focus and its cursor
35 1 : onMouseDown={(event) => event.preventDefault()}
36 1 : onClick={(event) => {
37 1 : event.stopPropagation();
38 1 : if (props.canTarget) props.onTarget();
39 1 : }}
40 1 : aria-label={`Mention ${props.scriptName} at the cursor`}
41 1 : aria-disabled={props.canTarget ? undefined : 'true'}
42 1 : title={`Insert ${renderScriptReference(props.scriptName)} at the cursor`}
43 1 : data-testid="script-mention"
44 : >
45 1 : {renderScriptReference(props.scriptName)}
46 1 : </button>
47 : );
48 1 : };
49 :
50 1 : function ScriptNameInput(props: Props) {
51 1 : const [draft, setDraft] = React.useState(props.scriptName);
52 1 : const inputRef = React.useRef<HTMLInputElement>(null);
53 1 : const doneRef = React.useRef(false);
54 : // Focus after the frame, a closing menu returns the focus to its trigger first
55 1 : React.useEffect(() => {
56 1 : const frame = requestAnimationFrame(() => {
57 1 : inputRef.current?.focus();
58 1 : inputRef.current?.select();
59 1 : });
60 1 : return () => cancelAnimationFrame(frame);
61 1 : }, []);
62 1 : const finish = (commit: boolean) => {
63 1 : if (doneRef.current) return;
64 1 : doneRef.current = true;
65 1 : const name = draft.trim();
66 1 : if (commit && name && name !== props.scriptName) props.onRename(name);
67 1 : props.onRenamingChange(false);
68 1 : };
69 1 : return (
70 1 : <span className={styles.feed_entry_mention} data-editing="">
71 1 : <span aria-hidden="true">{SCRIPT_REFERENCE_PREFIX}</span>
72 1 : <input
73 1 : ref={inputRef}
74 1 : className={styles.feed_entry_mention_input}
75 1 : type="text"
76 1 : aria-label={`Rename ${props.scriptName} ${props.subject ?? 'script'}`}
77 1 : data-testid="script-name-input"
78 1 : value={draft}
79 1 : size={Math.max(draft.length, 1)}
80 1 : spellCheck={false}
81 1 : onChange={event => setDraft(event.currentTarget.value)}
82 1 : onBlur={() => finish(true)}
83 1 : onKeyDown={(event) => {
84 : // The feed must not see the keys, Enter would accept a rewrite
85 1 : event.stopPropagation();
86 1 : if (event.key === 'Enter') finish(true);
87 1 : else if (event.key === 'Escape') finish(false);
88 1 : }}
89 1 : onClick={event => event.stopPropagation()}
90 1 : onPointerDown={event => event.stopPropagation()}
91 1 : />
92 1 : </span>
93 : );
94 1 : }
|