@startsimpli/ui 0.4.144 → 0.4.145

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startsimpli/ui",
3
- "version": "0.4.144",
3
+ "version": "0.4.145",
4
4
  "description": "Shared UI components package for StartSimpli applications",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -152,8 +152,8 @@
152
152
  "tailwindcss": "^3.4.19",
153
153
  "tsup": "^8.5.1",
154
154
  "typescript": "^6.0.3",
155
- "@startsimpli/realtime": "0.0.2",
156
- "@startsimpli/i18n": "0.1.3"
155
+ "@startsimpli/i18n": "0.1.3",
156
+ "@startsimpli/realtime": "0.0.2"
157
157
  },
158
158
  "keywords": [
159
159
  "ui",
@@ -24,7 +24,7 @@
24
24
  * Presentational: every write is a callback, so the host decides the
25
25
  * precondition (the version an accept asserts) and folds the result.
26
26
  */
27
- import { useId, useMemo, useRef, useState, type KeyboardEvent, type ReactNode } from 'react';
27
+ import { useId, useLayoutEffect, useMemo, useRef, useState, type KeyboardEvent, type ReactNode } from 'react';
28
28
 
29
29
  import { ActorIdentity, actorKindTitle } from '../ActorIdentity';
30
30
  import { describeChange, describeSuggestion, describeThread, quoted, who } from './describe';
@@ -587,6 +587,20 @@ function Composer({
587
587
  const [hi, setHi] = useState(0);
588
588
  const ref = useRef<HTMLTextAreaElement>(null);
589
589
  const listId = `mention-${useId()}`;
590
+ // Where the caret goes after a picked mention. Applied in a LAYOUT effect,
591
+ // i.e. synchronously after React writes the new value and before the
592
+ // browser can deliver another keystroke. A requestAnimationFrame here let a
593
+ // fast typist's next characters land at the end of the text and then jump
594
+ // (caught on the deployed gate: "...nly).(sandbox o").
595
+ const caretRef = useRef<number | null>(null);
596
+ useLayoutEffect(() => {
597
+ const at = caretRef.current;
598
+ const el = ref.current;
599
+ if (at == null || !el) return;
600
+ caretRef.current = null;
601
+ el.focus();
602
+ el.setSelectionRange(at, at);
603
+ }, [body]);
590
604
 
591
605
  const matches = useMemo(() => {
592
606
  if (!q) return [];
@@ -603,11 +617,7 @@ function Composer({
603
617
  setBody(next);
604
618
  setPicked((p) => (p.some((x) => x.sub === m.sub) ? p : [...p, m]));
605
619
  setQ(null);
606
- requestAnimationFrame(() => {
607
- const pos = q.start + tag.length;
608
- el?.focus();
609
- el?.setSelectionRange(pos, pos);
610
- });
620
+ caretRef.current = q.start + tag.length;
611
621
  };
612
622
 
613
623
  const submit = async () => {
@@ -373,6 +373,18 @@ describe('ChangesRail', () => {
373
373
  await waitFor(() => expect(onComment).toHaveBeenCalledWith('Ask @rita@x.io', ['rita'], selection))
374
374
  })
375
375
 
376
+ it('puts the caret right after a picked mention before the next keystroke can land', async () => {
377
+ render(<ChangesRail {...base} canAccept={false} onComment={jest.fn()} mentionable={[{ sub: 'rita', label: 'rita@x.io' }]} />)
378
+ const box = screen.getByLabelText('New comment on Blog') as HTMLTextAreaElement
379
+ fireEvent.change(box, { target: { value: 'Ask @ri and more', selectionStart: 7 } })
380
+ fireEvent.mouseDown(await screen.findByRole('option', { name: 'rita@x.io' }))
381
+ // Synchronously, with no animation frame in between: a fast typist's next
382
+ // character must go after the tag, not wherever the value update left it.
383
+ expect(box.value).toBe('Ask @rita@x.io and more')
384
+ expect(box.selectionStart).toBe('Ask @rita@x.io '.length)
385
+ expect(document.activeElement).toBe(box)
386
+ })
387
+
376
388
  it('keeps the typed comment when the write fails', async () => {
377
389
  const onComment = jest.fn().mockRejectedValue(new Error('Could not add the comment: no.'))
378
390
  render(<ChangesRail {...base} canAccept={false} onComment={onComment} />)