Add action and close buttons to toast view
Screenshot: https://screenshot.googleplex.com/7U6DZ86wNtYxJVo.png
controller will be added in a later CL
Low-Coverage-Reason: TESTS_IN_SEPARATE_CL end-to-end tests for the taost
Fixed: 358617589
Change-Id: Ifb0c828125eb40b1b561de1476d297f29e4e9f2d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5817683
Reviewed-by: Eshwar Stalin <[email protected]>
Code-Coverage: [email protected] <[email protected]>
Commit-Queue: Charles Meng <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1351016}
diff --git a/chrome/browser/ui/toasts/api/toast_specification.h b/chrome/browser/ui/toasts/api/toast_specification.h
index c79dde4..783b13a 100644
--- a/chrome/browser/ui/toasts/api/toast_specification.h
+++ b/chrome/browser/ui/toasts/api/toast_specification.h
@@ -63,7 +63,7 @@
std::optional<int> action_button_string_id() const {
return action_button_string_id_;
}
- base::RepeatingClosure action_button_callback() {
+ base::RepeatingClosure action_button_callback() const {
return action_button_closure_;
}
ui::SimpleMenuModel* menu_model() const { return menu_model_.get(); }
diff --git a/chrome/browser/ui/toasts/toast_controller.cc b/chrome/browser/ui/toasts/toast_controller.cc
index a7fd685..298ea34f 100644
--- a/chrome/browser/ui/toasts/toast_controller.cc
+++ b/chrome/browser/ui/toasts/toast_controller.cc
@@ -92,6 +92,7 @@
void ToastController::OnWidgetDestroyed(views::Widget* widget) {
current_toast_params_ = std::nullopt;
+ toast_ = nullptr;
toast_observer_.Reset();
toast_widget_ = nullptr;
toast_close_timer_.Stop();
@@ -127,12 +128,9 @@
return;
}
+ CHECK(toast_);
CHECK(toast_widget_);
- // TODO(crbug.com/358615317): Make the toast animate out and then complete
- // the rest of the logic synchronously afterwards.
- // TODO(crbug.com/358610872): Log toast close reason metric and potentially
- // integrate with Widget::CloseReason.
- toast_widget_->Close();
+ toast_->Close(views::Widget::ClosedReason::kUnspecified);
}
void ToastController::CreateToast(const ToastSpecification* spec) {
@@ -146,7 +144,15 @@
browser_window_interface_->TopContainer(),
FormatString(spec->body_string_id(),
current_toast_params_->body_string_replacement_params_),
- spec->icon());
+ spec->icon(), spec->has_close_button());
+ if (spec->action_button_string_id().has_value()) {
+ toast->AddActionButton(
+ FormatString(
+ spec->action_button_string_id().value(),
+ current_toast_params_->action_button_string_replacement_params_),
+ spec->action_button_callback());
+ }
+ toast_ = toast.get();
toast_widget_ =
views::BubbleDialogDelegateView::CreateBubble(std::move(toast));
toast_observer_.Observe(toast_widget_);
diff --git a/chrome/browser/ui/toasts/toast_controller.h b/chrome/browser/ui/toasts/toast_controller.h
index 8fa974bd..d6c6321 100644
--- a/chrome/browser/ui/toasts/toast_controller.h
+++ b/chrome/browser/ui/toasts/toast_controller.h
@@ -19,6 +19,10 @@
class ToastSpecification;
enum class ToastId;
+namespace toasts {
+class ToastView;
+}
+
namespace views {
class Widget;
}
@@ -58,6 +62,7 @@
void ShowToast(ToastParams params);
void CreateToast(const ToastSpecification*);
virtual void CloseToast();
+ void OnToastClosed();
std::u16string FormatString(int string_id,
std::vector<std::u16string> replacement);
@@ -71,6 +76,7 @@
base::ScopedObservation<views::Widget, views::WidgetObserver> toast_observer_{
this};
+ raw_ptr<toasts::ToastView> toast_;
raw_ptr<views::Widget> toast_widget_;
};
diff --git a/chrome/browser/ui/toasts/toast_view.cc b/chrome/browser/ui/toasts/toast_view.cc
index 5efa47f..8e2c7e8 100644
--- a/chrome/browser/ui/toasts/toast_view.cc
+++ b/chrome/browser/ui/toasts/toast_view.cc
@@ -9,9 +9,17 @@
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
+#include "components/strings/grit/components_strings.h"
+#include "components/vector_icons/vector_icons.h"
#include "ui/base/interaction/element_identifier.h"
+#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
+#include "ui/views/accessibility/view_accessibility.h"
+#include "ui/views/controls/button/image_button.h"
+#include "ui/views/controls/button/image_button_factory.h"
+#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/view_class_properties.h"
+#include "ui/views/widget/widget.h"
#include "ui/views/window/dialog_delegate.h"
namespace toasts {
@@ -19,10 +27,12 @@
ToastView::ToastView(views::View* anchor_view,
const std::u16string& toast_text,
- const gfx::VectorIcon& icon)
+ const gfx::VectorIcon& icon,
+ bool has_close_button)
: BubbleDialogDelegateView(anchor_view, views::BubbleBorder::NONE),
toast_text_(toast_text),
- icon_(icon) {
+ icon_(icon),
+ has_close_button_(has_close_button) {
SetShowCloseButton(false);
DialogDelegate::SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone));
set_corner_radius(ChromeLayoutProvider::Get()->GetDistanceMetric(
@@ -34,6 +44,13 @@
ToastView::~ToastView() = default;
+void ToastView::AddActionButton(const std::u16string& action_button_text,
+ base::RepeatingClosure action_button_callback) {
+ has_action_button_ = true;
+ action_button_text_ = action_button_text;
+ action_button_callback_ = std::move(action_button_callback);
+}
+
void ToastView::Init() {
ChromeLayoutProvider* lp = ChromeLayoutProvider::Get();
SetLayoutManager(
@@ -46,6 +63,7 @@
label_ = AddChildView(std::make_unique<views::Label>(
toast_text_, views::style::CONTEXT_BUTTON, views::style::STYLE_PRIMARY));
+ label_->SetEnabledColorId(ui::kColorToastForeground);
label_->SetMultiLine(false);
label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
label_->SetAllowCharacterBreak(false);
@@ -53,16 +71,65 @@
label_->SetLineHeight(
lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_HEIGHT_CONTENT));
+ if (has_action_button_) {
+ label_->SetProperty(
+ views::kMarginsKey,
+ gfx::Insets::TLBR(
+ 0, 0, 0,
+ lp->GetDistanceMetric(
+ DISTANCE_TOAST_BUBBLE_BETWEEN_LABEL_ACTION_BUTTON_SPACING) -
+ lp->GetDistanceMetric(
+ DISTANCE_TOAST_BUBBLE_BETWEEN_CHILD_SPACING)));
+
+ action_button_ = AddChildView(std::make_unique<views::MdTextButton>(
+ action_button_callback_.Then(base::BindRepeating(
+ &ToastView::Close, base::Unretained(this),
+ views::Widget::ClosedReason::kCloseButtonClicked)),
+ action_button_text_));
+ action_button_->SetEnabledTextColorIds(ui::kColorToastButton);
+ action_button_->SetBgColorIdOverride(ui::kColorToastBackground);
+ action_button_->SetStrokeColorIdOverride(ui::kColorToastButton);
+ action_button_->SetPreferredSize(gfx::Size(
+ action_button_->GetPreferredSize().width(),
+ lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_HEIGHT_ACTION_BUTTON)));
+ action_button_->SetStyle(ui::ButtonStyle::kProminent);
+ action_button_->GetViewAccessibility().SetRole(ax::mojom::Role::kAlert);
+ }
+
+ if (has_close_button_) {
+ close_button_ = AddChildView(views::CreateVectorImageButtonWithNativeTheme(
+ base::BindRepeating(&ToastView::Close, base::Unretained(this),
+ views::Widget::ClosedReason::kCloseButtonClicked),
+ vector_icons::kCloseIcon,
+ lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_HEIGHT_CONTENT) -
+ lp->GetInsetsMetric(views::INSETS_VECTOR_IMAGE_BUTTON).height(),
+ ui::kColorToastForeground));
+ views::InstallCircleHighlightPathGenerator(close_button_);
+ close_button_->SetAccessibleName(l10n_util::GetStringUTF16(IDS_CLOSE));
+ }
+
// Height of the toast is set implicitly by adding margins depending on the
// height of the tallest child.
- int total_vertical_margins =
+ const int total_vertical_margins =
lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_HEIGHT) -
- lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_HEIGHT_CONTENT);
- int top_margin = total_vertical_margins / 2;
+ lp->GetDistanceMetric(action_button_
+ ? DISTANCE_TOAST_BUBBLE_HEIGHT_ACTION_BUTTON
+ : DISTANCE_TOAST_BUBBLE_HEIGHT_CONTENT);
+ const int top_margin = total_vertical_margins / 2;
+ const int right_margin = lp->GetDistanceMetric(
+ close_button_ ? DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_CLOSE_BUTTON
+ : action_button_ ? DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_ACTION_BUTTON
+ : DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_LABEL);
set_margins(gfx::Insets::TLBR(
top_margin, lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_MARGIN_LEFT),
- total_vertical_margins - top_margin,
- lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_LABEL)));
+ total_vertical_margins - top_margin, right_margin));
+}
+
+void ToastView::Close(views::Widget::ClosedReason reason) {
+ // TODO(crbug.com/358610872): Take in and log toast close reason metric. Then
+ // map to Widget close reason for Widget::CloseWithReason.
+ // TODO(crbug.com/358615317): Make the toast animate out.
+ GetWidget()->CloseWithReason(reason);
}
gfx::Rect ToastView::GetBubbleBounds() {
@@ -89,7 +156,6 @@
*icon_, color_provider->GetColor(ui::kColorToastForeground),
ChromeLayoutProvider::Get()->GetDistanceMetric(
DISTANCE_TOAST_BUBBLE_HEIGHT_CONTENT)));
- label_->SetEnabledColor(color_provider->GetColor(ui::kColorToastForeground));
}
std::u16string ToastView::GetAccessibleWindowTitle() const {
diff --git a/chrome/browser/ui/toasts/toast_view.h b/chrome/browser/ui/toasts/toast_view.h
index aa38934..1a1a1bf 100644
--- a/chrome/browser/ui/toasts/toast_view.h
+++ b/chrome/browser/ui/toasts/toast_view.h
@@ -7,9 +7,18 @@
#include <string>
+#include "base/functional/callback_forward.h"
#include "base/memory/raw_ptr.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
+#include "ui/views/widget/widget.h"
+
+namespace views {
+class ImageButton;
+class ImageView;
+class Label;
+class MdTextButton;
+} // namespace views
namespace toasts {
// The view for toasts.
@@ -20,13 +29,25 @@
DECLARE_CLASS_ELEMENT_IDENTIFIER_VALUE(kToastViewId);
ToastView(views::View* anchor_view,
const std::u16string& toast_text,
- const gfx::VectorIcon& icon);
+ const gfx::VectorIcon& icon,
+ bool has_close_button);
~ToastView() override;
+ // Must be called prior to Init (which is called from
+ // views::BubbleDialogDelegateView::CreateBubble).
+ void AddActionButton(
+ const std::u16string& action_button_text,
+ base::RepeatingClosure action_button_callback = base::DoNothing());
+
// views::BubbleDialogDelegateView:
void Init() override;
+ // Animates out the toast, then closes the toast widget.
+ void Close(views::Widget::ClosedReason reason);
+
views::Label* label_for_testing() { return label_; }
+ views::MdTextButton* action_button_for_testing() { return action_button_; }
+ views::ImageButton* close_button_for_testing() { return close_button_; }
protected:
// views::BubbleDialogDelegateView:
@@ -37,9 +58,16 @@
private:
const std::u16string toast_text_;
const raw_ref<const gfx::VectorIcon> icon_;
+ const bool has_close_button_;
+ bool has_action_button_ = false;
+ std::u16string action_button_text_;
+ base::RepeatingClosure action_button_callback_;
+ // Raw pointers to child views.
raw_ptr<views::Label> label_ = nullptr;
raw_ptr<views::ImageView> icon_view_ = nullptr;
+ raw_ptr<views::MdTextButton> action_button_ = nullptr;
+ raw_ptr<views::ImageButton> close_button_ = nullptr;
};
} // namespace toasts
diff --git a/chrome/browser/ui/toasts/toast_view_browsertest.cc b/chrome/browser/ui/toasts/toast_view_browsertest.cc
index 0bf9fc98..5404de6 100644
--- a/chrome/browser/ui/toasts/toast_view_browsertest.cc
+++ b/chrome/browser/ui/toasts/toast_view_browsertest.cc
@@ -22,17 +22,16 @@
ToastViewTest() = default;
void ShowUi(const std::string& name) override {
- const std::u16string& toast_text =
- l10n_util::GetStringUTF16(IDS_LINK_COPIED);
views::View* anchor_view =
BrowserView::GetBrowserViewForBrowser(browser())->top_container();
+ const std::u16string& toast_text =
+ l10n_util::GetStringUTF16(IDS_LINK_COPIED);
const gfx::VectorIcon& icon = vector_icons::kLinkIcon;
- std::unique_ptr<toasts::ToastView> toast;
- if (name == "basic") {
- toast =
- std::make_unique<toasts::ToastView>(anchor_view, toast_text, icon);
- } else {
- ADD_FAILURE();
+ std::unique_ptr<toasts::ToastView> toast =
+ std::make_unique<toasts::ToastView>(anchor_view, toast_text, icon,
+ name == "CloseButton");
+ if (name == "ActionButton") {
+ toast->AddActionButton(l10n_util::GetStringUTF16(IDS_APP_OK));
}
toast_ = toast.get();
widget_ = views::BubbleDialogDelegateView::CreateBubble(std::move(toast));
@@ -56,11 +55,39 @@
widget_ = nullptr;
}
+ toasts::ToastView* toast() { return toast_; }
+
private:
raw_ptr<toasts::ToastView> toast_;
raw_ptr<views::Widget> widget_;
};
-IN_PROC_BROWSER_TEST_F(ToastViewTest, InvokeUi_basic) {
+IN_PROC_BROWSER_TEST_F(ToastViewTest, InvokeUi_Basic) {
ShowAndVerifyUi();
}
+
+IN_PROC_BROWSER_TEST_F(ToastViewTest, InvokeUi_ActionButton) {
+ ShowUi("ActionButton");
+ ASSERT_TRUE(VerifyUi());
+ ChromeLayoutProvider* lp = ChromeLayoutProvider::Get();
+ EXPECT_TRUE(toast()->action_button_for_testing());
+ EXPECT_EQ(lp->GetDistanceMetric(
+ DISTANCE_TOAST_BUBBLE_BETWEEN_LABEL_ACTION_BUTTON_SPACING),
+ toast()->action_button_for_testing()->x() -
+ toast()->label_for_testing()->bounds().right());
+ EXPECT_EQ(
+ lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_ACTION_BUTTON),
+ toast()->GetDialogClientView()->width() - toast()->bounds().right());
+ DismissUi();
+}
+
+IN_PROC_BROWSER_TEST_F(ToastViewTest, InvokeUi_CloseButton) {
+ ShowUi("CloseButton");
+ ASSERT_TRUE(VerifyUi());
+ ChromeLayoutProvider* lp = ChromeLayoutProvider::Get();
+ EXPECT_TRUE(toast()->close_button_for_testing());
+ EXPECT_EQ(
+ lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_CLOSE_BUTTON),
+ toast()->GetDialogClientView()->width() - toast()->bounds().right());
+ DismissUi();
+}
diff --git a/chrome/browser/ui/views/chrome_layout_provider.cc b/chrome/browser/ui/views/chrome_layout_provider.cc
index f35dc82d..f975f4a 100644
--- a/chrome/browser/ui/views/chrome_layout_provider.cc
+++ b/chrome/browser/ui/views/chrome_layout_provider.cc
@@ -164,12 +164,20 @@
return 8;
case DISTANCE_TOAST_BUBBLE_BETWEEN_CHILD_SPACING:
return 4;
+ case DISTANCE_TOAST_BUBBLE_BETWEEN_LABEL_ACTION_BUTTON_SPACING:
+ return 16;
case DISTANCE_TOAST_BUBBLE_HEIGHT:
return 48;
+ case DISTANCE_TOAST_BUBBLE_HEIGHT_ACTION_BUTTON:
+ return 36;
case DISTANCE_TOAST_BUBBLE_HEIGHT_CONTENT:
return 24;
case DISTANCE_TOAST_BUBBLE_MARGIN_LEFT:
return 12;
+ case DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_ACTION_BUTTON:
+ return 6;
+ case DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_CLOSE_BUTTON:
+ return 12;
case DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_LABEL:
return 16;
}
diff --git a/chrome/browser/ui/views/chrome_layout_provider.h b/chrome/browser/ui/views/chrome_layout_provider.h
index 133b481..45fe8da 100644
--- a/chrome/browser/ui/views/chrome_layout_provider.h
+++ b/chrome/browser/ui/views/chrome_layout_provider.h
@@ -102,12 +102,22 @@
DISTANCE_RICH_HOVER_BUTTON_ICON_HORIZONTAL,
// Distance between most child elements inside the toast.
DISTANCE_TOAST_BUBBLE_BETWEEN_CHILD_SPACING,
+ // Distance between the toast label and action button.
+ DISTANCE_TOAST_BUBBLE_BETWEEN_LABEL_ACTION_BUTTON_SPACING,
// Height of the toast.
DISTANCE_TOAST_BUBBLE_HEIGHT,
+ // Height of toast action buttons.
+ DISTANCE_TOAST_BUBBLE_HEIGHT_ACTION_BUTTON,
// Height of the toast text and icon.
DISTANCE_TOAST_BUBBLE_HEIGHT_CONTENT,
// Distance between left border of the toast and the icon.
DISTANCE_TOAST_BUBBLE_MARGIN_LEFT,
+ // Distance between the right border of the toast and the action button, if
+ // the action button is the rightmost element.
+ DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_ACTION_BUTTON,
+ // Distance between the right border of the toast and the close button, if the
+ // close button is the rightmost element.
+ DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_CLOSE_BUTTON,
// Distance between the right border of the toast and the label, if the label
// is the rightmost element.
DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_LABEL,