Skip to content

Commit a523a46

Browse files
authored
Move focus to input when switching Omnibar tabs (#1823)
1 parent 2c84443 commit a523a46

File tree

4 files changed

+58
-8
lines changed

4 files changed

+58
-8
lines changed

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { h } from 'preact';
2-
import { useRef } from 'preact/hooks';
2+
import { useEffect, useRef } from 'preact/hooks';
33
import { eventToTarget } from '../../../../../shared/handlers';
44
import { ArrowRightIcon } from '../../components/Icons';
55
import { usePlatformName } from '../../settings.provider';
@@ -14,16 +14,23 @@ import styles from './AiChatForm.module.css';
1414
/**
1515
* @param {object} props
1616
* @param {string} props.chat
17+
* @param {boolean} [props.autoFocus]
1718
* @param {(chat: string) => void} props.onChange
1819
* @param {(params: { chat: string, target: OpenTarget }) => void} props.onSubmit
1920
*/
20-
export function AiChatForm({ chat, onChange, onSubmit }) {
21+
export function AiChatForm({ chat, autoFocus, onChange, onSubmit }) {
2122
const { t } = useTypedTranslationWith(/** @type {Strings} */ ({}));
2223
const platformName = usePlatformName();
2324

2425
const formRef = useRef(/** @type {HTMLFormElement|null} */ (null));
2526
const textAreaRef = useRef(/** @type {HTMLTextAreaElement|null} */ (null));
2627

28+
useEffect(() => {
29+
if (autoFocus && textAreaRef.current) {
30+
textAreaRef.current.focus();
31+
}
32+
}, [autoFocus]);
33+
2734
const disabled = chat.length === 0;
2835

2936
/** @type {(event: SubmitEvent) => void} */

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import { useContext, useState } from 'preact/hooks';
33
import { LogoStacked } from '../../components/Icons';
44
import { useTypedTranslationWith } from '../../types';
55
import { AiChatForm } from './AiChatForm';
6+
import { Container } from './Container';
67
import styles from './Omnibar.module.css';
8+
import { OmnibarContext } from './OmnibarProvider';
79
import { SearchForm } from './SearchForm';
810
import { TabSwitcher } from './TabSwitcher';
9-
import { Container } from './Container';
10-
import { OmnibarContext } from './OmnibarProvider';
1111

1212
/**
1313
* @typedef {import('../strings.json')} Strings
@@ -26,6 +26,7 @@ export function Omnibar({ mode, setMode, enableAi }) {
2626
const { t } = useTypedTranslationWith(/** @type {Strings} */ ({}));
2727
const [query, setQuery] = useState(/** @type {String} */ (''));
2828
const [resetKey, setResetKey] = useState(0);
29+
const [autoFocus, setAutoFocus] = useState(false);
2930

3031
const { openSuggestion, submitSearch, submitChat } = useContext(OmnibarContext);
3132

@@ -52,21 +53,34 @@ export function Omnibar({ mode, setMode, enableAi }) {
5253
resetForm();
5354
};
5455

56+
/** @type {(mode: OmnibarConfig['mode']) => void} */
57+
const handleChangeMode = (nextMode) => {
58+
setAutoFocus(true);
59+
setMode(nextMode);
60+
};
61+
5562
return (
5663
<div class={styles.root} data-mode={mode}>
5764
<LogoStacked class={styles.logo} aria-label={t('omnibar_logoAlt')} />
58-
{enableAi && <TabSwitcher mode={mode} onChange={setMode} />}
65+
{enableAi && <TabSwitcher mode={mode} onChange={handleChangeMode} />}
5966
<Container overflow={mode === 'search'}>
6067
{mode === 'search' ? (
6168
<SearchForm
6269
key={`search-${resetKey}`}
6370
term={query}
71+
autoFocus={autoFocus}
6472
onChangeTerm={setQuery}
6573
onOpenSuggestion={handleOpenSuggestion}
6674
onSubmitSearch={handleSubmitSearch}
6775
/>
6876
) : (
69-
<AiChatForm key={`chat-${resetKey}`} chat={query} onChange={setQuery} onSubmit={handleSubmitChat} />
77+
<AiChatForm
78+
key={`chat-${resetKey}`}
79+
chat={query}
80+
autoFocus={autoFocus}
81+
onChange={setQuery}
82+
onSubmit={handleSubmitChat}
83+
/>
7084
)}
7185
</Container>
7286
</div>

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { h } from 'preact';
2-
import { useId } from 'preact/hooks';
2+
import { useEffect, useId } from 'preact/hooks';
33
import { SearchIcon } from '../../components/Icons.js';
44
import { useTypedTranslationWith } from '../../types';
55
import styles from './SearchForm.module.css';
@@ -16,11 +16,12 @@ import { useSuggestions } from './useSuggestions';
1616
/**
1717
* @param {object} props
1818
* @param {string} props.term
19+
* @param {boolean} [props.autoFocus]
1920
* @param {(term: string) => void} props.onChangeTerm
2021
* @param {(params: {suggestion: Suggestion, target: OpenTarget}) => void} props.onOpenSuggestion
2122
* @param {(params: {term: string, target: OpenTarget}) => void} props.onSubmitSearch
2223
*/
23-
export function SearchForm({ term, onChangeTerm, onOpenSuggestion, onSubmitSearch }) {
24+
export function SearchForm({ term, autoFocus, onChangeTerm, onOpenSuggestion, onSubmitSearch }) {
2425
const { t } = useTypedTranslationWith(/** @type {Strings} */ ({}));
2526
const suggestionsListId = useId();
2627

@@ -44,6 +45,12 @@ export function SearchForm({ term, onChangeTerm, onOpenSuggestion, onSubmitSearc
4445

4546
const inputRef = useSuggestionInput(termBase, termSuggestion);
4647

48+
useEffect(() => {
49+
if (autoFocus && inputRef.current) {
50+
inputRef.current.focus();
51+
}
52+
}, [autoFocus]);
53+
4754
/** @type {(event: SubmitEvent) => void} */
4855
const handleSubmit = (event) => {
4956
event.preventDefault();

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
@@ -249,6 +249,28 @@ test.describe('omnibar widget', () => {
249249
await omnibar.expectSelectedSuggestion('pizza dough recipe');
250250
});
251251

252+
test('focus is moved to the active input on tab switch', async ({ page }, workerInfo) => {
253+
const ntp = NewtabPage.create(page, workerInfo);
254+
const omnibar = new OmnibarPage(ntp);
255+
await ntp.reducedMotion();
256+
await ntp.openPage({ additional: { omnibar: true } });
257+
await omnibar.ready();
258+
259+
// Initial state: Search tab is selected and the input should NOT be focused
260+
await omnibar.expectMode('search');
261+
await expect(omnibar.searchInput()).not.toBeFocused();
262+
263+
// Switch to Duck.ai tab and expect focus to move
264+
await omnibar.aiTab().click();
265+
await omnibar.expectMode('ai');
266+
await expect(omnibar.chatInput()).toBeFocused();
267+
268+
// Then switch back to Search tab and expect focus to move
269+
await omnibar.searchTab().click();
270+
await omnibar.expectMode('search');
271+
await expect(omnibar.searchInput()).toBeFocused();
272+
});
273+
252274
test('suggestions list arrow up navigation', async ({ page }, workerInfo) => {
253275
const ntp = NewtabPage.create(page, workerInfo);
254276
const omnibar = new OmnibarPage(ntp);

0 commit comments

Comments
 (0)