From 497be4cc59fd2bd88816f60bd9573c1318fc2ab9 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Fri, 18 Jul 2025 10:25:29 +1000 Subject: [PATCH 1/3] Add fixed width and height to SVG icons and remove redundant SVG size styles in CSS --- .../pages/new-tab/app/components/Icons.js | 20 +++++++++---------- .../omnibar/components/AiChatForm.module.css | 5 ----- .../omnibar/components/SearchForm.module.css | 2 -- .../components/SuggestionsList.module.css | 2 -- .../omnibar/components/TabSwitcher.module.css | 5 ----- 5 files changed, 10 insertions(+), 24 deletions(-) diff --git a/special-pages/pages/new-tab/app/components/Icons.js b/special-pages/pages/new-tab/app/components/Icons.js index 57c967ed92..e58c0e2a1b 100644 --- a/special-pages/pages/new-tab/app/components/Icons.js +++ b/special-pages/pages/new-tab/app/components/Icons.js @@ -170,7 +170,7 @@ export function BackChevron() { */ export function SearchIcon(props) { return ( - + + @@ -218,7 +218,7 @@ export function SearchColorIcon(props) { */ export function AiChatIcon(props) { return ( - + + + + + @@ -351,7 +351,7 @@ export function HistoryIcon(props) { */ export function FavoriteIcon(props) { return ( - + + + Date: Fri, 18 Jul 2025 11:28:17 +1000 Subject: [PATCH 2/3] Add Duck.ai toggle to Customizer --- .../app/customizer/CustomizerProvider.js | 17 +++++++++- .../customizer/components/CustomizerDrawer.js | 3 +- .../components/CustomizerDrawerInner.js | 17 ++++++++-- .../CustomizerDrawerInner.module.css | 3 +- .../app/customizer/components/SettingsLink.js | 32 +++++++++---------- .../app/omnibar/components/OmnibarConsumer.js | 31 +++++++++++++++--- .../app/omnibar/components/OmnibarProvider.js | 13 ++++++++ .../new-tab/app/omnibar/omnibar.service.js | 12 +++++++ .../pages/new-tab/app/omnibar/strings.json | 8 +++++ .../new-tab/public/locales/en/new-tab.json | 8 +++++ 10 files changed, 117 insertions(+), 27 deletions(-) diff --git a/special-pages/pages/new-tab/app/customizer/CustomizerProvider.js b/special-pages/pages/new-tab/app/customizer/CustomizerProvider.js index f4c5d95765..8adc24a82b 100644 --- a/special-pages/pages/new-tab/app/customizer/CustomizerProvider.js +++ b/special-pages/pages/new-tab/app/customizer/CustomizerProvider.js @@ -14,6 +14,14 @@ import { applyDefaultStyles } from './utils.js'; * @typedef {import('../service.hooks.js').Events} Events */ +/** + * @typedef {{ + * title: string, + * icon: import('preact').ComponentChild, + * onClick: () => void, + * }} SettingsLinkData + */ + /** * These are the values exposed to consumers. */ @@ -47,6 +55,10 @@ export const CustomizerContext = createContext({ * @param {UserImageContextMenu} _params */ customizerContextMenu: (_params) => {}, + /** + * @type {import('@preact/signals').Signal>} + */ + settingsLinks: signal({}), }); /** @@ -126,8 +138,11 @@ export function CustomizerProvider({ service, initialData, children }) { /** @type {(p: UserImageContextMenu) => void} */ const customizerContextMenu = useCallback((params) => service.contextMenu(params), [service]); + /** @type {import('@preact/signals').Signal>} */ + const settingsLinks = useSignal({}); + return ( - + {children} ); diff --git a/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawer.js b/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawer.js index d645feed8a..59debaddfa 100644 --- a/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawer.js +++ b/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawer.js @@ -13,7 +13,7 @@ export function CustomizerDrawer({ displayChildren }) { } function CustomizerConsumer() { - const { data, select, upload, setTheme, deleteImage, customizerContextMenu } = useContext(CustomizerContext); + const { data, select, upload, setTheme, deleteImage, customizerContextMenu, settingsLinks } = useContext(CustomizerContext); return ( ); } diff --git a/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawerInner.js b/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawerInner.js index e0dc06169c..59c43c4036 100644 --- a/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawerInner.js +++ b/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawerInner.js @@ -13,10 +13,12 @@ import { BorderedSection, CustomizerSection } from './CustomizerSection.js'; import { SettingsLink } from './SettingsLink.js'; import { DismissButton } from '../../components/DismissButton.jsx'; import { InlineErrorBoundary } from '../../InlineErrorBoundary.js'; -import { useTypedTranslationWith } from '../../types.js'; +import { useMessaging, useTypedTranslationWith } from '../../types.js'; +import { Open } from '../../components/icons/Open.js'; /** * @import { Widgets, WidgetConfigItem, WidgetVisibility, VisibilityMenuItem, CustomizerData, BackgroundData, UserImageContextMenu } from '../../../types/new-tab.js' + * @import { SettingsLinkData } from '../CustomizerProvider'; * @import enStrings from '../strings.json'; */ @@ -28,10 +30,12 @@ import { useTypedTranslationWith } from '../../types.js'; * @param {(theme: import('../../../types/new-tab').ThemeData) => void} props.setTheme * @param {(id: string) => void} props.deleteImage * @param {(p: UserImageContextMenu) => void} props.customizerContextMenu + * @param {import('@preact/signals').Signal>} props.settingsLinks */ -export function CustomizerDrawerInner({ data, select, onUpload, setTheme, deleteImage, customizerContextMenu }) { +export function CustomizerDrawerInner({ data, select, onUpload, setTheme, deleteImage, customizerContextMenu, settingsLinks }) { const { close } = useDrawerControls(); const { t } = useTypedTranslationWith(/** @type {enStrings} */ ({})); + const messaging = useMessaging(); return (
@@ -65,7 +69,14 @@ export function CustomizerDrawerInner({ data, select, onUpload, setTheme, delete - + {Object.entries(settingsLinks.value).map(([key, link]) => ( + link.onClick()} /> + ))} + } + onClick={() => messaging.open({ target: 'settings' })} + />
)} diff --git a/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawerInner.module.css b/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawerInner.module.css index c41145c13f..0b5f187e72 100644 --- a/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawerInner.module.css +++ b/special-pages/pages/new-tab/app/customizer/components/CustomizerDrawerInner.module.css @@ -257,6 +257,7 @@ align-items: center; text-decoration: none; color: var(--ntp-color-primary); + margin-bottom: var(--sp-3); &:focus { outline: none; @@ -264,4 +265,4 @@ &:focus-visible { text-decoration: underline; } -} \ No newline at end of file +} diff --git a/special-pages/pages/new-tab/app/customizer/components/SettingsLink.js b/special-pages/pages/new-tab/app/customizer/components/SettingsLink.js index bab7a3e9b1..fd2469eef8 100644 --- a/special-pages/pages/new-tab/app/customizer/components/SettingsLink.js +++ b/special-pages/pages/new-tab/app/customizer/components/SettingsLink.js @@ -1,27 +1,25 @@ import cn from 'classnames'; import styles from './CustomizerDrawerInner.module.css'; import { h } from 'preact'; -import { useMessaging, useTypedTranslationWith } from '../../types.js'; -import { Open } from '../../components/icons/Open.js'; /** - * @import enStrings from '../strings.json'; + * @param {object} props + * @param {string} props.title + * @param {import('preact').ComponentChild} props.icon + * @param {() => void} props.onClick */ - -/** - * Settings link - */ -export function SettingsLink() { - const messaging = useMessaging(); - const { t } = useTypedTranslationWith(/** @type {enStrings} */ ({})); - function onClick(e) { - e.preventDefault(); - messaging.open({ target: 'settings' }); - } +export function SettingsLink({ title, icon, onClick }) { return ( - - {t('customizer_settings_link')} - + { + event.preventDefault(); + onClick(); + }} + > + {title} + {icon} ); } diff --git a/special-pages/pages/new-tab/app/omnibar/components/OmnibarConsumer.js b/special-pages/pages/new-tab/app/omnibar/components/OmnibarConsumer.js index b65a93b5d5..6effd5fc0e 100644 --- a/special-pages/pages/new-tab/app/omnibar/components/OmnibarConsumer.js +++ b/special-pages/pages/new-tab/app/omnibar/components/OmnibarConsumer.js @@ -1,9 +1,13 @@ -import { useContext } from 'preact/hooks'; +import { useContext, useEffect } from 'preact/hooks'; import { OmnibarContext } from './OmnibarProvider.js'; import { h } from 'preact'; import { Omnibar } from './Omnibar.js'; +import { CustomizerContext } from '../../customizer/CustomizerProvider.js'; +import { AiChatIcon } from '../../components/Icons.js'; +import { useTypedTranslationWith } from '../../types.js'; /** + * @typedef {import('../strings.json')} Strings * @typedef {import('../../../types/new-tab.js').OmnibarConfig} OmnibarConfig */ @@ -31,7 +35,26 @@ export function OmnibarConsumer() { * @param {object} props * @param {OmnibarConfig} props.config */ -function OmnibarReadyState({ config }) { - const { setMode } = useContext(OmnibarContext); - return ; +function OmnibarReadyState({ config: { enableAi = true, mode } }) { + const { t } = useTypedTranslationWith(/** @type {Strings} */ ({})); + + const { settingsLinks } = useContext(CustomizerContext); + const { setMode, setEnableAi } = useContext(OmnibarContext); + + useEffect(() => { + settingsLinks.value = { + ...settingsLinks.value, + duckAi: { + title: enableAi ? t('omnibar_hideDuckAi') : t('omnibar_showDuckAi'), + icon: , + onClick: () => setEnableAi(!enableAi), + }, + }; + return () => { + const { duckAi: _, ...rest } = settingsLinks.value; + settingsLinks.value = rest; + }; + }, [enableAi]); + + return ; } diff --git a/special-pages/pages/new-tab/app/omnibar/components/OmnibarProvider.js b/special-pages/pages/new-tab/app/omnibar/components/OmnibarProvider.js index 99ab7a6933..fb4199e796 100644 --- a/special-pages/pages/new-tab/app/omnibar/components/OmnibarProvider.js +++ b/special-pages/pages/new-tab/app/omnibar/components/OmnibarProvider.js @@ -22,6 +22,10 @@ export const OmnibarContext = createContext({ setMode: () => { throw new Error('must implement'); }, + /** @type {(enableAi: NonNullable) => void} */ + setEnableAi: () => { + throw new Error('must implement'); + }, /** @type {(term: string) => Promise} */ getSuggestions: () => { throw new Error('must implement'); @@ -78,6 +82,14 @@ export function OmnibarProvider(props) { [service], ); + /** @type {(enableAi: NonNullable) => void} */ + const setEnableAi = useCallback( + (enableAi) => { + service.current?.setEnableAi(enableAi); + }, + [service], + ); + /** @type {(term: string) => Promise} */ const getSuggestions = useCallback( (term) => { @@ -125,6 +137,7 @@ export function OmnibarProvider(props) { value={{ state, setMode, + setEnableAi, getSuggestions, onSuggestions, openSuggestion, diff --git a/special-pages/pages/new-tab/app/omnibar/omnibar.service.js b/special-pages/pages/new-tab/app/omnibar/omnibar.service.js index 639d4ca78d..11d83451c3 100644 --- a/special-pages/pages/new-tab/app/omnibar/omnibar.service.js +++ b/special-pages/pages/new-tab/app/omnibar/omnibar.service.js @@ -66,6 +66,18 @@ export class OmnibarService { }); } + /** + * @param {NonNullable} enableAi + */ + setEnableAi(enableAi) { + this.configService.update((old) => { + return { + ...old, + enableAi, + }; + }); + } + /** * Get suggestions for the given search term * @param {string} term diff --git a/special-pages/pages/new-tab/app/omnibar/strings.json b/special-pages/pages/new-tab/app/omnibar/strings.json index c177f4850e..abf620f7c6 100644 --- a/special-pages/pages/new-tab/app/omnibar/strings.json +++ b/special-pages/pages/new-tab/app/omnibar/strings.json @@ -30,5 +30,13 @@ "omnibar_searchFormPlaceholder": { "title": "Search or enter address", "description": "Placeholder text for the search input field." + }, + "omnibar_hideDuckAi": { + "title": "Hide Duck.ai", + "description": "Label for the button to hide the Duck.ai chat interface." + }, + "omnibar_showDuckAi": { + "title": "Show Duck.ai", + "description": "Label for the button to show the Duck.ai chat interface." } } diff --git a/special-pages/pages/new-tab/public/locales/en/new-tab.json b/special-pages/pages/new-tab/public/locales/en/new-tab.json index 1dd694a0c5..40fcfe9c39 100644 --- a/special-pages/pages/new-tab/public/locales/en/new-tab.json +++ b/special-pages/pages/new-tab/public/locales/en/new-tab.json @@ -157,6 +157,14 @@ "title": "Search or enter address", "description": "Placeholder text for the search input field." }, + "omnibar_hideDuckAi": { + "title": "Hide Duck.ai", + "description": "Label for the button to hide the Duck.ai chat interface." + }, + "omnibar_showDuckAi": { + "title": "Show Duck.ai", + "description": "Label for the button to show the Duck.ai chat interface." + }, "nextSteps_sectionTitle": { "title": "Next Steps", "note": "Text that goes in the Next Steps bubble above the first card" From 754daadd8f79ff2eaf12808777bce9690be38bbc Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Fri, 18 Jul 2025 11:42:24 +1000 Subject: [PATCH 3/3] Add integration tests --- .../omnibar/integration-tests/omnibar.page.js | 24 ++++++-- .../omnibar/integration-tests/omnibar.spec.js | 55 +++++++++++++++++++ 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/special-pages/pages/new-tab/app/omnibar/integration-tests/omnibar.page.js b/special-pages/pages/new-tab/app/omnibar/integration-tests/omnibar.page.js index 5c73e8a712..93b2c5d2c5 100644 --- a/special-pages/pages/new-tab/app/omnibar/integration-tests/omnibar.page.js +++ b/special-pages/pages/new-tab/app/omnibar/integration-tests/omnibar.page.js @@ -49,6 +49,26 @@ export class OmnibarPage { return this.suggestionsList().getByRole('option'); } + selectedSuggestion() { + return this.suggestionsList().getByRole('option', { selected: true }); + } + + customizeButton() { + return this.page.getByRole('button', { name: 'Customize' }); + } + + toggleSearchButton() { + return this.page.getByRole('switch', { name: 'Toggle Search' }); + } + + showDuckAiButton() { + return this.page.getByRole('link', { name: 'Show Duck.ai' }); + } + + hideDuckAiButton() { + return this.page.getByRole('link', { name: 'Hide Duck.ai' }); + } + /** * @param {number} count */ @@ -56,10 +76,6 @@ export class OmnibarPage { await expect(this.suggestions()).toHaveCount(count); } - selectedSuggestion() { - return this.suggestionsList().getByRole('option', { selected: true }); - } - /** * @param {string} text */ diff --git a/special-pages/pages/new-tab/app/omnibar/integration-tests/omnibar.spec.js b/special-pages/pages/new-tab/app/omnibar/integration-tests/omnibar.spec.js index 604167c71a..10fab68ba8 100644 --- a/special-pages/pages/new-tab/app/omnibar/integration-tests/omnibar.spec.js +++ b/special-pages/pages/new-tab/app/omnibar/integration-tests/omnibar.spec.js @@ -217,6 +217,61 @@ test.describe('omnibar widget', () => { await expect(omnibar.tabList()).toHaveCount(0); }); + test('can toggle Duck.ai on', async ({ page }, workerInfo) => { + const ntp = NewtabPage.create(page, workerInfo); + const omnibar = new OmnibarPage(ntp); + await ntp.reducedMotion(); + + await ntp.openPage({ additional: { omnibar: true, 'omnibar.enableAi': false } }); + await omnibar.ready(); + + // Start out with no tab selector + await expect(omnibar.tabList()).toHaveCount(0); + + // Enable Duck.ai via Customize panel + await omnibar.customizeButton().click(); + await omnibar.showDuckAiButton().click(); + + // Tab selector is now visible + await expect(omnibar.tabList()).toBeVisible(); + }); + + test('can toggle Duck.ai off', async ({ page }, workerInfo) => { + const ntp = NewtabPage.create(page, workerInfo); + const omnibar = new OmnibarPage(ntp); + await ntp.reducedMotion(); + + await ntp.openPage({ additional: { omnibar: true } }); + await omnibar.ready(); + + // Start out with a tab selector + await expect(omnibar.tabList()).toBeVisible(); + + // Disable Duck.ai via Customize panel + await omnibar.customizeButton().click(); + await omnibar.hideDuckAiButton().click(); + + // Tab selector is now gone + await expect(omnibar.tabList()).toHaveCount(0); + }); + + test('hiding Omnibar widget hides Duck.ai toggle', async ({ page }, workerInfo) => { + const ntp = NewtabPage.create(page, workerInfo); + const omnibar = new OmnibarPage(ntp); + await ntp.reducedMotion(); + + await ntp.openPage({ additional: { omnibar: true } }); + await omnibar.ready(); + + // Open Customize panel - Duck.ai toggle should be visible + await omnibar.customizeButton().click(); + await expect(omnibar.hideDuckAiButton()).toBeVisible(); + + // Hide the Omnibar widget - Duck.ai toggle should be hidden + await omnibar.toggleSearchButton().click(); + await expect(omnibar.hideDuckAiButton()).toHaveCount(0); + }); + test('suggestions list arrow down navigation', async ({ page }, workerInfo) => { const ntp = NewtabPage.create(page, workerInfo); const omnibar = new OmnibarPage(ntp);