Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,10 @@ describe('EditModeAnnouncementDialog', () => {
expect(dialog?.textContent).toContain('never writes to your files on disk');
expect(dialog?.textContent).toContain('One file at a time');
expect(dialog?.textContent).toContain('Settings → Editor → Edit Code to Suggest');
expect(buttonWithText('Turn it on')).toBeTruthy();
expect(buttonWithText('Keep it off')).toBeTruthy();
const enableSwitch = document.querySelector('[role="switch"]');
expect(enableSwitch).not.toBeNull();
expect(enableSwitch?.getAttribute('aria-checked')).toBe('false');
expect(buttonWithText('Done')).toBeTruthy();
});

test.skipIf(!hasDom)('renders the bundled demo recording by default', async () => {
Expand Down Expand Up @@ -108,30 +110,53 @@ describe('EditModeAnnouncementDialog', () => {
expect(document.querySelector('[data-edit-mode-demo-placeholder]')).toBeNull();
});

test.skipIf(!hasDom)('"Turn it on" fires onEnable and never onDismiss', async () => {
test.skipIf(!hasDom)('Done with the switch untouched dismisses without enabling', async () => {
const onEnable = mock(() => {});
const onDismiss = mock(() => {});
await mountDialog({ onEnable, onDismiss });

expect(document.activeElement?.textContent).toContain('Turn it on');
// The focused default action must be the consent-neutral one: pressing it
// blind keeps the feature off.
expect(document.activeElement?.textContent).toContain('Done');

await act(async () => buttonWithText('Turn it on').click());
await act(async () => buttonWithText('Done').click());

expect(onDismiss).toHaveBeenCalledTimes(1);
expect(onEnable).not.toHaveBeenCalled();
expect(document.querySelector('[data-edit-mode-announcement-dialog]')).toBeNull();
});

test.skipIf(!hasDom)('flipping the switch then Done fires onEnable and never onDismiss', async () => {
const onEnable = mock(() => {});
const onDismiss = mock(() => {});
await mountDialog({ onEnable, onDismiss });

const enableSwitch = document.querySelector<HTMLButtonElement>('[role="switch"]');
expect(enableSwitch).not.toBeNull();
await act(async () => enableSwitch!.click());
expect(enableSwitch!.getAttribute('aria-checked')).toBe('true');

await act(async () => buttonWithText('Done').click());

expect(onEnable).toHaveBeenCalledTimes(1);
expect(onDismiss).not.toHaveBeenCalled();
expect(document.querySelector('[data-edit-mode-announcement-dialog]')).toBeNull();
});

test.skipIf(!hasDom)('"Keep it off" dismisses without enabling', async () => {
test.skipIf(!hasDom)('the switch is a toggle: on then off again ends with a plain dismissal', async () => {
const onEnable = mock(() => {});
const onDismiss = mock(() => {});
await mountDialog({ onEnable, onDismiss });

await act(async () => buttonWithText('Keep it off').click());
const enableSwitch = document.querySelector<HTMLButtonElement>('[role="switch"]');
await act(async () => enableSwitch!.click());
await act(async () => enableSwitch!.click());
expect(enableSwitch!.getAttribute('aria-checked')).toBe('false');

await act(async () => buttonWithText('Done').click());

expect(onDismiss).toHaveBeenCalledTimes(1);
expect(onEnable).not.toHaveBeenCalled();
expect(document.querySelector('[data-edit-mode-announcement-dialog]')).toBeNull();
});

test.skipIf(!hasDom)('Escape dismisses the dialog and restores prior focus', async () => {
Expand Down
50 changes: 37 additions & 13 deletions packages/review-editor/components/EditModeAnnouncementDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useEffect, useRef } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Ban, MessageSquarePlus, Pencil, Send } from 'lucide-react';
import { TextShimmer } from '@plannotator/ui/components/TextShimmer';
import { EDIT_MODE_DEMO_POSTER_SRC, EDIT_MODE_DEMO_VIDEO_SRC } from './editModeDemoMedia';

/**
Expand Down Expand Up @@ -127,6 +128,7 @@ export function EditModeAnnouncementDialog({
const primaryActionRef = useRef<HTMLButtonElement>(null);
const previousFocusRef = useRef<HTMLElement | null>(null);
const onDismissRef = useRef(onDismiss);
const [enableChoice, setEnableChoice] = useState(false);

useEffect(() => {
onDismissRef.current = onDismiss;
Expand All @@ -135,6 +137,8 @@ export function EditModeAnnouncementDialog({
useEffect(() => {
if (!isOpen) return;

// A reopened dialog must not remember a previously flipped switch.
setEnableChoice(false);
previousFocusRef.current = document.activeElement instanceof HTMLElement
? document.activeElement
: null;
Expand Down Expand Up @@ -198,8 +202,8 @@ export function EditModeAnnouncementDialog({
className="mt-1.5 max-w-3xl text-sm leading-relaxed text-muted-foreground"
>
A new way to give review feedback: make the change you want to see, right in the
diff. It is experimental and off by default. Turn it on now or keep it off, and
nothing else changes.
diff. It is experimental and off by default. Turn it on with the switch below, or
leave it off, and nothing else changes.
</p>
</header>

Expand Down Expand Up @@ -251,21 +255,41 @@ export function EditModeAnnouncementDialog({
<p className="text-xs text-muted-foreground">
Change this anytime in Settings → Editor → Edit Code to Suggest.
</p>
<div className="flex items-center gap-2">
<button
type="button"
onClick={onDismiss}
className="min-h-10 rounded-lg border border-border px-4 text-sm font-medium text-foreground hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-card"
>
Keep it off
</button>
{/* The enable decision is an explicit switch, deliberately separate from the
dismiss action: a primary "Turn it on" button reads as a generic continue
and gets clicked blind. Done applies whatever the switch says; with the
switch untouched it is a plain dismissal. */}
<div className="flex items-center gap-3">
<div className="flex items-center gap-3 rounded-lg border border-border bg-muted/20 px-3.5 py-2">
<span id="edit-mode-enable-label">
<TextShimmer className="text-sm font-medium" duration={2.5} spread={1.5}>
Enable Edit Mode
</TextShimmer>
</span>
<button
type="button"
role="switch"
aria-labelledby="edit-mode-enable-label"
aria-checked={enableChoice}
onClick={() => setEnableChoice((value) => !value)}
className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-card ${
enableChoice ? 'bg-primary' : 'bg-muted'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white shadow-sm transition-transform ${
enableChoice ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
<button
ref={primaryActionRef}
type="button"
onClick={onEnable}
onClick={() => (enableChoice ? onEnable() : onDismiss())}
className="min-h-10 rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-card"
>
Turn it on
Done
</button>
</div>
</footer>
Expand Down
4 changes: 2 additions & 2 deletions packages/review-editor/utils/editModeAnnouncement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe('Edit Mode announcement persistence', () => {

markEditModeAnnouncementSeen();

expect(stored.get(SEEN_KEY)).toBe('1');
expect(stored.get(SEEN_KEY)).toBe('3');
expect(needsEditModeAnnouncement()).toBe(false);
});

Expand All @@ -48,7 +48,7 @@ describe('Edit Mode announcement persistence', () => {
enableEditSuggestionsFromAnnouncement();

expect(stored.get(SETTING_KEY)).toBe('true');
expect(stored.get(SEEN_KEY)).toBe('1');
expect(stored.get(SEEN_KEY)).toBe('3');
expect(needsEditModeAnnouncement()).toBe(false);
});
});
Expand Down
4 changes: 3 additions & 1 deletion packages/review-editor/utils/editModeAnnouncement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import { configStore } from '@plannotator/ui/config';
*/
const STORAGE_KEY = 'plannotator-edit-mode-announcement-seen';
// Bump to re-show the announcement after a meaningful revision.
const CURRENT_VERSION = '1';
// '3': shimmer label on the enable switch. '2': footer redesigned from a Turn it on / Keep it off button pair to an
// explicit enable switch plus a neutral Done (pre-release, so nobody re-sees).
const CURRENT_VERSION = '3';

export function needsEditModeAnnouncement(): boolean {
return storage.getItem(STORAGE_KEY) !== CURRENT_VERSION;
Expand Down