From bfb5ba729054352d5971986ac04cd787bd950d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 15 May 2026 18:42:15 +0200 Subject: [PATCH] feat(coach): tap-friendly composer hint and accurate evidence aria-expanded - New `` primitive (Radix-backed, Dracula surface tokens) mirrors the existing `` wrapper. Tooltips do not tap-toggle reliably on touch; popovers do. - The Coach composer info-icon swaps off `` onto `` so the long-form "Enter to send, Shift+Enter for a new line" copy is reachable via a tap on mobile while the keyboard hover-preview semantic is preserved on desktop. The `aria-label` on the trigger keeps screen-reader users announced. - The composer textarea now carries `enterKeyHint="send"`, `autoCapitalize="sentences"`, and an opt-in `autoFocusOnOpen` prop. The drawer flips that prop on when it mounts the fallback composer, so the textarea claims the cursor the first time the drawer opens; subsequent re-renders of an already-open drawer do not steal focus back. - Coach evidence `
` is now controlled by local state. The summary carries an accurate `aria-expanded` that flips in lock-step with the disclosure; the native Enter / Space toggle semantics are preserved. --- .../__tests__/coach-input.test.tsx | 13 ++- .../insights/coach-panel/coach-drawer.tsx | 6 ++ .../insights/coach-panel/coach-input.tsx | 99 +++++++++++++------ .../insights/coach-panel/message-thread.tsx | 17 ++++ src/components/ui/popover.tsx | 55 +++++++++++ 5 files changed, 156 insertions(+), 34 deletions(-) create mode 100644 src/components/ui/popover.tsx diff --git a/src/components/insights/coach-panel/__tests__/coach-input.test.tsx b/src/components/insights/coach-panel/__tests__/coach-input.test.tsx index 9bbbd9bed..fcf9290e1 100644 --- a/src/components/insights/coach-panel/__tests__/coach-input.test.tsx +++ b/src/components/insights/coach-panel/__tests__/coach-input.test.tsx @@ -26,12 +26,15 @@ describe("", () => { expect(html).not.toContain("Coach replies are generated"); }); - it("renders the localised placeholder + the tooltip-trigger hint", () => { + it("renders the localised placeholder + the popover-trigger hint", () => { // v1.4.27 F15 — the verbose prose hint footer was replaced by an - // Info icon wrapped in a tooltip. The same `composerHint` - // translation now surfaces as the trigger's `aria-label` (so - // screen-readers still announce it on focus) and as the tooltip - // body for sighted users on hover. + // Info icon button. The same `composerHint` translation surfaces + // as the trigger's `aria-label` (so screen-readers announce it on + // focus) and as the popover body for sighted users on tap. + // + // v1.4.27 MB3 / CF-31 — the disclosure swapped off Radix Tooltip + // (no reliable tap-toggle on touch surfaces) onto Radix Popover, + // so the contract is now "tap to open" instead of "hover". const html = render( {}} onSubmit={() => {}} />, ); diff --git a/src/components/insights/coach-panel/coach-drawer.tsx b/src/components/insights/coach-panel/coach-drawer.tsx index 51199a7ef..78db3bf99 100644 --- a/src/components/insights/coach-panel/coach-drawer.tsx +++ b/src/components/insights/coach-panel/coach-drawer.tsx @@ -455,6 +455,12 @@ export function CoachDrawer({ onSubmit={() => handleSubmit(inputValue)} disabled={send.isStreaming} isStreaming={send.isStreaming} + // v1.4.27 MB3 / CF-30 — the composer mounts on drawer + // open and unmounts on close (the body collapses with + // the sheet), so a one-shot mount focus matches the + // "drawer just opened" semantic without any re-render + // gymnastics. + autoFocusOnOpen /> ) } diff --git a/src/components/insights/coach-panel/coach-input.tsx b/src/components/insights/coach-panel/coach-input.tsx index 8979761af..5abc60789 100644 --- a/src/components/insights/coach-panel/coach-input.tsx +++ b/src/components/insights/coach-panel/coach-input.tsx @@ -11,11 +11,10 @@ import { Info, Loader2, Send } from "lucide-react"; import { Button } from "@/components/ui/button"; import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { cn } from "@/lib/utils"; import { useTranslations } from "@/lib/i18n/context"; @@ -50,6 +49,14 @@ export interface CoachInputProps { isStreaming?: boolean; /** Optional textarea ref so the parent can focus on drawer open. */ inputId?: string; + /** + * v1.4.27 MB3 / CF-30 — when set, focuses the composer textarea once + * on mount. The drawer flips this to `true` when the drawer becomes + * the freshly-opened surface so the user can start typing + * immediately. It must stay opt-in (lazy): on every re-render of an + * already-open drawer we do not steal focus back. + */ + autoFocusOnOpen?: boolean; } /** @@ -94,10 +101,29 @@ export function CoachInput({ disabled = false, isStreaming = false, inputId = "coach-composer-textarea", + autoFocusOnOpen = false, }: CoachInputProps) { const { t } = useTranslations(); const textareaRef = useRef(null); + // v1.4.27 MB3 / CF-30 — focus the textarea once on mount when the + // parent flags this surface as the freshly-opened one. The empty + // dependency array keeps this from re-firing on every value change + // (the auto-grow effect handles those). We bail when disabled so we + // do not yank focus into a non-interactive field. + useEffect(() => { + if (!autoFocusOnOpen || disabled) return; + const el = textareaRef.current; + if (!el) return; + // Defer to the next paint so the drawer's own focus-trap finishes + // resolving before we hand the cursor to the composer. + const id = window.requestAnimationFrame(() => { + el.focus(); + }); + return () => window.cancelAnimationFrame(id); + // eslint-disable-next-line react-hooks/exhaustive-deps -- one-shot on mount + }, []); + // v1.4.25 W5 — auto-grow side effect. Runs on every value change // (the controlled `value` prop is the source of truth) plus on mount // so the initial paint already shows the 1-line height even when the @@ -168,6 +194,13 @@ export function CoachInput({ onKeyDown={handleKeyDown} placeholder={t("insights.coach.composerPlaceholder")} disabled={disabled} + // v1.4.27 MB3 / CF-30 — surface the "send" virtual-keyboard + // return on iOS / Android instead of the generic newline + // glyph, and lift autocapitalize to "sentences" so a + // question typed on phone reads as one. The submit handler + // still preserves Shift+Enter for explicit newlines. + enterKeyHint="send" + autoCapitalize="sentences" // v1.4.25 W5 — single-line initial state, Claude-web-style. // `rows={1}` is the SSR-stable baseline; the `useEffect` // above grows the textarea up to `AUTO_GROW_MAX_LINES`. Past @@ -185,31 +218,39 @@ export function CoachInput({ it ate ~140 px of vertical room for an Apple-Health-like single-message exchange. The hint now sits behind a tiny Info icon left of the send button; the existing - translation string surfaces as the tooltip body so - screen-reader users still hear it on focus. */} + translation string surfaces as the popover body so + screen-reader users still hear it on focus. + + v1.4.27 MB3 / CF-31 — swapped off Radix `` (which + never tap-toggles reliably on touch) onto `` so + mobile users can open the hint via a plain tap on the icon + button. The aria-label still labels the icon for screen + readers; the popover body carries the long-form copy. */}
- - - - - - - {t("insights.coach.composerHint")} - - - + + + + + + {t("insights.coach.composerHint")} + +