Skip to content

Commit c47d5aa

Browse files
committed
Support Shift+Enter for newlines in AI chat form
1 parent 9c9ef67 commit c47d5aa

File tree

5 files changed

+76
-16
lines changed

5 files changed

+76
-16
lines changed

special-pages/pages/new-tab/app/omnibar/components/AiChatForm.js

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { h } from 'preact';
22
import { useContext, useRef } from 'preact/hooks';
3+
import { eventToTarget } from '../../../../../shared/handlers';
34
import { ArrowRightIcon } from '../../components/Icons';
5+
import { usePlatformName } from '../../settings.provider';
46
import { useTypedTranslationWith } from '../../types';
57
import styles from './AiChatForm.module.css';
68
import { OmnibarContext } from './OmnibarProvider';
@@ -17,19 +19,53 @@ import { OmnibarContext } from './OmnibarProvider';
1719
export function AiChatForm({ chat, setChat }) {
1820
const { submitChat } = useContext(OmnibarContext);
1921
const { t } = useTypedTranslationWith(/** @type {Strings} */ ({}));
22+
const platformName = usePlatformName();
2023

2124
const formRef = useRef(/** @type {HTMLFormElement|null} */ (null));
2225
const textAreaRef = useRef(/** @type {HTMLTextAreaElement|null} */ (null));
2326

27+
const disabled = chat.length === 0;
28+
2429
/** @type {(event: SubmitEvent) => void} */
2530
const onSubmit = (event) => {
2631
event.preventDefault();
32+
if (disabled) return;
2733
submitChat({
2834
chat,
2935
target: 'same-tab',
3036
});
3137
};
3238

39+
/** @type {(event: KeyboardEvent) => void} */
40+
const onKeyDown = (event) => {
41+
if (event.key === 'Enter' && !event.shiftKey) {
42+
event.preventDefault();
43+
if (disabled) return;
44+
submitChat({
45+
chat,
46+
target: eventToTarget(event, platformName),
47+
});
48+
}
49+
};
50+
51+
/** @type {(event: import('preact').JSX.TargetedEvent<HTMLTextAreaElement>) => void} */
52+
const onChange = (event) => {
53+
const form = formRef.current;
54+
const textArea = event.currentTarget;
55+
56+
const { paddingTop, paddingBottom } = window.getComputedStyle(textArea);
57+
textArea.style.height = 'auto'; // Reset height
58+
textArea.style.height = `calc(${textArea.scrollHeight}px - ${paddingTop} - ${paddingBottom})`;
59+
60+
if (textArea.scrollHeight > textArea.clientHeight) {
61+
form?.classList.add(styles.hasScroll);
62+
} else {
63+
form?.classList.remove(styles.hasScroll);
64+
}
65+
66+
setChat(textArea.value);
67+
};
68+
3369
return (
3470
<form ref={formRef} class={styles.form} onClick={() => textAreaRef.current?.focus()} onSubmit={onSubmit}>
3571
<textarea
@@ -40,22 +76,8 @@ export function AiChatForm({ chat, setChat }) {
4076
aria-label={t('aiChatForm_placeholder')}
4177
autoComplete="off"
4278
rows={1}
43-
onChange={(event) => {
44-
const form = formRef.current;
45-
const textArea = event.currentTarget;
46-
47-
const { paddingTop, paddingBottom } = window.getComputedStyle(textArea);
48-
textArea.style.height = 'auto'; // Reset height
49-
textArea.style.height = `calc(${textArea.scrollHeight}px - ${paddingTop} - ${paddingBottom})`;
50-
51-
if (textArea.scrollHeight > textArea.clientHeight) {
52-
form?.classList.add(styles.hasScroll);
53-
} else {
54-
form?.classList.remove(styles.hasScroll);
55-
}
56-
57-
setChat(textArea.value);
58-
}}
79+
onKeyDown={onKeyDown}
80+
onChange={onChange}
5981
/>
6082
<div class={styles.buttons}>
6183
<button

special-pages/pages/new-tab/app/omnibar/components/Container.module.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
position: relative;
1313
transition: height 200ms ease;
1414

15+
@media (prefers-reduced-motion: reduce) {
16+
transition: none;
17+
}
18+
1519
&:focus-within {
1620
border-radius: 14px;
1721
box-shadow: 0 0 0 2px var(--ntp-color-primary), 0 0 0 4px rgba(57, 105, 239, 0.2);

special-pages/pages/new-tab/app/omnibar/components/TabSwitcher.module.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,8 @@
3838
position: absolute;
3939
top: 0;
4040
transition: translate 200ms ease;
41+
42+
@media (prefers-reduced-motion: reduce) {
43+
transition: none;
44+
}
4145
}

special-pages/pages/new-tab/app/omnibar/integration-tests/omnibar.page.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,12 @@ export class OmnibarPage {
132132
const calls = await this.ntp.mocks.waitForCallCount({ method, count: 1 });
133133
expect(calls[0].payload.params).toEqual(expectedParams);
134134
}
135+
136+
/**
137+
* @param {string} method
138+
*/
139+
async expectMethodNotCalled(method) {
140+
const calls = await this.ntp.mocks.outgoing({ names: [method] });
141+
expect(calls).toHaveLength(0);
142+
}
135143
}

special-pages/pages/new-tab/app/omnibar/integration-tests/omnibar.spec.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,28 @@ test.describe('omnibar widget', () => {
4949
});
5050
});
5151

52+
test('AI chat form shift+enter creates new line', async ({ page }, workerInfo) => {
53+
const ntp = NewtabPage.create(page, workerInfo);
54+
const omnibar = new OmnibarPage(ntp);
55+
await ntp.reducedMotion();
56+
57+
await ntp.openPage({ additional: { omnibar: true } });
58+
await omnibar.ready();
59+
60+
await omnibar.aiTab().click();
61+
await omnibar.expectMode('ai');
62+
63+
await omnibar.chatInput().fill('first line');
64+
await omnibar.chatInput().press('Shift+Enter');
65+
await omnibar.chatInput().pressSequentially('second line');
66+
67+
// Check that the textarea contains both lines with a newline
68+
await expect(omnibar.chatInput()).toHaveValue('first line\nsecond line');
69+
70+
// Verify that the form was not submitted (no method call should have been made)
71+
await omnibar.expectMethodNotCalled('omnibar_submitChat');
72+
});
73+
5274
test('mode switching preserves query state', async ({ page }, workerInfo) => {
5375
const ntp = NewtabPage.create(page, workerInfo);
5476
const omnibar = new OmnibarPage(ntp);

0 commit comments

Comments
 (0)