Skip to content

Commit 754365a

Browse files
committed
wip
1 parent c25e799 commit 754365a

File tree

14 files changed

+471
-18
lines changed

14 files changed

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

56
export function factory() {
67
return (
78
<Centered data-entry-point="omnibar">
8-
<OmnibarCustomized />
9+
<PersistentTermsProvider>
10+
<PersistentModeProvider>
11+
<OmnibarCustomized />
12+
</PersistentModeProvider>
13+
</PersistentTermsProvider>
914
</Centered>
1015
);
1116
}

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

Lines changed: 4 additions & 2 deletions
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 { useQueryWithLocalPersistence } from './PersistentTermsProvider.js';
1314

1415
/**
1516
* @typedef {import('../strings.json')} Strings
@@ -23,11 +24,12 @@ import { TabSwitcher } from './TabSwitcher';
2324
* @param {OmnibarConfig['mode']} props.mode
2425
* @param {(mode: OmnibarConfig['mode']) => void} props.setMode
2526
* @param {boolean} props.enableAi
27+
* @param {string|null|undefined} props.tabId
2628
*/
27-
export function Omnibar({ mode, setMode, enableAi }) {
29+
export function Omnibar({ mode, setMode, enableAi, tabId }) {
2830
const { t } = useTypedTranslationWith(/** @type {Strings} */ ({}));
2931

30-
const [query, setQuery] = useState(/** @type {String} */ (''));
32+
const [query, setQuery] = useQueryWithLocalPersistence(tabId);
3133
const [resetKey, setResetKey] = useState(0);
3234
const [autoFocus, setAutoFocus] = useState(false);
3335

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useVisibility } from '../../widget-list/widget-config.provider.js';
66
import { Omnibar } from './Omnibar.js';
77
import { OmnibarContext } from './OmnibarProvider.js';
88
import { ArrowIndentCenteredIcon } from '../../components/Icons.js';
9+
import { useModeWithLocalPersistence } from './PersistentTermsProvider.js';
910

1011
/**
1112
* @typedef {import('../strings.json')} Strings
@@ -27,7 +28,7 @@ import { ArrowIndentCenteredIcon } from '../../components/Icons.js';
2728
export function OmnibarConsumer() {
2829
const { state } = useContext(OmnibarContext);
2930
if (state.status === 'ready') {
30-
return <OmnibarReadyState config={state.config} />;
31+
return <OmnibarReadyState config={state.config} key={state.config.tabId} />;
3132
}
3233
return null;
3334
}
@@ -36,12 +37,17 @@ export function OmnibarConsumer() {
3637
* @param {object} props
3738
* @param {OmnibarConfig} props.config
3839
*/
39-
function OmnibarReadyState({ config: { enableAi = true, showAiSetting = true, mode } }) {
40+
function OmnibarReadyState({ config }) {
41+
const { enableAi = true, showAiSetting = true, tabId, mode: defaultMode } = config;
4042
const { setEnableAi, setMode } = useContext(OmnibarContext);
43+
const mode = useModeWithLocalPersistence(tabId, defaultMode);
4144
return (
4245
<>
4346
{showAiSetting && <AiSetting enableAi={enableAi} setEnableAi={setEnableAi} />}
44-
<Omnibar mode={mode} setMode={setMode} enableAi={showAiSetting && enableAi} />
47+
<pre style="width: 200px; position: fixed; top: 0; left:0">
48+
<code>{JSON.stringify({ config, mode }, null, 2)}</code>
49+
</pre>
50+
<Omnibar mode={mode} setMode={setMode} enableAi={showAiSetting && enableAi} tabId={tabId} />
4551
</>
4652
);
4753
}

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: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { h, createContext } from 'preact';
2+
import { useCallback, useContext, useEffect, useState } from 'preact/hooks';
3+
import { OmnibarContext, useOmnibarService } from './OmnibarProvider.js';
4+
import { OmnibarValue } from '../omnibar.term.js';
5+
6+
/**
7+
* @typedef {import("../../../types/new-tab.js").OmnibarConfig["mode"]} Mode
8+
*/
9+
10+
const InitialTermContext = createContext(/** @type {OmnibarValue<string>|null} */ (null));
11+
const InitialModeContext = createContext(/** @type {OmnibarValue<Mode>|null} */ (null));
12+
13+
/**
14+
* @param {object} props
15+
* @param {import('preact').ComponentChildren} props.children
16+
*/
17+
export function PersistentTermsProvider({ children }) {
18+
const [terms] = useState(() => /** @type {OmnibarValue<string>} */ (new OmnibarValue()));
19+
return <InitialTermContext.Provider value={terms}>{children}</InitialTermContext.Provider>;
20+
}
21+
/**
22+
* @param {object} props
23+
* @param {import('preact').ComponentChildren} props.children
24+
*/
25+
export function PersistentModeProvider({ children }) {
26+
const [terms] = useState(() => /** @type {OmnibarValue<Mode>} */ (new OmnibarValue()));
27+
return <InitialModeContext.Provider value={terms}>{children}</InitialModeContext.Provider>;
28+
}
29+
30+
/**
31+
* When the Omnibar Service becomes ready, subscribe to tab updates so that we can prune old state
32+
*/
33+
export function PruneInitialTerms() {
34+
const value = useContext(InitialTermContext);
35+
const service = useOmnibarService();
36+
useEffect(() => {
37+
return service?.onConfig(({ source, data }) => {
38+
if (source === 'subscription') {
39+
if (typeof data.tabId === 'string' && Array.isArray(data.tabIds)) {
40+
value?.prune({ preserve: data.tabIds });
41+
}
42+
}
43+
});
44+
}, [service]);
45+
return null;
46+
}
47+
48+
/**
49+
* A normal set-state, but with values recorded. Must be used when the Omnibar Service is ready
50+
* @param {string|null|undefined} tabId
51+
*/
52+
export function useQueryWithLocalPersistence(tabId) {
53+
const terms = useContext(InitialTermContext);
54+
55+
invariant(
56+
useContext(OmnibarContext).state.status === 'ready',
57+
'Cannot use `useQueryWithPersistence` without Omnibar Service being ready.',
58+
);
59+
60+
const [query, setQuery] = useState(() => terms?.byId(tabId) || '');
61+
62+
/** @type {(term: string) => void} */
63+
const setter = useCallback(
64+
(term) => {
65+
if (tabId) {
66+
terms?.update({ id: tabId, value: term });
67+
}
68+
setQuery(term);
69+
},
70+
[tabId],
71+
);
72+
73+
return /** @type {const} */ ([query, setter]);
74+
}
75+
76+
/**
77+
* A normal set-state, but with values recorded. Must be used when the Omnibar Service is ready
78+
*
79+
* @param {string|null|undefined} tabId
80+
* @param {Mode} defaultMode
81+
* @return {Mode}
82+
*/
83+
export function useModeWithLocalPersistence(tabId, defaultMode) {
84+
const values = useContext(InitialModeContext);
85+
86+
const [mode, setState] = useState(() => {
87+
const prev = values?.byId(tabId);
88+
if (prev) return prev;
89+
if (tabId && defaultMode) {
90+
values?.update({ id: tabId, value: defaultMode });
91+
}
92+
return defaultMode;
93+
});
94+
95+
values?.print();
96+
97+
invariant(
98+
useContext(OmnibarContext).state.status === 'ready',
99+
'Cannot use `useQueryWithPersistence` without Omnibar Service being ready.',
100+
);
101+
102+
const service = useOmnibarService();
103+
104+
useEffect(() => {
105+
if (!service) return;
106+
return service.onConfig((v) => {
107+
if (tabId && v.source === 'manual') {
108+
console.log('DID UPDATE');
109+
values?.update({ id: tabId, value: v.data.mode });
110+
}
111+
setState(v.data.mode);
112+
});
113+
}, [service, tabId]);
114+
115+
return mode;
116+
}
117+
118+
/**
119+
* @param {any} condition
120+
* @param {string} [message]
121+
* @return {asserts condition}
122+
*/
123+
export function invariant(condition, message) {
124+
if (condition) return;
125+
if (message) throw new Error('Invariant failed: ' + message);
126+
throw new Error('Invariant failed');
127+
}

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,13 @@ export class OmnibarPage {
102102
await expect(this.searchInput()).toHaveValue(value);
103103
}
104104

105+
/**
106+
* @param {string} value
107+
*/
108+
async expectChatValue(value) {
109+
await expect(this.chatInput()).toHaveValue(value);
110+
}
111+
105112
/**
106113
* @param {number} startIndex
107114
* @param {number} endIndex
@@ -167,4 +174,29 @@ export class OmnibarPage {
167174
const calls = await this.ntp.mocks.outgoing({ names: [method] });
168175
expect(calls).toHaveLength(0);
169176
}
177+
178+
async didSwitchToTab(id, tabs) {
179+
await this.ntp.mocks.simulateSubscriptionMessage(
180+
sub('omnibar_onConfigUpdate'),
181+
config({
182+
tabId: id,
183+
tabIds: tabs,
184+
mode: 'search',
185+
}),
186+
);
187+
}
188+
}
189+
190+
/**
191+
* @param {import("../../../types/new-tab.js").NewTabMessages["subscriptions"]["subscriptionEvent"]} name
192+
*/
193+
function sub(name) {
194+
return name;
195+
}
196+
197+
/**
198+
* @param {import("../../../types/new-tab.js").OmnibarConfig} c
199+
*/
200+
function config(c) {
201+
return c;
170202
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
test('remembers `mode` across tabs', async ({ page }, workerInfo) => {
32+
const ntp = NewtabPage.create(page, workerInfo);
33+
await page.clock.install();
34+
const omnibar = new OmnibarPage(ntp);
35+
await ntp.reducedMotion();
36+
await ntp.openPage({ additional: { omnibar: true, 'omnibar.tabs': true } });
37+
await omnibar.ready();
38+
39+
// first fill
40+
await omnibar.searchInput().fill('shoes');
41+
await page.getByRole('tab', { name: 'Duck.ai' }).click();
42+
await page.clock.fastForward(1000);
43+
44+
// new tab, should remain on duck.ai
45+
await omnibar.didSwitchToTab('02', ['01', '02']);
46+
await omnibar.expectChatValue('');
47+
//
48+
// // switch back
49+
// await omnibar.didSwitchToTab('01', ['01', '02']);
50+
// await omnibar.expectChatValue('shoes');
51+
});
52+
});

0 commit comments

Comments
 (0)