Skip to content

Commit a271a98

Browse files
authored
ntp: omnibar states (#1882)
* ntp: support persistent values * docs * docs * some more docs * feedback * only use saved mode if enableAi is on * only use saved mode if enableAi is on * better handling of global * another global fix
1 parent 71fc9b3 commit a271a98

25 files changed

+879
-79
lines changed

special-pages/pages/new-tab/app/customizer/components/CustomizerMenu.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,5 +110,8 @@ export function useCustomizer({ title, id, icon, toggle, visibility, index }) {
110110

111111
useEffect(() => {
112112
window.dispatchEvent(new Event(UPDATE_EVENT));
113+
return () => {
114+
window.dispatchEvent(new Event(UPDATE_EVENT));
115+
};
113116
}, [visibility]);
114117
}

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export class CustomizerPage {
2020
this.ntp = ntp;
2121
}
2222

23+
context = () => this.ntp.page.locator('aside');
24+
2325
async showsColorSelectionPanel() {
2426
const { page } = this.ntp;
2527
await page.locator('aside').getByLabel('Solid Colors').click();
@@ -459,4 +461,34 @@ export class CustomizerPage {
459461
button: 'right',
460462
});
461463
}
464+
465+
/**
466+
* @param {string} name
467+
* @returns {Promise<void>}
468+
*/
469+
async isChecked(name) {
470+
await expect(this.context().getByRole('switch', { name })).toBeChecked();
471+
}
472+
473+
/**
474+
* @param {string} name
475+
* @returns {Promise<void>}
476+
*/
477+
async isUnchecked(name) {
478+
await expect(this.context().getByRole('switch', { name })).not.toBeChecked({ timeout: 1000 });
479+
}
480+
481+
/**
482+
* @param {string} name
483+
*/
484+
async hasSwitch(name) {
485+
await expect(this.context().getByRole('switch', { name })).toBeVisible();
486+
}
487+
488+
/**
489+
* @param {string} name
490+
*/
491+
async doesntHaveSwitch(name) {
492+
await expect(this.context().getByRole('switch', { name })).not.toBeVisible();
493+
}
462494
}

special-pages/pages/new-tab/app/customizer/mocks.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ export function customizerMockTransport() {
7171
case 'customizer_onBackgroundUpdate':
7272
case 'customizer_onImagesUpdate': {
7373
subscriptions.set(sub, cb);
74-
console.log('did add sub', sub);
7574
return () => {
7675
console.log('-- did remove sub', sub);
7776
return subscriptions.delete(sub);

special-pages/pages/new-tab/app/index.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { CustomizerService } from './customizer/customizer.service.js';
1616
import { InlineErrorBoundary } from './InlineErrorBoundary.js';
1717
import { DocumentVisibilityProvider } from '../../../shared/components/DocumentVisibility.js';
1818
import { applyDefaultStyles } from './customizer/utils.js';
19+
import { TabsService } from './tabs/tabs.service.js';
20+
import { TabsDebug, TabsProvider } from './tabs/TabsProvider.js';
1921

2022
/**
2123
* @import {Telemetry} from "./telemetry/telemetry.js"
@@ -89,6 +91,7 @@ export async function init(root, messaging, telemetry, baseEnvironment) {
8991

9092
// Resolve the entry points for each selected widget
9193
const entryPoints = await resolveEntryPoints(init.widgets, didCatch);
94+
const tabs = new TabsService(messaging, init.tabs || TabsService.DEFAULT);
9295

9396
// Create an instance of the global widget api
9497
const widgetConfigAPI = new WidgetConfigService(messaging, init.widgetConfigs);
@@ -129,7 +132,10 @@ export async function init(root, messaging, telemetry, baseEnvironment) {
129132
widgets={init.widgets}
130133
entryPoints={entryPoints}
131134
>
132-
<App />
135+
<TabsProvider service={tabs}>
136+
{environment.urlParams.has('tabs.debug') && <TabsDebug />}
137+
<App />
138+
</TabsProvider>
133139
</WidgetConfigProvider>
134140
</DocumentVisibilityProvider>
135141
</CustomizerProvider>

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

Lines changed: 75 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { freemiumPIRDataExamples } from './freemium-pir-banner/mocks/freemiumPIR
1010
import { activityMockTransport } from './activity/mocks/activity.mock-transport.js';
1111
import { protectionsMockTransport } from './protections/mocks/protections.mock-transport.js';
1212
import { omnibarMockTransport } from './omnibar/mocks/omnibar.mock-transport.js';
13+
import { tabsMockTransport } from './tabs/tabs.mock-transport.js';
1314

1415
/**
1516
* @typedef {import('../types/new-tab').Favorite} Favorite
@@ -119,6 +120,7 @@ export function mockTransport() {
119120
activity: activityMockTransport(),
120121
protections: protectionsMockTransport(),
121122
omnibar: omnibarMockTransport(),
123+
tabs: tabsMockTransport(),
122124
};
123125

124126
return new TestTransportConfig({
@@ -492,68 +494,7 @@ export function mockTransport() {
492494
return Promise.resolve(fromStorage);
493495
}
494496
case 'initialSetup': {
495-
/** @type {import('../types/new-tab.ts').Widgets} */
496-
const widgetsFromStorage = read('widgets') || [
497-
{ id: 'updateNotification' },
498-
{ id: 'rmf' },
499-
{ id: 'freemiumPIRBanner' },
500-
{ id: 'nextSteps' },
501-
{ id: 'favorites' },
502-
];
503-
504-
/** @type {import('../types/new-tab.ts').WidgetConfigs} */
505-
const widgetConfigFromStorage = read('widget_config') || [{ id: 'favorites', visibility: 'visible' }];
506-
507-
/** @type {UpdateNotificationData} */
508-
let updateNotification = { content: null };
509-
const isDelayed = url.searchParams.has('update-notification-delay');
510-
511-
if (!isDelayed && url.searchParams.has('update-notification')) {
512-
const value = url.searchParams.get('update-notification');
513-
if (value && value in updateNotificationExamples) {
514-
updateNotification = updateNotificationExamples[value];
515-
}
516-
}
517-
518-
/** @type {import('../types/new-tab.ts').InitialSetupResponse} */
519-
const initial = {
520-
widgets: widgetsFromStorage,
521-
widgetConfigs: widgetConfigFromStorage,
522-
platform: { name: 'integration' },
523-
env: 'development',
524-
locale: 'en',
525-
updateNotification,
526-
};
527-
528-
widgetsFromStorage.push({ id: 'protections' });
529-
widgetConfigFromStorage.push({ id: 'protections', visibility: 'visible' });
530-
531-
if (url.searchParams.has('omnibar')) {
532-
const favoritesWidgetIndex = widgetsFromStorage.findIndex((widget) => widget.id === 'favorites') ?? 0;
533-
widgetsFromStorage.splice(favoritesWidgetIndex, 0, { id: 'omnibar' });
534-
const favoritesWidgetConfigIndex = widgetConfigFromStorage.findIndex((widget) => widget.id === 'favorites') ?? 0;
535-
widgetConfigFromStorage.splice(favoritesWidgetConfigIndex, 0, { id: 'omnibar', visibility: 'visible' });
536-
}
537-
538-
initial.customizer = customizerData();
539-
540-
/** @type {import('../types/new-tab').NewTabPageSettings} */
541-
const settings = {
542-
customizerDrawer: { state: 'enabled' },
543-
};
544-
545-
if (url.searchParams.get('autoOpen') === 'true' && settings.customizerDrawer) {
546-
settings.customizerDrawer.autoOpen = true;
547-
}
548-
549-
if (url.searchParams.get('adBlocking') === 'enabled') {
550-
settings.adBlocking = { state: 'enabled' };
551-
}
552-
553-
// feature flags
554-
initial.settings = settings;
555-
556-
return Promise.resolve(initial);
497+
return Promise.resolve(initialSetup(url));
557498
}
558499
default: {
559500
return Promise.reject(new Error('unhandled request' + msg));
@@ -563,6 +504,78 @@ export function mockTransport() {
563504
});
564505
}
565506

507+
/**
508+
* @param {URL} url
509+
* @return {import('../types/new-tab').InitialSetupResponse}
510+
*/
511+
export function initialSetup(url) {
512+
/** @type {import('../types/new-tab.ts').Widgets} */
513+
const widgetsFromStorage = [
514+
{ id: 'updateNotification' },
515+
{ id: 'rmf' },
516+
{ id: 'freemiumPIRBanner' },
517+
{ id: 'nextSteps' },
518+
{ id: 'favorites' },
519+
];
520+
521+
/** @type {import('../types/new-tab.ts').WidgetConfigs} */
522+
const widgetConfigFromStorage = [{ id: 'favorites', visibility: 'visible' }];
523+
524+
/** @type {UpdateNotificationData} */
525+
let updateNotification = { content: null };
526+
const isDelayed = url.searchParams.has('update-notification-delay');
527+
528+
if (!isDelayed && url.searchParams.has('update-notification')) {
529+
const value = url.searchParams.get('update-notification');
530+
if (value && value in updateNotificationExamples) {
531+
updateNotification = updateNotificationExamples[value];
532+
}
533+
}
534+
535+
/** @type {import('../types/new-tab.ts').InitialSetupResponse} */
536+
const initial = {
537+
widgets: widgetsFromStorage,
538+
widgetConfigs: widgetConfigFromStorage,
539+
platform: { name: 'integration' },
540+
env: 'development',
541+
locale: 'en',
542+
updateNotification,
543+
};
544+
545+
widgetsFromStorage.push({ id: 'protections' });
546+
widgetConfigFromStorage.push({ id: 'protections', visibility: 'visible' });
547+
548+
if (url.searchParams.has('omnibar')) {
549+
const favoritesWidgetIndex = widgetsFromStorage.findIndex((widget) => widget.id === 'favorites') ?? 0;
550+
widgetsFromStorage.splice(favoritesWidgetIndex, 0, { id: 'omnibar' });
551+
const favoritesWidgetConfigIndex = widgetConfigFromStorage.findIndex((widget) => widget.id === 'favorites') ?? 0;
552+
widgetConfigFromStorage.splice(favoritesWidgetConfigIndex, 0, { id: 'omnibar', visibility: 'visible' });
553+
}
554+
555+
initial.customizer = customizerData();
556+
557+
/** @type {import('../types/new-tab').NewTabPageSettings} */
558+
const settings = {
559+
customizerDrawer: { state: 'enabled' },
560+
};
561+
562+
if (url.searchParams.get('autoOpen') === 'true' && settings.customizerDrawer) {
563+
settings.customizerDrawer.autoOpen = true;
564+
}
565+
566+
if (url.searchParams.get('adBlocking') === 'enabled') {
567+
settings.adBlocking = { state: 'enabled' };
568+
}
569+
570+
if (url.searchParams.has('tabs')) {
571+
initial.tabs = { tabId: '01', tabIds: ['01'] };
572+
}
573+
574+
// feature flags
575+
initial.settings = settings;
576+
return initial;
577+
}
578+
566579
/**
567580
* @template {{id: string}} T
568581
* @param {T[]} array

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ children:
1111
- ./customizer/customizer.md
1212
- ./protections/protections.md
1313
- ./omnibar/omnibar.md
14+
- ./tabs/tabs.md
1415
---
1516

1617
## Requests

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 './PersistentOmnibarValuesProvider.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: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ 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 './PersistentOmnibarValuesProvider.js';
10+
import { useTabState } from '../../tabs/TabsProvider.js';
911

1012
/**
1113
* @typedef {import('../strings.json')} Strings
1214
* @typedef {import('../../../types/new-tab.js').OmnibarConfig} OmnibarConfig
15+
* @typedef {import('../../../types/new-tab.js').OmnibarMode} Mode
1316
*/
1417

1518
/**
@@ -26,22 +29,27 @@ import { ArrowIndentCenteredIcon } from '../../components/Icons.js';
2629
*/
2730
export function OmnibarConsumer() {
2831
const { state } = useContext(OmnibarContext);
32+
const { current } = useTabState();
2933
if (state.status === 'ready') {
30-
return <OmnibarReadyState config={state.config} />;
34+
return <OmnibarReadyState config={state.config} key={current.value} tabId={current.value} />;
3135
}
3236
return null;
3337
}
3438

3539
/**
3640
* @param {object} props
3741
* @param {OmnibarConfig} props.config
42+
* @param {string} props.tabId
3843
*/
39-
function OmnibarReadyState({ config: { enableAi = true, showAiSetting = true, mode } }) {
44+
function OmnibarReadyState({ config, tabId }) {
45+
const { enableAi = true, showAiSetting = true, mode: defaultMode } = config;
4046
const { setEnableAi, setMode } = useContext(OmnibarContext);
47+
const modeForCurrentTab = useModeWithLocalPersistence(tabId, defaultMode);
48+
4149
return (
4250
<>
4351
{showAiSetting && <AiSetting enableAi={enableAi} setEnableAi={setEnableAi} />}
44-
<Omnibar mode={mode} setMode={setMode} enableAi={showAiSetting && enableAi} />
52+
<Omnibar mode={modeForCurrentTab} setMode={setMode} enableAi={showAiSetting && enableAi} tabId={tabId} />
4553
</>
4654
);
4755
}

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

Lines changed: 10 additions & 7 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 { PersistentModeProvider, PersistentTextInputProvider } from './PersistentOmnibarValuesProvider.js';
910

1011
/**
1112
* @import enStrings from "../strings.json"
@@ -30,13 +31,15 @@ export function OmnibarCustomized() {
3031

3132
useCustomizer({ title: sectionTitle, id, icon: <SearchIcon />, toggle, visibility: visibility.value, index });
3233

33-
if (visibility.value === 'hidden') {
34-
return null;
35-
}
36-
3734
return (
38-
<OmnibarProvider>
39-
<OmnibarConsumer />
40-
</OmnibarProvider>
35+
<PersistentTextInputProvider>
36+
<PersistentModeProvider>
37+
{visibility.value === 'visible' && (
38+
<OmnibarProvider>
39+
<OmnibarConsumer />
40+
</OmnibarProvider>
41+
)}
42+
</PersistentModeProvider>
43+
</PersistentTextInputProvider>
4144
);
4245
}

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+
}

0 commit comments

Comments
 (0)