Skip to content

Commit 736b62f

Browse files
committed
ntp: omnibar states
1 parent 089d908 commit 736b62f

File tree

14 files changed

+250
-5
lines changed

14 files changed

+250
-5
lines changed
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { h } from 'preact';
22
import { Centered } from '../components/Layout.js';
33
import { OmnibarCustomized } from '../omnibar/components/OmnibarCustomized.js';
4+
import { PersistentTermsProvider } from '../omnibar/components/PersistentTermsProvider.js';
45

56
export function factory() {
67
return (
78
<Centered data-entry-point="omnibar">
8-
<OmnibarCustomized />
9+
<PersistentTermsProvider>
10+
<OmnibarCustomized />
11+
</PersistentTermsProvider>
912
</Centered>
1013
);
1114
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { SearchForm } from './SearchForm';
1010
import { SearchFormProvider } from './SearchFormProvider';
1111
import { SuggestionsList } from './SuggestionsList';
1212
import { TabSwitcher } from './TabSwitcher';
13+
import { useQueryWithPersistence } from './PersistentTermsProvider.js';
1314

1415
/**
1516
* @typedef {import('../strings.json')} Strings
@@ -27,7 +28,7 @@ import { TabSwitcher } from './TabSwitcher';
2728
export function Omnibar({ mode, setMode, enableAi }) {
2829
const { t } = useTypedTranslationWith(/** @type {Strings} */ ({}));
2930

30-
const [query, setQuery] = useState(/** @type {String} */ (''));
31+
const [query, setQuery] = useQueryWithPersistence();
3132
const [resetKey, setResetKey] = useState(0);
3233
const [autoFocus, setAutoFocus] = useState(false);
3334
const [focusRing, setFocusRing] = useState(/** @type {boolean|undefined} */ (undefined));

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { ArrowIndentCenteredIcon } from '../../components/Icons.js';
2727
export function OmnibarConsumer() {
2828
const { state } = useContext(OmnibarContext);
2929
if (state.status === 'ready') {
30-
return <OmnibarReadyState config={state.config} />;
30+
return <OmnibarReadyState config={state.config} key={state.config.tabId} />;
3131
}
3232
return null;
3333
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { h } from 'preact';
66

77
import { OmnibarConsumer } from './OmnibarConsumer.js';
88
import { SearchIcon } from '../../components/Icons.js';
9+
import { PruneInitialTerms } from './PersistentTermsProvider.js';
910

1011
/**
1112
* @import enStrings from "../strings.json"
@@ -37,6 +38,7 @@ export function OmnibarCustomized() {
3738
return (
3839
<OmnibarProvider>
3940
<OmnibarConsumer />
41+
<PruneInitialTerms />
4042
</OmnibarProvider>
4143
);
4244
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createContext, h } from 'preact';
2-
import { useCallback, useEffect, useReducer, useRef } from 'preact/hooks';
2+
import { useCallback, useContext, useEffect, useReducer, useRef } from 'preact/hooks';
33
import { useMessaging } from '../../types.js';
44
import { reducer, useInitialDataAndConfig, useConfigSubscription } from '../../service.hooks.js';
55
import { OmnibarService } from '../omnibar.service.js';
@@ -153,7 +153,7 @@ export function OmnibarProvider(props) {
153153
/**
154154
* @return {import("preact").RefObject<OmnibarService>}
155155
*/
156-
export function useService() {
156+
function useService() {
157157
const service = useRef(/** @type {OmnibarService|null} */ (null));
158158
const ntp = useMessaging();
159159
useEffect(() => {
@@ -165,3 +165,7 @@ export function useService() {
165165
}, [ntp]);
166166
return service;
167167
}
168+
169+
export function useOmnibarService() {
170+
return useContext(OmnibarServiceContext);
171+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { h, createContext } from 'preact';
2+
import { useCallback, useContext, useEffect, useState } from 'preact/hooks';
3+
import { OmnibarTerm } from '../omnibar.term.js';
4+
import { OmnibarContext, useOmnibarService } from './OmnibarProvider.js';
5+
6+
const InitialTermContext = createContext(/** @type {OmnibarTerm|null} */ (null));
7+
8+
/**
9+
* @param {object} props
10+
* @param {import('preact').ComponentChildren} props.children
11+
*/
12+
export function PersistentTermsProvider({ children }) {
13+
const [terms] = useState(() => new OmnibarTerm());
14+
return <InitialTermContext.Provider value={terms}>{children}</InitialTermContext.Provider>;
15+
}
16+
17+
/**
18+
* When the Omnibar Service becomes ready, subscribe to tab updates so that we can prune old state
19+
*/
20+
export function PruneInitialTerms() {
21+
const terms = useContext(InitialTermContext);
22+
const service = useOmnibarService();
23+
useEffect(() => {
24+
return service?.onConfig(({ source, data }) => {
25+
if (source === 'subscription') {
26+
if (typeof data.tabId === 'string' && Array.isArray(data.tabIds)) {
27+
terms?.prune({ preserve: data.tabIds });
28+
}
29+
}
30+
});
31+
}, [service]);
32+
return null;
33+
}
34+
35+
/**
36+
* A normal set-state, but with values recorded. Must be used when the Omnibar Service is ready
37+
*/
38+
export function useQueryWithPersistence() {
39+
const terms = useContext(InitialTermContext);
40+
const { state } = useContext(OmnibarContext);
41+
42+
if (state.status !== 'ready') {
43+
throw new Error('Cannot use `useQueryWithPersistence` without Omnibar Service being ready.');
44+
}
45+
46+
const [query, _setQuery] = useState(() => terms?.byId(state.config.tabId) || '');
47+
48+
/** @type {(term: string) => void} */
49+
const setter = useCallback(
50+
(term) => {
51+
if (state.config.tabId) {
52+
terms?.update({ id: state.config.tabId, term });
53+
_setQuery(term);
54+
}
55+
},
56+
[state.config.tabId],
57+
);
58+
59+
return /** @type {const} */ ([query, setter]);
60+
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,29 @@ export class OmnibarPage {
156156
const calls = await this.ntp.mocks.outgoing({ names: [method] });
157157
expect(calls).toHaveLength(0);
158158
}
159+
160+
async didSwitchToTab(id, tabs) {
161+
await this.ntp.mocks.simulateSubscriptionMessage(
162+
sub('omnibar_onConfigUpdate'),
163+
config({
164+
tabId: id,
165+
tabIds: tabs,
166+
mode: 'search',
167+
}),
168+
);
169+
}
170+
}
171+
172+
/**
173+
* @param {import("../../../types/new-tab.js").NewTabMessages["subscriptions"]["subscriptionEvent"]} name
174+
*/
175+
function sub(name) {
176+
return name;
177+
}
178+
179+
/**
180+
* @param {import("../../../types/new-tab.js").OmnibarConfig} c
181+
*/
182+
function config(c) {
183+
return c;
159184
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { test } from '@playwright/test';
2+
import { NewtabPage } from '../../../integration-tests/new-tab.page.js';
3+
import { OmnibarPage } from './omnibar.page.js';
4+
5+
test.describe('omnibar widget persistence', () => {
6+
test('remembers input across tabs', async ({ page }, workerInfo) => {
7+
const ntp = NewtabPage.create(page, workerInfo);
8+
const omnibar = new OmnibarPage(ntp);
9+
await ntp.reducedMotion();
10+
await ntp.openPage({ additional: { omnibar: true, 'omnibar.tabs': true } });
11+
await omnibar.ready();
12+
13+
// first fill
14+
await omnibar.searchInput().fill('shoes');
15+
16+
// switch
17+
await omnibar.didSwitchToTab('02', ['01', '02']);
18+
await omnibar.expectInputValue('');
19+
20+
// second fill
21+
await omnibar.searchInput().fill('dresses');
22+
23+
// back to first
24+
await omnibar.didSwitchToTab('01', ['01', '02']);
25+
await omnibar.expectInputValue('shoes');
26+
27+
// back to second again
28+
await omnibar.didSwitchToTab('02', ['01', '02']);
29+
await omnibar.expectInputValue('dresses');
30+
});
31+
});

special-pages/pages/new-tab/app/omnibar/mocks/omnibar.mock-transport.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ export function omnibarMockTransport() {
5454
if (showAiSettingOverride === 'true' || showAiSettingOverride === 'false') {
5555
config.showAiSetting = showAiSettingOverride === 'true';
5656
}
57+
if (url.searchParams.has('omnibar.tabs')) {
58+
config.tabId = '01';
59+
config.tabIds = ['01'];
60+
}
5761
return config;
5862
}
5963
case 'omnibar_getSuggestions': {

special-pages/pages/new-tab/app/omnibar/omnibar.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ title: Omnibar Widget
3434
}
3535
```
3636

37+
Note: On Windows `tabId` and `tabIds` are required:
38+
39+
```json
40+
{
41+
"mode": "search",
42+
"tabId": "01",
43+
"tabIds": ["01"]
44+
}
45+
```
46+
3747
### `omnibar_getSuggestions`
3848
- {@link "NewTab Messages".OmnibarGetSuggestionsRequest}
3949
- Used to fetch search suggestions based on user input
@@ -75,6 +85,25 @@ title: Omnibar Widget
7585
- {@link "NewTab Messages".OmnibarOnConfigUpdateSubscription}
7686
- The omnibar widget configuration updates
7787
- returns {@link "NewTab Messages".OmnibarConfig}
88+
- Note: tabId and tabIds are required fields on Windows
89+
90+
```json
91+
{
92+
"mode": "ai",
93+
"tabId": "01",
94+
"tabIds": ["01"]
95+
}
96+
```
97+
98+
You can force the frontend to switch to data for a particular tab like this:
99+
100+
```json
101+
{
102+
"mode": "ai",
103+
"tabId": "01",
104+
"tabIds": ["01", "02", "03"]
105+
}
106+
```
78107

79108
## Notifications:
80109

0 commit comments

Comments
 (0)