From 4c3226c731f8d077ad975cee16169e2e5bd4eead Mon Sep 17 00:00:00 2001 From: Jay Harris Date: Thu, 14 Mar 2024 10:57:10 +1300 Subject: [PATCH] [Search]: Add Brave Search Widget to the New Tab Page (#22231) --- browser/about_flags.cc | 8 + browser/brave_content_browser_client.cc | 22 +-- browser/ui/BUILD.gn | 2 + browser/ui/brave_ui_features.cc | 14 ++ browser/ui/brave_ui_features.h | 17 +++ .../ui/webui/new_tab_page/brave_new_tab_ui.cc | 18 +++ .../ui/webui/new_tab_page/brave_new_tab_ui.h | 6 + .../ui/webui/realbox/realbox_handler.cc | 28 ++++ .../omnibox/browser/omnibox_client.cc | 10 ++ .../omnibox/browser/omnibox_client.h | 17 +++ .../omnibox/browser/omnibox_controller.cc | 5 +- components/brave_new_tab_ui/BUILD.gn | 2 + .../components/default/page/index.tsx | 5 + .../components/search/SearchBox.tsx | 124 ++++++++++++++++ .../components/search/SearchDialog.tsx | 140 ++++++++++++++++++ .../components/search/SearchPlaceholder.tsx | 21 +++ .../components/search/SearchResult.tsx | 92 ++++++++++++ .../components/search/SearchResults.tsx | 111 ++++++++++++++ .../containers/newTab/index.tsx | 6 + .../omnibox/browser/brave_omnibox_client.cc | 10 -- .../omnibox/browser/brave_omnibox_client.h | 19 --- components/omnibox/browser/sources.gni | 2 - ...r-ui-omnibox-chrome_omnibox_client.h.patch | 13 +- .../components-omnibox-browser-BUILD.gn.patch | 8 + ui/webui/resources/BUILD.gn | 4 + 25 files changed, 648 insertions(+), 56 deletions(-) create mode 100644 browser/ui/brave_ui_features.cc create mode 100644 browser/ui/brave_ui_features.h create mode 100644 chromium_src/chrome/browser/ui/webui/realbox/realbox_handler.cc create mode 100644 chromium_src/components/omnibox/browser/omnibox_client.cc create mode 100644 chromium_src/components/omnibox/browser/omnibox_client.h create mode 100644 components/brave_new_tab_ui/components/search/SearchBox.tsx create mode 100644 components/brave_new_tab_ui/components/search/SearchDialog.tsx create mode 100644 components/brave_new_tab_ui/components/search/SearchPlaceholder.tsx create mode 100644 components/brave_new_tab_ui/components/search/SearchResult.tsx create mode 100644 components/brave_new_tab_ui/components/search/SearchResults.tsx delete mode 100644 components/omnibox/browser/brave_omnibox_client.cc delete mode 100644 components/omnibox/browser/brave_omnibox_client.h diff --git a/browser/about_flags.cc b/browser/about_flags.cc index 81ee3037ae4..2989c86f0cb 100644 --- a/browser/about_flags.cc +++ b/browser/about_flags.cc @@ -12,6 +12,7 @@ #include "brave/browser/brave_features_internal_names.h" #include "brave/browser/ethereum_remote_client/buildflags/buildflags.h" #include "brave/browser/ethereum_remote_client/features.h" +#include "brave/browser/ui/brave_ui_features.h" #include "brave/browser/ui/tabs/features.h" #include "brave/components/ai_chat/core/common/buildflags/buildflags.h" #include "brave/components/brave_ads/browser/ad_units/notification_ad/custom_notification_ad_feature.h" @@ -500,6 +501,13 @@ FEATURE_VALUE_TYPE( \ ntp_background_images::features::kBraveNTPBrandedWallpaperDemo), \ }, \ + { \ + "brave-ntp-search-widget", \ + "Brave Search Widget on the NTP", \ + "Enables searching directly from the New Tab Page", \ + kOsDesktop, \ + FEATURE_VALUE_TYPE(features::kBraveNtpSearchWidget), \ + }, \ { \ "brave-adblock-cname-uncloaking", \ "Enable CNAME uncloaking", \ diff --git a/browser/brave_content_browser_client.cc b/browser/brave_content_browser_client.cc index 057da936e47..b7825f92135 100644 --- a/browser/brave_content_browser_client.cc +++ b/browser/brave_content_browser_client.cc @@ -36,6 +36,7 @@ #include "brave/browser/profiles/brave_renderer_updater_factory.h" #include "brave/browser/profiles/profile_util.h" #include "brave/browser/skus/skus_service_factory.h" +#include "brave/browser/ui/brave_ui_features.h" #include "brave/browser/ui/webui/skus_internals_ui.h" #include "brave/components/ai_chat/core/common/buildflags/buildflags.h" #include "brave/components/brave_federated/features.h" @@ -236,6 +237,7 @@ using extensions::ChromeContentBrowserClientExtensionsPart; #include "brave/components/brave_shields/core/common/cookie_list_opt_in.mojom.h" #include "brave/components/commands/common/commands.mojom.h" #include "brave/components/commands/common/features.h" +#include "components/omnibox/browser/omnibox.mojom.h" #endif #if BUILDFLAG(ENABLE_PLAYLIST) @@ -627,6 +629,15 @@ void BraveContentBrowserClient::RegisterWebUIInterfaceBrokers( } #if !BUILDFLAG(IS_ANDROID) + auto ntp_registration = + registry.ForWebUI() + .Add() + .Add(); + + if (base::FeatureList::IsEnabled(features::kBraveNtpSearchWidget)) { + ntp_registration.Add(); + } + if (base::FeatureList::IsEnabled( brave_news::features::kBraveNewsFeedUpdate)) { registry.ForWebUI() @@ -825,22 +836,11 @@ void BraveContentBrowserClient::RegisterBrowserInterfaceBindersForFrame( #endif #endif -// Brave News -#if !BUILDFLAG(IS_ANDROID) - content::RegisterWebUIControllerInterfaceBinder< - brave_news::mojom::BraveNewsController, BraveNewTabUI>(map); -#endif - #if BUILDFLAG(ENABLE_SPEEDREADER) && !BUILDFLAG(IS_ANDROID) content::RegisterWebUIControllerInterfaceBinder< speedreader::mojom::ToolbarFactory, SpeedreaderToolbarUI>(map); #endif -#if !BUILDFLAG(IS_ANDROID) - content::RegisterWebUIControllerInterfaceBinder< - brave_new_tab_page::mojom::PageHandlerFactory, BraveNewTabUI>(map); -#endif - #if BUILDFLAG(ENABLE_PLAYLIST) map->Add( base::BindRepeating(&BindPlaylistMediaHandler)); diff --git a/browser/ui/BUILD.gn b/browser/ui/BUILD.gn index d4c6c7afce6..dfb56712586 100644 --- a/browser/ui/BUILD.gn +++ b/browser/ui/BUILD.gn @@ -36,6 +36,8 @@ source_set("ui") { public_deps = [] sources = [ + "brave_ui_features.cc", + "brave_ui_features.h", "webui/brave_adblock_internals_ui.cc", "webui/brave_adblock_internals_ui.h", "webui/brave_adblock_ui.cc", diff --git a/browser/ui/brave_ui_features.cc b/browser/ui/brave_ui_features.cc new file mode 100644 index 00000000000..5f708dba695 --- /dev/null +++ b/browser/ui/brave_ui_features.cc @@ -0,0 +1,14 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/browser/ui/brave_ui_features.h" + +namespace features { + +BASE_FEATURE(kBraveNtpSearchWidget, + "BraveNtpSearchWidget", + base::FEATURE_DISABLED_BY_DEFAULT); + +} diff --git a/browser/ui/brave_ui_features.h b/browser/ui/brave_ui_features.h new file mode 100644 index 00000000000..1e9cb62d0dc --- /dev/null +++ b/browser/ui/brave_ui_features.h @@ -0,0 +1,17 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_BROWSER_UI_BRAVE_UI_FEATURES_H_ +#define BRAVE_BROWSER_UI_BRAVE_UI_FEATURES_H_ + +#include "base/feature_list.h" + +namespace features { + +BASE_DECLARE_FEATURE(kBraveNtpSearchWidget); + +} + +#endif // BRAVE_BROWSER_UI_BRAVE_UI_FEATURES_H_ diff --git a/browser/ui/webui/new_tab_page/brave_new_tab_ui.cc b/browser/ui/webui/new_tab_page/brave_new_tab_ui.cc index 75ec929a6c9..4d182c74d30 100644 --- a/browser/ui/webui/new_tab_page/brave_new_tab_ui.cc +++ b/browser/ui/webui/new_tab_page/brave_new_tab_ui.cc @@ -5,6 +5,7 @@ #include "brave/browser/ui/webui/new_tab_page/brave_new_tab_ui.h" +#include #include #include "base/check.h" @@ -12,6 +13,7 @@ #include "brave/browser/brave_news/brave_news_controller_factory.h" #include "brave/browser/new_tab/new_tab_shows_options.h" #include "brave/browser/ntp_background/brave_ntp_custom_background_service_factory.h" +#include "brave/browser/ui/brave_ui_features.h" #include "brave/browser/ui/webui/brave_webui_source.h" #include "brave/browser/ui/webui/new_tab_page/brave_new_tab_message_handler.h" #include "brave/browser/ui/webui/new_tab_page/brave_new_tab_page_handler.h" @@ -25,11 +27,13 @@ #include "chrome/browser/search_engines/template_url_service_factory.h" #include "chrome/common/pref_names.h" #include "components/grit/brave_components_resources.h" +#include "components/omnibox/browser/omnibox.mojom.h" #include "components/strings/grit/components_strings.h" #include "content/public/browser/navigation_entry.h" #include "content/public/browser/url_data_source.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_ui_data_source.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" using ntp_background_images::NTPCustomImagesSource; @@ -78,6 +82,10 @@ BraveNewTabUI::BraveNewTabUI(content::WebUI* web_ui, const std::string& name) "featureFlagBraveNewsFeedV2Enabled", base::FeatureList::IsEnabled(brave_news::features::kBraveNewsFeedUpdate)); + source->AddBoolean( + "featureFlagSearchWidget", + base::FeatureList::IsEnabled(features::kBraveNtpSearchWidget)); + web_ui->AddMessageHandler(base::WrapUnique( BraveNewTabMessageHandler::Create(source, profile, was_restored))); web_ui->AddMessageHandler( @@ -116,6 +124,16 @@ void BraveNewTabUI::BindInterface( page_factory_receiver_.Bind(std::move(pending_receiver)); } +void BraveNewTabUI::BindInterface( + mojo::PendingReceiver pending_page_handler) { + auto* profile = Profile::FromWebUI(web_ui()); + DCHECK(profile); + + realbox_handler_ = std::make_unique( + std::move(pending_page_handler), profile, web_ui()->GetWebContents(), + nullptr, nullptr); +} + void BraveNewTabUI::CreatePageHandler( mojo::PendingRemote pending_page, mojo::PendingReceiver diff --git a/browser/ui/webui/new_tab_page/brave_new_tab_ui.h b/browser/ui/webui/new_tab_page/brave_new_tab_ui.h index 0b2f6e5e8fa..708095b3eb7 100644 --- a/browser/ui/webui/new_tab_page/brave_new_tab_ui.h +++ b/browser/ui/webui/new_tab_page/brave_new_tab_ui.h @@ -11,6 +11,8 @@ #include "brave/components/brave_new_tab_ui/brave_new_tab_page.mojom.h" #include "brave/components/brave_news/common/brave_news.mojom.h" +#include "chrome/browser/ui/webui/realbox/realbox_handler.h" +#include "components/omnibox/browser/omnibox.mojom.h" #include "content/public/browser/web_ui_controller.h" #include "mojo/public/cpp/bindings/pending_receiver.h" #include "mojo/public/cpp/bindings/pending_remote.h" @@ -40,6 +42,9 @@ class BraveNewTabUI : public ui::MojoWebUIController, mojo::PendingReceiver pending_receiver); + void BindInterface( + mojo::PendingReceiver pending_page_handler); + private: // new_tab_page::mojom::PageHandlerFactory: void CreatePageHandler( @@ -48,6 +53,7 @@ class BraveNewTabUI : public ui::MojoWebUIController, pending_page_handler) override; std::unique_ptr page_handler_; + std::unique_ptr realbox_handler_; mojo::Receiver page_factory_receiver_; diff --git a/chromium_src/chrome/browser/ui/webui/realbox/realbox_handler.cc b/chromium_src/chrome/browser/ui/webui/realbox/realbox_handler.cc new file mode 100644 index 00000000000..5afdb369373 --- /dev/null +++ b/chromium_src/chrome/browser/ui/webui/realbox/realbox_handler.cc @@ -0,0 +1,28 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "chrome/browser/ui/webui/realbox/realbox_handler.h" + +#include "brave/components/vector_icons/vector_icons.h" +#include "components/omnibox/browser/vector_icons.h" + +namespace { +constexpr char kLeoWindowTabNewIconResourceName[] = + "//resources/brave-icons/window-tab-new.svg"; +} + +// This patches |PedalVectorIconToResourceName| to include the additional +// omnibox icons we add in Brave. Unfortunately, the method is used inside the +// file, so we can't just override the method. There is only one usage of +// kSwitchIcon in the file, so we add additional statements from it. +#define kSwitchIcon kSwitchIcon.name) {} \ + if (icon.name == kLeoWindowTabNewIcon.name) { \ + return kLeoWindowTabNewIconResourceName; \ + } \ + if (icon.name == omnibox::kSwitchIcon + +#include "src/chrome/browser/ui/webui/realbox/realbox_handler.cc" + +#undef kSwitchIcon diff --git a/chromium_src/components/omnibox/browser/omnibox_client.cc b/chromium_src/components/omnibox/browser/omnibox_client.cc new file mode 100644 index 00000000000..ea57afa7fc1 --- /dev/null +++ b/chromium_src/components/omnibox/browser/omnibox_client.cc @@ -0,0 +1,10 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "src/components/omnibox/browser/omnibox_client.cc" + +bool OmniboxClient::IsAutocompleteEnabled() const { + return true; +} diff --git a/chromium_src/components/omnibox/browser/omnibox_client.h b/chromium_src/components/omnibox/browser/omnibox_client.h new file mode 100644 index 00000000000..3ee2955386e --- /dev/null +++ b/chromium_src/components/omnibox/browser/omnibox_client.h @@ -0,0 +1,17 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_CHROMIUM_SRC_COMPONENTS_OMNIBOX_BROWSER_OMNIBOX_CLIENT_H_ +#define BRAVE_CHROMIUM_SRC_COMPONENTS_OMNIBOX_BROWSER_OMNIBOX_CLIENT_H_ + +#define CurrentPageExists \ + IsAutocompleteEnabled() const; \ + virtual bool CurrentPageExists + +#include "src/components/omnibox/browser/omnibox_client.h" // IWYU pragma: export + +#undef CurrentPageExists + +#endif // BRAVE_CHROMIUM_SRC_COMPONENTS_OMNIBOX_BROWSER_OMNIBOX_CLIENT_H_ diff --git a/chromium_src/components/omnibox/browser/omnibox_controller.cc b/chromium_src/components/omnibox/browser/omnibox_controller.cc index 5fb20fbf789..d13fe064190 100644 --- a/chromium_src/components/omnibox/browser/omnibox_controller.cc +++ b/chromium_src/components/omnibox/browser/omnibox_controller.cc @@ -5,16 +5,13 @@ #include "components/omnibox/browser/omnibox_controller.h" -#include "brave/components/omnibox/browser/brave_omnibox_client.h" - #define StartAutocomplete StartAutocomplete_ChromiumImpl #include "src/components/omnibox/browser/omnibox_controller.cc" #undef StartAutocomplete void OmniboxController::StartAutocomplete( const AutocompleteInput& input) const { - auto* client = static_cast(client_.get()); - if (!client->IsAutocompleteEnabled()) { + if (!client_->IsAutocompleteEnabled()) { ClearPopupKeywordMode(); return; } diff --git a/components/brave_new_tab_ui/BUILD.gn b/components/brave_new_tab_ui/BUILD.gn index 9dc4528abef..651e52b7326 100644 --- a/components/brave_new_tab_ui/BUILD.gn +++ b/components/brave_new_tab_ui/BUILD.gn @@ -14,6 +14,8 @@ transpile_web_ui("brave_new_tab_ui") { deps = [ ":mojom_js", "//brave/components/brave_news/common:mojom_js", + "//components/omnibox/browser:mojo_bindings", + "//components/omnibox/browser:mojo_bindings_js", ] resource_name = "brave_new_tab" } diff --git a/components/brave_new_tab_ui/components/default/page/index.tsx b/components/brave_new_tab_ui/components/default/page/index.tsx index 9fbfe68cb75..4879a83ba26 100644 --- a/components/brave_new_tab_ui/components/default/page/index.tsx +++ b/components/brave_new_tab_ui/components/default/page/index.tsx @@ -7,6 +7,7 @@ import * as React from 'react' import styled, { createGlobalStyle, css } from 'styled-components' import { requestAnimationFrameThrottle } from '../../../../common/throttle' import { defaultState } from '../../../storage/new_tab_storage' +import { spacing } from '@brave/leo/tokens/css' const breakpointLargeBlocks = '980px' const breakpointEveryBlock = '870px' @@ -278,6 +279,10 @@ export const GridItemNavigationBraveNews = styled('div') <{}>` [data-show-news-prompt] & { bottom: 120px; } + + display: flex; + flex-direction: column; + gap: ${spacing.l}; ` export const Footer = styled('footer') <{}>` diff --git a/components/brave_new_tab_ui/components/search/SearchBox.tsx b/components/brave_new_tab_ui/components/search/SearchBox.tsx new file mode 100644 index 00000000000..1c2cfe98b69 --- /dev/null +++ b/components/brave_new_tab_ui/components/search/SearchBox.tsx @@ -0,0 +1,124 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +import Flex from '$web-common/Flex'; +import Dropdown from '@brave/leo/react/dropdown'; +import Icon from '@brave/leo/react/icon'; +import Input from '@brave/leo/react/input'; +import { icon, spacing } from '@brave/leo/tokens/css'; +import { stringToMojoString16 } from 'chrome://resources/js/mojo_type_util.js'; +import { AutocompleteResult, OmniboxPopupSelection, PageHandler, PageHandlerRemote, PageInterface, PageReceiver } from 'gen/components/omnibox/browser/omnibox.mojom.m'; +import * as React from 'react'; +import styled from 'styled-components'; + +interface SearchEngine { + name: string, + icon: string +} +const searchEngines: SearchEngine[] = [{ + name: 'Brave', + icon: 'brave-icon-search-color' +}, { + name: 'Google', + icon: 'google-color' +}, { + name: 'DuckDuckGo', + icon: 'duckduckgo-color' +}, { + name: 'Bing', + icon: 'bing-color' +}] + +const SearchInput = styled(Input)` + --leo-control-padding: 6px; + + display: inline-block; + width: 540px; +` + +const EnginePicker = styled(Dropdown)` +` + +const SelectedIcon = styled(Icon)` + --leo-icon-size: ${icon.l}; +` + +const SearchIconContainer = styled.div` + padding-right: ${spacing.m}; +` + +const Option = styled.div` + display: flex; + gap: ${spacing.m}; +` + +export const omniboxController: PageHandlerRemote = PageHandler.getRemote(); +(window as any).omnibox = omniboxController; + +class SearchPage implements PageInterface { + private receiver = new PageReceiver(this) + private result: AutocompleteResult | undefined + private resultListeners: Array<(result?: AutocompleteResult) => void> = [] + private selectionListeners: Array<(selection: OmniboxPopupSelection) => void> = [] + + constructor() { + omniboxController.setPage(this.receiver.$.bindNewPipeAndPassRemote()) + } + + addResultListener(listener: (result?: AutocompleteResult) => void) { + this.resultListeners.push(listener) + if (this.result) listener(this.result) + } + + removeResultListener(listener: (result?: AutocompleteResult) => void) { + this.resultListeners = this.resultListeners.filter(r => r !== listener) + } + + addSelectionListener(listener: (selection: OmniboxPopupSelection) => void) { + this.selectionListeners.push(listener) + } + + removeSelectionListener(listener: (selection: OmniboxPopupSelection) => void) { + this.selectionListeners = this.selectionListeners.filter(s => s !== listener) + } + + autocompleteResultChanged(result: AutocompleteResult) { + this.result = result; + for (const listener of this.resultListeners) listener(result) + } + + updateSelection(selection: OmniboxPopupSelection) { + for (const listener of this.selectionListeners) listener(selection) + } +} + +export const search = new SearchPage() + +export default function SearchBox() { + const [query, setQuery] = React.useState('') + const [searchEngine, setSearchEngine] = React.useState('Brave') + + React.useEffect(() => { + omniboxController.queryAutocomplete(stringToMojoString16(query), false); + }, [query]) + + const searchInput = React.useRef() + return setQuery(e.detail.value)} placeholder="Search the web privately"> + + setSearchEngine(e.detail.value)}> +
+ s.name === searchEngine)?.icon} /> +
+ {searchEngines.map(s => + + )} +
+
+ + + +
+} diff --git a/components/brave_new_tab_ui/components/search/SearchDialog.tsx b/components/brave_new_tab_ui/components/search/SearchDialog.tsx new file mode 100644 index 00000000000..e29b886cf08 --- /dev/null +++ b/components/brave_new_tab_ui/components/search/SearchDialog.tsx @@ -0,0 +1,140 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +import * as React from 'react'; +import styled, { keyframes } from 'styled-components' +import SearchBox from './SearchBox'; +import SearchResults from './SearchResults'; +import { spacing } from '@brave/leo/tokens/css'; +import { createPortal } from 'react-dom'; + +interface Props { + onClose: () => void + offsetY: number +} + +const duration = '0.12s' + +const enterDialog = keyframes` + from { + transform: translateY(calc(var(--offset-y) - var(--margin-top))); + } + + to { + transform: translateY(0); + } +` + +const exitDialog = keyframes` + from { + transform: translateY(0); + } + + to { + transform: translateY(calc(var(--offset-y) - var(--margin-top))); + } +` + +const enterBackdrop = keyframes` + from { + background: transparent; + backdrop-filter: blur(0); + } + + to { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(64px); + } +` + +const exitBackdrop = keyframes` + from { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(64px); + } + + to { + background: transparent; + backdrop-filter: blur(0); + } +` + +const Dialog = styled.dialog<{ offsetY: number }>` + --margin-top: ${spacing['9Xl']}; + --offset-y: ${p => p.offsetY}px; + + outline: none; + border: none; + background: transparent; + margin-top: var(--margin-top); + padding: 2px; + + animation: ${enterDialog} ${duration} ease-in-out; + + &::backdrop { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(64px); + animation: ${enterBackdrop} ${duration} ease-in-out; + } + + &.closing { + animation: ${exitDialog} ${duration} ease-in-out; + + &::backdrop { + animation: ${exitBackdrop} ${duration} ease-in-out; + } + } +` + +export default function Component(props: Props) { + const ref = React.useRef(null) + + React.useEffect(() => { + ref.current?.showModal() + }, []) + + const doClose = React.useCallback(() => { + const el = ref.current! + el.addEventListener('animationend', () => props.onClose()); + el.classList.add('closing'); + }, []) + + React.useEffect(() => { + const el = ref.current + if (!el) return + + const closeHandler = (e: CloseEvent) => { + e.preventDefault() + } + + el.addEventListener('close', closeHandler) + el.addEventListener('cancel', e => e.preventDefault()) + return () => { + el.removeEventListener('close', closeHandler) + el.removeEventListener('cancel', closeHandler) + } + }, [props.onClose]) + + React.useEffect(() => { + setTimeout(() => { + (ref.current?.querySelector('leo-input') as HTMLElement)?.focus() + }) + }, []) + return createPortal( { + const rect = e.currentTarget.getBoundingClientRect(); + + const clickedInDialog = ( + rect.top <= e.clientY && + e.clientY <= rect.top + rect.height && + rect.left <= e.clientX && + e.clientX <= rect.left + rect.width + ); + if (!clickedInDialog) { + doClose() + } + }}> + + + , document.body) +} diff --git a/components/brave_new_tab_ui/components/search/SearchPlaceholder.tsx b/components/brave_new_tab_ui/components/search/SearchPlaceholder.tsx new file mode 100644 index 00000000000..b79c86bd5fd --- /dev/null +++ b/components/brave_new_tab_ui/components/search/SearchPlaceholder.tsx @@ -0,0 +1,21 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +import * as React from 'react'; +import SearchBox from './SearchBox'; +import SearchDialog from './SearchDialog'; + +export default function SearchPlaceholder() { + const [open, setOpen] = React.useState(false) + const [boxPos, setBoxPos] = React.useState(0) + return <> + {!open &&
{ + setOpen(true) + setBoxPos(e.currentTarget.getBoundingClientRect().y) + }}> + +
} + {open && setOpen(false)} />} + +} diff --git a/components/brave_new_tab_ui/components/search/SearchResult.tsx b/components/brave_new_tab_ui/components/search/SearchResult.tsx new file mode 100644 index 00000000000..d92eec1b408 --- /dev/null +++ b/components/brave_new_tab_ui/components/search/SearchResult.tsx @@ -0,0 +1,92 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +import { color, font, radius, spacing } from '@brave/leo/tokens/css'; +import { mojoString16ToString } from 'chrome://resources/js/mojo_type_util.js'; +import { AutocompleteMatch } from 'gen/components/omnibox/browser/omnibox.mojom.m'; +import * as React from 'react'; +import styled from 'styled-components'; +import Flex from '../../../common/Flex'; +import { omniboxController } from './SearchBox'; + +interface Props { + match: AutocompleteMatch + line: number + selected: boolean +} + +const Container = styled.a` + padding: ${spacing.s} ${spacing['2Xl']}; + + display: flex; + flex-direction: row; + align-items: center; + gap: ${spacing.l}; + + text-decoration: none; + + &[aria-selected=true] { + background: ${color.container.interactive}; + } + + &:hover { + background: ${color.container.highlight}; + } +` + +const IconContainer = styled.div` + border-radius: ${radius.m}; + width: 32px; + height: 32px; + + background: ${color.container.highlight}; + + display: flex; + align-items: center; + justify-content: center; + + flex-shrink: 0; +` + +const FavIcon = styled.img` + width: 20px; + height: 20px; +` + +const Content = styled.span` + font: ${font.large.regular}; + color: ${color.text.secondary}; +` + +const Description = styled.span` + font: ${font.small.regular}; + color: ${color.text.tertiary}; +` + +const Hint = styled.span` + color: ${color.text.interactive}; +` + +export default function SearchResult({ match, line, selected }: Props) { + const contents = mojoString16ToString(match.swapContentsAndDescription ? match.description : match.contents) + const description = mojoString16ToString(match.swapContentsAndDescription ? match.contents : match.description) + + const hint = description && match.destinationUrl.url + ? description + : '' + + const subtitle = match.destinationUrl.url || description + return { + e.preventDefault() + omniboxController.openAutocompleteMatch(line, match.destinationUrl, true, e.button, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey) + }}> + + + + + {contents}{hint ? ` - ${hint}` : ''} + {subtitle} + + +} diff --git a/components/brave_new_tab_ui/components/search/SearchResults.tsx b/components/brave_new_tab_ui/components/search/SearchResults.tsx new file mode 100644 index 00000000000..b7720e39c6e --- /dev/null +++ b/components/brave_new_tab_ui/components/search/SearchResults.tsx @@ -0,0 +1,111 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +import { color, radius, spacing } from '@brave/leo/tokens/css' +import { AutocompleteResult, OmniboxPopupSelection } from 'gen/components/omnibox/browser/omnibox.mojom.m' +import * as React from 'react' +import styled from 'styled-components' +import { omniboxController, search } from './SearchBox' +import SearchResult from './SearchResult' + +const Container = styled.div` + margin-top: ${spacing.m}; + background: ${color.container.background}; + border-radius: ${radius.m}; + padding: ${spacing.m} 0; + gap: ${spacing.m}; + display: flex; + flex-direction: column; + + width: 540px; + overflow: hidden; + text-wrap: nowrap; +` + +export default function SearchResults() { + const [result, setResult] = React.useState() + const [selectedMatch, setSelectedMatch] = React.useState(); + + React.useEffect(() => { + const listener = (result?: AutocompleteResult) => { + setResult(result) + setSelectedMatch(prev => { + if (!result) return undefined + + if (!prev) { + const defaultMatchIndex = result.matches.findIndex(r => r.allowedToBeDefaultMatch) + if (defaultMatchIndex !== -1) return defaultMatchIndex + + return undefined + } + + if (prev >= result.matches.length) { + return result.matches.length - 1 + } + + return prev + }) + } + search.addResultListener(listener) + return () => search.removeResultListener(listener) + }, []) + + React.useEffect(() => { + const listener = (selection: OmniboxPopupSelection) => { + console.log("Set selection:", selection) + setSelectedMatch(selection.line) + } + search.addSelectionListener(listener) + return () => { + search.removeSelectionListener(listener) + } + }, []) + + React.useEffect(() => { + const keyHandler = (e: KeyboardEvent) => { + const handledKeys = ['ArrowUp', 'ArrowDown'] + if (!handledKeys.includes(e.key)) { + return + } + + e.preventDefault() + + const direction = e.key === 'ArrowUp' ? -1 : 1 + setSelectedMatch(s => { + if (!result || result.matches.length === 0) return undefined + + const start = s ?? -1 + const next = start + direction + if (next < 0) return (result.matches.length - 1) + if (next >= result.matches.length) return 0 + return next + }) + } + document.addEventListener('keydown', keyHandler) + return () => { + document.removeEventListener('keydown', keyHandler) + } + + }, [result]) + + React.useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.key !== 'Enter') return + + e.preventDefault() + + const match = result?.matches[selectedMatch!] + if (!match) return + + omniboxController.openAutocompleteMatch(selectedMatch!, match.destinationUrl, true, 0, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey); + } + document.addEventListener('keydown', handler) + return () => { + document.removeEventListener('keydown', handler) + } + }, [result, selectedMatch]) + return result && result?.matches.length ? + {result?.matches.map((r, i) => )} + : null +} diff --git a/components/brave_new_tab_ui/containers/newTab/index.tsx b/components/brave_new_tab_ui/containers/newTab/index.tsx index 4b0d7897a1d..3485d2b9b0d 100644 --- a/components/brave_new_tab_ui/containers/newTab/index.tsx +++ b/components/brave_new_tab_ui/containers/newTab/index.tsx @@ -23,6 +23,7 @@ import Stats from './stats' // Helpers import { getLocale } from '$web-common/locale' import VisibilityTimer from '$web-common/visibilityTimer' +import { loadTimeData } from '$web-common/loadTimeData' import isReadableOnBackground from '../../helpers/colorUtil' // Types @@ -46,6 +47,7 @@ import * as style from './style' import { defaultState } from '../../storage/new_tab_storage' const BraveNewsPeek = React.lazy(() => import('../../../brave_news/browser/resources/Peek')) +const SearchWidget = React.lazy(() => import('../../components/search/SearchPlaceholder')) interface Props { newTabData: NewTab.State @@ -702,6 +704,10 @@ class NewTabPage extends React.Component { {newTabData.showToday && + {loadTimeData.getBoolean('featureFlagSearchWidget') + && + + } {defaultState.featureFlagBraveNewsFeedV2Enabled ? diff --git a/components/omnibox/browser/brave_omnibox_client.cc b/components/omnibox/browser/brave_omnibox_client.cc deleted file mode 100644 index 3536a3167f2..00000000000 --- a/components/omnibox/browser/brave_omnibox_client.cc +++ /dev/null @@ -1,10 +0,0 @@ -/* Copyright (c) 2019 The Brave Authors. All rights reserved. - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "brave/components/omnibox/browser/brave_omnibox_client.h" - -bool BraveOmniboxClient::IsAutocompleteEnabled() const { - return true; -} diff --git a/components/omnibox/browser/brave_omnibox_client.h b/components/omnibox/browser/brave_omnibox_client.h deleted file mode 100644 index 5d26229de36..00000000000 --- a/components/omnibox/browser/brave_omnibox_client.h +++ /dev/null @@ -1,19 +0,0 @@ -/* Copyright (c) 2019 The Brave Authors. All rights reserved. - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#ifndef BRAVE_COMPONENTS_OMNIBOX_BROWSER_BRAVE_OMNIBOX_CLIENT_H_ -#define BRAVE_COMPONENTS_OMNIBOX_BROWSER_BRAVE_OMNIBOX_CLIENT_H_ - -#include "components/omnibox/browser/omnibox_client.h" - -class BraveOmniboxClient : public OmniboxClient { - public: - virtual bool IsAutocompleteEnabled() const; - - protected: - ~BraveOmniboxClient() override {} -}; - -#endif // BRAVE_COMPONENTS_OMNIBOX_BROWSER_BRAVE_OMNIBOX_CLIENT_H_ diff --git a/components/omnibox/browser/sources.gni b/components/omnibox/browser/sources.gni index 2ca2272dded..ab9bc935e1d 100644 --- a/components/omnibox/browser/sources.gni +++ b/components/omnibox/browser/sources.gni @@ -15,8 +15,6 @@ brave_components_omnibox_browser_sources = [ "//brave/components/omnibox/browser/brave_history_url_provider.h", "//brave/components/omnibox/browser/brave_local_history_zero_suggest_provider.cc", "//brave/components/omnibox/browser/brave_local_history_zero_suggest_provider.h", - "//brave/components/omnibox/browser/brave_omnibox_client.cc", - "//brave/components/omnibox/browser/brave_omnibox_client.h", "//brave/components/omnibox/browser/brave_omnibox_prefs.cc", "//brave/components/omnibox/browser/brave_omnibox_prefs.h", "//brave/components/omnibox/browser/brave_search_provider.cc", diff --git a/patches/chrome-browser-ui-omnibox-chrome_omnibox_client.h.patch b/patches/chrome-browser-ui-omnibox-chrome_omnibox_client.h.patch index 8e916732131..d132b4c4984 100644 --- a/patches/chrome-browser-ui-omnibox-chrome_omnibox_client.h.patch +++ b/patches/chrome-browser-ui-omnibox-chrome_omnibox_client.h.patch @@ -1,20 +1,13 @@ diff --git a/chrome/browser/ui/omnibox/chrome_omnibox_client.h b/chrome/browser/ui/omnibox/chrome_omnibox_client.h -index db1e94ddecdefe05ef45d090722eeee3e5eb0105..d79e108c47a13155f0f0e558564d1baf29b7606c 100644 +index db1e94ddecdef..4ad6d835f8d50 100644 --- a/chrome/browser/ui/omnibox/chrome_omnibox_client.h +++ b/chrome/browser/ui/omnibox/chrome_omnibox_client.h -@@ -17,13 +17,14 @@ - #include "components/omnibox/browser/favicon_cache.h" - #include "components/omnibox/browser/omnibox.mojom-shared.h" - #include "components/omnibox/browser/omnibox_client.h" -+#include "brave/components/omnibox/browser/brave_omnibox_client.h" - - class Browser; - class GURL; +@@ -23,7 +23,7 @@ class GURL; class LocationBar; class Profile; -class ChromeOmniboxClient final : public OmniboxClient { -+class ChromeOmniboxClient : public BraveOmniboxClient { ++class ChromeOmniboxClient : public OmniboxClient { public: ChromeOmniboxClient(LocationBar* location_bar, Browser* browser, diff --git a/patches/components-omnibox-browser-BUILD.gn.patch b/patches/components-omnibox-browser-BUILD.gn.patch index 9db14bcc37f..2518e3f572e 100644 --- a/patches/components-omnibox-browser-BUILD.gn.patch +++ b/patches/components-omnibox-browser-BUILD.gn.patch @@ -10,3 +10,11 @@ index feb42d8d0a7f231d162085f0076092f0a022157a..c38fe2e842f9606aa0216651334ee6a7 configs += [ "//build/config/compiler:wexit_time_destructors" ] +@@ -929,6 +930,7 @@ fuzzer_test("search_suggestion_parser_fuzzer") { + + mojom("mojo_bindings") { + sources = [ "omnibox.mojom" ] ++ generate_legacy_js_bindings = true + webui_module_path = "/" + + public_deps = [ diff --git a/ui/webui/resources/BUILD.gn b/ui/webui/resources/BUILD.gn index f46dfdd30ac..db6735b39fe 100644 --- a/ui/webui/resources/BUILD.gn +++ b/ui/webui/resources/BUILD.gn @@ -153,9 +153,11 @@ leo_icons = [ "autoplay-on.svg", "backward.svg", "bar-chart.svg", + "bing-color.svg", "bluetooth-off.svg", "bluetooth.svg", "brave-icon-release-color.svg", + "brave-icon-search-color.svg", "browser-extensions.svg", "browser-ntp-widget.svg", "carat-down.svg", @@ -189,6 +191,7 @@ leo_icons = [ "discover.svg", "download-off.svg", "download.svg", + "duckduckgo-color.svg", "edit-box.svg", "edit-pencil.svg", "erase.svg", @@ -208,6 +211,7 @@ leo_icons = [ "font-size.svg", "forward-15.svg", "fullscreen-on.svg", + "google-color.svg", "graph.svg", "grid04.svg", "hamburger-menu.svg",